Build your own React学习笔记

434 阅读12分钟

原版文章: pomb.us/build-your-…

学习目的:了解React的工作原理、更轻松地深入React代码库

0 回顾基本概念

// 定义一个React element
const element = <h1 title="foo">Hello</h1>
// 从dom获取一个节点
const container = document.getElementById("root")
// 将React元素渲染到容器中
ReactDOM.render(element, container)
// 这是用JSX定义的elmenet,需要用有效JS替换它
const element = <h1 title="foo">Hello</h1>
// 通过Babel构建工具将jsx转换为js
const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
)
// React.createElement根据其参数创建一个对象
const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

我们需要替换的另一段代码是对ReactDOM.render的调用。render 是React更改DOM的地方,所以让我们自己进行更新。

1、首先使用element.type创建一个dom elements

2、然后分配所有的element.props给dom元素

*为避免混淆,我将使用“element”来指代React元素,并使用“node”来指代DOM元素。

3、为children创建一个nodes。使用textNode而不是设置innerText将使我们以后以相同的方式对待所有元素。

注意:设置nodeValue就像设置h1 title一样,等同于children有props: {nodeValue: "hello"}

4、添加textNode到h1,添加h1到container

现在,有了和以前相同的应用程序,但是没有使用react。

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

const container = document.getElementById("root")
​
const node = document.createElement(element.type)
node["title"] = element.props.titleconst text = document.createTextNode("")
text["nodeValue"] = element.props.children
​
node.appendChild(text)
container.appendChild(node)
// 初始代码,方便对比
const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)

1 createElement Function

让我们再次从另一个应用程序开始,这次将用自己的React版本替换React代码。 我们将从编写自己的createElement开始。让我们将JSX转换为JS,这样我们就可以看到createElement调用。

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

正如我们在上一步中看到的,一个element是一个拥有type和props的对象,function唯一需要做的就是创建该对象。

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  }
}
​
const element = React.createElement(
  "div",
  { id: "foo" },
  React.createElement("a", null, "bar"),
  React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

将扩展操作用于props,将剩余参数语法用于children,这样props的children将始终是一个数组。

// For example, createElement("div") returns:
{
  "type": "div",
  "props": { "children": [] }
}
// createElement("div", null, a) returns:
{
  "type": "div",
  "props": { "children": [a] }
}
// createElement("div", null, a, b) returns:
{
  "type": "div",
  "props": { "children": [a, b] }
}

该children数组还可以包含原始值,例如字符串或数字。所以我们将不是对象的所有内容包装在自己的元素中,并为它们创建一个特殊的类型:TEXT_ELEMENT。

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: [],
    },
  }
}
​
const element = React.createElement(
  "div",
  { id: "foo" },
  React.createElement("a", null, "bar"),
  React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

使用Didact(为了教学目的,给自己的库起的类似React的名字)替换react的createElement:

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: [],
    },
  }
}
// *
const Didact = {
  createElement,
}
//const element = Didact.createElement(
//  "div",
//  { id: "foo" },
//  Didact.createElement("a", null, "bar"),
//  Didact.createElement("b")
//)

