热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

如何编写自己的虚拟DOM?方法介绍

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

有两个概念:

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

用JS对象模拟DOM树

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

  • item 1
  • item 2

看起来很简单,对吧? 如何用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,如下:

  • item 1
  • item 2

编译成:

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

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

/** @jsx h */
  • item 1
  • item 2

它实际上告诉 Babel ' 嘿,小老弟帮我编译 JSX 语法,用 h(...) 函数代替 React.createElement(…),然后 Babel 就开始编译。'

综上所述,我们将DOM写成这样:

/** @jsx h */
const a = (
  
  • item 1
  • item 2
);

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 = (
  
  • item 1
  • item 2
); const $root = document.getElementById('root'); $root.appendChild(createElement(a));

比较两棵虚拟DOM树的差异

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

怎么比较 DOM 树?需要处理下面的情况:

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

编写一个名为 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 

完整的代码

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 
    
  • item 1
  • item 2
  • ); const b = (
    • item 1
    • hello!
    ); const $root = document.getElementById('root'); const $reload = document.getElementById('reload'); updateElement($root, a); $reload.addEventListener('click', () => { updateElement($root, b, a); });

    HTML

    
    

    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 及其插件

    原文地址:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060

    作者:deathmood

    为了保证的可读性,本文采用意译而非直译。

    更多编程相关知识,请访问:编程入门!!

    以上就是如何编写自己的虚拟DOM?方法介绍的详细内容,更多请关注 第一PHP社区 其它相关文章!


    推荐阅读
    • CSS3选择器的使用方法详解,提高Web开发效率和精准度
      本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
    • Voicewo在线语音识别转换jQuery插件的特点和示例
      本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
    • 《数据结构》学习笔记3——串匹配算法性能评估
      本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
    • 动态规划算法的基本步骤及最长递增子序列问题详解
      本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
    • 本文介绍了Java的集合及其实现类,包括数据结构、抽象类和具体实现类的关系,详细介绍了List接口及其实现类ArrayList的基本操作和特点。文章通过提供相关参考文档和链接,帮助读者更好地理解和使用Java的集合类。 ... [详细]
    • Java验证码——kaptcha的使用配置及样式
      本文介绍了如何使用kaptcha库来实现Java验证码的配置和样式设置,包括pom.xml的依赖配置和web.xml中servlet的配置。 ... [详细]
    • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
    • 基于layUI的图片上传前预览功能的2种实现方式
      本文介绍了基于layUI的图片上传前预览功能的两种实现方式:一种是使用blob+FileReader,另一种是使用layUI自带的参数。通过选择文件后点击文件名,在页面中间弹窗内预览图片。其中,layUI自带的参数实现了图片预览功能。该功能依赖于layUI的上传模块,并使用了blob和FileReader来读取本地文件并获取图像的base64编码。点击文件名时会执行See()函数。摘要长度为169字。 ... [详细]
    • Java实战之电影在线观看系统的实现
      本文介绍了Java实战之电影在线观看系统的实现过程。首先对项目进行了简述,然后展示了系统的效果图。接着介绍了系统的核心代码,包括后台用户管理控制器、电影管理控制器和前台电影控制器。最后对项目的环境配置和使用的技术进行了说明,包括JSP、Spring、SpringMVC、MyBatis、html、css、JavaScript、JQuery、Ajax、layui和maven等。 ... [详细]
    • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
    • 后台获取视图对应的字符串
      1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
    • 本文介绍了通过ABAP开发往外网发邮件的需求,并提供了配置和代码整理的资料。其中包括了配置SAP邮件服务器的步骤和ABAP写发送邮件代码的过程。通过RZ10配置参数和icm/server_port_1的设定,可以实现向Sap User和外部邮件发送邮件的功能。希望对需要的开发人员有帮助。摘要长度:184字。 ... [详细]
    • 高质量SQL书写的30条建议
      本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
    • 本文介绍了指针的概念以及在函数调用时使用指针作为参数的情况。指针存放的是变量的地址,通过指针可以修改指针所指的变量的值。然而,如果想要修改指针的指向,就需要使用指针的引用。文章还通过一个简单的示例代码解释了指针的引用的使用方法,并思考了在修改指针的指向后,取指针的输出结果。 ... [详细]
    • 在project.properties添加#Projecttarget.targetandroid-19android.library.reference.1..Sliding ... [详细]
    author-avatar
    竹茹士弘宜来
    这个家伙很懒,什么也没留下!
    PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
    Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有