前言:
接上篇 React源码解析之completeWork和HostText的更新 ,本文讲解下HostComponent
多次渲染阶段的更新(下篇讲第一次渲染阶段的更新)。
一、HostComponent
作用:
更新DOM
节点
源码:
//DOM 节点的更新,涉及到 virtual dom //https://zh-hans.reactjs.org/docs/faq-internals.html#___gatsby case HostComponent: { //context 相关,暂时跳过 //只有当contextFiber的 current 是当前 fiber 时,才会出栈 popHostContext(workInProgress); const rootContainerInstance = getRootHostContainer(); //================== //节点类型,比如<div>标签对应的 fiber 对象的 type 为 "div" const type = workInProgress.type; //如果不是第一次渲染的话 if (current !== null && workInProgress.stateNode != null) { //更新 DOM 时进行 diff 判断 //获取更新队列 workInProgress.updateQueue updateHostComponent( current, workInProgress, type, newProps, rootContainerInstance, ); //ref指向有变动的话,更新 ref if (current.ref !== workInProgress.ref) { ////添加 Ref 的 EffectTag markRef(workInProgress); } } else { //如果是第一次渲染的话 //如果没有新 props 更新,但是执行到这里的话,可能是 React 内部出现了问题 if (!newProps) { invariant( workInProgress.stateNode !== null, 'We must have new props for new mounts. This error is likely ' + 'caused by a bug in React. Please file an issue.', ); // This can happen when we abort work. break; } //context 相关,暂时跳过 const currentHostContext = getHostContext(); // TODO: Move createInstance to beginWork and keep it on a context // "stack" as the parent. Then append children as we go in beginWork // or completeWork depending on we want to add then top->down or // bottom->up. Top->down is faster in IE11. //是否曾是服务端渲染 let wasHydrated = popHydrationState(workInProgress); //如果是服务端渲染的话,暂时跳过 if (wasHydrated) { // TODO: Move this and createInstance step into the beginPhase // to consolidate. if ( prepareToHydrateHostInstance( workInProgress, rootContainerInstance, currentHostContext, ) ) { // If changes to the hydrated node needs to be applied at the // commit-phase we mark this as such. markUpdate(workInProgress); } } //不是服务端渲染 else { //创建 fiber 实例,即 DOM 实例 let instance = createInstance( type, newProps, rootContainerInstance, currentHostContext, workInProgress, ); //插入子节点 appendAllChildren(instance, workInProgress, false, false); // Certain renderers require commit-time effects for initial mount. // (eg DOM renderer supports auto-focus for certain elements). // Make sure such renderers get scheduled for later work. if ( //初始化事件监听 //如果该节点能够自动聚焦的话 finalizeInitialChildren( instance, type, newProps, rootContainerInstance, currentHostContext, ) ) { //添加 EffectTag,方便在 commit 阶段 update markUpdate(workInProgress); } //将处理好的节点实例绑定到 stateNode 上 workInProgress.stateNode = instance; } //如果 ref 引用不为空的话 if (workInProgress.ref !== null) { // If there is a ref on a host node we need to schedule a callback //添加 Ref 的 EffectTag markRef(workInProgress); } } break; } 复制代码
解析:
(1) 非第一次渲染阶段(多次渲染阶段)
① 执行updateHostComponent()
方法,进行diff
判断哪些props
是需要update
的,将其push
进该fiber
对象的updateQueue
(更新队列)属性中
② 如果当前节点的ref
指向有变动的话,执行markRef()
,添加Ref
的EffectTag
(2) 第一次渲染阶段(暂不考虑server
端渲染)
① 执行createInstance()
,创建fiber
实例
② 执行appendAllChildren()
,插入所有子节点
③ 执行finalizeInitialChildren()
,初始化事件监听,并执行markUpdate()
,以添加Update
的EffectTag
,以便在commit
阶段执行真正的DOM
更新
④ 将处理好的节点实例绑定到fiber
对象的stateNode
上
⑤ 如果当前节点的ref
指向有变动的话,执行markRef()
,添加Ref
的EffectTag
注意:
「第一次渲染阶段」放在下篇文章讲。
我们来解析下HostComponent
多次渲染阶段下的执行方法
二、updateHostComponent
作用:
更新DOM
时进行prop diff
判断,获取更新队列workInProgress.updateQueue
源码:
updateHostComponent = function( current: Fiber, workInProgress: Fiber, type: Type, newProps: Props, rootContainerInstance: Container, ) { // If we have an alternate, that means this is an update and we need to // schedule a side-effect to do the updates. //老 props const oldProps = current.memoizedProps; //新老 props 对象引用的内存地址没有变过,即没有更新 if (oldProps === newProps) { // In mutation mode, this is sufficient for a bailout because // we won't touch this node even if children changed. return; } // If we get updated because one of our children updated, we don't // have newProps so we'll have to reuse them. // 如果该节点是因为子节点的更新而更新的,那么是没有新 props 需要更新的,但得复用新 props // TODO: Split the update API as separate for the props vs. children. // Even better would be if children weren't special cased at all tho. //todo:用不同的 updateAPI 来区分自身更新和因子节点而更新,是更好的方式 //获取 DOM 节点实例 const instance: Instance = workInProgress.stateNode; //暂时跳过 const currentHostContext = getHostContext(); // TODO: Experiencing an error where oldProps is null. Suggests a host // component is hitting the resume path. Figure out why. Possibly // related to `hidden`. //比较更新得出需要更新的 props 的集合:updatepayload:Array const updatePayload = prepareUpdate( instance, type, oldProps, newProps, rootContainerInstance, currentHostContext, ); // TODO: Type this specific to this type of component. //将需更新的 props 集合赋值到 更新队列上 workInProgress.updateQueue = (updatePayload: any); // If the update payload indicates that there is a change or if there // is a new ref we mark this as an update. All the work is done in commitWork. //注意:即使是空数组也会加上 Update 的 EffectTag,如input/option/select/textarea if (updatePayload) { markUpdate(workInProgress); } }; 复制代码
解析:
(1) 如果新老props
对象引用的内存地址没有变过,即没有更新,则return
(2) 执行prepareUpdate()
,比较更新得出需要更新的 props 的集合:updatepayload
(3) 将需更新的props
集合赋值到「更新队列:updateQueue
」上
(4) 如果更新集合不为null
的话,执行markUpdate()
,加上Update
的EffectTag
注意:
即使updatePayload
为空数组[ ]
,也会执行markUpdate()
(5) 简单看下markUpdate()
:
//添加 Update 的 EffectTag function markUpdate(workInProgress: Fiber) { // Tag the fiber with an update effect. This turns a Placement into // a PlacementAndUpdate. workInProgress.effectTag |= Update; } 复制代码
三、prepareUpdate
作用:
比较更新得出需要更新的 props 的集合:updatepayload
源码:
//比较更新得出需要更新的 props 的集合 export function prepareUpdate( domElement: Instance, type: string, oldProps: Props, newProps: Props, rootContainerInstance: Container, hostContext: HostContext, ): null | Array<mixed> { //删除了 dev 代码 //计算出新老 props 的差异 //return updatepayload:Array return diffProperties( domElement, type, oldProps, newProps, rootContainerInstance, ); } 复制代码
解析:
主要是执行了diffProperties()
方法,可能你会有疑惑:为什么不直接把diffProperties()
放到外面去执行,因为 React 在 dev 环境有其他的操作,但是我删除了 dev 代码。
四、diffProperties
作用:
计算出新老props
的差异,也就是prop diff
策略
源码:
// Calculate the diff between the two objects. //计算出新老 props 的差异 //return updatepayload:Array export function diffProperties( domElement: Element, tag: string, lastRawProps: Object, nextRawProps: Object, rootContainerElement: Element | Document, ): null | Array<mixed> { //删除了 dev 代码 //需要更新的 props 集合 let updatePayload: null | Array<any> = null; //老 props let lastProps: Object; //新 props let nextProps: Object; // input/option/select/textarea 无论内容是否有变化都会更新 switch (tag) { case 'input': //获取老 props lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps); //获取新 props nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps); updatePayload = []; break; case 'option': lastProps = ReactDOMOptionGetHostProps(domElement, lastRawProps); nextProps = ReactDOMOptionGetHostProps(domElement, nextRawProps); updatePayload = []; break; case 'select': lastProps = ReactDOMSelectGetHostProps(domElement, lastRawProps); nextProps = ReactDOMSelectGetHostProps(domElement, nextRawProps); updatePayload = []; break; case 'textarea': lastProps = ReactDOMTextareaGetHostProps(domElement, lastRawProps); nextProps = ReactDOMTextareaGetHostProps(domElement, nextRawProps); updatePayload = []; break; default: //oldProps lastProps = lastRawProps; //newProps nextProps = nextRawProps; //如果需要更新绑定 click 方法的话 if ( typeof lastProps.onClick !== 'function' && typeof nextProps.onClick === 'function' ) { // TODO: This cast may not be sound for SVG, MathML or custom elements. //初始化 onclick 事件,以便兼容Safari移动端 trapClickOnNonInteractiveElement(((domElement: any): HTMLElement)); } break; } //判断新属性,比如 style 是否正确赋值 assertValidProps(tag, nextProps); let propKey; let styleName; let styleUpdates = null; //循环操作老 props 中的属性 //将删除 props 加入到数组中 for (propKey in lastProps) { if ( //如果新 props 上有该属性的话 nextProps.hasOwnProperty(propKey) || //或者老 props 没有该属性的话(即原型链上的属性,比如:toString() ) !lastProps.hasOwnProperty(propKey) || //或者老 props 的值为 'null' 的话 lastProps[propKey] == null ) { //跳过此次循环,也就是说不跳过此次循环的条件是该 if 为 false //新 props 没有该属性并且在老 props 上有该属性并且该属性不为 'null'/null //也就是说,能继续执行下面的代码的前提是:propKey 是删除的属性 continue; } //能执行到这边,说明 propKey 是新增属性 //对 style 属性进行操作,<div style={{height:30,}}></div> if (propKey === STYLE) { //获取老的 style 属性对象 const lastStyle = lastProps[propKey]; //遍历老 style 属性,如:height for (styleName in lastStyle) { //如果老 style 中本来就有 styleName 的话,则将其重置为'' if (lastStyle.hasOwnProperty(styleName)) { if (!styleUpdates) { styleUpdates = {}; } //重置(初始化) styleUpdates[styleName] = ''; } } } //dangerouslySetInnerHTML //https://zh-hans.reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml else if (propKey === DANGEROUSLY_SET_INNER_HTML || propKey === CHILDREN) { // Noop. This is handled by the clear text mechanism. } //suppressHydrationWarning //https://zh-hans.reactjs.org/docs/dom-elements.html#suppresshydrationwarning //suppressContentEditableWarning //https://zh-hans.reactjs.org/docs/dom-elements.html#suppresscontenteditablewarning else if ( propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { // Noop } else if (propKey === AUTOFOCUS) { // Noop. It doesn't work on updates anyway. } //如果有绑定事件的话 else if (registrationNameModules.hasOwnProperty(propKey)) { // This is a special case. If any listener updates we need to ensure // that the "current" fiber pointer gets updated so we need a commit // to update this element. if (!updatePayload) { updatePayload = []; } } else { // For all other deleted properties we add it to the queue. We use // the whitelist in the commit phase instead. //将不符合以上条件的删除属性 propKey push 进 updatePayload 中 //比如 ['className',null] (updatePayload = updatePayload || []).push(propKey, null); } } //循环新 props 的 propKey for (propKey in nextProps) { //获取新 prop 的值 const nextProp = nextProps[propKey]; //获取老 prop 的值(因为是根据新 props 遍历的,所以老 props 没有则为 undefined) const lastProp = lastProps != null ? lastProps[propKey] : undefined; if ( //如果新 props 没有该 propKey 的话( 比如原型链上的属性,toString() ) !nextProps.hasOwnProperty(propKey) || //或者新 value 等于老 value 的话(即没有更新) nextProp === lastProp || //或者新老 value 均「宽松等于」 null 的话('null'还有其他情况吗?) //也就是没有更新 (nextProp == null && lastProp == null) ) { //不往下执行 //也就是说往下执行的条件是:新 props 有该 propKey 并且新老 value 不为 null 且不相等 //即有更新的情况 continue; } //能执行到这边,说明新 prop 的值与老 prop 的值不相同/新增 prop 并且有值 //关于 style 属性的更新 <input style={{xxx:yyy}}/> if (propKey === STYLE) { //删除了 dev 代码 //如果老 props 本来就有这个 prop 的话 if (lastProp) { // Unset styles on `lastProp` but not on `nextProp`. //如果新 style 没有该 css 的话,将其置为''(也就是删掉该 css 属性) for (styleName in lastProp) { if ( lastProp.hasOwnProperty(styleName) && (!nextProp || !nextProp.hasOwnProperty(styleName)) ) { if (!styleUpdates) { styleUpdates = {}; } //将其置为'' styleUpdates[styleName] = ''; } } // Update styles that changed since `lastProp`. //这里才是更新 style 属性 for (styleName in nextProp) { if ( //新 props 有 style 并且与老 props 不一样的话,就更新 style 属性 nextProp.hasOwnProperty(styleName) && lastProp[styleName] !== nextProp[styleName] ) { if (!styleUpdates) { styleUpdates = {}; } //更新 style //更新统一放在 styleUpdates 对象中 styleUpdates[styleName] = nextProp[styleName]; } } } //如果不是更新的 style 而是新增的话 else { // Relies on `updateStylesByID` not mutating `styleUpdates`. //第一次初始化 if (!styleUpdates) { if (!updatePayload) { updatePayload = []; } //将 'style'、null push 进数组 updatePayload 中 //['style',null] updatePayload.push(propKey, styleUpdates); } //styleUpdates 赋成新 style 的值 styleUpdates = nextProp; //该方法最后有个 if(styleUpdates),会 push 这种情况: //['style',null,'style',{height:22,}] } } // __html else if (propKey === DANGEROUSLY_SET_INNER_HTML) { //新 innerHTML const nextHtml = nextProp ? nextProp[HTML] : undefined; //老 innerHTML const lastHtml = lastProp ? lastProp[HTML] : undefined; //push('__html','xxxxx') if (nextHtml != null) { if (lastHtml !== nextHtml) { (updatePayload = updatePayload || []).push(propKey, '' + nextHtml); } } else { // TODO: It might be too late to clear this if we have children // inserted already. } } //子节点的更新 //https://zh-hans.reactjs.org/docs/glossary.html#propschildren else if (propKey === CHILDREN) { if ( lastProp !== nextProp && //子节点是文本节点或数字 (typeof nextProp === 'string' || typeof nextProp === 'number') ) { //push 进数组中 (updatePayload = updatePayload || []).push(propKey, '' + nextProp); } } else if ( propKey === SUPPRESS_CONTENT_EDITABLE_WARNING || propKey === SUPPRESS_HYDRATION_WARNING ) { // Noop } ////如果有绑定事件的话,如<div onClick=(()=>{ xxx })></div> else if (registrationNameModules.hasOwnProperty(propKey)) { //绑定事件里有回调函数的话 if (nextProp != null) { // We eagerly listen to this even though we haven't committed yet. //删除了 dev 代码 //找到 document 对象,React 是将节点上绑定的事件统一委托到 document 上的 //涉及到event 那块了,暂时跳过 //想立即知道的,请参考: //https://www.cnblogs.com/Darlietoothpaste/p/10039127.html?utm_source=tuicool&utm_medium=referral ensureListeningTo(rootContainerElement, propKey); } if (!updatePayload && lastProp !== nextProp) { // This is a special case. If any listener updates we need to ensure // that the "current" props pointer gets updated so we need a commit // to update this element. //特殊的情况. //在监听器更新前,React 需要确保当前 props 的指针得到更新, // 因此 React 需要一个 commit (即 updatePayload ),确保能更新该节点 //因此 updatePayload 要不为 null updatePayload = []; } } //不符合以上的需要更新的新 propsKey else { // For any other property we always add it to the queue and then we // filter it out using the whitelist during the commit. //将新增的 propsKey push 进 updatePayload //在之后的 commit 阶段,会用白名单筛选出这些 props (updatePayload = updatePayload || []).push(propKey, nextProp); } } //将有关 style 的更新 push 进 updatePayload 中 if (styleUpdates) { //删除了 dev 代码 (updatePayload = updatePayload || []).push(STYLE, styleUpdates); } //类似于['style',{height:14},'__html',xxxx,...] //我很奇怪为什么 React 不用{style:{height:14}, '__html':xxx, } //这种方式去存更新的 props? return updatePayload; } 复制代码
解析:
有些长,整体结构是:
① switch()语句判断
② 执行assertValidProps()
③ 循环操作老props
中的属性
④ 循环操作新props
中的属性
⑤ 将有关style
的更新push
进updatePayload
中
⑥ 最后返回updatePayload
更新数组
(1) switch()语句判断
① 无论input/option/select/textarea
的内容是否有变化都会更新,即updatePayload = []
,它们获取新老props
的方式也不一样,不细讲了
② 其他情况的新老props
是获取的传进来的参数
③ 做兼容:执行trapClickOnNonInteractiveElement()
,初始化onclick
事件,以便兼容Safari移动端
trapClickOnNonInteractiveElement()
:
//初始化 onclick 事件,以便兼容Safari移动端 export function trapClickOnNonInteractiveElement(node: HTMLElement) { // Mobile Safari does not fire properly bubble click events on // non-interactive elements, which means delegated click listeners do not // fire. The workaround for this bug involves attaching an empty click // listener on the target node. // http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html // Just set it using the onclick property so that we don't have to manage any // bookkeeping for it. Not sure if we need to clear it when the listener is // removed. // TODO: Only do this for the relevant Safaris maybe? node.onclick = noop; } 复制代码
(2) 执行assertValidProps()
,判断新属性,比如style
是否正确赋值
assertValidProps()
:
//判断新属性,比如 style 是否正确赋值 function assertValidProps(tag: string, props: ?Object) { if (!props) { return; } // Note the use of `==` which checks for null or undefined. //判断目标节点的标签是否可以包含子标签,如 <br/>、<input/> 等是不能包含子标签的 if (voidElementTags[tag]) { //不能包含子标签,报出 error invariant( props.children == null && props.dangerouslySetInnerHTML == null, '%s is a void element tag and must neither have `children` nor ' + 'use `dangerouslySetInnerHTML`.%s', tag, __DEV__ ? ReactDebugCurrentFrame.getStackAddendum() : '', ); } //__html设置的标签内有子节点,比如:__html:"<span>aaa</span>" ,就会报错 if (props.dangerouslySetInnerHTML != null) { invariant( props.children == null, 'Can only set one of `children` or `props.dangerouslySetInnerHTML`.', ); invariant( typeof props.dangerouslySetInnerHTML === 'object' && HTML in props.dangerouslySetInnerHTML, '`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' + 'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' + 'for more information.', ); } //删除了 dev 代码 //style 不为 null,但是不是 Object 类型的话,报以下错误 invariant( props.style == null || typeof props.style === 'object', 'The `style` prop expects a mapping from style properties to values, ' + "not a string. For example, style={{marginRight: spacing + 'em'}} when " + 'using JSX.%s', __DEV__ ? ReactDebugCurrentFrame.getStackAddendum() : '', ); } 复制代码
可以看到,主要是以下 3 点的判断:
① 判断目标节点的标签是否可以包含子标签,如<br/>
、<input/>
等是不能包含子标签的
② 判断__html
设置的标签内是否有子节点,如:__html:"aaa" ,就会报错
③ style
属性不为null
,但不是Object
类型的话,报错
(3) 循环操作老props
中的属性,将需要删除的props
加入到数组中
① 如果不是删除的属性(老props
有,新props
没有)的话,则跳过,不执行下面代码
② 如果是删除的属性的话,则执行下方代码
以下逻辑是propKey为删除的属性的操作
③ 如果propKey
是style
属性的话,循环style
对象中的CSS
属性
如果老props
有该CSS
属性的话,则将其值置为空字符串''
比如:
<div style={{height:14,}}>aaa</div> 复制代码
置为
<div style={{height:'',}}>aaa</div> 复制代码
④ 如果有绑定事件的话,则初始化updatePayload
数组,表示会更新
registrationNameModules
包含了所有的事件集合,打印出来是这个样子:
⑤ 除了代码中上述的其他情况,均将propKey
置为null
比如:className
updatePayload = ['className',null] 复制代码
(4) 循环操作新props
中的属性,将新增/更新的props
加入到数组中
以下操作是针对新增/更新的props的
① 如果propKey
是style
属性的话,循环style
对象中的CSS
属性
[1] 如果老style
的CSS
属性有值,新style
对象没有该CSS
属性,则删除该CSS
属性,比如:
<div style={{height:14,}}>aaa</div> 复制代码
置为
<div style={{height:'',}}>aaa</div> 复制代码
[2] 如果新style
内的css
属性的值与老style
内的值不同的话,更新styleUpdates
,比如:
<div style={{height:14,}}>aaa</div> 复制代码
置为
<div style={{height:22,}}>aaa</div> 复制代码
则styleUpdates
为:
{ height:22, } 复制代码
[3] 如果style
这个propKey
是新增属性的话,则将styleUpdates
直接置为style
对象的值,比如:
<div>aaa</div> 复制代码
置为
<div style={{height:22,}}>aaa</div> 复制代码
则styleUpdates
为:
{ height:22, } 复制代码
② 如果propKey
是__html
的话,比较新老innerHTML
的值,并放进updatePayload
更新数组中
③ 如果propKey
是children
的话
当子节点是文本或数字时,直接将其push
进updatePayload
数组中
④ 如果propKey
是绑定事件的话
[1] 绑定事件有回调函数,则执行ensureListeningTo()
,找到document
对象
React 这样做的目的是,要将节点上绑定的事件统一委托到document
上,想立即知道的,请参考:
www.cnblogs.com/Darlietooth…
[2] 初始化updatePayload
为[ ]
,也就是要更新
⑤ 除了代码中上述的其他情况,均将更新的propKey
push 进 updatePayload 中
(5) 将有关 style 的更新 push 进 updatePayload 中
注意下这边:有三种情况
① 如果是新增的style
属性
import React, {useEffect} from 'react'; import './App.css'; function App() { const [styleObj, setStyleObj] = React.useState( null); useEffect(()=>{ setTimeout(()=>{ setStyleObj({height:14,}) },2000) },[]) return ( <div className="App"> <div style={styleObj}> aaa </div> </div> ); } export default App; 复制代码
则updatePayload为:
[ 'style', null, 'style', { height:14 } ] 复制代码
② 如果是更新的style
属性
import React, {useEffect} from 'react'; import './App.css'; function App() { const [styleObj, setStyleObj] = React.useState({}); useEffect(()=>{ setTimeout(()=>{ setStyleObj({height:14,}) },2000) },[]) return ( <div className="App"> <div style={styleObj}> aaa </div> </div> ); } export default App; 复制代码
则updatePayload
为:
[ 'style', { height:14 } ] 复制代码
③ 如果是删除的style
属性
import React, {useEffect} from 'react'; import './App.css'; function App() { const [styleObj, setStyleObj] = React.useState({height:14,}); useEffect(()=>{ setTimeout(()=>{ setStyleObj(null) },2000) },[]) return ( <div className="App"> <div style={styleObj}> aaa </div> </div> ); } export default App; 复制代码
则updatePayload
为:
[ 'style', { height:'' } ] 复制代码
(6) 最终返回updatePayload
数组,类似于
['style',{height:14},'__html',xxxx,...] 复制代码
我很奇怪为什么 React 不用{style:{height:14}, '__html':xxx, }
这种方式去存更新的 props?
希望后面能有答案
五、补充
在我早期写的一篇文章React之diff算法中,主要介绍了tree diff
、component diff
、element diff
这三个diff
策略,也是通过解析 React 源码,才发现了第四个diff
策略——prop diff
,也就是本文所讲的内容。
六、GitHubReactFiberCompleteWork.js
:
github.com/AttackXiaoJ…
ReactDOMHostConfig.js
:
github.com/AttackXiaoJ…
ReactDOMComponent.js
:
github.com/AttackXiaoJ…
assertValidProps.js
:
github.com/AttackXiaoJ…
(完)