// 仍然想在这里使用JSX,如何告诉babel使用Didact的createElement而不是React呢?
/** @jsx Didact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

流程总结

1、JSX元素

const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

=> 2、Babel转为有效的js代码

const element = Didact.createElement(
 "div",
 { id: "foo" },
 Didact.createElement("a", null, "bar"),
 Didact.createElement("b")
)

=> 3、createElement生成元素(具有type和props)

// 一个element就是一个对象,它有type和props。type指示要创建的dom节点,props包含jsx属性的所有key和value。
const element = {
    "type":"div",
    "props":{
        "id":"foo",
        "children":[
            {
                "type":"a",
                "props":{
                    "children":[{
                            "type":"TEXT_ELEMENT",
                            "props":{ "nodeValue":"bar", "children":[] }
                        }]
                }
            },
            {
                "type":"b",
                "props":{ "children":[] }
            }
        ]}
}

2 render Function

接下来,我们需要编写ReactDOM.render函数的版本。

function render(element, container) {
  // 用element.type创建dom节点,然后添加到container(注意区分文本元素)
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)
  // 将元素的props分配给节点 
  const isProperty = key => key !== "children"
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name]
    })
​ // 递归地为每一个child做相同操作
  element.props.children.forEach(child =>
    render(child, dom)
  )
​
  container.appendChild(dom)
}

现在,我们有了一个可以将JSX呈现到DOM的库。

codesandbox:将JSX呈现到DOM

codesandbox.io/s/didact-2-…

以第一节的代码为例,其中参数element指的都是注释下面的第一个对象{ }:

// 第1次调用render,参数element如下,container为 <div id="root"></div>
// 经过运行,dom为 <div id="foo"></div>,也是第2次和第4次调用的入参
// 运行element.props.children.forEach方法,分别是第2次和第4次调用
// 第2次调用render后,这里的dom变为 
// <div id="foo">
//    <a>bar</a>
// </div>
// 第4次调用render后,这里的dom变为 
// <div id="foo">
//    <a>bar</a>
// </div>
{
    "type":"div",
    "props":{
        "id":"foo",
        "children":[
            // 第2次调用render,参数element如下,container为 <div id="foo"></div>
            // 经过运行,dom为 <a></a>,也是第3次调用的入参
            // 运行element.props.children.forEach方法
            // 第3次调用render后,这里的dom变为 <a>bar</a>,
            // 运行container.appendChild(dom),得到
            // <div id="foo">
            //    <a>bar</a>
	    // </div>
            // 完成第2次后运行第4次
            {
                "type":"a",
                "props":{
                    "children":[
                        // 第3次调用render,参数element如下,container为<a></a>
                        // 经过运行,dom为"bar";由于children为[],不再继续运行forEach方法
                        // 运行container.appendChild(dom)得到 <a>bar</a>
                        // 完成后继续运行第2次的appendChild方法
                        {
                            "type":"TEXT_ELEMENT",
                            "props":{ "nodeValue":"bar", "children":[] }
                        }]
                }
            },
            // 第4次调用render,参数element如下,container和第2次一样
            // 经过运行,dom为 <b></b>;由于children为[],不再继续运行forEach方法
            // 第2次调用render后,这里的container变为 
            // <div id="foo">
            //    <a>bar</a>
	    // </div>
            // 运行container.appendChild(dom),得到
            // <div id="foo">
            //    <a>bar</a>
            //    <b></b>
	    // </div>
            // 完成第4次后继续运行第1次的appendChild方法
            {
                "type":"b",
                "props":{ "children":[] }
            }
        ]}
}

3 Concurrent Mode 并发模式

先重构。上面的递归调用存在问题。

一旦开始渲染,就不会停止,直到渲染了完整元素树。如果元素树很大,它可能会阻塞主线程很长时间。如果浏览器需要执行高优先级的操作(例如处理用户输入或保持动画流畅),则它必须等到渲染完成。

因此,将把工作分解成几个小单元,在完成每个单元后,如果需要执行其他任何操作,我们将让浏览器中断渲染。

...
container.appendChild(dom)
}
​
let nextUnitOfWork = nullfunction workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )    
    shouldYield = deadline.timeRemaining() < 1 // timeRemaining剩余时间
  } 
  requestIdleCallback(workLoop) // Idle闲置的,IdleCallback空闲回调
}
// 去做循环,视为setTimeout,区别是浏览器在主线程空闲时运行回调,而不是人为告诉它何时运行
requestIdleCallback(workLoop)
// 执行工作 & 返回下一个工作单元
function performUnitOfWork(nextUnitOfWork) {
  // TODO
}
​
const Didact = {
  createElement,
  render,
...

使用requestIdleCallback来调用workLoop循环。可以将requestIdleCallback视为setTimeout,但是浏览器将在主线程空闲时运行回调,而不是我们告诉它何时运行。

requestIdleCallback提供了deadline参数,可以使用它来检查在浏览器需要再次控制之前,我们还有多少时间。

React不再使用requestIdleCallback了。现在它使用scheduler package github.com/facebook/re… 但是对于此用例,它在概念上是相同的。

要开始使用循环,我们需要设置第一个工作单元,然后编写一个performUnitOfWork函数,它不仅执行工作而且还返回下一个工作单元。

4 Fibers

定义

为了组织工作单元,需要一个数据结构:a fiber tree

每一个element都有一个fiber,并且每一个fiber将是一个工作单元。

假设我们要渲染一个像这样的元素树:

Didact.render(
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>,
  container
)

在render中我们创建root fiber,并将其设置为nextUnitOfWork。剩下的工作将在performUnitOfWork函数里进行,我们将为每个fiber做三件事:

1、add the element to the DOM

2、create the fibers for the element’s children

3、select the next unit of work

实现目标

该数据结构的目标之一是使查找下一个工作单元变得容易。这就是为什么每个fiber都链接到其第一个子节点,下一个兄弟节点和父节点。

工作原理 & 执行查找顺序

当完成一个fiber上的执行工作后,如果有child,它将成为下一个工作单元。如果没有child,使用sibling作为下一个工作单元。

在这个示例中,当完成div fiber上的工作时,下一个工作单元是h1 fiber。p fiber没有child,所以p完成后移动到a fiber。

如果fiber没有child也没有sibling,就去找“叔叔”节点(父节点的兄弟节点),就像示例中的a fiber和h2 fiber。如果父节点没有兄弟节点,将继续遍历它的父节点,直到找到带有兄弟节点的父节点,或到达根节点为止。如果到达根节点,则意味着已经完成了render的所有执行工作。

// 创建dom节点(携带props参数),不带childern数据
// 例如 <div id="foo"></div>,<a></a> 这样
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
}
// 1.在render里设置fiber tree的root为nextUnitOfWork
function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {
      children: [element],
    },
  }
}
​
let nextUnitOfWork = null
// 2.当浏览器准备就绪,将调用workLoop,开始在root上工作
function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
  requestIdleCallback(workLoop)
}
​
requestIdleCallback(workLoop)
​
function performUnitOfWork(fiber) {
  // 3. add dom node
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }
  // 4. 为每个child创建一个新fiber
  const elements = fiber.props.children
  let index = 0
  let prevSibling = nullwhile (index < elements.length) {
    const element = elements[index]
​
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }
    // 5. 添加newFiber到fiber树中,它是child还是sibling取决于是否是第一个child
    if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
  // 6. return next unit of work
  // 最后,我们搜索下一个工作单元。首先尝试child,然后sibling,然后uncle,依此类推。
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

流程梳理

*这部分为自己梳理所用,非官方。

1、createElement()

生成element,传给render:

{
    "type":"div",
    "props":{
        "id":"div",
        "children":[ ]
    }
}

2、render()

入参element见1

入参container:<div id="root"></div>

然后交叉调用了几次workLoop()和createElement(),期间nextUnitOfWork值为undefined

3、workLoop()

此时nextUnitOfWork =

{
    "dom": <div id="root"></div>,
    "props":{
        "children":[{
                "type":"div",
                "props":{
                    "id":"div",
                    "children":[ ] }
            }]
    }
}

4、performUnitOfWork()

入参fiber:第一次是 3 的nextUnitOfWork

第二次:

const divProps = {
        "id":"div",
        "children":[ ] }
        
const fiber = {
    "type": "div",
    "props": divProps,
    "parent":{
        "dom":"div#div",
        "props":{  
              "children":[{
                     "type":"div",
                     "props": divProps,
               }]
         },
         // 详见下图,值为此fiber,嵌套多层之后,最里面的child为 [ ]
        "child": { } 
    },
   "dom": <div id="root"></div>,
}

5、createDom()

自定义codesandbox:理解performUnitOfWork流程

codesandbox.io/s/didact-6-…

其中nextUnitOfWork的变化如下:

pTextElement = {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: "pText",
      children: []
    }
  }
  h2TextElement = {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: "h2Text",
      children: []
    }
  }
  pElement = {
    type: "p",
    props: {
      children: [this.pTextElement]
    }
  }
  aElement = {
    type: "a",
    props: {
      children: []
    }
  }
  h1Element = {
    type: "h1",
    props: {
      children: [this.pElement, this.aElement]
    }
  }
  h2Element = {
    type: "h2",
    props: {
      id: "h2Id",
      children: [this.h2TextElement]
    }
  }
  divElement = {
    type: "div",
    props: {
      id: "divId",
      children: [this.h1Element, this.h2Element]
    }
  }
// 截图一共7条记录,按顺序展开,这里只记录了每个对象的type、props、parent部分属性
  a1 = {
    ...this.divElement,
    parent: {
      props: {
        children: [this.divElement]
      },
    }
  }
  b2 = {
    ...this.h1Element,
    parent: {
      props: {
        id: "divId",
        children: [this.h1Element, this.h2Element]
      },
    }
  }
  c3 = {
    ...this.pElement,
    parent: {
      props: {
        children: [this.pElement, this.aElement]
      },
    }
  }
  d4 = {
    ...this.pTextElement,
    parent: {
      props: {
        children: [this.pTextElement]
      },
    }
  }
  e5 = {
    ...this.aElement,
    parent: {
      props: {
        children: [this.pElement, this.aElement]
      },
    }
  }
  f6 = {
    ...this.h2Element,
    parent: {
      props: {
        id: "divId",
        children: [this.h1Element, this.h2Element]
      },
    }
  }
  g7 = {
    ...this.h2TextElement,
    parent: {
      props: {
        id: "h2Id",
        children: [this.h2TextElement]
      },
    }
  }

5 Render和Commit阶段

function performUnitOfWork(fiber) {
  ...
  // 这里删除,在下面的commitRoot里递归地添加所有节点到dom
  if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  ...
}

这里还有另一个问题。每次处理一个元素时,都会向DOM添加一个新节点。但是在完成渲染整个树之前,浏览器可能会中断我们的工作。在这种情况下,用户将看到不完整的UI。所以从此处删除对dom进行变异的部分。相反,我们将跟踪fiber tree的root,称其为wipRoot。

// 将所有节点递归添加到dom
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)
}

function render(element, container) {
 // 跟踪fiber tree的root,称其为wipRoot
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}
​
let nextUnitOfWork = null
let wipRoot = null // 追踪fiber tree的root,称其为wipRoot

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }
  // 一旦完成所有工作(没有下一个工作单元),将整个fiber tree提交给dom
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }
  requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)

6 Reconciliation (Reconcile调和)

目前为止仅向dom添加了内容,那么更新或删除节点呢?需要将render函数中获取的elements元素提交给dom的最后一棵fiber树进行比较。

currentRoot & alternate

因此,在完成提交之后,需要保存对“提交给DOM的最后一棵fiber树”的引用。称之为currentRoot

给每一个fiber添加alternate(备用)属性,这个属性是旧fiber的link,旧fiber是在上一个提交阶段提交给dom的。

function commitRoot() {
  commitWork(wipRoot.child)
  // 1.保存对“提交给DOM的最后一棵fiber树”的引用
  currentRoot = wipRoot
  wipRoot = null
}
function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    // 2.添加alternate(备用)属性,链接到旧fiber
    alternate: currentRoot,
  }
  nextUnitOfWork = wipRoot
}
​
let nextUnitOfWork = null
let currentRoot = null // *
let wipRoot = 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) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  const elements = fiber.props.children
  // 3.提取这里创建新fiber的代码,放到新加的reconcileChildren函数里
  reconcileChildren(fiber, elements) 
​
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}
// reconcile:调和
function reconcileChildren(wipFiber, elements) {
  let index = 0
  let prevSibling = nullwhile (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++
  }
}

oldFiber & element

接下来使用新元素来调和旧fibers。我们同时遍历旧fiber(wipFiber.alternate)的children要reconcile(调和)的元素数组

如果我们忽略同时遍历数组和链表所需的所有boilerplate(样板),在此期间剩下的最重要的是:oldFiber and element元素是要渲染到DOM的东西,oldFiber是最后一次渲染的东西。 我们需要对它们进行比较,以查看是否需要对DOM进行任何更改。

1、旧fiber和新元素的type相同,dom节点不变,仅用新的props更新。

2、type不同 & 有新元素,创建一个新的dom节点。

3、type不同 & 有旧fiber,删除旧节点。

在这里,React也使用keys,这样可以实现更好的reconciliation。For example, it detects when children change places in the element array.(它检测children何时更改元素数组中的位置)

示例的JSX(对应nextUnitOfWork,也就是performUnitOfWork()的入参fiber):

  <div id="divId">                // div有child
    <h1>                          // h1有parent、sibling、child
      <p>pText</p>                // p有parent、sibling、child
      <a></a>                     // a有parent
    </h1>
    <h2 id="h2Id">h2Text</h2>     // h2有parentchild
  </div>

所有element都有type、props、dom(dom标签/Text),可选属性有parent、sibling、child

  pTextElement = {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: "pText",
      children: []
    }
  }
  pElement = {
    type: "p",
    props: {
      children: [this.pTextElement]
    },
    child: this.pTextElement,
    sibling: this.aElement
  }
  aElement = {
    type: "a",
    props: {
      children: []
    },
    parent: this.h1Element,
    dom: html标签结构
  }
  h1Element = {
    type: "h1",
    props: {
      children: [this.pElement, this.aElement]
    },
    parent: this.divElement,
    sibling: this.h2Element
    child: this.pElement,
  }
  divElement = {
    type: "div",
    props: {
      id: "divId",
      children: [this.h1Element, this.h2Element]
    },
    child: this.h1Element
  }

reconcileChildren(wipFiber, elements)

// wipFiber = performUnitOfWork(fiber)的入参fiber
// elements = fiber.props.children;
function reconcileChildren(wipFiber, elements) {
  let index = 0
  let oldFiber =
    wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = nullwhile (
    index < elements.length ||
    oldFiber != null
  ) {
    const element = elements[index]
    let newFiber = nullconst sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type// update node. DOM node from the old fiber & props from the element
    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE", // 新属性,稍后在提交阶段使用
      }
    }
    // add node. element needs a new DOM node
    if (element && !sameType) {
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT", // 放置
      }
    }
    // delete the oldFiber's node,见删除说明
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION"
      deletions.push(oldFiber)
    }
​
    if (oldFiber) {
      oldFiber = oldFiber.sibling
    }
​
    if (index === 0) {

删除说明:

But when we commit the fiber tree to the DOM we do it from the work in progress root, which doesn’t have the old fibers.So we need an array to keep track of the nodes we want to remove.(当我们将fiber树提交给DOM时,是从正在工作的root开始进行的,该root没有旧的fiber。所以需要一个数组来跟踪要删除的节点。

And then, when we are commiting the changes to the DOM, we also use the fibers from that array.

接下来在commitWork()里处理effectTags,并添加新方法updateDom()。

// 事件监听是一种特殊的prop类型,它的名称以“on”前缀开头
const isEvent = key => key.startsWith("on")
const isProperty = key =>
  key !== "children" && !isEvent(key)
// props的key值是否有变化
const isNew = (prev, next) => key =>
  prev[key] !== next[key]
// 旧props的key不存在于新props对象里,返回true,代表旧key isGone
const isGone = (prev, next) => key => !(key in next)
// 更新节点(使用新props)。比较新旧fiber的props,删除消失的props,设置新的或变化的props
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() {
  // 将更改提交给dom时也使用
  deletions.forEach(commitWork) 
  commitWork(wipRoot.child)
  currentRoot = wipRoot 
  wipRoot = null
}
function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  // 添加节点
  if (
    fiber.effectTag === "PLACEMENT" &&
    fiber.dom != null
  ) {
    domParent.appendChild(fiber.dom)
  } 
  // 更新节点(使用新props)
  else if (
    fiber.effectTag === "UPDATE" &&
    fiber.dom != null
  ) {
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } 
  // 删除节点
  else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom)
  }
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

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) { ... }

codesandbox:带reconciliation的版本

codesandbox.io/s/didact-6-…

7 Function Components

我们使用这个简单的示例:

/** @jsx Didact.createElement */
function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)

