如何编写你自己的 Virtual DOM

来源:互联网 发布:linux如何安装bind 编辑:程序博客网 时间:2024/05/17 03:45

如何编写你自己的 Virtual DOM

本文转载自:众成翻译
译者:yanni4night
链接:http://www.zcfy.cc/article/1136
原文:https://medium.com/@deathmood/how-to-write-your-own-virtual-dom-ee74acc13060

为了构建你自己的 Virtual DOM,你只需要知道两件事,甚至你都不必深入 React 或者其它 Virtual DOM 实现的源码。因为它们都太庞大和复杂了 —— 但是实际上 Virtual DOM 的主要部分可以用少于 50 行代码实现。50 行!!!

两个概念:

  • Virtual DOM 是真实 DOM 的任意一种表达形式;
  • 在 Virtual DOM 树上的改动,会创建一个新的 Virtual DOM 树。比较新老 Virtual DOM 树的算法,会计算差异并对真实 DOM 进行最小的更改,所谓“虚拟”

就是这些,让我们深挖每个概念的含义。

更新:关于 Virtual DOM 中设置属性和事件的第二篇文章在这里。

描述 DOM 树

首先,我们需要以某种方式在内存中存储 DOM 树。可以利用纯 JavaScript 对象实现。假如我们有这样一棵树:

<ul class=”list”>  <li>item 1</li>  <li>item 2</li></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 的文本节点

但是以这种方式写大型的树是非常困难的。所以我们来写一个帮助函数,使得理解这个结构更容易一些:

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

现在向树中写入数据是这样的:

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

看起来清晰多了,是不是?我们更进一步。你听说过 JSX,对么?嗯,我也要实现它。那么它是如何工作的呢?

如果你阅读过 Babel 的官方 JSX 文档,你会知道,Babel 把下面的代码:

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

转译成:

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

注意到相似点了么?对对对,如果我们把 React.createElement(…) 替换成我们的 h(…) 就好了 —— 我们确实可以使用所谓的 jsx 编译指令 做到这一点。只要在源码的开头放一行像注释的东西:

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

这一行实际上告诉 Babel:嘿,用 h 而不是 React.createElement 来编译 jsx。你可以将 h 替换成任何东西,都会被编译。

因此,总结上面我所说的来看,我们会以下面的形式写 DOM:

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

Babel 会把它转译成:

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

当函数 h 被执行时,它会返回纯 JS 对象 —— 我们的 Virtual DOM 表示形式:

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

JSFiddle

应用 DOM 表达形式

Ok,现在我们有了纯 JS 对象以及自己结构的 DOM 树表达形式。非常酷,但是我们得利用它创建一个真实的 DOM。毕竟我们不能直接把表达式写入 DOM。

首先我们先进行一系列假设并设定一些术语:

  • 我会用 $ 开头的变量代表真实 DOM 节点(元素以及文本),那么 $parent 就是一个真实 DOM 元素;
  • Virtual DOM 表达形式存储于变量 node 中;
  • 像 React 一样,你可以只有一个根节点 —— 其它都是其后代节点

Ok,如前所述,我们写一个函数 createElement(…) 把虚拟 DOM 节点转换成真实 DOM 节点。暂时忘记 propschildren —— 过后再说:

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

因为我们已经有了纯 JS 字符串表示的文本节点和像下面的以 JS 对象表示的元素

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

因此,我们在这里既可以处理虚拟文本节点也可以处理虚拟元素节点。

现在我们来考虑 children —— 每一个要么是一个文本节点要么是一个元素。所以他们都可以用我们的 createElement(…) 函数来创建。啊…你感到了么?我感受到了递归 :)) 于是我们在 children 的每一个元素上调用 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 放一放,过后再讨论它,因为理解基本的 Virtual DOM 概念不需要它们,只会徒增复杂性。

JSFiddle

处理更新

Ok,现在我们能够把虚拟 DOM 转换为真实 DOM,到了该比较虚拟树差异的时候了。基本上我们要写个算法,比较两棵新旧树的差异,并对真实 DOM 做最少必要的更新。

如何比较树的差异?我们需要处理下面几个问题:

  • 某个位置有新节点 —— 因此节点是被增加的,我们需要 appendChild(…) 它;

  • 某个位置有旧节点 —— 因此节点是被删除的,我们需要 removeChild(…) 它;

  • 某个位置有不同的节点 —— 节点被更新,我们需要 replaceChild(…) 它;

  • 节点是相同的,我需要到下一层比较子节点

