虚拟 DOM 是目前主流前端框架的技术核心之一,本文阐述如何实现一个简单的虚拟 DOM 系统。

为什么需要虚拟 DOM?

虚拟 DOM 就是一棵由虚拟节点组成的树,这棵树展现了真实 DOM 的结构。这些虚拟节点是轻量的、无状态的,一般是字符串或者仅仅包含必要字段的 JavaScript 对象。虚拟节点可以被组装成节点树树,通过特定的 "diff" 算法对两个节点树进行对比,找出其中细微的变更点,然后更新到真实 DOM 上去。

之所以会有虚拟 DOM,是因为直接更新真实 DOM 非常昂贵。通过新比对虚拟 DOM,然后只将变化的部分更新到真实 DOM 上去。这么做都是操作纯 JavaScript 对象,尽量避免了直接操作 DOM,读写成本低很多。

如何实现虚拟 DOM

在开始之前,我们需要明确一个虚拟 DOM 系统应该包含哪些必要的组成部分?

首先,我们要定义清楚什么是虚拟节点。一个虚拟节点可以是一个普通 JavaScript 对象,也可以是一个字符串。

我们定义一个函数 createNode 来创建虚拟节点。一个虚拟节点至少包含三个信息:

  • tag:保存虚拟节点的标签名,字符串
  • props:保存虚拟节点的 properties/attributes,普通对象
  • children:保存虚拟节点的子节点,数组

下面的代码是 createNode 实现样例:

const createNode = (tag, props, children) => ({
tag,
props,
children,
});

我们通过 createNode 可以轻松的创建虚拟节点:

createNode('div', { id: 'app' }, ['Hello World']);

// 返回如下:
{
tag: 'div',
props: { id: 'app' },
children: ['Hello World'],
}

现在,我们需要定义一个 createElement 函数来根据虚拟节点创建真实的 DOM 元素。

createElement 中,我们需要创建一个新的 DOM 元素,然后遍历虚拟节点的 props 属性,将其中的属性添加到 DOM 元素上去,之后再遍历 children 属性。如下代码是一个实现样例:

const createElement = vnode => {
if (typof vnode === 'string') {
return document.createTextNode(vnode); // 如果是字符串就直接返回文本元素
}
const el = document.createElement(vnode.tag);
if (vnode.props) {
Object.entries(vnode.props).forEach(([name, value]) => {
el[name] = value;
});
}
if (vnode.children) {
vnode.children.forEach(child => {
el.appendChild(createElement(child));
});
}
return el;
}

现在,我们可以通过 createElement 将虚拟节点转变成真实 DOM 了。

createElement(createNode("div", { id: "app" }, ["Hello World"]));

// 输出: <div id="app">Hello World</div>

我们再来定义一个 diff 函数来实现 'diff' 算法。这个 diff 函数接收三个参数,一个是已经存在的 DOM 元素,一个是旧的虚拟节点,一个是新的虚拟节点。在这个函数中,我们将对比两个虚拟节点,在需要的时候,将旧的元素替换掉。

const diff = (el, oldVNode, newVNode) => {
const replace = () => el.replaceWith(createElement(newVNode));
if (!newVNode) return el.remove();
if (!oldVNode) return el.appendChild(createElement(newVNode));
// 处理纯文本的情况
if (typeof oldVNode === 'string' || typeof newVNode === 'string') {
if (oldVNode !== newVNode) return replace();
} else {
// 对比标签名
if (oldVNode.tag !== newVNode.tag) return replace();
// 对比 props
if (!oldVNode.props?.some((prop) => oldVNode.props?[prop] === newVNode.props?[prop])) return replace();
// 对比 children
[...el.childNodes].forEach((child, i) => {
diff(child, oldVNode.children?[i], newVNode.children?[i]);
});
}
}

在这个函数中,我们先处理纯文本的情况,如果新旧两个字符串不相同,则直接替换。之后,我们就可以假定两个虚拟节点都是对象了。我们先对比两个节点的标签名是否相同,不同则直接替换。之后对比两个节点的 props 是否相同,不同也直接替换。最后我们在递归的使用 diff 函数对比两个虚拟节点的 children。

至此,我们就实现了一个简版虚拟 DOM 系统所必须的所有功能。下面是使用样例:

const oldVNode = createNode("div", { id: "app" }, ["Hello World"]);
const newVNode = createNode("div", { id: "app" }, ["Goodbye World"]);
const el = createElement(oldVNode);
// <div id="app">Hello World</div> diff(el, oldVNode, newVNode);
// el will become: <div id="app">Goodbye World</div>

文中的实现侧重于展示虚拟 DOM 的实现原理,在实现代码中并未考虑性能等其他因素。

如何快速实现一个虚拟 DOM 系统的更多相关文章

  1. vue 快速入门 系列 —— 虚拟 DOM

    其他章节请看: vue 快速入门 系列 虚拟 DOM 什么是虚拟 dom dom 是文档对象模型,以节点树的形式来表现文档. 虚拟 dom 不是真正意义上的 dom.而是一个 javascript 对 ...

  2. 手撸一个虚拟DOM,不错

    大家好,我是半夏,一个刚刚开始写文的沙雕程序员.如果喜欢我的文章,可以关注 点赞 加我微信:frontendpicker,一起学习交流前端,成为更优秀的工程师-关注公众号:搞前端的半夏,了解更多前端知 ...

  3. 手动实现一个虚拟DOM算法

    发现一个好文:<深度剖析:如何实现一个 Virtual DOM 算法> 源码 文章写得非常详细,仔细看了一遍代码,加了一些注释.其实还有有一些地方看的不是很懂(毕竟我菜qaq 先码 有时间 ...

  4. 手写一个虚拟DOM库,彻底让你理解diff算法

    所谓虚拟DOM就是用js对象来描述真实DOM,它相对于原生DOM更加轻量,因为真正的DOM对象附带有非常多的属性,另外配合虚拟DOM的diff算法,能以最少的操作来更新DOM,除此之外,也能让Vue和 ...

  5. PHP7 学习笔记(十一)使用phpstudy快速配置一个虚拟主机

    说明:为了windows本地开发php方便,这里推荐使用PHP集成环境phpstudy. 目的:使用域名访问项目(tinywan.test) 1.官网:http://www.phpstudy.net ...

  6. 虚拟DOM介绍

    [转自]:https://www.jianshu.com/p/616999666920 为什么需要虚拟DOM 先介绍浏览器加载一个HTML文件需要做哪些事,帮助我们理解为什么我们需要虚拟DOM.web ...

  7. 虚拟dom?diff算法?key?Vue原理的核心三问?打包教你搞定。

    为什么需要虚拟DOM 先介绍浏览器加载一个HTML文件需要做哪些事,帮助我们理解为什么我们需要虚拟DOM.webkit引擎的处理流程,如下图所示: 所有浏览器的引擎工作流程都差不多,如上图大致分5步: ...

  8. 2.ReactJS基础(虚拟DOM,JSX语法)

    将脚手架(create-react-app)创建的todolist项目精简为hello world示例 即,删除自动生成的样式文件.logo.svt.App.test.js.serviceWorker ...

  9. 虚拟DOM与DOM diff算法

    虚拟DOM是什么? 一个虚拟DOM(元素)是一个一般的js对象, 准确的说是一个对象树(倒立的) 虚拟DOM保存了真实DOM的层次关系和一些基本属性,与真实DOM一一对应,如果只是更新虚拟DOM, 页 ...

随机推荐

  1. 基于多IP地址Web服务

    [Centos7.4版本] !!!测试环境我们首关闭防火墙和selinux [root@localhost ~]# systemctl stop firewalld [root@localhost ~ ...

  2. ansible-一键完成LNMP架构_期中架构

    ansible-一键完成LNMP架构 ansible剧本托管地址 https://github.com/Gshelldong/ansible.git 网站架构图 ansible一键完成lnmp架构 a ...

  3. 7.7-9 chage、chpasswd、su

    7.7 chage:修改用户密码有效期 chage命令用于查看或修改用户密码的有效期,有些参数和passwd的功能相同.   -d    设置上一次密码更改的日期 -E    账号过期的日期.日期格式 ...

  4. 常用数据库连接池配置及使用(Day_11)

    世上没有从天而降的英雄,只有挺身而出的凡人. --致敬,那些在疫情中为我们挺身而出的人. 运行环境 JDK8 + IntelliJ IDEA 2018.3  优点: 使用连接池的最主要的优点是性能.创 ...

  5. [论文阅读笔记] Community aware random walk for network embedding

    [论文阅读笔记] Community aware random walk for network embedding 本文结构 解决问题 主要贡献 算法原理 参考文献 (1) 解决问题 先前许多算法都 ...

  6. PHP转JAVA开发30分钟实战攻略

    服务端开发中,有很多知识是相通的,例如mysql,redis,http协议等. 基于这些基础,在编程语言上的转变并不困难. 本文主要从下面几点出发,讲述如何快速从php开发转为java开发: 使用框架 ...

  7. GO学习-(7) Go语言基础之流程控制

    流程控制是每种编程语言控制逻辑走向和执行次序的重要部分,流程控制可以说是一门语言的"经脉". Go语言中最常用的流程控制有if和for,而switch和goto主要是为了简化代码. ...

  8. Git使用总结(包含Git Bash和Git GUI的使用)(转自CSDN)

    基本命令 初始化设置 配置本机的用户名和Email地址 $ git config --global user.name "Your Name" $ git config --glo ...

  9. 用Taro写一个微信小程序(三)—— 配置dva

    一.关于dva dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻 ...

  10. Django(58)viewsets视图集详解

    前言 ViewSet 只是一种基于类的视图,它不提供任何方法处理程序(如 .get()或.post()),而是提供诸如.list()和 .create() 之类的操作. ViewSet 的方法处理程序 ...