react 18.2源码解析 - 初次mount - 函数调用流程

175 阅读5分钟

react16.8 image.png

react18 image.png

为什么react18.2不需要手动引入?import React from 'react';

react的jsx会由babel进行编译,下面对<p id="title">hello</p>进行编译。

babel在线转换工具

react16.8

/*#__PURE__*/React.createElement("p", {
  id: "title"
}, "hello");

react18.2

import { jsx as _jsx } from "react/jsx-runtime";
/*#__PURE__*/_jsx("p", {
  id: "title",
  children: "hello"
});

可以看到,react16.8中,编译后的结果是React.createElement,并没有自动引入React,而react18.2中,编译后的结果是自动引入并使用了了_jsx方法,所以是不需要再手动引入React的。

react中的jsx函数

function ReactElement(type, key, ref, props) {
    //虚拟dom结构
    return {
        $$typeof: REACT_ELEMENT_TYPE,
        type,
        key,
        ref,
        props,
    }
}
export function jsxDEV(type, config, maybeKey) {
    let propName //属性名
    const props = {} //属性对象
    let key = null //每个虚拟dom可以有一个可选的key属性,用来区分一个父节点的不同子节点
    let ref = null //ref属性,可以获取真实dom节点
    if (typeof maybeKey !== 'undefined') {
        key = maybeKey
    }
    if (hasValidRef(config)) {
        ref = config.ref
    }
    //将config中的属性复制到props中
    //排除原形链上的属性、排除RESERVED_PROPS中的属性
    for (propName in config) {
        if (
            hasOwnProperty.call(config, propName) &&
            !RESERVED_PROPS.hasOwnProperty(propName)
        ) {
            props[propName] = config[propName]
        }
    }
    return ReactElement(type, key, ref, props)
}

可以看到,其实就是返回了ReactElement对象,这个对象的属性包$$typeof,type,key,ref,props,也就是vdom(虚拟dom)

react中的fiber

/**
 *
 * @param {*} tag  fiber类型,函数组件 类组件 原生组件 根元素
 * @param {*} pendingProps 新属性,等待处理或生效的属性
 * @param {*} key  唯一标识
 */
export function FiberNode(tag, pendingProps, key) {
    this.tag = tag
    this.key = key
    this.type = null //fiber类型,来自于虚拟dom节点的type span div p
    this.stateNode = null //此fiber对应的真实dom节点
    this.return = null //父fiber
    this.child = null //指向第一个子fiber
    this.sibling = null //指向弟弟

    this.pendingProps = pendingProps //等待生效的属性
    this.memoizedProps = null //已经生效的属性

    //每个fiber还会有自己的状态,每一种fiber状态存的类型是不一样的
    //类组件对应的fiber,存的是类的实例的状态,HostRoot存的就是要渲染的元素
    this.memoizedState = null //组件的状态

    this.updateQueue = null //更新队列

    //副作用的标识,表示要针对此fiber节点进行何种操作
    this.flags = NoFlags
    //子节点对应的副作用使用标识
    this.subtreeFlags = NoFlags
    //替身、轮替
    this.alternate = null
    this.index = 0
    this.deletions = null
    this.lanes = NoLanes
}

初次挂载流程

import { useEffect, useState } from 'react'
import { createRoot } from 'react-dom/client'

const Element = () => {
    const [count, setCount] = useState(0)

    useEffect(() => {
        console.log('useEffect1')
        return () => {
            console.log('destroy useEffect1')
        }
    })

    return (
        <p
            key={1}
            onClick={() => {
                setCount(count + 1)
            }}
            style={{ fontSize: count === 0 ? '14px' : '20px' }}
        >
            {count}
        </p>
    )
}
const root = createRoot(document.getElementById('root'))
root.render(<Element></Element>)

上面的代码经过babel编译后,得到下面的代码

import { useEffect, useState } from "/node_modules/.vite/deps/react.js?v=59e60701";
import { createRoot } from "/src/react-dom/client.js";
const Element = () => {
  _s();
  const [count, setCount] = useState(0);
  useEffect(() => {
    console.log("useEffect1");
    return () => {
      console.log("destroy useEffect1");
    };
  });
  return /* @__PURE__ */ jsxDEV(
    "p",
    {
      onClick: () => {
        setCount(count + 1);
      },
      style: { fontSize: count === 0 ? "14px" : "20px" },
      children: count
    },
    1,
    false,
    {
      fileName: "/Users/liuxuanlong/liuxl/code/personal/mini-react/src/main.jsx",
      lineNumber: 15,
      columnNumber: 5
    },
    this
  );
};
_s(Element, "/xL7qdScToREtqzbt5GZ1kHtYjQ=");
_c = Element;
const root = createRoot(document.getElementById("root"));
root.render(/* @__PURE__ */ jsxDEV(Element, {}, void 0, false, {
  fileName: "/Users/liuxuanlong/liuxl/code/personal/mini-react/src/main.jsx",
  lineNumber: 27,
  columnNumber: 13
}, this));

