《深入React技术栈》

react 全系列

React 简介

React 是 Facebook 在 2013 年开源 JavaScript 库。它把界面抽象成一个个组件,通过组合这些组件,开发者可以得到功能丰富的页面。同时引入了 JSX 语法,使得复用组件变得容易,且结构清晰。并且有了组件这层的抽象,代码和真实渲染目标分离,除了可以在浏览器端渲染到 DOM 开发网页外,还能用于原生移动应用的开发。

专注视图层

React 并不是完整的 MVC/MVVM 框架,它专注于 View(视图)层解决方案。与模板引擎不同,React 又是一个包括 View 和 Controller 的库。

Virtual DOM

React 把真实 DOM 树转换成 JavaScript 对象树,也就是 Virtual DOM。每次数据更新,对比前后的 Virtual DOM,对发生变化的部分做批量更新,提升性能。并且 Virtual DOM 可以方便地与其他平台集成,比如 react-native 就是基于 Virtual DOM 渲染原生控件的。

函数式编程

命令式编程是给计算机下命名,而函数式编程,对应的是声明式编程。声明式编程的本质是 lambda 演算,比如我们要操作一个数组里的每个元素,返回一个新数组。我们的做法是构建一个 f 函数(规则)作用在数组上,然后返回新数组。这样,计算可以被重复利用。

JSX 语法

JSX 是 react 为了方便 View 层组件化,承载构建 HTML 结构化页面职责的而创立的语言(语法)。

DOM 元素和组件元素

在 react 中创建的虚拟元素可以分为两类,DOM 元素(DOM element)与组件元素(component element)。分别对应着原生 DOM 元素和自定义元素。

DOM 元素

当使用 JavaScript 来描述 Web 页面的 HTML 元素时,可以表示为纯粹的 JSON 对象。例如,描述一个按钮

1
2
3
<button class="btn btn-blue">
<em>Confirm</em>
</button>

->

1
2
3
4
5
6
7
8
9
10
11
12
{
type: 'button',
props: {
className: 'btn btn-blue',
children: [{
type: 'em',
props: {
children: 'Confirm'
}
}]
}
}

在 react 中,到处可见的元素并不是真实的实例,它们只是页面的描述对象。

组件元素

React 还可以自定义组件元素。
类比如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Button = ({ color, text }) => {
return {
type: "button",
props: {
className: `btn btn-${color}`,
children: [
{
type: "em",
props: {
children: text
}
}
]
}
};
};

Button 其实也可以作为元素而存在,方法名对应了元素类型,参数对应了元素属性。
这也是 React 的核心思想之一,我们可以让 DOM 元素、组件元素嵌套、组合,最后用递归渲染的方式构建出完全的 DOM 元素树。
但是这种写法不容易阅读和维护了,JSX 语法就应运而生了。

1
2
3
4
5
const Button = ({ color, text }) => {
<button className={`btn btn-${color}`}>
<em>{text}</em>
</button>;
};

JSX 将 HTML 语法直接加入到 JavaScript 代码中,再通过翻译器转换成纯 JavaScript 后再由浏览器执行。

JSX 基本语法

JSX 的官方定义是类 XML 语法的 ECMAScript 扩展。

XML 基本语法

使用类 XML 语法,我们可以清晰地看到 DOM 树状结果及其属性。只不过它被包裹在 JavaScript 的方法中

1
2
3
4
5
6
const List = () => (
<ul>
<li>1</li>
<li>2</li>
</ul>
);

需要注意几点

  1. 定义标签时,只允许被一个标签包裹。
1
const c = () => (<span>1</span><span>2</span>);

会报错,最外层没有包裹,显然无法转译成 React.createElement 方法调用

  1. 标签必须闭合,如<div></div><div />

元素类型

小写字母对应 DOM 元素,大写字母对应组件元素
此外,还有一些特殊的标签值得讨论,比如注释和 DOCTYPE 头
JSX 还是 JavsScript,依然可以用简单的方法使用注释,在子元素位置使用注释要用{}包起来。
对于常用于判断浏览器版本的条件注释

1
2
3
<!--[if IE]>
<p>work in IE</p>
<![endif]-->

需要用 JavaScript 判断来实现

1
2
3
4
{
(!!window.ActiveXObject || 'ActiveXObject' in window) ?
<p>work in IE</p> : ''
}

DOCTYPE 头是一个非常特殊的标志,一般会在 React 服务端渲染时用到。DOCTYPE 是没有闭合的,我们无法渲染它。常见的做法是构造一个保存 HTML 的变量,将 DOCTYPE 和整个 HTML 标签渲染后的结果串联起来。

元素属性

DOM 元素属性是标准规范属性,但 class 和 for 由于是关键字,由 className 和 htmlFor 替代。
组件元素属性是完全自定义的属性,也可以理解为实现组件所需要的参数。一般采用小驼峰写法。
此外还有一些特有的属性表达

  1. 省略 Boolean 属性值会导致 JSX 认为 bool 值设为了 true
1
<Checkbox checked={true} />

可以简写成

1
<Checkbox checked />
1
<Checkbox checked={false} />

可以省略为

1
<Checkbox />
  1. 展开属性
    使用 ES6 rest/spread 特性可以提高开发效率
1
2
const data = { name: "foo", value: "bar" };
const component = <Component {...data} />;
  1. 自定义 HTML 属性
    往 DOM 元素传入自定义属性,React 是不会渲染的
1
<div d="xxx">content</div>

需要使用 data-前缀,这和 HTML 标准也是一致的

1
<div data-attr="xxx">content</div>

然而在自定义标签中,任意属性都是被支持的

1
<CustomComponent d="xxx" />
  1. JavaScript 属性表达式
1
<Person name={true ? 1 : 2} />
  1. HTML 转义
    React 会将所有要显示到 DOM 的字符串转义,防止 XSS。如&copy;不会正确显示。
    可以通过以下方法解决 1.直接使用 UTF-8 字符 2.使用 Unicode 编码 3.使用功数组组装<div>{['cc ', <span>&copy;</span>, ' 2015']}</div> 4.直接插入原始的 HTML

React 还提供了 dangerouslySetInnerHTML 属性。它避免了 React 转义字符,请在确定必要的情况下使用它

1
<div dangerouslySetInnerHTML={{ __html: "cc &copy; 2015" }} />

React 组件

组件的演变

在 MVC架构出现之前,组件主要分为两种

  1. 狭义上的组件,又称 UI 组件,主要围绕交互动作上的抽象,利用 JavaScript 操作 DOM 结构或 style 样式来控制
  2. 广义上的组件,带有业务含义和数据的 UI 组件组合。它更倾向于采用分层的思想去处理
    对于 UI 组件,分为 3 部分:结构、样式和交互行为,对应于 HTML、CSS 和 JavaScript.

封装的基本思路就是面向对象思想。交互基本上以操作 DOM 为主。逻辑上是结构上哪里需要变,我们就操作哪里。以下是几项规范标准组件的信息。

  1. 基本的封装性。尽管说 JavaScript 没有真正面向对象的方法,但是我们还是可以通过实例化的方法来制造对象。
  2. 简单的生命周期呈现。如 contructor 和 destroy,代表了组件的挂载和卸载过程。
  3. 明确的数据流动。这里的数据指的是调用组件的参数。一旦确定参数的值,就会解析传进来的参数,根据参数的不同作出不同的响应。

这个阶段,前端在应用级别没有过多复杂的交互。传统组件的主要问题在于结构、样式与行为没有很好地结合,不同参数下的逻辑可能会导致不同的渲染逻辑,这时就会存在大量的 HTML 结构与 style 样式的拼装。逻辑一旦复杂,开发及维护成本相当高。

于是分层思想引进了,出现了 MVC 架构。View 只关心怎么输出变量,所以就诞生了各种各样的模板语言。让模板本身承载逻辑,可以帮我们解决 View 上的逻辑问题。对于组件来说,可以将拼装 HTML 的逻辑部分解耦出去,解决了数据与界面耦合的问题。

模板作为一个 DSL,也有其局限性。在 Angular 中,我们看到了在 HTML 上定义指令的方式。

W3C 将类似的思想制定成了规范,称为 Web Components。它通过定义 Custom Elements(自定义元素)的方式来统一组件。每个自定义元素可以定义自己对外提供的属性、方法,还有事件,内部可以像写一个页面一样,专注于实现功能来完成对组件的封装。

Web Components 由 4 个组成部分:HTML Templates 定义了之前模板的概念,Custom Elements 定义了组件的展现形式,Shadow DOM 定义了组件的作用域范围、可以囊括样式,HTML Imports 提出了新的引入方式。

事实上,它还是需要时间的考验的。因为诸如如何包装在这套规范之上的框架,如何获得在浏览器端的全部支持,怎么与现代应用架构结合等等。但它却是开辟了一条罗马大道,告诉我们组件化可以这样去做。

React 组件的构建

React 的本质就是关心元素的构成,React 组件即为组件元素。组件元素被描述成纯粹的 JSON 对象,意味着可以使用方法或是类来构建。React 组件基本上由 3 个部分组成-属性(props)、状态(state)以及生命周期方法。

React 组件可以接收参数,也可能有自身状态。一旦接收到的参数或自身状态有所改变,React 组件就会执行相应的生命周期方法,最后渲染。

1.React 与 Web Components
从 React 组件上看,它与 Web Components 传达的理念是一致的,但两者的实现方式不同:

  1. React 自定义元素是库自己构建的,与 Web Components 规范并不通用;
  2. React 渲染过程包括了模板的概念,即 JSX
  3. React 组件的实现均在方法与类中,因此可以做到相互隔离,但不包括样式。
  4. React 引用方式遵循 ES6 module 标准

React 在纯 JavaScript 上下了工夫,将 HTML 结构彻底引入到 JavaScript 中。这种做法褒贬不一,但有效地解决了组件所要解决的问题之一。

2.React 组件的构建方法
React 组件基本上由组件的构建方式、组件内的属性状态与生命周期方法组成。

React 组件构建上提供了 3 种不同的方法:React.createClass、ES6 classes 和无状态函数。

React.createClass
用 React.createClass 构建组件是 React 最传统、也是兼容性最好的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Button = React.createClass({
getDefaultProps() {
return {
color: "blue",
text: "Confirm"
};
},

render() {
const { color, text } = this.props;

return (
<button className={`btn btn-${color}`}>
<em>{text}</em>
</button>
);
}
});

ES6 classes
ES6 classes 的写法是通过 ES6 标准的类语法的方式来构建方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Component } from "react";

class Button extends Component {
contructor(props) {
super(props);
}

static defaultProps = {
color: "blue",
text: "Confirm"
};

render() {
const { color, text } = this.props;

return (
<button className={`btn btn-${color}`}>
<em>{text}</em>
</button>
);
}
}

与 createClass 的结果相同的是,调用类实现的组件会创建实例对象。

我们很容易联想到组件抽象过程中也可以使用继承的思路。在实际应用中,我们极少让子类去继承功能组件。继承牵一发而动全身。在 React 组件开发中,常用的方式是将组件拆分到合理的粒度,用组合的方式合成业务组件。

说明:React 的所有组件都继承自顶层类 React.Component。它的定义非常简洁,只是初始化了 React.Component 方法,声明了 props、context、refs 等,并在原型上定义了 setState 和 foreUpdate 方法。内部初始化的生命周期方法与 createClass 方式使用的是同一个方法创建的。

无状态函数
使用无状态函数构建的组件称为无状态组件

1
2
3
4
5
6
7
function Button({ color = "blue", text = "Confirm" }) {
return (
<button className={`btn btn-${color}`}>
<em>{text}</em>
</button>
);
}

