来源 | https://segmentfault.com/a/1190000021331850要构建自己的虚拟 DOM,需要知道两件事。你甚至不需要深入 React 的源代码或者深入任何其他虚拟 DOM 实现的源代码,因为它们是如此庞大和复杂——但实际上,虚拟 DOM 的主要部分只需不到 50 行代码。有两个概念:

  • Virtual DOM 是真实 DOM 的映射

  • 当虚拟 DOM 树中的某些节点改变时,会得到一个新的虚拟树。算法对这两棵树(新树和旧树)进行比较,找出差异,然后只需要在真实的 DOM 上做出相应的改变。

用 JS 对象模拟 DOM 树

首先,我们需要以某种方式将 DOM 树存储在内存中。可以使用普通的 JS 对象来做。假设我们有这样一棵树:

<ul class="”list”">    <li>item 1li>    <li>item 2li>ul>

看起来很简单,对吧? 如何用 JS 对象来表示呢?

{ type: ‘ul’, props: { ‘class’: ‘list’ }, children: [  { type: ‘li’, props: {}, children: [‘item 1’] },  { type: ‘li’, props: {}, children: [‘item 2’] }] }

这里有两件事需要注意:

  • 用如下对象表示 DOM 元素

{ type: ‘…’, props: { … }, children: [ … ] }
  • 用普通 JS 字符串表示 DOM 文本节点

但是用这种方式表示内容很多的 Dom 树是相当困难的。这里来写一个辅助函数,这样更容易理解:

function h(type, props, …children) {  return { type, props, children };}

用这个方法重新整理一开始代码:

h(‘ul’, { ‘class’: ‘list’ },  h(‘li’, {}, ‘item 1’),  h(‘li’, {}, ‘item 2’),);

这样看起来简洁多了,还可以更进一步。这里使用 JSX,如下:

<ul className="”list”">    <li>item 1li>    <li>item 2li>ul>

编译成:

React.createElement(‘ul’, { className: ‘list’ },  React.createElement(‘li’, {}, ‘item 1’),  React.createElement(‘li’, {}, ‘item 2’),);

是不是看起来有点熟悉?如果能够用我们刚定义的 h(...)函数代替 React.createElement(…),那么我们也能使用 JSX 语法。其实,只需要在源文件头部加上这么一句注释:

/** @jsx h */<ul className="”list”">    <li>item 1li>    <li>item 2li>ul>

它实际上告诉 Babel ‘ 嘿,小老弟帮我编译 JSX 语法,用 h(...)函数代替 React.createElement(…),然后 Babel 就开始编译。’综上所述,我们将 DOM 写成这样:

/** @jsx h */ const a = (<ul className="”list”">    <li>item 1li>    <li>item 2li>ul>);

Babel 会帮我们编译成这样的代码:

const a = (  h(‘ul’, { className: ‘list’ },    h(‘li’, {}, ‘item 1’),    h(‘li’, {}, ‘item 2’),  ););

当函数 “h”执行时,它将返回普通 JS 对象-即我们的虚拟 DOM:

const a = (  { type: ‘ul’, props: { className: ‘list’ }, children: [    { type: ‘li’, props: {}, children: [‘item 1’] },    { type: ‘li’, props: {}, children: [‘item 2’] }  ] });

从 Virtual DOM 映射到真实 DOM

好了,现在我们有了 DOM 树,用普通的 JS 对象表示,还有我们自己的结构。这很酷,但我们需要从它创建一个真正的 DOM。首先让我们做一些假设并声明一些术语:

  • 使用以’ $ ‘开头的变量表示真正的 DOM 节点(元素,文本节点),因此 \$parent 将会是一个真实的 DOM 元素

  • 虚拟 DOM 使用名为 node 的变量表示

*就像在React中一样,只能有一个根节点——所有其他节点都在其中。那么,来编写一个函数 createElement(…),它将获取一个虚拟 DOM 节点并返回一个真实的 DOM 节点。这里先不考虑 props和 children属性:

function createElement(node) {  if (typeof node === ‘string’) {    return document.createTextNode(node);  }  return document.createElement(node.type);}

上述方法我也可以创建有两种节点分别是文本节点和 Dom 元素节点,它们是类型为的 JS 对象:

