写一个属于自己的react【手把手react源码】之二

314 阅读13分钟

前言

本文为【手把手react源码】系列第二篇文章。

欢迎关注相关 github项目

第一篇文章介绍了如何高效阅读源码的准备工作

本文暂时未开始正式的源码阅读,而是在源码阅读之前系统地了解 react 相关的概念,以及实现属于自己的功能齐全的 react

本文翻译自 pomb.us/build-your-…

建议先读下原文,是入门fiber的绝佳选择。

这篇文章循序渐进地介绍实现以下几个概念,遵循本篇文章基本就能搞懂为什么需要 fiber,为啥需要 commitphases、reconciliation 阶段等原理。本篇文章又不完全和原文一致,这里会加入一些思考,翻译也做了一定的取舍

我们的react会一步一步实现以下功能

  1. createElement 函数
  2. render 函数
  3. 并发模式
  4. Fibers
  5. Render 和 Commit 阶段
  6. Reconciliation (协调)
  7. 函数组件
  8. hooks

Step 0: 回顾

首先我们回顾一些基本的概念

我们可以用三行代码使用React app

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

但这是里react jsx的写法 jsx 是由类似babel一样的构建工具转化成普通js的,这样的转化通常比较简单:将标签内的代码替换为对 createElement 的调用,将标签名称、props和子元素作为参数传递。

所以

const element = <h1 title="foo">Hello</h1>

// 将会被转化成
const element = React.createElement(
  "h1",
  { title: "foo" },
  "Hello"
)

最终elememt长这样

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

在最上面的三行代码中,还有第三行的render需要转化,需要以下步骤

  1. 创建类型元素
  2. 指定props
  3. 创建子元素
  4. 挂载在container
ReactDOM.render(element, container)

变为

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)

所以我们使用普通的js代码来实现前面的三行代码,长这样

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)

Step 1:实现 createElement 函数

前面提到,jsx在babel转化后会变成 createElement(类型, props, 子元素) 的调用

我们可以去babel官网做一个小实验 将这段代码去转化

const Didact = {
  createElement,
}