无状态组件只传入 props 和 context 两个参数;也就是说,它不存在 state,也没有生命周期方法,组件本身即上面两种 React 组件构建方法中的 render。不过,像 propTypes 和 defaultProps 还是可以通过向方法设置静态属性来实现的。

无状态组件不像上述两种方法在调用时会创建新实例,它创建时始终保持了一个实例,避免了不必要的检查和内存分配。

React 数据流

在 React 中,数据是自顶向下单向流动的,即从父组件到子组件。

state 与 props 是组件中最重要的概念。如果顶层组件初始化 props,那么 React 会向下遍历整棵组件树,重新尝试渲染所有相关的子组件。state 只关心组件自己内部的状态,这些状态只能在组件内改变。把组件看成一个函数,props 就是它的参数,内部由 state 作为函数的内部参数,返回一个 Virtual DOM 的实现。

state

在 React 中,state 为组件内部状态。当组件内部使用 setState 方法时,该组件会尝试重新渲染。

值得注意的,setState 是一个异步方法,一个生命周期内所有的 setState 方法会合并操作。

我们思考一个常规的 Tabs 组件,对于 activeIndex 作为 state,就有两种不同的视角。

  1. 在内部更新。当我们切换 tab 标签时,可以看作是组件内部的交互行为,被选择后通过回调函数返回具体选择的索引。
  2. 在外部更新。当我们切换 tab 标签时,可以看作是组件外部在传入具体的索引,而组件就像“木偶”一样被操控着。
    这两种情形在 React 组件的设计中非常常见,我们分别称为智能组件和木偶组件

当然,实现组件时,可以同时考虑兼容这两种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
constructor(props) {
super(props);

const currProps = this.props;

let activeIndex = 0;
// 来源于需要外部更新的
if (activeIndex in currProps) {
activeIndex = currProps.activeIndex;
// 来源于使用内部更新的
} else if ('defaultActiveIndex' in currProps) {
activeIndex = currProps.defaultActiveIndex;
}

this.state = {
activeIndex,
prevIndex: activeIndex,
};
}

props

props 是 React 用来让组件之间互相联系的一种机制,通俗地说就像方法的参数一样。

props 的传递过程,对于 React 组件来说非常直观。React 的单向数据流,主要的流动管道就是 props。props 本身是不可变的。组件的 props 一定来自于默认属性或通过父组件传递而来。

React 为 props 提供了默认配置,通过 defaultProps 静态变量的方式来定义。

1
2
3
4
static defaultProps = {
classPrefix: 'tabs',
onChange: () => {},
};

子组件 prop

在 React 中有一个重要且内置的 props——children,它代表组件的子组件集合。

实现的基本思路以 TabContent 组件渲染 TabPane 子组件集合为例来讲

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
getTabPanes() {
const { classPrefix, activeIndex, panels, isActive } = this.props;

return React.Children.map(panels, (child) => {
if (!child) { return; }

const order = parseInt(child.props.order, 10);

return React.cloneElement(child, {
classPrefix,
isActive,
children: child.props.children,
key: `tabpane-${order}`,
});
});
}

它是通过 React.Children.map 方法遍历子组件,同时利用 React 的 cloneElement 方法克隆到 TabPane 组件,最后返回这个 TabPane 组件集合。

React.Children 是 React 官方提供的一系列操作 children 的方法。它提供诸如 map、forEach、count 等实用函数。
使用 getTabPanes

1
2
3
render () {
return (<div>{this.getTabPanes()}</div>);
}

假如我们把 render 方法中的 this.getTabPanes 方法中对子组件的遍历直接放进去

1
2
3
render() {
return (<div>{React.Children.map(this.props.children, (child) => {...})}</div>)
}

这种调用方式称为 Dynamic Children(动态子组件)。

组件props

也可以将子组件以props的形式传递。一般我们会用这种方法让开发者定义组件的某一个prop,让其具备多种类型,来做到简单配置和自定义配置组合在一起的效果。

用function prop与父组件通信

1
this.props.onChange(activeIndex, prevIndex)

触发了onChange prop回调函数给父组件必要的值。

propTypes

propTypes用于规范props的类型与必需的状态。它会在开发环境下,对组件的prop值的类型作检查。

1
2
3
static propTypes = {
classPrefix: React.PropTypes.string,
}

propTypes有很多类型支持,不仅有基本类型,还包括枚举和自定义类型。

React生命周期

挂载和卸载过程

1.组件的挂载
这个过程主要做组件状态的初始化,我们推荐以下面例子为模板写初始化组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React, { Component, PropTypes } from 'react';

class App extends Component {
static propTypes = {
// ...
}

static defaultProps = {
// ...
}

constructor(props) {
super(props)

this.state = {
// ...
}
}

componentWillMount() {
// ...
}

componentDidMount() {
// ...
}

render () {
return <div>This is a demo</div>
}
}

如果我们在componentWillMount中执行setState方法,组件会更新state,但组件只渲染一次。因此,这是无意义的执行,完全可以放在constructor初始化state中。
如果我们在componentDidMount中执行setState方法,组件会再次更新,不过在初始化过程就渲染了两次组件,这并不是一次好事。但实际情况,有一些场景必须这么做,比如需要获取组件的位置。

  1. 组件的卸载
    componentWillUnmount,我们常常会执行一些清理方法,比如事件回收、清除定时器。

数据更新过程

更新过程指的是父组件向下传递props或组件自身执行setState方法时发生的一系列更新动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React, { Component, PropTypes } from 'react'

class App extends Component {
componentWillReceiveProps(nextProps) {
// this.setState({})
}

shouldComponentUpdate(nextProps, nextState) {
// return true
}

componentWillUpdate(nextProps, nextState) {

}

componentDidUpdate(prevProps, prevState) {

}

render() {

}
}

如果组件自身的state更新了,会依次执行shouldComponentUpdate、componentWillUpdate、render和componentDidUpdate

shouldComponentUpdate接收需要更新的props和state,让开发者增加判断逻辑,不需要更新方法最终返回false即可,这是性能优化的手段之一。

无状态组件是没有生命周期方法的,这也意味着它没有shouldComponentUpdate。渲染该类组件,每次都会重新渲染。

componentWillUpdate方法提供的是需要更新的props和state,而componentDidUpdate提供更新前的props和state。

注意不能在componentWillUpdate执行setState方法,会导致循环执行render。

如果组件是由父组件更新props而更新的,那么在shouldComponentUpdate之前会先执行componentWillRecieveProps方法。此方法可以作为React在props传入后,渲染之前setState的机会,在此方法中调用setState是不会二次渲染的。

1
2
3
4
5
6
7
componentWillReceiveProps(nextProps) {
if ('activeIndex' in nextProps) {
this.setState({
activeIndex: nextProps.activeIndex
})
}
}

React与DOM

ReactDOM

ReactDOM中的API非常少,只有findDOMNode、unmountComponentAtNode和render。
1.findDOMNode
Reactz提供的获取DOM元素的方法有两种,其中一种就是ReactDOM提供的findDOMNode:

1
DOMElement findDOMNode(ReactComponent component)

当组件被渲染到DOM后,findDOMNode返回该React组件实例相应的DOM节点。它可以用于获取表单的value以及用于DOM的测量。

1
2
3
4
5
6
7
class App extends Component {
componentDidMount() {
const dom = ReactDOM.findDOMNode(this)
}

render() {}
}

render

1
2
3
4
5
ReactComponent render(
ReactElement element,
DOMElement container,
[function callback]
)

该方法把元素挂载到container中,并且返回element的实例(即refs引用)。如果是无状态组件,render会返回null。当组件装载完毕时,callback被调用。

与render相反,React还提供了一个很少使用的unmountComponentAtNode方法来进行卸载操作。

ReactDOM的不稳定方法

unstable_renderSubtreeIntoContainer。它可以更新组件到传入的DOM节点。它与render方法相比,区别在于是否传入父节点。

另一个ReactDOM中的不稳定方法unstable_batchedUpdates是关于setState更新策略的。

refs

它是React组件中非常特殊的prop,可以附加到任何一个组件上。组件被调用时会新建一个该组件的实例,而refs就会指向这个实例。

findDOMNode和refs都无法用于无状态组件中,无状态组件挂载只是方法调用,没有新建实例。

React之外的DOM操作

调用HTML5 Audio/Video的play方法和input的focus方法,React就无能为力了,需要使用相应的DOM方法来实现。

还有组件以外区域(一般指document、body)的事件绑定、DOM的尺寸计算。

事件系统

React基于Virtual DOM实现了一个SyntheticEvent(合成事件)层,我们定义的处理器会接收一个SyntheticEvent对象的实例,它完全符合W3C标准,不会存在任何IE的兼容性问题。并且与原生的浏览器事件一样拥有同样的接口,同样支持事件的冒泡机制,我门可以使用stopPropagation()和preventDefault()来中断它。如果需要访问原生事件对象,可以使用nativeEvent属性。

合成事件的绑定方式

React事件的绑定方式与原生的HTML事件监听器属性很相似。

1
<button onClick={this.handleClick}>Test</button>

合成事件的实现机制

在React底层,主要对合成事件做了两件事:事件委派和自动绑定。

1.事件委派

React不会把事件处理函数直接绑定到真实的节点上,而是把所有事件绑定到结构的最外层,使用一个统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。(实现原理:对最外层的容器进行绑定,依赖事件的冒泡机制完成委派。)这样简化了事件处理和回收机制,效率也有很大提升。

2.自定绑定

在React组件中,每个方法的上下文都会指向该组件的实例,即自定绑定this为当前组件。而且React还会对这种引用进行缓存。在使用ES6 classes或者纯函数时,这种自定绑定就不复存在了,我们需要手动实现this的绑定。
我们来看几种绑定方法
bind方法

1
2
3
4
5
6
class App extends Component {
constuctor() {
super(props)
this.handleClick = this.handleClick.bind(this)
}
}

箭头函数可以自动绑定此函数的作用域的this

1
2
3
class App extends Component {
handleClick= () => {}
}

在React中使用原生事件

1
2
3
4
5
6
7
8
class NativeEventDemo extends Component {
componentDidMount() {
this.refs.button.addEventListener('click', this.handleClick)
}
componentWillUnmout() {
this.refs.button.removeEventListener('click', this.handleClick)
}
}

对比React合成事件与JavaScript原生事件

1.事件传播与阻止事件传播

浏览器原生DOM事件的传播可以分为3个阶段:事件捕获阶段、目标对象本身的事件处理程序调用、事件冒泡。可以将e.addEventListener的第三个参数设置为true时,为元素e注册捕获事件处理程序。事件捕获在IE9以下无法使用。事件捕获在应用程序开发中意义不大,React在合成事件中并没有实现事件捕获,仅仅支持了事件冒泡机制。

阻止原生事件传播需要使用e.stopPropagation,不过对于不支持该方法的浏览器(IE9以下)只能使用e.cancelBubble = true来阻止。而在React合成事件中,只需要使用stopPropagation()即可。阻止React事件冒泡的行为只能用于React合成事件系统中,且没有办法阻止原生事件的冒泡。反之,原生事件阻止冒泡,可以阻止React合成事件的传播。

2.事件类型

React合成事件的事件类型是JavaScript原生事件类型的一个子集。它仅仅实现了DOM Level3的事件接口,并且统一了浏览器的兼容问题。有些事件React没有实现,或者受某些限制没办法去实现,如window的resize事件。

3.事件绑定方式

受到DOM标准影响,浏览器绑定原生事件的方式有很多种。React合成事件的绑定方式则简单很多

1
<button onClick={this.handleClick}>Test</button>

