在 上篇文章 中,我们讲了 「mutation
」 子阶段的插入(Placement
)操作,接下来我们讲更新(Update
)和删除(Deletion
)操作:
//替换并更新该节点是Placement和Update的结合,就不讲了
case PlacementAndUpdate: {
// Placement
//针对该节点及子节点进行插入操作
commitPlacement(nextEffect);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
nextEffect.effectTag &= ~Placement;
// Update
const current = nextEffect.alternate;
//对 DOM 节点上的属性进行更新
commitWork(current, nextEffect);
break;
}
//更新节点
//旧节点->新节点
case Update: {
const current = nextEffect.alternate;
//对 DOM 节点上的属性进行更新
commitWork(current, nextEffect);
break;
}
case Deletion: {
//删除节点
commitDeletion(nextEffect);
break;
}
作用:
对DOM
节点上的属性进行更新
源码:
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
//因为是执行 DOM 操作,所以supportsMutation为 true,下面这一段不看
if (!supportsMutation) {
//删除了本情况代码
}
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// Note: We currently never use MountMutation, but useLayout uses
// UnmountMutation.
//循环 FunctionComponent 上的 effect 链,
//根据hooks 上每个 effect 上的 effectTag,执行destroy/create 操作(类似于 componentDidMount/componentWillUnmount)
//详情请看:[React源码解析之Commit第一子阶段「before mutation」](https://mp.weixin.qq.com/s/YtgEVlZz1i5Yp87HrGrgRA)中的「三、commitHookEffectList()」
commitHookEffectList(UnmountMutation, MountMutation, finishedWork);
return;
}
case ClassComponent: {
return;
}
//DOM 节点的话
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
//待更新的属性
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
//旧的属性
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
//需要更新的属性的集合
//比如:['style',{height:14},'__html',xxxx,...]
//关于updatePayload,请看:
// [React源码解析之HostComponent的更新(上)](https://juejin.im/post/5e5c5e1051882549003d1fc7)中的「四、diffProperties」
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
//进行节点的更新
if (updatePayload !== null) {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
case HostText: {
invariant(
finishedWork.stateNode !== null,
'This should have a text node initialized. This error is likely ' +
'caused by a bug in React. Please file an issue.',
);
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
const oldText: string =
current !== null ? current.memoizedProps : newText;
//源码即:textInstance.nodeValue = newText;
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostRoot: {
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
commitSuspenseComponent(finishedWork);
attachSuspenseRetryListeners(finishedWork);
return;
}
case SuspenseListComponent: {
attachSuspenseRetryListeners(finishedWork);
return;
}
case IncompleteClassComponent: {
return;
}
case EventComponent: {
return;
}
default: {
invariant(
false,
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
}
}
解析:
(1) 因为是执行DOM
操作,所以supportsMutation
为true
,下面这一段不看:
if (!supportsMutation) {
//删除了本情况代码
}
(2) 主体逻辑是根据目标fiber
的tag
类型,进行不同的操作:① 如果tag
是函数组件FunctionComponent
的话,则执行commitHookEffectList()
方法,作用是:
循环FunctionComponent
上的effect
链,根据hooks
上每个effect
上的effectTag
,执行destroy/create
操作(类似于componentDidMount
/componentWillUnmount
)
关于commitHookEffectList()
的源码,请看:
React源码解析之Commit第一子阶段「before mutation」中的 「三、commitHookEffectList()
」
② 如果tag
是DOM节点HostComponent
的话,则获取要更新的属性newProps
、旧属性oldProps
和要更新的属性集合updatePayload
,并执行commitUpdate()
,进行更新
补充:
关于updatePayload
更新队列是如何生成的,请看:
React源码解析之HostComponent的更新(上) 中的 「四、diffProperties
」
③ 如果tag
是text
文本节点HostText
的话,则比较简单了,执行commitTextUpdate()
,源码就是替换文本值:
export function commitTextUpdate(
textInstance: TextInstance,
oldText: string,
newText: string,
): void {
textInstance.nodeValue = newText;
}
接下来,我们就看下DOM
节点的更新—commitUpdate()
方法
作用:
进行DOM
节点的更新
源码:
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// Update the props handle so that we know which props are the ones with
// with current event handlers.
//挂载属性:node[internalEventHandlersKey] = props;
updateFiberProps(domElement, newProps);
// Apply the diff to the DOM node.
//更新 DOM 属性
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}
解析:
(1) 执行updateFiberProps()
,将待更新的属性挂载到fiber
对象的internalEventHandlersKey
属性上
updateFiberProps()
的源码如下:
const randomKey = Math.random().toString(36).slice(2)
const internalEventHandlersKey = '__reactEventHandlers$' + randomKey
export function updateFiberProps(node, props) {
node[internalEventHandlersKey] = props;
}
(2) 执行updateProperties()
,更新DOM
属性
作用:
diff prop
操作,找出DOM
节点上属性的不同,以更新
源码:
// Apply the diff.
//diff prop,找出DOM 节点上属性的不同,以更新
export function updateProperties(
domElement: Element,
updatePayload: Array<any>,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
): void {
// Update checked *before* name.
// In the middle of an update, it is possible to have multiple checked.
// When a checked radio tries to change name, browser makes another radio's checked false.
//如果是 radio 标签的话
if (
tag === 'input' &&
nextRawProps.type === 'radio' &&
nextRawProps.name != null
) {
//单选按钮的相关操作,可不看
ReactDOMInputUpdateChecked(domElement, nextRawProps);
}
//判断是否是自定义的 DOM 标签,具体请看:
//[React源码解析之HostComponent的更新(下)](https://mp.weixin.qq.com/s/aB8jRVFzJ6EkkIqPVF3r1Q)中的「八、setInitialProperties」
//之前是否是自定义标签
const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
//待更新的是否是自定义标签
const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
// Apply the diff.
updateDOMProperties(
domElement,
updatePayload,
wasCustomComponentTag,
isCustomComponentTag,
);
// TODO: Ensure that an update gets scheduled if any of the special props
// changed.
//特殊标签的特殊处理,可不看
switch (tag) {
case 'input':
// Update the wrapper around inputs *after* updating props. This has to
// happen after `updateDOMProperties`. Otherwise HTML5 input validations
// raise warnings and prevent the new value from being assigned.
ReactDOMInputUpdateWrapper(domElement, nextRawProps);
break;
case 'textarea':
ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
break;
case 'select':
// <select> value update needs to occur after <option> children
// reconciliation
ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
break;
}
}
解析: (1) 一些特殊标签的特殊处理就不细说了
(2) 关于isCustomComponent()
,判断是否是自定义的 DOM 标签的源码,请看:
React源码解析之HostComponent的更新(下) 中的 「八、setInitialProperties
」
接下来重点看下updateDOMProperties()
,也就是DOM
节点属性更新的核心源码
作用:
进行DOM
节点的更新
源码:
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
// TODO: Handle wasCustomComponentTag
//遍历更新队列,注意 i=i+2,因为 updatePayload 是这样的:['style',{height:14},'__html',xxxx,...]
//关于updatePayload,请看:
// [React源码解析之HostComponent的更新(上)](https://juejin.im/post/5e5c5e1051882549003d1fc7)中的「四、diffProperties」
for (let i = 0; i < updatePayload.length; i += 2) {
//要更新的属性
const propKey = updatePayload[i];
//要更新的值
const propValue = updatePayload[i + 1];
//要更新style 属性的话,则执行setValueForStyles
if (propKey === STYLE) {
// 设置 style 的值,请看:
// [React源码解析之HostComponent的更新(下)](https://juejin.im/post/5e65f86f6fb9a07cdc600e09)中的「八、setInitialProperties」中的第八点
setValueForStyles(domElement, propValue);
}
else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
// 设置innerHTML属性,请看:
// [React源码解析之HostComponent的更新(下)](https://juejin.im/post/5e65f86f6fb9a07cdc600e09)中的「八、setInitialProperties」中的第八点
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
//设置textContent属性,请看:
// [React源码解析之HostComponent的更新(下)](https://juejin.im/post/5e65f86f6fb9a07cdc600e09)中的「八、setInitialProperties」中的第八点
setTextContent(domElement, propValue);
} else {
//为DOM节点设置属性值,即 setAttribute
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}
解析:
逻辑也简单,遍历更新队列,对不同的属性,进行不同的操作 总共进行了 4 种情况的操作:
(1) 对style
属性,执行setValueForStyles()
,来设置style
的值
关于setValueForStyles()
的讲解·,请看:
React源码解析之HostComponent的更新(下) 中的 「八、setInitialProperties
」 中的第八点
(2) 对innerHTML
属性,执行setInnerHTML()
,来设置innerHTML
的值
关于setInnerHTML()
的讲解·,请看:
React源码解析之HostComponent的更新(下) 中的「八、setInitialProperties」中的第八点
(3) 对children
属性,即设置 DOM 标签内部的值,执行setTextContent()
,来设置textContent
属性
关于setTextContent()
的讲解·,请看:
React源码解析之HostComponent的更新(下) 中的「八、setInitialProperties」中的第八点
(4) 除此之外的情况,就是为DOM
节点设置属性值的情况,比如className
,则执行setValueForProperty()
,也就是调用setAttribute
方法,就不解析了,放下源码:
export function setValueForProperty(
node: Element,
name: string,
value: mixed,
isCustomComponentTag: boolean,
) {
const propertyInfo = getPropertyInfo(name);
if (shouldIgnoreAttribute(name, propertyInfo, isCustomComponentTag)) {
return;
}
if (shouldRemoveAttribute(name, value, propertyInfo, isCustomComponentTag)) {
value = null;
}
// If the prop isn't in the special list, treat it as a simple attribute.
if (isCustomComponentTag || propertyInfo === null) {
if (isAttributeNameSafe(name)) {
const attributeName = name;
if (value === null) {
node.removeAttribute(attributeName);
} else {
node.setAttribute(attributeName, '' + (value: any));
}
}
return;
}
const {mustUseProperty} = propertyInfo;
if (mustUseProperty) {
const {propertyName} = propertyInfo;
if (value === null) {
const {type} = propertyInfo;
(node: any)[propertyName] = type === BOOLEAN ? false : '';
} else {
// Contrary to `setAttribute`, object properties are properly
// `toString`ed by IE8/9.
(node: any)[propertyName] = value;
}
return;
}
// The rest are treated as attributes with special cases.
const {attributeName, attributeNamespace} = propertyInfo;
if (value === null) {
node.removeAttribute(attributeName);
} else {
const {type} = propertyInfo;
let attributeValue;
if (type === BOOLEAN || (type === OVERLOADED_BOOLEAN && value === true)) {
attributeValue = '';
} else {
// `setAttribute` with objects becomes only `[object]` in IE8/9,
// ('' + value) makes it output the correct toString()-value.
attributeValue = '' + (value: any);
if (propertyInfo.sanitizeURL) {
sanitizeURL(attributeValue);
}
}
if (attributeNamespace) {
node.setAttributeNS(attributeNamespace, attributeName, attributeValue);
} else {
node.setAttribute(attributeName, attributeValue);
}
}
}
① 文本节点,执行textInstance.nodeValue = newText;
,来替换文本值
② DOM标签,遍历更新队列updatePayload(['style',{height:14},'__html',xxxx,...]
),针对style
、innerHTML
、children
和attribute
进行属性更新
commitWork()
:
https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberCommitWork.js
commitUpdate()
:
https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-dom/src/client/ReactDOMHostConfig.js
updateDOMProperties()
:
https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-dom/src/client/ReactDOMComponent.js
setValueForProperty()
:
https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-dom/src/client/DOMPropertyOperations.js