html5新的dom函数,从0到1: 实现一个虚拟 DOM(上)

写在前面

本文分上下两篇,实现一个基础版本的虚拟 DOM。

上篇首先介绍什么是虚拟 DOM、为什么要使用虚拟 DOM,其次完成项目创建、实现 h 函数、render 函数以及 mount 函数,完成创建虚拟 DOM 到挂载到页面成为真实 DOM的过程;下篇将通篇介绍虚拟 DOM 的核心,diff 算法的实现。

本文上篇代码已上传GitHub。

目录

1 什么是虚拟 DOM

2 为什么要使用虚拟 DOM

3 虚拟 DOM 实现

3.1 项目创建

3.2 h 函数:用 JS 对象模拟 DOM 树

3.2 render 函数:实现渲染

3.3 mount 函数:实现挂载

3.4 diff 算法

1. 什么是虚拟 DOM

用 JavaScript 对象描述真实的 DOM 结构,就是虚拟 DOM。

237ac3c70e362e2d21d0b2e2b6389713.png

hello world!

复制代码

因为真实 DOM 本身天然具有树状结构,因此用 JavaScript 对象描述非常容易。在这个对象中,我们只需要使用 tagName 、props、children 三个属性就可以完整的描述上面的 HTML 结构:

{

tagName: 'div',

props: {

id: 'app'

},

children: [

{

tagName: 'h1',

props: {},

children: ['hello world!']

}

]

}

复制代码

上面的代码就是一个虚拟 DOM 了,简单吧!

5e7059d7ff0e0d1206328408db9d817a.png

2. 为什么要使用虚拟 DOM?

第一个原因:DOM 操作太慢了。这里的慢有两方面,一是性能低,页面慢。二是手动操作 DOM,效率低,从而导致开发节奏慢。

我们可以在浏览器打开一个空白页面:about:blank,打开控制台,输入以下代码:

const div = document.createElement('div')

let str = ''

for (let key in div) {

str = str + key + ','

}

console.log(str)

复制代码

此时你会看到:

088da863ad5022a119496e359c1b2adf.png

仅仅一个 div 元素的属性就这么庞大,可想而知为什么操作 DOM 的花销会及其巨大了。

494eaa918800c61bfcb485e3b1d1009e.png

而相比于 DOM 对象,JavaScript 对象处理起来就非常快了,再加上 diff 算法,找出最小差异,然后进行批量 patch。这样我们可以极大的减少真实的 DOM 操作,减少重排,提升性能。

第二个原因:真实 DOM 与浏览器强相关,而虚拟 DOM 本质上是 JavaScript 对象,JavaScript 对象能够更方便地进行跨平台操作。如服务端渲染,移动端开发等等。

3. 虚拟 DOM 实现

说了这么多理论,我们就开始 step by step,一步步实现一个虚拟 DOM 吧。

35e9f6098cde55f317df6192b1a5fd1c.png

3.1 项目创建

首先,我们创建项目目录:

$ mkdir virtual-dom-lite

$ cd virtual-dom-lite

复制代码

然后,我们进行 npm 初始化

$ npm init -y

复制代码

最后,我们安装一下 Parcel Bundler,一个无需配置的小型项目打包器。安装后可以启动一个 dev-server,并具有热更新功能。

$ npm install parcel-bundler

复制代码

安装好依赖后,创建一个 src 目录,在 src 目录下,创建两个文件:

src/index.html

Virtual DOM

hello world

复制代码

src/main.js

const vApp = {

tagName: 'div',

props: {

id: 'app'

}

}

console.log(vApp)

复制代码

最后修改 package.json。

package.json

{

"scripts": {

"dev": "parcel src/index.html"

}

}

复制代码

在终端输入 npm run dev 就可以启动我们的项目啦。

3.2 h 函数:用 JS 对象模拟 DOM 树

一般情况下,我们都使用 createElement 方法创建虚拟 DOM。在绝大多数虚拟 DOM 库以及流行框架中,通常用 h 函数指代,因此我们也来创建一个 h 函数。

