乐趣区

关于javascript:手写系列实现一个铂金段位的-React

一、前言

本文基于 https://pomb.us/build-your-own-react/ 实现简略版 React。

本文学习思路来自 卡颂 - b 站 -React 源码,你在第几层。

模仿的版本为 React 16.8。

将实现以下性能:

  1. createElement(虚构 DOM)
  2. render
  3. 可中断渲染
  4. Fibers
  5. Render and Commit Phases
  6. 协调(Diff 算法)
  7. 函数组件
  8. hooks

上面上正餐,请持续浏览。

二、筹备

1. React Demo

先来看看一个简略的 React Demo,代码如下:

const element = <div title="foo">hello</div>
const container = document.getElementById('container')
ReactDOM.render(element, container);

本例残缺源码见:reactDemo

在浏览器中关上 reactDemo.html,展现如下:

咱们须要实现本人的 React,那么就须要晓得下面的代码到底做了什么。

1.1 element

const element = <div>123</div> 实际上是 JSX 语法。

React 官网 对 JSX 的解释如下:

JSX 是一个 JavaScript 语法扩大。它相似于模板语言,但它具备 JavaScript 的全副能力。JSX 最终会被 babel 编译为 React.createElement() 函数调用。

通过 babel 在线编译 const element = <div>123</div>

可知 const element = <div>123</div> 通过编译后的理论代码如下:

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

再来看看上文的 React.createElement 理论生成了一个怎么样的对象。

在 demo 中打印试试:

const element = <div title="foo">hello</div>
console.log(element)
const container = document.getElementById('container')
ReactDOM.render(element, container);

能够看到输入的 element 如下:

简化一下 element:

const element = {
    type: 'div',
    props: {
        title: 'foo',
        children: 'hello'
    }
}

简略总结一下,React.createElement 实际上是生成了一个 element 对象,该对象领有以下属性:

  • type: 标签名
  • props

    • title: 标签属性
    • children: 子节点

1.2 render

ReactDOM.render() 将 element 增加到 id 为 container 的 DOM 节点中,上面咱们将简略手写一个办法代替 ReactDOM.render()

  1. 创立标签名为 element.type 的节点;
const node = document.createElement(element.type)
  1. 设置 node 节点的 title 为 element.props.title;

    node["title"] = element.props.title
  2. 创立一个空的文本节点 text;

    const text = document.createTextNode("")
  3. 设置文本节点的 nodeValue 为 element.props.children;

    text["nodeValue"] = element.props.children
  4. 将文本节点 text 增加进 node 节点;

    node.appendChild(text)
  5. 将 node 节点增加进 container 节点

    container.appendChild(node)

本例残缺源码见:reactDemo2

运行源码,后果如下,和引入 React 的后果统一:

三、开始

上文通过模仿 React,简略代替了 React.createElement、ReactDOM.render 办法,接下来将真正开始实现 React 的各个性能。

1. createElement(虚构 DOM)

下面有理解到 createElement 的作用是创立一个 element 对象,构造如下:

// 虚构 DOM 构造
const element = {
    type: 'div', // 标签名
    props: { // 节点属性,蕴含 children
        title: 'foo', // title 属性
        children: 'hello' // 子节点,注:实际上这里应该是数组构造,帮忙咱们存储更多子节点
    }
}

依据 element 的构造,设计了 createElement 函数,代码如下:

/**
 * 创立虚构 DOM 构造
 * @param {type} 标签名
 * @param {props} 属性对象
 * @param {children} 子节点
 * @return {element} 虚构 DOM
 */
function createElement (type, props, ...children) {
    return {
        type,
        props: {
            ...props,
            children: children.map(child => 
                typeof child === 'object'
                ? child
                : createTextElement(child)
            )
        }
    }
}

这里有思考到,当 children 是非对象时,应该创立一个 textElement 元素,代码如下:

/**
 * 创立文本节点
 * @param {text} 文本值
 * @return {element} 虚构 DOM
 */
function createTextElement (text) {
    return {
        type: "TEXT_ELEMENT",
        props: {
            nodeValue: text,
            children: []}
    }
}