{ type: ‘…’, props: { … }, children: [ … ] }

因此,可以在函数 createElement传入虚拟文本节点和虚拟元素节点——这是可行的。现在让我们考虑子节点——它们中的每一个都是文本节点或元素。所以它们也可以用 createElement(…) 函数创建。是的,这就像递归一样,所以我们可以为每个元素的子元素调用 createElement(…),然后使用 appendChild()添加到我们的元素中:

function createElement(node) {  if (typeof node === ‘string’) {    return document.createTextNode(node);  }  const $el = document.createElement(node.type);  node.children    .map(createElement)    .forEach($el.appendChild.bind($el));  return $el;}

哇,看起来不错。先把节点 props属性放到一边。待会再谈。我们不需要它们来理解虚拟 DOM 的基本概念,因为它们会增加复杂性。完整代码如下:

/** @jsx h */function h(type, props, ...children) {    return { type, props, children };}function createElement(node) {    if (typeof node === "string") {        return document.createTextNode(node);    }    const $el = document.createElement(node.type);    node.children.map(createElement).forEach($el.appendChild.bind($el));    return $el;}const a = (    <ul class="list">        <li>item 1li>        <li>item 2li>    ul>);const $root = document.getElementById("root");$root.appendChild(createElement(a));

比较两棵虚拟 DOM 树的差异

现在我们可以将虚拟 DOM 转换为真实的 DOM,这就需要考虑比较两棵 DOM 树的差异。基本的,我们需要一个算法来比较新的树和旧的树,它能够让我们知道什么地方改变了,然后相应的去改变真实的 DOM。怎么比较 DOM 树?需要处理下面的情况:

  • 添加新节点,使用 appendChild(…) 方法添加节点

  • 移除老节点,使用 removeChild(…) 方法移除老的节点

  • 节点的替换,使用 replaceChild(…) 方法

如果节点相同的——就需要需要深度比较子节点

编写一个名为 updateElement(…) 的函数,它接受三个参数—— $parent、newNode 和 oldNode,其中 \$parent 是虚拟节点的一个实际 DOM 元素的父元素。现在来看看如何处理上面描述的所有情况。

添加新节点

function updateElement($parent, newNode, oldNode) {    if (!oldNode) {        $parent.appendChild(createElement(newNode));    }}

移除老节点

这里遇到了一个问题——如果在新虚拟树的当前位置没有节点——我们应该从实际的 DOM 中删除它—— 这要如何做呢?如果我们已知父元素(通过参数传递),我们就能调用 $parent.removeChild(…)方法把变化映射到真实的 DOM 上。但前提是我们得知道我们的节点在父元素上的索引,我们才能通过 \$parent.childNodes[index] 得到该节点的引用。好的,让我们假设这个索引将被传递给 updateElement 函数(它确实会被传递——稍后将看到)。代码如下:

function updateElement($parent, newNode, oldNode, index = 0) {    if (!oldNode) {        $parent.appendChild(createElement(newNode));    } else if (!newNode) {        $parent.removeChild($parent.childNodes[index]);    }}

节点的替换

首先,需要编写一个函数来比较两个节点(旧节点和新节点),并告诉节点是否真的发生了变化。还有需要考虑这个节点可以是元素或是文本节点:

function changed(node1, node2) {  return typeof node1 !== typeof node2 ||         typeof node1 === ‘string’ && node1 !== node2 ||         node1.type !== node2.type}

现在,当前的节点有了 index 属性,就可以很简单的用新节点替换它:

function updateElement($parent, newNode, oldNode, index = 0) {    if (!oldNode) {        $parent.appendChild(createElement(newNode));    } else if (!newNode) {        $parent.removeChild($parent.childNodes[index]);    } else if (changed(newNode, oldNode)) {        $parent.replaceChild(createElement(newNode), $parent.childNodes[index]);    }}

比较子节点

最后,但并非最不重要的是——我们应该遍历这两个节点的每一个子节点并比较它们——实际上为每个节点调用updateElement(…)方法,同样需要用到递归。

  • 当节点是 DOM 元素时我们才需要比较( 文本节点没有子节点 )

  • 我们需要传递当前的节点的引用作为父节点

  • 我们应该一个一个的比较所有的子节点,即使它是 undefined 也没有关系,我们的函数也会正确处理它。

  • 最后是 index,它是子数组中子节点的 index