还记得 JavaScript 对象中用哪三个属性就可以描述 DOM 树么?对,就是 tagName、props 以及 children。其中 props 和 children 是可选项,因为想要表示一个 DOM 结构,标签是必选项,但可以没有属性,也可以没有子元素。

src/vdom/creatElement.js

export default (tagName, opts) => {

const vElement = Object.create(null)

Object.assign(vElement, {

tagName,

props: opts.props || {},

children: opts.children || []

});

return vElement;

}

复制代码

使用对象解构后,可以优化上面的代码

src/vdom/createElement.js

export default (tagName, { props = {}, children = [] } = {}) => {

const vElement = Object.create(null)

Object.assign(vElement, {

tagName,

props,

children

});

return vElement;

}

复制代码

src/main.js

import h from './vdom/createElement'

const vApp = h('div', {

props: {

id: 'app'

}

})

console.log(vApp)

复制代码

这样,在浏览器的控制台中,我们就可以看到虚拟 DOM 对象啦。

0bab68d98c75eff677a7d3d89a5b72b9.png

3.2 render 函数:实现渲染

有了虚拟 DOM 对象,那么如何将它转化为真正的 DOM 呢?这就是渲染函数要干的事情。

在这里我们仅针对元素节点和文本节点进行渲染。

src/vdom/render.js

const render = (vNode) => {

// 如果字符串,认为是文本节点,我们创建一个一个文本节点

if (typeof vNode === 'string') return document.createTextNode(vNode)

// 其他情况默认为元素节点

// 创建一个元素

const $el = document.createElement(vNode.tagName)

// 添加虚拟 DOM 对象上所有的属性

for (const [key, value] of Object.entries(vNode.props)) {

$el.setAttribute(key, value)

}

// 如果虚拟 DOM 上有子元素,则追加(这里其实有一个递归思想)

for (const child of vNode.children) {

$el.appendChild(render(child))

}

return $el

}

export default render

复制代码

回到 main.js 文件,我们尝试用 render 函数渲染一下我们的虚拟 DOM,并在控制台打印一下:

src/main.js

import h from './vdom/createElement'

import render from './vdom/render'

const vApp = h('div', {

props: {

id: 'app'

},

children: [

h('h1', {

props: {

id: 'title'

},

children: ['hello world!']

})

]

})

const $app = render(vApp)

console.log($app)

复制代码

此时,在控制台打印,可以看到以下结果:

ae61c0053a5403ad3caae195f22cdf08.png

3.3 mount 函数:实现挂载

现在,我们可以创建虚拟 DOM 并渲染其为真实 DOM 了。下一步就是激动人心的时刻 —— 将渲染出来的真实 DOM 挂载到页面上!!!

我们首先修改 index.html 页面

src/index.html

Virtual DOM

复制代码

然后编写我们的挂载函数:

src/vdom/mount.js

export default ($node, $target) => {

return $target.appendChild($node)

}

复制代码

src/main.js

import h from './vdom/createElement'

import render from './vdom/render'

import mount from './vdom/mount'

const vApp = h('div', {

props: {

id: 'app'

},

children: [

h('h1', {

props: {

id: 'title'

},

children: ['hello world!']

})

]

})

const $app = render(vApp)

mount($app, document.getElementById('root'))

复制代码

da994c9758e9ed18615d02a0a5f7ef44.png

终于,我们的页面可以出现自己渲染的虚拟 DOM 啦!里程碑!扭起来!

3a224fae0fa2a70d035895fab750a81a.gif

高兴的同时,其实现在的页面还是有一些的问题的:当我们需要修改页面时,无论是增删改元素、属性或者子元素,我们只能全量渲染和挂载,这又会造成不必要的性能损耗,因此,整个实现虚拟 DOM 过程中最重要的 diff 算法要出场了。

因为 diff 算法是整个虚拟 DOM 实现的核心,篇幅略长,我们在下一篇重点实现。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值