通过手写一个简单版 Fiber 学习其实现原理

308 阅读15分钟

前情提要


各位好,上篇我们介绍了 React 的实现思路,但并没有讲 React 中非常关键的 Fiber 结构,今天就来作一篇 Fiber 的学习笔记。

在 React 16 之前更新 Virtual DOM 的过程是采用循环加递归实现的,这种比对方式有一个问题,就是一旦任务开始进行就无法中断,如果应用中组件数量庞大,主线程被长期占用,直到整棵 VirtualDOM 树比对更新完成之后主线程才能被释放,主线程才能执行其他任务。这就会导致一些用户交互,动画等任务无法立即得到执行,页面就会产生卡顿, 非常的影响用户体验。

为了解决这个问题,React 16 之后开始使用 Fiber 结构。在 Fiber 结构中,为了实现任务的终止再继续,放弃递归只采用循环,因为循环可以被中断,然后将任务拆分成一个个的小任务,利用浏览器空闲时间执行任务,拒绝长时间占用主线程。

完整项目地址:github.com/zhtzhtx/Ter…

初始化项目


上篇介绍了 JSX 语法需要使用 Babel 进行转化成 Virtual DOM,同时我们还需要 Webpack 帮我们启动开发环境和进行打包,所以先来安装依赖。

npm install webpack webpack-cli webpack-node-externals @babel/core @babel/preset-env @babel/preset-react babel-loader nodemon npm-run-all -D

我们的开发服务端使用 Express 进行编写,使用还需要安装 Express

npm install express

server.js

下面我们使用 Express 创建 web 服务器,在项目文件夹下创建 server.js,这个之后做为我们开发服务端的入口文件

import express from "express"
const app = express()
app.use(express.static("dist"))
const template = `
  <html>
    <head>
      <title>React Fiber</title>
    </head>
    <body>
      <div id="root"></div>
			<script src="bundle.js"></script>
    </body>
  </html>
`
app.get("*", (req, res) => {
  res.send(template)
})
app.listen(3000, () => console.log("server is running"))

webpack.config.server.js

接着配置 webpack 服务端的配置文件,将 server.js 作为服务端入口文件,同时将 JavaScript 文件交给 Babel 进行编译

const path = require("path")
const nodeExternals = require("webpack-node-externals")

module.exports = {
  target: "node",
  mode: "development",
  entry: "./server.js",
  output: {
    filename: "server.js",
    path: path.resolve(__dirname, "build")
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader"
        }
      }
    ]
  },
  externals: [nodeExternals()]
}

.babelrc

下面创建 Babel 配置文件,用于编译 JSX 语法

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

webpack.config.client.js

我们还需要对客户端进行配置,配置入口文件和使用 Babel 编译 JSX 语法

const path = require("path")

module.exports = {
    target: "web",
    mode: "development",
    entry: "./src/index.js",
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "bundle.js"
    },
    devtool: "source-map",
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: {
                    loader: "babel-loader"
                }
            }
        ]
    }
}

package.json

最后,在 package.json 中配置启动命令,这样我们的项目就初始化完成了。

"scripts": {
    "start": "npm-run-all --parallel dev:*",
    "dev:server-compile": "webpack --config webpack.config.server.js --watch",
    "dev:server": "nodemon ./build/server.js",
    "dev:client-compile": "webpack --config webpack.config.client.js --watch"
}

创建任务队列并添加队列


首先,我们在项目的 src 文件下创建一个 index.js 文件,在其中编写一段 JSX 代码,Babel 会自动调用 React.createElement 方法将其转成 Virtual DOM,然后调用 React.render 方法将其渲染到页面上。

import React, { render } from "./react"

const root = document.getElementById("root")

const jsx = (
    <div>
        <p>Hello React</p>
        <p>hi Fiber</p>
    </div>
)

render(jsx, root)

createElement

我们需要先来创建 createElement 方法,在 src 文件夹下创建一个 react 文件夹,在其中创建 CreateElement/index.js 文件,这块逻辑和上篇文章中的 createElement 方法逻辑一样,就不详细介绍了。

