不同于React其他组件props的更新会经历schedule - render - commit流程。 对于input、textarea、select,React有一条单独的更新路径,这条路径触发的更新被称为discreteUpdate。 这条路径的工作流程如下:
- 先以非受控的形式更新表单DOM
- 以同步的优先级开启一次更新
- 更新后的value在commit阶段并不会像其他props一样作用于DOM
- 调用restoreStateOfTarget方法,比较DOM的实际value(即步骤1中的非受控value)与步骤3中更新的value,如果相同则退出,如果不同则用步骤3的value更新DOM
本文是React源码中如何实现受控组件 文章中,关于 更新后的value在commit阶段并不会像其他props一样作用于DOM 这句描述的探究展开。
探究阶段一
首先 props 在 commit 阶段作用于 DOM 的方法是 updateDOMProperties 。
这个方法执行于 commit 阶段的 mutation 阶段,也就是在 commitMutationEffects 方法执行范围内。描述一下这个路径:
commitMutationEffects --> commitWork(update) --> commitUpdate(case HostComponent) --> updateProperties --> updateDOMProperties
看下 updateDOMProperties 代码:
function updateDOMProperties(domElement, updatePayload, wasCustomComponentTag, isCustomComponentTag) {
for (var i = 0; i < updatePayload.length; i += 2) {
var propKey = updatePayload[i];
var propValue = updatePayload[i + 1];
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}
那 input 更新 value 后,不作用于 DOM,意味着不经过这个方法(updateDOMProperties)或 updatePayload 为空。从代码来看,mutation 阶段都会调用到这个方法,所以 updatePayload 值应为空数组。
在 updateProperties 调用的 updateDOMProperties 方法打上断点:
function updateProperties(domElement, updatePayload, tag, lastRawProps, nextRawProps) {
// 忽略其他代码
updateDOMProperties(domElement, updatePayload, wasCustomComponentTag, isCustomComponentTag); // TODO: Ensure that an update gets scheduled if any of the special props
}
会发现 input 更新时,该断点的 updatePayload 的确是空数组。
探究阶段二
那么,input 更新在什么时候进行了特殊处理呢?
我们梳理一下,当 input 发生更新,进入 render 阶段,会经历 2 个方法,beginWork 和 completeWork。
其中 beginWork 对 input 这些 HostComponent 没有进行特殊处理,只是执行了 updateHostComponent 方法生成 Fiber 节点。
当经历 completeWork 时,对于 HostComponent 会执行 updateHostComponent 方法:
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
// 忽略无关代码
switch (workInProgress.tag) {
case HostComponent: {
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
}
}
}
}
而 updateHostComponent 方法里会对 updatePayload 进行赋值。这个 updatePayload 就是我们追寻的:
input 更新时,该断点的 updatePayload 的确是空数组 的 updatePayload。看下 updateHostComponent 方法的代码:
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
return;
}
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
workInProgress.updateQueue = (updatePayload: any);
if (updatePayload) {
markUpdate(workInProgress);
}
};
从代码可以看到,该方法的主要作用是对 updatePayload 赋值以及标记该节点是否需要更新。
看下 prepareUpdate 方法:
function prepareUpdate(
domElement: Instance,
type: string,
oldProps: Props,
newProps: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): null | Array<mixed> {
return diffProperties(
domElement,
type,
oldProps,
newProps,
rootContainerInstance,
);
}
function diffProperties(
domElement: Element,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
rootContainerElement: Element | Document,
): null | Array<mixed> {
// 已忽略其他无关代码
let updatePayload: null | Array<any> = null;
let lastProps: Object;
let nextProps: Object;
switch (tag) {
case 'input':
lastProps = ReactDOMInputGetHostProps(domElement, lastRawProps);
nextProps = ReactDOMInputGetHostProps(domElement, nextRawProps);
updatePayload = [];
break;
default:
lastProps = lastRawProps;
nextProps = nextRawProps;
if (
typeof lastProps.onClick !== 'function' &&
typeof nextProps.onClick === 'function'
) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((domElement: any): HTMLElement));
}
break;
}
return updatePayload;
}
prepareUpdate 返回的是 diffProperties 方法的执行结果,而在 diffProperties 方法内可以看到对 input 元素的特殊处理:updatePayload = [] 。
这里应该明白:更新后的value在commit阶段并不会像其他props一样作用于DOM 的原因了。
总结
从头梳理一下:
- input 输入新内容,触发了更新;
beginWork生成了新的 input 节点;completeWork对HostComponent进行处理(updateHostComponent),经过diffProperties方法执行后返回对应的updatePayload值。input 类型的updatePayload的值是空数组;- 在
commit的mutation阶段,对DOM进行更新,调用updateDOMProperties,此时 input 类型的元素节点的updatePayload为空数组,所以不对它进行任何的 DOM 操作;
BTW
也大概记录一下非 input ,textarea,select 元素的更新路径吧:
beginWork生成Fiber节点completeWork经历diffProperties方法执行后获得updatePayload,在updateHostComponent方法里根据updatePaylaod的值,标记是否需要更新commit的mutation阶段进行DOM 更新(updateDOMProperties)
参考
[React源码中如何实现受控组件] mp.weixin.qq.com/s/Im8c0ZwZt…
[updateProperties] github.com/facebook/re…
[updateDOMProperties] github.com/facebook/re…
[completeWork / updateHostComponent] github.com/facebook/re…
[prepareUpdate / diffProperties] github.com/facebook/re…