虚拟 DOM 的好处
虚拟 DOM
就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有 10 次更新 DOM
的动作,虚拟 DOM
不会立即操作 DOM
,而是将这 10 次更新的 diff
内容保存到本地一个 JS
对象中,最终将这个 JS
对象一次性 attch
到 DOM
树上,再进行后续操作,避免大量无谓的计算量。所以,用 JS
对象模拟 DOM
节点的好处是,页面的更新可以先全部反映在 JS
对象(虚拟 DOM
)上,操作内存中的 JS
对象的速度显然要更快,等更新完成后,再将最终的 JS
对象映射成真实的 DOM
,交由浏览器去绘制。
虽然这一个虚拟 DOM 带来的一个优势,但并不是全部。虚拟 DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓和 IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI。
回到最开始的问题,虚拟 DOM 到底是什么,说简单点,就是一个普通的 JavaScript 对象,包含了 tag
、props
、children
三个属性。
接下来我们手动实现下 虚拟 DOM。
分两种实现方式:
一种原生 js DOM 操作实现;
另一种主流虚拟 DOM 库(snabbdom、virtual-dom)的实现(用h函数渲染)(暂时还不理解)
算法实现
(1) 用 JS 对象模拟 DOM 树:
<div id="virtual-dom"> <p>Virtual DOM</p> <ul id="list"> <li class="item">Item 1</li> <li class="item">Item 2</li> <li class="item">Item 3</li> </ul> <div>Hello World</div> </div>
我们用 JavaScript
对象来表示 DOM
节点,使用对象的属性记录节点的类型、属性、子节点等。
/** * Element virdual-dom 对象定义 * @param {String} tagName - dom 元素名称 * @param {Object} props - dom 属性 * @param {Array<Element|String>} - 子节点 */ function Element(tagName, props, children) { this.tagName = tagName; this.props = props; this.children = children; // dom 元素的 key 值,用作唯一标识符 if (props.key) { this.key = props.key } } function el(tagName, props, children) { return new Element(tagName, props, children); }
构建虚拟的 DOM
,用 javascript 对象来表示
let ul = el('div', { id: 'Virtual DOM' }, [ el('p', {}, ['Virtual DOM']), el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 1']), el('li', { class: 'item' }, ['Item 2']), el('li', { class: 'item' }, ['Item 3']) ]), el('div', {}, ['Hello, World']) ])
现在 ul
就是我们用 JavaScript
对象表示的 DOM
结构,我们输出查看 ul
对应的数据结构如下:
(2) 将用 js 对象表示的虚拟 DOM 转换成真实 DOM:需要用到 js 原生操作 DOM 的方法。
/** * render 将virdual-dom 对象渲染为实际 DOM 元素 */ Element.prototype.render = function () { // 创建节点 let el = document.createElement(this.tagName); let props = this.props; // 设置节点的 DOM 属性 for (let propName in props) { let propValue = props[propName]; el.setAttribute(propName, propValue) } let children = this.children || [] for (let child of children) { let childEl = (child instanceof Element) ? child.render() // 如果子节点也是虚拟 DOM, 递归构建 DOM 节点 : document.createTextNode(child) // 如果是文本,就构建文本节点 el.appendChild(childEl); } return el; }
我们通过查看以上 render
方法,会根据 tagName
构建一个真正的 DOM
节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。
我们将构建好的 DOM
结构添加到页面 body
上面,如下:
let ulRoot = ul.render(); document.body.appendChild(ulRoot);
这样,页面 body
里面就有真正的 DOM
结构,效果如下图所示:
我们知道虚拟 DOM 的好处和虚拟 DOM 的实现后就要讲讲 key 的作用了。
贴一下上面实现地完整代码
<script> /** * Element virdual-dom 对象定义 * @param {String} tagName - dom 元素名称 * @param {Object} props - dom 属性 * @param {Array<Element|String>} - 子节点 */ function Element(tagName, props, children) { this.tagName = tagName; this.props = props; this.children = children; // dom 元素的 key 值,用作唯一标识符 if (props.key) { this.key = props.key } } function el(tagName, props, children) { return new Element(tagName, props, children); } let ul = el('div', { id: 'Virtual DOM' }, [ el('p', {}, ['Virtual DOM']), el('ul', { id: 'list' }, [ el('li', { class: 'item' }, ['Item 1']), el('li', { class: 'item' }, ['Item 2']), el('li', { class: 'item' }, ['Item 3']) ]), el('div', {}, ['Hello, World']) ]) /** * render 将virdual-dom 对象渲染为实际 DOM 元素 */ Element.prototype.render = function () { // 创建节点 let el = document.createElement(this.tagName); let props = this.props; // 设置节点的 DOM 属性 for (let propName in props) { let propValue = props[propName]; el.setAttribute(propName, propValue) } let children = this.children || [] for (let child of children) { let childEl = (child instanceof Element) ? child.render() // 如果子节点也是虚拟 DOM, 递归构建 DOM 节点 : document.createTextNode(child) // 如果是文本,就构建文本节点 el.appendChild(childEl); } return el; } let ulRoot = ul.render(); document.body.appendChild(ulRoot); console.log(ul); </script>
虚拟DOM中key的作用
key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】, 随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下:
- 旧虚拟DOM中找到了与新虚拟DOM相同的key:
- ①.若虚拟DOM中内容没变, 直接使用之前的真实DOM!
- ②.若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。
- 旧虚拟DOM中未找到与新虚拟DOM相同的key
- 创建新的真实DOM,随后渲染到到页面。