export default function createElement(type, props, ...children) {
    // 判断是否为文本节点,如果是文本节点则手动创建VNode
    const childElements = [].concat(...children).reduce((result, child) => {
        if (child !== false && child !== true && child !== null) {
            if (child instanceof Object) {
                result.push(child)
            } else {
                result.push(createElement("text", { textContent: child }))
            }
        }
        return result
    }, [])

    return {
        type,
        props: Object.assign({ children: childElements }, props),
        children: childElements
    }
}

render

下面来看 render 方法,这块开始逻辑就和之前不一样了,我们在 react 文件夹下创建 reconciliation/index.js 文件,在其中编写 render 方法。

render 方法的作用就是向任务队列中添加任务,并在浏览器空闲时间时执行任务。我们通过createTaskQueue 方法创建一个任务队列,并通过它的 push 方法向其添加渲染任务。

// 创建任务队列
const taskQueue = createTaskQueue()

export const render = (element, dom) => {
    /**
     *  1. 向任务队列中添加任务
     *  2. 指定在浏览器空闲时间时执行任务
     */
    taskQueue.push({
        dom,
        props: { children: element }
    })
}

createTaskQueue

我们来看一下 createTaskQueue 方法的实现,在 src 文件夹下创建一个 Misc 文件夹用于存储之后用到的工具函数,在其中创建 CreateTaskQueue/index.js 文件。

createTaskQueue 的作用其实就是创建一个全局数组用来存储任务对象,并且提供了几个操作该数组的方法。

const createTaskQueue = () => {
    const taskQueue = []
    return {
        // 向任务队列中添加任务
        push: item => taskQueue.push(item),
        // 从任务队列中获取任务
        pop: () => taskQueue.shift(),
        // 判断任务队列中是否还有任务
        isEmpty: () => taskQueue.length === 0
    }
}

export default createTaskQueue

实现任务的调度逻辑


我们已经将成功地将任务添加到任务队列中,接下来我们来实现将任务队列中地任务进行调度逻辑。

render

我们在 render 方法中通过 RequestIdleCallback API 判断浏览器是否空闲,然后通过 performTask 方法执行任务。

RequestIdleCallback API 的详情可以去 MDN 了解这里就不赘述了,需要说明的是 React 本身实现 Fiber 结构时为了兼容性并没有使用 RequestIdleCallback API,而是自己实现了一个类似的功能,但在这里我们没必要那么复杂。

export const render = (element, dom) => {
    /**
     *  1. 向任务队列中添加任务
     *  2. 指定在浏览器空闲时间时执行任务
     */
    taskQueue.push({
        dom,
        props: { children: element }
    })
    requestIdleCallback(performTask)
}

performTask

performTask 方法负责调度任务,不负责执行任务。它从 requestIdleCallback 中得到一个形参 deadline 用于判断是否浏览器空闲时间。在 performTask 方法中通过 workLoop 来执行任务,然后判断如果任务队列中还有任务没有执行,则再次执行任务。

const performTask = deadline => {
    // 执行任务
    workLoop(deadline)
    // 判断任务是否存在
    // 判断任务队列中是否还有任务没有执行
    if (subTask || !taskQueue.isEmpty()) {
        // 再一次告诉浏览器在空闲的时间执行任务
        requestIdleCallback(performTask)
    }
}

workLoop

在 workLoop 中,首先判断当前是否有任务需要执行,如果没有则通过 getFirstTask 方法获取任务队列中第一个任务,如果任务存在并且浏览器有空闲时间就调用 executeTask 方法执行任务

// 初始化当前任务
let subTask = null

// 任务循环
const workLoop = deadline => {
    // 如果子任务不存在,就去获取子任务
    if (!subTask) {
        subTask = getFirstTask()
    }
    // 如果任务存在并且浏览器有空闲时间就调用
    // executeTask 方法执行任务 接受任务 返回新的任务
    while (subTask && deadline.timeRemaining() > 1) {
        subTask = executeTask(subTask)
    }
}

构建 Fiber 对象


下面我们开始构建 Fiber 对象的根节点,熟悉 React 的同学一定知道,JSX 语法中一定要有一个根节点,所以在 Fiber 对象中根节点也是唯一的。

getFirstTask

当 subTask 为 null 时,说明是初次加载所以我们应该通过 getFirstTask 来获取根节点的任务。在 getFirstTask 方法中,首先通过 taskQueue.pop 方法获取根节点的 virtual DOM 对象,然后将其封装成一个 fiber 对象返回。