4.事件对象

在React合成事件系统中,不存在兼容性问题,可以得到一个合成事件对象。

表单

在React中,一切数据都是状态,当然也包括表单数据。接下来我们讲讲React是如何处理表单的。

应用表单组件

html表单中的所有组件在React的JSX都有实现,只是它们在用法上有些区别,有些是JSX语法上的,有些则是由于React对状态处理上导致的一些区别。

1.文本框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import React, { Component } from 'react';

class App extends Component {
constructor(props) {
super(props);

this.state = {
inputValue: '',
textareaValue: ''
}
}

handleInputChange = (e) => {
this.setState({
inputValue: e.target.value
});
}

handleTextareaChange = (e) => {
this.setState({
textareaValue: e.target.value
})
}

render() {
const { inputValue, textareaValue } = this.state;
return (
<div>
<p>
单行输入框:
<input type="text" value={inputValue} onChange={this.handleInputChange}/>
</p>
<p>
多行输入框:
<textarea type="text" value={textareaValue} onChange={this.handleTextareaChange}/>
</p>
</div>
)
}

}

在HTML中textarea的值是通过children来表示的,而在react中是用一个value prop来表示表单的值的。

2.单选按钮与复选框

在HTML中,用类型为radio的input标签表示单选按钮,用类型为checkbox的input标签表示复选框。这两种表单的value值一般是不会改变的,而是通过一个布尔类型的checked prop来表示是否为选中状态。在JSX中这些是相同的,不过用法上还是有些区别。

单选按钮的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import React, { Component } from 'react';

class App extends Component {
construtor(props) {
super(props);
this.state = {
radioValue: '',
}
}

handleChange = (e) => {
this.setState(
radioValue: e.target.value
)
}

render() {
const { radioValue } = this.state;

return (
<div>
<p>gender:</p>
<label>
male:
<input
type="radio"
value="male"
checked={radioValue === 'male'}
onChange={this.handleChange}
/>
</label>
<label>
female:
<input
type="radio"
value="female"
checked={radioValue === 'female'}
onChange={this.handleChange}
/>
</label>
</div>
)
}
}

复选按钮的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import React, { Component } from 'react';

class App extends Component {
constructor(props) {
super(props)

this.state = {
coffee: []
}
}

handleChange = (e) => {
const { checked, value } = e.target;
let { coffee } = this.state;

if (checked && coffee.indexOf(value) === -1) {
coffee.push(value)
} else {
coffee = coffee.filter(i => i !== value)
}

this.setState({
coffee,
})
}

render() {
const { coffee } = this.state;
return (
<div>
<p>请选择你最喜欢的咖啡</p>
<label>
<input
type="checkbox"
value="Cappuccino"
checked={coffee.indexOf('Cappuccino') !== -1}
onChange={this.handleChange}
/>
Cappuccino
</label>
<br />
<label>
<input
type="checkbox"
value="CafeMocha"
checked={coffee.indexOf('CafeMocha') !== -1}
onChange={this.handleChange}
/>
CafeMocha
</label>
</div>
)
}
}

3.Select组件

在HTML的select元素中,存在单选和多选两种。在JSX语法中,同样可以通过设置select标签的multiple={true}来实现一个多选下拉列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class App extends Component {
constructor(props) {
super(props)

this.state = {
area: ''
}
}

handleChange = (e) => {
this.setState({
area: e.target.value
})
}

render() {
const { area } = this.state;

return (
<select value={area} onChange={this.handleChange}>
<option value='beijing'>北京</option>
<option value='shangehai'>上海</option>
</select>
)
}
}

select元素设置multiple={true}的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class App extends Component {
constructor(props) {
super(props)

this.state = {
area: ['beijing', 'shanghai']
}
}

handleChange = (e) => {
const { options } = e.target;
const area = Object.keys(options)
.filter(i => options[i].selected === true)
.map(i => options[i].value);

this.setState({
area,
})
}

render () {
const { area } = this.state;

return (
<select multiple={true} value={area} onChange={this.handleChange}>
<option value="北京">北京</option>
<option value="上海">上海</option>
</select>
)
}
}

在HTMl的option组件需要一个selected属性来表示默认选中的列表项,而React的处理方式是通过为select组件添加value prop来表示选中的option,在一定程度上统一了接口。

实际上,也可以写成这种形式,不过开发体验就会差很多,React也会抛警告。

1
2
3
4
<select multiple={true} onChange={this.handleChange}>
<option value="beijing" selected={area.indexOf('beijing') !== -1}>北京</option>
<option value="shanghai" selected={area.indexOf('shanghai') !== -1}>上海</option>
</select>

受控组件

每当表单的状态发生变化,都会被写入到组件的state中,这种组件在React中被称为受控组件。在受控组件中,组件渲染出的状态与它的value或checked prop相对应。React通过这种方式消除了组件的局部状态,使得应用的整个状态更加可控。

非受控组件

如果一个表单组件没有value prop(或checked prop),就可以称之为非受控组件。相应的你可以使用defaultValue和defaultChecked prop来表示组件的默认状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class App extends Compoent {
constructor(props) {
super(props)

}

handleSubmit = (e) => {
e.preventDefault();

const { value } = this.refs.name;
console.log(value)
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<input ref="name" type="text" defaultValue="Hangzhou" />
<button type="submit">submit</button>
</form>
)
}
}

在React中,非受控组件是一种反模式,它的值不受组件自身的state或props控制。通常,需要为其添加ref prop来访问渲染后的底层DOM元素。

对比受控组件和非受控组件

受控组件和非受控组件的最大区别是:非受控组件的状态并不会受应用状态的控制,应用中也多了局部组件状态,而受控组件的值来自于组件的state。

1.性能上的问题

受控组件onChange后,调用setState会重新渲染,确实会有一些性能损耗。

2.是否需要事件绑定

受控组件需要为每个组件绑定一个change事件,并且定义一个事件处理器来同步表单值和组件的状态。

尽管如此,在React仍然提倡使用受控组件,因为它可以使得应用的整个状态更加可控。

表单组件的几个重要属性

1.状态属性

React的form组件提供了几个重要的属性,用于展示组件的状态。
value: 类型为text的input组件、textarea组件以及select组件都借助value prop来展示应用的状态。
checked: 类型为radio或checkbox的组件借助值为boolean类型的checked prop来展示应用的状态。
selected: 该属性可作用于select组件下面的option上,React并不建议使用功这种方式,推荐使用value.

2.事件属性

在状态属性发生变化时,会触发onChange事件属性。实际上,受控组件中的change事件与HTML DOM中提供的input事件更为类似。React支持DOM Level3中定义的所有表单事件。

样式处理

基本样式设置

React可以通过设置className prop给html设置class,也可以通过style prop给组件设置行内样式。

使用classnames库

我们可以通过classnames库来设置html的类名

CSS Modules

CSS模块化的解决方案有很多,但主要有两类。

  1. Inline Style。这种方案彻底抛弃CSS,使用JavaScript或JSON来写样式,能给CSS提供JavaScript同样强大的模块化能力。但缺点同样明显,它几乎不能利用CSS本身的特性,比如级联、媒体查询等,:hover和:active等伪类处理起来比较复杂。另外,这种方案需要依赖框架实现,其中与React相关的有Radium、jsxstyle和react-style
  2. CSS Modules。依旧使用CSS,但使用JavaScript来管理样式依赖。CSS Modules能最大化地结合现有CSS生态和JavaScript模块化能力,其API非常简洁。发布时依旧编译出单独的JavaScript和CSS文件。现在,webpack css-loader内置CSS Modules功能。

1.CSS模块化遇到了哪些问题

CSS模块化重要的是解决好以下两个问题:CSS样式的导入与导出。灵活按需导入以便复用代码,导出时要隐藏内部作用域,以免造成全局污染。Sass、Less、PostCSS等试图解决CSS编程能力弱的问题,但并没有解决模块化这个问题。React实际开发需要的CSS相关问题有:

  1. 全局污染:CSS使用全局选择器机制来设置样式,优点是方便重写样式。缺点是所有的样式全局生效,样式可能被错误覆盖。因此产生了非常丑陋的!important,甚至inline !important和复杂的选择器权重计数表,提高犯错概率和使用成本。Web Component标准中的Shadow DOM能彻底解决这个问题,但它把样式彻底局部化,造成外部无法重写样式,损失了灵活性。
  2. 命名混乱:由于全局污染的问题,多人协同开发时为了避免样式冲突,选择器越来越复杂,容易形成不同的命名风格,样式变多后,命名将更加混乱。
  3. 依赖管理不彻底:组件应该相互独立,引入一个组件时,应该只引入它所需要的CSS样式。现在的做法是除了引入JavaScript,还要再引入它的CSS,而且Sass/Less很难实现对每个组件都编译出单独的CSS,引入所有模块的CSS又造成浪费。JavaScript的模块化已经非常成熟,如果能让JavaScript来管理CSS依赖是很好的解决办法,而webpack的css-loader提供了这种能力。
  4. 无法共享变量:复杂组件要使用JavaScript和CSS来共同处理样式,就会造成有些变量在JavaScript和CSS中冗余,而预编译语言不能提供跨JavaScript和CSS共享变量的这种能力。
  5. 代码压缩不彻底:对与非常长的类名压缩无能为力。

2.CSS Modules模块化方案

CSS Modules内部通过ICSS来解决样式导入和导出这两个问题,分别对应:import和:export两个新增的伪类。

1
2
3
4
5
6
7
:import("path/to/dep.css") {
localAlias: keyFromDep;
}

:export {
exportedKey: exportedValue;
}

但直接使用这两个关键字编程太烦琐,项目中很少会直接使用它们,我们需要的是用JavaScript来管理CSS的能力。结合webpack的css-loader,就可以在CSS中定义样式,在JavaScript文件中导出。

启用CSS Modules

1
css?modules&localIdentName=[name]__[local]-[hash:base64:5]

加上modules即为启用,其中localIdentName是设置生成样式命名规则

下面我们看看js是怎么引入CSS的:

1
2
/* button相关的所有样式 */
.normal {}

1
2
3
import styles from './Button.css'

buttonElm.outerHTML = `<button class=${styles.normal}>Submit</button>`

最终生成的HTML是这样的

1
<button class="button--normal-abc5436">Processing...</button>

这样class的名称基本就是唯一的。
CSS Modules对CSS中的class名都做了处理,使用对象来保存原class和混淆后class的对应关系。通过这些简单的处理,CSS Modules实现了以下几点:

  1. 所有样式都是局部化的,解决了命名冲突和全局污染问题
  2. class名生成规则配置灵活,可以以此来压缩class名
  3. 只需要引用组件的JavaScript,就能搞定组件所有的JavaScript和CSS
  4. 依然是CSS,学习成本几乎为零

    样式默认局部

    使用CSS Modules相当于给每个class名外加了:local,以此来实现样式的局部化。如果我们想切换到全局模式,可以使用:global包裹
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    .normal {
    color: green;
    }
    /* 与上面等价 */
    :local(.normal) {
    color: green;
    }
    /* 定义全局样式 */
    :global(.btn) {
    color: red;
    }
    /* 定义多个全局样式 */
    :global {
    .link {
    color: green;
    }
    .box {
    color: yellow;
    }
    }

使用composes来组合样式

对于样式复用,CSS Modules只提供了唯一的方式来处理——composes组合。

1
2
3
4
5
6
7
/* components/Button.css */
.base { /* 所有通用的样式 */ }

.normal {
composes: base;
/* normal其他样式 */
}

此外,使用composes还可以组合外部文件中的样式

1
2
3
4
5
6
7
8
9
10
11
12
/* settings.css */
.primary-color {
color: #f40;
}

