ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] <br> <br> # 模板转换成视图的过程 * Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树 * 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。 <br> 简单点讲,在Vue的底层实现上,Vue将模板编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应到DOM操作上。 <br> ![](https://user-gold-cdn.xitu.io/2019/6/26/16b9230a4c3cdd40?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> 我们先对上图几个概念加以解释: * **渲染函数**:渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。 * **VNode 虚拟节点**:它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成**节点描述对象**,它描述了应该怎样去创建真实的DOM节点。 * **patch(也叫做patching算法)**:虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。Vue的Virtual DOM Patching算法是基于[Snabbdom](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fsnabbdom%2Fsnabbdom "https://github.com/snabbdom/snabbdom")的实现,并在些基础上作了很多的调整和改进。 <br> <br> # Virtual DOM ## Virtual DOM 是什么? Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。 <br> 简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。 <br> 对于虚拟DOM,咱们来看一个简单的实例,就是下图所示的这个,详细的阐述了`模板 → 渲染函数 → 虚拟DOM树 → 真实DOM`的一个过程 ![image.png](https://user-gold-cdn.xitu.io/2019/6/25/16b8a4e46453e643?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> <br> ## Virtual DOM 作用是什么? **虚拟DOM的最终目标是将虚拟节点渲染到视图上**。但是如果直接使用虚拟节点覆盖旧节点的话,会有很多不必要的DOM操作。例如,一个ul标签下很多个li标签,其中只有一个li有变化,这种情况下如果使用新的ul去替代旧的ul,因为这些不必要的DOM操作而造成了性能上的浪费。 <br> 为了避免不必要的DOM操作,虚拟DOM在虚拟节点映射到视图的过程中,将虚拟节点与上一次渲染视图所使用的旧虚拟节点(oldVnode)做对比,找出真正需要更新的节点来进行DOM操作,从而避免操作其他无需改动的DOM。 <br> 其实虚拟DOM在Vue.js主要做了两件事: * 提供与真实DOM节点所对应的虚拟节点vnode * 将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图 <br> <br> ## 为何需要Virtual DOM? * 具备跨平台的优势 由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。 <br> * 操作 DOM 慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。 因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。 <br> Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM) <br> * 提升渲染性能 Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。 为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要。**我们通过patch 的核心----diff 算法,找出本次DOM需要更新的节点来更新,其他的不更新**。那diff 算法的实现过程是怎样的? <br> ## 实现 vnode **(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` 对应的数据结构如下: ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1e14fcff074f0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) **(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` 结构,效果如下图所示: ![](https://user-gold-cdn.xitu.io/2019/7/23/16c1e179a748d425?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> <br> # diff 算法 ![diff 算法](https://user-gold-cdn.xitu.io/2019/6/23/16b82599955f3694?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) Vue的diff算法是基于snabbdom改造过来的,**仅在同级的vnode间做diff,递归地进行同级vnode的diff,最终实现整个DOM树的更新**。因为跨层级的操作是非常少的,忽略不计,这样时间复杂度就从O(n3)变成O(n)。 <br> diff 算法包括几个步骤: * 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中 * 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异 * 把所记录的差异应用到所构建的真正的DOM树上,视图就更新了 ![image.png](https://user-gold-cdn.xitu.io/2019/6/24/16b8a2b9e4725e5a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> ## diff 过程 <br> ![img](https://user-gold-cdn.xitu.io/2017/9/18/ed3fe3ef6c580e16711c39159ce87cd4?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> 首先,在新老两个VNode节点的左右头尾两侧都有一个变量标记,在遍历过程中这几个变量都会向中间靠拢。当oldStartIdx <= oldEndIdx或者newStartIdx <= newEndIdx时结束循环。 <br> 索引与VNode节点的对应关系: oldStartIdx => oldStartVnode oldEndIdx => oldEndVnode newStartIdx => newStartVnode newEndIdx => newEndVnode <br> 在遍历中,如果存在key,并且满足sameVnode,会将该DOM节点进行复用,否则则会创建一个新的DOM节点。 <br> 首先,oldStartVnode、oldEndVnode与newStartVnode、newEndVnode两两比较一共有2\*2=4种比较方法。 <br> 当新老VNode节点的start或者end满足sameVnode时,也就是sameVnode(oldStartVnode, newStartVnode)或者sameVnode(oldEndVnode, newEndVnode),直接将该VNode节点进行patchVnode即可。 <br> ![img](https://user-gold-cdn.xitu.io/2017/9/18/dbf1c71d42eaddc6de60301aad17c860?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> 如果oldStartVnode与newEndVnode满足sameVnode,即sameVnode(oldStartVnode, newEndVnode)。 <br> 这时候说明oldStartVnode已经跑到了oldEndVnode后面去了,进行patchVnode的同时还需要将真实DOM节点移动到oldEndVnode的后面。 <br> ![img](https://user-gold-cdn.xitu.io/2017/9/18/0b5beb1c771c3965a77c787fe55a3b57?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> 如果oldEndVnode与newStartVnode满足sameVnode,即sameVnode(oldEndVnode, newStartVnode)。 这说明oldEndVnode跑到了oldStartVnode的前面,进行patchVnode的同时真实的DOM节点移动到了oldStartVnode的前面。 ![img](https://user-gold-cdn.xitu.io/2017/9/18/dc9a1e0b27411b2585960e971559382f?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> 如果以上情况均不符合,则通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNode,value为对应index序列的哈希表。从这个哈希表中可以找到是否有与newStartVnode一致key的旧的VNode节点,如果同时满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面。 ![img](https://user-gold-cdn.xitu.io/2017/9/18/ed03e90b708939205236225c582e26fb?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> 当然也有可能newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点。 ![img](https://user-gold-cdn.xitu.io/2017/9/18/73241a7ea0b6f52c0df4d835a827f3b4?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> 到这里循环已经结束了,那么剩下我们还需要处理多余或者不够的真实DOM节点。 <br> 1.当结束时oldStartIdx > oldEndIdx,这个时候老的VNode节点已经遍历完了,但是新的节点还没有。说明了新的VNode节点实际上比老的VNode节点多,也就是比真实DOM多,需要将剩下的(也就是新增的)VNode节点插入到真实DOM节点中去,此时调用addVnodes(批量调用createElm的接口将这些节点加入到真实DOM中去)。 ![img](https://user-gold-cdn.xitu.io/2017/9/18/22369d39d970155963bd71a1370e9b07?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> 2。同理,当newStartIdx > newEndIdx时,新的VNode节点已经遍历完了,但是老的节点还有剩余,说明真实DOM节点多余了,需要从文档中删除,这时候调用removeVnodes将这些多余的真实DOM删除。 ![img](https://user-gold-cdn.xitu.io/2017/9/18/c067fa75aa884a2c231d940de35ef7a1?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> <br> # patch patch将新老VNode节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图,而不是将整个视图根据新的VNode重绘。patch的核心在于diff算法,这套算法可以高效地比较viturl dom的变更,得出变化以修改视图。 <br> 那么patch如何工作的呢? <br> 首先说一下patch的核心diff算法,diff算法是通过同层的树节点进行比较而非对树进行逐层搜索遍历的方式,所以时间复杂度只有O(n),是一种相当高效的算法。 <br> ![img](https://user-gold-cdn.xitu.io/2017/9/18/599392157760360fa2c45895fe3438e9?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> ![img](https://user-gold-cdn.xitu.io/2017/9/18/ebc9bc6e6792591c8a716c9ed876f87d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) <br> 着两张图代表旧的VNode与新VNode进行patch的过程,他们只是在同层级的VNode之间进行比较得到变化(第二张图中相同颜色的方块代表互相进行比较的VNode节点),然后修改变化的视图,所以十分高效。