这个故事是系列
DIY👋自己-React
的一个部分, 但因为我们要重写大部分旧代码的, 无论如何, 我会 tl;dr它一下的:
TL;DR : 到目前为止的系列: 我们正在编写一个React克隆简化版本,来了解React在底层做了什么. 我们称之为
Didact
. 为了简化代码, 我们只关注-React-的主要函数. 首先我,们介绍如何渲染元素并使JSX工作. 我们编写了对比算法来重新渲染那些仅在更新期间更改了的内容. 然后我们添加了-Component - class
和setState()
.
现在React-16
已经不复存在, 并且有了一个新的内部架构, 需要重写-React-的大部分代码.
这意味着一些期待已久的函数 - 旧,的架构很难发展 - 被送走了🐶.
这也意味着我们在这个系列中编写的大部分代码现在都是毫无价值的. 但是思想/逻辑可以留下😛
在本文中, 我们将重写-didact-系列中的大部分代码, 以遵循 React-16 新架构. 我们将尝试从React代码库中 模拟 它的结构, 变量和函数名称. 我们将跳过我们不需要的-公共API-:
-
Didact.createElement()
-
Didact.render()
(只有DOM渲染) -
Didact.Component
(使用setState()
但没有context
或者生命周期方法)
如果你想跳到前面看代码的运行情况, 你可以去
现在, 让我解释为什么我们需要重写旧代码.
这不会提供
React-Fiber
的完整过程. 如果您想了解更多信息, 请查看-react-fiber资源列表
.
当浏览器的主线程长时间忙于运行时, 关键的简短任务必须等待一段不可接受的时间,才能完成.
为了展示这个问题, 我做了一个小演示. 为了保持行星的旋转, 主线程需要在每16ms左右就要运行一次. 如果主线程被其他东西阻塞, 让我们定个200毫秒, 你会注意到动画丢失帧和行星冻结/卡顿, 直到主线程再次释放.
是什么让主线程如此繁忙, 以至于无法将一些 ms, 花费在保持
动画平滑
和UI响应
上呢?
记住-对比算法代码
-3-实例-对比和虚拟dom? 一旦开始对比, 它就不会停止. 如果主线程需要做其他任何事情, 它将不得不等待. 而且, 因为很大程度上它取决于递归调用, 所以很难使它停止,再继续. 这就是为什么我们要用一个新的数据结构来重写它, 这将允许我们用循环
-替换-递归调用
.
如果你是新手,请点击并观察 本🌰中两种不同 jsbin-fibre
-
看了上面这个🌰, 你需要明白,
fibre
带有数据流动 的认知 -
然后你看
此资源列表
, 这是为了加深-对-react-fibre 的认知, 我们需要其中的demo-🌰子
说回 react-fibre 这个念想
: 带有数据的特性 的-fibre-
中 被记录了-优先级-
属性.
可以看到demo-🌰子 带有共 三个选择或输入项
pleace input text
没有变化的
Async mode
默认
sync mode
会到同步, 也就是没有变化
可以看到 -Async mode
- 的卡顿, 因为它这个组件元素被分配的优先级低, 而sync mode
的优先级比 Async mode
高, Async mode
要为优先级高的让道. 比如优先级高的动画.
当然,
react-fibre
不止做了这件事:P
我们需要将工作-分解为更小的部分, 可以短时间运行这些部分, 让主线程执行更高优先级
的任务, 并且如果有-任何待处理的事情-再回来完成工作.
我们会在[requestIdleCallback()]((https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback)函数的帮助下,做到这一点. 它将下一次浏览器空闲时,调用我们的preformWork
回调函数, 并加入一个deadline
参数, 用于描述我们的代码可用时间:
const ENOUGH_TIME = 1; // milliseconds
let workQueue = [];
let nextUnitOfWork = null; // 全局变量, 那么一次只能走一个回调
function schedule(task) { // 1. 加
workQueue.push(task); // 2. 存好了
requestIdleCallback(performWork); // 3. 下一次空闲运行, performWork 函数
}
function performWork(deadline) { // 空闲机会来了
if (!nextUnitOfWork) {
nextUnitOfWork = workQueue.shift(); // 4. 拿出来,
}
// 下一回调 与 看看有没有 足够的时间 再走一趟
while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
// 5. DO something
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (nextUnitOfWork || workQueue.length > 0) {
// 6. 如果还没有搞定, 那么 再等空闲咯
requestIdleCallback(performWork);
}
}
真正的工作发生在performUnitOfWork
函数的内部. 我们需要在那里编写我们的对比-{reconciliation}算法
代码. 该函数应该运行一部分工作, 然后-返回-{return}
-下一次恢复工作需要的所有信息.
为了跟踪这些工作, 我们将使用Fibre
.
我们将为每个想要渲染的组件创建一个Fibre
.
nextUnitOfWork
将是对下一个工作Fibre
的参考.performUnitOfWork
拿到Fibre
,并在其上工作, 并返回一个新的Fibre
用于下一次 - 直到所有工作完成.
容许下, 我会稍后详细解释这一点.
Fibre
是怎样的 ?
let fiber = {
tag: HOST_COMPONENT,
type: "div",
parent: parentFiber,
child: childFiber,
sibling: null,
alternate: currentFiber,
stateNode: document.createElement("div"),
props: { children: [], className: "foo"},
partialState: null,
effectTag: PLACEMENT,
effects: []
};
这是一个普通的旧JavaScript对象.
我们将使用parent
, child
和sibling
-属性-打造的Fibre
树来描述组件的树.
这stateNode
将是对Component
实例的引用. 它可以是DOM元素
, 也可以是用户定义的Component-class
的实例.
例如:
在这个例子中, 我们可以看到我们将支持的三种不同类型的组件:
我们将用
tag: HOST_COMPONENT,
来识别它们.
-
fibre.type
是html元素的标签:string
. -
fibre.props
是元素的-属性-和-事件监听器-.
它的tag
是CLASS_COMPONENT
和type
来自 用户定义的Didact.Component
.
因为它有DOM元素可以作为stateNode
, 但作为树的根, 它会得到特殊处理. Fibre.tag
会是HOST_ROOT. 请注意, Fibre.stateNode
是传递给Didact.render()
的DOM节点.
我们需要它, 因为大多数时候我们会有两棵Fibre
树.
-
一棵树将对应于我们已经呈现给-
html-DOM
-的东西, 我们将它称为当前树或旧树. -
另一棵是我们在创建新更新(调用
setState()
或Didact.render()
时构建的树, 我们将此树称为正在进行中的树, 简称为工作树
工作树不会与旧树共享任何Fibre
. 一旦我们完成-工作树
-的工作建设,并取得所需的 DOM变化, 工作树
会成为旧树.
因此, 我们使用alternate
链接 正在进行中的Fiber
与 相应旧树的Fiber
. Fibre
和它的alternate
分享相同tag
, type
与stateNode
. 有时,当我们渲染新的东西时,Fibre
不会有alternate
.
最后, 我们有effects
列表和effectTag
. 当我们发现工作树
的Fibre
有需要改变的DOM, 我们将设置effectTag
为PLACEMENT
, UPDATE
或DELETION
. 为了更容易将所有DOM变化一起提交, 我们保留了所有Fibre
(来自Fibre
子树)的effectTag
项到列表effects
.
这可能是一次太多的信息, 如果你没有跟上, 不要担心, 我们很快就会看到
Fibre
树的运行.
要了解我们要编写的代码的流程, 请查看此图表:
我们将从render()
和setState()
开始, 并遵循以commitAllWork()
结尾的流程。
我告诉过你, 我们将重写大部分代码, 但我们首先回顾一下我们不会h重写的代码.
-
在
元素创建和JSX
中, 我们编写了用于编译JSX的函数代码createElement()
. 我们不需要改变它, 我们将继续使用相同的元素. 如果你不知道元素中, 关于type
,props
和children
这些, 请查看旧帖子. -
在实例, 对比算法和虚拟DOM中, 我们编写了用于更新节点DOM属性的
updateDomProperties()
函数. 我还扩展了用于创建DOM元素的函数createDomElement()
. 你可以在这个dom-utils.js 的 gist中看到这两个函数.
gist 内容在这里 👌
const isEvent = name => name.startsWith("on");
const isAttribute = name =>
!isEvent(name) && name != "children" && name != "style";
const isNew = (prev, next) => key => prev[key] !== next[key];
const isGone = (prev, next) => key => !(key in next);
function updateDomProperties(dom, prevProps, nextProps) {
// Remove 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 attributes
Object.keys(prevProps)
.filter(isAttribute)
.filter(isGone(prevProps, nextProps))
.forEach(name => {
dom[name] = null;
});
// Set attributes
Object.keys(nextProps)
.filter(isAttribute)
.filter(isNew(prevProps, nextProps))
.forEach(name => {
dom[name] = nextProps[name];
});
// Set style
prevProps.style = prevProps.style || {};
nextProps.style = nextProps.style || {};
Object.keys(nextProps.style)
.filter(isNew(prevProps.style, nextProps.style))
.forEach(key => {
dom.style[key] = nextProps.style[key];
});
Object.keys(prevProps.style)
.filter(isGone(prevProps.style, nextProps.style))
.forEach(key => {
dom.style[key] = "";
});
// 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]);
});
}
function createDomElement(fiber) {
const isTextElement = fiber.type === TEXT_ELEMENT;
const dom = isTextElement
? document.createTextNode("")
: document.createElement(fiber.type);
updateDomProperties(dom, [], fiber.props);
return dom;
}
- 在组件和状态中, 我们编写了Component-基类. 让我们改变它,以便
setState()
可以调用scheduleUpdate()
,和createInstance()
来保存实例中Fibre
的引用:
class Component {
constructor(props) {
this.props = props || {};
this.state = this.state || {};
}
setState(partialState) {
scheduleUpdate(this, partialState); // <==
}
}
function createInstance(fiber) {
const instance = new fiber.type(fiber.props);
instance.__fiber = fiber;
return instance;
}
从这段代码开始, 让我们从头开始重写其余部分
除了Component
class 和 createElement()
之外, 我们还会有两个公共函数: render()
和 setState()
, 我们看到这setState()
只是调用scheduleUpdate()
.
render()
和scheduleUpdate()
是相似的, 他们会收到一个新的更新并对其进行排队:
// Fiber tags
const HOST_COMPONENT = "host";
const CLASS_COMPONENT = "class";
const HOST_ROOT = "root";
// Global state
const updateQueue = [];
let nextUnitOfWork = null;
let pendingCommit = null;
function render(elements, containerDom) {
updateQueue.push({ // 用作一个队列, 先进先出
from: HOST_ROOT,
dom: containerDom,
newProps: { children: elements }
});
requestIdleCallback(performWork); // 下一个浏览器空闲时
}
function scheduleUpdate(instance, partialState) { // 提供给 setState 使用
updateQueue.push({
from: CLASS_COMPONENT,
instance: instance,
partialState: partialState
});
requestIdleCallback(performWork);
}
我们将使用该updateQueue
数组来跟踪待处理的更新. 每次运行render()
或scheduleUpdate()
会推送一个新的更新到updateQueue
. 每个更新中的更新信息都不同, 等会你会看到,我们在之后的resetNextUnitOfWork()
函数中使用它.
将更新推送到队列后, 我们触发延迟回调performWork()
函数
const ENOUGH_TIME = 1; // milliseconds
function performWork(deadline) {
workLoop(deadline);
if (nextUnitOfWork || updateQueue.length > 0) {
// 是否 有 待审批工作
requestIdleCallback(performWork);
}
}
function workLoop(deadline) {
if (!nextUnitOfWork) {
resetNextUnitOfWork();
}
while (nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME) {
// 关注时间 是否足够 运行另一个工作单元
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (pendingCommit) {
commitAllWork(pendingCommit);
}
}
正如我们在 5.2 调度微任务 中 了解到的循环工作流程, 只是具体分支起了对应的名称
是之前我们使用performUnitOfWork()
时,看到的模式.
requestIdleCallback()
提供截止日期-{deadline}
作为目标函数的参数调用. performWork()
将deadline
传给它的workLoop()
. 然后workLoop()
运行返回, performWork()
检查是否还有待审批工作. 如果有的话, 它会为它自己安排一个新的requestIdleCallback(performWork)
.
workLoop()
是关注时间的函数. 如果deadline
时间太少, 它会停止工作循环,并保持下一次nextUnitOfWork
更新需要的Fibre
状态, 以便下次恢复.
我们使用
ENOUGH_TIME
(1ms常数, 与React相同)来检查deadline.timeRemaining()
,是否有足够运行另一个工作单元的时间. 如果performUnitOfWork()
超过这一时间, 我们将留待下次继续.deadline
只是来自浏览器的建议, 所以将其超过几毫秒并不是那么糟糕.
performUnitOfWork()
将用正在进行的更新,和找出我们需要对-DOM-应用哪些更改来构建工作树
. 这将逐步完成, 每次一段Fibre
数据.
当performUnitOfWork()
完成当前更新的所有工作时, 它将返回null
和将在DOM中待处理的更改留给pendingCommit
. 最后, commitAllWork()
获取来自pendingCommit
的effects
, 和变更DOM.
请注意, commitAllWork()
在循环之外调用. performUnitOfWork()
完成的工作并不会改变DOM, 因此可以将其分开.
另一方面, commitAllWork()
将改变DOM, 它应该一次完成, 以避免不一致的UI.
我们还没有看到第一个
nextUnitOfWork
来自哪里
resetNextUnitOfWork()
运行在上一小节的workLoop
函数中
resetNextUnitOfWork()
获取一次更新,并将其转换为第一个nextUnitOfWork
:
function resetNextUnitOfWork() {
const update = updateQueue.shift();
if (!update) {
return;
}
// Copy the setState parameter from the update payload to the corresponding fiber
if (update.partialState) {
update.instance.__fiber.partialState = update.partialState;
}
const root =
update.from == HOST_ROOT
? update.dom._rootContainerFiber
: getRoot(update.instance.__fiber);
nextUnitOfWork = {
tag: HOST_ROOT,
stateNode: update.dom || root.stateNode, // 两种情况
props: update.newProps || root.props,
alternate: root
};
}
function getRoot(fiber) {
let node = fiber;
while (node.parent) {
node = node.parent;
}
return node;
}
-
resetNextUnitOfWork()
首先从队列中提取第一个更新. -
如果有更新,
partialState
我们将它存储在属于组件实例的Fibre
上, 以便稍后在调用组件render()
时使用它. -
然后我们找到旧
Fibre
树的根. 如果更新是第一次调用render()
时, 我们不会有一个根Fibre
,所以root == null
. 如果它来自后续的render()
调用, 我们可以在DOM节点的_rootContainerFiber
属性上找到根. 如果更新来自一次setState()
, 我们需要从实例Fibre
往上找, 直到找到一个Fibre
是没有-parent
的. -
然后我们让
nextUnitOfWork
重新获得了一个新的Fibre
.这Fibre
是一个新的工作树的根.
如果我们没有一个 旧 的根, 那么这个DOM节点stateNode
就会在render()
调用中,作为参数被接收. 这props
将是来自更新的newProps
: 对象中的一个children
属性具有元素,children
是render()
的另一个参数. 该alternate == null
.
如果我们有一个 旧 的根, 那么stateNode
将是前一个根的DOM节点. 该props
如果不是null,又会是newProps
, 否则我们将从 旧 根复制props
. 那这alternate
将是 旧 根.
我们现在拥有正在进行中的树
的根, 让我们开始构建剩余的树.
function performUnitOfWork(wipFiber) {
beginWork(wipFiber);
if (wipFiber.child) { // 工作没有完成, 返回下一次更新的状态
return wipFiber.child;
}
// No child, we call completeWork until we find a sibling
let uow = wipFiber;
while (uow) {
completeWork(uow);
if (uow.sibling) {
// Sibling 返回, 再次变为 wipFiber, 被 beginWork 调用
return uow.sibling;
}
uow = uow.parent;
}
}
performUnitOfWork()
会运行 进行中的工作树.
-
我们称之为
beginWork()
- 创造一个新的Fibre
孩子 - 然后让第一个孩子返回,成为nextUnitOfWork
. -
如果没有任何的孩子, 我们接着
completeWork()
,并返回sibling
作为nextUnitOfWork
. -
如果没有
sibling
, 我们会继续往parent
中找,completeWork()
直到我们找到sibling
(将成为nextUnitOfWork
)或到达根部.
performUnitOfWork()
多次调用,会沿着 工作树 向下, 创建每根Fibre
的第一个child
的children
, 若找到没有孩子的Fibre
. 就向'右'和向'上'找, 就好像有兄弟姐妹,父母,整个家族图似的. (为了更加生动, 可以尝试在fiber-调试器上渲染一些组件)
function beginWork(wipFiber) {
if (wipFiber.tag == CLASS_COMPONENT) {
updateClassComponent(wipFiber);
} else {
updateHostComponent(wipFiber);
}
}
function updateHostComponent(wipFiber) {
if (!wipFiber.stateNode) {
wipFiber.stateNode = createDomElement(wipFiber);
}
const newChildElements = wipFiber.props.children;
reconcileChildrenArray(wipFiber, newChildElements);
}
function updateClassComponent(wipFiber) {
let instance = wipFiber.stateNode;
if (instance == null) {
// 调用类初始化
instance = wipFiber.stateNode = createInstance(wipFiber);
} else if (wipFiber.props == instance.props && !wipFiber.partialState) {
// 不需要更新,最后 复制 孩子
cloneChildFibers(wipFiber);
return;
}
instance.props = wipFiber.props;
instance.state = Object.assign({}, instance.state, wipFiber.partialState);
wipFiber.partialState = null;
const newChildElements = wipFiber.stateNode.render();
reconcileChildrenArray(wipFiber, newChildElements);
}
beginWork()
做两件事:
- 创造
stateNode
,如果我们没有 - 获取组件子项并将它们传递给
reconcileChildrenArray()
因为两者都取决于我们处理的组件的类型, 所以我们将它分成两部分: updateHostComponent()
和updateClassComponent()
.
-
updateHostComponent()
处理 主机组件 以及 根组件. 如果需要的话, 它会创建一个新的DOM节点(只有一个节点, 没有子节点, 并且不会将其附加到DOM). 然后它通过调用reconcileChildrenArray()
函数, 使用来自Fibre
中props
属性的子元素. -
updateClassComponent()
处理类组件实例. 如果需要的话, 它会创建一个新实例,通过调用createInstance
函数. 它更新实例的props
,state
,因此它的render()
函数可以获取新的孩子. -
updateClassComponent()
也验证调用render()
是否有意义. 这是一个简单版本的shouldComponentUpdate()
. 如果看起来我们不需要重新渲染, 那么我们只是将当前的子树克隆到正在进行的工作树中, 而不进行任何调整.
我们现在有了newChildElements
,准备好为 工作中的Fibre
创建子Fibre
// Effect tags
const PLACEMENT = 1;
const DELETION = 2;
const UPDATE = 3;
function arrify(val) {
return val == null ? [] : Array.isArray(val) ? val : [val];
}
function reconcileChildrenArray(wipFiber, newChildElements) {
const elements = arrify(newChildElements);
let index = 0;
let oldFiber = wipFiber.alternate ? wipFiber.alternate.child : null;
let newFiber = null;
while (index < elements.length || oldFiber != null) {
const prevFiber = newFiber;
const element = index < elements.length && elements[index];
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
newFiber = {
type: oldFiber.type,
tag: oldFiber.tag,
stateNode: oldFiber.stateNode,
props: element.props,
parent: wipFiber,
alternate: oldFiber,
partialState: oldFiber.partialState,
effectTag: UPDATE
};
}
if (element && !sameType) {
newFiber = {
type: element.type,
tag:
typeof element.type === "string" ? HOST_COMPONENT : CLASS_COMPONENT,
props: element.props,
parent: wipFiber,
effectTag: PLACEMENT
};
}
if (oldFiber && !sameType) {
oldFiber.effectTag = DELETION;
wipFiber.effects = wipFiber.effects || [];
wipFiber.effects.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index == 0) {
wipFiber.child = newFiber;
} else if (prevFiber && element) {
prevFiber.sibling = newFiber;
}
index++;
}
}
这是本库的核心, 正在进行的工作树
在不断增长, 我们决定在提交阶段对 DOM 做什么更改.
-
开始之前, 我们确保
newChildElements
是一个数组. (与之前的对比算法不同, 这个算法总是与子数组一起工作, 这意味着我们现在可以在组件的render()
函数上返回数组) -
然后我们开始比较旧
Fibre
树的孩子和新元素(我们将Fibre
与元素进行比较). 来自 旧Fibre
树的孩子们正是wipFiber.alternate
的孩子们. 新元素是我们从wipFiber.props.children
调用或wipFiber.stateNode.render()
调用来的.
我们的对比算法通过 第一个旧Fibre
(匹配wipFiber.alternate.child
)与第一子元素(elements[0]), 第二个旧Fibre
(wipFiber.alternate.child.sibling
)的第二子元素(elements[1])等. 对于每对 oldFiber
- element
:
-
如果
oldFiber
和element
同样的type
, 好消息, 这意味着我们可以保留旧的stateNode
. 我们创建一个基于旧的Fibre
的新Fibre
. 我们添加UPDATE
effectTag
. 我们将新Fibre
添加到正在进行的工作树
中. -
如果我们
element
与oldFiber
有一个不同type
和我们没有oldFiber
(因为我们有更多的新孩子), 我们用element
的信息创建一个新的Fibre
. 请注意, 这种新Fibre
没有alternate
, 也不会有stateNode
(我们将在beginWork()
创建stateNode
). 该Fibre
的effectTag
是PLACEMENT
. -
如果
oldFiber
和element
有不同的type
和有oldFiber
,也没有任何的element
(因为我们有更多 旧 孩子), 我们将标记oldFiber
为DELETION
. 鉴于这种Fibre
不是正在进行工作的树的一部分, 我们需要将它添加到wipFiber.effects
列表中, 以便我们不会丢失它的踪迹.
与 React 不同的是, 我们没有使用 key 来进行比对, 所以我们不知道一个孩子是否从之前的位置移动过来.
updateClassComponent()
有一个特殊情况, 我们采取了快捷方式, 将旧的Fibre
子树克隆到正在进行中的工作树, 而不是进行调整.
function cloneChildFibers(parentFiber) {
const oldFiber = parentFiber.alternate;
if (!oldFiber.child) {
return;
}
let oldChild = oldFiber.child;
let prevChild = null;
while (oldChild) {
const newChild = {
type: oldChild.type,
tag: oldChild.tag,
stateNode: oldChild.stateNode,
props: oldChild.props,
partialState: oldChild.partialState,
alternate: oldChild,
parent: parentFiber
};
if (prevChild) {
prevChild.sibling = newChild;
} else {
parentFiber.child = newChild;
}
prevChild = newChild;
oldChild = oldChild.sibling;
}
}
cloneChildFibers()
克隆每个wipFiber.alternate
孩子并将其附加到正在进行中的树
中. 我们不需要添加任何内容, 因为我们确信effectTag
没有任何变化.
在performUnitOfWork()
,当wipFiber
没有新的孩子或者我们已经完成了所有孩子的工作时, 我们运行completeWork()
.
function completeWork(fiber) {
if (fiber.tag == CLASS_COMPONENT) {
fiber.stateNode.__fiber = fiber;
}
if (fiber.parent) {
const childEffects = fiber.effects || [];
const thisEffect = fiber.effectTag != null ? [fiber] : [];
const parentEffects = fiber.parent.effects || [];
fiber.parent.effects = parentEffects.concat(childEffects, thisEffect);
} else {
pendingCommit = fiber;
}
}
-
completeWork()
首先更新与CLASS_COMPONENT
实例相关的Fibre
引用. (说实话, 这并不是真的需要在这里, 但它必须在某个地方) -
然后它建立一个
effects
列表. 该列表将包含来自 正在进行中的子树 的所有Fibre.effectTag
(它也包含来自旧子树的Fibre
-DELETION effectTag
). 这个想法是在根effects
列表中集合所有的Fibre.effectTag
. -
最后, 如果
Fibre
没有parent, 我们拿到了 正在进行工作的树的根. 所以我们完成了这次更新的所有工作,并收集了所有的effects
. 我们分配根到pendingCommit
,以便workLoop()
中commitAllWork()
可以调用.
我们需要做的最后一件事是: 改变DOM.
function commitAllWork(fiber) {
fiber.effects.forEach(f => {
commitWork(f);
});
fiber.stateNode._rootContainerFiber = fiber;
nextUnitOfWork = null; // Reset
pendingCommit = null;
}
function commitWork(fiber) {
if (fiber.tag == HOST_ROOT) {
return;
}
let domParentFiber = fiber.parent;
while (domParentFiber.tag == CLASS_COMPONENT) {
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.stateNode;
if (fiber.effectTag == PLACEMENT && fiber.tag == HOST_COMPONENT) {
domParent.appendChild(fiber.stateNode); // add
} else if (fiber.effectTag == UPDATE) {
updateDomProperties(fiber.stateNode, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag == DELETION) {
commitDeletion(fiber, domParent);
}
}
function commitDeletion(fiber, domParent) {
let node = fiber;
while (true) {
if (node.tag == CLASS_COMPONENT) {
node = node.child;
continue;
}
domParent.removeChild(node.stateNode);
while (node != fiber && !node.sibling) {
node = node.parent;
}
if (node == fiber) {
return;
}
node = node.sibling;
}
}
commitAllWork()
首先迭代effects
中每个项调用commitWork()
. commitWork()
检查每根Fibre
的effectTag
:
-
如果是
PLACEMENT
, 我们查找父DOM节点, 然后简单地追加Fibre.stateNode
. -
如果是
UPDATE
, 我们会把stateNode
旧道具和新道具放在一起, 然后updateDomProperties()
决定要更新什么. -
如果它是
DELETION
并且Fibre
是主机组件, 那很简单, 我们只是removeChild()
. 但是如果Fibre
是类组件, 在调用removeChild()
之前, 我们需要从Fibre
子树中,找到需要删除的所有主机组件.
一旦我们完成了所有的效果, 我们可以重置nextUnitOfWork
和pendingCommit
. 正在进行的工作树 不再是 正在进行中的工作树, 并成为旧树, 因此我们将其根指定给_rootContainerFiber
. 之后, 我们完成当前的更新, 我们准备开始下一个🚀
如果你想把所有的东西放在一起, 只公开API, 你可以这样做:
function importDidact() {
// ...
// All the code we wrote
// ...
return {
createElement,
render,
Component
};
}
/** @jsx Didact.createElement */
const Didact = importDidact();
class HelloMessage extends Didact.Component {
render() {
return <div>Hello {this.props.name}</div>;
}
}
Didact.render(
<HelloMessage name="John" />,
document.getElementById("container")
);
或者你可以查看的codepen.IO. 所有这些代码也可以在Didact的仓库和npm - didact中获得.
Didact缺少很多React的函数, 但我特别有兴趣根据优先级安排更新:
module.exports = {
NoWork: 0, // No work is pending.
SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
TaskPriority: 2, // Completes at the end of the current tick.
HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
LowPriority: 4, // Data fetching, or result from updating stores.
OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
};
所以啊, 下一篇文章可能会这样.
就这样!如果你喜欢它, 不要忘记拍手👏, 在Twitter上关注我, 发表评论和所有这些内容.
谢谢阅读. !!