const getFirstTask = () => {
    // 从任务队列中获取任务
    const task = taskQueue.pop()

    // 返回最外层节点的fiber对象
    return {
        props: task.props, // 节点属性
        stateNode: task.dom, // 节点 DOM 对象 | 组件实例对象
        tag: "host_root", // 节点标记
        effects: [], // 数组, 存储需要更改的 fiber 对象
        child: null, //当前 Fiber 的子级 Fiber
        alternate: null // Fiber 备份 fiber 比对时使用
    }
}

executeTask

接着我们来看构建子级节点 Fiber 对象,在 executeTask 方法中调用 reconcileChildren 方法构建了子级 fiber 对象。

如果 子级 fiber 对象中依然存在子级怎么办?所以要将子级返回给 subTask,形成递归构建子级节点。

如果 fiber 节点有兄弟节点,则将兄弟节点返回给 subTask,形成递归构建兄弟节点,否则判断父级是否需要构建兄弟节点

const executeTask = fiber => {
    // 构建子级fiber对象
    reconcileChildren(fiber, fiber.props.children)
    // 如果 fiber 有子级,则将子级返回给 subTask,形成递归构建子级节点
    if (fiber.child) {
        return fiber.child
    }

    // 获取当前构建的 fiber 节点
    let currentExecutelyFiber = fiber

    while (currentExecutelyFiber.parent) {
        // 如果 fiber 节点有兄弟节点,则将兄弟节点返回给 subTask,形成递归构建兄弟节点
        if (currentExecutelyFiber.sibling) {
            return currentExecutelyFiber.sibling
        }
        // 否则判断是否需要构建父级的兄弟节点
        currentExecutelyFiber = currentExecutelyFiber.parent
    }
}

reconcileChildren

在 reconcileChildren 方法中,我们首先将传入的 children 使用 arrified 方法统一转化为数组,然后遍历 children 数组将每一个 virtual DOM 转化成 fiber 对象。

需要注意的是,如果是第一个子节点,则需要将其赋值到父节点的 child 属性上,否则赋值到前一个 fiber 节点的 sibling 属性。

const reconcileChildren = (fiber, children) => {
    // children可能是对象也可能是数组
    // 将children转化成数组
    const arrifiedChildren = arrified(children)
    // 循环 children 使用的索引
    let index = 0
    // children 数组中元素的个数
    let numberOfElements = arrifiedChildren.length
    // 循环过程中的循环项 就是子节点的 virtualDOM 对象
    let element = null
    // 子级 fiber 对象
    let newFiber = null
    // 遍历 children 数组
    while (index < numberOfElements) {
        // 子级 virtualDOM 对象
        element = arrifiedChildren[index]
        // 更新操作
        newFiber = {
            type: element.type,
            props: element.props,
            tag: "host_component",
            effects: [],
            effectTag: "placement",
            stateNode: null,
            parent: fiber
        }
        
        // 为父级 fiber 添加子级 fiber
        if (index === 0) {
            fiber.child = newFiber
        } else {
            // 为fiber添加下一个兄弟fiber
            preFiber.sibling = newFiber
        }
        
        index++
    }
}

arrified

在 Misc 文件夹下创建 Arrified/index.js 文件

const arrified = arg => Array.isArray(arg) ? arg : [arg]

export default arrified

为 Fiber 对象添加 stateNode 和 tag 属性


在上述代码中,fiber对象的 stateNode 和 tag 属性是写死的,这显然不行下面我们就来将其转化为动态获取。

reconcileChildren

在 reconcileChildren 方法中,我们通过 createStateNode 方法来创建 fiber 对象对应的 DOM 节点,通过 getTag 方法动态获取 fiber 对象的节点标记

const reconcileChildren = (fiber, children) => {
    // 其它内容
    // 遍历 children 数组
    while (index < numberOfElements) {
        // 子级 virtualDOM 对象
        element = arrifiedChildren[index]
        // 更新操作
        newFiber = {
            type: element.type,
            props: element.props,
            tag: getTag(element),
            effects: [],
            effectTag: "placement",
            stateNode: null,
            parent: fiber
        }
        newFiber.stateNode = createStateNode(newFiber)
        // 为父级 fiber 添加子级 fiber
        // 其它内容
    }
}

