实现一个简版的React框架(2)-DOM渲染

marvin

DOM 渲染是一个核心步骤, 能够将 jsx 渲染到页面上, 这里还需要借助 babel 来实现, 为了能够方便的配置 babel, 这里不使用任何打包工具, 为了演示效果需要创建以下文件:
├── index.html
├── lib
│ └── dom.js
├── package.json
├── style
│ └── todo-app.css
├── todo-app.js
└── yarn.lock
这里以一个 todo 应用作为演示项目, index.html 是入口文件, 所有的库文件都放在 lib 目录下, 其中 dom 负责解析渲染 DOM 元素. 样式放在 style 文件夹下, todo-app.css 是 todo 应用的样式文件, todo-app.js 是 todo 应用的业务代码文件. index.html 的内容如下:
<html>
<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</title>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"
integrity="sha512-kp7YHLxuJDJcOzStgd6vtpxr4ZU9kjn77e6dBsivSz+pUuAuMlE2UTdKB7jjsWT84qbS8kdCWHPETnP/ctrFsA==" crossorigin="anonymous">
</script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/all.min.css" integrity="sha512-+4zCK9k+qNFUR5X+cKL9EIR+ZOhtIloNl9GIKS57V1MyNsYpYcUrUeQc9vNfzsWfV28IaLL3i96P9sdNyeRssA=="
crossorigin="anonymous"
/>
<link rel="stylesheet" href="./style/todo-app.css">
</head>
<body>
<div id="root"></div>
<script type="text/babel" src="./lib/dom.js"></script>
<script type="text/babel" src="./todo-app.js"></script>
</body>
</html>
这里通过 cdn 的方式引入了 babel, 用于编译 jsx 代码, 另外还引入了 font-awesome 这个会在 todo 应用中用到部分样式, 最后分别引入了样式文件, dom 解析文件和业务代码文件.
安装 http-server
为了启动这个包含演示的简版 react 项目, 需要启动一个本地服务器, 需要安装 http-server:
yarn init
yarn add http-server
设置启动脚本
为了通过命令使用 http-server, 需要在 package.json 中新增 start 命令, 修改内容如下:
// ...
"scripts": "http-server"
// ...
之后就可以通过运行 yarn start 启动项目了
创建虚拟 DOM
React 的虚拟 DOM 是通过 createElement 创建的, 这里也以同样的接口实现, 为了方便, 我们将以一个类实现整个库, lib/dom.js 修改如下:
class TinyReact {
static createElement(type, props = {}, ...children) {
const childElements = [...children].map(child => {
if(child != null && child !== true && child !== false) {
return child instanceof Object ? child : TinyReact.createElement('text', { textContent: child })
}
}).filter(i => !!i)
return {
type,
children: childElements,
props: { children: childElements, ...props }
}
}
}
createElement 接受三个参数, 分别是元素类型, 元素的属性对象, 子元素. 这里首先对子元素进行遍历, 过滤掉 null, undefined, true, false 这几种值, 解析每一个子元素的虚拟 DOM, 最后返回一个虚拟 DOM 对象, 其中包含:
- type 元素类型
- children 子元素的虚拟 DOM 数组
- props 包含 children 的属性对象
将 JSX 转换为虚拟 DOM
为了使用 jsx, 我们需要修改业务代码, React 通过 babel 转译之后默认是 React.createElement, 为了将 React 改为 TinyReact 需要在使用 JSX 的地方通过 @jsx 注释告诉 babel 我们需要替换的意图, todo-app.js 修改如下:
// @jsx TinyReact.createElement
const root = document.getElementById('root');
const exp = (
<div>
<h1 className="header">Hello world!</h1>
<h2>Hi</h2>
</div>
)
console.log(exp)
保存后通过 yarn start 运行项目, 然后打开浏览器, 可以看到控制台输出了 VDOM 对象
将虚拟 DOM 渲染到页面上
在 react 通过 render 函数可以将 VDOM 渲染到页面上, 我们这里也创建一个 render 函数:
static render(vdom, container, oldDom = container.firstChild) {
if(!oldDom) {
TinyReact.mountElement(vdom, container, oldDom);
}
}
static mountElement(vdom, container, oldDom) {
return TinyReact.mountSimpleNode(vdom, container, oldDom)
}
static mountSimpleNode(vdom, container, oldDomElement, parentComponent) {
let newDomElement = null;
const nextSibling = oldDomElement && oldDomElement.nextSibling;
if(vdom.type === 'text') {
newDomElement = document.createTextNode(vdom.props.textContent);
} else {
newDomElement = document.createElement(vdom.type);
}
newDomElement._virtualElement = vdom;
if(nextSibling) {
container.insertBefore(newDomElement, nextSibling);
} else {
container.appendChild(newDomElement);
}
vdom.children.forEach(child => {
TinyReact.mountElement(child, newDomElement);
})
}
这里我们传入了3个参数:
- vdom 虚拟 DOM 对象
- container html 根元素
- html 根元素的第一个节点元素
这里目前只考虑了根节点为空的情况, 这时直接执行挂载操作, 如果存在兄弟节点, 则将当前元素插入到这个兄弟节点之前, 否则将当前元素插入到 container 父容器中, 这里的 container 在第一次执行的时候代表 id 为 root 的 div 元素, 最后通过递归将子元素依次渲染到当前元素上面
写在最后
虽然目前能够将元素渲染出来, 但虚拟 DOM 中的属性和事件, Diff我们还没有处理, 这部分留给第三篇完成.