通过 miniReact 掌握 React 体系
前言
本篇文章翻译、学习自 build your own react,感兴趣的可以直接看原文。
通过一个 mini 版的 React,可以掌握 createElement()
、render()
、Fibers
、Render 阶段和 Commit 阶段
、Function Component
、Hooks
是如何在 React 中构建的。
首先,我们创建一个自己的类,并做初始化工作
const myReact = {};
// 下面这样注释了之后,babel 编译时就会调用 myReact 类身上的方法
/** @jsx myReact.createElement */
const container = document.getElementById("root");
createElement
React 中的 JSX 语法通过 babel 编译后才能被浏览器识别:
// jsx
const element = (
<div id='foo'>
<a>bar</a>
<b />
</div>
);
// babel 编译时,会默认调用 myReact.createElement 方法,因此等价于下面这种写法
const element = myReact.createElement(
"div",
{ id: "foo" },
React.createElement("a", null, "bar"),
React.createElement("b")
);
上面这段等价的代码中,createElement
就是将 JSX 转 VDOM 的函数,VDOM 结构如下:
const VDOM = {
type: "div",
props: {
id: "foo",
children: [
{
type: "a",
props: {
children: [
{
type: "TEXT",
props: {
nodeValue: "bar",
},
},
],
},
},
{
type: "b",
props: null,
},
],
},
};
我们发现上面的数据结构,始终包含 type
、props
两个属性,也就是说,createElement
返回的结果就是嵌套的 { type, props }
对象
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children,
},
};
}
但对于 children
来说,它可能是 文本节点
,也可能是嵌套的 dom节点
,所以要分情况处理
const myReact = {
//挂载方法到 myReact 类上
createElement,
}
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) =>
// 如果 child 的类型不是 object 就是文本节点
typeof child === "object" ? child : createTextElement(child)
),
},
};
}
// 对于文本节点,通过 createTextElement 方法返回对应的 VDOM
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: []
}
};
}
至此,createElement
方法就实现了,挺简单的
render
接下来,我们需要实现 myReact.render
方法,原本的 ReactDOM.render(element, container) 方法接受两个参数:
element
:要渲染的 React 元素container
:React 元素要挂载到哪个 DOM 上
在这里我们先只考虑添加元素
,删除、更新后面再说
在新增元素节点时,我们会根据 VDOM 的 type
字段,去创建对应的 DOM 节点,然后遍历 children
数组,对每一个 child
都做同样的工作。全部完成后,挂载 DOM 到 container
上
function render(element, container) {
// 根据 type 创建对应的 DOM 节点
const dom = document.createElement(element.type);
// 对 children 递归做处理
element.props.children.forEach((child) => render(child, dom));
// 将 dom 挂在到 container 上,也就是 root 根节点
container.appendChild(dom);
}
但是还没完,我们还需要判断当前节点是不是文本节点
,所以稍微改造一下
function render(element, container) {
// 判断是不是文本节点,是文本节点就通过 createTextNode() 去创建文本节点
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
element.props.children.forEach((child) => render(child, dom));
container.appendChild(dom);
}
现在是能创建节点了,但是 VDOM 里面的 props
还没赋值给节点呢,所以还需要处理 props
function render(element, container) {
const dom =
element.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
// 把除了 children 的 prop 挂载到 dom 节点上
const isProperty = (key) => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach((name) => {
dom[name] = element.props[name];
});
element.props.children.forEach((child) => render(child, dom));
container.appendChild(dom);
}
这样,我们就写好了 render,但请注意:这里我们是通过 递归操作DOM
的方式去实现的 render
方法,这种方式的坏处在于:如果 VDOM 树很大,一旦递归开始,直到整棵树完成渲染前,是不能停止的,如果此时有优先级更高的操作(比如用户输入),这会导致卡顿,render 占用 JS 主线程的时间过长,所以我们需要对代码进行改造,这里就涉及到了 Concurrent Mode
Concurrent Mode 是 React 的一种新的更新模式,他将渲染的工作分成多个工作单元 nextUnitOfWork
去做,如果此时有优先级更高的任务,则可以实现 异步可中断更新
的效果,让应用一直保持响应,也就是让用户不会觉得页面卡顿,带来更好的交互体验。这里就稍微提一下 concurrent Mode,有兴趣的可以自己去搜索看看,我们接下来分析改造代码
核心就是:把递归创建 DOM 的操作改为 多个工作单元
// 最小工作单元
let nextUnitOfWork = null;
function workLoop(deadline) {
let shouldYield = false;
// 是否有下一个工作单元需要执行,且此时还不需要让出主线程给浏览器
while (nextUnitOfWork && !shouldYield) {
// 调用 performUnitOfWork 函数,返回下一个工作单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
// 执行完一个工作单元就判断是否需要让出主线程控制权
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);
}
// 在浏览器空闲时调用 workLoop
requestIdleCallback(workLoop);
function performUnitOfWork(nextUnitOfWork) {
// TODO
}
在这里,我们使用 requestIdleCallback
调用 workLoop
。可以将 requestIdleCallback
视为 setTimeout,浏览器将会在主线程空闲时运行回调它。
而 React 新架构中不再使用 requestIdleCallback,而是通过现在它 Scheduler
调度任务,但是在这里我们是为了弄清 React 整体的大致流程,所以这里用 requestIdleCallback 来代替,不影响理解
shouldYield = deadline.timeRemaining() < 1;
是判断当前距离下一次浏览器掌控主线程还剩多少时间,如果剩余时间不足,就会让出线程控制器给浏览器,终止当前任务的渲染,在下一次浏览器空闲时再次循环调用 performUnitOfWork
,每一次 performUnitOfWork 都会处理当前工作单,并且返回下一个工作单元,直到所有的工作都做完,那就会进入 Commit阶段
Fibers
上面我们将任务分成多个工作单元,但为了管理组织这些工作单元,还需要 Fiber tree
。还记得 render 的第一个参数 element
吗?React 会为每一个 element 创建对应的 Fiber 节点,每一个 Fiber 节点就是一个最小工作单元
比如说:
myReact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
);
JSX 被 babel 编译后调用 createElement 转成 VDOM 树,然后 VDOM 树 会转 Fiber 树,这个 Fiber 树的结构如下图所示:
在调用 render 时,会创建一个根Fiber(root fiber)
,也就是 fiber 树的根节点,且它会作为第一个工作单元 nextUnitOfWork
,然后将工作单元传入 performUnitOfWork
里面处理,也就是相当于对每一个 fiber 节点进行处理(每一个 fiber 节点就是一个最小工作单元
),会做三件事:
- 根据 fiber 节点创建对应的 DOM 节点
- 对
children
也进行 fiber 节点的创建 - 返回下一个工作单元
其中,在第二步中,当前 React 元素的所有子节点都创建好了 fiber 节点后,就会得到像上图
中所示的数据结构:也就是 fiber 树
,三个索引 prop 分别为 parent
、child
、sibling
在第三步中,会按照 child
、sibling
、parent
的优先级顺序寻找下一个工作单元(也就是 fiber 节点),如果它最终找到了 root fiber
,就说明渲染的工作已经宣布完成,就进入 Commit
阶段
现在就需要重构 render 函数:
- 首先,我们把创建 DOM 的部分单独抽出来到
createDom
函数中
// 刚才也说了,是根据 fiber 节点去创建对应的 DOM
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
const isProperty = key => key !== "children"
Object.keys(fiber.props)
.filter(isProperty)
.forEach(name => {
dom[name] = fiber.props[name]
})
return dom
}
- 在 render 函数中,我们去设置第一个工作单元给
nextUnitOfWork
function render(element, container) {
nextUnitOfWork = {
dom: container,
props: {
children: [element],
},
}
}
let nextUnitOfWork = null;
- 然后,当浏览器空虚时,执行
workLoop
,而 workLoop 会去调用performUnitOfWork
方法处理 fiber(当前最小工作单元),分为三个步骤:根据 fiber 创建 dom
、对 children 也进行 fiber 节点的创建
、返回下一个工作单元
function performUnitOfWork(fiber) {
// 1、根据 fiber 创建 DOM
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// 2、对 children 也创建 fiber
const elements = fiber.props.children
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: fiber,
dom: null,
}
// index === 0,说明是子节点,所以当前 newFiber 挂载到 fiber.child 上
if (index === 0) {
fiber.child = newFiber
} else {
// 否则,当前 newFiber 就作为前一个 fiber 的兄弟节点
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
// 3、返回下一个工作单元(fiber),按照 child、sibling、parent 的优先级顺序返回
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
至此,我们就避免了递归
,改成了以最小工作为单位,构建 fiber tree 的形式去渲染
但还不够,因为浏览器可能中断我们的工作,而此时我们还并没有呈现完整的UI给用户,所以还需要进行优化
了解 Fiber 的同学应该清楚,Fiber 有个 双缓存
架构,即也就是有两个 fiber 树,分为 currentFiber
、workInProgressFiber
其中 currentFiber
就是当前呈现给用户看的,而 workInProgressFiber
就是在 Rerender
时在内存中构建的 fiber 树,他会通过 alternate
属性找到旧的 fiber,然后会根据 旧的 fiber.type 和当前新的 VDOM.type
是否一致,来对变化的 fiber 打上 effectTag
标记。当两棵 fiber 树对比完成后,会根据 workInProgressFiber
中每个 fiber 的 effectTag
对真实 DOM 进行相应的 增、删、改
的操作,最后把当前这个 workInProgressFiber
作为新的 currentFiber
。
Fiber 相关的具体的可以看我这篇文章 # React原理:通俗易懂的 Fiber。
因此,我们要在代码中新增一个变量 wipRoot
作为 workInProgressFiber
let nextUnitOfWork = null
let wipRoot = null
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
}
// wipRoot 会作为第一个工作单元,然后传入 performUnitOfWork
nextUnitOfWork = wipRoot
}
然后当完成渲染工作后,进入 Commit 阶段
// Commit 阶段的入口函数
function commitRoot() {
// 这个阶段的工作就是递归 workInPrgressFiber 根据每一个 fiber 节点创建真实DOM
// 然后插入到 container 上
commitWork(wipRoot.child)
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
const domParent = fiber.parent.dom
domParent.appendChild(fiber.dom)
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function workLoop(deadline) {
// ...
// 当没有下一个工作单元了,且已经存在 workInProgressFiber 树时,说明渲染的任务完成了
// 进入 Commit 阶段
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
更新、删除节点
到目前为止,我们在内存中创建好了 workInProgressFiber
,并且我们通过 createDom
添加了 DOM 节点,但是如何更新或删除节点呢?
前面我们也说过,会去对比 currentFiber
和 workInProgressFiber
,根据 effectTag
走增删改的逻辑。所以我们加一个变量 currentRoot
和 一个属性 alternate
let currentRoot = null;
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
// alternate 指向旧的 currentFiber
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
}
function commitRoot() {
commitWork(wipRoot.child)
// 加上这句话,表示当前构建好的 workInProgressFiber 会作为下一次的 currentFiber
currentRoot = wipRoot
wipRoot = null
}
我们把之前 performUnitOfWork
中的第二步:对 children
也创建 fiber 单独提到 reconcileChildren
函数中去
function reconcileChildren(wipFiber, elements) {
// ... 之前的对 children 创建 fiber 的代码逻辑
}
function performUnitOfWork(fiber) {
// 1、根据 fiber 创建 DOM
// 2、对 children 也创建 fiber
const elements = fiber.props.children;
reconcileChildren(fiber, elements);
// 3、返回下一个工作单元(fiber),按照 child、sibling、parent 的优先级顺序返回
}
然后,我们对 reconcileChildren
的逻辑修改一下:通过 alternate
找到旧的 fiber,然后比较新的 VDOM 和 旧的 fiber,决定生成怎样的新的 fiber,这里会根据 旧的 fiber 的 type 属性和新的 VDOM 的 type 属性对比决定,也就是 oldFiber && element && element.type == oldFiber.type
,然后就会分新增、更新、删除三种情况去处理
function reconcileChildren(wipFiber, elements) {
let index = 0;
// 1、通过 alternate 找到旧的 fiber
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
// 2、比较旧的 fiber 和新的 VDOM,决定生成怎样的新的 fiber
// 通过 oldFiber.type 和 element.type 来决定 effectTag 的值
const sameType = oldFiber && element && element.type == oldFiber.type;
// 3、分情况讨论
if (sameType) {
// 更新节点
}
if (element && !sameType) {
// 新增节点
}
if (oldFiber && !sameType) {
// 删除节点
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
// 4、对于新的 fiber,根据 index 判断是字节点还是兄弟节点
// index === 0,说明是子节点,所以当前 newFiber 挂载到 fiber.child 上
if (index === 0) {
fiber.child = newFiber;
} else {
// 否则,当前 newFiber 就作为前一个 fiber 的兄弟节点
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
我们一个一个来看,如果是更新节点
:
if (sameType) {
newFiber = {
type: oldFiber.type, //因为是更新,所以 type 直接用 olderFIber.type,比如 "div"
props: element.props, //记录新的 props
dom: oldFiber.dom, //更新时,dom 不变,只是改 props
parent: wipFiber, //当前 newFiber 是作为 workInProgressFiber 树的一个 fiber 节点
alternate: oldFiber, //指向旧的 fiber 节点
effectTag: "UPDATE" //更新时,effectTag 打标记为 "UPDATE"
};
}
如果是新增节点
:
if (element && !sameType) {
newFiber = {
type: element.type, //新增时,记录 element.type,比如 "div"
props: element.props, //记录 props
dom: null, //新增时,页面上没有对于的 DOM
parent: wipFiber, //当前 newFiber 是作为 workInProgressFiber 树的一个 fiber 节点
alternate: null, //新增时,没有对应的 oldFiber
effectTag: "PLACEMENT" //更新时,effectTag 打标记为 "PLACEMENT"
};
}
如果是删除节点
:
function render(element, container) {
// ...
deletions = [];
}
let deletions = null;
function reconcileChildren(wipFiber, elements) {
//... 之前的代码
if (oldFiber && !sameType) {
// 因为是删除节点,所以直接对 oldFiber 打标机
oldFiber.effectTag = "DELETION";
// 然后用一个数组去记录要删除的节点
deletions.push(oldFiber);
}
}
到这里,我们就把 workInProgressFiber
处理好了,那么进入 Commit 阶段
时,就会根据 effectTag
做对于的 DOM 操作了
Commit 阶段
前面我们写了 Commit 阶段的代码,我们稍微补充下
function workLoop(deadline) {
// ...
// 所有的工作单元都执行完了,就去 Commit 阶段,Commit 的入口函数
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
}
function commitRoot() {
// 删除 deletions 里面记录的元素
deletions.forEach(commitWork);
//...之前的代码
}
function commitWork(fiber) {
// 根据 effectTag 处理 DOM
}
现在我们就来完善 commitWork
函数。前面我们的 workInPrgressFiber
的每一个 fiber 节点身上都有 effectTag
了,那我们就可以通过 effectTag
去操作DOM 了
function commitWork(fiber) {
// 1、如果 fiber 处理完了,return
if (!fiber) {
return;
}
// 2、找到当前 fiber 的父节点的真实 dom
const domParent = fiber.parent.dom;
// 3、根据 effectTag 做不同的操作
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
// 3.1、如果是新增
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
// 3.2、如果是更新,则需要把新的 props 覆盖 旧的 props,然后更新到对于的 dom 上去
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
// 3.3、如果是删除
domParent.removeChild(fiber.dom)
}
// 递归处理子节点和兄弟节点
commitWork(fiber.child);
commitWork(fiber.sibling);
}
那我继续来看 updateDom
的具体实现
const isEvent = (key) => key.startsWith("on");
const isProperty = (key) => key !== "children" && !isEvent(key);
const isNew = (prev, next) => (key) => prev[key] !== next[key];
const isGone = (prev, next) => (key) => !(key in next);
function updateDom(dom, prevProps, nextProps) {
//Remove old or changed event listeners
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.removeEventListener(eventType, prevProps[name]);
});
// Remove old properties
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps))
.forEach((name) => {
dom[name] = "";
});
// Set new or changed properties
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
dom[name] = nextProps[name];
});
// Add event listeners
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
updateDom
的思路就是:移除旧的或者改变了的事件监听、移除旧的属性、设置新的属性、添加新的事件监听,代码也不复杂。
到这里,我们就完成了通过 effectTag
更新 DOM 的操作了。
Function Component
在下面的例子中:
function App(props) {
return <h1>Hi {props.name}</h1>;
}
const element = <App name='foo' />;
const container = document.getElementById("root");
Didact.render(element, container);
babel 编译后如下:
function App(props) {
return myReact.createElement(
"h1",
null,
"Hi ",
props.name
)
}
const element = Didact.createElement(App, {
name: "foo",
})
我们可以发现,编译后会将函数组件内部 return 的 JSX 通过 createElement
转 VDOM,那么在转了 VDOM 后的逻辑应该和之前的一样,只不过在 performUnitOfWork
里面,我们判断 fiber.type
是不是函数来区分是不是函数式组件
function performUnitOfWork(fiber) {
const isFunctionComponent =
fiber.type instanceof Function
if (isFunctionComponent) {
// 如果是函数组件,走这里
updateFunctionComponent(fiber)
} else {
// 否则,走这里
updateHostComponent(fiber)
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
function updateFunctionComponent(fiber) {
// 函数组件的逻辑
}
function updateHostComponent(fiber) {
// 这里面就是之前创建 DOM 的逻辑
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
而对于 updateFunctionComponent
的思路也比较简单,就是拿到函数组件对应 VDOM 上的 children prop,然后调用 reconcileChildren
构建 workInProgressFiber
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
然后需要去更改 commitWork
函数,因为函数组件对应的 fiber.dom 是 null,所以要更改一点逻辑
function commitWork(fiber) {
if (!fiber) {
return;
}
// 函数式组件处理时,不会创建 dom 挂载到 fiber.dom 上
// 所以要循环向上找到有 dom 的那个 fiber
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
// 之前的逻辑
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
// 这里改成往 domParent 上添加 dom
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
// 删除的逻辑也需要改一下
commitDeletion(fiber, domParent)
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function commitDeletion(fiber, domParent) {
//删除时,如果当前 fiber 没有 dom,则向下找,注意,不是往上找
//直到找到有 fiber.dom 属性的 fiber 子节点
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}
至此,函数组件的处理也完了
hooks
换个例子:
/** @jsx myReact.createElement */
function Counter() {
const [state, setState] = myReact.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)
现在我们给函数组件添加了状态,需要实现一个自己的 useState
获取或设置状态
就以 useState
为例,我们应该在调用函数组件前,初始化全局变量 hookIndex
用于记录当前最新的 hook 的下标(一个组件可以多次调用 useState),然后,我们把多个 useState 存到 fiber 的 hooks 属性上
,是个数组。
当调用函数组件时,我们会通过 hookIndex
和 alternate
找到旧的 hook,如果有旧的 hook,并且我们没有初始化新的 hook 的状态,那就把旧的 hook 的状态复制给新的 hook 状态,然后把新的 hook push 到 hooks
数组中,然后 hookIndex++
,返回新 hook.state
还需要注意 setState
的调用,在 hook 上创建一个 queue
的属性,是个数组,存放对于 hook.state 的 setState 函数
let wipFiber = null;
let hookIndex = null;
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
// 当前 workInProgressFiber 身上定义 hooks 数组,源码里面是 memoizedState 字段
wipFiber.hooks = [];
//...
}
function useState(initial) {
// 找旧的 hook
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
// 存放 setState
queue: [],
}
// 在下次渲染组件时,拿到旧的 queue,循环执行 actions ,应用新的状态
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action) => {
hook.state = action(hook.state);
});
const setState = (action) => {
// 把 setState 的逻辑存入 queue 上
hook.queue.push(action);
// 这里和 render 差不多
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
deletions = [];
};
// 新 hook push 到 hooks 数组中
wipFiber.hooks.push(hook)
// hooksIndex 下标加一
hookIndex++
return [hook.state, setState]
}
而在 React 源码里面, hooks 其实是一个链表
,然后挂载到 fiber 的 memoizedState
字段上,具体的可以看我这篇文章 # React:通俗易懂的 hooks 链表
至此,mini React 就完成了
总结
React 的流程分为 Render阶段
、Commit阶段
,Render阶段
可以概括为:babel 编译 JSX(通过 React.createElement)转 VDOM,然后在内存中会构建一个 workInProgressFiber
树(它以 VDOM 为基础来创建对应的 Fiber 节点),在构建的过程中,会通过 alternate
对比新的 VDOM 和旧的 Fiber,来决定生成怎样的新的 Fiber,然后给每个 Fiber 打标记 effectTag
,这一部分就是在 performUnitOfWork
函数中执行的。当然在这期间会适当让出主线程的控制器给浏览器,去执行更高优先级的任务(比如用户输入),然后当空闲时,继续执行工作单元任务(Fiber 架构异步可终端)
。当新的 workInProgressFiber
构建好且没有下一个工作单元任务了,就进入 Commit
阶段
Commit 阶段时执行入口函数 commitRoot
,那他首先就会去删除 effectTag
被打上删除标记的 dom 节点,然后依次处理剩余的节点,更新时就更新对应 dom 的 props,新增时直接加到父节点下就行了。完成之后,就把 currentFiber = workInProgressFiber
,也就是把最新的 workInProgressFiber
作为当前展示在页面的 fiber 树,然后 workInProgressFiber = null
制空内存的那个 fiber 树,下次 render 时,重复上面的步骤
对于函数组件和 hook 来说的话,在 performUnitOfWork
中会根据 fiber.type
判断是不是函数,是的话就做两件事情,记录 hooks
和 创建每一个 child 的 fiber
,创建 child 的 fiber 和前面的相同,记录 hooks 就是往当前工作单元(也就是当前的 fiber) 身上挂一个 hooks 数组(源码中是链表
,且挂载到 memoizedState
属性上,这里我们为了理解过程),记录当前 hook 的下标 hookIndex
,在本例子中,我们以 useState
为例子,useState 的 hook 节点身上会有 queue
属性,去存储 setState
的行为,当改变组件状态时,就会通过 hookIndex
和 alternate
找到应该更新的旧的 hook 节点,然后调用旧 hook 的 setState,把新的状态 赋值给新的 hook 节点,也就是 hook.state = action(hook.state)
,然后把最新的 setState
存储在新的 hook.queue 上,把新的 hook 存储在 workInProgressFiber.hooks
上,然后返回 [state, setState]
如果上述有什么问题,欢迎探讨!
转载自:https://juejin.cn/post/7243985578202988605