如何编写你自己的 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 节点。暂时忘记 props
和 children
—— 过后再说:
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 中设置属性和事件的第二篇文章在这里。
- 如何编写你自己的 Virtual DOM
- 如何编写你自己的编译器
- 编写你自己的单点登陆
- 编写你自己的Mutex类
- 编写你自己的jquery插件
- 服务程序编写-------详细介绍如何构建你自己的服务程序(上)
- 如何实现一个 Virtual DOM 算法
- 如何实现一个 Virtual DOM 算法
- 如何定制你自己的DataGrid
- 如何定制你自己的jQuery
- 如何测试你自己的 RubyGem?
- 如何创建你自己的Framework
- 如何改进你自己的CNN?
- 用C编写你自己的php扩展
- day2:如何实现一个Virtual DOM算法 和 MVVM、MV*等模式的学习
- 基于React的Virtual-dom的学习
- 如何选购你自己的蓝牙适配器
- 如何定义你自己的JavaScript类
- CentOS6.5离线安装mysql遇到的几个问题
- Leetcode 93 Restore IP Addresses
- Android实战——改造PullToRefresh下拉刷新和上拉加载
- openwrt hotplug
- 深剖VR,AR和MR三者之间关系
- 如何编写你自己的 Virtual DOM
- Chrome调试前端页面的若干技巧
- 单链表的实现(部分)
- CTF之常见的加密
- AbstractFactory_Level6
- mybatis入门
- 大连首闻grid二次开发增强文档
- HDU - 1032 The 3n + 1 problem
- 轮播图片