从头实现一个简易版React(二)地址:https://segmentfault.com/a/11...
在上一节,咱们的react已经具有了渲染功能。
在这一节咱们将着重实现它的更新,说到更新,你们可能都会想到React的diff算法,它能够说是React性能高效的保证,同时也是最神秘,最难理解的部分(我的以为),想当初我也是看了好多文章,敲了N次代码,调试了几十遍,才总算理解了它的大概。在这也算是把个人理解阐述出来。javascript
一样,咱们会实现三种ReactComponent的update方法。不过在这以前,咱们先想一想,该如何触发React的更新呢?没错,就是setState方法。html
// 全部自定义组件的父类 class Component { constructor(props) { this.props = props } setState(newState) { this._reactInternalInstance.updateComponent(null, newState) } } //代码地址:src/react/Component.js
这里的reactInternalInstance就是咱们在渲染ReactCompositeComponent时保存下的自身的实例,经过它调用了ReactCompositeComponent的update方法,接下来,咱们就先实现这个update方法。java
这里的update方法同mount有点相似,都是调用生命周期和render方法,先上代码:react
class ReactCompositeComponent extends ReactComponent { constructor(element) { super(element) // 存放对应的组件实例 this._instance = null this._renderedComponent = null } mountComponent(rootId) { //内容略 } // 更新 updateComponent(nextVDom, newState) { // 若是有新的vDom,就使用新的 this._vDom = nextVDom || this._vDom const inst = this._instance // 获取新的state,props const nextState = { ...inst.state, ...newState } const nextProps = this._vDom.props // 判断shouldComponentUpdate if (inst.shouldComponentUpdate && (inst.shouldComponentUpdate(nextProps, nextState) === false)) return inst.componentWillUpdate && inst.componentWillUpdate(nextProps, nextState) // 更改state,props inst.state = nextState inst.props = nextProps const prevComponent = this._renderedComponent // 获取render新旧的vDom const prevRenderVDom = prevComponent._vDom const nextRenderVDom = inst.render() // 判断是须要更新仍是从新渲染 if (shouldUpdateReactComponent(prevRenderVDom, nextRenderVDom)) { // 更新 prevComponent.updateComponent(nextRenderVDom) inst.componentDidUpdate && inst.componentDidUpdate() } else { // 从新渲染 this._renderedComponent = instantiateReactComponent(nextRenderVDom) // 从新生成对应的元素内容 const nextMarkUp = this._renderedComponent.mountComponent(this._rootNodeId) // 替换整个节点 $(`[data-reactid="${this._rootNodeId}"]`).replaceWith(nextMarkUp) } } } //代码地址:src/react/component/ReactCompositeComponent.js
有两点要说明:git
// 判断是更新仍是渲染 function shouldUpdateReactComponent(prevVDom, nextVDom) { if (prevVDom != null && nextVDom != null) { const prevType = typeof prevVDom const nextType = typeof nextVDom if (prevType === 'string' || prevType === 'number') { return nextType === 'string' || nextType === 'number' } else { return nextType === 'object' && prevVDom.type === nextVDom.type && prevVDom.key === nextVDom.key } } } //代码地址:src/react/component/util.js
注意,这里咱们使用到了key,当type相同时使用key能够快速准确得出两个vDom是否相同,这是为何React要求咱们在循环渲染时必须添加key这个props。es6
ReactTextComponent的update方法很是简单,判断新旧文本是否相同,不一样则更新内容,直接贴代码:github
class ReactTextComponent extends ReactComponent { mountComponent(rootId) { //省略 } // 更新 updateComponent(nextVDom) { const nextText = '' + nextVDom if (nextText !== this._vDom) { this._vDom = nextText } // 替换整个节点 $(`[data-reactid="${this._rootNodeId}"]`).html(this._vDom) } // 代码地址:src/react/component/ReactTextComponent.js }
ReactDomComponent的update最复杂,能够说diff的核心都在这里,本文的重心也就放在这。
整个update分为两块,props的更新和children的更新。算法
class ReactDomComponent extends ReactComponent { mountComponent(rootId) { //省略 } // 更新 updateComponent(nextVDom) { const lastProps = this._vDom.props const nextProps = nextVDom.props this._vDom = nextVDom // 更新属性 this._updateDOMProperties(lastProps, nextProps) // 再更新子节点 this._updateDOMChildren(nextVDom.props.children) } // 代码地址:src/react/component/ReactDomComponent.js }
props的更新很是简单,无非就是遍历新旧props,删除不在新props里的老props,添加不在老props里的新props,更新新旧都有的props,事件特殊处理。segmentfault
_updateDOMProperties(lastProps, nextProps) { let propKey = '' // 遍历,删除已不在新属性集合里的老属性 for (propKey in lastProps) { // 属性在原型上或者新属性里有,直接跳过 if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey)) { continue } // 对于事件等特殊属性,须要单独处理 if (/^on[A-Za-z]/.test(propKey)) { const eventType = propKey.replace('on', '') // 针对当前的节点取消事件代理 $(document).undelegate(`[data-reactid="${this._rootNodeId}"]`, eventType, lastProps[propKey]) continue } } // 对于新的属性,须要写到dom节点上 for (propKey in nextProps) { // 更新事件属性 if (/^on[A-Za-z]/.test(propKey)) { var eventType = propKey.replace('on', '') // 之前若是已经有,须要先去掉 lastProps[propKey] && $(document).undelegate(`[data-reactid="${this._rootNodeId}"]`, eventType, lastProps[propKey]) // 针对当前的节点添加事件代理 $(document).delegate(`[data-reactid="${this._rootNodeId}"]`, `${eventType}.${this._rootNodeId}`, nextProps[propKey]) continue } if (propKey === 'children') continue // 更新普通属性 $(`[data-reactid="${this._rootNodeId}"]`).prop(propKey, nextProps[propKey]) } } // 代码地址:src/react/component/ReactDomComponent.js
children的更新则相对复杂了不少,陈屹老师的《深刻React技术栈》中提到,diff算法分为3块,分别是数组
上文中的shouldUpdateReactComponent就属于component diff,接下来,让咱们依据这三种diff实现updateChildren。
// 全局的更新深度标识,用来断定触发patch的时机 let updateDepth = 0 // 全局的更新队列 let diffQueue = [] _updateDOMChildren(nextChildVDoms) { updateDepth++ // diff用来递归查找差别,组装差别对象,并添加到diffQueue中 this._diff(diffQueue, nextChildVDoms) updateDepth-- if (updateDepth === 0) { // 具体的dom渲染 this._patch(diffQueue) diffQueue = [] }
这里经过updateDepth对vDom树进行层级控制,只会对相同层级的DOM节点进行比较,只有当一棵DOM树所有遍历完,才会调用patch处理差别。也就是所谓的tree diff。
确保了同层次后,咱们要实现_diff方法。
已经渲染过的子ReactComponents在这里是数组,咱们要遍历出里面的vDom进行比较,这里就牵扯到上文中的key,在有key时,咱们优先用key来获取vDom,因此,咱们首先遍历数组,将其转为map(这里先用object代替,之后会更改为es6的map),若是有key值的,就用key值做标识,无key的,就用index。
下面是array到map的代码:
// 将children数组转化为map export function arrayToMap(array) { array = array || [] const childMap = {} array.forEach((item, index) => { const name = item && item._vDom && item._vDom.key ? item._vDom.key : index.toString(36) childMap[name] = item }) return childMap }
部分diff方法:
// 将以前子节点的component数组转化为map const prevChildComponents = arrayToMap(this._renderedChildComponents) // 生成新的子节点的component对象集合 const nextChildComponents = generateComponentsMap(prevChildComponents, nextChildVDoms)
将ReactComponent数组转化为map后,用老的ReactComponents集合和新vDoms数组生成新的ReactComponents集合,这里会使用shouldUpdateReactComponent进行component diff,若是相同,则直接更新便可,反之,就从新生成ReactComponent
/** * 用来生成子节点的component * 若是是更新,就会继续使用之前的component,调用对应的updateComponent * 若是是新的节点,就会从新生成一个新的componentInstance */ function generateComponentsMap(prevChildComponents, nextChildVDoms = []) { const nextChildComponents = {} nextChildVDoms.forEach((item, index) => { const name = item.key ? item.key : index.toString(36) const prevChildComponent = prevChildComponents && prevChildComponents[name] const prevVdom = prevChildComponent && prevChildComponent._vDom const nextVdom = item // 判断是更新仍是从新渲染 if (shouldUpdateReactComponent(prevVdom, nextVdom)) { // 更新的话直接递归调用子节点的updateComponent prevChildComponent.updateComponent(nextVdom) nextChildComponents[name] = prevChildComponent } else { // 从新渲染的话从新生成component const nextChildComponent = instantiateReactComponent(nextVdom) nextChildComponents[name] = nextChildComponent } }) return nextChildComponents }
经历了以上两步,咱们已经得到了新旧同层级的ReactComponents集合。须要作的,只是遍历这两个集合,进行比较,同属性的更新同样,进行移动,新增,和删除,固然,在这个过程当中,我会包含咱们的第三种优化,element diff。它的策略是这样的:首先对新集合的节点进行循环遍历,经过惟一标识能够判断新老集合中是否存在相同的节点,若是存在相同节点,则进行移动操做,但在移动前须要将当前节点在老集合中的位置与 lastIndex 进行比较,if (prevChildComponent._mountIndex < lastIndex),则进行节点移动操做,不然不执行该操做。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),若是新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其余节点的位置,所以不用添加到差别队列中,即不执行移动操做,只有当访问的节点比 lastIndex 小时,才须要进行移动操做。
上完整的diff方法代码:
// 差别更新的几种类型 const UPDATE_TYPES = { MOVE_EXISTING: 1, REMOVE_NODE: 2, INSERT_MARKUP: 3 } // 追踪差别 _diff(diffQueue, nextChildVDoms) { // 将以前子节点的component数组转化为map const prevChildComponents = arrayToMap(this._renderedChildComponents) // 生成新的子节点的component对象集合 const nextChildComponents = generateComponentsMap(prevChildComponents, nextChildVDoms) // 从新复制_renderChildComponents this._renderedChildComponents = [] for (let name in nextChildComponents) { nextChildComponents.hasOwnProperty(name) && this._renderedChildComponents.push(nextChildComponents[name]) } let lastIndex = 0 // 表明访问的最后一次老的集合位置 let nextIndex = 0 // 表明到达的新的节点的index // 经过对比两个集合的差别,将差别节点添加到队列中 for (let name in nextChildComponents) { if (!nextChildComponents.hasOwnProperty(name)) continue const prevChildComponent = prevChildComponents && prevChildComponents[name] const nextChildComponent = nextChildComponents[name] // 相同的话,说明是使用的同一个component,须要移动 if (prevChildComponent === nextChildComponent) { // 添加差别对象,类型:MOVE_EXISTING prevChildComponent._mountIndex < lastIndex && diffQueue.push({ parentId: this._rootNodeId, parentNode: $(`[data-reactid="${this._rootNodeId}"]`), type: UPDATE_TYPES.MOVE_EXISTING, fromIndex: prevChildComponent._mountIndex, toIndex: nextIndex }) lastIndex = Math.max(prevChildComponent._mountIndex, lastIndex) } else { // 若是不相同,说明是新增的节点 // 若是老的component在,须要把老的component删除 if (prevChildComponent) { diffQueue.push({ parentId: this._rootNodeId, parentNode: $(`[data-reactid="${this._rootNodeId}"]`), type: UPDATE_TYPES.REMOVE_NODE, fromIndex: prevChildComponent._mountIndex, toIndex: null }) // 去掉事件监听 if (prevChildComponent._rootNodeId) { $(document).undelegate(`.${prevChildComponent._rootNodeId}`) } lastIndex = Math.max(prevChildComponent._mountIndex, lastIndex) } // 新增长的节点 diffQueue.push({ parentId: this._rootNodeId, parentNode: $(`[data-reactid="${this._rootNodeId}"]`), type: UPDATE_TYPES.INSERT_MARKUP, fromIndex: null, toIndex: nextIndex, markup: nextChildComponent.mountComponent(`${this._rootNodeId}.${name}`) }) } // 更新_mountIndex nextChildComponent._mountIndex = nextIndex nextIndex++ } // 对于老的节点里有,新的节点里没有的,所有删除 for (let name in prevChildComponents) { const prevChildComponent = prevChildComponents[name] if (prevChildComponents.hasOwnProperty(name) && !(nextChildComponents && nextChildComponents.hasOwnProperty(name))) { diffQueue.push({ parentId: this._rootNodeId, parentNode: $(`[data-reactid="${this._rootNodeId}"]`), type: UPDATE_TYPES.REMOVE_NODE, fromIndex: prevChildComponent._mountIndex, toIndex: null }) // 若是渲染过,去掉事件监听 if (prevChildComponent._rootNodeId) { $(document).undelegate(`.${prevChildComponent._rootNodeId}`) } } } } // 代码地址:src/react/component/ReactDomCompoent.js
调用diff方法后,会回到tree diff那一步,当一整棵树遍历完后,就须要经过Patch将更新的内容渲染出来了,patch方法相对比较简单,因为咱们把更新的内容都放入了diffQueue中,只要遍历这个数组,根据不一样的类型进行相应的操做就行。
// 渲染 _patch(updates) { // 处理移动和删除的 updates.forEach(({ type, fromIndex, toIndex, parentNode, parentId, markup }) => { const updatedChild = $(parentNode.children().get(fromIndex)) switch (type) { case UPDATE_TYPES.INSERT_MARKUP: insertChildAt(parentNode, $(markup), toIndex) // 插入 break case UPDATE_TYPES.MOVE_EXISTING: deleteChild(updatedChild) // 删除 insertChildAt(parentNode, updatedChild, toIndex) break case UPDATE_TYPES.REMOVE_NODE: deleteChild(updatedChild) break default: break } }) } // 代码地址:src/react/component/ReactDomComponent.js
以上,整个简易版React就完成了,能够试着写些简单的例子跑跑看了,是否是很是有成就感呢?
总结下更新:
ReactCompositeComponent:负责调用生命周期,经过component diff将更新都交给了子ReactComponet
ReactTextComponent:直接更新内容
ReactDomComponent:先更新props,在更新children,更新children分为三步,tree diff保证同层级比较,使用shouldUpdateReactComponent进行component diff,最后在element diff经过lastIndex顺序优化
至此,整个从头实现简易版React就结束了,感谢你们的观看。
参考资料,感谢几位前辈的分享:
https://www.cnblogs.com/sven3...
https://github.com/purplebamb...陈屹 《深刻React技术栈》