乐趣区

关于html5:学习笔记构建你自己的React

Build your own React

Build your own React 的学习笔记

预览

Build your own React CN

前言

重写 React, 遵循 React 代码中的架构, 然而没有进行优化。基于 React16.8, 应用 hook 并删除了所有与类相干的代码。

零: review

首先回顾一些 React 的概念,上面是一个简略的 React 应用程序。一共三行代码,第一行定义了一个 React 元素, 第二行获取了 DOM 节点, 最初一行将 React 元素渲染到容器中。

const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)

第一行中,咱们应用了 JSX, JSX 不是无效的 JavaScript,咱们应用原生 js 替换它。通常通过 Babel 等构建工具,JSX 转换为 JS。应用 createElement 替换 JSX 标记,并将标签名,props,子级作为参数。

const element = React.createElement(
  "h1",
  {title: "foo"},
  "Hello"
);

React.createElement, 会依据参数创立一个对象。除了一些验证外,这就是 React.createElement 所做的全副。咱们能够间接 React.createElement 函数替换成它的输入。

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

一个一般的 JavaScript 对象, 次要有两个属性 typepropstype属性是一个字符串,示意咱们创立的 DOM 节点的类型。它也能够是一个函数,然而咱们留在前面说。props是一个对象, props中有一个非凡的属性 children。在以后的状况children 是字符串,然而通常状况下它是蕴含更多元素的数组。接下来咱们须要替换ReactDOM.render

首先应用 type 属性,创立一个节点。咱们将 element 的所有 props 调配给该节点,目前只有 title 属性。而后咱们为子节点创立节点。咱们的 children 是一个字符串,因而咱们创立一个文本节点。

为什么应用 createTextNode 而不是 innerText 呢?因为在之后都会以雷同的形式解决所有元素。

最初将 textNode 增加到 h1 中,h1 增加到 container 中。

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}
​const container = document.getElementById("root")

const node = document.createElement(element.type)
node["title"] = element.props.title
const text = document.createTextNode("")
text["nodeValue"] = element.props.children
node.appendChild(text)
container.appendChild(node)

目前咱们领有了和之前一样的程序,然而没有应用 React。

一: createElement

咱们从一个新的程序开始,这次咱们应用本人的 React 替换原来的 React 代码。

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

咱们从编写本人的 createElement 开始。

const element = createElement(
  "div",
  {id: "foo"},
  createElement("a", null, "bar"),
  createElement("b")
)
const container = document.getElementById("root")
render(element, container)

createElement须要做的就是创立一个 typeprops的对象。createElement函数中, children参数应用 rest 运算符, children始终就会为数组。

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  }
};

例如, createElement("div", null, a, b)会返回:

{
  "type": "div",
  "props": {"children": [a, b] }
}

目前 children 数组中会蕴含原始值,比方字符串和数字。咱们须要对它们进行包装。咱们创立一个非凡的类型TEXT_ELEMENT

在 React 源码中,不会包装原始值, 或者在没有子级的状况下创立空的数组。咱们这样做的目标是为了简化咱们的代码.

function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],},
  }
}

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}

咱们如何让 Babel 在编译的过程中,应用咱们本人创立的 createElement 呢?咱们在配置 babel@babel/preset-react插件时自定义 pragma 参数

二: render

接下来咱们须要编写本人的ReactDOM.render

目前咱们只关怀向 DOM 中增加内容,稍后解决更新和删除

咱们首先应用元素的类型创立 DOM 节点,而后将新节点增加到容器中

function render(element, container) {const dom = document.createElement(element.type)
  container.appendChild(dom)
}

咱们须要递归的为每一个 children 元素做雷同的事件

function render(element, container) {const dom = document.createElement(element.type)
  element.props.children.forEach(child =>
    render(child, dom)
  )
  container.appendChild(dom)
}

之前增加了文本元素的节点,所以在创立节点时须要判断元素的类型

function render(element, container) {
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)

  element.props.children.forEach(child =>
    render(child, dom)
  )
  container.appendChild(dom)
}

最初咱们须要将元素的 props 增加到节点的属性上

function render(element, container) {
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type)

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

目前为止,咱们曾经有了一个将 JSX 出现到 DOM 的库。

三: 并发模式

在这之前,咱们须要重构代码。

递归渲染存在问题,一旦开始渲染就无奈进行,直到咱们渲染实现整个树。如果树很大,会阻塞主线程过长的工夫。