createStateNode

我们先来看创建 fiber 对象对应的 DOM 节点的 createStateNode 方法,在 Misc 文件夹下创建 createStateNode/index.js 文件,在 createStateNode 方法中判断是否为普通 fiber 对象,如果是 React 组件的情况我们之后再考虑。

如果一个 fiber 对象的 tag 属性值是 host_component,我们就认为它是普通 fiber 对象,通过调用 createDOMElement 方法创建 DOM 节点。

const createStateNode = fiber => {
    if (fiber.tag === "host_component") {
        return createDOMElement(fiber)
    } 
}

export default createStateNode

createDOMElement

createDOMElement 方法中的逻辑和之前手写 React 中的逻辑是一样的,这里就不再赘述了。在 react 文件夹下创建 DOM 文件夹,将之前项目的 createDOMElement.js、updateNodeElement.js 和 index.js 文件直接复制过来。

import updateNodeElement from "./updateNodeElement"

export default function createDOMElement(virtualDOM) {
    let newElement = null
    if (virtualDOM.type === "text") {
        // 文本节点
        newElement = document.createTextNode(virtualDOM.props.textContent)
    } else {
        // 元素节点
        newElement = document.createElement(virtualDOM.type)
        updateNodeElement(newElement, virtualDOM)
    }
    
    return newElement
}

getTag

下面我们来写获取 fiber 对象的节点类型的方法 getTag,在 Misc 文件夹下创建一个 getTag/index.js 文件。

如果一个 virtual DOM 对象的 type 属性是字符串,我们就认为它是一个普通的 fiber 节点,React 组件的情况我们之后再来考虑

const getTag = vdom => {
    if (typeof vdom.type === "string") {
        return "host_component"
    }
}

export default getTag

将所有 Fiber 对象添加到根节点的 effect 数组中


为什么需要将所有 Fiber 对象添加到根节点的 effect 数组中呢?还记得一开始我们说的 Fiber 结构是用循环代替递归,将所有 Fiber 对象添加到根节点的 effect 数组后,我们只需要遍历该数组就能依次将 fiber 对象渲染到页面上。

executeTask

那么如何将所有 Fiber 对象添加到根节点的 effect 数组中呢?我们只需要在构建 fiber 对象时将其添加到自己的 effect 数组中,然后将 effect 数组同父级的 effect 数组进行合并,这样依次合并到根节点。

const executeTask = fiber => {
    // 其它内容
    while (currentExecutelyFiber.parent) {
        // 构建 fiber 对象时将其添加到自己的 effect 数组中,然后将 effect 数组同父级的 effect 数组进行合并,这样依次合并到根节点。
        currentExecutelyFiber.parent.effects = currentExecutelyFiber.parent.effects.concat(
            currentExecutelyFiber.effects.concat([currentExecutelyFiber])
        )
        // 其它内容
    }
}

初步渲染 Fiber 对象


上面我们已经完成了普通 fiber 对象的构建,下面我们来将实现的 fiber 对象渲染到页面上

executeTask

我们首先定义一个 pendingCommit 全局变量,表示等待渲染到页面的根节点 fiber 对象。在 executeTask 方法的最后,这时循环已经结束,所以当前的 fiber 对象是根节点的 fiber 对象将其赋值给 pendingCommit 变量。

const executeTask = fiber => {
    // 其它内容
    pendingCommit = currentExecutelyFiber
}

workLoop

在 workLoop 方法中,如果当前 pendingCommit 变量有值,说明当前有 fiber 对象等待渲染到页面上,调用 commitAllWork 方法将其渲染到页面上

const workLoop = deadline => {
    // 其它内容
    // 如果当前 pendingCommit 变量有值
    if (pendingCommit) {
        // 说明当前有 fiber 对象等待渲染到页面上,调用 commitAllWork 方法将其渲染到页面上
        commitAllWork(pendingCommit)
    }
}

commitAllWork

在 commitAllWork 方法中,遍历根节点 fiber 对象的 effect 数组,每个 fiber 对象都可以通过 parent 属性获取它的父 fiber 对象,通过 stateNode 属性获取它对应的 DOM 对象,这样就可以将它对应的 DOM 对象放置在父对象对应的 DOM 对象下。