/** @jsx Didact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

image

而 createElement 的作用则是 根据一个类型,props,children 返回一个json

以下面的代码为例

React.createElement接收的children有可能是原子值,比如字符串或者数字等,React.createElement('h1', {title: 'foo'}, 'Hello')。为了简化我们的代码,创建一个特殊的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 Didact = {
  createElement,
}


Didact.createElement("div", null, a, b) 

最终的返回值长这样


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

以上就是基本的 createElement 方法的实现

总结,其一, createElement函数,是把 元素配置,如果有babel就是 jsx语法转化成json, 方便下一步调用

Step2:实现 render 函数

目前,我们只关心向 DOM 添加内容,更新和删除再后续章节处理。

render函数的作用是创建dom,append到container

function render(element, container) {
  // 创建dom
  // 兼容text
  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)
  )

  // append到节点    
  container.appendChild(dom)
}

const Didact = {
  createElement,
  render,
}

以上是 render 函数的简单实现。在babel编译后,我们自定义的Didact已经可以让最基本的三行代码跑起来了

render 函数递归创建真实的dom元素,然后将各个元素append到其父元素中,最后整个dom树append到root container中,渲染完成,这个过程一旦开始,中间是无法打断的,直到整个应用渲染完成。这也是 React16 版本以前的渲染过程

注意,只有当整个dom树append到root container中时,页面才会显示

Step 3: 实现 Concurrent Mode (并发模式)

React16以后, rendering 是可以被中断的。

在第上一章中可以看到,当前版本的render函数是递归构建dom树,最后才append到root container,最终页面才渲染出来。这里有个问题,如果dom节点数量庞大,递归层级过深,这个过程其实是很耗时的,导致render函数长时间占用主线程,浏览器无法响应用户输入等事件,造成卡顿的现象。

因此我们需要将render过程拆分成小的任务单元,每执行完一个单元,都允许浏览器打断render过程并执行高优先级的任务,等浏览器得空再继续执行render过程

如果对requestIdleCallback不熟悉的,可以自行了解一下。真实React代码中并没有使用这个api,因为有兼容性问题。因此React使用scheduler package模拟这个调度过程

先简单介绍requestIdleCallback

  1. 使用方法:requestIdleCallback(callback[, options])

  2. callback 函数会接受一个IdleDeadline参数,该参数有一个 IdleDeadline.timeRemaining()方法,当返回值是浮点数值。它用来表示当前闲置周期的预估剩余毫秒数。如果idle period已经结束,则它的值是0。

let nextUnitOfWork = null

// 开始执行
requestIdleCallback(workLoop)

function workLoop(deadline) {
  let shouldYield = false
  
  // 有下一个任务并且还有空闲
  while (nextUnitOfWork && !shouldYield) {
    // performUnitOfWork 会在执行完之后返回下一个任务的引用
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    // 判断有无空闲,当还有空闲时间时,继续执行,没有的话跳出循环
    shouldYield = deadline.timeRemaining() < 1
  }
  
  // 跳出循环后,在下一个空闲继续执行任务,直到任务执行完毕
  requestIdleCallback(workLoop)
}


function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

performUnitOfWork接收当前工作单元,并返回下一个工作单元。工作单元可以理解为就是一个fiber对象节点

workLoop循环里会循环调用performUnitOfWork,直到所有工作单元都已经处理完毕,或者当前帧浏览器已经没有空闲时间,则循环终止。等下次浏览器空闲时间再接着继续执行

因此我们需要一种数据结构,能够支持任务打断并且可以接着继续执行,很显然,链表就非常适合

Step 4: Fibers

介绍

Fibers就是一种数据结构,支持将渲染过程拆分成工作单元,本质上就是一个双向链表。这种数据结构的好处就是方便找到下一个工作单元

Fiber包含三层含义:

  • 作为架构来说,之前React 15Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack ReconcilerReact 16Reconciler基于Fiber节点实现,被称为Fiber Reconciler
  • 作为静态的数据结构来说,每个Fiber节点对应一个React Element,保存了该组件的类型(函数组件/类组件/html标签)、对应的DOM节点信息等
  • 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作等

Fiber的几点冷知识:

  • 一个Fiber节点对应一个React Element节点,同时也是一个工作单元
  • 每个fiber节点都有指向第一个子元素,下一个兄弟元素,父元素的指针**

以下面代码为例:

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

对应的fiber tree如下:

image.png

总之,我们要实现一种用来实现虚拟dom的数据结构 能实现3件事情

  • add the element to the DOM
  • create the fibers for the element’s children
  • select the next unit of work

所以必须有三类节点

  • 能找到下一个单元
  • 能找到下一个兄弟节点
  • 能找到父节点

首先把render的创建dom函数单独抽出来

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

  const isProperty = key => key !== "children"
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = fiber.props[name]
    })

  return dom
}

在render函数中设置 nextUnitOfWork

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

let nextUnitOfWork = null

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

实现 performUnitOfWork 。这个函数就是构建fiber的基本实现

// performUnitOfWork函数主要逻辑:
//   将element元素添加到DOM
//   给element的子元素创建对应的fiber节点
//   返回下一个工作单元,即下一个fiber节点,查找过程:
//      1.如果有子元素,则返回子元素的fiber节点
//      2.如果没有子元素,则返回兄弟元素的fiber节点
//      3.如果既没有子元素又没有兄弟元素,则往上查找其父节点的兄弟元素的fiber节点
//      4.如果往上查找到root fiber节点,说明render过程已经结束
function performUnitOfWork(fiber) {
  // 第一步 根据fiber节点创建真实的dom节点,并保存在fiber.dom属性中
  if(!fiber.dom){
    fiber.dom = createDom(fiber)
  }
  // 第二步 将当前fiber节点的真实dom添加到父节点中,注意,这一步是会触发浏览器回流重绘的!!!
  if(fiber.parent){
    fiber.parent.dom.appendChild(fiber.dom)
  }
  // 第三步 给子元素创建对应的fiber节点
  const children = fiber.props.children
  let prevSibling
  children.forEach((child, index) => {
    const newFiber = {
      type: child.type,
      props: child.props,
      parent: fiber,
      dom: null
    }
    if(index === 0){
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
    prevSibling = newFiber
  })
  // 第四步,查找下一个工作单元
  if(fiber.child){
    return fiber.child
  }
  let nextFiber = fiber
  while(nextFiber){
    if(nextFiber.sibling){
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
 
}

这里有一点值得细品,React.createElement返回的element treeperformUnitOfWork创建的fiber tree有什么联系。如下图所示:

  • React Element Tree是由React.createElement方法创建的树形结构对象
  • Fiber Tree是根据React Element Tree创建来的树。每个Fiber节点保存着真实的DOM节点,并且保存着对React Element Tree中对应的Element节点的应用。注意,Element节点并不会保存对Fiber节点的应用

image.png

Step5: Render and Commit 阶段

上一章的performUnitOfWork有些问题,在第二步中我们直接将新创建的真实dom节点挂载到了容器上,这样会带来两个问题:

  • 每次执行performUnitOfWork都会造成浏览器回流重绘,因为真实的dom已经被添加到浏览器上了,性能极差
  • 浏览器是可以打断渲染过程的,因此可能会造成用户看到不完整的UI界面

我们需要改造一下我们的代码,在performUnitOfWork阶段不把真实的dom节点挂载到容器上。保存fiber tree根节点的引用。等到fiber tree构建完成,再一次性提交真实的dom节点并且添加到容器上。

所以我们需要创建一个变量去追踪fiber root 叫 wipRoot

同时,我们需要在所有工作完成之后,有一个commit 操作。我们命名为 commitWork

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 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
}
let nextUnitOfWork = null
let wipRoot = null
function render(element, container){
  wipRoot = {
    dom: container,
    props: {
      children: [element], // 此时的element还只是React.createElement函数创建的virtual dom树
    },
  }
  nextUnitOfWork = wipRoot
}
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 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) {
  // 第一步 根据fiber节点创建真实的dom节点,并保存在fiber.dom属性中
  if(!fiber.dom){
    fiber.dom = createDom(fiber)
  }
  // 第二步 将当前fiber节点的真实dom添加到父节点中,注意,这一步是会触发浏览器回流重绘的!!!
  // if(fiber.parent){
  //   fiber.parent.dom.appendChild(fiber.dom)
  // }
  // 第三步 给子元素创建对应的fiber节点
  const children = fiber.props.children
  let prevSibling
  children.forEach((child, index) => {
    const newFiber = {
      type: child.type,
      props: child.props,
      parent: fiber,
      dom: null
    }
    if(index === 0){
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }
    prevSibling = newFiber
  })
  // 第四步,查找下一个工作单元
  if(fiber.child){
    return fiber.child
  }
  let nextFiber = fiber
  while(nextFiber){
    if(nextFiber.sibling){
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
 
}
const Didact = {
  createElement:  (type, props, ...children) => {
    return {
      type,
      props: {
        ...props,
        children: children.map(child => {
          if(typeof child === 'object'){
            return child
          }
          return {
            type: 'TEXT_ELEMENT',
            props: {
              nodeValue: child,
              children: [],
            }
          }
        })
      }
    }
  },
  render
}

Step6:实现 Reconciliation

目前为止,我们只考虑添加dom节点到容器上这一单一场景,更新删除还没实现。

我们需要对比最新的React Element Tree和最近一次的Fiber Tree的差异

我们需要再提交之后保存 Fiber Tree的引用,我们称之为 currentRoot

我们需要给每个fiber节点添加一个alternate属性来保存旧的fiber节点

alternate保存的旧的fiber节点主要有以下几个用途:

  • 复用旧fiber节点上的真实dom节点
  • 旧fiber节点上的props和新的element节点的props对比
  • 旧fiber节点上保存有更新的队列,在创建新的fiber节点时执行这些队列以获取最新的页面
  const children = fiber.props.children
  reconcileChildren(fiber, 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++
    }
}

如上代码所示:

协调过程:

  • 本质上依然是根据新的React Element Tree创建新的Fiber Tree,不过为了节省内存开销,协调过程会判断新的fiber节点能否复用旧的fiber节点上的真实dom元素,如果能复用,就不需要再从头到尾全部重新创建一遍真实的dom元素。同时每个新fiber节点上还会保存着对旧fiber节点的引用,方便在commit阶段做新旧属性props的对比。
  • 如果old fiber.typenew element.type相同,则保留旧的dom节点,只更新props属性
  • 如果type不相同并且有new element,则创建一个新的真实dom节点
  • 如果type不同并且有old fiber节点,则删除该节点对应的真实dom节点
  • 删除节点需要有个专门的数组收集需要删除的旧的fiber节点。由于新的element tree创建出来的新的fiber tree不存在对应的dom,因此需要收集旧的fiber节点,并在commit阶段删除

注意,协调过程,还是以最新的React Element Tree为主去创建一个新的fiber tree,只不过是新的fiber节点复用旧的fiber节点的真实dom元素,毕竟频繁创建真实dom是很消耗内存的。新的fiber节点还是会保存着对旧的fiber节点的引用,方便在commit阶段进行新属性和旧属性的比较。这里会有个问题,如果新fiber节点保留旧fiber节点的引用,那么随着更新次数越来越多,旧的fiber tree是不是也会越来越多,如何销毁?

最终代码如下

function createDom(fiber) {
  const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type)
  updateDom(dom, {}, fiber.props)
  return dom
}
let nextUnitOfWork = null
let wipRoot = null // 保存着对root fiber的引用
let currentRoot = null // 保存着当前页面对应的fiber tree
let deletions = null
function render(element, container){
  wipRoot = {
    dom: container,
    props: {
      children: [element], // 此时的element还只是React.createElement函数创建的virtual dom树
    },
    alternate: currentRoot,
  }
  deletions = []
  nextUnitOfWork = wipRoot
}
function commitRoot(){
  deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}
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 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) {
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom)
  }
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}
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 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++
  }
}
function performUnitOfWork(fiber) {
  // 第一步 根据fiber节点创建真实的dom节点,并保存在fiber.dom属性中
  if(!fiber.dom){
    fiber.dom = createDom(fiber)
  }
  // 第二步 将当前fiber节点的真实dom添加到父节点中,注意,这一步是会触发浏览器回流重绘的!!!
  // if(fiber.parent){
  //   fiber.parent.dom.appendChild(fiber.dom)
  // }
  // 第三步 给子元素创建对应的fiber节点
  const children = fiber.props.children
  // let prevSibling
  // children.forEach((child, index) => {
  //   const newFiber = {
  //     type: child.type,
  //     props: child.props,
  //     parent: fiber,
  //     dom: null
  //   }
  //   if(index === 0){
  //     fiber.child = newFiber
  //   } else {
  //     prevSibling.sibling = newFiber
  //   }
  //   prevSibling = newFiber
  // })
  reconcileChildren(fiber, children)
  // 第四步,查找下一个工作单元
  if(fiber.child){
    return fiber.child
  }
  let nextFiber = fiber
  while(nextFiber){
    if(nextFiber.sibling){
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
 
}
const Dideact = {
  createElement:  (type, props, ...children) => {
    return {
      type,
      props: {
        ...props,
        children: children.map(child => {
          if(typeof child === 'object'){
            return child
          }
          return {
            type: 'TEXT_ELEMENT',
            props: {
              nodeValue: child,
              children: [],
            }
          }
        })
      }
    }
  },
  render
}
/** @jsx Dideact.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>
  )
 Dideact.render(element, container)
}
rerender("World")

Step7: 实现函数组件

我们需要支持函数组件

/** @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)

经过babel之后,我们得到

/** @jsx Didact.createElement */
function App(props) {
  return Didact.createElement("h1", null, "Hi ", props.name);
}