????️: React Fiber 架构应用了链表树实现了可中断渲染,如果大家有趣味能够参考这篇文章

因而咱们须要把工作分解成几个小单元,在咱们实现每个单元后,有重要的事件要做,咱们中断渲染。

咱们应用 requestIdleCallback 实现循环, 浏览器会在闲暇时,执行 requestIdleCallback 的回调。React 的外部并不应用 requestIdleCallback, React 外部应用 scheduler package, 通过requestIdleCallback 咱们还能够取得咱们还有多少可用工夫用于渲染。

????️: 对于 requestIdleCallback 的更多细节能够查看这篇文章,详解 requestIdleCallback

let nextUnitOfWork = null
​
function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1}
  requestIdleCallback(workLoop)
}
​
requestIdleCallback(workLoop)
​
function performUnitOfWork(nextUnitOfWork) {// TODO}

????️: nextUnitOfWork 变量放弃了 Fiber 中须要工作节点援用或者为 null, 如果是 null 示意没有工作。

要开始咱们的 workLoop, 咱们须要第一个工作单元(Fiber 节点),而后编写performUnitOfWork 函数,performUnitOfWork函数执行工作,并返回下一个须要工作的节点。

四: Fibers

咱们须要一个数据结构 Fiber 树(链表树)。每一个元素都有对应的 Fiber 节点, 每一个 Fiber 是一个工作单元。

假如咱们须要渲染这样的一颗树:

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

render 中,创立 Fiber,并将根节点的 Fiber 调配给 nextUnitOfWork 变量。余下的工作在 performUnitOfWork 函数进行,须要做三件事:

  1. 将元素增加到 DOM
  2. 为子节创立 Fiber
  3. 返回下一个工作单元

Fiber 树是一个链表树,每一个 Fiber 节点有 child, parent, sibling 属性

  • child, 第一个子级的援用
  • sibling, 第一个同级的援用
  • parent,父级的援用

????️: 在 React 的 Fiber 节点中,应用 return 字段保留了对父 Fiber 节点的援用

遍历 Fiber 树 (链表树) 时应用了深度优先遍历,说一下遍历的过程:

  1. 从根节点 root 获取第一个子节点
  2. 如果 root 有子节点,将以后指针设置为第一个子节点,并进入下一次迭代。(深度优先遍历)
  3. 如果 root 的第一个子节点,没有子节点,则尝试获取它的第一个兄弟节点。
  4. 如果有兄弟节点,将以后指针设置为第一个子节点,而后兄弟节点进入深度优先遍历。
  5. 如果没有兄弟节点,则返回根节点 root。尝试获取父节点的兄弟节点。
  6. 如果父节点没有兄弟节点,则返回根节点 root。最初完结遍历。

好,接下来咱们开始增加代码, 将创立的 DOM 的代码独自抽离出, 稍后应用它

function createDom(fiber) {
  const dom = fiber.type == "TEXT_ELEMENT"
    ? document.createTextNode("")
    : document.createElement(element.type)

  const isProperty = key => key !== "children"

  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {dom[name] = element.props[name]
    })
  return dom
}

render 函数中,将 nextUnitOfWork 变量设置为 Fiber 节点树的根

function render(element, container) {
  nextUnitOfWork = {
    dom: container,
    props: {children: [element],
    },
  }
}

当浏览器准备就绪,调用 workLoop,开始解决根节点

let nextUnitOfWork = null
​
function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1}
  requestIdleCallback(workLoop)
}
​
requestIdleCallback(workLoop)
​
function performUnitOfWork(fiber) {
  // 增加 DOM 节点
  // 创立 Fiber
  // 获取下一个解决工作的 Fiber 节点
}

首先创立 DOM, 并增加到 Fiber 节点的 dom 字段中,咱们在 dom 字段中保留对 dom 的援用

function performUnitOfWork(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber)
  }
​
  if (fiber.parent) {fiber.parent.dom.appendChild(fiber.dom)
  }
}

????️: 在 React 的 Fiber 节点中,stateNode字段,保留对 class 组件实例的援用, DOM 节点或其余与 Fiber 节点相关联的 React 元素类实例的援用。

接下来为每一个子元素创立 Fiber 节点。同时因为 Fiber 树是一个链表树,所以咱们须要为 Fiber 节点增加 child, parent, sibling 字段

