«

react源码解密3-实现初始化渲染

时间:2026-4-4 00:02     作者:john     分类:


dom和虚拟dom

真实dom是前端必修课,她是浏览器内提供的一套底层文档对象模型,从而让你渲染浏览器内的内容以及操控浏览器内的内容。

如果从技术本质上来看,她其实应该是浏览器这个软件本身底层 c++所实现的渲染之上的一层逻辑抽象。用一种dom这样的数据结构来管理浏览器画布里绘制的东西。

再从技术层面往上层看,c++的dom模型系统为了能被开发者调用,她暴露了api出来给外层编程接口。但浏览器内并不能直接用c++代码来调用,因为浏览器还有一层语言转换层。于是语言转换层负责把js调用交给v8引擎解析后变成c++调用再调到对应的那套dom模型系统、也可能是调用到bom模型系统等等。

虚拟dom

可以看到浏览器里面一个真实dom的属性极其复杂,几百个属性。而且我们知道:当你做一次真实dom操作的时候,你会穿透到浏览器底层c++层面,这里是存在耗时成本的。
(尽管现代浏览器会在你同时调用多次dom操作的时候给你合并成一次c++穿透,但是一旦你是在读取offsetHeight等动作时候她并不会合并而是会每次都穿透到底层c++)。

于是,为了更轻量化就出现了虚拟DOM,所有复杂交互我们先在虚拟dom层面搞完,最后再计算出差异后再去操作真实dom。

jsx

虚拟dom本质上是一个 createElement(tagName, props, children)这样的函数,来创建出一个类似于真实dom的数据结构对象出来。为了能融入到js编写组件的过程中,react发明了jsx语法。

如果你打印一下这样一个div,你就会发现他已经createElment完成并且返回了我所说的对象。

react17之后,貌似已经变成从 react/jsx-runtime 这里引入jsxDEV这么个函数来当做React.createElement函数,这玩意就等于说我可以脱离React来单独使用jsx咯。
babel官网试验场可以做实验:
https://babeljs.io/repl

我们自己本地编写的代码构建后其实也变成了jsxDev:

实现初始化页面

理解react-dom和react的关系

react仅仅是用于必要的react组件的实现。即那个抽象的js层面的组件概念的实现。
react-dom他是用于渲染web平台的库,即把你的react组件渲染的web平台上。同理要渲染react-native的话可能就有reactnative的包。 其中react-dom他包括服务端的渲染api和客户端的api,且他依赖于react-reconciler。

环境搭建

为了简化构建工具搭建步骤,我直接通过vite官方模板vite 创建了一个js版本的react项目。然后我们把原来的react导入给删掉换成我们自己的应该就可以了。

先实现React.createElement

我们先按照以前旧的jsx来实现。即createElement而不是jsxDev。

如果要改回旧的React.createElement实现,注意不要像下面这样在npm start的时候改成这样启动:

DISABLE_NEW_JSX_TRANSFORM=true

因为最新vite项目是需要你去配置plugin-react的配置:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: [react({
      // 将 jsxRuntime 设置为 classic 即可恢复为 React.createElement
      jsxRuntime: 'classic' 
    })],
})

然后接下来你就可以在你的main.jsx里面去import一下React(这是传统模式的要求):

import { createRoot } from 'react-dom/client'
import React from 'react'

console.log(<div>i am init virtual</div>)

createRoot(document.getElementById('root')).render(
    <div>hello</div>
)

于是最终浏览器渲染就变成了:

createElement的实现

他的目的就是你传递的标签名、属性、子标签,给你转变成js层面的一种“虚拟dom结构”。

我们先看下这函数应该返回什么:这个之前已经打印过了:

其中type那个是个Symbol类型的常量枚举值吧,他代表这个vnode到底是元素类型还是其他xx类型。其他那些babel转义自带的东西咱们不关心。所以其实最核心的就是ref、$$typeof、type、key、props。

接下来开始编写类型常量:

export const REACT_ELEMENT_TYPE = Symbol.for('react.element')

接下来开始编写react.js里这个最重要的createElement,即React.createElement:

import { REACT_ELEMENT_TYPE } from "./utils"

const createElement = (type, props, ...children) => {
  // 删除无用属性
  ['__self', '__source'].forEach(k => {
    delete props[k]
  })
  // 主逻辑
  if (props.children) {
    props.children = Array.isArray(props.children) ? props.children : [props.children]
  }
  if (props.children && children) {
    props.children = props.children.concat(children)
  }

  const res = {
    $$typeof: REACT_ELEMENT_TYPE,
    type,
    ref: props.ref ?? null,
    key: props.key ?? null,
    props: {
      ...props,
      children
    }
  }
  delete props.key
  delete props.ref
  return res
}

export default {
  createElement
}

测试一把,我们在main.js里面这么使用他:

import React from '../myreact/react'

console.log('虚拟dom生成', <div>hello myreact</div>)
// 之所以我们这里可以直接用jsx,是因为vite的pluin-react配置下,他确实可以把上述模板转成React.createElement调用。而我们上方已经给他提供了来自我们自己编写的React这个类。

测试成功:

实现react-dom的render

为了简便,我们也不去实现react18和19的createRoot之后再render 了,咱们就直接来实现reactDOM.render这么个函数就行。
即: reactDOM.render(vnode, containerDOM)

首先我们的基础骨架是这样的:

import { REACT_ELEMENT_TYPE } from "./utils"

const renderVNode = (vnode, rootDOM) => {
  console.log('renderVNode', vnode, rootDOM)
  // 1. 做一些其他事情(未来再学习这块)
  // 2. 将虚拟dom转成真正的document.createElement的真实dom
  // 3. 将转换后的真实dom挂载到画布容器rootDOM上面
  doMount(vnode, rootDOM)
}

const ReactDOM = {
  render: renderVNode
}

export default ReactDOM

即,我们现在初步要实现的就是这个doMount函数,用于把虚拟dom改成真实dom,然后把真实dom的根元素挂到我们期望的画布rootDOM上。
这里涉及到一个递归(深度优先的遍历,我觉得可以理解成“中左右”那种中序遍历,即先从根节点开始看,看完createElement后再去看左侧之后看右侧)。


// 这是核心算法:这里涉及到深度优先遍历的算法
function doMount(vnode, parentRealDOM) {
  const { type, $$typeof, props } = vnode
  if (type && $$typeof === REACT_ELEMENT_TYPE) {
    const realNode = _createRealDom(vnode)
    if (props?.children && Array.isArray(props.children)) {
      props.children.forEach(childVnode => doMount(childVnode, realNode))
    }
    parentRealDOM.appendChild(realNode)
    _setRealElementProps(vnode.props, realNode)
  }
  else if (typeof vnode === 'string') {
    const realNode = document.createTextNode(vnode)
    parentRealDOM.appendChild(realNode)
  }
}

function _createRealDom(vnode) {
  const { type } = vnode
  let ele = null
  if (type && vnode.$$typeof === REACT_ELEMENT_TYPE) {
    ele = document.createElement(type)

  }
  return ele
}

function _setRealElementProps(props, realNode) {
  if (!props) return
  Object.keys(props).forEach(k => {
    if (k === 'children') {
      return
    }
    else if (k === 'className') {
      realNode.setAttribute('class', props[k])
    }
    else if (/^on[A-Z]\w*/.test(k)) {
      // todo: 事件处理
    }
    else if (k === 'style' && typeof props[k] === 'object') {
      const styleObj = props[k]
      Object.keys(styleObj).forEach(s => {
        realNode.style[s] = styleObj[s]
      })
    }
    else {
      realNode.setAttribute(k, props[k])
    }
  })
}