/* component/Button.css */
.base { /* 所有通用样式 */ }

.primary {
composes: base;
composes: $primary-color from './settings.css'
}

对于大多数项目,有了composes后,已经不再需要预编译处理器了。但是如果想用的话,由于composes不是标准的CSS语法,编译会报错,此时只能使用预处理自己的语法做样式复用了。

class命名技巧

CSS Modules的命名规范是从BEM扩展而来的。BEM把样式名分为3个级别

  1. Block: 对应模块名,如Dialog
  2. Element: 对应模块中的节点名 Confirm Button
  3. Modifier: 对应节点相关的状态,如disabled和highlight
    如dialog__confirm-button–highlight。

    实现CSS与JavaScript变量共享

    :export关键字可以把CSS中的变量输出到JavaScript中
    1
    2
    3
    4
    5
    $primary-color: #f40;

    :export {
    primaryColor: $primary-color;
    }
1
2
3
4
// app.js
import style from 'config.scss'

console.log(style.primaryColor);

CSS Modules使用技巧

建议遵循如下原则

  1. 不使用选择器,只使用class名来定义样式
  2. 不层叠多个class,只使用一个class把所有样式定义好
  3. 所有样式通过composes组合来实现复用
  4. 不嵌套
    常见问题
    1.如果在一个style文件使用同名class?
    虽然编译后可能是随机码,但仍是同名的。
    2.如果在style文件中使用了id选择器、伪类和标签选择器等呢?
    这些选择器不被转换,原封不动地出现在编译后的CSS中。也就是CSS Moudles只会转换class名相关的样式

    CSS Modules结合历史遗留项目实践

1.外部如何覆盖局部样式

因为无法预知最终的class名,不能通过一般选择器覆盖样式。我们可以给组件关键节点加上data-role属性,然后通过属性选择器来覆盖样式。

1
2
3
4
// dialog.js
return (
<div className={styles.root} data-role="dialog-root"></div>
);

1
2
3
4
// dialog.css
[data-role="dialog-root"] {
// override style
}

2.如何与全局样式共存

修改webpack配置

1
2
3
4
5
6
7
8
9
10
11
module: {
loaders: [{
test: /\.scss$/,
exclude: path.resolve(__dirname, 'src/views'),
loader: 'style!css?modules&localIdentName=[name]__[local]!sass?sourceMap=true',
}, {
test: /\.scss$/,
include: path.resolve(__dirname, 'src/styles'),
loader: 'style!css!sass?sourceMap=true'
}]
}

1
2
3
4
5
6
/* src/app.js */
import './styles/app.scss';
import Component from './view/Component'

/* src/views/Component.js */
import './Component.scss'

CSS Modules结合React实践

1
2
3
4
5
6
7
8
9
import styles from './dialog.css';

class Dialog extends Component {
render() {
return (
<div className={styles.root}></div>
)
}
}

如果不想频繁地输入styles.**,可以使用react-css-modules

组件间通信

父组件向子组件通信

父组件可以通过props向子组件传递需要的信息

子组件向父组件通信

有两种方法:1.利用回调函数。2.利用自定义事件机制:这种方法更通用,设计组件时考虑加入事件机制往往可以达到简化组件API的目的。

在React中,可以使用任意一种方法,在简单场景下使用自定义事件过于复杂,一般利用回调函数。

跨级组件通信

当需要让子组件跨级访问信息时,若像之前那样向更高级别的组件层层传递props,此时代码显得不那么优雅,甚至有些冗余。在React中,我们还可以使用context来实现跨级父子组件间的通信。

1
2
3
4
5
6
7
8
9
10
11
class ListItem extends Component {
static contextTypes = {
color: PropTypes.string,
}

render () {
return (
<li style={{ background: this.context.color }}></li>
)
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class List extends Component {
static childContextTypes = {
color: PropTypes.string,
}

getChildContext() {
return {
color: 'red'
}
}
render() {

}
}

React官方并不建议大量使用context,因为当组件结构复杂的时候,我们很难知道context是从哪传过来的。使用context比较好的场景是真正意义上的全局信息且不会更改,例如界面主题、用户信息等。总体的原则是如果我们真的需要它,那么建议写成高阶组件来实现。

没有嵌套关系的组件通信

没有嵌套关系的,那只能通过可以影响全局的一些机制去考虑。之前讲的自定义事件机制不失为一种上佳的方法。

我们在处理事件过程中需要注意,在componentDidMount事件中,如果组件挂载完成,再订阅事件;当组件卸载的时候,在componentWillUnmount事件中取消事件的订阅。

对于React使用的场景,EventEmitter只需要单例就可以了

1
2
3
import { EventEmitter } from 'events';

export default new EventEmitter();

1
2
3
import emitter from './events';

emitter.emit('ItenChange', entry)
1
2
3
4
5
6
7
8
9
10
class App extends Component {
componentDidMount() {
this.itemChange = emitter.on('ItemChange', (data) => {
console.log(data)
})
}
componentWillUnmount() {
emitter.removeListener(this.itemChange)
}
}

一般来说,程序中出现多级传递或跨级传递,那么要个重新审视一下是否有更合理的方式。Pub/Sub的模式可能也会带来逻辑关系的混乱。

跨级通信往往是反模式的,应该尽量避免仅仅通过例如Pub/Sub实现的设计思路,加入强依赖与约定来进一步梳理流程是更好的方法。(如使用Redux)

组件间抽象

常常有这样的场景,有一类功能需要被不同的组件公用,此时就涉及抽象的话题。我们重点讨论两种:mixin和高阶组件

封装mixin方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const mixin = function(obj, mixins) {
const newObj = obj;
newObj.prototype = Object.create(obj.prototype);

for (let prop in mixins) {
if (mixins.hasOwnProperty(prop)) {
newObj.prototype[prop] = mixins[prop]
}
}
}

const BigMixin = {
fly: () => {
console.log('fly');
}
}

const Big = function() {
console.log('new big');
}

consg FlyBig = mixin(Big, BigMixin)

const flyBig = new FlyBig();
flyBig.fly(); // => 'fly'

对于广义的mixin方法,就是用赋值的方式将mixin对象里的方法都挂载到原对象上,来实现对对象的混入。

看到上述实现,你可能会联想到underscore库中的extend或lodash库中的assign方法,或者说ES6中的Object.assign()方法。MDN上的解释是把任意多个源对象所拥有的自身可枚举属性复制给目标对象,然后返回目标对象。

在React中使用mixin

React在使用createClass构建组件时提供了mixin属性,比如官方封装的PureRenderMixin

1
2
3
4
5
6
7
import React from 'react';
import PureRenderMixin from 'react-addons-pure-render-mixin';

React.createClass({
mixins: [PureRenderMixin],
render() {}
})

mixins数组也可以增加多个mixin,其若mixin方法之间有重合,对于普通方法,在控制台里会报一个ReactClassInterface的错误。对于生命周期方法,会将各个模块的生命周期方法叠加在一起顺序执行。

mixin为组件做了两件事:

  1. 工具方法。如果你想共享一些工具类方法,可以定义它们,直接在各个组件中使用。
  2. 生命周期继承,props与state合并,mixin也可以作用在getInitialState的结果上,作state的合并,而props也是这样合并的。

    ES6 Classes与decorator

    ES6 classes形式构建组件,它并不支持mixin。decorator语法糖可以实现class上的mixin。

core-decorators库为开发者提供了一些实用的decorator,其中实现了我们正想要的@mixin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { getOwnPropertyDescriptors } from './private/utils';

const { defineProperty } = Object;

function handleClass(target, mixins) {
if (!mixins.length) {
// throw error;
}

for(let i = 0, l = mixins.length; i < l; i ++) {
const descs = getOwnPropertyDescriptors(mixins[i])

for (const key in descs) {
if (!(key in target.prototype)) {
defineProperty(target.prototype, key, descs[key])
}
}
}
}

export default function mixin(...mixins) {
if (typeof mixins[0] === 'function') {
return handleClass(mixins[0], [])
} else {
return target => {
return handleClass(target, mixins)
}
}
}

原理也很简单,它将每一个mixin对象的方法都叠加到target对象的原型上以达到mixin的目的。这样就可以用@mixin来做多个重用模块的叠加了。

1
2
3
4
5
6
7
8
9
10
11
12
const PureRender = {
shouldComponentUpdate() {}
}

const Theme = {
setTheme() {}
}

@mixin(PureRender, Theme)
class MyComponent extends Component {
render() {}
}

mixin的逻辑和最早实现的简单逻辑很相似,之前直接给对象的prototype属性赋值,但这里用了getOwnPropertyDescriptor和defineProperty这两个方法,有什么区别呢?

这样实现的好在于definedProperty这个方法,也就是定义和赋值的区别,定义是对已有的定义,赋值是覆盖已有的定义。前者并不会覆盖已有方法,但后者会。本质上与官方的mixin方法都很不一样,除了定义方法级别不能覆盖外,还得加上对生命周期方法的继承,以及对state的合并。

decorator还有作用在方法上的,它可以控制方法的自有属性,也可以作decorator的工厂方法。

mixin的问题

mixin存在很多问题,已经被官方弃用了,由高阶组件替代。

1.破坏的原有组件的封装

我们知道mixin方法会混入方法,给原有组件带来新的特性,比如mixin中有一个renderList方法,给我们带来了渲染List的能力,但它也可能带来新的state和props,这意味着组件有一些”不可见”的状态需要我们去维护,但我们在使用的时候并不清楚。此外renderList中的方法会调用组件中方法,但很可能被其他mixin截获,带来很多不可知。

2.不同mixin的命名冲突

3.增加复杂性

我们设计一个组件,引入PopupMixin的mixin,这样就给组件引进了PopupMixin生命周期方法。当我们再引入HoverMixin,将有更多的方法被引进。当然我们可以进一步抽象出TooltipMixin,将两个整合在一起,但我们发现它们都有compomentDidUpdate方法。过一段时间,你会发现它的逻辑已经复杂到难以理解了。

我们写React组件时,首先考虑的往往是单一的功能、简洁的设计和逻辑。当加入功能的时候,可以继续控制组件的输入和输出。如果说因为复杂性,我们不断加入新的状态,那么组件会变得非常难维护。

高阶组件

高阶函数是函数式编程中的一个基本概念,这种函数接受函数作为输入,或是输出一个函数。
高阶组件类似高阶函数,它接受React组件作为输入,输出一个新的React组件。

高阶组件让我们的代码更具有复用性、逻辑性与抽象特性,它可以对render方法作劫持,也可以控制props和state。

实现高阶组件的方法有两种:

  1. 属性代理:通过被包裹的React组件来操作props
  2. 反向继承:继承于被包裹的React组件

1.属性代理

1
2
3
4
5
6
7
const MyContainer = (WrappedComponent) => {
class extends Component {
render() {
return <WrappedComponent {...this.props} />
}
}
}

这样,我们就可以通过高阶组件来传递props,这种方法即为属性代理。
这样组件就可以一层层地作为参数被调用,原始组件就具备了高阶组件对它的修饰。保持了单个组件封装性同时还保留了易用性。当然,也可以用decorator来转换

1
2
3
4
5
6
@MyContainer
class MyComponent extends Component {
render() {}
}

export default MyComponent

上述执行生命周期的过程类似于堆栈调用:
didmount -> HOC didmount -> (HOCs didmount) -> (HOCs will unmount) -> HOC will unmount -> unmount
从功能上,高阶组件一样可以做到像mixin对组件的控制,包括控制props、通过refs使用引用、抽象state和使用其他元素包裹WrappedComponent.

1.控制props

我们可以读取、增加、编辑或是移除从WrappedComponent传进来的props,但需要小心删除与编辑重要的props。我们应该尽可能对高阶组件的props作新的命名以防止混淆。

例如,我们需要增加一个新的prop:

1
2
3
4
5
6
7
8
9
10
const MyContainer = (WrappedComponent) => {
class extends Component {
render() {
const newProps = {
text: newText,
};
return <WrappedComponent {...this.props} {...newProps} />;
}
}
}

2.通过refs使用引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const MyContainer = (WrappedComponent) => {
class extends Component {
proc(wrappedComponentInstance) {
wrappedComponentInstance.method();
}

render() {
const props = Object.assign({}, this.props, {
ref: this.proc.bind(this),
})
return <WrappedComponent {...props} />
}
}
}

这样就可以方便地用于读取或增加实例的props,并调用实例的方法。
3.抽象state
高阶组件可以将原组件抽象为展示型组件,分离内部状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const MyContainer = (WrappedComponent) => {
class extends Component {
constructor(props) {
super(props)
this.state = {
name: ''
}

this.onNameChange = this.onNameChange.bind(this)
}

onNameChange(event) {
this.setState({
name: event.target.value
})
}

render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onNameChange,
}
}
return <WrappedComponent {...this.props} {...newProps}>
}
}
}