function updateElement($parent, newNode, oldNode, index = 0) {    if (!oldNode) {        $parent.appendChild(createElement(newNode));    } else if (!newNode) {        $parent.removeChild($parent.childNodes[index]);    } else if (changed(newNode, oldNode)) {        $parent.replaceChild(createElement(newNode), $parent.childNodes[index]);    } else if (newNode.type) {        const newLength = newNode.children.length;        const oldLength = oldNode.children.length;        for (let i = 0; i < newLength || i < oldLength; i++) {            updateElement(                $parent.childNodes[index],                newNode.children[i],                oldNode.children[i],                i            );        }    }}

完整的代码

Babel+JSX
/*_ @jsx h_ /

function h(type, props, ...children) {    return { type, props, children };}function createElement(node) {    if (typeof node === "string") {        return document.createTextNode(node);    }    const $el = document.createElement(node.type);    node.children.map(createElement).forEach($el.appendChild.bind($el));    return $el;}function changed(node1, node2) {    return (        typeof node1 !== typeof node2 ||        (typeof node1 === "string" && node1 !== node2) ||        node1.type !== node2.type    );}function updateElement($parent, newNode, oldNode, index = 0) {    if (!oldNode) {        $parent.appendChild(createElement(newNode));    } else if (!newNode) {        $parent.removeChild($parent.childNodes[index]);    } else if (changed(newNode, oldNode)) {        $parent.replaceChild(createElement(newNode), $parent.childNodes[index]);    } else if (newNode.type) {        const newLength = newNode.children.length;        const oldLength = oldNode.children.length;        for (let i = 0; i < newLength || i < oldLength; i++) {            updateElement(                $parent.childNodes[index],                newNode.children[i],                oldNode.children[i],                i            );        }    }}// ---------------------------------------------------------------------const a = (    <ul>        <li>item 1li>        <li>item 2li>    ul>);const b = (    <ul>        <li>item 1li>        <li>hello!li>    ul>);const $root = document.getElementById("root");const $reload = document.getElementById("reload");updateElement($root, a);$reload.addEventListener("click", () => {    updateElement($root, b, a);});

HTML

<button id="reload">RELOADbutton><div id="root">div>

CSS

#root {    border: 1px solid black;    padding: 10px;    margin: 30px 0 0 0;}

打开开发者工具,并观察当按下“Reload”按钮时应用的更改。

总结

现在我们已经编写了虚拟 DOM 实现及了解它的工作原理。作者希望,在阅读了本文之后,对理解虚拟 DOM 如何工作的基本概念以及在幕后如何进行响应有一定的了解。然而,这里有一些东西没有突出显示(将在以后的文章中介绍它们):

  • 设置元素属性(props)并进行 diffing/updating

  • 处理事件——向元素中添加事件监听

  • 让虚拟 DOM 与组件一起工作,比如 React

  • 获取对实际 DOM 节点的引用

  • 使用带有库的虚拟 DOM,这些库可以直接改变真实的 DOM,比如 jQuery 及其插件

虚拟dom_从0到1实现一个虚拟DOM相关推荐

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

    写在前面 本文分上下两篇,实现一个基础版本的虚拟 DOM. 上篇首先介绍什么是虚拟 DOM.为什么要使用虚拟 DOM,其次完成项目创建.实现 h 函数.render 函数以及 mount 函数,完成创 ...

  2. 虚拟服务器放2个网站,如何一个虚拟主机空间放两个网站的基本原理

    1.我们在说基本原理时为了更加清楚的解说,我问大家有没有用于站长统计.百度统计等流量监控软件. 2.他们的基本原来就是把用户访问网站的来源记录下来,我们就可以用这个原理来做一个虚拟主机放两个网站,以下 ...

  3. 虚拟dom_通过编写简易虚拟DOM,来学习虚拟DOM 的原理

    茫茫人海中与你相遇 相信未来的你不会很差 作者:前端小智 来源:https://segmentfault.com/a/1190000037599333 要构建自己的虚拟DOM,需要知道两件事.你甚至不 ...

  4. recycleviewitem 列表加载动画_用vue实现一个虚拟列表

    上次分享了超长列表分片加载的方式,这种方式现在一般不会使用,因为dom会变的无比庞大,页面会很卡,今天分享用vue实现一个虚拟列表的简易实现,本来是想用原生实现一个,后来觉得直接使用computed, ...

  5. Ubuntu18.04 LTS Melodic ROS 教程:使用URDF创建一个虚拟机器人模型

    Ubuntu18.04 LTS Melodic ROS 教程:使用URDF创建一个虚拟机器人模型 Building a Visual Robot Model with URDF from Scratc ...

  6. diff算法_[VUE学习]徒手撸一个虚拟dom+diff算法

    首先先来聊一下,什么是虚拟dom? 为什么要使用虚拟dom? 其实答案很简单,虚拟dom就是位于js层和html层之间的一个层,使用js模拟出一个dom树,然后通过diff算法,来侦测到dom发生更改 ...

  7. 32驱动_轻松掌握pinctrl子系统驱动开发——一个虚拟pinctrl dev驱动开发

    这周主要对pinctrl子系统进行分析,该分析的基本上已经分析完成,唯一没有细说的估计就是gpio与pinctrl之间的关联了.本章即是pinctrl子系统分析的最后一章,本章我们主要实现一个虚拟的p ...

  8. STM32驱动开发(二)--USB Device RNDIS虚拟网卡(USB2.0 基础概念讲解)

    STM32驱动开发(二)–USB Device RNDIS虚拟网卡(USB2.0基础概念讲解) 一.简介   本文基于stm32 Rndis实例,github开源, 使用STM32F407单板.结合协 ...

  9. 面试官:什么是虚拟DOM?如何实现一个虚拟DOM?

    故心故心故心故心小故冲啊 文章目录 一.什么是虚拟DOM 二.为什么需要虚拟DOM 三.如何实现虚拟DOM 小结 参考文献 一.什么是虚拟DOM 虚拟 DOM (Virtual DOM )这个概念相信 ...

最新文章

  1. WEB中get请求和post请求的区别
  2. git 移动分支指针_git 分支( branch ) 的基本使用
  3. python如何调用日期函数_当前时间等于另一个时间时如何使用函数:Python
  4. Perl用LWP实现GET/POST数据发送
  5. 60后即将退休的人,有多少存款就算富有了?
  6. get与post在技术上的区别
  7. python中oserror winerror,在python中将WindowsError转换为OSError
  8. 【流放之路召唤开荒攻略】
  9. 【原创】JQWidgets-TreeGrid 2、初探源码
  10. 老男孩教育Python课程内容
  11. CodeForces 961G Partitions 题解
  12. abp 使用mysql_ABP Vnext使用mysql数据库
  13. 语雀可以导出html吗,Confluence 迁移到语雀
  14. NoSQL数据库之Redis(三):常用五大数据类型
  15. 洗料系列-杂谈篇-麻将自动化---第一章、麻将基础入门
  16. 重庆邮电大学计算机2019湖北分数线,2019重庆邮电大学录取分数线及历年专业分数线统计表【文科 理科】...
  17. 四年级学生学情分析计算机,四年级小学信息技术教学计划
  18. P315 GCD等于XOR UVa12176 “不难发现”的解释 以及完整推导过程
  19. implementation和compile的区别
  20. dnf如何查询服务器角色信息,怎么查看网易账号下所有游戏角色

热门文章

  1. 【剑指offer】面试题38:字符串的排列(Java)
  2. c语言多个子函数声明,C函数在多个源文件中的声明和定义
  3. 设置目录_[LaTeX 尝试] titletoc - 为算法目录、定理目录等设置子目录
  4. feign 整合sentinel_SpringCloud实战五-Sentinel上
  5. ssrf漏洞 php,DokuWiki fetch.php SSRF漏洞与tok安全验证绕过分析
  6. C语言bound函数,C/C++-STL中lower_bound与upper_bound的用法以及cmp函数
  7. float php 运算_系统的讲解 - PHP 浮点数高精度运算
  8. extjs中store获取后台数据_Excel实战技巧64: 从工作簿中获取数据(不使用VBA)
  9. C++数组指针不能自增1/自减1
  10. ffmpy3与ffmpeg的简单使用