接下来试一下,代码如下:

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

本例残缺源码见:reactDemo3

失去的 element 对象如下:

const element = {
    "type": "div", 
    "props": {
        "id": "foo", 
        "children": [
            {
                "type": "a", 
                "props": {
                    "children": [
                        {
                            "type": "TEXT_ELEMENT", 
                            "props": {
                                "nodeValue": "bar", 
                                "children": []}
                        }
                    ]
                }
            }, 
            {
                "type": "b", 
                "props": {"children": []
                }
            }
        ]
    }
}

JSX

实际上咱们在应用 react 开发的过程中,并不会这样创立组件:

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

而是通过 JSX 语法,代码如下:

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

在 myReact 中,能够通过增加正文的模式,通知 babel 转译咱们指定的函数,来应用 JSX 语法,代码如下:

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

本例残缺源码见:reactDemo4

2. render

render 函数帮忙咱们将 element 增加至实在节点中。

将分为以下步骤实现:

  1. 创立 element.type 类型的 dom 节点,并增加至容器中;
/**
 * 将虚构 DOM 增加至实在 DOM
 * @param {element} 虚构 DOM
 * @param {container} 实在 DOM
 */
function render (element, container) {const dom = document.createElement(element.type)
    container.appendChild(dom)
}
  1. 将 element.children 都增加至 dom 节点中;
element.props.children.forEach(child => 
    render(child, dom)
)
  1. 对文本节点进行非凡解决;
const dom = element.type === 'TEXT_ELEMENT'
    ? document.createTextNode("")
    : document.createElement(element.type)
  1. 将 element 的 props 属性增加至 dom;
const isProperty = key => key !== "children"
Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {dom[name] = element.props[name]
})

以上咱们实现了将 JSX 渲染到实在 DOM 的性能,接下来试一下,代码如下:

const myReact = {
    createElement,
    render
}
/** @jsx myReact.createElement */
const element = (
    <div id='foo'>
        <a>bar</a>
        <b></b>
    </div>
)

myReact.render(element, document.getElementById('container'))

本例残缺源码见:reactDemo5

后果如图,胜利输入:

3. 可中断渲染(requestIdleCallback)

再来看看下面写的 render 办法中对于子节点的解决,代码如下:

/**
 * 将虚构 DOM 增加至实在 DOM
 * @param {element} 虚构 DOM
 * @param {container} 实在 DOM
 */
function render (element, container) {
    // 省略
    // 遍历所有子节点,并进行渲染
    element.props.children.forEach(child =>
        render(child, dom)
    )
    // 省略
}

这个递归调用是有问题的,一旦开始渲染,就会将所有节点及其子节点全副渲染实现这个过程才会完结。

当 dom tree 很大的状况下,在渲染过程中,页面上是卡住的状态,无奈进行用户输出等交互操作。

可分为以下步骤解决上述问题:

  1. 容许中断渲染工作,如果有优先级更高的工作插入,则临时中断浏览器渲染,待实现该工作后,复原浏览器渲染;
  2. 将渲染工作进行合成,分解成一个个小单元;

应用 requestIdleCallback 来解决容许中断渲染工作的问题。

window.requestIdleCallback 将在浏览器的闲暇时段内调用的函数排队。这使开发者可能在主事件循环上执行后盾和低优先级工作,而不会影响提早要害事件,如动画和输出响应。

window.requestIdleCallback 具体介绍可查看文档:文档

代码如下:

// 下一个工作单元
let nextUnitOfWork = null
/**
 * workLoop 工作循环函数
 * @param {deadline} 截止工夫
 */
function workLoop(deadline) {
  // 是否应该进行工作循环函数
  let shouldYield = false
  
  // 如果存在下一个工作单元,且没有优先级更高的其余工作时,循环执行
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    
    // 如果截止工夫快到了,进行工作循环函数
    shouldYield = deadline.timeRemaining() < 1}
  
  // 告诉浏览器,闲暇工夫应该执行 workLoop
  requestIdleCallback(workLoop)
}
// 告诉浏览器,闲暇工夫应该执行 workLoop
requestIdleCallback(workLoop)