const commitAllWork = fiber => {
    fiber.effects.forEach(item => {
        let fiber = item
        let parentFiber = item.parent
        if (fiber.tag === "host_component") {
            parentFiber.stateNode.appendChild(fiber.stateNode)
        }
    })
}

处理类组件


接下来我们开始处理 React 组件,首先从类组件开始

Component

在 react 文件夹下创建 Component/index.js 文件,这里的 Component 我们就不详细写了,具体的可以看上篇 react 的原理介绍。

export class Component {
    constructor(props) {
        this.props = props
    }
}

getTag

在 getTag 方法中,如果一个 Virtual DOM 的 type 属性的构造函数是 Component,我们就认为它是类组件

import { Component } from "../../Component"

const getTag = vdom => {
    if (typeof vdom.type === "string") {
        return "host_component"
    } else if (Object.getOwnPropertyOf(vdom.type) === Component) {
        return "class_component"
    }
}

export default getTag

createStateNode

在 createStateNode 方法中,我们只处理了普通 fiber 对象,现在我们要通过 createReactInstance 方法创建组件对应的 DOM

import { createDOMElement } from "../../DOM"
import { createReactInstance } from "../createReactInstance"


const createStateNode = fiber => {
    if (fiber.tag === "host_component") {
        return createDOMElement(fiber)
    } else {
        return createReactInstance(fiber)
    }
}

export default createStateNode

createReactInstance

在 Misc 文件夹下创建一个 createReactInstance/index.js,在 createReactInstance 方法中如果 fiber 对象的 tag 属性是 class_component,那么就返回它的实例

export const createReactInstance=fiber=>{
    let instance = null
    if(fiber.tag === "class_component"){
       instance = new fiber.type(fiber.props)
    }
    return instance
}

executeTask

在 executeTask 方法中,我们判断当前 fiber 对象是否为类组件,如果是则通过它的实例中 render 方法获取它对应的 DOM,将其传给 reconcileChildren 方法。

const executeTask = fiber => {
    // 构建子级fiber对象
    if (fiber.tag === "class_component") {
        if (fiber.stateNode.__fiber && fiber.stateNode.__fiber.partialState) {
            fiber.stateNode.state = {
                ...fiber.stateNode.state,
                ...fiber.stateNode.__fiber.partialState
            }
        }
        reconcileChildren(fiber, fiber.stateNode.render())
    } else {
        reconcileChildren(fiber, fiber.props.children)
    }

    // 其它内容
}

commitAllWork

在 commitAllWork 方法中,我们需要注意的是,如果当前 fiber 对象的父级是组件,那么我们不能直接将当前 fiber 对象对应的 DOM 插入到组件 fiber 对象的 stateNode 属性中,而是要插入到组件的父级 fiber 中

const commitAllWork = fiber => {
    fiber.effects.forEach(item => {
        if (item.effectTag === "placement") {
            let fiber = item
            let parentFiber = item.parent
            while (parentFiber.tag === "class_component" ) {
                parentFiber = parentFiber.parent
            }
            if (fiber.tag === "host_component") {
                parentFiber.stateNode.appendChild(fiber.stateNode)
            }
        }
    })
}

处理函数组件


处理完类组件我们来处理函数组件

getTag

在 getTag 方法中,如果一个 Virtual DOM 的 type 属性既不是字符串又不是由 Component 作为构造函数,那么我们就认为它是函数组件

import { Component } from "../../Component"

const getTag = vdom => {
    if (typeof vdom.type === "string") {
        return "host_component"
    } else if (Object.getOwnPropertyOf(vdom.type) === Component) {
        return "class_component"
    } else {
        return "function_component"
    }
}

export default getTag

createReactInstance

在 createReactInstance 方法中,如果是函数组件直接返回该函数

export const createReactInstance=fiber=>{
    let instance = null
    if(fiber.tag === "class_component"){
       instance = new fiber.type(fiber.props)
    }else{
        instance = fiber.type
    }
    return instance
}

executeTask

在 executeTask 方法中,通过调用该函数来获取该组件的 DOM 对象

