之所以会讨论这个话题,是因为最近在看react源码的时候,对组件更新时beginWork
工作中的props
校验还有一定的疑问,然后就开始查看组件Fiber
节点上pendingProps
的生成,然后一直向上追寻,到react元素对象,再到编译生成的react代码,最后到我们定义的组件,所以在探寻组件更新策略之前写下了这篇笔记。
1,定义的组件
首先使用vite
搭建一个react
项目,这里使用vite
脚手架来搭建,是因为create-react-app
搭建的react
项目在生产构建后关于webpack
的代码太多,影响我们对组件源码的观察,所以采用的vite
。
注意: 这两个脚手架编译react时都是采用相同的
babel
插件,所以更换脚手架对编译后的组件自身源码没有任何影响的。
准备一个案例:
js
// App.js
import { lazy } from 'react'
const MyFun = lazy(() => import('./views/MyFun.jsx'))
const MyClass = lazy(() => import('./views/MyClass.jsx'))
export default function App() {
console.log('App Start')
return (
<div className="App">
<div>react code</div>
<MyFun name='MyFun'></MyFun>
<MyClass name='MyClass'></MyClass>
</div>
);
}
js
// MyFun.js
import { useState, useRef } from 'react'
export default function MyFun(props) {
console.log('MyFun Start')
const [count, setCount] = useState(1)
const ref = useRef()
function handleClick() {
setCount(2)
}
return (
<div className="MyFun">
<div ref={ref}>DOM Instance</div>
<div>state: {count}</div>
<div>name: {props.name}</div>
<button onClick={handleClick}>Button</button>
</div>
)
}
js
// MyClass.js
import { Component, createRef } from 'react';
export default class MyClass extends Component {
constructor(props) {
super(props)
console.log('MyClass Start')
this.state = {
count: 1
}
this.ref = createRef();
}
componentDidMount() {
console.log('MyClass Mounted')
}
handleClick = () => {
this.setState({ count: 2})
}
render() {
return (
<div className='MyClass'>
<div ref={this.ref}>DOM Instance</div>
<div>state: {this.state.count}</div>
<div>name: {this.props.name}</div>
<button onClick={this.handleClick}>Button</button>
</div>
);
}
}
然后直接执行yarn build
命令,构建编译生成生产环境的代码。
2,编译后的代码
首先我们查看函数组件MyFun
编译后的代码:
将编译后的代码拿过来:
js
import{r as t,j as n}from"./index-e471acce.js";
function a(s){
console.log("MyFun Start");
const[e,o]=t.useState(1), c=t.useRef();
function r(){
o(2)
}
return n.jsxs("div",{
className:"MyFun",
children:[
n.jsx("div",{ref:c,children:"DOM Instance"}),
n.jsxs("div",{children:["state: ",e]}),
n.jsxs("div",{children:["name: ",s.name]}),
n.jsx("button",{onClick:r,children:"Button"})
]
})
}
export{a as default};
这里的代码已经手动格式化过 ,方便我们观察对比。对于变量和函数重命名都是代码编译很常见的操作,不是我们的重点。这里我们主要关注的对jsx
内容的处理,可以发现目前的react组件编译之后没有存在react.createElement
方法了,
js
react.createElement('div', null, '...')
因为新版本的react
采用的新的编译模式,这里的n.jsx
的n
就是jsxRunTime
导出的对象,所以目前创建react元素对象【react-element
】是直接使用的jsx
运行时中的方法,而不再使用react.createElement
方法。既然编译后不再需要react,所以我们的组件中在不需要react时就可以不再引入react了,不像以前要必须引入。
这里我们可以查看jsx-runtime
运行时的源码:
js
// packages\react\jsx-runtime.js
export {Fragment, jsx, jsxs} from './src/jsx/ReactJSX';
这里就是上面编译后的代码使用的jsx
和jsxs
方法。
继续查看它的来源:
js
// packages\react\src\jsx\ReactJSX.js
import {jsx as jsxProd} from './ReactJSXElement';
const jsx = __DEV__ ? jsxWithValidationDynamic : jsxProd;
const jsxs = __DEV__ ? jsxWithValidationStatic : jsxProd;
const jsxDEV = __DEV__ ? jsxWithValidation : undefined;
export {REACT_FRAGMENT_TYPE as Fragment, jsx, jsxs, jsxDEV};
我们可以发现jsx
和jsxs
方法在生产环境下都是引用的同一个方法jsxProd
。
即ReactJSXElement
文件中的jsx
方法,如下图所示:
3,创建React元素对象
下面我们将打包后的代码部署到服务器中,查看jsx
方法运行时如何创建的react元素对象。
这里我们首先查看第一个react元素的创建:
js
n.jsx("div",{ref:c,children:"DOM Instance"}),
在继续调试之前,我们还得先学习jsx
方法源码,查看它的执行逻辑:
js
// packages\react\src\jsx\ReactJSXElement.js
export function jsx(type, config, maybeKey) {
let propName;
// 存储此节点的props
const props = {};
let key = null;
let ref = null;
if (maybeKey !== undefined) {
key = '' + maybeKey;
}
// 组件key处理
if (hasValidKey(config)) {
key = '' + config.key;
}
// ref处理
if (hasValidRef(config)) {
ref = config.ref;
}
// props处理
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
// props默认值处理
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
# 创建react元素对象
return ReactElement(
type,
key,
ref,
undefined,
undefined,
ReactCurrentOwner.current,
props,
);
}
根据jsx
方法源码可以看出,创建react元素对象的逻辑也比较简单,主要就是针对组件key
,ref
,以及props
的处理。
key
的处理:其实给组件传递的key值,这个key会存储到创建的react元素对象上,之后会备份到根据react元素创建的Fiber
节点之上,用于react组件更新diff
时的优化条件。ref
的处理:就是给DOM绑定的ref对象,这里的hasValidRef
校验就是判断ref
不等于undefined
,才会设置ref
。props
的处理:props的处理其实就是循环config
对象,将此对象的所有属性内容拷贝到新的props
对象中。
这里在新增props
属性时,有两个判断条件:
- 使用
hasOwnProperty
方法校验必须为config
的自有属性,而非原型链上的属性。 - 非
RESERVED_PROPS
对象拥有的属性,及非保留属性。
js
// 保留属性,新增的props不能是这几个属性
const RESERVED_PROPS = {
key: true,
ref: true,
__self: true,
__source: true,
};
只有同时满足这两个条件,才会将此属性新增到props
对象。并且这里还有一个对props
默认值的处理,如果定义了defaultProps
,则会在此将默认值进行初始化的赋值。
最后调用ReactElement
方法创建一个react元素对象,此方法内部就是创建一个新对象,然后直接返回。
js
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
return element;
};
到此,调用jsxRunTime.jsx
方法创建react元素的逻辑就执行完成。
最后注意: 一个函数组件在加载时,它会递归的将本组件内所有react元素创建完成。
4,创建Fiber节点
在react元素对象创建完成之后,下一步就是根据此对象创建对应的Fiber
节点【虚拟DOM】。
在react内部会调用createFiberFromElement
方法来创建Fiber
节点,此方法从名字上就可以看出它的作用:根据react-element
对象创建Fiber
节点。当然createFiberFromElement
方法内还会有一些其他的逻辑和判断,具体的内容这里不会展开。
js
createFiberFromElement => createFiberFromTypeAndProps => createFiber
最终会来到createFiber
方法中,这个方法的内容就是调用FiberNode
构造函数,创建Fiber
对象实例。
js
const createFiber = function(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
): Fiber {
// 创建Fiber节点
return new FiberNode(tag, pendingProps, key, mode);
};
查看FiberNode
构造函数:
js
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag; // 节点类型,不同的值代表不同的节点对象
this.key = key; // 组件key
this.elementType = null; // 大部分情况同type,存储原始组件函数
this.type = null;
// 存储FiberNode对象对应的dom元素【hostCompoent】,
// 函数组件此属性无值,
// 类组件此属性存储的是组件实例instance
this.stateNode = null;
# FiberNode节点之间的链接
this.return = null; // 指向父级节点对象FiberNode
this.child = null; // 指向第一个子节点FiberNode
this.sibling = null; // 指向下一个兄弟节点FiberNode
this.index = 0;
this.ref = null; // ref引用
# hooks相关
this.pendingProps = pendingProps; // 新的,等待处理的props
this.memoizedProps = null; // 旧的,上一次存储的props
this.updateQueue = null; // 存储update更新对象链表
this.memoizedState = null; // 类组件:旧的,上一次存储的state; 函数组件:存储hook链表
this.dependencies = null;
this.mode = mode; // 模式,作用?
// 各种effect副作用相关的执行标记
this.flags = NoFlags;
this.subtreeFlags = NoFlags; // 子孙节点的副作用标记,默认无副作用
this.deletions = null; // 删除标记
// 优先级调度,默认为0
this.lanes = NoLanes;
this.childLanes = NoLanes;
# 这个属性指向另外一个缓冲区对应的FiberNode
// current.alternate === workInProgress
// workInProgress.alternate === current
this.alternate = null;
...
}
这里创建Fiber
节点时初始化的属性不多,重点是tag
属性和pendingProps
属性。
tag
属性是标记不同的组件类型,比如函数组件,类组件,普通DOM节点组件hostCompoent
。pendingProps
属性就是存储的由react元素对象传递过来的props
对象。
注意ref
属性是在Fiber
节点创建完成之后赋值的,不是直接传递给FiberNode
构造函数的。
js
// packages\react-reconciler\src\ReactChildFiber.new.js
const fiber = createFiberFromElement(element, returnFiber.mode, lanes);
// 设置ref
fiber.ref = coerceRef(returnFiber, currentFirstChild, element);
5,创建DOM元素
在Fiber
节点创建完成之后,下一步就是执行该Fiber
节点的工作流程,而每一个Fibe
节点都有两个工作模块内容:
beginWork
工作。completeWork
工作。
对于组件节点来说,它的重点在于beginWork
工作,比如函数组件和类组件的加载逻辑会在这个流程中执行。
对于普通DOM节点来说,它的重点在于completeWork
工作,在这里会根据Fiber
节点中的type
创建真实的DOM元素。
js
// 比如type: div
document.createElement('div')
在DOM元素创建完成之后,都会调用一个appendAllChildren
方法,将子节点内容添加到自身元素上。
js
appendAllChildren(instance, workInProgress, false, false);
同时在这里还会处理新建DOM元素的事件绑定和样式内容。
最后在一个组件内所有DOM节点组件的工作执行完成后,在组件的根节点的stateNode
属性上就会形成一个相对完整的DOM结构,而对于App
根组件来说,它的根节点元素【div.App】对应的Fiber.stateNode
属性上就会存在一个离屏的DOM树。
js
export default function MyFun(props) {
console.log('MyFun Start')
const [count, setCount] = useState(1)
const ref = useRef()
function handleClick() {
setCount(2)
}
return (
<div className="MyFun">
<div ref={ref}>DOM Instance</div>
<div>state: {count}</div>
<div>name: {props.name}</div>
<button onClick={handleClick}>Button</button>
</div>
)
}
比如MyFun
组件的completeWork
工作完成之后,它组件内的根dom元素 对应的Fiber
上就会存储组件内整个DOM结构。
js
// 根dom元素对应的fiber节点
<div className="MyFun">
此时它的stateNode
属性存储的就是组件的DOM结构:
注意:这并不是最终的DOM结构。
6,构建DOM树
在DOM元素处理之后,最终会来到react渲染流程的最后一个阶段:commit
阶段。
在commit
阶段的第二个子阶段:Muation
阶段会进行真实的DOM树构建,因为在之前每个组件虽然已经形成了部分DOM结构,但是这个DOM结构并不是最终确定的,因为组件的状态变化会影响DOM树的结构,具体来说就是会给DOM节点对应的Fiber
节点标记相应的副作用,比如DOM插入,移动和删除。在这些副作用都执行完成之后,才是最终确定的DOM树,最后会将这颗完整的DOM树添加到react应用的容器节点之中,到此页面的加载渲染就执行完成。
js
<div className="App"></div>
此时App
根组件内它的根元素对应的Fiber
节点的stateNode
属性存储的就是一颗处理完成的完整DOM树。
最后将这个div
添加到#root
容器元素内,页面即加载显示完成。
js
// #root
container.appendChild('div');
最后我们再来看一下类组件编译后的代码:
js
class d extends r.Component{
constructor(t){
super(t);
o(this,"handleClick",()=>{this.setState({count:2})});
console.log("MyClass Start"),
this.state={count:1},
this.ref=r.createRef()
}
componentDidMount(){
console.log("MyClass Mounted")
}
render(){
return n.jsxs("div",{
className:"MyClass",
children:[
n.jsx("div",{ref:this.ref,children:"DOM Instance"}),
n.jsxs("div",{children:["state: ",this.state.count]}),
n.jsxs("div",{children:["name: ",this.props.name]}),
n.jsx("button",{onClick:this.handleClick,children:"Button"})
]
})
}
}
export{d as default};
可以看出类组件在jsx
的转化和处理方面和函数组件是完全一样的,所以后面就不重复解释了。
结束语
以上就是从react
组件到真实DOM
渲染的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!