// 执行单元事件,并返回下一个单元事件
function performUnitOfWork(nextUnitOfWork) {// TODO}

performUnitOfWork 是用来执行单元事件,并返回下一个单元事件的,具体实现将在下文介绍。

4. Fiber

上文介绍了通过 requestIdleCallback 让浏览器在闲暇工夫渲染工作单元,防止渲染过久导致页面卡顿的问题。

注:实际上 requestIdleCallback 性能并不稳固,不倡议用于生产环境,本例仅用于模仿 React 的思路,React 自身并不是通过 requestIdleCallback 来实现让浏览器在闲暇工夫渲染工作单元的。

另一方面,为了让渲染工作能够拆散成一个个小单元,React 设计了 fiber。

每一个 element 都是一个 fiber 构造,每一个 fiber 都是一个渲染工作单元。

所以 fiber 既是一种数据结构,也是一个工作单元

下文将通过简略的示例对 fiber 进行介绍。

假如须要渲染这样一个 element 树:

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

生成的 fiber tree 如图:

橙色代表子节点,黄色代表父节点,蓝色代表兄弟节点。

每个 fiber 都有一个链接指向它的第一个子节点、下一个兄弟节点和它的父节点。这种数据结构能够让咱们更不便的查找下一个工作单元。

上图的箭头也表明了 fiber 的渲染过程,渲染过程详细描述如下:

  1. 从 root 开始,找到第一个子节点 div;
  2. 找到 div 的第一个子节点 h1;
  3. 找到 h1 的第一个子节点 p;
  4. 找 p 的第一个子节点, 如无子节点,则找下一个兄弟节点 ,找到 p 的兄弟节点 a;
  5. 找 a 的第一个子节点, 如无子节点,也无兄弟节点,则找它的父节点的下一个兄弟节点 ,找到 a 的 父节点的兄弟节点 h2;
  6. 找 h2 的第一个子节点,找不到,找兄弟节点,找不到,找父节点 div 的兄弟节点,也找不到,持续找 div 的父节点的兄弟节点,找到 root;
  7. 第 6 步曾经找到了 root 节点,渲染已全副实现。

上面将渲染过程用代码实现。

  1. 将 render 中创立 DOM 节点的局部抽离为 creactDOM 函数;
/**
 * createDom 创立 DOM 节点
 * @param {fiber} fiber 节点
 * @return {dom} dom 节点
 */
function createDom (fiber) {
    // 如果是文本类型,创立空的文本节点,如果不是文本类型,按 type 类型创立节点
    const dom = fiber.type === 'TEXT_ELEMENT'
        ? document.createTextNode("")
        : document.createElement(fiber.type)

    // isProperty 示意不是 children 的属性
    const isProperty = key => key !== "children"
    
    // 遍历 props,为 dom 增加属性
    Object.keys(fiber.props)
        .filter(isProperty)
        .forEach(name => {dom[name] = fiber.props[name]
        })
        
    // 返回 dom
    return dom
}
  1. 在 render 中设置第一个工作单元为 fiber 根节点;

fiber 根节点仅蕴含 children 属性,值为参数 fiber。

// 下一个工作单元
let nextUnitOfWork = null
/**
 * 将 fiber 增加至实在 DOM
 * @param {element} fiber
 * @param {container} 实在 DOM
 */
function render (element, container) {
    nextUnitOfWork = {
        dom: container,
        props: {children: [element]
        }
    }
}
  1. 通过 requestIdleCallback 在浏览器闲暇时,渲染 fiber;
/**
 * workLoop 工作循环函数
 * @param {deadline} 截止工夫
 */
function workLoop(deadline) {
  // 是否应该进行工作循环函数
  let shouldYield = false
  
  // 如果存在下一个工作单元,且没有优先级更高的其余工作时,循环执行
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    
    // 如果截止工夫快到了,进行工作循环函数
    shouldYield = deadline.timeRemaining() < 1}
  
  // 告诉浏览器,闲暇工夫应该执行 workLoop
  requestIdleCallback(workLoop)
}
// 告诉浏览器,闲暇工夫应该执行 workLoop
requestIdleCallback(workLoop)
  1. 渲染 fiber 的函数 performUnitOfWork;