const element = Didact.createElement(App, {
  name: "foo"
});
const container = document.getElementById("root");
Didact.render(element, container);

与之前Dom组件的对比,我们得到两点不同的地方

  • 函数组件对应的fiber节点没有对应的真实dom元素
  • 需要执行函数才能获取对应的children节点,而不是直接从props.children获取

由于函数组件没有对应的fiber节点,因此在commit阶段在找父fiber节点对应的dom时,需要判断是否存在该dom元素

我们需要判断是否是 函数组件,并使用不同的方式更新 updateHostComponent是在前文已实现的

function performUnitOfWork() {
   // some code
  const isFunctionComponent =
    fiber.type instanceof Function
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }
  // some code
 }
 
 
​
function updateFunctionComponent(fiber) {
  // TODO
}
​
function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  reconcileChildren(fiber, fiber.props.children)
}
​

updateFunctionComponent我们需要执行函数去获取 children,当我们拿到 children之后,将其进行 reconciliation操作

function updateFunctionComponent(fiber) {
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}

在 commitWork 函数中 删除 const domParent = fiber.parent.dom 添加

let domParentFiber = fiber.parent
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent
  }
  const domParent = domParentFiber.dom

同样,在删除节点操作时,也需要做区分判断

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    commitDeletion(fiber.child, domParent)
  }
}

