词汇表
smart component, dumb component
- dumb component。接收prop,只负责组件展示,也叫展示型组件
- smart component。管理state和生命周期,把数据传给dumb component。也叫容器型组件
dynamic children
组件内的子组件通过动态计算得到。
render() {
return (<div>{React.Children.map(this.props.children, (child) => {...})}</div>);
}
无状态组件
- 使用无状态函数构建的组件
- 本身只接收props和context,没有state,没有生命周期
- propTypes和defaultProps可以通过设置静态属性定义
- 创建时始终保持一个实例,避免了不必要的检查和内存分配
Q:如何保持一个实例?props更新怎么处理的
SytheticEvent 合成事件
- React基于Virtual DOM实现了合成事件层,我们定义的事件处理器会收到一个合成事件对象实例
- 符合W3C标准,支持事件冒泡,可以使用stopPropagation和preventDefault
- 可使用nativeEvent访问原生DOM事件
- 事件委派。所有事件绑定到最外层,使用统一的事件监听器,它维持了事件和处理函数 的映射
- 每个方法上下文会指向该组件实例,ES6 class和纯函数这种自动绑定不存在
受控组件和非受控组件
区分:value是否受react控制
HOC
两种实现方式:
- 属性代理
import React, { Component } from 'React';
const MyContainer = (WrappedComponent) => class extends Component {
render() {
return <WrappedComponent {...this.props} />;
}
}
- 反向继承
const MyContainer = (WrappedComponent) => class extends WrappedComponent {
render() {
return super.render();
}
}
调用顺序的区别:属性代理先是被包裹组件unmout,反向继承是先HOC unmout
React源码解析
源码结构
源码版本15.0。src下的文件夹:addons、isomorphic、renders、shared、core、test:
- addons。一系列工具方法
- ismorphic。同构方法
- shared。公用方法。Transaction等
- test。测试类
- core/tests。一些边界错误的测试用例
- renderers。核心功能,包含大部分功能实现。
- dom
- client。包含DOM操作方法(findDOMNode、setInnerHTML等),以及事件方法,主要是非底层的实用性事件方法,如事件监听(ReactEventListener)、常用方法(TapEventPlugin、EnterLeaveEventPlugin) 以及一些合成事件
- server。服务端渲染的实现和方法
- shared。文本组件(ReactDOMTextComponent)、标签组件(ReactDOMComponent)、DOM属性操作、CSS属性操作
- shared
- event。通用事件的插件系统代码,包含更为底层的事件方法。例如插件中心、事件注册、事件传播等通用方法。
-
reconciler。协调器,最核心的部分。包含自定义组件的实现(ReactCompositeComponent)、DOM diff算法(ReactMultiChild)。
react插件中心.png
- dom
Virtual DOM
Virtual DOM是浏览器端用JavaScript实现的一套DOM API。包含一整套Virtual DOM模型、生命周期的维护和管理、diff算法、将Virtual DOM展示为原生DOM的patch方法。
Virtual DOM模型
Virtual DOM模型的基本元素:
- 标签名
- 节点属性。包含样式、属性、事件等
- 子节点
- 标识id
样例:
{
tagName: "div", // 标签名
properties: { // 属性
style: {}, // 样式
},
children: [], // 子节点
key: 1, // 唯一标识
}
Virtual DOM中的节点称为ReactNode,分为三种:
- ReactElement
- ReactFragment
- ReactText
type ReactNode = ReactElement | ReactFragment | ReactText;
type ReactElement = ReactComponentElement | ReactDOMElement;
type ReactDOMElement = {
type: string,
props: {
children: ReactNodeList,
className: string,
...
},
key: string | number | boolean | null,
ref: string | null,
};
type ReactComponentElement<TProps> = {
type: ReactClass<TProps>,
props: Tprops,
key: string | number | boolean | null,
ref: string | null,
};
type ReactFragment = Array<ReactNode | ReactEmpty>;
type ReactNodeList = ReactNode | ReactEmpty;
type ReactText = string | number;
type ReactEmpty = boolean | null | undefined;
React元素的创建
使用JSX创建的元素最终会被编译成调用React.createElement方法,例如:
// 输入
const app = <Nav color="blue"><Profile>text</Profile></Nav>;
// 输出
const app = React.createElement(
Nav,
{ color: "blue" },
React.createElement(Profile, null, "click"),
);
React.createElement代码逻辑:
- 初始化参数
- 第二参数里的内容赋值给props
- 后续参数全部赋值给props.children
- 对确少的props如果有默认的,设置默认值
初始化组件的入口
使用React创建组件的实例的时候,首先会调用instantiateReactComponent,它通过node类型区分不同的组件的入口。逻辑如下:
- node为空时,初始化空组件ReactEmptyComponent.create(instantiateReactComponent)
- node类型为object时,如果elemtn.type是字符串,初始化DOM标签组件 ReactNativeComponent.createInternalComponent(elemnt),否则初始化自定义组件new ReactCompositeComponentWrapper(elemnt)。
- node类型为字符串或数字时,初始化文本组件ReactnativeComponent.createInstanceForText(node)
- 其他情况不处理
文本组件
node类型为文本节点时,不算Virtual DOM元素,但React为了保持渲染的一致性,将其封装为文本组件ReactDOMTextComponent。
- 在moutnComponent阶段,判断是否是通过transaction.useCreateElement创建判断是否是通过createElement方法创建,如果是创建相应的标签和domID,这样每个文本节点一样拥有自己的标识,也有Virtual DOM Diff的权利;否则,直接返回文本内容
不再为裸露的文本内容包裹span是React15的更新点 - 在receiveComponent阶段,通过DOMChildrenOperations.replaceDelimitedText来更新文本内容
DOM标签组件
ReactDOMComponent对Virtual DOM的处理包含两个部分:
- 更新属性。包括样式、更新属性、处理事件
- mountComponent时,ReactDOMComponent先生成标记和标签,通过this._createOpenTagAndPutListeners(tranaction)来处理DOM节点的属性和事件:
- 如果存在事件,调用enqueuePutListener(this, propKey, propValue, transaction)添加事件代理。
- 如果存在样式,合并然后通过CSSPropertyOperations.createMarkupForStyles(propValue, this)创建样式
- 通过DOMPropertyOperations.createMarkupForProperty(propKey, propValue)创建属性
- 通过DOMPropertyOperations.createMarkupForID(this.domID)创建唯一标识
- receiveComponent时,通过this.updateComponent来更新DOM节点属性。先删除旧的不要的属性,再更新属性
- mountComponent时,ReactDOMComponent先生成标记和标签,通过this._createOpenTagAndPutListeners(tranaction)来处理DOM节点的属性和事件:
- 更新子节点。包括更新内容、更新子节点,此部分涉及diff算法
- mountComponent。通过this._createContentMarkup(transaction, props, context)来处理DOM节点内容,如果有子节点,则递归调用mountComponent渲染
- receiveComponent。通过this._updateDOMChildren(lastProps, nextProps, transaction, context)更新内容,先删除不需要的子节点和内容,再更新子节点和内容,分别调用this.updateChildren和this.updateTextContent。updateChildre为diff内容
自定义组件
自定义组件实现了一套生命周期和setState机制
生命周期顺序
生命周期执行顺序:
- 首次挂载组件,按顺序执行getDefaultProps、getInitialState、componentWillMount、render、componentDidMount
- 卸载组件,执行componentWillUnmount
- 重新挂载组件,和首次挂载相比,不再执行getDefaultProps
- 组件更新,componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render、componentDidUpdate
生命周期主要通过三个阶段进行管理:MOUGTING、RECEIVE_PROPS、UNMOUTING
createClass创建自定义组件
该方法是创建自定义组件的入口,管理getDefaultProps生命周期,该方法只执行一次,所有实例共享默认的props。执行步骤:
- 初始化,按顺序合并mixin
- 执行getDefaultProps
- 从原型上删除未定义的生命周期钩子函数
MOUNTING
执行逻辑:
- 初始化公共的props和contexts
- 判断是否是无状态组件并初始化
- 将实例存储为引用,放在全局的一个map里
- 初始化state
- 初始化更新队列
- componentWillMount,此时setState不会rerender,会合并state inst.state = this._processPendingState(inst.props, inst.state)在componentWillMount后执行,所以该生命周期内的state不是最新的。React利用更新队列this._pendingStateQueue、更新状态this._pendingReplaceState、this.pendingForceUpdate来实现setState异步更新
- render,render之后会diff,render执行后会有key比较
- 递归渲染
- componentDidMount
RECEIVE_PROPS
执行逻辑:
- 分布判断context和props是否改变,改变了则处理,并标记willReceive为true
- 如果willReceive为true,并且存在componentWillReceiveProps则执行
- 将新的state合并到更新队列
- 判断是否需要update,此时也会执行shouldComponentUpdate。如果不需要更新,则设置state和props,如果需要更新,则执行更新
- 如果存在componentDidUpdate暂存一份旧的state,props,context作为后续的参数传递
- 执行componentWillUpdate
- render,render之后会diff,render执行后会有key比较
- componentDidUpdate
render执行前的生命周期,实例的state不是最新的,设置了inst.state = nextState之后才是
UNMOUNTING
此时会执行componentWillUnmount,调用setState不会rerender,因为所有更新队列和更新状态都是null
无状态组件
无状态组件只是一个render方法,没有组件类实例化过程也没有实例返回
setState机制
异步更新
state通过队列更新,setState会将更新放入到队列,直接对state赋值不会放入更新队列,下次对队列状态合并时,会忽略之前被修改的
循环调用风险
队列的一次更新会获取_pendingElement、_pengdingStateQueue、_pendingForceUpdate;如果在shouldCompoentUpdate和componentWillUpdate里调用setState,此时this.pendingStateQueue != null,则permUpdateIfNessary会调用updateComponent,然后再调用这两个生命周期,造成循环调用
// 如果在 _pendingElement、_pendingStateQueue和_pendingForceUpdate更新组件 performUpdateIfNecessary: function(transaction) {
if (this._pendingElement != null) {
ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
}
if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
}
}
调用栈
setState->newState存入pending队列 -> 调用enqueueUpdate,-> 判断是否批量更新,是则将组件保存到dirtyComponents,否,遍历dirtyComponents,updateComponent,更新pending state or props
setState是最终调用的enqueUpdate,然后执行batchingStrategy.batchedUpdates,该方法是ReactDefualtbatchingStrategy(注入上去的)的batchedUpdates方法,调用了transaction.perfomr,这个transaction也是注入上去的,属于事务部分提供的功能
事务
事务的大致逻辑:
- 将需要执行的方法用wrapper封装,在通过perform方法执行,先执行所有wrapper的initialize,然后perform,然后close,一个wrapper定义了一组initialize和close,事务支持多个wrapper叠加
- 事务提供一个mixin方法供其他模块实现自己需要的事务,这个模块还需要额外实现getTransactionWrapper,来获取所有封装的initialize和close
diff算法
传统的通过循环递归节点依次进行比较的diff算法,算法复杂度高达o(n3)。
React将Virtual DOM树转换成acturl树的最少操作的过程叫做调和(reconciliation)。
diff策略
- Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计
- 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
- 对于同一层级的一组子节点他们可以通过唯一的ID进行区分
React基于以上策略分别对tree diff、component diff、elment diff进行算法优化
tree diff
两棵树只会对同一层级进行比较,当出现跨层级移动时,会销毁节点创建新的
component diff
- 同类型组件,按照原策略比较Virtual DOM树
- 如果不是,将该组件设置为dirty component,从而替换整个组件
- 同类型组件,允许使用shouldComponentUpdate来判断组件是否需要进行diff算法分析
element diff
当节点处于同一层级的时候,diff提供了3中操作INSERT_MARKUP(插入)、MOVE_EXISTING(插入)、MOVE_NODE(删除)。
对同一层级节点,可以添加唯一key进行区分。
过程:
对新集合中的节点进行循环遍历,通过唯一的key判断集合中是否存在相同的节点,存在则进行移动操作,移动之前需要将当前节点在旧集合中的位置与lastIndex进行比较,如果小于则不移动
应该尽量减少最后一个元素移动到第一个的操作
React Patch
将只算出的节点反映在真实的DOM树上,源码位置:DOMChildrenOperations.js。会顺序遍历差异队列,根据更新类型,执行对应的操作
其他
熟悉事件系统代码
空组件初始化ReactEmptyComponent.create(instantiateReactComponent)里的参数是什么作用?
ReactDOMTextComponent初始化的时候创建的openingCommoent的作用?长什么样子?
react组件什么时候重新挂载
forceUpdate运行流程?