将jsx转换为js:

function App(props){
  return Didact.createElement(
    "h1",
    null,
    "hi",
    props.name
  )
}
const element = Didact.createElement(App,
{
  name: "foo",
})

函数组件有两点不同:

1、function component的fiber没有DOM节点。

2、children来自于运行function,而不是直接从props获取。

接下来针对之前的代码做修改:

1、performUnitOfWork()里判断fiber.type,并添加updateFunctionComponent()和移动部分老代码至新的updateHostComponent()

2、修改commitWork(),

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) {
  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]
      )
    })

  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = ""
    })

  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })

  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
  }
  // 旧方式:const domParent = fiber.parent.dom
  // 现在一些fiber没有dom节点,所以为了找到dom节点的parent,需要沿着fiber tree向上移动,直到找到带有dom节点的fiber
  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") {
    // 旧方式:domParent.removeChild(fiber.dom)
    commitDeletion(fiber, domParent)
  }
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
// 删除节点时,也需要保持移动,直到找到 a child with a DOM node
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
  }
}
// get the children
function updateFunctionComponent(fiber) {
  // 这里fiber.type是App函数,children是函数返回的h1元素
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
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 Didact = {
  createElement,
  render,
}

/** @jsx Didact.createElement */
const container = document.getElementById("root")

const updateValue = e => {
  rerender(e.target.value)
}

const rerender = value => {
  const element = (
    <div>
      <input onInput={updateValue} value={value} />
      <h2>Hello {value}</h2>
    </div>
  )
  Didact.render(element, container)
}

rerender("World")

8 hooks

最后一步,给function components添加state。

// 1.在调用函数组件之前初始化一些全局变量,以便可以在useState函数中使用它们
let wipFiber = null
let hookIndex = nullfunction updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  // 支持在同一组件中多次调用useState
  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: [],
  }
  // 下次渲染组件时,将从oldHook.queue中获取所有actions,然后逐一应用于新的hook.state,因此返回该state时,将对其进行更新。
  const actions = oldHook ? oldHook.queue : []
  actions.forEach(action => {
    hook.state = action(hook.state)
  })
