茫茫人海中与你相遇

相信未来的你不会很差

作者:前端小智

来源:https://segmentfault.com/a/1190000037599333

要构建自己的虚拟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 就开始编译。'它实际上告诉 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 节点。这里先不考虑 propschildren属性:

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(…) 的函数,它接受三个参数—— $parentnewNodeoldNode,其中 $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 及其插件

我们在虚拟的空间与你相遇,期待可以碰撞出不一样的火花

公众号ID:前端大联盟扫码关注最新动态

虚拟dom_通过编写简易虚拟DOM,来学习虚拟DOM 的原理相关推荐

  1. 从零写一个具有IOC-AOP-MVC功能的框架---学习笔记---11. MVC功能之http请求处理器的编写---简易框架最后一公里!

    从零写一个具有IOC-AOP-MVC功能的框架-学习笔记 专栏往期文章链接: IOC功能相关章节: 从零写一个具有IOC-AOP-MVC功能的框架-学习笔记-01.项目初始化 从零写一个具有IOC-A ...

  2. JavaScript高级(BOM+DOM)学习

    文章目录 1.DOM简单学习 2.事件简单学习 3.点灯开关练习 4.BOM 4.1 BOM的概念 4.2 BOM的功能 4.3 BOM对象 4.4 Window :窗口对象 4.5 Location ...

  3. 虚拟dom添加虚拟dom_虚拟DOM缓慢。 认识记忆化的DOM

    虚拟dom添加虚拟dom by Sindre Osen Aarsaether 通过Sindre Osen Aarsaether 虚拟DOM缓慢. 符合已记忆的DOM. (The Virtual DOM ...

  4. 虚拟dom_从0到1实现一个虚拟DOM

    来源 | https://segmentfault.com/a/1190000021331850要构建自己的虚拟 DOM,需要知道两件事.你甚至不需要深入 React 的源代码或者深入任何其他虚拟 D ...

  5. 虚拟dom_虚拟DOM与dom diff

    什么是虚拟DOM 虚拟DOM是一颗以JavaScript对象(node节点)作为基础的树,用对象属性来描述节点,他是对真实DOM的抽象,通过一些列操作使这棵树映射到真实环境上 虚拟dom就是能代表DO ...

  6. Vue 源码学习—Virtual DOM(虚拟 DOM)

    Virtual DOM Virtual DOM是什么 真实DOM成员 引入原因 作用 Virtual DOM是什么 定义: 普通的js对象来描述DOM对象,不是真实的DOM,顾名思义,称为虚拟DOM ...

  7. 虚拟DOM(VNode Virtul DOM )

    目录 分析 思路 回答范例 一.前言 二.render函数 三.render函数应用 四.深入数据对象 六.总结 备注 分析 现有框架几乎都引入了虚拟 DOM 来对真实 DOM 进行抽象,也就是现在大 ...

  8. 构建一个虚拟DOM并转换为真实DOM

    关于真实DOM与虚拟DOM 1.在学习虚拟DOM之前,让我们先来了解一下真实的DOM结构,这里不得不提的是关于浏览器渲染方面的知识. 当浏览器拿到一个HTML文件,首先会根据HTML文件构建出一个DO ...

  9. Vue深入学习—虚拟DOM和Diff算法

    1.snabbdom 是什么? snabbdom是"速度"的意思,源码只有200行,使用TS写的,让东西变得模块化 2.snabbdom 的 h 函数如何工作? h函数用于产生虚拟 ...

最新文章

  1. GDCM:Torture的测试程序
  2. d3.js 入门指南
  3. GIS实战应用案例100篇(三)-基于NDVI指数的绿地信息提取
  4. layuimini 图标颜色便捷指定
  5. python如何下载pandas、时间延长_大pandas,python – 如何在时间表中选择具体时间
  6. io 错误: socket closed_Tomcat NIO(9)IO线程Overall流程和关键类
  7. 那些相见恨晚的 JavaScript 技巧
  8. 将labelme工具生产的json文件批量转dataset
  9. php 数组添加键值对,php怎么为数组增加键值对
  10. python名词解释题库_Python题库
  11. ARM体系结构与编程模型总结
  12. 如何学习摸具设计?怎样学习摸具设计?学习摸具设计的方法。
  13. 墨画子卿第一章第9节:指教
  14. wp文章增加部分内容隐藏功能 -- 微信公众号吸粉
  15. 通过CMD命令查找产品密钥
  16. 损失函数大全Cross Entropy Loss/Weighted Loss/Focal Loss/Dice Soft Loss/Soft IoU Loss
  17. 这个彬彬就是逊啦—才搞懂小学知识求最小公倍数
  18. 解决oracle异常ORA-01012: not logged on
  19. QGIS 加载XYZ Tiles
  20. datagrip jdbc驱动_DataGrip连接MySQL数据库

热门文章

  1. 如何用 Nginx 在公网上搭建加密数据通道
  2. 安全起见,这款 IDEA 插件赶紧删了吧!
  3. 限时领取!阿里Java面试复盘手册.pdf
  4. 每日一皮:2020年度最佳微小说...
  5. Spring Cloud 2020 年路线图:二季度Spring Boot 2.3、四季度Spring Cloud Ilford
  6. Spring Boot构建多租户SaaS平台核心技术指南
  7. 死磕Java并发:Java内存模型之总结
  8. Spring Cloud构建微服务架构:分布式服务跟踪(整合logstash)【Dalston版】
  9. linux 并行计算命令,Linux下的并行神器——parallel
  10. excel大学计算机试题,大学计算机基础 excel测试题 求答案~~喵~~