【React 源码阅读】初次渲染 - CreatRoot

247 阅读7分钟

一、React 17 之前为什么要引用 React,为什么 17 又不用了?

一句话概括:

1、React 17 之前本质上调用的 React.CreateElement 这个函数,而JSX 文件只不过是个语法糖,在转化的时候,babel 已经帮我们转化好了,如果不引用 React 这个变量,会出现 CreateElement is not defined

2、React 17 又可以不引用了,是因为 React 和 babel 合作,在转义 JSX 的过程中,会自动在代码中引用 React 相关函数,比起以前直接引用一整个大的 React,现在改成 esm 模块引用,只需要 import {jsx as _jsx} from 'react/jsx-runtime' 就行了,文件小了,还能 tree shaking , 美滋滋。

React 17 之前,jsx 文件 代码首行必须引用 React,类似这样:

import React from react;

即使没有用到 React 相关 API,也要用到,不然会报错。

原因是因为在 React 16 版本时候,只要你写 React 的 jsx, 在 js 里面写上类似 html 模板语法的,最后都要调用 React.createElemnt() 函数,而我们为什么没有看到这个调用,因为 babel 帮我们对 jsx 转化了。

而在 React 17 版本中,官方与 babel 进行了合作,直接通过将 react/jsx-runtime 对 jsx 语法进行了新的转换而不依赖 React.createElement,转换的结果便是可直接供 ReactDOM.render 使用的 ReactElement 对象。

印记中文-React 17.0.0-rc.2 版本发布,引入全新的 JSX 转换

一文解读 React 17 与 React 18 的更新变化

React 17 中的新 JSX 增强功能

通过React.createElement() 创建元素是比较频繁的操作,本身也存在一些问题,无法做到性能优化,具体可见官方优化的 动机

react/jsx-runtimereact/jsx-dev-runtime 中的函数只能由编译器转换使用。如果你需要在代码中手动创建元素,你可以继续使用 React.createElement

Babel 的 v7.9.0 及以上版本可支持全新的 JSX 转换。

zh-hans.reactjs.org/blog/2020/0…

大白话,其实就是新版 React 让 babel 加了一个引用,省去了自己引用 React。但是呢,引用整个 React又太多了,所以单独建立一个包,专门转换 jsx 的。

有了 React.createElement 为什么还需要 JSX runtime,作用是什么? - 掘金

二、React 包的 jsx 函数 对 jsx 代码做了什么?(原理是什么)

首先陈述一个事实(验证过,~~但不明白为什么,还没读完全部源码 ~~):

如果只是跑<div>123</div> 这种级别的代码,React18 应该走 jsxDEV 这个函数,事实上,不但走这个函数,还会走 React-DOM 里面的 CreateElement 函数,所以这个函数在 React 中还挺重要的。

(因为先用 jsxDEV 生成 element 对象 ,然后再创建实例,后者的 CreateElement 是专门操作 DOM 的 )

要创建实例 DOM 就需要 CreateInstance这个函数,而这个函数就依赖 CreateElement 函数。反倒是 jsxDEV,是个单独额外的文件里面的函数。

2.1、函数调用链

ReactDOM.createRoot(...).render(<div>123</div>)

  1. createRoot() -> 对父/根 节点(容器)进行初始化操作 -> 返回ReactDOMRoot类型 -> render(_jsx()) -> _jsx() -> render ()

  2. <div>123</div> -> babel 转化 -> _jsx('div', {children:'123'})

  3. _jsx 就是 jsxWithValidationDynamic() 就是 jsxWithValidation()

  1. jsxWithValidation('div',{children:'123'}) -> 5-6 项检查校验工作 -> 调用jsxDEV()生成element

  2. render(children) -> updateContainer() -> scheduleUpdateOnFiber -> ensureRootIsScheduled() -> scheduleCallback$1() -> unstable_scheduleCallback() -> requestHostCallback -> schedulePerformWorkUntilDeadline() -> postMessage

workLoop 之前: scheduler.development.js

workLoop 之后:react-dom.development.js

  1. postMessage -> performWorkUntilDeadline() -> flushWork() -> workLoop() -> performConcurrentWorkOnRoot() (这个函数回调执行完,已经生成 DOM 了)-> renderRootSync() -> workLoopSync() -> performUnitOfWork() -> completeUnitOfWork() -> completeWork() -> createInstance() -> CreateElement()

scheduler.development.js 是怎么到 react-dom.development.js的,还不清楚。

2.2 说说 jsxDEV / createElement 内部函数的处理逻辑

其实 jsxDEV 上面还有一个函数:

jsxWithValidation(type, props, key, isStaticChildren, source, self)

1、首先,要知道每个函数的参数和返回值

jsxDEV(type,config,maybekey,source,self) -> ReactElement
createElement(type,config,children) -> ReactElement

2、分析 ReactElement 这个类型:

  var element = {
    // 这个标签使我们能够唯一地将其识别为一个React元素
    $$typeof: REACT_ELEMENT_TYPE,
    // 属于元素的内置属性
    type: type,
    key: key,
    ref: ref,
    props: props,
    // 记录负责创建这个元素的组件。
    _owner: owner
  };
  return element

3、校验 type

var validType = isValidElementType(type); // We warn in this case but don't throw. We expect the element creation to
    // succeed and there will likely be errors in render.

if (!validType) {
  var info = '';

  if (type === undefined || typeof type === 'object' && type !== null && Object.keys(type).length === 0) {
    info += ' You likely forgot to export your component from the file ' + "it's defined in, or you might have mixed up default and named imports.";
  }

  var sourceInfo = getSourceInfoErrorAddendum(source);

  if (sourceInfo) {
    info += sourceInfo;
  } else {
    info += getDeclarationErrorAddendum();
  }

  var typeString;

  if (type === null) {
    typeString = 'null';
  } else if (isArray(type)) {
    typeString = 'array';
  } else if (type !== undefined && type.$$typeof === REACT_ELEMENT_TYPE) {
    typeString = "<" + (getComponentNameFromType(type.type) || 'Unknown') + " />";
    info = ' Did you accidentally export a JSX literal instead of a component?';
  } else {
    typeString = typeof type;
  }

  error('React.jsx: type is invalid -- expected a string (for ' + 'built-in components) or a class/function (for composite ' + 'components) but got: %s.%s', typeString, info);
}

4、key 值处理

// 有key 处理key
if (maybeKey !== undefined) {
  {
    checkKeyStringCoercion(maybeKey);
  }

  key = '' + maybeKey;
}

if (hasValidKey(config)) {
  {
    checkKeyStringCoercion(config.key);
  }

  key = '' + config.key;
}

5、处理 ref

    if (hasValidRef(config)) {
      ref = config.ref;
      warnIfStringRefCannotBeAutoConverted(config, self);
    } // Remaining properties are added to a new props object

6、处理 config (依次赋值,剔除 key、ref、self、source)

    for (propName in config) {
      if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    } // Resolve default props

7、处理 defaultProps

if (type && type.defaultProps) {
      var defaultProps = type.defaultProps;

      for (propName in defaultProps) {
        if (props[propName] === undefined) {
          props[propName] = defaultProps[propName];
        }
      }
    }

8、收尾定义 key 和 props

9、对 props.children 处理

10、对 FRAGMENT 处理

11、 返回 element

