前言
React 的编写语法是 JSX、TSX, 但是他们并不能被浏览器识别,即运行的结果不能呈现在浏览器中。那么 React 是如何解决这个问题的呢?本文通过简易的 demo,理清 vdom 和 jsx 的原理。
JSX
JSX 本质上是个语法糖,在运行的时候,需要通过 babel 编译。
比如:
const ele = <div>123</div>
最终会被 babel 编译成下面的代码:
const ele = React.createElement(
'div',
{},
"123"
)
而上面 createElement(vdom) 中的 vdom 其实就是虚拟DOM。
VDOM
VDOM 可以理解为 babel 编译后的一种 数据结构
。
const ele = (
<div className='container'>
div文本
<h1> 我是h1 </h1>
<div onClick={() => alert(123)}>
啊啊啊
</div>
</div>
)
在经过 babel 编译后会得到如下的结果:
简单点写就是:
const ele = {
type: "div",
props: {
className: "container",
},
children: [
"div文本",
{
type: "h1",
props: {},
children: ["我是h1"]
},
{
type: "div",
props: {
onClick: function() {
alert(123)
}
},
children: [
"啊啊啊"
]
}
]
}
这就是 VDOM。
VDOM 渲染
那既然 React 代码被 babel 编译成了 React.createElement 包裹的 VDOM
,那么 VDOM 又是如何渲染到页面上的呢?我们可以通过简单的 demo 来理解它的原理。
React.createElement
从上面编译的结果来看,当通过 React.render 时,会将 ele 转成 vdom
React.render(
ele,
document.getElementById('root')
)
//等价于
React.render(
React.createElement(vdom),
document.getElementById('root')
);
所以 createElement 应该接受如下参数:
- type:当前 vdom 的类型,比如
html标签、自定义组件
- attrs:当前 vdom 的属性,比如
style、className、绑定的事件
- children:子节点
所以 createElement 实现如下:
const React = {
createElement,
}
function createElement(type, attrs, ...children) {
//...children 是因为子节点数量不固定,拓展出来
return {
type,
attrs,
children
}
}
createElement 返回一个用来记录 VDOM 信息的对象
,这样,我们就可以通过 vdom 去生成真实dom。即,我们需要实现 render 方法
React.render
render 其实也不难,总得来说过程如下:
- 根据
type
,判断当前节点是什么类型,创建对应类型的真实DOM。 - 根据
attrs
,为这个 真实DOM 设置属性。 - 根据
children
,递归执行 render,生成子节点的 真实DOM。 - 把最终的真实DOM结果返回。
代码如下:
/**
*
* @param {*} vnode 虚拟dom
* @param {*} container 根节点容器
* @returns
*/
function render(vnode, container) {
//如果是文本类型
if (isTextDom(vnode)) {
const textNode = document.createTextNode(vnode);
return container.appendChild(textNode);
} else if (isElementVdom(vnode)) {
//如果是元素类型
//根据 type 创建对应的元素
const dom = document.createElement(vnode.type);
//添加属性
if(vnode.attrs) {
Object.keys(vnode.attrs).forEach((key) => {
const value = vnode.attrs[key];
setAttribute(dom, key, value);
});
}
//递归 children 生成子节点真实dom
vnode.children.forEach((child) => render(child, dom));
//返回最终的结果
return container.appendChild(dom);
}
}
function isTextDom(vdom) {
return typeof vdom === 'string' || vdom === 'number'
}
function isElementVdom(vdom) {
return typeof vdom === 'object'&& typeof vdom.type === 'string'
}
function setAttribute(dom, key, value) {
//如果是事件属性,通过 addEventListener 绑定
if (typeof value == "function" && key.startsWith("on")) {
const eventType = key.slice(2).toLowerCase();
dom.addEventListener(eventType, value);
} else if (key == "style" && typeof value == "object") {
// 如果是 style
Object.assign(dom.style, value);
} else if (key === 'className') {
// 如果是 className, 改成 class
dom.setAttribute('class', value);
} else if (typeof value != "object" && typeof value != "function") {
// 如果是普通属性
dom.setAttribute(key, value);
}
}
最后,将 render 挂在到 ReactDOM 上:
const ReactDOM = {
render: (vnode, container) => {
container.innerHTML = '';
return render(vnode, container);
}
}
以上,我们就实现了 vdom 的渲染。现在我们来测试一下。
测试 demo
首先,创建一个 react 应用。
mkdir react_vdom && cd react_vdom && npm init -y
其次,添加相关依赖,其中,parcel
是零配置的前端打包工具,推荐各种 demo 里面用。
yarn add -D @babel/preset-react @babel/preset-env parcel-bundler
然后,添加 .babelrc 文件
//.babelrc
{
"presets": ["@babel/preset-react"]
}
根目录下,创建 src/index.js,将上面的代码拷贝
// src/index.js
const React = {
createElement,
};
const ReactDOM = {
render: (vnode, container) => {
container.innerHTML = "";
return render(vnode, container);
},
};
function createElement(tag, attrs, ...children) {
return {
tag,
attrs,
children,
};
}
function render(vnode, container) {
if (typeof vnode === "string") {
let textNode = document.createTextNode(vnode);
return container.appendChild(textNode);
}
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
Object.keys(vnode.attrs).forEach((key) => {
const value = vnode.attrs[key];
setAttribute(dom, key, value);
});
}
vnode.children.forEach((child) => render(child, dom));
return container.appendChild(dom);
}
function setAttribute(dom, key, value) {
if (typeof value == "function" && key.startsWith("on")) {
const eventType = key.slice(2).toLowerCase();
dom.addEventListener(eventType, value);
} else if (key == "style" && typeof value == "object") {
Object.assign(dom.style, value);
} else if (key === 'className') {
dom.setAttribute('class', value)
} else if (typeof value != "object" && typeof value != "function") {
dom.setAttribute(key, value);
}
}
const ele = (
<div className='container'>
div文本
<h1> 我是h1 </h1>
<div onClick={() => alert(2)}>啊啊啊</div>
</div>
);
ReactDOM.render(ele, document.getElementById("root")); //等价于ReactDOM.render(React.createElement(vdom), document.getElementById('root'))
根目录下,创建 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>react_vdom_code</title>
</head>
<body>
<div id="root"></div>
<script src="src/index.js"></script>
</body>
</html>
package.json 中添加命令
"scripts: {
"start": "parcel index.html"
},
运行 yarn start,打开页面如下:
结语
以上内容如有错误,欢迎留言指出,一起进步