function performUnitOfWork(nextUnitOfWork) {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, // 父 Fiber 节点的援用
      dom: null,
    }

    if (index === 0) {
      // 父 Fiber 节点增加 child 字段
      fiber.child = newFiber
    } else {
      // 同级的 Fiber 节点增加 sibling 字段
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

在实现的以后节点的工作后,咱们须要返回下一个节点。因为是深度优先遍历,首先尝试遍历 child,而后是sibling, 最初回溯到parent, 尝试遍历parentsibling

function performUnitOfWork(nextUnitOfWork) {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 字段
      fiber.child = newFiber
    } else {
      // 同级的 Fiber 节点增加 sibling 字段
      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
  }
}

五: render 和 commit

目前存在的问题,在遍历 Fiber 树的时候,咱们目前会在这里向 DOM 中增加新节点,因为咱们应用requestIdleCallback, 浏览器可能会中断咱们的渲染,用户会看到不残缺的 UI。这违反了一致性的准则。

????️: React 的外围准则之一是 ” 一致性 ”, 它总是一次性更新 DOM, 不会显示局部后果。

????️: 在 React 的源码中, React 分为两个阶段执行工作, render阶段和 commit 阶段。render阶段的工作是能够异步执行的,React 依据可用工夫解决一个或者多个 Fiber 节点。当产生一些更重要的事件时,React 会进行并保留已实现的工作。等重要的事件解决实现后,React 从中断处持续实现工作。然而有时可能会放弃曾经实现的工作,从顶层从新开始。此阶段执行的工作是对用户是不可见的,因而能够实现暂停。然而在 commit 阶段始终是同步的它会产生用户可见的变动, 例如 DOM 的批改. 这就是 React 须要一次性实现它们的起因。

咱们须要删除 performUnitOfWork 函数中更改 DOM 的代码。

function performUnitOfWork(nextUnitOfWork) {if (!fiber.dom) {fiber.dom = createDom(fiber)
  }

  const elements = fiber.props.children

  // ...

咱们须要保留 Fiber 树根的援用, 咱们称其为正在工作的 root 或wipRoot

????️: 在 React 中 Fiber 树的根被称为 HostRoot。咱们能够在通过容器的 DOM 节点获取, 容器 DOM._reactRootContainer._internalRoot.current

????️: wipRoot相似 React 源码中 workInProgress tree 的根节点,在 React 应用程序中,咱们能够通过 容器 DOM._reactRootContainer._internalRoot.current.alternate, 获取 workInProgress tree 的根节点。

let wipRoot = null

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}

实现了所有的工作。咱们须要把整个 Fiber 树更新到 DOM 上。咱们须要在 commitRoot 函数中实现这个性能。

function commitWork(fiber) {if (!fiber) {return}
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  // 递归子节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function commitRoot() {commitWork(wipRoot.child)
  wipRoot = null
}

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1}
  // 如果 nextUnitOfWork 为假, 阐明所有的工作都曾经做完了, 咱们须要进入 commit 阶段
  if (!nextUnitOfWork && wipRoot) {
    // 增加 dom
    commitRoot()}
}

????️: 在 React 的源码中 commit 阶段从 completeRoot 函数开始,在开始任何工作前,它将 FiberRootfinishedWork属性设置为 null。

六: 协调

目前为止,咱们仅仅向 DOM 中增加了内容,然而更新和删除呢?咱们须要将 render 函数接管到元素和提交到 DOM 上的最初的 Fiber 树进行比照。

因而在 commit 咱们须要保留最初的 Fiber 树的援用,咱们称之为 currentRoot。咱们还将alternate 字段增加到每一个 Fiber 节点上,alternate字段上保留了 currentRoot 的援用。

????️: 在 React 源码中,在第一次渲染实现后,React 会生成一个 Fiber 树。该树映射了应用程序的状态,这颗树被称为 current tree。当应用程序开始更新时,React 会构建一个workInProgress tree, workInProgress tree 映射了将来的状态。

????️: 所有的工作都是在 workInProgress tree 上的 Fiber 上进行的。当 React 开始遍历 Fiber 时,它会为每一个现有的 Fiber 节点创立一个备份, 在 alternate 字段中,备份形成了workInProgress tree

let nextUnitOfWork = null
let wipRoot = null
let currentRoot = null