本章完整代码:

function createDom(fiber) {
  const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type)
  updateDom(dom, {}, fiber.props)
  return dom
}
let nextUnitOfWork = null
let wipRoot = null // 保存着对root fiber的引用
let currentRoot = null // 保存着当前页面对应的fiber tree
let deletions = null
function render(element, container){
  wipRoot = {
    dom: container,
    props: {
      children: [element], // 此时的element还只是React.createElement函数创建的virtual dom树
    },
    alternate: currentRoot,
  }
  deletions = []
  nextUnitOfWork = wipRoot
}
function commitRoot(){
  deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}
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 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") {
    // domParent.removeChild(fiber.dom)
    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 workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1
  }
  if(!nextUnitOfWork && wipRoot){
    commitRoot()
  }
  requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
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++
  }
}
function performUnitOfWork(fiber) {
  // 1.函数组件对应的fiber节点没有真实dom元素
  // 2.函数组件需要运行函数获取children
  const isFunctionComponent = fiber.type instanceof Function
  if(!isFunctionComponent && !fiber.dom){
    fiber.dom = createDom(fiber)
  }
  const children = isFunctionComponent ? [fiber.type(fiber.props)] : fiber.props.children
  // 第二步 为每一个新的react element节点创建对应的fiber节点,并判断旧的fiber节点上的真实dom元素是否可以复用。
  // 节省创建真实dom元素的开销
  reconcileChildren(fiber, children)
  // 第三步,查找下一个工作单元
  if(fiber.child){
    return fiber.child
  }
  let nextFiber = fiber
  while(nextFiber){
    if(nextFiber.sibling){
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
 
}
const DidReact = {
  createElement:  (type, props, ...children) => {
    return {
      type,
      props: {
        ...props,
        children: children.map(child => {
          if(typeof child === 'object'){
            return child
          }
          return {
            type: 'TEXT_ELEMENT',
            props: {
              nodeValue: child,
              children: [],
            }
          }
        })
      }
    }
  },
  render
}
/** @jsx Dideact.createElement */
const container = document.getElementById("root")
function App(props){
  return <h1>Hi { props.name }</h1>
}
const element = <App name="foo" />
Dideact.render(element, container)

Step 8: 实现 hooks

现在我们已经有函数组件了,现在我们为其添加状态

以老套的计数组件为例

/** @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)

我们需要添加一个useState的函数,并且加上一些全局变量, 存放hooks的数组可以让我们在同一个组件多次调用useState``

let wipFiber = null
let hookIndex = nullfunction updateFunctionComponent(fiber) {
  wipFiber = fiber
  hookIndex = 0
  wipFiber.hooks = []
  const children = [fiber.type(fiber.props)]
  reconcileChildren(fiber, children)
}
​
function useState(initial) {
  // TODO
}
​

在调用 useState 时,我们会先判断它是否有旧的hook 用对应index的hooks的alternate来检查,如果有,我们从旧的hooks拿状态,如果不是,则初始化。如下

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

此外,我们还需要返回 setState 方法,并添加一个 action 队列

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)
  })
​
 // 添加setState方法
  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]
}

本章完整代码

import React from 'react';
function createDom(fiber) {
  const dom = fiber.type === 'TEXT_ELEMENT' ? document.createTextNode("") : document.createElement(fiber.type)
  updateDom(dom, {}, fiber.props)
  return dom
}
let nextUnitOfWork = null
let wipRoot = null // 保存着对root fiber的引用
let currentRoot = null // 保存着当前页面对应的fiber tree
let deletions = null
function render(element, container){
  wipRoot = {
    dom: container,
    props: {
      children: [element], // 此时的element还只是React.createElement函数创建的virtual dom树
    },
    alternate: currentRoot,
  }
  deletions = []
  nextUnitOfWork = wipRoot
}
function commitRoot(){
  deletions.forEach(commitWork)
  commitWork(wipRoot.child)
  currentRoot = wipRoot
  wipRoot = null
}
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 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") {
    // domParent.removeChild(fiber.dom)
    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 workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    shouldYield = deadline.timeRemaining() < 1
  }
  if(!nextUnitOfWork && wipRoot){
    commitRoot()
  }
  requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop)
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++
  }
}
function performUnitOfWork(fiber) {
  // 1.函数组件对应的fiber节点没有真实dom元素
  // 2.函数组件需要运行函数获取children
  const isFunctionComponent = fiber.type instanceof Function
  if(!isFunctionComponent && !fiber.dom){
    fiber.dom = createDom(fiber)
  }
  const children = isFunctionComponent ? updateFunctionComponent(fiber) : fiber.props.children
  // 第二步 为每一个新的react element节点创建对应的fiber节点,并判断旧的fiber节点上的真实dom元素是否可以复用。
  // 节省创建真实dom元素的开销
  reconcileChildren(fiber, children)
  // 第三步,查找下一个工作单元
  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 = []
  return [fiber.type(fiber.props)]
}
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]
}
const Dideact = {
  createElement:  (type, props, ...children) => {
    return {
      type,
      props: {
        ...props,
        children: children.map(child => {
          if(typeof child === 'object'){
            return child
          }
          return {
            type: 'TEXT_ELEMENT',
            props: {
              nodeValue: child,
              children: [],
            }
          }
        })
      }
    }
  },
  render,
  useState,
}
/** @jsx Dideact.createElement */
const container = document.getElementById("root")
function Counter(){
  const [state, setState] = Dideact.useState(1)
  return (
    <h1 onClick={() => setState(c => c + 1)}>
      Count: { state }
    </h1>
  )
}
const element = <Counter />
Dideact.render(element, container)

总结

这篇文章的目标之一是让您更轻松地深入研究 React 代码库。这就是为什么我们几乎在所有地方都使用相同的变量和函数名称。 例如,如果你在一个真正的 React 应用程序中的一个函数组件中添加一个断点,调用堆栈应该会显示

  • workLoop
  • performUnitOfWork
  • updateFunctionComponent

当然,我们并没有做很多React的特性和优化,

在 Didact 中,我们在渲染阶段遍历整个树。相反,React 遵循一些提示和启发式方法来跳过没有任何变化的整个子树。

我们还在提交阶段遍历整个树。React则是仅保留了一个链表,其中仅包含了有起作用的fibers

每当我们创建一个新的工作树时,我们为每个fiber创建一个object,而react则是从之前的树中回收fibers

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

后记

感谢阅读。因水平有限,如有错漏,还望指出。