​
  const setState = action => {
    hook.queue.push(action)
    // 做类似render函数里的一些操作。将新的进行中的工作root设为nextUnitOfWork,这样workLoop函数可以开始一个新的渲染阶段
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    nextUnitOfWork = wipRoot
    deletions = []
  }
​
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}

const Didact = {
  createElement,
  render,
  useState,
}
​
/** @jsx Didact.createElement */
function Counter() {
  const [state, setState] = Didact.useState(1)
  return (
    <h1 onClick={() => setState(c => c + 1)}>
      Count: {state}
    </h1>
  )
}
const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)

codesandbox:完整版本

codesandbox.io/s/didact-8-…

尾声

我们没有包括很多React features和优化。例如,以下是React做的一些不同的事情:

1、在Didact中,我们在渲染阶段遍历整棵树。 相反,React遵循一些提示和试探法(hints and heuristics),以跳过没有任何更改的整个子树。

2、我们还在提交阶段遍历整棵树。React keeps a linked list with just the fibers that have effects and only visit those fibers.

3、在progress tree中,每次我们创建一个新work时,就会为每个fiber创建新对象。React recycles the fibers from the previous trees.

4、当Didact在渲染阶段收到新的更新时,它将丢弃进行中的工作树,然后从根开始重新进行。React使用过期时间戳标记每个更新,并使用它来决定哪个更新具有更高的优先级。