目录
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
,props
3个实例属性
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源码分析-下篇(更新机制实现原理
属于自己的文字,理解,观点,欢迎交流