一、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.createElement()创建元素是比较频繁的操作,本身也存在一些问题,无法做到性能优化,具体可见官方优化的 动机
react/jsx-runtime和react/jsx-dev-runtime中的函数只能由编译器转换使用。如果你需要在代码中手动创建元素,你可以继续使用React.createElement
Babel 的 v7.9.0 及以上版本可支持全新的 JSX 转换。
大白话,其实就是新版 React 让 babel 加了一个引用,省去了自己引用 React。但是呢,引用整个 React又太多了,所以单独建立一个包,专门转换 jsx 的。
二、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>)
-
createRoot()-> 对父/根 节点(容器)进行初始化操作 -> 返回ReactDOMRoot类型 -> render(_jsx()) ->_jsx()-> render () -
<div>123</div>-> babel 转化 ->_jsx('div', {children:'123'}) -
_jsx就是jsxWithValidationDynamic()就是jsxWithValidation()
-
jsxWithValidation('div',{children:'123'})-> 5-6 项检查校验工作 -> 调用jsxDEV()生成element -
render(children)->updateContainer()->scheduleUpdateOnFiber->ensureRootIsScheduled()->scheduleCallback$1()->unstable_scheduleCallback()->requestHostCallback->schedulePerformWorkUntilDeadline()->postMessage
workLoop 之前: scheduler.development.js
workLoop 之后:react-dom.development.js
-
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 版本一定属于如下情况之一:
- v15 及之前的老架构
- v16 之后的新架构,未开启并发更新,与情况 1 行为一致
- v16 之后的新架构,未开启并发更新,但是启用了一些新功能(比如 Automatic Batching)
- v16 之后的新架构,开启并发更新
v16、v17 默认属于情况 2。
之所以划分多种情况,是因为情况 4 的 React 一些行为异于情况 1、2、3(比如部分以 componentWill 开头的生命周期函数的调用时机发生变化),也就是说开启并发更新可能造成老代码不兼容。
为了让广大开发者能够平滑过渡,React 团队采用了「渐进升级」方案。
一句话总结:v18 以后只会有并发特性,不会有并发模式。
详细内容看 React17 官方文档
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().render, rende只有一个函数,每次就不用访问 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…。
疑问?
1、React18 和 React17 有什么区别? React16 和 17 有什么区别?
2、react/jsx-runtime 和 React.CreateElement 有啥区别?
- 参数不同? CreateElement 可以用剩余参数
3、减少引用 CreateElement 带来的好处?
4、React 包有两个函数 jsxDEV 和 createElement。 React-dom 包还有个 createElement 函数,有啥区别?
React 包:
(看了 jsxDEV和 CreateElement 两个的逻辑基本一致,硬说区别 :
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… 来源:知乎