Ok,我们写一个函数 updateElement(…),输入 3 个参数,parent_**, **_newNode_** and **_oldNode_**,其中 **_parent 是我们的虚拟节点的对应的真实节点的父节点。现在看看我们如何处理上面提到的问题。

有一个新节点

相当简单了,都不必注释:

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

有旧节点

这里有问题了 —— 如果在 Virtual DOM 树的当前位置没有节点 —— 我们应该从真实 DOM 树中移除它 —— 但是我们如果做到?是的,我们知道父元素(传给函数了),于是,我们该调用 parent.removeChild(…)_** 并传入真实的 DOM 元素引用。但是我们并没有这个引用。如果知道在父元素中的位置的话,我们则可以用 **_parent.childNodes[index] 获取引用,这里 index 是索引:

假设这个 index 被传入了我们的函数(后面会看到,确实被传入了)。所以我们的代码是:

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(…)。对,又是递归。

在编写代码之前,有一些事情还需要考虑:

  • 我们只会比较元素的子节点(文本没有子元素);
  • 现在我们把当前节点的引用作为父节点;
  • 我们应该一个一个地比较所有子节点 —— 即使遇到 undefined,没关系,我们的函数能处理它;
  • 最后 index —— 它只是子节点在 children 中的索引
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      );    }  }}

综合

好了,我们已经完成了任务,我把所有的代码放到了 JSFiddle,实现部分确实使用了 50 行代码,亦如我承诺你的那样。去玩玩它吧。

打开开发者工具,在你按下 Reload 按钮后观察应用的更新。

总结

恭喜你!我们达到了目的,实现了自己的 Virtual DOM,并且能正常工作。我希望在阅读完这篇文章后,你已经对 Virtual DOM 是如何工作的、React 的内部机制有了基本的了解。

然而,这里我们有些事情没有强调(我会在未来的文章中涉及到):

  • 设置元素属性并且比较或更新它们;
  • 处理事件 —— 为元素增加事件;
  • 让 Virtual DOM 和组件一起工作,像 React 那样;
  • 获取到真实 DOM 节点的引用;
  • 让 Virtual DOM 与直接操作 DOM 的库一同工作,如 jQuery 极其插件;
  • 其它…

P.S.

如果在代码或文字中有任何错误,或者代码可以有的任何优化,请在评论中指出 :) 另外,对于我的英语我很抱歉 :)

更新:关于 Virtual DOM 中设置属性和事件的第二篇文章在这里。

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 小米平板电脑锁屏密码忘了怎么办 小米手机进水了黑屏了嗡嗡响怎么办 华为诺娃2手机声音小怎么办 华为平板锁屏密码忘记了怎么办 华为麦芒6手机按键摔坏了怎么办 定频空调加错了佛里昂怎么办 定频空调外机噪音大怎么办 老美的定频空调出现p0怎么办 华为揽阅m2青春版卡顿了怎么办 华为揽阅M2青春版发热卡顿怎么办 全民突击网速不给力经常掉线怎么办 华为手机微信视频黑屏了怎么办 相机拍照后找不到拍的照片怎么办 苹果手机下载软件不被信任怎么办 苹果x手机下载软件不受信任怎么办 华为畅享7plus主板坏了怎么办 华为手机手机主板坏了没备份怎么办 华为手机一年内主板坏了怎么办 华为手机保修期内主板坏了怎么办 华为手机外置sd卡满了怎么办 红米4充不进去电怎么办 苹果5s锁屏密码忘记了怎么办 华为手机桌面和锁屏自动一样怎么办 苹果手机没电了没带充电器怎么办 华为p8手机后摄像头模糊的怎么办 中兴手机充电的地方坏了怎么办? 小米手机与电脑蓝牙传输失败怎么办 捡个华为收机没有账号密码怎么办 华为手机p9激活码忘了怎么办 华为畅享7plus声音小怎么办 华为畅享7plus忘记密码怎么办 华为畅享8plus卡顿怎么办 华为畅享7plus卡机怎么办 华为畅享8plus图标字小怎么办 华为畅享6反应慢发热怎么办 华为畅享5S反应迟钝怎么办 华为畅玩5x玩王者荣耀卡怎么办 不小心把手机里的照片删了怎么办 u盘文件在手机上删了怎么办 荒野行动透视挂功能加载失败怎么办 白色t桖衫被奶茶弄脏了该怎么办