时间:2022-05-08 10:04:57 | 栏目:vue | 点击:次
虚拟DOM就是为了解决浏览器性能问题而被设计出来的。例如,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。
----- 就是以JS的计算性能来换取操作真实DOM所消耗的性能,Vue是通过VNode类来实例化不同类型的虚拟DOM节点,并且学习了不同类型节点生成的属性的不同,所谓不同类型的节点其本质还是一样的,都属VNode类的实例,只是实例化的时候传入的参数不同罢了。
有了数据变化前后的VNode,我们才能进行后续的DOM-Diff找出差异,最终做到只更新有差异的视图,从而达到尽可能少的操作真实DOM的目的,以节省性能
----- 而找出更新有差异的DOM节点,已达到最少操作真实DOM更新视图的目的。而对比新旧两份VNode并找出差异的过程就是所谓的DOM-Diff过程,DOM-Diff算法是整个虚拟DOM的核心所在。
在Vue中,把DOM-Diff过程就叫做patch过程,patch意思为补丁,一个思想:所谓旧的VNode(odlNode)就是数据变化之前属于所对应的虚拟DOM节点,而新的NVode是数据变化之后将要渲染的视图所对应的虚拟DOM节点,所以我们要以生成的新的VNode为基准,对比旧的oldVNode,如果新的VNode上有的节点而旧的oldVNode没有,那么就在旧的oldVNode上加上去,如果新的VNode上没有的节点而旧的oldVNode上有,那么就在旧的oldVnode上去掉。如果新旧Vnode节点都有,则以新的VNode为准,更新旧的oldVNode,从而让新旧VNode相同。
整个patch:就是在创建节点:新的VNode有,旧的没有。就在旧的oldVNode中创建
删除节点:新的VNode中没有,而旧的oldVNode有,就从旧的oldVNode中删除
更新节点:新的旧的都有,就以新的VNode为准,更新旧的oldVNode
更新子节点
/* 对比两个子节点数组肯定是要通过循环,外层循环newChildren,内层循环oldCHildren数组,每循环外层 newChildren数组里的每一个子节点,就去内层oldChildren数组里找看有没有与之相同的子节点 */ for (let i = 0; i < newChildred.length; i++) { const newChild = newChildren[i] for (let j = 0; j < oldChildren.length; j++) { const oldChild = oldChildren[i] if (newChild === oldChild) { // ... } } }
那么以上这个过程将会存在一下四种情况
我们一再强调更新节点要以新Vnode为基准,然后操作旧的oldVnode,使之最后旧的oldVNode与新的VNode相同。
如果VNode和oldVNode均为静态节点,
我们说了,静态节点无论数据发生任何变化都与它无关,所以都为静态节点的话则直接跳过,无需处理
如果VNode是文本节点
如果VNode是我文本节点即表示这个节点内只包含纯文本,那么只需要看oldVNode是否也是文本节点,如果是那就比较两个文本是否不同,如果不用则把oldVNode里的文本改成跟VNode的文本一样,如果oldVNode不是文本节点,那么不论它是什么,直接调用setTextNode方法把他改成文本节点,并且文本内容跟VNode相同
如果VNode是元素节点,则又细分以下两种情况
// 更新节点 function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) { // vnode 与 oldVnode 是否完全一样,如果是,退出程序 if (oldVnode === vnode) { return } const elm = vnode.elm = oldVnode.elm // vnode 与 oldVnode是否都是静态节点,如果是退出程序 if (isTrue(vnode.isStatic) && isTrue(vnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) { return } const oldCh = oldVnode.children const ch = vnode.children // vnode 有 text属性,若没有 if (isUndef(vnode.text)) { if (isDef(oldCh) && isDef(ch)) { // 若都存在,判断子节点是否相同,不同则更新子节点 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } // 若只有vnode的子节点的存在 else if (isDef(ch)) { /** * 判断oldVnode是否有文本 * 若没有,则把Vnode的子节点添加到真实DOM中 * 若有,则清空DOM中的文本,再把vnode的子节点添加到真实DOM中 * */ if (isDef(oldVnode.text)) nodeOps.setTextContext(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } // 如果只有oldnode的子节点存在 else if (isDef(oldCh)) { // 清空DOM中的所有子节点 removeVnodes(elm, oldCh, 0, oldCh.length - 1) } // 若vnode和oldnode都没有子节点,但是oldnode中有文本 else if (isDef(oldVnode.text)) { nodeOps.setTextContext(elm, '') } // 上面两个判断一句话概括就是,如果vnode中既没有text,也没有子节点,那么对应的oldnode中有什么清空什么 } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContext(elm, vnode.text) } }
上面的我们了解了Vue的patch也就是DOM-DIFF算法,并且知道了在patch过程之中基本会干三件事,分别是创建节点,删除节点和更新节点。 创建节点和删除节点比较简单,而更新节点因为要处理各种可能出现的情况逻辑就比较复杂一些。 更新过程中九点Vnode可能都包含子节点,对于子系?G但的对比更新会有额外的一些逻辑,那么本篇文章就来学习Vue中是如何对比子节点的
更新子节点
当新的Vnode与旧的oldVnode都是元素节点并且都包含子节点的时候,那么这连个节点VNode实例上的chidlren属性就是所包含的子节点数组,对比两个子节点的通过循环,外层循环newChildren数组,内层循环oldChildren数组,每循环外层newChildren数组里的一个子节点,,就去内层oldChiildren数组里找看有没有与之相同的子节点
. 创建子节点
创建子节点的位置应该是在所有未处理节点之前,而并非所有已处理节点之后。 因为如果把子节点插入到已处理后面,如果后续还要插入新节点,那么新增子节点就乱了
. 移动子节点
所有未处理结点之前就是我们要移动的目的的位置
前面我们介绍了当新的VNode与旧的oldVNode都是元素节点并且都包含了子节点的时候,vue对子节点是先外层循环newChildren数组,再内层循环oldChildren数组,每循环外层newChildren数组里的一个子节点,就去内层oldChildren数组里找看有没有与之相同的子节点,最后根据不同的情况做出不同的操作。这种还存在可优化的地方,比如当包含子节点数量较多的时候,这样循环算法的时间复杂度就会变得很大,不利于性能提升。
方法:
在前面几篇文章中,介绍了Vue中的虚拟DOM以及虚拟DOM的patch(DOM-Diff)过程,而虚拟DOM存在的必要条件是的现有VNode,那么VNode又是从哪里来的。 把用户写的模板进行编译,就会产生VNode
什么是模板编译:把用户template标签里面的写的类似于原生HTML的内容进行编译,把原生HTML的内容找出来,再把非原生的HTML找出来,经过一系列的逻辑处理生成渲染函数,也就是render函数的这一段过程称之为模板编译过程。 render函数会将模板内容生成VNode
整体渲染流程,所谓渲染流程,就是把用户写的类似于原生HTML的模板经过一系列的过程最终反映到视图中称之为整个渲染流程,这个流程在上文中已经说到了。
抽象语法树AST:
具体流程:
模板解析阶段:将一堆模板字符串用正则表达式解析成抽象语法树AST
优化阶段:编译AST,找出其中的静态节点,并打上标记
代码生成阶段: 将AST转换成渲染函数
有了模板编译,才有了虚拟DOM,才有了后续的视图更新