初次挂载的代码调用流程如下:

image.png

createRoot(document.getElementById('root'))

export function createRoot(container) {
    // div#root
    const root = createContainer(container)
    // 事件监听
    listenToAllSupportedEvents(container)
    return new ReactDOMRoot(root)
}

createRoot干了两件事:

  1. 创建fiberRoot 和 rootFiber,建立联系,初始化rootFiber的updateQueue。 构建出的结构如下
    image.png rootFiber的updateQueue指的是这个fiber上的更新队列。

  2. 处理事件监听的逻辑(此处先不做说明)

root.render(<Element></Element>)

render函数定义在ReactDOMRoot原型上。

//render
ReactDOMRoot.prototype.render = function (children) {
    const root = this._internalRoot
    updateContainer(children, root)
}

updateContainer

export function updateContainer(element, container) {
    //获取当前的根fiber
    const current = container.current
    //请求一个更新车道
    const lane = requestUpdateLane(current)
    //创建更新
    const update = createUpdate(lane)
    //要更新的虚拟dom
    update.payload = { element }
    //把此更新对象添加到current这个根fiber的updateQueue队列中,返回根节点
    const root = enqueueUpdate(current, update, lane)
    //在fiber上调度更新
    scheduleUpdateOnFiber(root, current, lane)
}
  1. updateContainer中构建了rootFiber的更新队列,updateQueue.shared.pending指向最后1个更新(lastUpdate),lastUpdate.next 指向第一个更新(在这里是它本身)。

image.png 2. 调用scheduleUpdateOnFiber

scheduleUpdateOnFiber

export function scheduleUpdateOnFiber(root, fiber, lane) {
    markRootUpdated(root, lane)
    ensureRootIsScheduled(root)
}

调用ensureRootIsScheduled

ensureRootIsScheduled

    ...
    if (newCallbackPriority === SyncLane) {
        // 先把performSyncWorkOnRoot添加到同步队列中
        scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root))
        // 再把flushSyncCallbacks放入微任务
        queueMicrotask(flushSyncCallbacks)
        newCallbackNode = null
    } else {
        //如果不是同步,需要调度一个新的任务
        let schedulerPriorityLevel
        ...
        newCallbackNode = scheduleCallback(
            schedulerPriorityLevel,
            performConcurrentWorkOnRoot.bind(null, root)
        )
    }

scheduleSyncCallback & scheduleCallback

根据调度算法选择 同步(scheduleSyncCallback) 的方式执行还是 异步(scheduleCallback) 的方式执行。

同步执行performSyncWorkOnRoot.bind(null, root)
异步执行performConcurrentWorkOnRoot.bind(null, root)

performConcurrentWorkOnRoot & performConcurrentWorkOnRoot

renderRootConcurrent & renderRootSync

两种法都是按照调度算法来执行renderRootConcurrent 或者 renderRootSync

两个函数内部都处理两种逻辑。 首先构建workInProgress根节点。构建完成的结构如下图

image.png

然后进行 workLoop,进行beginWork、complateWork递归的构建fiber树的过程。 在构建过程中会标记副作用

commitRoot

commitRoot中,根据标记的副作用,生成真实的dom,挂载到div #root中,最终完成dom的挂载。

总结

真实dom生成流程

jsx -> vdom -> fiber节点 -> 真实dom

整体的函数调用流程如下

image.png 整个源码中有两大循环:

  1. 任务调度循环,控制所有任务的调度。(更新优先级、批处理、并发渲染等基于此来进行)
  2. fiber构造循环,控制fiber树的构建,整个过程是一个树的深度优先遍历。(diff算法、 hooks等基于此来进行)

这里引用此文章中的话来解释

结合上文的宏观概览图(展示核心包之间的调用关系), 可以将 react 运行的主干逻辑进行概括:

  1. 输入: 将每一次更新(如: 新增, 删除, 修改节点之后)视为一次更新需求(目的是要更新DOM节点).

  2. 注册调度任务: react-reconciler收到更新需求之后, 并不会立即构造fiber树, 而是去调度中心scheduler注册一个新任务task, 即把更新需求转换成一个task.

  3. 执行调度任务(输出): 调度中心scheduler通过任务调度循环来执行task(task的执行过程又回到了react-reconciler包中).

    • fiber构造循环task的实现环节之一, 循环完成之后会构造出最新的 fiber 树.
    • commitRoottask的实现环节之二, 把最新的 fiber 树最终渲染到页面上, task完成.

主干逻辑就是输入到输出这一条链路, 为了更好的性能(如批量更新可中断渲染等功能), react在输入到输出的链路上做了很多优化策略, 比如本文讲述的任务调度循环fiber构造循环相互配合就可以实现可中断渲染.

后续的源码解析,请关注本专栏的后续文章

本文参考和引用文章列表:(如有侵权,请告知删除)
github.com/7kms/react-…