const executeTask = fiber => {
    // 构建子级fiber对象
    if (fiber.tag === "class_component") {
        if (fiber.stateNode.__fiber && fiber.stateNode.__fiber.partialState) {
            fiber.stateNode.state = {
                ...fiber.stateNode.state,
                ...fiber.stateNode.__fiber.partialState
            }
        }
        reconcileChildren(fiber, fiber.stateNode.render())
    } else if (fiber.tag === "function_component") {
        reconcileChildren(fiber, fiber.stateNode(fiber.props))
    } else {
        reconcileChildren(fiber, fiber.props.children)
    }

    // 其它内容
}

commitAllWork

在 commitAllWork 方法中,加上函数组件的判断

const commitAllWork = fiber => {
    fiber.effects.forEach(item => {
        if (item.effectTag === "placement") {
            let fiber = item
            let parentFiber = item.parent
            while (parentFiber.tag === "class_component" || parentFiber.tag === "function_component" ) {
                parentFiber = parentFiber.parent
            }
            if (fiber.tag === "host_component") {
                parentFiber.stateNode.appendChild(fiber.stateNode)
            }
        }
    })
}

实现更新节点


上面我们已经实现 fiber 对象的初始化渲染,下面我们开始实现 fiber 对象的更新操作。

commitAllWork

既然是更新 fiber 节点,那么我们是不是应该先获取之前的旧 fiber 对象,然后和新 fiber 对象进行比较,从而将差异更新到页面?那么如何获取旧 fiber 对象呢?

在 commitAllWork 方法的最后,这时我们已经将子级 DOM 插入到 DOM 树中了,所以这是最新的根节点 fiber 对象,这时我们可以来备份它。

const commitAllWork = fiber => {
    // 其它内容
    // 备份旧的 Fiber 对象
    fiber.stateNode.__rootFiberContainer = fiber
}

getFirstTask

在 getFirstTask 方法中,我们可以通过上面定义的__rootFiberContainer 属性来获取之前的根节点 fiber 对象,如果是初始化渲染,那么该属性的值为 null

const getFirstTask = () => {
    // 其它内容
    return {
        props: task.props, // 节点属性
        stateNode: task.dom, // 节点 DOM 对象 | 组件实例对象
        tag: "host_root", // 节点标记
        effects: [], // 数组, 存储需要更改的 fiber 对象
        child: null, //当前 Fiber 的子级 Fiber
        alternate: task.dom.__rootFiberContainer // Fiber 备份 fiber 比对时使用
    }
}

reconcileChildren

在 reconcileChildren 方法中,先通过 alternate 属性获取旧 fiber 对象,如果旧 fiber 对象存在则说明是更新操作,生成的新 fiber 对象的 effectTag 属性为 update。

在更新操作中,如果 virtual DOM 的 type 属性值没有改变,则说明其对应的 DOM 节点可以复用,否则通过 createStateNode 方法创建新的 DOM 对象。

const reconcileChildren = (fiber, children) => {
    // 其它内容
    // 旧 fiber 对象的子节点
    let alternate = null
    if (fiber.alternate && fiber.alternate.child) {
        alternate = fiber.alternate.child
    }
    while (index < numberOfElements || alternate) {
        // 子级 virtualDOM 对象
        element = arrifiedChildren[index]
        if (element && alternate) {
            // 更新操作
            newFiber = {
                type: element.type,
                props: element.props,
                tag: getTag(element),
                effects: [],
                effectTag: "update",
                parent: fiber,
                alternate
            }
            if (element.type === alternate.type) {
                // 类型相同
                newFiber.stateNode = alternate.stateNode
            } else {
                // 类型不同
                newFiber.stateNode = createStateNode(newFiber)
            }
        } else if (element && !alternate) {
            // 其它内容
        }
        // 其它内容
        // 如果旧 fiber 对象存在且其有兄弟节点,则将 alternate 的值更新
        if (alternate && alternate.sibling) {
            alternate = alternate.sibling
        } else {
            alternate = null
        }        
    }
}

Component

下面我们来实现类组件的更新,类组件的更新使通过 Component Class 上的 setState 方法来更新类组件的 state 数据的,所以我们先在 Component Class 上新增 setState 方法。

在 setState 方法中,通过调用 scheduleUpdate 方法来更新类组件的 state 数据

// 用于更新类组件的 state
import { scheduleUpdate } from "../reconciliation"

export class Component {
    constructor(props) {
        this.props = props
    }
    setState(partialState) {
        scheduleUpdate(this, partialState)
    }
}

scheduleUpdate

