广告:宝塔Linux面板高效运维的服务器管理软件 点击【 https://www.bt.cn/p/uNLv1L 】立即购买

虚拟DOM技术使得我们的页面渲染的效率更高,减轻了节点的操作从而提高性能。本篇文章带大家深入解析一下vue中 Virtual DOM的技术原理和 Vue 框架的具体实现。(学习视频分享:vue视频教程)
DOM和其解析流程 本节我们主要介绍真实 DOM 的解析过程,通过介绍其解析过程以及存在的问题,从而引出为什么需要虚拟DOM。一图胜千言,如下图为 webkit 渲染引擎工作流程图
所有的浏览器渲染引擎工作流程大致分为5步:创建 DOM 树 —> 创建 Style Rules -> 构建 Render 树 —> 布局 Layout -—> 绘制 Painting。
注意点:
1、DOM 树的构建是文档加载完成开始的? 构建 DOM 树是一个渐进过程,为达到更好的用户体验,渲染引擎会尽快将内容显示在屏幕上,它不必等到整个 HTML 文档解析完成之后才开始构建 render 树和布局。
2、Render 树是 DOM 树和 CSS 样式表构建完毕后才开始构建的? 这三个过程在实际进行的时候并不是完全独立的,而是会有交叉,会一边加载,一边解析,以及一边渲染。
3、CSS 的解析注意点? CSS 的解析是从右往左逆向解析的,嵌套标签越多,解析越慢。
4、JS 操作真实 DOM 的代价? 用我们传统的开发模式,原生 JS 或 JQ 操作 DOM 时,浏览器会从构建 DOM 树开始从头到尾执行一遍流程。在一次操作中,我需要更新 10 个 DOM 节点,浏览器收到第一个 DOM 请求后并不知道还有 9 次更新操作,因此会马上执行流程,最终执行10 次。例如,第一次计算完,紧接着下一个 DOM 更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算 DOM 节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作 DOM 的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验
Virtual-DOM 基础2.1、虚拟 DOM 的好处 虚拟 DOM 就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有 10 次更新 DOM 的动作,虚拟 DOM 不会立即操作 DOM,而是将这 10 次更新的 diff 内容保存到本地一个 JS 对象中,最终将这个 JS 对象一次性 attch 到 DOM 树上,再进行后续操作,避免大量无谓的计算量。所以,用 JS 对象模拟 DOM 节点的好处是,页面的更新可以先全部反映在 JS 对象(虚拟 DOM )上,操作内存中的 JS 对象的速度显然要更快,等更新完成后,再将最终的 JS 对象映射成真实的 DOM,交由浏览器去绘制。
JS 对象模拟 DOM 树(1)如何用 JS 对象模拟 DOM 树
例如一个真实的 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.js 中表示节点对象代码如下:
/** * 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 } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素个数 this.count = count}function createElement(tagName, props, children){ return new Element(tagName, props, children);}module.exports = createElement;登录后复制根据 element 对象的设定,则上面的 DOM 结构就可以简单表示为:
var el = require("./element.js");var 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 对象
但是页面上并没有这个结构,下一步我们介绍如何将 ul 渲染成页面上真实的 DOM 结构,相关渲染函数如下:
/** * render 将virdual-dom 对象渲染为实际 DOM 元素 */Element.prototype.render = function () { var el = document.createElement(this.tagName) var props = this.props // 设置节点的DOM属性 for (var propName in props) { var propValue = props[propName] el.setAttribute(propName, propValue) } var children = this.children || [] children.forEach(function (child) { var childEl = (child instanceof Element) ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点 : document.createTextNode(child) // 如果字符串,只构建文本节点 el.appendChild(childEl) }) return el}登录后复制我们通过查看以上 render 方法,会根据 tagName 构建一个真正的 DOM 节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。
我们将构建好的 DOM 结构添加到页面 body 上面,如下:
ulRoot = ul.render();document.body.appendChild(ulRoot);登录后复制
这样,页面 body 里面就有真正的 DOM 结构,效果如下图所示:
DOM 树的差异 — diff 算法diff 算法用来比较两棵 Virtual DOM 树的差异,如果需要两棵树的完全比较,那么 diff 算法的时间复杂度为O(n^3)。但是在前端当中,你很少会跨越层级地移动 DOM 元素,所以 Virtual DOM 只会对同一个层级的元素进行对比,如下图所示, div 只会和同一层级的 div 对比,第二层级的只会跟第二层级对比,这样算法复杂度就可以达到 O(n)。
(1)深度优先遍历,记录差异
在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:
在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。
// diff 函数,对比两棵树function diff(oldTree, newTree) { var index = 0 // 当前节点的标志 var patches = {} // 用来记录每个节点差异的对象 dfsWalk(oldTree, newTree, index, patches) return patches}// 对两棵树进行深度优先遍历function dfsWalk(oldNode, newNode, index, patches) { var currentPatch = [] if (typeof (oldNode) === "string" && typeof (newNode) === "string") { // 文本内容改变 if (newNode !== oldNode) { currentPatch.push({ type: patch.TEXT, content: newNode }) } } else if (newNode!=null && oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) { // 节点相同,比较属性 var propsPatches = diffProps(oldNode, newNode) if (propsPatches) { currentPatch.push({ type: patch.PROPS, props: propsPatches }) } // 比较子节点,如果子节点有'ignore'属性,则不需要比较 if (!isIgnoreChildren(newNode)) { diffChildren( oldNode.children, newNode.children, index, patches, currentPatch ) } } else if(newNode !== null){ // 新节点和旧节点不同,用 replace 替换 currentPatch.push({ type: patch.REPLACE, node: newNode }) } if (currentPatch.length) { patches[index] = currentPatch }}登录后复制从以上可以得出,patches[1] 表示 p ,patches[3] 表示 ul ,以此类推。
(2)差异类型
DOM 操作导致的差异类型包括以下几种:
div 换成 h1;顺序互换:移动、删除、新增子节点,例如上面 div 的子节点,把 p 和 ul 顺序互换;属性更改:修改了节点的属性,例如把上面 li 的 class 样式类删除;文本改变:改变文本节点的文本内容,例如将上面 p 节点的文本内容更改为 “Real Dom”;以上描述的几种差异类型在代码中定义如下所示:
var REPLACE = 0 // 替换原先的节点var REORDER = 1 // 重新排序var PROPS = 2 // 修改了节点的属性var TEXT = 3 // 文本内容改变登录后复制
(3)列表对比算法
子节点的对比算法,例如 p, ul, div 的顺序换成了 div, p, ul。这个该怎么对比?如果按照同层级进行顺序对比的话,它们都会被替换掉。如 p 和 div 的 tagName 不同,p 会被 div 所替代。最终,三个节点都会被替换,这样 DOM 开销就非常大。而实际上是不需要替换节点,而只需要经过节点移动就可以达到,我们只需知道怎么进行移动。
将这个问题抽象出来其实就是字符串的最小编辑距离问题(Edition Distance),最常见的解决方法是 Levenshtein Distance , Levenshtein Distance 是一个度量两个字符序列之间差异的字符串度量标准,两个单词之间的 Levenshtein Distance 是将一个单词转换为另一个单词所需的单字符编辑(插入、删除或替换)的最小数量。Levenshtein Distance 是1965年由苏联数学家 Vladimir Levenshtein 发明的。Levenshtein Distance 也被称为编辑距离(Edit Distance),通过动态规划求解,时间复杂度为 O(M*N)。
定义:对于两个字符串 a、b,则他们的 Levenshtein Distance 为:
示例:字符串 a 和 b,a=“abcde” ,b=“cabef”,根据上面给出的计算公式,则他们的 Levenshtein Distance 的计算过程如下:
本文的 demo 使用插件 list-diff2 算法进行比较,该算法的时间复杂度伟 O(n*m),虽然该算法并非最优的算法,但是用于对于 dom 元素的常规操作是足够的。
该算法具体的实现过程这里不再详细介绍,该算法的具体介绍可以参照:https://github.com/livoras/list-diff
(4)实例输出
两个虚拟 DOM 对象如下图所示,其中 ul1 表示原有的虚拟 DOM 树,ul2 表示改变后的虚拟 DOM 树
var ul1 = 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'])]) var ul2 = el('div',{id:'virtual-dom'},[ el('p',{},['Virtual DOM']), el('ul', { id: 'list' }, [el('li', { class: 'item' }, ['Item 21']),el('li', { class: 'item' }, ['Item 23']) ]), el('p',{},['Hello World'])]) var patches = diff(ul1,ul2);console.log('patches:',patches);登录后复制我们查看输出的两个虚拟 DOM 对象之间的差异对象如下图所示,我们能通过差异对象得到,两个虚拟 DOM 对象之间进行了哪些变化,从而根据这个差异对象(patches)更改原先的真实 DOM 结构,从而将页面的 DOM 结构进行更改。
DOM 对象的差异应用到真正的 DOM 树(1)深度优先遍历 DOM 树
因为步骤一所构建的 JavaScript 对象树和 render 出来真正的 DOM 树的信息、结构是一样的。所以我们可以对那棵 DOM 树也进行深度优先的遍历,遍历的时候从步骤二生成的 patches 对象中找出当前遍历的节点差异,如下相关代码所示:
function patch (node, patches) { var walker = {index: 0} dfsWalk(node, walker, patches)}function dfsWalk (node, walker, patches) { // 从patches拿出当前节点的差异 var currentPatches = patches[walker.index] var len = node.childNodes ? node.childNodes.length : 0 // 深度遍历子节点 for (var i = 0; i < len; i++) { var child = node.childNodes[i] walker.index++ dfsWalk(child, walker, patches) } // 对当前节点进行DOM操作 if (currentPatches) { applyPatches(node, currentPatches) }}登录后复制(2)对原有 DOM 树进行 DOM 操作
我们根据不同类型的差异对当前节点进行不同的 DOM 操作 ,例如如果进行了节点替换,就进行节点替换 DOM 操作;如果节点文本发生了改变,则进行文本替换的 DOM 操作;以及子节点重排、属性改变等 DOM 操作,相关代码如 applyPatches 所示 :
function applyPatches (node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } })}登录后复制登录后复制(3)DOM结构改变
通过将第 2.2.2 得到的两个 DOM 对象之间的差异,应用到第一个(原先)DOM 结构中,我们可以看到 DOM 结构进行了预期的变化,如下图所示:
相关代码实现已经放到 github 上面,有兴趣的同学可以clone运行实验,github地址为:https://github.com/fengshi123/virtual-dom-example%E3%80%82
Virtual DOM 算法主要实现上面三个步骤来实现:
用 JS 对象模拟 DOM 树 — element.js
<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>登录后复制登录后复制
比较两棵虚拟 DOM 树的差异 — diff.js
将两个虚拟 DOM 对象的差异应用到真正的 DOM 树 — patch.js
function applyPatches (node, currentPatches) { currentPatches.forEach(currentPatch => { switch (currentPatch.type) { case REPLACE: var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render() node.parentNode.replaceChild(newNode, node) break case REORDER: reorderChildren(node, currentPatch.moves) break case PROPS: setProps(node, currentPatch.props) break case TEXT: node.textContent = currentPatch.content break default: throw new Error('Unknown patch type ' + currentPatch.type) } })}登录后复制登录后复制三、Vue 源码 Virtual-DOM 简析我们从第二章节(Virtual-DOM 基础)中已经掌握 Virtual DOM 渲染成真实的 DOM 实际上要经历 VNode 的定义、diff、patch 等过程,所以本章节 Vue 源码的解析也按这几个过程来简析。
VNode 模拟 DOM 树3.1.1、VNode 类简析在 Vue.js 中,Virtual DOM 是用 VNode 这个 Class 去描述,它定义在 src/core/vdom/vnode.js 中 ,从以下代码块中可以看到 Vue.js 中的 Virtual DOM 的定义较为复杂一些,因为它这里包含了很多 Vue.js 的特性。实际上 Vue.js 中 Virtual DOM 是借鉴了一个开源库 snabbdom 的实现,然后加入了一些 Vue.js 的一些特性。
/** * 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 } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素个数 this.count = count}function createElement(tagName, props, children){ return new Element(tagName, props, children);}module.exports = createElement;2登录后复制这里千万不要因为 VNode 的这么属性而被吓到,或者咬紧牙去摸清楚每个属性的意义,其实,我们主要了解其几个核心的关键属性就差不多了,例如:
tag 属性即这个vnode的标签属性data 属性包含了最后渲染成真实dom节点后,节点上的class,attribute,style以及绑定的事件children 属性是vnode的子节点text 属性是文本属性elm 属性为这个vnode对应的真实dom节点key 属性是vnode的标记,在diff过程中可以提高diff的效率3.1.2、源码创建 VNode 过程(1)初始化vue
我们在实例化一个 vue 实例,也即 new Vue( ) 时,实际上是执行 src/core/instance/index.js 中定义的 Function 函数。
/** * 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 } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素个数 this.count = count}function createElement(tagName, props, children){ return new Element(tagName, props, children);}module.exports = createElement;3登录后复制通过查看 Vue 的 function,我们知道 Vue 只能通过 new 关键字初始化,然后调用 this._init 方法,该方法在 src/core/instance/init.js 中定义。
/** * 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 } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素个数 this.count = count}function createElement(tagName, props, children){ return new Element(tagName, props, children);}module.exports = createElement;4登录后复制(2)Vue 实例挂载
Vue 中是通过 $mount 实例方法去挂载 dom 的,下面我们通过分析 compiler 版本的 mount 实现,相关源码在目录 src/platforms/web/entry-runtime-with-compiler.js 文件中定义:。
/** * 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 } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素个数 this.count = count}function createElement(tagName, props, children){ return new Element(tagName, props, children);}module.exports = createElement;5登录后复制我们发现最终还是调用用原先原型上的 $mount 方法挂载 ,原先原型上的 $mount 方法在 src/platforms/web/runtime/index.js 中定义 。
/** * 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 } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素个数 this.count = count}function createElement(tagName, props, children){ return new Element(tagName, props, children);}module.exports = createElement;6登录后复制我们发现$mount 方法实际上会去调用 mountComponent 方法,这个方法定义在 src/core/instance/lifecycle.js 文件中
/** * 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 } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素个数 this.count = count}function createElement(tagName, props, children){ return new Element(tagName, props, children);}module.exports = createElement;7登录后复制登录后复制从上面的代码可以看到,mountComponent 核心就是先实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。
(3)创建虚拟 Node
Vue 的 _render 方法是实例的一个私有方法,它用来把实例渲染成一个虚拟 Node。它的定义在 src/core/instance/render.js 文件中:
/** * 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 } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素个数 this.count = count}function createElement(tagName, props, children){ return new Element(tagName, props, children);}module.exports = createElement;8登录后复制Vue.js 利用 _createElement 方法创建 VNode,它定义在 src/core/vdom/create-elemenet.js 中:
/** * 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 } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素个数 this.count = count}function createElement(tagName, props, children){ return new Element(tagName, props, children);}module.exports = createElement;9登录后复制_createElement 方法有 5 个参数,context 表示 VNode 的上下文环境,它是 Component 类型;tag表示标签,它可以是一个字符串,也可以是一个 Component;data 表示 VNode 的数据,它是一个 VNodeData 类型,可以在 flow/vnode.js 中找到它的定义;children 表示当前 VNode 的子节点,它是任意类型的,需要被规范为标准的 VNode 数组;
为了更直观查看我们平时写的 Vue 代码如何用 VNode 类来表示,我们通过一个实例的转换进行更深刻了解。
例如,实例化一个 Vue 实例:
var el = require("./element.js");var 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'])])0登录后复制我们打印出其对应的 VNode 表示:
diff 过程3.2.1、Vue.js 源码的 diff 调用逻辑Vue.js 源码实例化了一个 watcher,这个 ~ 被添加到了在模板当中所绑定变量的依赖当中,一旦 model 中的响应式的数据发生了变化,这些响应式的数据所维护的 dep 数组便会调用 dep.notify() 方法完成所有依赖遍历执行的工作,这包括视图的更新,即 updateComponent 方法的调用。watcher 和 updateComponent方法定义在 src/core/instance/lifecycle.js 文件中 。
/** * 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 } var count = 0 children.forEach(function (child, i) { if (child instanceof Element) { count += child.count } else { children[i] = '' + child } count++ }) // 子元素个数 this.count = count}function createElement(tagName, props, children){ return new Element(tagName, props, children);}module.exports = createElement;7登录后复制登录后复制完成视图的更新工作事实上就是调用了vm._update方法,这个方法接收的第一个参数是刚生成的Vnode,调用的vm._update方法定义在 src/core/instance/lifecycle.js中。
var el = require("./element.js");var 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'])])2登录后复制在这个方法当中最为关键的就是 vm.__patch__ 方法,这也是整个 virtual-dom 当中最为核心的方法,主要完成了prevVnode 和 vnode 的 diff 过程并根据需要操作的 vdom 节点打 patch,最后生成新的真实 dom 节点并完成视图的更新工作。
接下来,让我们看下 vm.__patch__的逻辑过程, vm.__patch__ 方法定义在 src/core/vdom/patch.js 中。
var el = require("./element.js");var 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'])])3登录后复制在 patch 方法中,我们看到会分为两种情况,一种是当 oldVnode 不存在时,会创建新的节点;另一种则是已经存在 oldVnode ,那么会对 oldVnode 和 vnode 进行 diff 及 patch 的过程。其中 patch 过程中会调用 sameVnode 方法来对对传入的2个 vnode 进行基本属性的比较,只有当基本属性相同的情况下才认为这个2个vnode 只是局部发生了更新,然后才会对这2个 vnode 进行 diff,如果2个 vnode 的基本属性存在不一致的情况,那么就会直接跳过 diff 的过程,进而依据 vnode 新建一个真实的 dom,同时删除老的 dom节点。
var el = require("./element.js");var 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'])])4登录后复制diff 过程中主要是通过调用 patchVnode 方法进行的:
var el = require("./element.js");var 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'])])5登录后复制从以上代码得知,
diff 过程中又分了好几种情况,oldCh 为 oldVnode的子节点,ch 为 Vnode的子节点:
oldVnode.text !== vnode.text,那么就会直接进行文本节点的替换;在vnode 没有文本节点的情况下,进入子节点的 diff;当 oldCh 和 ch 都存在且不相同的情况下,调用 updateChildren 对子节点进行 diff;若 oldCh不存在,ch 存在,首先清空 oldVnode 的文本节点,同时调用 addVnodes 方法将 ch 添加到elm真实 dom 节点当中;若 oldCh存在,ch不存在,则删除 elm 真实节点下的 oldCh 子节点;若 oldVnode 有文本节点,而 vnode 没有,那么就清空这个文本节点。3.2.2、子节点 diff 流程分析(1)Vue.js 源码
这里着重分析下updateChildren方法,它也是整个 diff 过程中最重要的环节,以下为 Vue.js 的源码过程,为了更形象理解 diff 过程,我们给出相关的示意图来讲解。
var el = require("./element.js");var 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'])])6登录后复制在开始遍历 diff 前,首先给 oldCh和 newCh 分别分配一个 startIndex 和 endIndex 来作为遍历的索引,当oldCh 或者 newCh 遍历完后(遍历完的条件就是 oldCh 或者 newCh 的 startIndex >= endIndex ),就停止oldCh 和 newCh 的 diff 过程。接下来通过实例来看下整个 diff 的过程(节点属性中不带 key 的情况)。
(2)无 key 的 diff 过程
我们通过以下示意图对以上代码过程进行讲解:
(2.1)首先从第一个节点开始比较,不管是 oldCh 还是 newCh 的起始或者终止节点都不存在 sameVnode ,同时节点属性中是不带 key标记的,因此第一轮的 diff 完后,newCh的 startVnode 被添加到 oldStartVnode的前面,同时 newStartIndex前移一位;
(2.2)第二轮的 diff中,满足 sameVnode(oldStartVnode, newStartVnode),因此对这2个 vnode 进行diff,最后将 patch 打到 oldStartVnode 上,同时 oldStartVnode和 newStartIndex 都向前移动一位 ;
(2.3)第三轮的 diff 中,满足 sameVnode(oldEndVnode, newStartVnode),那么首先对 oldEndVnode和newStartVnode 进行 diff,并对 oldEndVnode进行 patch,并完成 oldEndVnode 移位的操作,最后newStartIndex前移一位,oldStartVnode 后移一位;
(2.4)第四轮的 diff中,过程同步骤3;
(2.5)第五轮的 diff 中,同过程1;
(2.6)遍历的过程结束后,newStartIdx > newEndIdx,说明此时 oldCh 存在多余的节点,那么最后就需要将这些多余的节点删除。
(3)有 key 的 diff 流程
在 vnode 不带 key 的情况下,每一轮的 diff 过程当中都是起始和结束节点进行比较,直到 oldCh 或者newCh 被遍历完。而当为 vnode 引入 key 属性后,在每一轮的 diff 过程中,当起始和结束节点都没有找到sameVnode 时,然后再判断在 newStartVnode 的属性中是否有 key,且是否在 oldKeyToIndx 中找到对应的节点 :
key,那么就将这个 newStartVnode作为新的节点创建且插入到原有的 root 的子节点中;如果存在这个 key,那么就取出 oldCh 中的存在这个 key 的 vnode,然后再进行 diff 的过;通过以上分析,给vdom上添加 key属性后,遍历 diff 的过程中,当起始点,结束点的搜寻及 diff 出现还是无法匹配的情况下时,就会用 key 来作为唯一标识,来进行 diff,这样就可以提高 diff 效率。
带有 Key属性的 vnode的 diff 过程可见下图:
(3.1)首先从第一个节点开始比较,不管是 oldCh 还是 newCh 的起始或者终止节点都不存在 sameVnode,但节点属性中是带 key 标记的, 然后在 oldKeyToIndx 中找到对应的节点,这样第一轮 diff 过后 oldCh 上的B节点被删除了,但是 newCh 上的B节点上 elm 属性保持对 oldCh 上 B节点 的elm引用。
(3.2)第二轮的 diff 中,满足 sameVnode(oldStartVnode, newStartVnode),因此对这2个 vnode 进行diff,最后将 patch 打到 oldStartVnode上,同时 oldStartVnode 和 newStartIndex 都向前移动一位 ;
(3.3)第三轮的 diff中,满足 sameVnode(oldEndVnode, newStartVnode),那么首先对 oldEndVnode 和newStartVnode 进行 diff,并对 oldEndVnode 进行 patch,并完成 oldEndVnode 移位的操作,最后newStartIndex 前移一位,oldStartVnode后移一位;
(3.4)第四轮的diff中,过程同步骤2;
(3.5)第五轮的diff中,因为此时 oldStartIndex 已经大于 oldEndIndex,所以将剩余的 Vnode 队列插入队列最后。
patch 过程通过3.2章节介绍的 diff 过程中,我们会看到 nodeOps 相关的方法对真实 DOM 结构进行操作,nodeOps 定义在 src/platforms/web/runtime/node-ops.js 中,其为基本 DOM 操作,这里就不在详细介绍。
var el = require("./element.js");var 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'])])7登录后复制3.4、总结通过前三小节简析,我们从主线上把模板和数据如何渲染成最终的 DOM 的过程分析完毕了,我们可以通过下图更直观地看到从初始化 Vue 到最终渲染的整个过程。
本文从通过介绍真实 DOM 结构其解析过程以及存在的问题,从而引出为什么需要虚拟 DOM;然后分析虚拟DOM 的好处,以及其一些理论基础和基础算法的实现;最后根据我们已经掌握的基础知识,再一步步去查看Vue.js 的源码如何实现的。从存在问题 —> 理论基础 —> 具体实践,一步步深入,帮助大家更好的了解什么是Virtual DOM、为什么需要 Virtual DOM、以及 Virtual DOM的具体实现,希望本文对您有帮助。
(学习视频分享:web前端开发、编程基础视频)
以上就是深入解析Vue中的虚拟DOM的详细内容,更多请关注9543建站博客其它相关文章!


发表评论