React函数组件如何一步一步到真实DOM

之所以会讨论这个话题,是因为最近在看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.jsxn就是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';

这里就是上面编译后的代码使用的jsxjsxs方法。

继续查看它的来源:

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};

我们可以发现jsxjsxs方法在生产环境下都是引用的同一个方法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元素对象的逻辑也比较简单,主要就是针对组件keyref,以及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渲染的全部内容了,觉得有用的可以点赞收藏!如果有问题或建议,欢迎留言讨论!

相关推荐
崔庆才丨静觅11 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606111 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了11 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅11 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅12 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅12 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment12 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅13 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊13 小时前
jwt介绍
前端
爱敲代码的小鱼13 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax