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

实现初始化页面
理解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])
}
})
}