/**
 * performUnitOfWork 解决工作单元
 * @param {fiber} fiber
 * @return {nextUnitOfWork} 下一个工作单元
 */
function performUnitOfWork(fiber) {
  // TODO 增加 dom 节点
  // TODO 新建 filber
  // TODO 返回下一个工作单元(fiber)}

4.1 增加 dom 节点

function performUnitOfWork(fiber) {
    // 如果 fiber 没有 dom 节点,为它创立一个 dom 节点
    if (!fiber.dom) {fiber.dom = createDom(fiber)
    }

    // 如果 fiber 有父节点,将 fiber.dom 增加至父节点
    if (fiber.parent) {fiber.parent.dom.appendChild(fiber.dom)
    }
}

4.2 新建 filber

function performUnitOfWork(fiber) {
    // ~~省略~~
    // 子节点
    const elements = fiber.props.children
    // 索引
    let index = 0
    // 上一个兄弟节点
    let prevSibling = null
    // 遍历子节点
    while (index < elements.length) {const element = elements[index]

        // 创立 fiber
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }

        // 将第一个子节点设置为 fiber 的子节点
        if (index === 0) {fiber.child = newFiber} else if (element) {
        // 第一个之外的子节点设置为该节点的兄弟节点
            prevSibling.sibling = newFiber
        }

        prevSibling = newFiber
        index++
    }
}

4.3 返回下一个工作单元(fiber)


function performUnitOfWork(fiber) {
    // ~~省略~~
    // 如果有子节点,返回子节点
    if (fiber.child) {return fiber.child}
    let nextFiber = fiber
    while (nextFiber) {
        // 如果有兄弟节点,返回兄弟节点
        if (nextFiber.sibling) {return nextFiber.sibling}

        // 否则持续走 while 循环,直到找到 root。nextFiber = nextFiber.parent
    }
}

以上咱们实现了将 fiber 渲染到页面的性能,且渲染过程是可中断的。

当初试一下,代码如下:

const element = (
    <div>
        <h1>
        <p />
        <a />
        </h1>
        <h2 />
    </div>
)

myReact.render(element, document.getElementById('container'))

本例残缺源码见:reactDemo7

如预期输入 dom,如图:

5. 渲染提交阶段

因为渲染过程被咱们做了可中断的,那么中断的时候,咱们必定不心愿浏览器给用户展现的是渲染了一半的 UI。

对渲染提交阶段优化的解决如下:

  1. 把 performUnitOfWork 中对于把子节点增加至父节点的逻辑删除;
function performUnitOfWork(fiber) {
    // 把这段删了
    if (fiber.parent) {fiber.parent.dom.appendChild(fiber.dom)
    }
}
  1. 新增一个根节点变量,存储 fiber 根节点;
// 根节点
let wipRoot = null
function render (element, container) {
    wipRoot = {
        dom: container,
        props: {children: [element]
        }
    }
    // 下一个工作单元是根节点
    nextUnitOfWork = wipRoot
}
  1. 当所有 fiber 都工作实现时,nextUnitOfWork 为 undefined,这时再渲染实在 DOM;
function workLoop (deadline) {
    // 省略
    if (!nextUnitOfWork && wipRoot) {commitRoot()
    }
    // 省略
}
  1. 新增 commitRoot 函数,执行渲染实在 DOM 操作,递归将 fiber tree 渲染为实在 DOM;
// 全副工作单元实现后,将 fiber tree 渲染为实在 DOM;function commitRoot () {commitWork(wipRoot.child)
    // 须要设置为 null,否则 workLoop 在浏览器闲暇时一直的执行。wipRoot = null
}
/**
 * performUnitOfWork 解决工作单元
 * @param {fiber} fiber
 */
function commitWork (fiber) {if (!fiber) return
    const domParent = fiber.parent.dom
    domParent.appendChild(fiber.dom)
    // 渲染子节点
    commitWork(fiber.child)
    // 渲染兄弟节点
    commitWork(fiber.sibling)
}

本例残缺源码见:reactDemo8

源码运行后果如图:

6. 协调(diff 算法)

当 element 有更新时,须要将更新前的 fiber tree 和更新后的 fiber tree 进行比拟,失去比拟后果后,仅对有变动的 fiber 对应的 dom 节点进行更新。

通过协调,缩小对实在 DOM 的操作次数。

1. currentRoot

新增 currentRoot 变量,保留根节点更新前的 fiber tree,为 fiber 新增 alternate 属性,保留 fiber 更新前的 fiber tree;

let currentRoot = null
function render (element, container) {
    wipRoot = {
        // 省略
        alternate: currentRoot
    }
}
function commitRoot () {commitWork(wipRoot.child)
    currentRoot = wipRoot
    wipRoot = null
}

2. performUnitOfWork

将 performUnitOfWork 中对于新建 fiber 的逻辑,抽离到 reconcileChildren 函数;

/**
 * 协调子节点
 * @param {fiber} fiber
 * @param {elements} fiber 的 子节点
 */
function reconcileChildren (fiber, elements) {
    // 用于统计子节点的索引值
    let index = 0
    // 上一个兄弟节点
    let prevSibling = null

    // 遍历子节点
    while (index < elements.length) {const element = elements[index]

        // 新建 fiber
        const newFiber = {
            type: element.type,
            props: element.props,
            parent: fiber,
            dom: null,
        }

        // fiber 的第一个子节点是它的子节点
        if (index === 0) {fiber.child = newFiber} else if (element) {
        // fiber 的其余子节点,是它第一个子节点的兄弟节点
            prevSibling.sibling = newFiber
        }

        // 把新建的 newFiber 赋值给 prevSibling,这样就不便为 newFiber 增加兄弟节点了
        prevSibling = newFiber
        
        // 索引值 + 1
        index++
    }
}

3. reconcileChildren

在 reconcileChildren 中比照新旧 fiber;

3.1 当新旧 fiber 类型雷同时

保留 dom,仅更新 props,设置 effectTag 为 UPDATE;

function reconcileChildren (wipFiber, elements) {
    // ~~省略~~
    // oldFiber 能够在 wipFiber.alternate 中找到
    let oldFiber = wipFiber.alternate && wipFiber.alternate.child

    while (index < elements.length || oldFiber != null) {const element = elements[index]
        let newFiber = null

        // fiber 类型是否雷同
        const sameType =
            oldFiber &&
            element &&
            element.type == oldFiber.type

        // 如果类型雷同,仅更新 props
        if (sameType) {
            newFiber = {
                type: oldFiber.type,
                props: element.props,
                dom: oldFiber.dom,
                parent: wipFiber,
                alternate: oldFiber,
                effectTag: "UPDATE",
            }
        }
        // ~~省略~~
    }
    // ~~省略~~
}

3.2 当新旧 fiber 类型不同,且有新元素时

创立一个新的 dom 节点,设置 effectTag 为 PLACEMENT;

function reconcileChildren (wipFiber, elements) {
    // ~~省略~~
    if (element && !sameType) {
        newFiber = {
            type: element.type,
            props: element.props,
            dom: null,
            parent: wipFiber,
            alternate: null,
            effectTag: "PLACEMENT",
        }
    }
    // ~~省略~~
}

3.3 当新旧 fiber 类型不同,且有旧 fiber 时

删除旧 fiber,设置 effectTag 为 DELETION;

function reconcileChildren (wipFiber, elements) {
    // ~~省略~~
    if (oldFiber && !sameType) {
        oldFiber.effectTag = "DELETION"
        deletions.push(oldFiber)
    }
    // ~~省略~~
}

4. deletions

新建 deletions 数组存储需删除的 fiber 节点,渲染 DOM 时,遍历 deletions 删除旧 fiber;

let deletions = null
function render (element, container) {
    // 省略
    // render 时,初始化 deletions 数组
    deletions = []}

// 渲染 DOM 时,遍历 deletions 删除旧 fiber
function commitRoot () {deletions.forEach(commitWork)
}

5. commitWork

