转载自唐鼎的博客建造属于你的react(译文),原作者是Rodrigo Pombo的博文Build your own React,本文已获得转载权限,如果需要转载请联系原作者或者唐鼎。
我们将一步一步重建一个属于我们自己的react。我们的react架构将和真实的react架构相同,但是去掉了大部分的优化和一些目前不必要的功能。
构建一个自己的React(基于React16.8版本)
1.createElement (生成虚拟dom)
我们来实现一个我们自己的createElement,首先是把JSX替换成createElement函数
我们先大致写一个目录,然后只装个bable来将jsx转成createElement
npm init -y
npm install --save-dev @babel/core @babel/cli @babel/preset-react
根目录下新建个bable.config.json,把 ↓粘进去
{
"presets": ["@babel/preset-react"]
}
然后在package.json里写个脚本 对应build命令,运行他来让bable编译代码
“build”: “babel main.jsx --watch --out-dir build”
npm run build
大致如下
const App = (
<div id="foo">
<a>bar</a>
<b />
</div>
)
const container = document.getElementById("root")
React.render(element, container)
App被转换后会成这样↓
const App = React.createElement(
'div',
{id: 'foo'},
React.createElement('a',null,'bar'),
React.createElement('b')
)
我们来写几个例子
// 使用 createElement("div"),返回:
{
"type": "div",
"props": { "children": [] }
}
// 使用 createElement("div", {foo: 'bar'}, a), 返回:
{
"type": "div",
"props": { "children": [a],"foo": "bar" }
}
// 使用 createElement("div", null, a, b),返回:
{
"type": "div",
"props": { "children": [a, b] }
}
children数组除了dom之外,还可能包含一些基本数据类型,如数字或字符串,我们用TEXT_ELEMENT来表示
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === 'object'
? child
: createTextNode(child))
}
}
}
function createTextNode(text) {
return {
type:"TEXT_ELEMENT",
props:{
nodeValue: text,
children: []
}
}
}
const React = {createElement}
在实际的react中是不会把基本类型包装成对象,我这么做是为了后边简化代码
2.render函数
我们来完成自己的ReactDom.render函数
现在先只考虑将虚拟dom渲染到真实dom上,不考虑更新和删除
先将element上的type转换成dom,然后塞入到容器中,根据这个规律来递归的调用render完成渲染
function render (element, container) {
const { type,props } = element;
const { children } = props;
const dom = document.createElement(type)
children.forEach(child => {
render(child, dom)
})
container.appendChild(dom)
}
还需要单独处理type 为 TEXT_ELEMENT 的文本节点 修改如下
function render (element, container) {
// ...省略已有代码
const dom = type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(type)
// ...省略已有代码
}
最后我们还需要把 虚拟dom上的props 同步到dom上,完整代码如下↓
function render (element, container) {
const { type,props } = element;
const { children } = props;
const dom = type === 'TEXT_ELEMENT'
? document.createTextNode('')
: document.createElement(type);
// ------处理props
const isProperty = key => key !== "children"
Object.keys(props)
.filter(isProperty)
.forEach(name => dom[name] = props[name])
// ------处理props结束
children.forEach(child => {
render(child, dom)
})
container.appendChild(dom)
}
const React = {createElement,render}
到这里之后,我们就完成了一个简单的将jsx转换成真实dom。启动index.html就能看到。
3.Concurrent Mode
一旦开始把虚拟dom转成真实dom,我们在整个递归过程完成之前不能停止,如果 虚拟dom树过于庞大,这个渲染过程就会占用主线程过长时间,如果在这个时候浏览器需要去处理一些高响应的操作(比如用户输入)将在完成渲染前产生卡顿
因此我们要把工作拆成一个个小的单元,每个单元完成后查看一下浏览器是否有更重要的任务,如果有我们就打断当前的渲染循环。
let nextUnitOfWork = null; // 下一个工作单元
function wookLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
// 处理工作单元
function performUnitOfWork (nextUnitOfWork) {
// TODO
}
我们用requestIdleCallback这个浏览器的api来完成循环,可以把这个api理解成 setTimeout类似的功能(把任务放在当前微任务最后)但是不同的是requestIdleCallback会在浏览器主线程空闲的时候触发,而不是像setTimeout制定一个执行时间
requestIdleCallback提供了deadline参数,我们可以用它来确认在浏览器接管线程前我们我们有多少时间。
React不适用这个api,自己完成了一套调度器
- 浏览器兼容性不好
- 他的FPS只有20,人眼在FPS低于60的时候会感觉到卡顿,可以这么理解:FPS 20 就是一秒刷20张图 (20帧),1帧用时50毫秒(1000 / 20),也就是间隔50毫秒才刷新一次(1秒只触发20次),远远低于流畅度的要求
requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it’s not really useful for UI work。—— from Releasing Suspense
为了实现上面的循环,我们需要完成performUnitOfWork函数,他除了执行一个小单元的工作外,还需要返回下一个需要被执行的单元
4.Fibers
为了实现一个单元一个单元的工作,我们需要引入fiber的数据结构,一个虚拟dom对应一个fiber结构,一个fiber结构对应一个单元的工作。
来看一个例子,有这么一个需要渲染的元素树
React.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
上面结构映射成 fiber 树后大体为下图结构:
在render 函数中我们需要穿件rootFiber(根fiber),将其设置为nextUnitofWork,剩下的工作将在performUnitOfWork函数中完成,我们将在每一个fiber中做3件事:
- 给fiber生成对应的dom
- 给fiber的子节点们创建fiber
- 返回下一个工作单元
fiber有一个重要的功能就是能够轻易的找到下一个工作单元,这就是为什么fiber有指向第一个子节点和第一个兄弟节点和自己父节点的原因,当我们完成当前fiber的工作后,fiber的child属性可以直接指向下一个需要进行工作的子节点。
如果fiber没有子节点了我们就使用sibling属性去找他兄弟节点作为下一个工作单元。
当fiber既没有child也没有sibling,我们让fiber去找他叔叔(父亲的兄弟节点),如果fiber的父节点没有兄弟节点,我们继续往上找父节点的兄弟节点,直到根节点,当我们到达根节点的时候,这就意味着着一次render 我们完成了所有工作
现在我们按照这个思路来在代码中实现一下。
首先我们移除到render函数中的代码,将其移到createDom中,后续需要适用
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
}
function render (element, container) {
// TODO 生成rootFiber
}
在render函数中我们设置nextUnitOfWork为fiber root 节点
function render (element, container) {
nextUnitOfWork = {
dom: container,
props: {
children:[element] // 我们都用数组后边就不用刻意去区分数据类型了
}
}
}
let nextUnitOfWork = null;
这样当浏览器空闲的时候 requestIdleCallback就会触发,执行workLoop开始在root节点上工作
接下来我们来完成 performUnitOfWork
function performUnitOfWork (fiber) {
// 生成dom节点
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
// TODO 处理fiber的子节点们生成fiber然后建立fiber链
// TODO 返回下一个工作单元
}
然后我们循环给所有子节点创建fiber节点,我们把这些子节点根据是否为第一个子节点添加到 =fiber root的child或者child的sibling上面。
function performUnitOfWork (fiber) {
// ---省略创建dom
// 处理fiber的子节点们生成fiber然后建立fiber链
const elements = fiber.props.children
let index = 0;
let prevSibling = null;
while(index < elements.length) {
const element = elements[index]
const newFiber = {
dom: null,
props: element.props,
parent: fiber,
type: element.type
}
if (index === 0) {
fiber.child = element
}else {
prevSibling = newFiber
}
prevSibling = newFiber
index++
}
// TODO 返回下一个工作单元
}
最后我们来处理返回下一个工作单元, 先尝试返回child,然后是sibling,再然后是parent的sibling,继续往上直到fiber root。
function performUnitOfWork (fiber) {
// --- 省略生成dom节点
// --- 省略处理fiber的子节点们生成fiber然后建立fiber链
//返回下一个工作单元
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber;
while(nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nexrFiber = nextFiber.parent
}
}
目前为止我们的performUnitOfWork就完成,完成的函数如下:
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
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,
}
if (index === 0) {
fiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
5. Render和Commit 阶段
我们发现了一个新的问题。
我们在每一个工作单元中都给其父节点塞入了当前fiber的dom,但这有一个问题,在浏览器比较繁忙的时候会打断我们的工作,这样会程序按出一个不完整UI的渲染,所以我们删除performUnitOfWork中这行添加dom的操作;
function performUnitOfWork(fiber) {
// ... 省略
/* 删除 */
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
/* 删除 */
// ... 省略
}
取而代之,我们需要添加名为wipRoot(work in progress root)的fiber来记录fiber节点循环更新后的节点,一旦我们完成所有单元的工作(不存在next unit of work)的时候,我们一次性将fiber树更新到document上。
function commitRoot () {
// TODO add nodes to dom
}
function render (element, container) {
wipRoot = {
dom: container,
props: {children: [element]},
}
nextUnitOfWork = wipRoot
}
let wipRoot = null
let nextUnitOfWork = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
// 一次性全部提交
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
我们把这个提交 fiber 树的过程 放在commitRoot 中实现,递归的把节点添加到 document上
function commitRoot () {
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)
}
6. Reconciliation(调和)
目前我们只考虑了往document上添加元素,更新和删除却没有做,我们需要比较render函数这次收到的fiber结构和我们上次更新的fiber树有哪些不同。
因此我们需要在更新完毕后保存一份更新过的fiber树,我们叫它currentRoot。在每一个fiber节点当中我们也添加alternate属性,该属性指向上次更新的fiber节点
function commitRoot() {
commitWork(wipRoot.child)
// 添加 currentRoot
currentRoot = wipRoot
wipRoot = null
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
// 添加 alternate
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
}
let currentRoot = null
我们现在把performUnitOfWork函数创建新fiber节点部分的代码抽取出来放在reconcileChildren 函数。我们将在reconcileChildren函数中根据老的fiber节点来调和新的react元素
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
const elements = fiber.props.children
reconcileChildren(fiber, elements)
if (fiber.child) {
return fiber.child
}
let nextFiber = fiber
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling
}
nextFiber = nextFiber.parent
}
}
function reconcileChildren (wipFiber, elements) {
let index = 0
let prevSibling = null
while (index < elements.length) {
const element = elements[index]
const newFiber = {
type: element.type,
props: element.props,
parent: wipFiber,
dom: null,
}
if (index === 0) {
wipFiber.child = newFiber
} else {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
我们同时循环老的fiber 树的子节点和我们需要调和的新的react节点,此刻只关心oldFIber 和 react element。 react element 是我们想要更新到document上面的元素,oldFiber是我们上次更新完毕的老的fiber 节点。我们需要比较他们,如果前后有任何的改变都需要更新到domcument上面
我们需要用type来对他们进行比较
1. 如果老的fiber和react element 都拥有相同的type(dom节点相同),我们只需要更新他的属性
2. 如果type不同说明这里需要替换成新的dom节点。我们需要创建
3. 如果type 不同且同级存在old fiber 说明老接地那需要被删除,我们需要移除老的节点
react源码中还使用了keys来进行调度调和的优化,通过比较key属性可以得到react elements 中被替换的明确位置
function reconcileChildren (wipFiber, elements){
let index = 0;
let prevSibling = null;
let oldFiber =
wipFiber.alternate
&& wipFiber.alternate.child;
while(index < elements.length || oldFiber) {
const element = elements[index];
let newFiber = null;
const sameType =
element && oldFiber
&& element.type === oldFiber.type;
if (sameType) {
// TODO 更新节点
}
if (!sameType && element) {
// TODO 创建节点
}
if (!sameType && oldFiber) {
// TODO 删除节点
}
// ...省略已有代码
}
}
我们来完成比较的部分
当oldFiber 和 react element 拥有相同的type的时候,我们创建一个新的fiber节点来复用老fiber 的 dom节点,然后从react element 上面取到新的props
我们还给fiber节点新增一个effectTag属性,commit 阶段用这个标识来给节点更新props、新增还是删除。
更新给 effectTag 打 UPDATE。
当react element 需要创建新的dom节点时候,给effectTag 打 PLACEMENT。
第三种情况是当我们需要删除节点的时候我们不需要新的fiber节点,所有我们给旧的fiber添加 effectTag.但是这样操作,当我们把fiber树上的节点更新到document上时不会用到old fiber 的数据结构,这会导致删除的操作没做,所以我们还需要新添加一个数组,用来留存需要进行删除的旧fiber节点。
这部分同步到 render 函数和 commitRoot 函数。
function reconcileChildren (wipFiber, elements){
// ... 省略已有代码
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
errectTag: "UPDATE"
}
}
if (!sameType && element) {
newFiber = {
type: element.type,
props: element.props,
parent: wipFiber,
dom: null,
alternate: null,
errectTag: "PLACEMENT"
}
}
if (!sameType && oldFiber) {
oldFIber.effectTag = "DELETION"
deletions.push(oldFiber)
}
// ...省略已有代码
}
function render(element, container) {
wipRoot = {
dom: container,
props:{children: [element]},
alternate: currentRoot
}
// 新增记录删除的数组
deletions = []
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null;
let currentRoot = null;
let wipRoot = null;
// 新增记录删除的数组
let deletions = null;
function commitRoot () {
election.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot;
wipRoot = null
}
现在让我们来用刚刚添加的effectTag来更改commitWork函数的代码
当effectTag是PLACEMENT 时我们和之前的操作一样,给父fiber节点添加子节点,让为DELETION时我们做相反的操作。
当effectTag是UPDATE 时我们需要在dom节点上更新改变的props属性
function commitWork (fiber) {
if (!fiber) return;
const domParent = fiber.parent.dom;
if (fiber.effectTag === 'PLACEMENT' && fiber.dom != null) {
domParent.appendChild(fiber.dom)
}else if (fiber.effectTag === 'UPDATE' && fiber.dom !== null) {
updataDom(fiber.dom, fiber.alternate.props, fiber.props)
}else if (fiber.effectTag === 'DELETION') {
domParent.removeCHild(fiber.dom)
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
function updataDom (dom, prevProps, nextProps) {
//TODO 更新属性
}
现在我们来完成 updateDom 函数,我们比较新老节点上的props,移除、更新、新增属性。我们还需要对事件监听类的属性做特殊的处理(react中对事件类型统一 on开头),剔除掉on前缀,删除更改的事件,添加新的事件。
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]
)
})
// 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]
)
})
}
到此为止我们的Reconciliation就完成了
7. 函数组件
接下来我们需要增加对函数式组件(function components)的支持。首先我们需要更改例子为简单的函数式组件,它返回一个 h1 元素。
function App(props) {
return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
React.render(element, container)
同样的,我们把它从jsx转化为js:
function App(props) {
return React.createElement(
"h1",
null,
"Hi ",
props.name
)
}
const element = React.createElement(App, {
name: "foo",
})
函数组件有两点不同
1. 函数组件的fiber节点没有保存dom节点
2. 函数组件的子节点是通过运行函数得到的,而不是props的children中得到的。
我们通过检查fiber的type是否为function来确定它是否为函数组件从而进行不同的更新,在updateHostComponent 函数中我们仍然进行之前的逻辑进行非函数组件的更新
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
}
}
随后在updateFunctionComponent函数中我们运行函数式组件的函数,得到子节点,比如上面的例子,fiber节点的type保存的是App函数,我们运行函数将会得到h1节点。
一旦当我们得到子节点之后, reconciliation 函数将一样的工作,我们不需要更改任何的部分
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
但是commitWork函数还是需要进行对应的更改的,因为我们现在拥有了没有保存node节点的函数式组件。我们来更改两个地方。
首先为了找到dom节点的父节点,我们需要一直往上查找fiber树,直到我们找到拥有dom节点的 fiber 节点(类组件)。
删除节点的时候我们也需要一直往上查找直到找到拥有node节点的fiber节点。
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
}
// 更改为找到拥有dom节点的fiber为止
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
8. Hooks
最后一步,我们现在给函数式组件增加 state。我们来改变之前的例子,写一个经典的计数器组件。每当我们点击一下,计数将增加1。
function Counter() {
const [state, setState] = React.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
函数式组件在uploadFunctionComponent函数中完成,我们需要在调用函数组件之前初始化一些全局变量,这样可以在useState中使用他们。
首先我们需要设置一个本次调度的fiber树,同时需要一个保存hooks的数组来支持fiber在一个组件中调用多次useState。还有我们需要保持对当前hook的index的跟踪。
当函数组件调用useState的时候,我们先在alternate属性上面检查是否拥有老的hook。如果老的hook存在。我们直接复制hook上面的state来给新的hook,如果没有我们初始化一个state。然后我们在fiber上面添加这个新的hook,增加hook的index的追踪,然后返回state。
useState 还需要返回一个更新state的函数,所以我们来完成一个setState的函数,该函数接受一个action的入参,在上面的计数器的例子中action就是个函数来每次给计数加一。
我们把action保存到hook新增的一个queue属性中。接着我们做和render函数中类似的事情,新建一个fiber节点,把他设置为nextUnitOfWork(下一个工作单元)。这样在后续的更新中会进行调度更新。
但是目前为止我们仍然未执行action函数,我们在渲染组件的时候来执行action,我们从queue中得到所有的action,然后一个接一个的执行他们得到新的hook和state。所以我们返回的是已经更新过的state。
let wipFiber = null;
let hookIndex = null;
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
const oldHook =
wipFiber.alternate
&& wipFiber.alternate.hooks
&& wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : inital;
queue: []
}
const actions = oldHook ? oldHook.queue : [];
actions.forEach(action => hook.state = action(hook.state));
const setState = action => {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props;
alternate: currentRoot,
}
nextUnitOfWork = wipRoot;
deletions = []
}
wipFiber.hooks.push(hook);
hookIndex++
return [hook.state, setState]
}
整体的代码如下↓
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map(child =>
typeof child === "object"
? child
: createTextElement(child)
),
},
}
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [],
},
}
}
function createDom(fiber) {
const dom =
fiber.type == "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type)
updateDom(dom, {}, fiber.props)
return dom
}
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]
)
})
}
function commitRoot() {
deletions.forEach(commitWork)
commitWork(wipRoot.child)
currentRoot = wipRoot
wipRoot = null
}
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
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) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
}
deletions = []
nextUnitOfWork = wipRoot
}
let nextUnitOfWork = null
let currentRoot = null
let wipRoot = null
let deletions = null
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
if (!nextUnitOfWork && wipRoot) {
commitRoot()
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
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
}
}
let wipFiber = null
let hookIndex = null
function updateFunctionComponent(fiber) {
wipFiber = fiber
hookIndex = 0
wipFiber.hooks = []
const children = [fiber.type(fiber.props)]
reconcileChildren(fiber, children)
}
function useState(initial) {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}
const actions = oldHook ? oldHook.queue : []
actions.forEach(action => {
hook.state = action(hook.state)
})
const setState = action => {
hook.queue.push(action)
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
}
nextUnitOfWork = wipRoot
deletions = []
}
wipFiber.hooks.push(hook)
hookIndex++
return [hook.state, setState]
}
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
function reconcileChildren(wipFiber, elements) {
let index = 0
let oldFiber =
wipFiber.alternate && wipFiber.alternate.child
let prevSibling = null
while (
index < elements.length ||
oldFiber != null
) {
const element = elements[index]
let newFiber = null
const sameType =
oldFiber &&
element &&
element.type == oldFiber.type
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE",
}
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
}
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"
deletions.push(oldFiber)
}
if (oldFiber) {
oldFiber = oldFiber.sibling
}
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
prevSibling.sibling = newFiber
}
prevSibling = newFiber
index++
}
}
const React = {
createElement,
render,
useState,
}
function Counter() {
const [state, setState] = React.useState(1)
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
</h1>
)
}
const element = <Counter />
const container = document.getElementById("root")
React.render(element, container)