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

marvin

marvin

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

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 对象, 其中包含:

  1. type 元素类型
  2. children 子元素的虚拟 DOM 数组
  3. 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个参数:

  1. vdom 虚拟 DOM 对象
  2. container html 根元素
  3. html 根元素的第一个节点元素

这里目前只考虑了根节点为空的情况, 这时直接执行挂载操作, 如果存在兄弟节点, 则将当前元素插入到这个兄弟节点之前, 否则将当前元素插入到 container 父容器中, 这里的 container 在第一次执行的时候代表 id 为 root 的 div 元素, 最后通过递归将子元素依次渲染到当前元素上面

写在最后

虽然目前能够将元素渲染出来, 但虚拟 DOM 中的属性和事件, Diff我们还没有处理, 这部分留给第三篇完成.

安装 http-server
设置启动脚本
创建虚拟 DOM
将 JSX 转换为虚拟 DOM
将虚拟 DOM 渲染到页面上
写在最后