目录
0.前言
本文将结合简化版本的react代码,对react的核心工作流程和原理进行梳理和讲解,本文重点会顺着react的执行流程机制进行原理性的描述,整个简化版的react代码量不多,可以参照代码结合讲解,效果更佳,代码戳这里
1. React
React为一个对象,拥有render、createElement和createClass方法
1.1. React.render
传入虚拟dom及根节点进行挂载渲染
参数:
React.render(element, container)
element是React.createElement和React.createClass返回的元素对象,即虚拟dom(见第1.2.节解析)container是html内容挂载的节点
方法体:
var componentInstance = instantiateReactComponent(element)
var markup = componentInstance.mountComponent(React.nextReactRootIndex++)
instantiateReactComponent(element)
instantiateReactComponent是普通方法,通过判断element虚拟dom的type属性,返回不同虚拟dom类型对应的组件类实例(见第2.节解析),这些组件类实例除了保存虚拟dom对象之外,还拥有组装节点、更新节点等原型方法- 调用
componentInstance.mountComponent实例方法返回组装的html节点,并加以挂载
1.2. React.createElement
传入元素类型,属性和子节点
参数:
createElement(type, config, children)
type为传入对应的不同元素,可以为html默认元素(如’div’)或者自定义元素(class);文本节点也是其中一种type,但由于其没有children和key等config,故不用使用createElement方法生成虚拟dom而直接使用render方法传入文本渲染config为传入元素的属性,包含key,事件或自定义等属性children为传入元素的子节点,可以为createElement生成单个元素或多个元素数组
方法体:
- 处理config,将key单独提取出来,其他属性保存为props
- 处理children,将children保存到
props.children属性
返回:
new ReactElement(type, key, props)
- 参数在上一步已处理好,
ReactElement为虚拟dom类,保存了type,key,props3个实例属性
1.3. React.createClass
返回自定义元素类(CustomClass),该类继承了超类的原型方法,并将自定义原型方法合并入该类原型链
参数:
createClass(classConfig)
- 自定义元素的配置项,可以包含初始化state,时间及render等方法
方法体:
- 定义空的自定义元素类
CustomClass - 继承超类的原型方法,
CustomClass.prototype = new ReactClass(),ReactClass为CustomClass的超类,见下文解释 - 将自定义原型方法合并入该类原型链,
Object.assign(CustomClass.prototype, classConfig)
返回:
CustomClass
关联
超类ReactClass
- 所有自定义组件的超类
- 原型链方法
render,空方法,需要子类自定义 - 原型链方法
setState,调用自定义元素组件类实例的receiveComponent方法,更新自定义元素:
this._reactInternalInstance.receiveComponent(null, newState)
this._reactInternalInstance,自定义元素组件类的实例,自定义元素组件类是react内部的实现,即ReactCompositeComponent,保存了自定义元素类关联的虚拟dom及一系列dom的操作方法;该属性在ReactCompositeComponent实例的mountComponent方法初始化,见2.3.节
注意区别自定义元素类和自定义元素组件类,前者是我们自己写的自定义元素的那个class,后者是react内部的ReactComponent类,包含自定义元素类实例,具有组装节点和更新等方法
2. ReactComponent
ReactComponent是不同元素/虚拟dom对应的元素组件类,元素组件类保存虚拟dom等数据,以及拥有组装节点,更新等一系列操作dom的方法
2.1. ReactDOMTextComponent
纯文本元素组件类
实例属性
this._currentElement = text,保存当前文本this._rootNodeID,根节点id,在调用mountComponent方法的时候传入并保存
原型方法
mountComponent(rootID),传入根节点id,组装节点,返回html字符串receiveComponent(nextText),传入新文本节点,与旧节点对比,不同则更新dom节点内容
2.2. ReactDOMComponent
html默认元素组件类
实例属性
this._currentElement = element,保存当前虚拟dom节点(见1.2.节)this._rootNodeID,根节点id,在调用mountComponent方法的时候传入并保存this._renderedChildren,保存子节点元素类实例,对应createElement传入的第3个参数和虚拟dom的props.children属性(见1.2.节),用于后续的节点更新
原型方法
1.
mountComponent(rootID)
1.1. 根据保存的虚拟domthis._currentElement的type和props组装本节点的html
1.2. 根据this._currentElement的props.children,生成每个children的实例并执行mountComponent方法组装节点(该方法必要时会递归子节点mountComponent方法,见本文末尾流程图紫色线)2.
receiveComponent(nextElement)
传入新节点的虚拟domnextElement
2.1.this._updateDOMProperties(lastProps, nextProps),处理当前节点的属性变更
2.2.this._updateDOMChildren(nextElement.props.children),处理当前节点的子节点变更3.
_updateDOMProperties(lastProps, nextProps)
新老属性比对,更新当前节点的属性4.
_updateDOMChildren(nextChildrenElements)
处理子节点的变更
4.1.this._diff(diffQueue, nextChildrenElements),diff算法,递归找出差别,组装差异对象,添加到全局更新队列diffQueue
4.2.this._patch(diffQueue),patch算法,在合适的时机调用(一轮更新的递归调用完毕,具体代码使用全局计数器updateDepth来标记,具体可见仓库代码),根据diffQueue执行具体的dom操作5.
_diff(diffQueue, nextChildrenElements)
diff算法,递归找出差别,组装差异对象,添加到全局更新队列diffQueue
5.1.var prevChildren = flattenChildren(self._renderedChildren)
使用flattenChildren方法将_renderedChildren子节点元素组件类实例数组转化为一个映射,映射的键为子节点的key,若不存在则直接只用子节点的index作为键
5.2.nextChildren = generateComponentChildren(prevChildren, nextChildrenElements)
传入旧子节点映射及新节点子节点虚拟dom集合数组,返回新子节点元素组件类实例映射
5.2.1.遍历nextChildrenElements数组,取子节点nextElement虚拟dom的key或者index作为键值key
5.2.2.用上一步的key取出prevChildren子节点实例对用对应的虚拟domprevElement,使用_shouldUpdateReactComponent(prevElement, nextElement)判断子节点是需要更新还是直接替换(该方法见3.1.节)
5.2.3.需要更新的话则使用prevChild.receiveComponent(nextElement)更新(注意该方法可能会进行递归更新,见本文末尾流程图的绿色线),nextChildren的值仍然为prevChild ;不需要更新则使用instantiateReactComponent(nextElement)生成一个新子节点元素类的实例,并作为nextChildren的值
5.2.4.返回nextChildren映射对象
5.3.diff算法核心
5.3.1. 维护两个计数变量
lastIndex:代表访问的旧子节点集合最大index,该变量会随着遍历新子节点对应的旧子节点的最大index而变更
nextIndex:代表到达的新子节点的index,该变量会随着遍历新子节点而递增
5.3.2. 遍历nextChildren映射对象,通过key值获取对应的新子节点nextChild,从prevChildren取出旧子节点prevChild
5.3.3. 对比prevChild和nextChild
若相等,再看看prevChild._mountIndex < lastIndex是否成立,若成立,说明最新的nextChild对应的旧子节点在原来已经遍历过的旧子节点之前,但由于nextChildren是按顺序遍历的,所以新的nextChild应该是要在原来遍历过的旧子节点之后的,,也就是需要移动,所以prevChild._mountIndex < lastIndex成立的话,就把当前需要移动的位置和移动到哪个位置(index)记录下来,操作类型记为UPDATE_TYPES.MOVE_EXISTING,将该记录的对象push进diffQueue;同时更新lastIndex = Math.max(prevChild._mountIndex, lastIndex)
若不相等,则说明nextChild是新节点,说明需要新插入,使用nextChild.mountComponent方法组装html,并标注插入的位置(index),操作类型记为UPDATE_TYPES.INSERT_MARKUP,将该记录的对象push进diffQueue;同时再根据同名key获取prevChild,如果存在的话说明旧节点已经被新节点取代,需要删除,标注起始位置(index),操作类型记为UPDATE_TYPES.REMOVE_NODE,将该记录的对象push进diffQueue,同时更新lastIndex = Math.max(prevChild._mountIndex, lastIndex)
每个循环完毕执行nextIndex++
5.3.4.5.3.2-5.3.3只是处理了新子节点的新增和移动,还需要处理旧节点的删除。遍历prevChildren映射对象,通过key值获取对应的旧子节点并判断旧子节点是否还在新子节点对象nextChildren之中,若不存在说明需要删除,标注起始位置(index),操作类型记为UPDATE_TYPES.REMOVE_NODE,将该记录的对象push进diffQueue
diff核心算法(5.3.节)伪代码(若想细致了解diff算法可以戳这里)
lastIndex=0
nextIndex=0
do
if prevChild === nextChild
prevChild._mountIndex < lastIndex &&
diffQueue.push(MOVE_EXISTING)
lastIndex = Math.max(prevChild._
mountIndex, lastIndex)
else
if prevChild
diffQueue.push(REMOVE_
NODE);
lastIndex = Math.max(prevChild._
mountIndex, lastIndex)
end
diffQueue.push(INSERT_MARKUP)
end
nextChild._mountIndex = nextIndex
nextIndex++
while nextChild
//
do
if !nextChildren[prevChildren.name]
diffQueue.push(REMOVE_NODE)
end
while preChild
- 6._patch(diffQueue)
根据diff算法已经计算出的差异化队列diffQueue,根据每个不同的类型:UPDATE_TYPES.MOVE_EXISTING、UPDATE_TYPES.INSERT_MARKUP和UPDATE_TYPES.REMOVE_NODE,执行具体的dom操作
6.1. 遍历diffQueue,把需要移动(UPDATE_TYPES.REMOVE_NODE)和删除(UPDATE_TYPES.MOVE_EXISTING)的类型节点先找出来,统一放到deleteChildren.push(updatedChild),然后批量删除(执行dom操作):
$.each(deleteChildren, function(index, child) { $(child).remove(); });
6.2再次遍历diffQueue,处理新增和修改的节点,使用insertChildAt(parentNode, childNode, index)方法统一处理,传入的参数分别为父节点、子节点和需要移动的位置
普通方法
flattenChildren:见5.1.节generateComponentChildren:见5.2.节insertChildAt:见6.2节
2.3. ReactCompositeComponent
自定义元素组件类
实例属性
this._currentElement = element,保存当前虚拟dom节点(见1.2.节)this._rootNodeID,根节点id,在调用mountComponent方法的时候传入并保存this._instance,自定义元素类的实例,自定义元素类为传入createElement的第一个参数,继承ReactClass的子类,是我们可以书写的class;在调用mountComponent方法的时候保存(实际上应该为this._reactInternalInstance,自定义元素组件类的实例,自定义元素组件类是react内部的实现,即ReactCompositeComponent,保存了自定义元素类关联的虚拟dom及一系列dom的操作方法(注意区分自定义元素类和自定义元素组件类)this._instance._reactInternalInstance,因为不是该类的实例属性,故删除,可见1.3.ReactClass)this._renderedComponent,渲染的组件类的实例,即通过this._instance.render()返回元素对应的元素组件类的实例,是我们在class的渲染函数内自定义的渲染,可以是new ReactDOMTextComponent,new ReactDOMComponent()和new ReactCompositeComponent任意一种;该实例是自定义元素组件类真实渲染的组件实例
原型方法
1.
mountComponent(rootID)
1.1.实例化自定义元素类
var ReactClass = this._currentElement.type;
var inst = new ReactClass(publicProps);
this._instance = inst;
取出虚拟dom里的type,对应用户自定义的元素class,并进行实例化
1.2.调用自定义元素类实例inst.componentWillMount()生命周期(若有)
1.3.组装节点
调用this._instance.render()返回class的渲染虚拟dom,再调用instantiateReactComponent返回渲染的实例,最后使用renderedComponentInstance.mountComponent(this._rootNodeID)返回组装的节点html
1.4.调用自定义元素类实例inst.componentDidMount()生命周期(若有)2.
receiveComponent(nextElement, newState)
自定义元素组件类的更新方法,接收新节点对应的虚拟dom以及新state
2.1.更新this._currentElement = nextElement属性,合并statenextState = Object.assign(inst.state, newState)并更新this._instance.state = nextState,获取新propsnextProps = this._currentElement.props
2.2.调用自定义元素类实例inst.componentWillUpdate(nextProps, nextState)生命周期(若有)
2.3.重新调用this._instance.render()返回class新的渲染虚拟domnextRenderedElement,调用_shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)判断节点是需要更新还是直接替换(该方法见3.1.节)
若需要更新,则调用prevComponentInstance.receiveComponent(nextRenderedElement)更新(注意该方法可能会进行递归更新,见本文末尾流程图的绿色线),同时调用自定义元素类实例inst.componentDidUpdate()生命周期(若有)
若直接替换,则生成nextRenderedElement对应的元素组件实例同时调用mountComponent方法重新组装,并替换整个节点:
$('[data-reactid="' + this._rootNodeID + '"]').replaceWith(nextMarkup)
3. 其他方法
3.1 _shouldUpdateReactComponent
比较节点是否可以进行更新,用于新旧节点对比,以决定接下来是只更新还是替换
- 参数
_shouldUpdateReactComponent(prevElement, nextElement)
分别传入新旧节点的虚拟dom - 方法体
3.1.1. 比较是否均为文本节点,是的话返回true
3.1.2. 比较是否为虚拟dom节点,若是的话比较虚拟dom的type和key是否相同,是的话返回true,否的话返回false
4. 再谈diff算法
diff算法本质就是在比较两颗dom树,传统 diff 算法的复杂度为 O(n^3),react对该算法做了几个预设和优化,将复杂度降低为O(n)
tree diff
Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。对树进行分层比较,两棵树只会对同一层次的节点进行比较。该策略对应2.2.节_updateDOMChildren方法
component diff
如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。该策略对应3.1.节_shouldUpdateReactComponent方法element diff
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。该策略对应5.3.节diff核心算法。
此算法依赖于每个节点的唯一key,即key是每个节点的唯一标识,推荐固定的key值,不指定key则使用index作为key,在这种情况下,key会随着节点的位置改变,在某些情况下会造成大的渲染消耗或者不可复用(例如在子节点头部插入一个全新类型的子节点)。
关于diff算法的详细解释可参考这篇文章
5. 流程图
流程图是根据源代码绘制的,所以配合源代码和本文文字解读观看效果更佳,仓库链接
参考文章:
React 源码分析
React 源码剖析系列 - 不可思议的 react diff
reactjs源码分析-上篇(首次渲染实现原理)
reactjs源码分析-下篇(更新机制实现原理
属于自己的文字,理解,观点,欢迎交流