上篇文章已经了解到了react 的初次渲染,这篇文章来看下react中的单节点Dom-diff
import { createFiberRoot } from './ReactFiberRoot';
import { updateContainer } from './ReactFiberReconciler';
function render(element, container) {
//1.创建一个fiberRoot, 它其实指向是我们的div#root这个容器
let fiberRoot = container._reactRootContainer;
if (!fiberRoot) {
fiberRoot = container._reactRootContainer = createFiberRoot(container);
}
//把element这个虚拟DOM变成真实DOM插入容器中
updateContainer(element, fiberRoot);
}
const ReactDOM = {
render
}
export default ReactDOM;
if (!fiberRoot) {
fiberRoot = container._reactRootContainer = createFiberRoot(container);
}
这个就确保了渲染的时候赋值,更新的时候取值,确保操作的是同一个fiberRoot
function commitRoot() {
//指向新构建的fiber树
const finishedWork = workInProgressRoot.current.alternate;
workInProgressRoot.finishedWork = finishedWork;
commitMutationEffects(workInProgressRoot);
}
commitRoot就是提交fiber,修改dom的阶段,dom-diff就在这个阶段。commitMutationEffects中有个分支,就是当fiber的flags是Placement(添加 或者说创建 挂载),会有个commitPlacement的操作,看下之前的代码。
function commitMutationEffects(root) {
const finishedWork = root.finishedWork;
let nextEffect = finishedWork.firstEffect;
let effectsList = '';
while (nextEffect) {
effectsList += `(${getFlags(nextEffect.flags)}#${nextEffect.type}#${nextEffect.key})`;
const flags = nextEffect.flags;
if (flags === Placement) {
commitPlacement(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
effectsList += 'null';
console.log(effectsList);
root.current = finishedWork;
}
commitPlacement只是其中的一个节点的操作,我们将commitPlacement等相关方法提取到ReactFiberCommitWork.js中去。并且稍作改造。
export function commitPlacement(nextEffect) {
let stateNode = nextEffect.stateNode;
let parentStateNode = getParentStateNode(nextEffect);
- parentStateNode.appendChild(stateNode);
+ appendChild(parentStateNode, stateNode);
}
也是用来抹平平台差异性的操作。appendChild方法也来自ReactDOMComponent.js(真实dom操作)
export function appendChild(parentInstance, child) {
parentInstance.appendChild(child);
}
结果也是一样的,页面上也是渲染出来了title。接下来看下单节点dom-diff是如何实现的。
在beginWork(current, workInProgress)的时候,传入了当前的fiber树和正在构建的fiber树,而且在reconcileChildren的时候,并且是更新操作的时候就会进行比较。
export function reconcileChildren(current, workInProgress, nextChildren) {
//如果current有值,说明这是一类似于更新的作品
if (current) {
//进行比较 新老内容,得到差异进行更新
workInProgress.child = reconcileChildFibers(
workInProgress,//新fiber
current.child,//老fiber的第一个子fiber节点
nextChildren //新的虚拟DOM
);
} else {
///初次渲染,不需要比较 ,全是新的
workInProgress.child = mountChildFibers(
workInProgress,//新fiber
null,//老fiber的第一个子fiber节点
nextChildren //新的虚拟DOM
);
}
}
/**
*
* @param {*} returnFiber 新的父fiber
* @param {*} currentFirstChild current就是老的意思,老的第一个子fiber
* @param {*} newChild 新的虚拟DOM
*/
function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
//判断newChild是不是一个对象,如果是的话说明新的虚拟DOM只有一个React元素节点
const isObject = typeof newChild === 'object' && ( newChild );
//说明新的虚拟DOM是单节点
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(
returnFiber, currentFirstChild, newChild
));
}
}
}
单节点的diff会走到reconcileSingleElement中去,目前reconcileSingleElement还只是直接创建一个fiber节点,并没有对比的过程。
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
const created = createFiberFromElement(element);//div#title
created.return = returnFiber;
return created;
}
先来看下更新情况下,当前current和正在构建的workInProgress的关系图:
current永远是老的fiber树,就是当前页面视图对应的fiber树,workInProgress是当前正在构建的fiber树,他们相互指向,相互引用,就是之前说的双缓冲。
上图是单节点的diff流程图,按这个,我们就分为以下几种情况:
1.原来多个节点,现在只有一个节点,删除多余节点
具体代码实现:
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
//获取新的虚拟DOM的key
let key = element.key;
//获取第一个老的fiber节点
let child = currentFirstChild;
while (child) {
//老fiber的ekey和新的虚拟DOM的key相同
if (child.key === key) {
} else {
//如果不相同说明当前这个老fiber不是对应于新的虚拟DOM节点 把此老fiber标记为删除
deleteChild(returnFiber, child);
}
}
const created = createFiberFromElement(element);//div#title
created.return = returnFiber;
return created;
}
/**
* 因为这个老的子fiber在新的虚拟DOM树不存在了,则标记为删除
* @param {*} returnFiber
* @param {*} childToDelete
*/
function deleteChild(returnFiber, childToDelete) {
//如果不需要跟踪副作用,直接返回
if (!shouldTrackSideEffects) {
return;
}
//把自己这个副作用添加到父effectList中
//删除类型的副作用一般放在父fiber副作用链表的前面,在进行DOM操作时候先执行删除操作
const lastEffect = returnFiber.lastEffect;
if (lastEffect) {
lastEffect.nextEffect = childToDelete;
returnFiber.lastEffect = childToDelete;
} else {
//父fiber节点effectList是空
returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
}
//清空下一个副作用指向
childToDelete.nextEffect = null;
//标记为删除
childToDelete.flags = Deletion;
}
接下来就是处理下一个fiber,也就是它的sibling。
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
//获取新的虚拟DOM的key
let key = element.key;
//获取第一个老的fiber节点
let child = currentFirstChild;
while (child) {
//老fiber的ekey和新的虚拟DOM的key相同说明
if (child.key === key) {
} else {
//如果相同说明当前这个老fiber不是对应于新的虚拟DOM节点 把此老fiber标记为删除,并且继续弟弟
deleteChild(returnFiber, child);
}
+ // 继续匹配弟弟们
+ child = child.sibling;
}
const created = createFiberFromElement(element);//div#title
created.return = returnFiber;
return created;
}
2.key相同,类型不同,删除老节点,添加新节点
那此时key相同,按照流程图,就看下type是否相同,如果不相同,则删除包括当前的老fiber在内所所有后续的老fibe
while (child) {
//老fiber的ekey和新的虚拟DOM的key相同说明
if (child.key === key) {
//判断老的fiber的type和新的虚拟DOMtype是否相同
if (child.type == element.type) {
} else {
+ //已经配上key了,但是type不同,则删除包括当前的老fiber在内所所有后续的老fibe
+ deleteRemainingChildren(returnFiber, child);
+ break;
}
} else {
//如果不相同说明当前这个老fiber不是对应于新的虚拟DOM节点 把此老fiber标记为删除
deleteChild(returnFiber, child);
child = child.sibling;
}
}
复用child老fiber节点,删除剩下的其它fiber
function deleteRemainingChildren(returnFiber, childToDelete) {
while (childToDelete) {
deleteChild(returnFiber, childToDelete);
childToDelete = childToDelete.sibling;
}
}
3.key相同,类型相同,复用老节点,只更新属性
type如果是相同的,那就可以直接使用原来的fiber,并且更新属性
while (child) {
//老fiber的ekey和新的虚拟DOM的key相同说明
if (child.key === key) {
//判断老的fiber的type和新的虚拟DOMtype是否相同
if (child.type == element.type) {
+ //准备复用child老fiber节点,删除剩下的其它fiber
+ deleteRemainingChildren(returnFiber, child.sibling);
+ //在复用老fiber的时候,会传递新的虚拟DOM的属性对象到新fiber的pendingProps上
+ const existing = useFiber(child, element.props);
+ existing.return = returnFiber;
+ return existing;
} else {
//已经配上key了,但是type不同,则删除包括当前的老fiber在内所所有后续的老fibe
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
//如果相同说明当前这个老fiber不是对应于新的虚拟DOM节点 把此老fiber标记为删除,并且继续弟弟
deleteChild(returnFiber, child);
}
//继续匹配弟弟们
child = child.sibling;
}
复用老fiber,并清空弟弟
function useFiber(oldFiber, pendingProps) {
let clone = createWorkInProgress(oldFiber, pendingProps);
clone.sibling = null;//清空弟弟
return clone;
}
这些操作是执行在beginWork中,并没有正真的去删除掉,只是在这个地方做了标记,在commit阶段才会正真的去处理。
export function completeWork(current, workInProgress) {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostComponent:
// 在新的fiber构建完成的时候,收集更新并且标识 更新副作用
if (current && workInProgress.stateNode) {
updateHostComponent(current, workInProgress, workInProgress.tag, newProps);
} else {
//创建真实的DOM节点
const type = workInProgress.type;//div p span
//创建此fiber的真实DOM
const instance = createInstance(type, newProps);
//让此Fiber的真实DOM属性指向instance
workInProgress.stateNode = instance;
//给真实DOM添加属性 包括如果独生子是字符串或数字的情况
finalizeInitialChildren(instance, type, newProps);
}
break;
default:
break;
}
}
current && workInProgress.stateNode 就说明是更新操作了,在更新阶段,执行updateHostComponent
/**
*
* @param {*} current 老fiber
* @param {*} workInProgress 新fiber
* @param {*} tag
* @param {*} newProps 新的虚拟DOM上的新属性
*/
function updateHostComponent(current, workInProgress, tag, newProps) {
//老fiber上的老属性
let oldProps = current.memoizedProps;
//可复用真实的DOM节点
const instance = workInProgress.stateNode;
const updatePayload = prepareUpdate(instance, tag, oldProps, newProps);
workInProgress.updateQueue = updatePayload;
if (updatePayload) {
workInProgress.flags |= Update;
//当flags=6的话,就是既要插入新位置 ,又要更新,针对移动 节点的情况
}
}
prepareUpdate方法,主要是用来更新属性。
export function prepareUpdate(domElement, type, oldProps, newProps) {
return diffProperties(
domElement,
type,
oldProps,
newProps
);
}
export function diffProperties(domElement, tag, lastProps, nextProps) {
let updatePayload = null;
let propKey;
for (propKey in lastProps) {
if (lastProps.hasOwnProperty(propKey) && ( !nextProps.hasOwnProperty(propKey) )) {
//updatePayload更新数组 [更新的key1,更新的值1,更新的key2,更新的值2]
( updatePayload = updatePayload || [] ).push(propKey, null);
}
}
for (propKey in nextProps) {
const nextProp = nextProps[propKey];
if (propKey == 'children') {
if (typeof nextProp === 'string' || typeof nextProp === 'number') {
if (nextProp !== lastProps[propKey]) {
( updatePayload = updatePayload || [] ).push(propKey, nextProp);
}
}
} else {
//如果新的属性和老的属性不一样
if (nextProp !== lastProps[propKey]) {
( updatePayload = updatePayload || [] ).push(propKey, nextProp);
}
}
}
return updatePayload;
}
接下来,还需要处理commitMutationEffects,这个是commitRoot中执行的,之前只处理了Placement的情况,其他情况也需要处理。
function commitMutationEffects(root) {
const finishedWork = root.finishedWork;
let nextEffect = finishedWork.firstEffect;
let effectsList = '';
while (nextEffect) {
effectsList += `(${getFlags(nextEffect.flags)}#${nextEffect.type}#${nextEffect.key})=>`;
const flags = nextEffect.flags;
let current = nextEffect.alternate;
if (flags === Placement) {
commitPlacement(nextEffect);
} else if (flags === PlacementAndUpdate) {
commitPlacement(nextEffect);
nextEffect.flags = Update;
commitWork(current, nextEffect);
} else if (flags === Update) {
commitWork(current, nextEffect);
} else if (flags === Deletion) {
commitDeletion(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
effectsList += 'null';
console.log(effectsList);
root.current = finishedWork;
}
commitDeletion 删除操作
export function commitDeletion(fiber) {
if (!fiber) return;
let parentStateNode = getParentStateNode(fiber);
removeChild(parentStateNode, fiber.stateNode);
}
export function removeChild(parentInstance, child) {
parentInstance.removeChild(child);
}
commitWork提交DOM更新操作
/**
* 提交DOM更新操作
* @param {*} current
* @param {*} finishedWork
*/
export function commitWork(current, finishedWork) {
const updatePayload = finishedWork.updateQueue;
finishedWork.updateQueue = null;
if (updatePayload) {
updateProperties(finishedWork.stateNode, updatePayload);
}
}
updateProperties更新属性
export function updateProperties(domElement, updatePayload) {
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === 'children') {
domElement.textContent = propValue;
} else {
domElement.setAttribute(propKey, propValue);// id
}
}
}
这个地方i+=2是因为updatePayload更新数组是 [更新的key1,更新的值1,更新的key2,更新的值2]这样的存储数据格式;
测试1:
index.html
<div>
<button id="single1">1.key相同,类型相同,数量相同button><br/>
<div key="title" id="title"><br/>
div<br/>
</div><br/>
<button id="single1Update">复用老节点,只更新属性button><br/>
<div key="title" id="title2"><br/>
div2<br/>
</div>
div>
<hr/>
index.js
//1. key相同,类型相同,复用老节点,只更新属性
single1.addEventListener('click', () => {
let element = (
<div key="title" id="title">title</div>
);
ReactDOM.render(element, root);
});
single1Update.addEventListener('click', () => {
let element = (
<div key="title" id="title2">title2</div>
);
ReactDOM.render(element, root);
});
页面结果:
控制台也可以清楚的看到插入和更新的操作。
测试2:
<div>
<button id="single2">2.key相同,类型不同button><br/>
<div key="title" id="title"><br/>
div<br/>
</div><br/>
<button id="single2Update">删除老节点,添加新节点button><br/>
<p key="title" id="title"><br/>
p<br/>
</p><br/>
div>
<hr/>
//2.key相同,类型不同,删除老节点,添加新节点
single2.addEventListener('click', () => {
let element = (
<div key="title" id="title">title</div>
);
ReactDOM.render(element, root);
});
single2Update.addEventListener('click', () => {
let element = (
<p key="title" id="title">title</p>
);
ReactDOM.render(element, root);
});
可以看到先执行了删除div的操作,并且执行了插入P标签的操作。
测试3:
<div>
<button id="single3">3.类型相同,key不同button><br/>
<div key="title1" id="title"><br/>
title<br/>
</div><br/>
<button id="single3Update">删除老节点,添加新节点button><br/>
<div key="title2" id="title"><br/>
title<br/>
</div><br/>
div>
<hr/>
single3.addEventListener('click', () => {
let element = (
<div key="title1" id="title">title</div>
);
ReactDOM.render(element, root);
});
single3Update.addEventListener('click', () => {
let element = (
<div key="title2" id="title">title</div>
);
ReactDOM.render(element, root);
});
类型相同,key不同,删除老节点,添加新节点,先执行了删除div的操作,并且执行了插入div标签的操作。
测试4:
<div>
<button id="single4">4.原来多个节点,现在只有一个节点button><br/>
<ul key="ul"><br/>
<li key="A">A</li><br/>
<li key="B" id="B">B</li><br/>
<li key="C">C</li><br/>
</ul><br/>
<button id="single4Update">保留并更新这一个节点,删除其它节点button><br/>
<ul key="ul"><br/>
<li key="B" id="B2">B2</li><br/>
</ul><br/>
div>
<hr/>
//4.原来多个节点,现在只有一个节点,删除多余节点
single4.addEventListener('click', () => {
let element = (
<ul key="ul">
<li key="A">A</li>
<li key="B" id="B">B</li>
<li key="C">C</li>
</ul>
);
ReactDOM.render(element, root);
});
single4Update.addEventListener('click', () => {
let element = (
<ul key="ul">
<li key="B" id="B2">B2</li>
</ul>
);
ReactDOM.render(element, root);
});
测试发现,第一次渲染都没出来结果:
4.原来多个节点,现在只有一个节点,删除多余节点
这个是因为reconcileChildFibers的时候,目前只处理了单节点的情况,看代码:
function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
//判断newChild是不是一个对象,如果是的话说明新的虚拟DOM只有一个React元素节点
const isObject = typeof newChild === 'object' && ( newChild );
//说明新的虚拟DOM是单节点
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(
returnFiber, currentFirstChild, newChild
))
}
}
}
如果newChild是数组的话,是没有处理的,接下来处理这个
function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
//判断newChild是不是一个对象,如果是的话说明新的虚拟DOM只有一个React元素节点
const isObject = typeof newChild === 'object' && ( newChild );
//说明新的虚拟DOM是单节点
if (isObject) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(
returnFiber, currentFirstChild, newChild
))
}
}
// 处理多个虚拟DOM
if (Array.isArray(newChild)) {
return reconcileChildrenArray(returnFiber, currentFirstChild, newChild);
}
}
/**
* 如果新的虚拟DOM是一个数组的话, 也就是说有多个儿子的话
* @param {*} returnFiber ui
* @param {*} currentFirstChild null
* @param {*} newChild [liA,liB,liC]
*/
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
//将要返回的第一个新fiber
let resultingFirstChild = null;
//上一个新fiber
let previousNewFiber = null;
//当前的老fiber
let oldFiber = currentFirstChild;
//新的虚拟DOM的索引
let newIdx = 0;
// 如果没有老fiber,也就是初次挂载的时候
if (!oldFiber) {
// 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
if (!previousNewFiber) {
resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
} else {
previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
}
previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
}
return resultingFirstChild;
}
}
function createChild(returnFiber, newChild) {
const created = createFiberFromElement(newChild);
created.return = returnFiber;
return created;
}
createChild是根据新的虚拟dom创建一个fiber(created),并将它的return(父亲)指向传进来的returnFiber,并返回。
按照上图的fiber新旧节点来看,第一轮循环就是创建A,previousNewFiber肯定不存在,resultingFirstChild(将要返回的第一个新fiber)就指向了A,previousNewFiber也指向了A;第二轮的时候previousNewFiber有值,指向的是A,那就是liA.sibling=li(B),previousNewFiber指向了li(B);第三轮的时候,以此内推,liB.sibling=li©,previousNewFiber指向li©。
执行之后,可以看到有ul插入,但是没有li,这个是为啥呢?原因是在第一次reconcileChildren的时候,也就是在mountChildFibers的时候,current是null,current.child自然也不存在,应该是null,是null的话,export const mountChildFibers = childReconciler(false);shouldTrackSideEffects这个值就是false,没有给新的fiber添加flags,li自然是没有操作的。
function placeSingleChild(newFiber) {
//如果当前需要跟踪副作用,并且当前这个新的fiber它的替身不存在
if (shouldTrackSideEffects && !newFiber.alternate) {
//给这个新fiber添加一个副作用,表示在未来提前阶段的DOM操作中会向真实DOM树中添加此节点
newFiber.flags = Placement;
}
return newFiber;
}
下面是修改前后的对比:
export function reconcileChildren(current, workInProgress, nextChildren) {
//如果current有值,说明这是一类似于更新的作品
if (current) {
//进行比较 新老内容,得到差异进行更新
workInProgress.child = reconcileChildFibers(
workInProgress,//新fiber
current.child,//老fiber的第一个子fiber节点
nextChildren //新的虚拟DOM
);
} else {
///初次渲染,不需要比较 ,全是新的
workInProgress.child = mountChildFibers(
workInProgress,//新fiber
- current && current.child,//老fiber的第一个子fiber节点
+ null,//老fiber的第一个子fiber节点
nextChildren //新的虚拟DOM
);
}
}
我们可以在reconcileChildrenArray循环newChildren的时候,手动加上newFiber.flags = Placement
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
//将要返回的第一个新fiber
let resultingFirstChild = null;
//上一个新fiber
let previousNewFiber = null;
//当前的老fiber
let oldFiber = currentFirstChild;
//新的虚拟DOM的索引
let newIdx = 0;
// 如果没有老fiber,也就是初次挂载的时候
if (!oldFiber) {
// 循环虚拟DOM数组, 为每个虚拟DOM创建一个新的fiber
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(returnFiber, newChildren[newIdx]);//li(A) =>li(B)=> li(C)
+ newFiber.flags = Placement
if (!previousNewFiber) {
resultingFirstChild = newFiber;//resultingFirstChild=>li(A)
} else {
previousNewFiber.sibling = newFiber;//liA.sibling=li(B) => liB.sibling=li(C)
}
previousNewFiber = newFiber;//previousNewFiber=>li(A)=>li(B) => li(C)
}
return resultingFirstChild;
}
}
然后再来看下结果:
可以看到插入的顺序和结果,但是源码中初次渲染的并没有搜集副作用,而是在完成的时候(completeWork)的时候调用appendAllChildren(instance, workInProgress);
export function completeWork(current, workInProgress) {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostComponent:
//在新的fiber构建完成的时候,收集更新并且标识 更新副作用
if (current && workInProgress.stateNode) {
updateHostComponent(current, workInProgress, workInProgress.tag, newProps);
} else {
//创建真实的DOM节点
const type = workInProgress.type;//div p span
//创建此fiber的真实DOM
const instance = createInstance(type, newProps);
+ appendAllChildren(instance, workInProgress);
//让此Fiber的真实DOM属性指向instance
workInProgress.stateNode = instance;
//给真实DOM添加属性 包括如果独生子是字符串或数字的情况
finalizeInitialChildren(instance, type, newProps);
}
break;
default:
break;
}
}
function appendAllChildren(parent, workInProgress) {
let node = workInProgress.child;
while (node) {
if (node.tag === HostComponent) {
//把大儿子的真实DOM节点添加到父真实DOM上
appendChild(parent, node.stateNode);
}
if (node === workInProgress) {
return;
}
while (!( node.sibling )) {
if (!( node.return ) || node.return === workInProgress) {
return;
}
node = node.return;
}
node = node.sibling;
}
}
正真的DOM操作appendChild
export function appendChild(parentInstance, child) {
parentInstance.appendChild(child);
}
然后去掉newFiber.flags = Placement这个,再来看下结果:
最后看下更新的操作结果:
到这里,单节点的dom-diff已经是实现,测试也是ok的。