乐趣区

关于前端:从实现一个React到深度理解React框架核心原理

前言

这篇文章循序渐进地介绍实现以下几个概念,遵循本篇文章根本就能搞懂为啥须要 fiber,为啥须要 commit 和 phases、reconciliation 阶段等原理。本篇文章又不齐全和原文统一,这里会退出我本人的一些思考,比方通过 performUnitOfWork 解决后 fiber tree 和 element tree 的分割等。

  • createElement 函数
  • render 函数
  • Concurrent Mode
  • Fibers
  • Render and Commit Phases
  • Reconciliation
  • Function Components
  • Hooks

第一章 基本概念

以上面代码为例

// 1.jsx 语法不是非法的 js 语法
// const element = <h1 title="foo">Hello</h1>
// 2. 经 babel 等编译工具将 jsx 转换成 js,将 jsx 转换成 createElement 函数调用的形式
// const element = React.createElement(
//   "h1",
//   {title: "foo"},
//   "Hello"
// )
// 3.React.createElement 返回的最终对象大抵如下:const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}
const container = document.getElementById("root")
// ReactDOM.render(element, container)
// 4. 替换 ReactDOM.render 函数的逻辑,ReactDOM.render 大抵解决逻辑: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)

为了防止歧义,这里应用 element 示意 React elementsnode 示意实在的 DOM 元素节点。

至此,这段代码无需通过任何编译曾经可能在浏览器上跑起来了,不信你能够复制到浏览器控制台试试

这里有几点须要留神:

  • 先通过 node.appendChild(text) 将子元素增加到父元素,而后再通过 container.appendChild(node) 将父元素增加到容器 container 中触发浏览器渲染页面。这个程序不能反过来,也就是说只有整个实在 dom 树构建实现能力增加到容器中。假如这个程序反过来,比方先执行 container.appendChild(node),则触发浏览器回流。再执行node.appendChild(text) 又触发浏览器回流。性能极差
  • React.createElement返回的最终的对象就是 virtual dom 树,ReactDOM.render依据这个 virtual dom 创立实在的 dom 树

第二章 createElement 函数

以上面的代码为例

React.createElement接管的 children 有可能是原子值,比方字符串或者数字等,React.createElement('h1', {title: 'foo'}, 'Hello')。为了简化咱们的代码,创立一个非凡的TEXT_ELEMENT 类型将其转换成对象

React.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: [],}
        }
      })
    }
  }
}
// const element = (
//   <div id="foo">
//     <a>bar</a>
//     <b />
//   </div>
// )
// 将 jsx 转换成 js 语法
const element = React.createElement(
  "div",
  {id: "foo"},
  React.createElement("a", null, "bar"),
  React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

好了,当初咱们曾经实现了一个简略的 createElement 函数,咱们能够通过一段 非凡的正文 来通知 babel 在将 jsx 转换成 js 时应用咱们本人的 createElement 函数:

const MiniReact = {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: [],}
          }
        })
      }
    }
  }
}
/** @jsx MiniReact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
console.log('element======', element)
const container = document.getElementById("root")
ReactDOM.render(element, container)

第三章 render 函数

import React from 'react';

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)
}
const MiniReact = {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 MiniReact.createElement */
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)
console.log('element======', element)
const container = document.getElementById("root")
MiniReact.render(element, container)

render函数递归创立实在的 dom 元素,而后将各个元素 append 到其父元素中,最初整个 dom 树 append 到 root container 中,渲染实现,这个过程一旦开始,两头是无奈打断的,直到整个利用渲染实现。这也是 React16 版本以前的渲染过程

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

第四章 Concurrent Mode

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

因而咱们须要将 render 过程拆分成小的工作单元,每执行完一个单元,都容许浏览器打断 render 过程并执行高优先级的工作,等浏览器无暇再继续执行 render 过程

如果对 requestIdleCallback 不相熟的,能够自行理解一下。实在 React 代码中并没有应用这个 api,因为有兼容性问题。因而 React 应用 scheduler package 模仿这个调度过程

参考 前端进阶面试题具体解答

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}

performUnitOfWork接管当前工作单元,并返回下一个工作单元。工作单元能够了解为就是一个 fiber 对象节点

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

因而咱们须要一种数据结构,可能反对工作打断并且能够接着继续执行,很显然,链表就非常适合

第五章 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 如下:

import React from 'react';
// 依据 fiber 节点创立实在的 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
}

let nextUnitOfWork = null
// render 函数次要逻辑://   依据 root container 容器创立 root fiber
//   将 nextUnitOfWork 指针指向 root fiber
//   element 是 react element tree
function render(element, container){
  nextUnitOfWork = {
    dom: container,
    props: {children: [element], // 此时的 element 还只是 React.createElement 函数创立的 virtual dom 树
    },
  }
}

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

requestIdleCallback(workLoop)

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

}
const MiniReact = {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 MiniReact.createElement */
const element = (
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>
)
// const element = (
//   <div id="foo">
//     <a>bar</a>
//     <b />
//   </div>
// )

console.log('element======', element)
const container = document.getElementById("root")
MiniReact.render(element, container)

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

  • React Element Tree是由 React.createElement 办法创立的树形构造对象
  • Fiber Tree是依据 React Element Tree 创立来的树。每个 Fiber 节点保留着实在的 DOM 节点,并且保留着对 React Element Tree 中对应的 Element 节点的利用。留神,Element节点并不会保留对 Fiber 节点的利用

第六章 Render and Commit Phases

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

  • 每次执行 performUnitOfWork 都会造成浏览器回流重绘,因为实在的 dom 曾经被增加到浏览器上了,性能极差
  • 浏览器是能够打断渲染过程的,因而可能会造成用户看到不残缺的 UI 界面

咱们须要革新一下咱们的代码,在 performUnitOfWork 阶段不把实在的 dom 节点挂载到容器上。保留 fiber tree 根节点的援用。等到 fiber tree 构建实现,再一次性提交实在的 dom 节点并且增加到容器上。

import React from 'react';
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 MiniReact = {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 MiniReact.createElement */
const element = (
  <div>
    <h1>
      <p />
      <a />
    </h1>
    <h2 />
  </div>
)
// const element = (
//   <div id="foo">
//     <a>bar</a>
//     <b />
//   </div>
// )

console.log('element======', element)
const container = document.getElementById("root")
MiniReact.render(element, container)

第七章 Reconciliation

目前为止,咱们只思考增加 dom 节点到容器上这一繁多场景,更新删除还没实现。

咱们须要比照最新的 React Element Tree 和最近一次的 Fiber Tree 的差别

咱们须要给每个 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 是不是也会越来越多,如何销毁?

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}
  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 MiniReact = {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 MiniReact.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>
  )
  MiniReact.render(element, container)
}

rerender("World")

第八章 Function Components

本章以上面的代码为例:

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

jsx 通过 babel 编译后:

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

函数组件有两点不同的中央:

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

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

本章残缺代码:

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 ? [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 MiniReact = {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 MiniReact.createElement */
const container = document.getElementById("root")

function App(props){return <h1>Hi { props.name}</h1>
}

const element = <App name="foo" />
MiniReact.render(element, container)

第九章 Hooks

本章残缺代码

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 MiniReact = {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 MiniReact.createElement */
const container = document.getElementById("root")

function Counter(){const [state, setState] = MiniReact.useState(1)
  return (<h1 onClick={() => setState(c => c + 1)}>
      Count: {state}
    </h1>
  )
}

const element = <Counter />
MiniReact.render(element, container)
退出移动版