react Dom-diff之初次渲染
创建项目,修改内容yarn create react-app react-dom-diff使用react官方脚手架create-react-app搭建处理的项目如下:react-dom-diff├── README.md├── node_modules├── package.json├── .gitignore├── public│├── favicon.ico│├── index.html│└─
创建项目,修改内容
yarn create react-app react-dom-diff
使用react官方脚手架create-react-app搭建处理的项目如下:
react-dom-diff
├── README.md
├── node_modules
├── package.json
├── .gitignore
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
└── src
├── App.css
├── App.js
├── App.test.js
├── index.css
├── index.js
├── logo.svg
└── serviceWorker.js
将不需要的文件删除,src 保留index.js,并修改如下:
import React from 'react';
import ReactDOM from 'react-dom';
let element = <div key="title" id="title">title</div>
ReactDOM.render(element, document.getElementById('root'));
很显然,页面上会渲染出来一个div,内容是title,react是怎么实现这整个流程渲染的呢?
createElement方法的实现
我们先来打印下element是什么内容,看下长什么样子?
为什么会长这样?首先是使用了jsx语法,
let element = <div key="title" id="title">title</div>
这个有趣的标签语法既不是字符串也不是 HTML。
它被称为 JSX,是一个 JavaScript 的语法扩展。我们建议在 React 中配合使用 JSX,JSX 可以很好地描述 UI 应该呈现出它应有交互的本质形式。JSX 可能会使人联想到模版语言,但它具有 JavaScript 的全部功能。
react中jsx介绍了为什么要使用jsx以及jsx的优点,还有如何使用,这里就不在介绍了,来解释一下为啥长这个样子,Babel 会把 JSX 转译成一个名为 React.createElement()
函数调用。
以下两种示例代码完全等效:
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
使用babel在线工具能看到babel解析的结果:
那就说明react中有createElement这个方法,来实现下:
react.js
mport { REACT_ELEMENT_TYPE } from './ReactSymbols'
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true
}
/**
* 创建虚拟DOM
* @param {*} type 元素的类型
* @param {*} config 配置对象
* @param {*} children 第一个儿子,如果有多个儿子的话会依次放在后面
*/
function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
if (config) {
if (config.key) {
key = config.key;
}
if (config.ref) {
ref = config.ref;
}
for (propName in config) {
if (!RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName];
}
}
}
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = new Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
}
return {
$$typeof: REACT_ELEMENT_TYPE,//类型是一个React元素
type,
ref,
key,
props
}
}
const React = {
createElement
}
export default React;
ReactSymbols.js
export const REACT_ELEMENT_TYPE = Symbol.for('react.element');
配置下启动脚本,package.json中修改如下:
"scripts": {
"start": "set DISABLE_NEW_JSX_TRANSFORM=true&&react-scripts start",
"build": "set DISABLE_NEW_JSX_TRANSFORM=true&&react-scripts build",
"test": "set DISABLE_NEW_JSX_TRANSFORM=true&&react-scripts test",
"eject": "set DISABLE_NEW_JSX_TRANSFORM=true&&react-scripts eject"
},
修改这个原因是新版本react换了jsx的编译器,目的是为了使用我们自己写的createElement
修改React的引入路径, ReactDOM暂时还使用官方的。
import React from './react';
重新启动编译下,可以看到打印log如下:
这个结果跟使用官方的基本一致,某些属性我们暂时没有做添加。
ReactDOM.render的实现
ReactDOM.render(element, container[, callback])
在提供的
container
里渲染一个 React 元素,并返回对该组件的引用(或者针对无状态组件返回null
)。如果 React 元素之前已经在container
里渲染过,这将会对其执行更新操作,并仅会在必要时改变 DOM 以映射最新的 React 元素。如果提供了可选的回调函数,该回调将在组件被渲染或更新之后被执行。
react-dom.js
import { createFiberRoot } from './ReactFiberRoot';
import { updateContainer } from './ReactFiberReconciler';
function render(element, container) {
let fiberRoot = createFiberRoot(container);
updateContainer(element, fiberRoot);
}
const ReactDOM = {
render
}
export default ReactDOM;
let fiberRoot = createFiberRoot(container);
updateContainer(element, fiberRoot);
其中,这2行代码是比较核心的,createFiberRoot函数就是根据容器对象( div#root)来生成一个根fiber。
关于fiber架构以及调度我会单独再用文章去做阐述,我们这里只简单了解下fiber。
fiber是什么?
- 为了让渲染的过程可以不断,我们可以把整个渲染任务分成若干个task(工作单元),每个工作单元就是一个fiber
- 每个虚拟DOM节点内部表示为一个fiber对象
- render阶段会根据虚拟DOM以深度优先的方式构建fiber
fiber数据结构
function FiberNode(){
this.tag = tag; // fiber 标签 证明是什么类型fiber。
this.key = key; // key调和子节点时候用到。
this.type = null; // dom元素是对应的元素类型,比如div,组件指向组件对应的类或者函数。
this.stateNode = null; // 指向对应的真实dom元素,类组件指向组件实例,可以被ref获取。
this.return = null; // 指向父级fiber
this.child = null; // 指向子级fiber
this.sibling = null; // 指向兄弟fiber
this.index = 0; // 索引
this.ref = null; // ref指向,ref函数,或者ref对象。
this.pendingProps = pendingProps;// 在一次更新中,代表element创建
this.memoizedProps = null; // 记录上一次更新完毕后的props
this.updateQueue = null; // 类组件存放setState更新队列,函数组件存放
this.memoizedState = null; // 类组件保存state信息,函数组件保存hooks信息,dom元素为null
this.dependencies = null; // context或是时间的依赖项
this.mode = mode; //描述fiber树的模式,比如 ConcurrentMode 模式
this.effectTag = NoEffect; // effect标签,用于收集effectList
this.nextEffect = null; // 指向下一个effect
this.firstEffect = null; // 第一个effect
this.lastEffect = null; // 最后一个effect
this.expirationTime = NoWork; // 通过不同过期时间,判断任务是否过期, 在v17版本用lane表示。
this.alternate = null; //双缓存树,指向缓存的fiber。更新阶段,两颗树互相交替。
}
type 就是react的元素类型
export const FunctionComponent = 0; // 对应函数组件
export const ClassComponent = 1; // 对应的类组件
export const IndeterminateComponent = 2; // 初始化的时候不知道是函数组件还是类组件
export const HostRoot = 3; // Root Fiber 可以理解为跟元素 , 通过reactDom.render()产生的根元素
export const HostPortal = 4; // 对应 ReactDOM.createPortal 产生的 Portal
export const HostComponent = 5; // dom 元素 比如 <div>
export const HostText = 6; // 文本节点
export const Fragment = 7; // 对应 <React.Fragment>
export const Mode = 8; // 对应 <React.StrictMode>
export const ContextConsumer = 9; // 对应 <Context.Consumer>
export const ContextProvider = 10; // 对应 <Context.Provider>
export const ForwardRef = 11; // 对应 React.ForwardRef
export const Profiler = 12; // 对应 <Profiler/ >
export const SuspenseComponent = 13; // 对应 <Suspense>
export const MemoComponent = 14; // 对应 React.memo 返回的组件
再回来看下createFiberRoot的实现
export function createFiberRoot(containerInfo) {
const fiberRoot = { containerInfo };//fiberRoot指的就是容器对象containerInfo div#root
//创建fiber树的根节点
const hostRootFiber = createHostRootFiber();
//当前的fiberRoot的current指向这个根fiber
//current当前的意思,它指的当前跟我们页面中真实DOM相同的fiber树
fiberRoot.current = hostRootFiber;
//让此根fiber的真实DOM节点指向fiberRoot div#root stateNode就是指的真实DOM的意思
hostRootFiber.stateNode = fiberRoot;
return fiberRoot;
}
createHostRootFiber的实现如下
import { HostComponent, HostRoot } from './ReactWorkTags';
export function createHostRootFiber() {
return createFiber(HostRoot);
}
HostRoot其实就是对应的3,也就是对应的根元素
/**
* 创建fiber节点
* @param {*} tag fiber的标签 HostRoot指的是根节点 div span HostComponent
* @param {*} pendingProps 等待生效的属性对象
* @param {*} key
*/
function createFiber(tag, pendingProps, key) {
return new FiberNode(tag, pendingProps, key);
}
FiberNode是一个构造函数,createFiber就是根据不同类型,生成不同的fiber节点
function FiberNode(tag, pendingProps, key) {
this.tag = tag;
this.pendingProps = pendingProps;
this.key = key;
}
FiberNode其实有很多属性,暂且只添加这3个,updateContainer(element, fiberRoot)这个暂时没有实现,先注释掉,打印下fiberRoot看下长什么样:
function render(element, container) {
let fiberRoot = createFiberRoot(container);
+ console.log(fiberRoot);
+ // updateContainer(element, fiberRoot);
}
用下图可以表示出fiberRoot与hostRootFiber的关系:
到此,我们只是描述出了一个节点对应的fiber,渲染或者更新的时候fiber内部是如何知道怎么更新的呢?
export function createFiberRoot(containerInfo) {
const fiberRoot = { containerInfo };//fiberRoot指的就是容器对象containerInfo div#root
//创建fiber树的根节点
const hostRootFiber = createHostRootFiber();
//当前的fiberRoot的curent指向这个根fiber
//current当前的意思,它指的当前跟我们页面中真实DOM相同的fiber树
fiberRoot.current = hostRootFiber;
//让此根fiber的真实DOM节点指向fiberRoot div#root stateNode就是指的真实DOM的意思
hostRootFiber.stateNode = fiberRoot;
+ initializeUpdateQueue(hostRootFiber);
return fiberRoot;
}
实际上在==createFiberRoot()==的时候会有一个initializeUpdateQueue(hostRootFiber)的操作,初始化更新队列,所有的fiber都会等待理更新内容放在更新队列中,来看下initializeUpdateQueue的实现:
initializeUpdateQueue()
export function initializeUpdateQueue(fiber) {
const updateQueue = {
shared: {
pending: null
}
}
fiber.updateQueue = updateQueue;
}
export function createUpdate() {
return {};
}
这个其实就很简单,给fiber添加了一个updateQueue的属性,形成一个环状链表,如下图:
initializeUpdateQueue不仅有createUpdate方法还有enqueueUpdate方法,enqueueUpdate的操作就是向当前的fiber的更新队列中添加一个更新,来实现下:
/**
* 向当前的fiber的更新队列中添加一个更新
* @param {*} fiber
* @param {*} update
*/
export function enqueueUpdate(fiber, update) {
let updateQueue = fiber.updateQueue;
const sharedQueue = updateQueue.shared;
const pending = sharedQueue.pending;
if (!pending) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
怎么理解这个呢,首先在initializeUpdateQueue的时候,fiber添加了一个updateQueue,而updateQueue中有shared,shared中有个pending,这些都很好理解。
if (!pending) {
update.next = update;
}
pending为null的时候,把这个更新的next指向自己,什么意思呢,假如现在有个update1,它的指向如下图:
sharedQueue.pending = update;
在最后面有这样的一个执行,执行完之后就可以用下图来表示:
else {
update.next = pending.next;
pending.next = update;
}
如果还有下次更新那么就会走到else分支,如果还有个update2,通过update.next = pending.next操作之后就相当于update2.next = update1.next,然后把pending的next指向这次更新,如下2图所示:
上图为update.next = pending.next;
上图为pending.next = update;
如果有更多,那就像下图一样,依次往下:
pending永远都指向最新的更新,pending.next永远指向第一个更新
initializeUpdateQueue()测试
initializeUpdateQueue.test.js
function initializeUpdateQueue(fiber) {
const updateQueue = {
shared: {
pending: null
}
}
fiber.updateQueue = updateQueue;
}
function createUpdate() {
return {};
}
function enqueueUpdate(fiber, update) {
let updateQueue = fiber.updateQueue;
const sharedQueue = updateQueue.shared;
const pending = sharedQueue.pending;
if (!pending) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
let fiber = { baseState: { number: 0 } };
initializeUpdateQueue(fiber);
const update1 = createUpdate();
update1.payload = { number: 1 };//update1 = {payload:{number:1}}
//把update1添加到更新队列链表里
enqueueUpdate(fiber, update1);
const update2 = createUpdate();
update2.payload = { number: 2 };//update2 = {payload:{number:2}}
//把update2添加到更新队列链表里
enqueueUpdate(fiber, update2);
const update3 = createUpdate();
update3.payload = { number: 3 };//update3 = {payload:{number:3}}
//把update1添加到更新队列链表里
enqueueUpdate(fiber, update3);
console.log(fiber.updateQueue.shared.pending);
可以看到结果是pending指向{number:3},它的next指向{number:1},是一个循环链表的结构。
再看看fiberRoot的打印结果,fiberRoot.current中就增加了一个updateQuene,现在还没有更新,所以它的pending就是null。那接下来,我们需要处理什么呢,没错就是在render注释掉的updateContainer()方法。
updateContainer()
updateContainer的作用就是把虚拟DOM element变成真实DOM插入或者说渲染到container容器中
import { createUpdate, enqueueUpdate } from './ReactUpdateQueue';
import { scheduleUpdateOnFiber } from './ReactFiberWorkLoop';
/**
* 把虚拟DOM element变成真实DOM插入或者说渲染到container容器中
* @param {*} element
* @param {*} container
*/
export function updateContainer(element, container) {
//获取hostRootFiber fiber根的根节点
//正常来说一个fiber节点会对应一个真实DOM节点,hostRootFiber对应的DOM节点就是containerInfo div#root
const current = container.current;
const update = createUpdate();
update.payload = { element };
enqueueUpdate(current, update);
scheduleUpdateOnFiber(current);
}
createUpdate和enqueueUpdate在之前已经实现过验证过了,scheduleUpdateOnFiber顾名思义就是调度fiber的更新队列,接下来我们实现scheduleUpdateOnFiber这个方法。
scheduleUpdateOnFiber()的实现
export function scheduleUpdateOnFiber(fiber) {
const fiberRoot = markUpdateLaneFromFiberToRoot(fiber);
performSyncWorkOnRoot(fiberRoot);
}
这个方法,不管如何更新,不管谁来更新,都会调度到这个方法里,markUpdateLaneFromFiberToRoot就是根据当前fiber节点找到根fiber,并且返回了根fiber对应的真实DOM节点,来实现下:
function markUpdateLaneFromFiberToRoot(sourceFiber) {
let node = sourceFiber;
let parent = node.return;
while (parent) {
node = parent;
parent = parent.return;
}
//node其实肯定fiber树的根节点,其实就是 hostRootFiber .stateNode div#root
return node.stateNode;
}
在拿到根节点之后,执行performSyncWorkOnRoot(fiberRoot),它的主要用途就是根据老的fiber树和更新对象创建新的fiber树,然后根据新的fiber树更新真实DOM。具体如下:
// 当前正在更新的根
let workInProgressRoot = null;
// 当前正在更新fiber节点
let workInProgress = null;
/**
* 根据老的fiber树和更新对象创建新的fiber树,然后根据新的fiber树更新真实DOM
* @param {*} fiberRoot
*/
function performSyncWorkOnRoot(fiberRoot) {
workInProgressRoot = fiberRoot;
workInProgress = createWorkInProgress(workInProgressRoot.current);
}
创建了2个变量,workInProgressRoot和workInProgress,分别是当前正在更新的根和当前正在更新fiber节点。
双缓冲
双缓存机制是一种在内存中构建并直接替换的技术。React在协调的过程中就使用了这种技术。
在React中同时存在着两棵fiber tree。一棵是在屏幕上显示的dom对应的fiber tree,称为current fiber tree,而还有一棵是当触发新的更新任务时,React在内存中构建的fiber tree,称为workInProgress fiber tree。
current fiber tree和workInProgress fiber tree中的fiber节点通过alternate属性进行连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
React应用的根节点中也存在current属性,利用current属性在不同fiber tree的根节点之间进行切换的操作,就能够完成current fiber tree与workInProgress fiber tree之间的切换。
在协调阶段,React利用diff算法
,将产生update的React element
与current fiber tree
中的节点进行比较,并最终在内存中生成workInProgress fiber tree。此时Renderer会依据workInProgress fiber tree将update渲染到页面上。同时根节点的current属性会指向workInProgress fiber tree,此时workInProgress fiber tree就变为current fiber tree。
createWorkInProgress
createWorkInProgress就是根据老fiber创建新的fiber,具体实现如下:
/**
* 根据老fiber创建新的fiber
* @param {*} current 老fiber
* @param {*} pendingProps 等待生效的属性对象
*/
export function createWorkInProgress(current, pendingProps) {
let workInProgress = current.alternate;
if (!workInProgress) {
workInProgress = createFiber(current.tag, pendingProps, current.key);
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
}
workInProgress.flags = NoFlags;
workInProgress.child = null;
workInProgress.sibling = null;
workInProgress.updateQueue = current.updateQueue;
//在dom diff的过程会给fiber添加副作用
workInProgress.firstEffect = workInProgress.lastEffect = workInProgress.nextEffect = null;
return workInProgress;
}
createWorkInProgress在执行的时候,先去拿current.alternate,此时肯定是没有的,那我们就根据这个创建一个新的fiber,并且通过
workInProgress.alternate = current;
current.alternate = workInProgress;
workInProgress和current的alternate互相指向,这个也就是在react中的双缓冲使用了。
current和workInProgress就会在后面跟新的时候来回切换,workInProgress就是当前正在构建的fiber树,current代表的是和当前页面中真实DOM完全一样的fiber树,在后面dom-diff的时候会比较他们的区别。
可以从打印的日志中可以看到,current中多了alternate。到此render阶段根据vdom生成fiber树就此完成,接下来就是fiber树的工作了。
workLoopSync工作循环
function performSyncWorkOnRoot(fiberRoot) {
workInProgressRoot = fiberRoot;
workInProgress = createWorkInProgress(workInProgressRoot.current);
+ workLoopSync();//执行工作循环,构建副作用链
}
/**
* 开始自上而下构建新的fiber树
*/
function workLoopSync() {
while (workInProgress) {
performUnitOfWork(workInProgress);
}
}
workLoopSync的时候,react是有一个调度任务的,根据浏览器以及程序的优先级来判断是否药中断fibre的render。performUnitOfWork为处理每一个fiber,可以看到每个节点都会走beginWork和completeUnitOfWork,对应的就是fiber的开始和结束。unitOfWork.alternate前面也说过了对应的就是和当前页面中真实DOM完全一样的fiber树。
/**
* 执行单个工作单元
* unitOfWork 要处理的fiber
*/
function performUnitOfWork(unitOfWork) {
//获取当前正在构建的fiber的替身
const current = unitOfWork.alternate;
//开始构建当前fiber的子fiber链表
//它会返回下一个要处理的fiber,一般都是unitOfWork的大儿子
//div#title这个fiber 它的返回值是一个null
let next = beginWork(current, unitOfWork);
//在beginWork后,需要把新属性同步到老属性上
//div id =1 memoizedProps={id:2} pendingProps ={id:2}
unitOfWork.memoizedProps = unitOfWork.pendingProps;
//当前的fiber还有子节点
if (next) {
workInProgress = next;
} else {
//如果当前fiber没有子fiber,那么当前的fiber就算完成
completeUnitOfWork(unitOfWork);
}
}
beginWork就是构建当前fiber的子fiber链表,并且返回下一个要处理的fiber。
/**
* 创建当前fiber的子fiber
* @param current
* @param workInProgress
* @returns {*} 下一个fiber
*/
export function beginWork(current, workInProgress) {
switch (workInProgress.tag) {
case HostRoot:
return updateHostRoot(current, workInProgress);
case HostComponent:
return updateHostComponent(current, workInProgress);
default:
break;
}
}
Fiber 执行阶段
- 每次渲染有两个阶段:Reconciliation(协调\render 阶段)和 Commit(提交阶段)
- 协调阶段: 可以认为是 Diff 阶段, 这个阶段可以被中断, 这个阶段会找出所有节点变更,例如节点新增、删除、属性变更等等, 这些变更 React 称之为副作用(Effect)
- 提交阶段: 将上一个阶段计算出来的需要处理的副作用(Effects)一次性执行了。这个阶段必须同步执行,不能被打断
render 阶段遍历规则
- render 阶段会构建 fiber 树
let A1 = { type: "div", key: "A1" };
let B1 = { type: "div", key: "B1", return: A1 };
let B2 = { type: "div", key: "B2", return: A1 };
let C1 = { type: "div", key: "C1", return: B1 };
let C2 = { type: "div", key: "C2", return: B1 };
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2;
module.exports = A1;
-
从顶点开始遍历
-
如果有第一个儿子,先遍历第一个儿子
-
如果没有第一个儿子,标志着此节点遍历完成
-
如果有弟弟遍历弟弟
-
如果有没有下一个弟弟,返回父节点标识完成父节点遍历,如果有叔叔遍历叔叔
-
没有父节点遍历结束
-
遍历规则
-
1.下一个节点:先ル子,后弟弟,再叔叔
-
2.自己所有子节点完成后自己完成
-
先儿子,后弟弟,再叔叔,辈份越小越优先
-
什么时候一个节点遍历完成? 没有子节点,或者所有子节点都遍历完成了
-
没爹了就表示全部遍历完成了
上图就是对应节点的fiber树,下图是节点遍历的顺序
beginWork的时候根据fiber的tag(类型)进行不同节点的更新。
/**
* 更新或者说挂载根节点
* 依据什么构建fiber树? 虚拟DOM
* @param {*} current 老fiber
* @param {*} workInProgress 构建中的新fiber
*/
function updateHostRoot(current, workInProgress) {
const updateQueue = workInProgress.updateQueue;
//获取要渲染的虚拟DOM <div key="title" id="title">title</div>
const nextChildren = updateQueue.shared.pending.payload.element;//element
//处理子节点,根据老fiber和新的虚拟DOM进行对比,创建新的fiber树
reconcileChildren(current, workInProgress, nextChildren);
//返回第一个子fiber
return workInProgress.child;
}
function updateHostComponent(current, workInProgress) {
//获取 此原生组件的类型 span p
const type = workInProgress.type;
//新属性
const nextProps = workInProgress.pendingProps;//props.children
let nextChildren = nextProps.children;
//在react对于如果一个原生组件,它只有一个儿子,并且这个儿子是一个字符串的话,有一个优化
//不会对此儿子创建一个fiber节点,而是把它当成一个属性来处理
let isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) {
nextChildren = null;
}
//处理子节点,根据老fiber和新的虚拟DOM进行对比,创建新的fiber树
reconcileChildren(current, workInProgress, nextChildren);
//返回第一个子fiber
return workInProgress.child;
}
不管是什么样类型的fiber(根fiber,类组件,函数组件,原生DOM…)都会走到reconcileChildren这个方法中,这个地方才是dom-diff真正执行的阶段。
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节点
nextChildren //新的虚拟DOM
);
}
}
reconcileChildFibers和mountChildFibers一个是更新,一个是初次渲染,所需要的参数都是一样,新建一个childReconciler函数:
childReconciler
function childReconciler(shouldTrackSideEffects) {
function reconcileChildFibers(returnFiber, currentFirstChild, newChild) {
}
return reconcileChildFibers;
}
export const reconcileChildFibers = childReconciler(true);
export const mountChildFibers = childReconciler(false);
通过shouldTrackSideEffects参数来控制是更新还是渲染操作。
/**
* @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
));
}
}
}
reconcileSingleElement协调单元素节点
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
const created = createFiberFromElement(element);//div#title
created.return = returnFiber;
return created;
}
/**
* 根据虚拟DOM元素创建fiber节点
* @param {*} element 虚拟节点
*/
export function createFiberFromElement(element) {
const { key, type, props } = element;
let tag;
if (typeof type === 'string') {// span div p
tag = HostComponent;//标签等于原生组件
}
const fiber = createFiber(tag, props, key);
fiber.type = type;
return fiber;
}
function placeSingleChild(newFiber) {
// 如果当前需要跟踪副作用,并且当前这个新的fiber它的替身不存在
if (shouldTrackSideEffects && !newFiber.alternate) {
//给这个新fiber添加一个副作用,表示在未来提前阶段的DOM操作中会向真实DOM树中添加此节点
newFiber.flags = Placement; // Placement创建或者挂载
}
return newFiber;
}
completeUnitOfWork
performUnitOfWork中已经处理了beginWork,接下来就是处理completeUnitOfWork
function completeUnitOfWork(unitOfWork) {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// 完成此fiber对应的真实DOM节点创建和属性赋值的功能
completeWork(current, completedWork);
} while (workInProgress);
}
export function completeWork(current, workInProgress) {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostComponent:
//创建真实的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;
}
}
createInstance和finalizeInitialChildren都是react封装的根据虚拟Dom的一些DOM操作,但是为了抹平平台的差异性,都会有对应平台的操作dom的方法,比如在浏览器环境createInstance就对应了document.createElement(type)方法。
export function createInstance(type) {
return createElement(type);
}
export function finalizeInitialChildren(domElement, type, props) {
setInitialProperties(domElement, type, props);
}
export function createElement(type) {
return document.createElement(type);
}
export function setInitialProperties(domElement, type, props) {
for (const propKey in props) {
const nextProp = props[propKey];
if (propKey === 'children') {
if (typeof nextProp === 'string' || typeof nextProp === 'number') {
domElement.textContent = nextProp;
}
} else if (propKey === 'style') {
for (let stylePropKey in nextProp) {
domElement.style[stylePropKey] = nextProp[stylePropKey]
}
} else {
domElement[propKey] = nextProp;
}
}
}
completeUnitOfWork 在完成此fiber对应的真实DOM节点创建和属性赋值的功能之后,我们还需要收集当前fiber的副作用到父fiber上。
function completeUnitOfWork(unitOfWork) {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// 完成此fiber对应的真实DOM节点创建和属性赋值的功能
completeWork(current, completedWork);
+ collectEffectList(returnFiber, completedWork);
} while (workInProgress);
}
collectEffectList
- 为了避免遍历fiber树寻找有副作用的节点,所有才有了effectList
- 在fiber树构建过程中,每当一个fiber节点的flags的字段不为noFlags时代表需要执行副作用,fiber节点添加到effectList中去
- effectList是一个单向链表,lastEffect代表链表中的第一个节点,lastEffect代表链表中最后一个节点
- fiber树的构建是深度优先遍历的,也就是先向下构建子级fiber节点,子级节点构建完成后,再向上构建父级fiber节点,所以effectList中总是子级fiber节点在最前面
- fiber节点构建完成的操作执行在completeUnitOfWork方法,在这个方法里,不仅会对节点完成构建,也会将有flag的fiber节点添加到effectList中去
let rootFiber = { key: 'rootFiber' };
let fiberA = { key: 'A', flags: Placement };
let fiberB = { key: 'B', flags: Placement };
let fiberC = { key: 'C', flags: Placement };
假设有这样的一个fiber树,对应的图如下:
对于这样的一个结构,按照fiber节点的遍历顺序,应该就是rootFiber->A-B-C
function collectEffectList(returnFiber, completedWork) {
const flags = completedWork.flags;
//如果此完成的fiber有副使用,那么就需要添加到effectList里
if (flags) {
returnFiber.firstEffect = completedWork;
returnFiber.lastEffect = completedWork;
}
}
第一步:
collectEffectList(fiberA, fiberB);
B把自己的fiber给A,那么用图表示就如下:
第二步:
collectEffectList(fiberA, fiberC);
C把自己的fiber给A,C应该是在B的后面,也就是B应该有个nextEffect指向C ,所以collectEffectList就修改如下:
function collectEffectList(returnFiber, completedWork) {
const flags = completedWork.flags;
//如果此完成的fiber有副使用,那么就需要添加到effectList里
if (flags) {
//如果父fiber有lastEffect的话,说明父fiber已经有effect链表
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
可以用下图来表示第二步的操作结果:
第三步:
collectEffectList(rootFiber, fiberA);
这个时候就需要把A收集好的给到rootFiber,靠目前这个逻辑是实现不了,只能把A给到rootFiber,A收集的BC都丢失了,再来继续改造下collectEffectList方法:
function collectEffectList(returnFiber, completedWork) {
+ //如果父亲 没有effectList,那就让父亲 的firstEffect链表头指向自己的头
+ if (!returnFiber.firstEffect) {
+ returnFiber.firstEffect = completedWork.firstEffect;
+ }
const flags = completedWork.flags;
//如果此完成的fiber有副使用,那么就需要添加到effectList里
if (flags) {
//如果父fiber有lastEffect的话,说明父fiber已经有effect链表
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
returnFiber没有firstEffect,说明没有effectList,那就让父亲 的firstEffect链表头指向自己的头,A的firstEffect不就是B么,可以用下图表示:
collectEffectList还要继续改造下:
function collectEffectList(returnFiber, completedWork) {
//如果父亲 没有effectList,那就让父亲 的firstEffect链表头指向自己的头
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = completedWork.firstEffect;
}
//如果自己有链表尾,说明自己身上是有链的
if (completedWork.lastEffect) {
//并且父亲也有链表尾,说明父亲身上也是有链的
if (returnFiber.lastEffect) {
//把自己身上的effectList挂接到父亲的链表尾部
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
const flags = completedWork.flags;
//如果此完成的fiber有副使用,那么就需要添加到effectList里
if (flags) {
//如果父fiber有lastEffect的话,说明父fiber已经有effect链表
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
第三步完成之后整个链表如下:
完整的代码逻辑如下:
function collectEffectList(returnFiber, completedWork) {
//如果父亲 没有effectList,那就让父亲 的firstEffect链表头指向自己的头
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = completedWork.firstEffect;
}
//如果自己有链表尾
if (completedWork.lastEffect) {
//并且父亲也有链表尾
if (returnFiber.lastEffect) {
//把自己身上的effectList挂接到父亲的链表尾部
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
const flags = completedWork.flags;
//如果此完成的fiber有副使用,那么就需要添加到effectList里
if (flags) {
//如果父fiber有lastEffect的话,说明父fiber已经有effect链表
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
测试下collectEffectList的结果
let rootFiber = { key: 'rootFiber' };
let fiberA = { key: 'A', flags: Placement };
let fiberB = { key: 'B', flags: Placement };
let fiberC = { key: 'C', flags: Placement };
collectEffectList(fiberA, fiberB);
collectEffectList(fiberA, fiberC);
collectEffectList(rootFiber, fiberA);
let effectList = '';
let nextEffect = rootFiber.firstEffect;
while (nextEffect) {
effectList += `${nextEffect.key}=>`;
nextEffect = nextEffect.nextEffect;
}
effectList += 'null';
console.log(effectList);
结果就如下:
B=>C=>A=>null
commitRoot
接下来就是fiber的commit提交阶段
function performSyncWorkOnRoot(fiberRoot) {
workInProgressRoot = fiberRoot;
workInProgress = createWorkInProgress(workInProgressRoot.current);
workLoopSync();//执行工作循环,构建副作用链
+ commitRoot();//提交,修改DOM
}
function commitRoot() {
//指向新构建的fiber树
const finishedWork = workInProgressRoot.current.alternate;
workInProgressRoot.finishedWork = finishedWork;
commitMutationEffects(workInProgressRoot);
}
function commitMutationEffects(root) {
const finishedWork = root.finishedWork;
let nextEffect = finishedWork.firstEffect;
let effectsList = '';
while (nextEffect) {
effectsList += `(${getFlags(nextEffect.flags)}#${nextEffect.type}#${nextEffect.key})`;
nextEffect = nextEffect.nextEffect;
}
effectsList += 'null';
console.log(effectsList);
root.current = finishedWork;
}
接下来就是要把真正的节点给插入
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;
}
function commitPlacement(nextEffect) {
let stateNode = nextEffect.stateNode;
let parentStateNode = getParentStateNode(nextEffect);
parentStateNode.appendChild(stateNode);
}
function getParentStateNode(fiber) {
let parent = fiber.return;
do {
if (parent.tag === HostComponent) {//原生元素
return parent.stateNode;
} else if (parent.tag === HostRoot) {//根
return parent.stateNode.containerInfo;
} else {
//函数组件或类组件
parent = parent.return;
}
} while (parent);
}
到此,节点在页面上显示出来,完成了react的初次渲染。下一节来正真的看下react中的dom-diff。
更多推荐
所有评论(0)