这样就有效地抽象了同样的state操作。
4.使用其他元素包裹WrappedComponent
这既可以是为了加样式,也可以是为了布局

1
2
3
4
5
6
7
8
9
10
11
const MyContainer = (WrappedComponent) => {
class extends Component {
render() {
return (
<div style={{ display: 'block' }}>
<WrappedComponent {...this.props} />
</div>
)
}
}
}

反向继承

1
2
3
4
5
6
7
const MyContainer = (WrappedComponent) => {
class extends WrappedComponent{
render() {
return super.render()
}
}
}

高阶组件返回的组件继承于WrappedComponent,因为被动地继承了WrappedComponent,所有调用都会反向,这也是这种方法的由来。
因为依赖于继承的机制,HOC的调用顺序和队列是一样的
didmount->HOC didmount->(HOCs didmount)->will unmount->HOC will unmount->(HOCs will unmount)

在反向继承方法中,高阶组件可以使用WrappedComponent引用,这意味着它可以使用WrappedComponent的state、props、生命周期、和render。但它不能保证完整的子组件树被解析。

它有两大特点

1.渲染劫持

高阶组件可以控制WrappedComponent的渲染过程。可以在这个过程中在任何React元素输出的结果中读取;增加、修改。删除props,或读取或修改React元素树,或条件显示元素树,又或是用样式控制包裹元素树。

如果元素树中包括了函数类型的React组件,就不能操作组件的子组件。

条件渲染示例

1
2
3
4
5
6
7
8
9
10
11
const MyContainer = (WrappedComponent) => {
class extends WrappedComponent {
render() {
if (this.props.loggedIn) {
return super.render();
} else {
return null;
}
}
}
}

对render的输出结果进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const MyContainer = (WrappedComponent) => {
class extends WrappedComponent {
render() {
const elementsTree = super.render();
let newProps;
if (elementsTree && elementsTree.type === 'input') {
newProps = { value: 'may the force be with you' }
}
const props = Object.assign({}, elementsTree.props, newProps);
const newElementsTree = React.cloneElement(elementsTree, props, elementsTree.props.children);
return newElementsTree;
}
}
}

2.控制state

高阶组件可以读取、修改或删除WrappedComponent实例中的state,如果需要的话,也可以增加state。但这样做,可能会让WrappedComponent组件内部状态变得一团糟。大部分高阶组件都应该限制读取或增加state,尤其是后者,可以通过重命名state,以防止混淆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const MyContainer = (WrappedComponent) => {
class extends WrappedComponent {
render() {
return (
<div>
<h2>HOC Debugger Component</h2>
<p>Props</p><pre>{JSON.stringify(this.props, null, 2)}</pre>
<p>State</p><pre>{JSON.stringify(this.state, null, 2)}</pre>
{super.render()}
</div>
)
}
}
}

在这个例子中,显示了WrappedComponent的props和state,方便我们调试。

组件命名

高阶组件失去了原始的diplayName,我们应该为高阶组件命名

1
HOC.displayName = `HOC(${getDisplayName(WrappedComponent)})`


1
2
3
class HOC extends ... {
static displayName = `HOC(${getDisplayName(WrappedComponent)})`
}

1
2
3
4
5
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'
}

组件参数

1
2
3
4
5
6
7
8
9
function HOCFactoryFactory(...params) {
return function HOCFactory(WrappedComponent) {
return class HOC extends Component {
render() {
return <WrappedComponent {...this.props} />
}
}
}
}

组合式组件开发实践

我们多次提到,使用React开发组件时利用props传递参数。也就是说,用参数来配置组件时我们最常用的封装方式。随着场景发生变化,组件的形态也发生变化,我们必须不断增加props去应对变化,此时便会导致props的泛滥,而在拓展中又必须保证组件向下兼容,只增不减,使组件的可维护性降低。

我们就可以利用上述高阶组件的思想,提出组件组合式开发模式,有效地解决了配置式所存在的一些问题。

1.组件再分离

SelectInput、SearchInput与List三个颗粒度更细的组件可以组合成功能丰富的组件,而每个组件可以是纯粹的、木偶式的组件。

2.逻辑再抽象

组件中相同交互逻辑和业务逻辑也应该抽象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 完成SearchInput与List的交互
const searchDecorator = WrappedComponent => {
class SearchDecorator extends Component {
constructor(props) {
super(props)

this.handleSearch = this.handleSearch.bind(this)
}

handleSearch(keyword) {
this.setState({
data: this.props.data,
keyword,
})

this.props.onSearch(keyword)
}

render() {
const { data, keyword } = this.state;
return (
<WrappedComponent
{...this.props}
data={data}
keyword={keyword}
onSearch={this.handleSearch}
/>
)
}
}

return SearchDecorator;
}

// 完成List数据请求
const asyncSelectDecorator = WrappedComponent => {
class AsyncSelectDecorator extends Component {
componentDidMount() {
const { url, params } = this.props;

fetch(url, { params }).then(data => {
this.setState({
data
})
})
}

render() {
return (
<WrappedComponent
{...this.props}
data={this.state.data}
/>
)
}
}

return AsyncSelectDecorator;
}

最后,我们用compose将高阶组件层层包裹,将页面和逻辑完美结合在一起

1
const FinalSelector = compose(asyncSelectDecorator, searchDecorator, selectedItemDecorator)(Selector)

组件性能优化

从React的渲染过程中,如何防止不必要的渲染是最需要去解决的问题。
针对这个问题,React官方提供了一个便捷的方法来解决,那就是PureRender

纯函数

纯函数由三大原则构成

1.给定相同的输入,它总是返回相同的输出

2.过程没有副作用(我们不能改变外部状态)

3.没有额外的状态依赖

纯函数也非常方便进行方法级别的测试以及重构,可以让程序具有良好的扩展性及适应性。

PureRender

PureRender中的Pure指的就是组件满足纯函数的条件,即组件的渲染是被相同的props和state渲染进而得到相同的结果。

1.PureRender本质

官方在早期为开发者提供了一个react-addons-pure-render-mixin的插件,其原理为重新实现了shouldComponentUpdate生命周期方法,让当前传入的props和state与之前的作浅比较,如果返回false,那么组件就不会执行render方法。(若做深度比较,也很耗性能)

PuerRender源码中,只对新旧props作了浅比较,以下是shallowEqual的示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function shallowEqual(obj, newObj) {
if (obj === newObj) {
return true;
}

const objKeys = Object.keys(obj);
const newObjKeys = Object.keys(newObj);
if (objKeys.length !== newObjKeysl.length) {
return false;
}

return objKeys.every(key => {
return newObj[key] === obj[key];
})
}

3.优化PureRender

如果说props或state中有以下几种类型的情况,那么无论如何,它都会触发PureRender为true。

3.1直接为props设置对象或数组

引用的地址会改变

1
<Account style={{ color: 'black' }} />

避免这个问题

1
2
const defaultStyle = {};
<Account style={{ this.props.style || defaultStyle }} />

3.2设置props方法并通过事件绑定在元素上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyInput extends Component {
constructor(props) {
super(props)

this.handleChange = this.handleChange.bind(this)
}

handleChange(e) {
this.props.update(e.target.value)
}

render() {
return <input onChange={this.handleChange} />
}

}

3.3设置子组件

对于设置了子组件的React组件,在调用shouldComponentUpdate时,均返回true。

1
2
3
4
5
6
7
8
9
class NameItem extends Component {
render() {
return (
<Item>
<span>Arcthur</span>
</Item>
)
}
}

1
2
3
<Item 
children={React.createElement('span', {}, 'Arcthur')}
/>

怎么避免重复渲染覆盖呢?我们在NameItem设置PureRender,也就是提到父级来判断。

Immutable

在传递数据时可以直接使用Immutable Data来进一步提升组件的渲染性能。

JavaScript中的对象一般是可变的,因为使用了引用赋值,新的对象简单地引用了原始对象,改变新的对象将影响到原始对象。

使用浅拷贝或深拷贝可以避免修改,但这样做又造成了内存和CPU的浪费。

1.Immutable Data一旦被创建,就不能再更改数据,对Immutable对象进行修改,添加或删除操作,都会返回一个新的Immutable对象。Immutable实现的原理是持久化的数据结构,也就是使用旧数据创建新数据,要保存旧数据同时可用且不变,同时为了避免深拷贝把所有节点复制一遍带来的性能损耗,Immutable使用了结构共享,即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其他节点则进行共享。

Immutable的优点

1.降低了可变带来的复杂度。

可变数据耦合了time和value的概念,造成了数据很难被回溯

1
2
3
4
5
function touchAndLog (touchFn) {
let data = { key: '1' }
touchFn(data);
console.log(data.key)
}

若data是不可变的,能打印的结果是什么。

2.节省内存

Immutable使用的结构共享尽量复用内存。没有引用的对象会被垃圾回收。

1
2
3
4
5
6
7
8
9
10
11
import { map } from 'immutable';

let a = Map({
select: '1',
filter: Map({ name: '2' }),
});

let b = a.set('select', 'people');

a === b
a.get('filter') === b.get('filter')// true

3.撤销/重做,复制/粘贴,甚至时间旅行这些功能都很容易实现。

因为每次数据都是不一样的,那么只要把这些数据放到一个数组里存储起来,就能自由回退。

4.并发安全

数据天生是不可变的,后端常用的并发锁就不需要了,然而现在并没有用,因为一般JavaScript是单线程运行的。

5.拥抱函数式编程

Immutable本身就是函数式编程中的概念,只要输入一致,输出必然一致。

使用Immutable的缺点

容易与原生对象混淆是使用Immutale的过程中遇到的最大问题。
下面给出了一些办法

1.使用FlowType或TypeScript静态类型检查工具

2.约定变量命名规则,如Immutable类型对象以$$开头

3.使用Immutable.fromJS而不是Immutable.Map或Immutable.List来创建对象,这样可以避免Immutable对象和原生对象间的混用

Immutable.js

两个Immutable对象可以使用===来比较,这样是直接比较内存地址,其性能最好。但是即使两个对象的值是一样的,也会返回false。

Immutable提供了Immutable.is来作”值比较”,Immutable比较的是两个对象的hasCode或valueOf,由于Immutable内部使用了trie数据结构来存储,只要两个对象的hasCode相等,值就是一样的。这样的算法避免了深度遍历比较,因此性能非常好。