在 reconciliation/index.js 中,导出一个新的方法 scheduleUpdate,它接受两个参数:类组件实例和新 state 数据。

scheduleUpdate 方法创建一个新的代表类组件更新的 fiber 对象加入到任务队列中

export const scheduleUpdate = (instance, partialState) => {
    taskQueue.push({
        from: "class_component",
        instance,
        partialState // 新的 state 数据
    })
    requestIdleCallback(performTask)
}

commitAllWork

在 commitAllWork 方法中,由于我们更新类组件的 state 数据需要获取旧类组件的 fiber 对象,所以在遍历时需要将 fiber 对象设置在它的stateNode属性下的__fiber 属性中。

const commitAllWork = fiber => {
    fiber.effects.forEach(item => {
        if (item.tag === "class_component") {
            item.stateNode.__fiber = item
        }
        // 其它内容
    })
}

getFirstTask

由于当调用 setState 方法时,应该在初始化渲染之后,所以我们应该在 getFirstTask 方法中处理类组件的更新。

我们先通过 getRoot 方法获取根节点 fiber 对象,然后通过类组件对应的 fiber 对象的 partialState 更新,最后返回一个新的根节点 fiber 对象,这样触发整个 fiber 树更新。

const getFirstTask = () => {
    // 从任务队列中获取任务
    const task = taskQueue.pop()

    if (task.from === "class_component") {
        const root = getRoot(task.instance)
        task.instance.__fiber.partialState = task.partialState
        return {
            props: root.props,
            stateNode: root.stateNode,
            tag: "host_root",
            effects: [],
            child: null,
            alternate: root
        }
    }

    // 返回最外层节点的fiber对象
    return {
        props: task.props, // 节点属性
        stateNode: task.dom, // 节点 DOM 对象 | 组件实例对象
        tag: "host_root", // 节点标记
        effects: [], // 数组, 存储需要更改的 fiber 对象
        child: null, //当前 Fiber 的子级 Fiber
        alternate: task.dom.__rootFiberContainer // Fiber 备份 fiber 比对时使用
    }
}

getRoot

getRoot 方法就是通过不断递归 fiber 对象的 parent 属性,从而获取到根节点的 fiber 对象。

const getRoot = instance => {
  let fiber = instance.__fiber
  while (fiber.parent) {
    fiber = fiber.parent
  }
  return fiber
}

export default getRoot

executeTask

我们是在 executeTask 方法中将类组件的构造函数实例化的,所以在实例化之前应该更新类组件的 state 数据。

const executeTask = fiber => {
    // 构建子级fiber对象
    if (fiber.tag === "class_component") {
        // 如果是类组件,判断是否需要更新 state 数据
        if (fiber.stateNode.__fiber && fiber.stateNode.__fiber.partialState) {
            fiber.stateNode.state = {
                ...fiber.stateNode.state,
                ...fiber.stateNode.__fiber.partialState
            }
        }
        reconcileChildren(fiber, fiber.stateNode.render())
    } 
    // 其它内容
}

实现删除节点操作


最后,我们来看一下如何实现节点的删除操作

reconcileChildren

在 reconcileChildren 方法中,如果备份 fiber 对象存在但是当前 fiber 对象不存在,那么我们就判断它是删除操作

const reconcileChildren = (fiber, children) => {
    // 其它内容
    // 通过 alternate 是否存在判断是否为删除操作
    while (index < numberOfElements || alternate) {
        // 子级 virtualDOM 对象
        element = arrifiedChildren[index]
        if (!element && alternate) {
            // 删除操作
            alternate.effectTag = "delete"
            fiber.effects.push(alternate)
        } 
    }
    // 其它内容
}

commitAllWork

在 commitAllWork 方法中,通过 fiber 对象的 effectTag 判断是否为删除操作,如果是则直接在父级 fiber 对象对应的 DOM 对象中删除该节点

const commitAllWork = fiber => {
    fiber.effects.forEach(item => {
        // 其它内容
        if (item.effectTag === "delete") {
            item.parent.stateNode.removeChild(item.stateNode)
        } 
        // 其它内容
}

好了,这样我们的 Fiber 结构就完成了!

总结


在实现完 Fiber 结构之后,相信各位对 React 框架有了更深的了解,感谢你对本文的阅读!