function commitWork(fiber) {if (!fiber) {return}
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  // 递归子节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function commitRoot() {commitWork(wipRoot.child)
  // 保留最近一次输入到页面上的 Fiber 树
  currentRoot = wipRoot
  wipRoot = null
}

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {children: [element],
    },
    alternate: currentRoot,
  }
  nextUnitOfWork = wipRoot
}

接下来咱们须要从 performUnitOfWork 函数中将创立 Fiber 的代码提取进去,一个新的 reconcileChildren 函数。在这里咱们将对 currentRoot(当前页面对应的 Fiber 树) 与新元素进行协调。

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: fiber, // 父节点的援用
      dom: null,
    }

    if (index === 0) {
      // 父 Fiber 节点增加 child 字段,child 指向了第一个子节点
      wipFiber.child = newFiber
    } else {
      // 同级的 Fiber 节点增加 sibling 字段
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

function performUnitOfWork(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber)
  }
​
  const elements = fiber.props.children
  reconcileChildren(wipFiber, elements)

  // 首先尝试子节点
  if (fiber.child) {return fiber.child}
  let nextFiber = fiber
  while (nextFiber) {
    // 尝试同级节点
    if (nextFiber.sibling) {return nextFiber.sibling}
    nextFiber = nextFiber.parent
  }
}

咱们同时遍历旧的 Fiber 树,既 wipFiber.alternate,和须要协调的新的元素。如果咱们疏忽遍历链表和数组的模版代码。那么在while 循环中,最重要的就是 oldFiberelementelement是咱们须要渲染的 DOM, oldFiber是上次渲染的 Fiber。咱们须要比拟它们,以确定 DOM 是否须要任何的更改。

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

    // TODO compare oldFiber to element

    // ....

    if (oldFiber) {oldFiber = oldFiber.sibling}

    if (index === 0) {
      // 父 Fiber 节点增加 child 字段,child 指向了第一个子节点
      wipFiber.child = newFiber
    } else {
      // 同级的 Fiber 节点增加 sibling 字段
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

为了比拟它们咱们应用以下的规定:

  1. 如果 oldFiberelement具备雷同的类型,咱们保留 DOM 节点,并应用新的 props 更新
  2. 如果类型不同,并且有新元素。咱们须要创立一个新的 DOM 节点。
  3. 如果类型不同,存在之前的 Fiber,咱们须要移除旧节点
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) {// 更新节点}

    if (!sameType && element) {// 新增节点}

    if (!sameType && oldFiber) {// 删除节点}

    if (oldFiber) {oldFiber = oldFiber.sibling}

    if (index === 0) {
      // 父 Fiber 节点增加 child 字段,child 指向了第一个子节点
      wipFiber.child = newFiber
    } else {
      // 同级的 Fiber 节点增加 sibling 字段
      prevSibling.sibling = newFiber
    }
​
    prevSibling = newFiber
    index++
  }
}

在 React 中,React 应用了 key, 能够更好的进行协调,应用key 能够检测元素在列表中地位是否扭转,更好的复用节点。

当之前的 Fiber 和新元素具备雷同的类型时,咱们创立一个新的 Fiber 节点,保留旧 Fiber 的 DOM 节点和元素的 props。

并且为 Fiber 增加了一个新的属性 effectTag, 稍后在commit 阶段应用

????️: 在 React 源码中 effectTag, effectTag 编码的是与 Fiber 节点相干的 effects(副作用)。React 中effectTag 应用了数字的模式存储,应用了按位或结构了一个属性集。更多内容请查看

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 (!sameType && element) {// 新增节点}

    if (!sameType && oldFiber) {// 删除节点}

    // ...
  }
}

对于新增的节点,咱们在 effectTag 属性上,应用 PLACEMENT 标记进行标记。

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 && element) {
      // 新增节点
      newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
      }
    }

    if (!sameType && oldFiber) {// 删除节点}

    // ...
  }
}

对于须要删除节点,咱们不创立新的 Fiber,而是将 effectTag 设置为DELETION, 并增加到旧的 Fiber 节点上。

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 && oldFiber) {
      // 删除节点
      oldFiber.effectTag = "DELETION"
      deletions.push(oldFiber)
    }

    // ...
  }
}

当咱们在 commit 时, 咱们从新构建的 Fiber 节点树开始遍历,因为没有须要保留删除的旧节点。所以咱们须要额定应用一个数组 deletions 保留须要删除的旧节点

????️: 在 React 的源码,workInProgress tree的 Fiber 节点领有 current tree 对应节点的援用。反之亦然。