Immutable与cursor

这里的cursor和数据库中的游标是完全不同的概念。由于Immutable数据一般嵌套非常深,所以为了便于访问深层数据,cursor提供了可以直接访问这个深层数据的引用:

1
2
3
4
5
6
7
8
9
10
11
let data = Immutable.fromJS({ a: { b: { c: 1 } } });
let cursor = Cursor.from(data, ['a', 'b'], newData => {
// 当cursor或其子cursor执行更新时调用
console.log(newData)
})

// 1
cursor.get('c');
cursor = cursor.update('c', x => x + 1)
// 2
cursor.get('c');

Immutable与PureRender

React做性能优化时最常用的就是shouldComponentUpdate,使用深拷贝和深比较是非常昂贵的选择。而使使用Immutable.js,===和is是高效地判断数据是否变化的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { is } from 'immutable'

shouldComponentUpdate(nextProps, nextState) {
const thisProps = this.props || {};
const thisState = this.state || {};

if (Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length) {
return true;
}

for (const key in nextProps) {
if (nextProps.hasOwnProperty(key) && !is(thisProps[key], nextProps[key])) {
return true;
}
}

for (const key in nextState) {
if (nextState.hasOwnProperty(key) && !is(thisState[key], nextState[key])) {
return true;
}
}
}

Immutable与setState

React建议把this.state当作不可变的,因此修改前需要做一个深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
import _ from 'lodash';

class App extends Component {
this.state = {
data: { time: 0 }
}

handleAdd() {
let data = _.cloneDeep(this.state.data);
data.time = data.time + 1;
this.setState({ data });
}
}

使用Immutable后,操作变得很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Map } from 'immutable';

class App extends Component {
this.state = {
data: Map({ time: 0 })
}

handleAdd() {
this.setState(({ data }) => {
data: data.update('times', v => v + 1)
})
}
}

Immutable可以给应用带来极大的性能提升。

key

如果每一个组件是一个数组或迭代器的话,那么必须有一个唯一的key prop。它是用来标识当前项的唯一性的。如果使用index,它相当于一个随机键,无论有没有相同的项,更新都会渲染。如果key相同,react会抛警告,且只会渲染第一个key。

若有两个子组件需要渲染,可以用插件createFragment包裹来解决。

react-addons-perf

react-addons-perf通过Perf.start和Perf.stop两个API设置开始和结束的状态来作分析,它会把各组件渲染的各阶段的时间统计出来,然后打印出一张表格。

解读 React 源码

注:源码以React15.0

reconciler(协调器),是最核心的部分。包含 React 中自定义组件的实现(ReactCompositeComponent)、组件生命周期机制、setState 机制(ReactUpdates、ReactUpdateQueue)、DOM diff 算法(ReactMultiChild)等重要的特性方法。

Virtual DOM

构建一套简易的 Virtual DOM 并不复杂,它只需要具备一个 DOM 标签所需要的基本元素即可:

  1. 标签名
  2. 节点属性,包含样式、属性、事件等
  3. 子节点
  4. 标识 id
1
2
3
4
5
6
7
8
{
tagName: 'div',
properties: {
style: {}
},
children: [],
key: 1
}

Virtual DOM 当然远不止于此,却也离不开这些基础元素。下面让我们一探究竟

Virtual DOM 中的节点称为 ReactNode,它分为 3 种类型 ReactElement、ReactFragment 和 ReactText。其中 ReactElement 又分为 ReactComponentElement 和 ReactDOMElement。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
type ReactNode = ReactElement | ReactFragment | ReactText;

type ReactElement = ReactComponentElement | ReactDOMElement;

type ReactDOMElement = {
type: string,
props: {
children: ReactNodeList,
className: string,
etc.
},
key: string | boolean | number | null,
ref: string | null
};

type ReactComponentElement<TProps> = {
type: ReactClass<TProps>,
props: TProps,
key: string | boolean | number | null,
ref: string | null
};

type ReactFragment = Array<ReactNode | ReactEmpty>;

type ReactNodeList = ReactNode | ReactEmpty;

type ReactText = string | number;

type ReactEmpty = null | undefined | boolean;

Virtual DOM 是如何根据这些节点类型来创建元素的呢?

创建 React 元素

我们知道通过 JSX 创建的虚拟元素最终会被编译成调用 React 的 createElement 方法。那么 createElement 方法到底做了什么呢,其实 createElement 只是做了简单的参数修正,返回一个 ReactElement 实例对象,也就是虚拟元素实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
ReactElement.createElement = function(type, config, children) {
// 初始化参数
var propName;
var props = {};
var key = null;
var ref = null;
var self = null;
var source = null;

// 如果存在config,则提取里面的内容
if (config != null) {
ref = config.ref === undefined ? null : config.ref;
key = config.key === undefined ? null : '' + config.key;
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 复制config里的内容到props(如id和className等)
for (propName in config) {
if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
}

// 处理children,全部挂载到props的children属性上。如果只有一个参数,直接赋值给children,否则做合并处理。
var childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
var childArray = Array(childrenLength);
for (var i = 0; i < childrenLength; i ++) {
childArray[i] = arguments[i + 2]
}
props.children = childArray;
}

// 如果某个prop为空且存在默认的prop,则将默认prop赋给当前的prop
if (type && type.defaultProps) {
var defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (typeof props[propName] === 'undefined') {
props[propName] = defaultProps[propName]
}
}
}

// 返回一个ReactElement实例对象
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props)
}

Virtual DOM 通过 createElement 创建虚拟元素,那又是如何创建组件的呢?

初始化组件入口

当使用 React 创建组件时,首先会调用 instantiateReactComponent,这是初始化组件的入口函数,它通过判断 node 类型来区分不同组件的入口。

  1. 当 node 为空时,说明 node 不存在,则初始化空组件 ReactEmptyComponent.create(instantiateReactComponent).

  2. 当 node 类型为对象,需进一步判断,如果 element 类型为字符串时,则初始化 DOM 标签组件 ReactNativeComponent.createInternalComponent(element),否则初始化自定义组件
    ReactCompositeComponentWrapper()

  3. 当 node 类型为字符串或数字时,则初始化文本组件 ReactNativeComponent.createInstanceForText(node)

  4. 其他情况不处理

instantiateReactComponent方法的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 初始化组件入口
function instantiateReactComponent(node, parentCompositeType) {
var instance;

// 空组件(ReactEmptyComponent)
if (node === null || node === false) {
instance = ReactEmptyComponent.create(instantiateReactComponent);
}

if (typeof node === 'object') {
var element = node;
if (typeof element.type === 'string') {
// DOM标签(ReactDOMComponent)
instance = RectNativeComponent.createInternalComponent(element)
} else if (isInternalComponentType(element.type)) {
// 不是字符串表示的自定义组件暂无法使用,此处将不做组件的初始化操作
instance = new element.type(element);
} else {
// 自定义组件(ReactCompositeComponent)
instance = new ReactCompositeComponentWrapper();
}
} else if (typeof node === 'string' || typeof node === 'number') {
// 字符串或数字(ReactTextComponent)
instance = ReactNativeComponent.createInstanceForText(node)
} else {
// 不做处理
}

// 设置实例
instance.construct(node);
// 初始化参数
instance._mountIndex = 0;
instance._mountImage = null;

return instance;
}

文本组件

node 类型为文本节点是不算 Virtual DOM 元素的,但 React 为了保持渲染的一致性,将其封装为文本组件ReactDOMTextComponent。

在执行 mountComponent 方法时,ReactDOMTextComponent 通过 transaction.useCreateElement 判断该文本是否是通过 createElement 方法创建的节点,如果是,则为该节点创建相应的标签和标识 domID,这样每个文本节点也能与其他 React 节点一样拥有自己的唯一标识,同时也拥有了VirtualDOM diff的权利。如果不是,则不再为其创建<span>和 domID 标识,直接返回文本。

不再为裸露的文本内容包裹<span>标签,是React15.0版本的更新点之一。此前,React为裸露的文本内容包裹上<span>标签,其实并没有任何作用,反而增加了不必要的标签。

在执行 receiveComponent 方法时,可以通过 DOMChildrenOperations.replaceDelimitedText(componentNodes[0], componentNodes[1], nextStringText)来更新文本内容。

ReactDOMTextComponent的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 创建文本组件,这是ReactText,并不是ReactElementd
var ReactDOMTextComponent = function(text) {
// 保存当前的字符串
this._currentElement = text;
this._stringText = '' + text;

// ReactDOMComponentTree 需要使用的参数
this._nativeNode = null;
this._nativeParent = null;

// 属性
this._domID = null;
this._mountIndex = 0;
this._closingComment = null;
this._commentNodes = null;
};

Object.assign(ReactDOMTextComponent.prototype, {
mountComponent: function(transaction nativeParent, nativeContainerInfo, context) {
var domID = nativeContainerInfo._idCounter++;
var openingValue = ' react-text: ' + domID + '';
var closingValue = ' /react-text ';
this._domID = domID;
this._nativeParent = nativeParent;

// 如果使用createElement创建文本标签,则该文本会带上标签和domID
if (transaction.useCreateElement) {
var ownerDocument = nativeContainerInfo._ownerDocument;
var openingComment = ownerDocument.createCommet(openingValue);
var closingComment = ownerDocument.createComment(closingValue);
var lazyTree = DOMLazyTree(ownerDocument.createDocumentFragment());
// 开始标签
DOMLazyTree.queueChild(lazyTree, DOMLazyTree(openingComment));
// 如果是文本类型,则创建文本节点
if (this._stringText) {
DOMLazyTree.queueChild(lazyTree, DOMLazyTree(ownerDocument.createTextNode(this._stringText)));
}
// 结束标签
DOMLazyTree.queueChild(lazyTree, DOMLazyTree(closingComment));
ReactDOMComponentTree.precacheNode(this, openingComment);
this._closingComment = closingComment;
return lazyTree;
} else {
var escapedText = escapeTextContentForBrowser(this._stringText);
// 静态页面下直接返回文本
if (transaction.renderToStaticMarkup) {
return escapedText;
}
// 如果不是通过createElement创建的文本,则将标签和属性注释掉,直接返回文本内容。
return (
'!<--' + openingValue + '-->' + escapedText + '<!--' + closingValue + '-->'
);
}
},

// 更新文本内容
receiveComponent: function(nextComponent, transaction) {
if (nextText !== this._currentElement) {
this._currentElement = nextText;
var nextStringText = '' + nextText;
if (nextStringText !== this._stringText) {
this._stringText = nextStringText;
var componentNodes = this.getNativeNode();

DOMChildrenOerations.replaceDelimitedText(commentNodes[0], commentNodes[1], nextStringText)
}
}
}
})

DOM 标签组件

ReactDOMComponent 针对 Vitual DOM 标签的处理主要分为以下两个部分:

  1. 属性的更新,包括更新样式、更新属性、处理事件等;
  2. 子节点的更新,包括更新内容、更新子节点,此部分涉及 diff 算法

更新属性