在 commitWork 中对 fiber 的 effectTag 进行判断,并别离解决。

5.1 PLACEMENT

当 fiber 的 effectTag 为 PLACEMENT 时,示意是新增 fiber,将该节点新增至父节点中。

if (
    fiber.effectTag === "PLACEMENT" &&
    fiber.dom != null
) {domParent.appendChild(fiber.dom)
}

5.2 DELETION

当 fiber 的 effectTag 为 DELETION 时,示意是删除 fiber,将父节点的该节点删除。

else if (fiber.effectTag === "DELETION") {domParent.removeChild(fiber.dom)
}

5.3 UPDATE

当 fiber 的 effectTag 为 UPDATE 时,示意是更新 fiber,更新 props 属性。

else if (fiber.effectTag === 'UPDATE' && fiber.dom != null) {updateDom(fiber.dom, fiber.alternate.props, fiber.props)
}

updateDom 函数依据不同的更新类型,对 props 属性进行更新。

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

// 是否是新属性
const isNew = (prev, next) => key => prev[key] !== next[key]

// 是否是旧属性
const isGone = (prev, next) => key => !(key in next)

function updateDom(dom, prevProps, nextProps) {
    // 删除旧属性
    Object.keys(prevProps)
        .filter(isProperty)
        .filter(isGone(prevProps, nextProps))
        .forEach(name => {dom[name] = ""
        })

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

另外,为 updateDom 增加事件属性的更新、删除,便于追踪 fiber 事件的更新。

function updateDom(dom, prevProps, nextProps) {
    // ~~省略~~
    const isEvent = key => key.startsWith("on")
    // 删除旧的或者有变动的事件
    Object.keys(prevProps)
        .filter(isEvent)
        .filter(
          key =>
            !(key in nextProps) ||
            isNew(prevProps, nextProps)(key)
        )
        .forEach(name => {
          const eventType = name
            .toLowerCase()
            .substring(2)
          dom.removeEventListener(
            eventType,
            prevProps[name]
          )
        })

    // 注册新事件
    Object.keys(nextProps)
        .filter(isEvent)
        .filter(isNew(prevProps, nextProps))
        .forEach(name => {
        const eventType = name
            .toLowerCase()
            .substring(2)
        dom.addEventListener(
            eventType,
            nextProps[name]
        )
    })
    // ~~省略~~
}

替换 creactDOM 中设置 props 的逻辑。

function createDom (fiber) {
    const dom = fiber.type === 'TEXT_ELEMENT'
        ? document.createTextNode("")
        : document.createElement(fiber.type)
    // 看这里鸭
    updateDom(dom, {}, fiber.props)
    return dom
}

新建一个蕴含输出表单项的例子,尝试更新 element,代码如下:

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

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

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

rerender("World")

本例残缺源码见:reactDemo9

输入后果如图:

7. 函数式组件

先来看一个简略的函数式组件示例:

myReact 还不反对函数式组件,上面代码运行会报错,这里仅用于对比函数式组件的惯例应用形式。

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

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

const element = (<App name='foo' />)

myReact.render(element, container)

函数式组件和 html 标签组件相比,有以下两点不同:

  • 函数组件的 fiber 没有 dom 节点;
  • 函数组件的 children 须要运行函数后失去;

通过下列步骤实现函数组件:

  1. 批改 performUnitOfWork,依据 fiber 类型,执行 fiber 工作单元;
function performUnitOfWork(fiber) {
    // 是否是函数类型组件
    const isFunctionComponent = fiber && fiber.type && fiber.type instanceof Function
    // 如果是函数组件,执行 updateFunctionComponent 函数
    if (isFunctionComponent) {updateFunctionComponent(fiber)
    } else {
    // 如果不是函数组件,执行 updateHostComponent 函数
        updateHostComponent(fiber)
    }
    // 省略
}
  1. 定义 updateHostComponent 函数,执行非函数组件;

非函数式组件可间接将 fiber.props.children 作为参数传递。

function updateHostComponent(fiber) {if (!fiber.dom) {fiber.dom = createDom(fiber)
    }
    reconcileChildren(fiber, fiber.props.children)
}
  1. 定义 updateFunctionComponent 函数,执行函数组件;

函数组件须要运行来取得 fiber.children。

function updateFunctionComponent(fiber) {
    // fiber.type 就是函数组件自身,fiber.props 就是函数组件的参数
    const children = [fiber.type(fiber.props)]
    reconcileChildren(fiber, children)
}
  1. 批改 commitWork 函数,兼容没有 dom 节点的 fiber;

4.1 批改 domParent 的获取逻辑,通过 while 循环不断向上寻找,直到找到有 dom 节点的父 fiber;

function commitWork (fiber) {
    // 省略
    let domParentFiber = fiber.parent
    // 如果 fiber.parent 没有 dom 节点,则持续找 fiber.parent.parent.dom,直到有 dom 节点。while (!domParentFiber.dom) {domParentFiber = domParentFiber.parent}
    const domParent = domParentFiber.dom
    // 省略
}

4.2 批改删除节点的逻辑,当删除节点时,须要一直向下寻找,直到找到有 dom 节点的子 fiber;

function commitWork (fiber) {
    // 省略
    // 如果 fiber 的更新类型是删除,执行 commitDeletion
     else if (fiber.effectTag === "DELETION") {commitDeletion(fiber.dom, domParent)
    }
    // 省略
}

// 删除节点
function commitDeletion (fiber, domParent) {
    // 如果该 fiber 有 dom 节点,间接删除
    if (fiber.dom) {domParent.removeChild(fiber.dom)
    } else {
    // 如果该 fiber 没有 dom 节点,则持续找它的子节点进行删除
        commitDeletion(fiber.child, domParent)
    }
}

下面试一下下面的例子,代码如下:

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

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

const element = (<App name='foo' />)

myReact.render(element, container)

本例残缺源码见:reactDemo10

运行后果如图:

8. hooks

上面持续为 myReact 增加治理状态的性能,冀望是函数组件领有本人的状态,且能够获取、更新状态。

一个领有计数性能的函数组件如下:

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

已知须要一个 useState 办法用来获取、更新状态。

这里再重申一下, 渲染函数组件的前提是,执行该函数组件 ,因而,上述 Counter 想要更新计数,就会在每次更新都执行一次 Counter 函数。

通过以下步骤实现:

  1. 新增全局变量 wipFiber;
// 当前工作单元 fiber
let wipFiber = null
function updateFunctionComponent(fiber) {
    wipFiber = fiber
    // 当前工作单元 fiber 的 hook
    wipFiber.hook = []
    // 省略
}
  1. 新增 useState 函数;
// initial 示意初始参数,在本例中,initial=1
function useState (initial) {
    // 是否有旧钩子,旧钩子存储了上一次更新的 hook
    const oldHook =
        wipFiber.alternate &&
        wipFiber.alternate.hook

    // 初始化钩子,钩子的状态是旧钩子的状态或者初始状态
    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.hook = hook
    
    // 返回钩子的状态和设置钩子的函数
    return [hook.state, setState]
}

上面运行一下计数组件,代码如下:

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

本例残缺源码见:reactDemo11

运行后果如图:

本章节简略实现了 myReact 的 hooks 性能。

撒花完结,react 还有很多实现值得咱们去学习和钻研,心愿有下期,和大家一起手写 react 的更多功能。

总结

本文参考 pomb.us 进行学习,实现了包含虚构 DOM、Fiber、Diff 算法、函数式组件、hooks 等性能的自定义 React。

在实现过程中小编对 React 的根本术语及实现思路有了大略的把握,pomb.us 是非常适合初学者的学习材料,能够间接通过 pomb.us 进行学习,也举荐跟着本文一步步实现 React 的常见性能。

本文源码:github 源码。

倡议跟着一步步敲,进行实操练习。

心愿能对你有所帮忙,感激浏览~

别忘了点个赞激励一下我哦,笔芯❤️

参考资料

  • https://pomb.us/build-your-own-react/
  • 卡颂 - b 站 -React 源码,你在第几层
  • 手写一个简略的 React

    欢送关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。

退出移动版