????️: 在 React 的源码中,会把所有须要在 commit 阶段,执行副作用的 Fiber 节点,构建为线性列表,以不便疾速迭代。迭代线性列表要比迭代树快的多,因为不须要迭代没有 side-effects 的节点。

let deletions = null

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {children: [element],
    },
    alternate: currentRoot,
  }
  deletions = []
  nextUnitOfWork = wipRoot
}

当咱们进入 commit 阶段时,应用该数组中的 Fiber

function commitRoot() {deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}

当初让我批改 commitWork 函数以解决新的 effectTag 字段

如果 effectTagPLACEMENT, 与之前一样,将 DOM 增加增加到父节点上

function commitWork(fiber) {if (!fiber) {return}
  const domParent = fiber.parent.dom
  // 对于新增节点的解决
  if (
    fiber.effectTag === "PLACEMENT" &&
    fiber.dom != null
  ) {domParent.appendChild(fiber.dom)
  }
  // 递归解决子节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

如果 effectTagDELETION, 咱们从父节点上删除节点

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 === "DELETION") {
    // 对于删除节点的解决
    domParent.removeChild(fiber.dom)
  }
  // 递归解决子节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

如果 effectTagUPDATE, 咱们应用新的 props 更新当初的 DOM

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 === "DELETION") {
    // 对于删除节点的解决
    domParent.removeChild(fiber.dom)
  } else if (
    fiber.effectTag === "UPDATE" &&
    fiber.dom != null
  ) {
    // 对于须要更新节点的解决
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  }
  // 递归解决子节点
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

接下来须要实现 updateDom 函数

function updateDom(dom, prevProps, nextProps) {// TODO}

咱们应用旧的 Fiber 的 props 和新的 Fiber 的 props 进行比拟,移除删除的的 props,增加或更新已更改的 props

// 用于排除 children 属性
const isProperty = key => key !== "children"
// 用于判断是否更新了属性
const isNew = (prev, next) => key => prev[key] !== next[key]
// 用于判断在新的 props 上是否有属性
const isGone = (prev, next) => key => !(key in next)

function updateDom(dom, prevProps, nextProps) {
  // 删除之前的属性
  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]
    })
}

咱们须要对事件监听器做非凡的解决,如果 props 以 on 结尾, 咱们应用不同的形式去解决它们

// 判断 props 是否是 on 结尾
const isEvent = key => key.startsWith("on")
// 用于排除 children 属性,和 on 结尾的属性
const isProperty = key => key !== "children" && !isEvent(key)

如果事件处理程序产生了更改,咱们须要首先删除,而后增加新的处理程序

????️: 间接在 DOM 上增加事件处理程序的形式,有点相似 preact 中的解决形式

function updateDom(dom, prevProps, nextProps) {Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      key =>
        // 如果事件处理程序产生了更新,获取新的 props 上没有
        // 须要先删除之前的处理程序
        !(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 组件

咱们须要增加的下一件事是对 Function 组件的反对。咱们批改下咱们的例子。

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

咱们将 jsx 转换为 js

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

Function 组件和 DOM 次要有两个不同

  1. Function 组件的 Fiber 没有 DOM 节点
  2. children 来自 Function, 而不是间接从 DOM 中间接获取

咱们查看 Fiber 的类型是否为函数,并依据类型由不同的函数进行解决,如果是不同的 DOM,传入updateHostComponent

function performUnitOfWork(fiber) {
  // 判断是不是函数组件
  const isFunctionComponent =
    fiber.type instanceof Function

  if (isFunctionComponent) {updateFunctionComponent(fiber)
  } else {updateHostComponent(fiber)
  }
  // 接下来返回下一个须要解决的 Fiber 节点,因为是深度优先遍历,优先从子节点开始
  if (fiber.child) {return fiber.child}
  let nextFiber = fiber
  while (nextFiber) {if (nextFiber.sibling) {return nextFiber.sibling}
    nextFiber = nextFiber.parent
  }
}

updateHostComponent和咱们之前做的一样

function updateHostComponent () {if (!fiber.dom) {
    // 创立 dom 节点
    fiber.dom = createDom(fiber)
  }
​ // 子元素
  const elements = fiber.props.children
  // 子元素与旧的 Fiber 进行子协调
  reconcileChildren(wipFiber, elements)
}

updateFunctionComponent运行函数组件获取children。在咱们的例子中 App 会返回 h1 元素。一旦有了children, 协调就能够依照之前的形式进行了。不须要进行任何批改。

function updateFunctionComponent () {
  // 获取 Function 组件的 children
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

上面咱们须要批改 commitWork 函数。因为咱们的 Function 组件的 Fiber 节点没有 DOM 节点。咱们须要批改两件事。

首先如果要找到 DOM 节点的父节点,咱们须要顺次向上查找,找到带有 DOM 节点的 Fiber


function commitWork(fiber) {if (!fiber) {return}
  // 父级 Fiber
  let domParentFiber = fiber.parent
  // 直到找到含有 dom 的 Fiber 节点
  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)
  }
}

function commitWork(fiber) {
  // ...

  if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
    // 解决新增
    domParent.appendChild(fiber.dom)
  } else if (fiber.effectTag === "DELETION") {
    // 解决删除
    commitDeletion(fiber, domParent)
  } else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
    // 解决更新
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  }
  // ...
}