当执行 mountComponent 方法时,ReactDOMComponent 首先会生成标记和标签,通过 this.createOpenTagMarkupAndPutListeners(transaction)来处理 DOM 节点的属性和事件。

  1. 如果存在事件,则对当前的节点添加事件代理,即调用 enqueuePutListener(this, propKey, propValue, transaction)
  2. 如果存在样式,首先会对样式进行合并操作 Object.assign({}, props.style),然后通过 CSSPropertyOperations.createMarkupForStyles(propValue, this)创建样式。
  3. 通过DOMPropertyOperations.createMarkupForProperty(propKey, propValue)创建属性。
  4. 通过 DOMPropertyOperations.createMarkupForID(this._domID)创建唯一标识
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
_createOpenTagMarkupAndPutListeners: function(transaction, props) {
var ret = '<' + this._currentElement.type;
// 拼凑出属性
for(var propKey in props) {
var propValue = props[propKey]

if (registrationNameModules.hasOwnProperty(propKey)) {
// 针对当前的节点添加事件代理
if (propValue) {
enqueuePutListener(this, propKey, propValue, transaction)
}
} else {
if (propKey === STYLE) {
if (propValue) {
// 合并样式
propValue = this._previousStyleCopy = Object.assign({}, props.style)
}
propValue = CSSPropertyOperations.createMarkupForStyles(propValue, this)
}
// 创建属性标识
var markup = null;
if (this._tag != null && isCustomComponent(this._tag, props)) {
markup = DOMPropertyOperations.createMarkupForProperty(propKey, propValue)
}
if (markup) {
ret += ' ' + markup
}
}
}

// 对于静态页面,不需要设置react-id,这样可以节省大量字符
if (transaction.renderToStaticMarkup) {
return ret
}

// 设置react-id
if (!this._nativeParent) {
ret += ' ' + DOMPropertyOperations.createMarkupForRoot();
}
ret += ' ' + DOMPropertyOperations.createMarkupForID(this._domID)

return ret;
}

当执行 receiveComponent 方法时,ReactDOMComponent 会通过 this.updateComponent(transaction, prevElement, nextElement, context)来更新 DOM 节点属性。

先是删除不需要的旧属性。如果不需要旧样式,则遍历旧样式集合,并对每个样式进行置空删除;如果不需要事件,则将其事件监听的属性去掉,即针对当前的节点取消事件代理 deleteListener(this, propKey);如果旧属性不在新属性集合里时,则需要删除旧属性 DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey)。

再是更新新属性。如果存在新样式,则将新样式进行合并 Object.assign({}, nextProp);如果在旧样式中但不在新样式中,则清除该样式;如果既在旧样式中也在新样式中,且不相同,则更新样式 styleUpdates[styleName] = nextProp[styleName];如果在新样式中,但不在旧样式中,则直接更新为新样式 styleUpdates = nextProp;如果存在事件更新,则添加事件监听的属性 enqueuePutListener(this, propKey, nextProp, transaction);如果存在新属性,则添加新属性,或者更新旧的同名属性 DOMPropertyOperations.setValueForAttribute(node, propkey, nextProp).

至此,ReactDOMComponent完成了DOM节点属性更新的操作,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
_updateDOMProperties: function(lastProps, nextProps, transaction) {
var propKey;
var styleName;
var styleUpdates;

// 当一个旧属性不在新的属性集合里时,需要删除
for (propKey in lastProps) {
// 如果新属性里有,或者propKey是在原型上的则直接跳过,这样剩下的都是不在新属性集合里的。
if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) {
continue;
}
// 从DOM上删除不需要的样式
if (propKey === STYLE) {
var lastStyle = this._previousStyleCopy;
for (styleName in lastStyle) {
if (lastStyle.hasOwnProperty(styleName)) {
styleUpdates = styleUpdates || {};
styleUpdates[styleName] = '';
}
}
this._previousStyleCopy = null
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (lastProps[propKey]) {
// 这里的事件监听的属性需要去掉监听,针对当前的节点取消事件代理
deleteListener(this, propKey)
}
} else if (DOMProperty.isStandardName[propKey] || DOMProperty.isCustomAttribute(propKey)) {
// 从DOM上删除不需要的属性
DOMPropertyOperations.deleteValueForProperty(getNode(this), propKey)
}
}

// 对于新的属性,需要写到DOM节点上
for (propKey in nextProps) {
var nextProp = nextProps[propKey];
var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps != null ? lastProps[propKey] : undefined;
// 不在新属性中,或与旧属性相同,则跳过
if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp === null && lastProp == null) {
continue;
}
// 在DOM上写入新样式(更新样式)
if (propKey === STYLE) {
if (nextProp) {
nextProp = this._previousStyleCopy = Object.assign({}, nextProp)
}
if (lastProp) {
// 在旧样式中且不在新样式中,清除该样式
for (styleName in lastProp) {
if (lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName))) {
styleUpdates = styleUpdates || {}
styleUpdates[styleName] = '';
}
}
// 即在旧样式中也在新样式中,且不相同,更新该样式
for (styleName in nextProp) {
if (nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName]) {
styleUpdates = styleUpdates || {}
styleUpdates[styleName] = nextProp[styleName]
}
}
} else {
// 不存在旧样式,直接写入新样式
styleUpdates = nextProp
}
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
// 添加事件监听的属性
enqueuePutListener(this, propKey, nextProp, transaction)
} else {
deleteListener(this, propKey)
}
// 添加新的属性,或者是更新旧的同名属性
} else if (isCustomComponent(this._tag, nextProps)) {
if (!RESERVED_PROPS.hasOwnProperty(propKey)) {
// setValueForAttribute 更新属性
DOMPropertyOperations.setValueForAttribute(getNode(this), propKey, nextProp)
}
} else if (DOMProperty.properties[propKey] || DOMProperty.isCustomAttribute(propKey)) {
var node = getNode(this);
if (nextProp != null) {
DOMPropertyOperations.setValueForProperty(node, propKey, nextProp)
} else {
// 如果更新为null或undefined,则执行删除属性操作
DOMPropertyOperations.deleteValueForProperty(node, propKey)
}
}
// 如果styleUpdates不为空,则设置新样式
if (styleUpdates) {
CSSPropertyOperations.setValueForStyles(getNode(this), styleUpdates, this)
}
}
}

更新子节点

当执行mountComponent方法时,ReactDOMComponent会通过this._createContentMarkup(transaction, props, context)来处理DOM节点的内容。

首先,获取节点内容props.dangerouslySetInnerHTML。如果存在子节点,则通过this.mountChildren(childrenToUse, transaction, context)对子节点进行初始化渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
_createContentMarkup: function(transaction, props, context) {
var ret = '';

// 获取子节点渲染出的内容
var innerHTML = props.dangerouslySetInnerHTML;

if (innerHTML != null) {
if (innerHTML.__html != null) {
ret = innerHTML.__html;
}
} else {
var contentToUse = CONTENT_TYPES[typeof props.children] ? props.children : null;
var childrenToUse = contentToUse != null ? null : props.children;

if (childrenToUse != null) {
ret = escapeTextContentForBrowser(contentToUse)
} else if (childrenToUse != null) {
// 对子节点进行初始化渲染
var mountImages = this.mountChildren(childrenToUse, transaction, context)

ret = mountImages.join('')
}
}
// 是否需要换行
if (newlineEatingTags[this._tag] && ret.charAt(0) === '\n') {
return '\n' + ret;
} else {
return ret;
}
}

当执行 receiveComponent 方法时,ReactDOMComponent 会通过 this._updateDOMChildren(lastProps, nextProps, transaction, context)来更新 DOM 内容和子节点。

先是删除不需要的子节点和内容。如果旧节点存在,而新节点不存在,说明当前节点在更新后被删除,此时执行 this.updateChildren(null, transaction, context);如果旧的内容存在,而新的内容不存在,说明当前内容在更新后被删除,此时执行方法 this.updateTextContent(‘’)。

再是更新子节点和内容。如果新子节点存在,则更新其子节点,此时执行方法 this.updateChildren(nextChildren, transaction, context);如果新内容存在,更新内容,执行 this.updateTextContent(‘’ + nextContent)

至此,ReactDOMComponent完成了DOM子节点和内容的更新操作

1
2
3
4
5
6
7
8
_updateDOMChildren: function(lastProps, nextProps, transaction, context) {
var lastContent = CONTENT_TYPES[typeof lastProps.children] ? lastProps.children : null;
var nextContent = CONTENT_TYPES[typeof nextProps.children] ? nextProps.children : null;
var lastHtml = lastProps.dangerouslySetInnerHTML && lastProps.dangerouslySetInnerHTML._html;
var nextHtml = nextProps.dangerouslySetInnerHTML && nextProps.dangerouslySetInnerHTML._html;

var lastChildren = lastContent != null ? nu
}

当卸载组件时,ReactDOMComponent 会卸载子节点、清除事件监听、情空标识等。

自定义组件

ReactCompositeComponent 自定义组件实现了一整套 React 生命周期和 setState 机制,因此自定义组件是在生命周期的环境中进行更新属性、内容和子节点的操作。这些更新操作与 ReactDOMComponent 的操作类似,在此不再赘述。

初探 React 生命周期

  1. 当首次挂载组件时,按顺序执行 getDefaultProps、getInitialState、componentWillMount、render 和 componentDidMount
  2. 当卸载组件时,执行 componentWillUnmount
  3. 当重新挂载组件时,此时按顺序执行 getInitialState、componentWillMount、render 和 componentDidMount,不执行 getDefaultProps
  4. 当再次渲染组件时,组件接受到更新状态,此时按顺序执行 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate

在 ES6 语法中,static defaultProps = {} 就是调用的内部 getDefaultProps,constructor 中的 this.state = {}就是调用内部的 getInitialState。

详解 React 生命周期

自定义组件(ReactCompositeComponent)的生命周期主要通过 3 个阶段进行管理——MOUNTING、RECEIVE_PROPS 和 UNMOUNTING。这个 3 个阶段对应 3 种方法,分别为:mountComponent、updateComponent、unmountComponent,每个方法都提供了几种处理方法,其中带 will 前缀的方法在进入状态之前调用,带 did 前缀的方法在进入状态之后调用。3 个阶段共包括 5 种处理方法,还有两种特殊状态的处理方法。

1.使用 createClass 创建自定义组件

createClass 是创建自定义组件的入口方法,负责管理生命周期的 getDefaultProps。该方法在整个生命周期中只执行一次,这样所有实例初始化的 props 将会被共享。
通过 createClass 创建自定义组件,利用原型继承 ReactClassComponent 父类,按顺序合并 mixin,设置初始化 defaultProps,返回构造函数。
class MyComponent extends React.Component 其实就是调用内部方法 createClass 创建组件。

2.阶段一:MOUNTING

mountComponent 负责管理生命周期中的 getInitialState、componentWillMount、render 和 componentDidMount。
由于 getDefaultProps 是通过构造函数进行管理的,所以在整个生命周期中最先开始执行,也解释了为什么它只执行一次。
由于通过 ReactCompositeComponentBase 返回的是一个虚拟节点,所以需要利用 instantiateReactComponent 去得到实例,再使用 mountComponent 拿到结果作为当前自定义元素的结果。

通过 mountComponent 挂载组件,初始化序号、标记等参数,判断是否为无状态组件,并进行对应组件初始化工作,比如初始化 props、context 等参数。利用 getInitialState 获取初始化 state、初始化更新队列和更新状态。

若存在 componentWillMount,则执行。如果此时在 componentWillMount 中调用 setState 方法,是不是触发 re-render 的,而是会进行 state 合并,且 inst.state = this._processPendingState(inst.props, inst.context)是在 componentWillMount 之后执行的,因此 componentWillMount 中的 this.state 并不是最新的,在 render 中才可以获取更新后的 this.state。

因此,React 是利用更新队列 this._pendingStateQueue 以及更新状态 this._pendingReplaceState 和 this._pendingForceUpdate 来实现 setState 的异步更新机制。

当渲染完成后,若存在 ComponentDidMount,则调用。这就解释了 componentWillMount、render、componentDidMountzz 这三者之间的执行顺序。