三、React 三种模式 (React 17 才有,React 18 已经没有了)

  • Concurrent mode 全并发模式: ReactDOM.createRoot(rootNode).render(<App />)
  • Blocking mode 渐进模式: ReactDOM.createBlockingRoot(rootNode).render(<App />
  • Legacy mode 传统(同步)模式: ReactDOM.render(<App />, rootNode)

Legacy 模式在渲染时候触发的同步渲染策略,Block 模式只是 Legacy 模式到 Concurrent 模式的渐进。

具体来说 Legacy 和 Concurrent 两个模式的区别:渲染的区别,工作流程基本是是:初始化,render,commit 三个步骤,模式的不同,会决定是否一气呵成还是分步执行。

当前所有 React 版本一定属于如下情况之一:

  1. v15 及之前的老架构
  2. v16 之后的新架构,未开启并发更新,与情况 1 行为一致
  3. v16 之后的新架构,未开启并发更新,但是启用了一些新功能(比如 Automatic Batching)
  4. v16 之后的新架构,开启并发更新

v16、v17 默认属于情况 2。

之所以划分多种情况,是因为情况 4 的 React 一些行为异于情况 1、2、3(比如部分以 componentWill 开头的生命周期函数的调用时机发生变化),也就是说开启并发更新可能造成老代码不兼容。

为了让广大开发者能够平滑过渡,React 团队采用了「渐进升级」方案。

一句话总结:v18 以后只会有并发特性,不会有并发模式。

详细内容看 React17 官方文档

by -> Adopting Concurrent Mode (Experimental) – React

React 18 发布时支持并发。但是,不再有“模式”, 新行为是完全选择加入的,并且仅在您使用新功能时启用。

关于 React 18 并发 ****API 的详细介绍,请参考:

  • React.Suspense参考
  • React.startTransition参考
  • React.useTransition参考
  • React.useDeferredValue参考

React 18 新功能:

  • useDeferredValue
  • useId
  • 全新的 Suspense
  • Transitions( useTransition、startTransition)

四、React 18 中,为什么要从 ReactDOM.render 转换成 createRoot ?

1、新函数支持 concurrent 并发特性。

2、人类工程学问题。以前是 render 有两个参数,第二个参数是容器 DOM,每次更新,都要访问容器 DOM,现在换成 createRoot().renderrende只有一个函数,每次就不用访问 DOM 了。

老版本:

import * as ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

// Initial render.
ReactDOM.render(<App tab="home" />, container);

// 在更新过程中,React会访问 DOM元素的根 
ReactDOM.render(<App tab="profile" />, container);

旧版本:

import * as ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

// Create a root.
const root = ReactDOM.createRoot(container);

// Initial render: Render an element to the root.
root.render(<App tab="home" />);

// 在更新期间,不需要再次传递容器。
root.render(<App tab="profile" />);

3、和注水 (hydrate) 有关系,以前用 ReactDOM.render() 后面还可以跟着回调函数,但是这个在注水中是没有意义之的。

我们更改这个 API 有以下几个原因。

首先,这修复了 API 在运行更新时的一些人类工程学问题。如上所示,在 Legacy API 中,你需要多次将容器元素传递给 render,即使它从未更改过。这也意味着我们不需要将根元素存储在 DOM 节点上,尽管我们今天仍然这样做。

其次,这一变化允许让我们可以移除 hydrate 方法并替换为 root 上的一个选项;删除渲染回调,这些回调在部分 hydration 中是没有意义的。

译者注:「这一变化允许让我们可以移除 hydrate 方法并替换为 root 上的一个选项」这句话的意思是可以这么用 createRoot: createRoot(container, { hydrate: true })。render() 但是值得注意的是,最新的版本中 createRoot 要废弃 hydrate: true 这一用法,并引入新的 hydrateRoot 支持,具体见 github.com/facebook/re…

链接:juejin.cn/post/699243…

疑问?

1、React18 和 React17 有什么区别? React16 和 17 有什么区别?

www.51cto.com/article/690…

2、react/jsx-runtime 和 React.CreateElement 有啥区别?

  • 参数不同? CreateElement 可以用剩余参数

3、减少引用 CreateElement 带来的好处?

4、React 包有两个函数 jsxDEV 和 createElement。 React-dom 包还有个 createElement 函数,有啥区别?

React 包:

(看了 jsxDEVCreateElement 两个的逻辑基本一致,硬说区别 :

1、CreateElement 有剩余参数,平铺摆放,在代码里会对剩余参数做处理。jsxDEV 没有剩余参数,它把 后面的参数 都放到 config 里面的 children 了

2、jsxDEV 一些代码逻辑被拆分了好几个函数。

3、对 key 值的处理有区别(by 卡颂)

5、React 源码文件互相是怎么通信的

postMessage?

6、注水 hydrate 是什么意思?

这个词在 react 中是 ssr 相关的,因为 ssr 时服务器输出的是字符串,而浏览器端需要根据这些字符串完成 react 的初始化工作,比如创建组件实例,这样才能响应用户操作。这个过程就叫 hydrate,有时候也会说 re-hydrate

可以把 hydrate 理解成给干瘪的字符串”注水”

链接:www.zhihu.com/question/66… 来源:知乎