八: hooks

最初一步。目前咱们有了 Function 组件,当初让咱们增加状态。上面是一个计数器的例子

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

咱们应用 useState 获取和更新计数器的值。在调用函数组件前,咱们须要初始化一些全局变量,以便在 useState 函数中应用它们。

首先获取正在工作的 Fiber,咱们在 Fiber 节点中增加 hooks 数组,应用数组的目标是为了反对多个 useState。并且援用以后hooks 的索引。

// 以后正在工作的 Fiber
let wipFiber = null
// 以后 Fiber 的 hooks 的索引
let hookIndex = null

function updateFunctionComponent () {
  // 正在工作的 Fiber
  wipFiber = fiber
  // 以后 hooks 的索引默认为 0
  hookIndex = 0
  // hooks 的汇合
  wipFiber.hooks = []
  // 获取 Function 组件的 children
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

当组件调用 useState 时,首先咱们查看是否之前是否有 hook,如果存在旧的 hook 把之前的状态复制到新 hook。否则,应用初始值初始化 hook。

而后将 hook 增加到 Fiber,并将 hook 的索引加 1

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  // 判断之前是否有状态
  const hook = {state: oldHook ? oldHook.state : initial,}
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state]
}

useState还应该返回一个函数,更新状态。因而咱们定义 setState 用于接管 action, 用于更新状态。setState 会将 action 推入到 hook 的队列上。

而后咱们执行与 render 函数中相似的操作,咱们设置 nextUnitOfWork 开始进行新的渲染阶段。

function useState(initial) {
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex]
  // 判断之前是否有状态
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: [], // 更新队列}
  const setState = (action) => {
    // action 增加到队列中
    hook.queue.push(action)
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    // 当 nextUnitOfWork 不为空时,就会进入渲染阶段
    nextUnitOfWork = wipRoot
    deletions = []}
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}

????️: 这里简化了 setState, setState 只接管函数作为参数。

然而目前咱们还没有更新state。在下次渲染组件时,咱们从旧的队列中获取所有action。而后将它们逐个利用到新的 hook state 上。当咱们返回状态时,state 会被更新。

????️: 调用 setState,不会立即更新 state。而是在进入 render 阶段后更新 state,而后 useState 会返回新的状态。

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) => {
    // action 增加到队列中
    hook.queue.push(action)
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot,
    }
    // 当 nextUnitOfWork 不为空时,就会进入渲染阶段
    nextUnitOfWork = wipRoot
    deletions = []}
  wipFiber.hooks.push(hook)
  hookIndex++
  return [hook.state, setState]
}

咱们曾经建设了好了本人的 React。

结语

除了帮忙你了解 react 是工作原理外,本文的另一个目标是让你在后续可能更轻松深刻 React。所以咱们屡次应用了和 react 源码中一样的函数名以及变量名。

咱们省略了很多了 React 的优化

  • render 阶段遍历整棵树,然而 React 中会跳过没有任何更改的子树。
  • commit阶段,React 会进行线性遍历
  • 目前咱们会每次都创立一个新的 Fiber,而 React 中会复用之前的 Fiber 节点

还有很多 …

咱们还能够持续增加性能,比方:

  1. 增加 key
  2. 增加 useEffect
  3. 应用对象作为款式的 props
  4. children 扁平化

参考

  • Build your own React(基于 hooks 实现)
  • Didact: a DIY guide to build your own React(基于 class 实现)
  • didact
退出移动版