其实,mountComponent 本质上是通过递归渲染内容的,由于递归的特性,父组件的 componentWillMount 在其子组件的 componentWillMount 之前调用,而 componentDidMount 在子组件的 componentDidMount 之后调用。

3.阶段二:RECEIVE_PROPS

updateComponent 负责管理生命周期中的 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate。

首先通过 updateComponent 更新组件,如果前后元素不一致,说明需要进行组件更新。

若存在 componentWillReceiveProps,则执行。如果此时在 componentWillReceiveProps 中调用 setState,是不会触发 re-render 的,而是会进行 state 合并。且在 componentWillReceiveProps、shouldComponentUpdate 和 componentWillUpdate 中也还是无法获取到更新后的 this.state,即此时访问的 this.state 仍然是未更新的数据,需要设置 inst.state = nextState 后才可以,因此只有在 render 和 componentDidUpdate 中才能获取到更新后的 this.state。

调用 shouldComponentUpdate 判断是否需要进行组件更新,如果存在 componentWillUpdate 则执行。

updateComponent 本质上也是通过递归渲染内容的,由于递归的特性,父组件的 componentWillUpdate 是其子组件的 componentWillUpdate 之前调用的,componentDidUpdate 在之后调用。

当渲染完成后,若存在 componentDidUpdate,则触发。

禁止在 shouldComponentUpdate 和 componentWillUpdate 中调用 setState,这会造成循环调用,直至耗光浏览器内存后崩溃。

4.阶段三:UNMOUNTING

unmountComponnet 负责管理生命周期中的 componentWillUnmount。

如果存在 componentWillUnmount,则执行并重置所有相关参数、更新队列以及更新状态,如果此时在 componentWillUnmount 中调用 setState,是不会触发 re-render 的,这是因为所有更新队列和更新状态都被重置为 null,并清除了公共类,完成了组件卸载操作。

无状态组件

无状态组件只是一个 render 方法,并没有组件类的实例化过程,也没有实例返回。

无状态组件没有状态,没有生命周期,只是简单接受 props 渲染生成 DOM 结构,是一个纯粹为渲染而生的组件。

setState 异步更新

setState 通过一个队列机制实现 state 更新。当执行 setState 时,会将需要更新的 state 合并后放入状态队列,而不会立刻更新 this.state,队列机制可以高效地批量更新 state。如果不通过 setState 而直接修改 this.state 的值,那么该 state 将不会被放入状态队列中,当下次调用 setState 并对状态队列进行合并时,将会忽略之前直接被修改的 state。

1
2
3
4
5
6
7
8
// 将新的state合并到状态更新队列中
var nextState = this._processPendingState(nextProps, nextContext);

// 根据更新队列和shouldComponentUpdate的状态来判断是否需要更新组件
var shouldUpdate =
this._pendingForceUpate ||
!inst.shouldComponentUpdate ||
inst.shouldComponentUpdate(nextProps, nextState, nextContext);

setState 循环调用风险

当调用 setState 时,实际上会执行 enqueueSetState 方法,并对 partialState 以及_pendingStateQueue 更新队列进行合并操作,最终通过 enqueueUpdate 执行 state 更新。

而 performUpdateIfNecessary 方法会获取_pendingElement、_pendingStateQueue、_pendingForeUpdate,并调用 receiveComponent 和 updateComponent 方法进行组件更新。

如果在 shouldComponentUpdate 或 componentWillUpdate 方法中调用 setState,此时 this._pendingStateQueue != null,则 performUpdateIfNecessary 方法就会调用 updateComponent 方法进行组件更新,但 updateComponent 方法又会调用 shouldComponentUpdate 和 componentWillUpdate 方法,因此造成循环调用。

setState 调用栈

setState 最终是通过 enqueueUpdate 执行 state 更新的,那么 enqueueUpdate 到底是如何更新 state 的呢?
代码如下:

1
2
3
4
5
6
7
8
9
10
11
function enqueueUpdate(component) {
ensureInjected();

// 如果不处于批量更新模式
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 如果处于批量更新模式,则将该组件保存在dirtyComponents中
dirtyComponents.push(component);
}

batchingStrategy 其实只是一个简单对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,

batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;

if (alreadyBatchingUpdates) {
callback(a, b, c, d, e);
} else {
transaction.perform(callback, null, a, b, c, d, e);
}
}
};

初识事务

事务就是将需要执行的方法使用 wrapper 封装起来,再通过事务提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法,执行完 perform 之后(即执行 method 方法吧之后)再执行所有的 close 方法。一组 initialize 及 close 方法称为一个 wrapper,事务支持多个 wrapper 叠加。

到实现上,事务提供了一个 mixin 方法供其他模块实现自己需要的事务。而要使用事务的模块,除了需要把 mixin 混入自己的事务实现中,还要额外实现一个抽象的 getTransactionWrappers 接口。这个接口用来获取所有需要封装的前置方法(initialize)和收尾方法(close),因此它需要返回一个数组的对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var Transaction = require("./Transaction");

// 我们自己定义的事务
var MyTransaction = function() {
// ...
};

Object.assign(MyTransaction.prototype, Transaction.Mixin, {
getTransactionWrappers: function() {
return [
{
initialize: function() {
console.log("before method");
},
close: function() {
console.log("after method");
}
}
];
}
});

var transaction = new MyTransaction();
var testMethod = function() {
console.log("test");
};
transaction.perform(testMethod);
// 打印出
// before method
// test
// after method

???需补充 setState 状态的方法

diff 算法

Redux(基于 v3.5.2 版本)

Redux 是什么

redux 是一个”可预测的状态容器”。

Redux 三大原则

1.单一数据源 2.状态是只读的 3.状态修改均由纯函数完成
没有副作用、可以每次数据变化前后的状态,可以时间旅行调试。

Redux 核心 API

createStore(reducers[,initialState])
在 redux 中,负责响应 action 并修改数据的角色是 reducer。reducer 本质是纯函数。
`reducer(previousState, action) => newState
createStore 创建的 store 是一个对象,包含 4 个方法
1.getState()
2.dispatch(action)
3.subscribe(listener)
4.replaceReducer(nextReducer):更新当前 store 里的 reducer,一般只会在开发模式中调用。

与 React 绑定

react-redux 提供了一个组件(<Provider>)和一个 API(connect())帮助 Redux 和 React 进行绑定。

Redux middleware

middleware 提供了一个分类处理 action 的机会。在 middleware 中,你可以检阅每一个流过的 action,挑选出特定类型的 action 进行相应操作,给你一次改变 action 的机会。

理解 middleware 机制

Redux 提供了 applyMiddleware 方法来加载 middleware:

1
2
3
4
5
6
7
8
9
10
11
// compose.js
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg;
} else {
const last = funcs[funcs.length - 1];
const rest = funcs.slice(0, -1);
return (...args) =>
rest.reduceRight((composed, f) => f(composed), last(...args));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import compose from "./compose";

export default function applyMiddleware(...middlewares) {
return createStore => (reducer, initialState, enhancer) => {
var store = createStore(reducer, initialState, enhancer);
var dispatch = store.dispatch;
var chain = [];

var middlewareAPI = {
getState: store.getState,
dispatch: action => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);

return {
...store,
dispatch
};
};
}

我们实现一个 logger middleware:

1
2
3
4
5
export default store => next => action => {
console.log("dispatch:", action);
next(action);
console.log("finish:", action);
};

接下来,我们分 4 步来深入解析 middleware 的运行原理 1.函数式编程思想设计
middleware 的设计有点特殊,是一个层层包裹的匿名函数,这其实是函数式编程中的 curring,它是一种使用匿名单参数函数来实现多参数函数的方法。applyMiddleware 会对 logger 这个 middleware 进行层层调用,动态地将 store 和 next 的参数赋值。
curring 的 middleware 结构的好处主要有以下两点

  1. 易串联:通过 currying 形成的 middleware 可以累积参数,再配合组合(compose)的方式,很容易形成 pipeling 来处理数据流。
  2. 共享 store: 在 applyMiddleware 执行的过程中,store 还是旧的,但是因为闭包的存在,applyMiddleware 完成后,所有的 middleware 内部拿到的 store 是最新且相同的。

2.给 middleware 分发 store
通过如下方式创建一个普通的 store:

1
let newStore = applyMiddleWare(mid1, mid2, ...)(createStore)(reducer, null);

解读 Redux

createStore 几乎囊括了 Redux 的核心功能。

参数归一化

先看看 createStore 的函数签名:

1
2
3
export default function createStore(reducer, initialState, enhancer) {
// ...
}

前两个参数比较常见,那么 enhancer 扮演了什么角色呢?

1
2
3
4
5
6
7
8
9
10
11
12
if (typeof initialState === "function" && typeof enhancer === "undefined") {
enhancer = initialState;
initialState = undefined;
}

if (typeof enhancer !== "undefined") {
if (typeof enhancer !== "function") {
throw new Error("Expected the enhancer to be a function.");
}

return enhancer(createStore)(reducer, initialState);
}

在 enhancer(createStore)(reducer, initialState)调用中,enhancer 对 createStore 的能力进行了增强,最终返回 store。
典型使用案例是 redux-devtools-extension,它将 Redux DevTools 做成浏览器插件。

初始状态及 getState

完成基本参数校验后,在 createStore 中声明了如下变量及 getState 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var currentReducer = reducer;
var currentState = initialState;
var currentListeners = [];
var nextListeners = currentListeners;
var isDispatching = false;

function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice();
}
}

function getState() {
return currentState;
}
  1. currentReducer:当前的 reducer,支持通过 store.replaceReducer 方式动态替换 reducer,为代码热替换提供了可能。
  2. currentState:应用的当前状态,默认为初始化时的状态。
  3. currentListeners、nextListeners:监听器
  4. isDispatching:某个 action 是否处于分发的处理过程中

getState 方法用于返回当前状态。

subscribe、unsubscribe

在 getState 之后定义了 store 的 subscribe 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function subscribe(listener) {
if (typeof listener !== "function") {
throw new Error("Expected listener to be a function.");
}

var isSubscribed = true;

ensureCanMutateNextListeners();
nextListeners.push(listener);

return function unsubscribe() {
if (!isSubscribed) {
return;
}

isSubscribed = false;

ensureCanMutateNextListeners();
var index = nextListeners.indexOf(listener);
nextListeners.splice(index, 1);
};
}

subscribe 将 listener 添加到 nextListeners 中,unsubscribe 可以删除这个 listener。

dispatch

接下来就是 dispatch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function dispatch(action) {
if (!isPlainObject(action)) {
throw new Error(
"Actions must be plain objects. " +
"Use custom middleware for async actions."
);
}

if (typeof action.type === "undefined") {
throw new Error(
'Actions may not have an undefined "type" property. ' +
"Have you misspelled a constant?"
);
}

if (isDispatching) {
throw new Error("Reducers may not dispatch actions.");
}

try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}

var listeners = (currentListeners = nextListeners);
for (var i = 0; i < listeners.length; i++) {
listeners[i]();
}

return action;
}

先是一些校验,然后将当前的状态和 action 传给当前的 reducer,用于生成最新的 state。再依次调用所有的监听器,通知状态的变更。最后返回 action

replaceReducer

这个方法主要用于 reducer 的热替换,在开发过程中很少使用

1
2
3
4
5
6
7
8
function replaceReducer(nextReducer) {
if (typeof nextReducer !== "function") {
throw new Error("Expected the nextReducer to be a function.");
}

currentReducer = nextReducer;
dispatch({ type: ActionTypes.INIT });
}

dispatch({ type: ActionTypes.INIT })是为了拿到所有 reducer 中的初始状态,只用所有状态都成功获取后,Redux 应用才能有条不紊地运作。

0%