文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Virtual DOM作用是什么

2024-04-02 19:55

关注

这篇文章主要介绍“Virtual DOM作用是什么”,在日常操作中,相信很多人在Virtual DOM作用是什么问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Virtual DOM作用是什么”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

什么是Virtual DOM

Virtual  DOM(虚拟DOM),在形态上表现为一个能够描述DOM结构及其属性信息的普通的JS对象,因为不是真实的DOM对象,所以叫虚拟DOM。

<div></div>
{   sel: 'div',   data: {},   chidren:undefined,   elm:undefined,   key:undefined, }

Virtual DOM 本质上JS和DOM之间的一个映射缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM  这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

为什么需要Virtual DOM

手动操作DOM比较麻烦,还需要考虑浏览器兼容性问题,虽然有jquery等库简化DOM操作,但是随着项目的复杂DOM操作复杂提升。

为了简化DOM的复杂操作于是出现了MVVM框架,MVVM框架解决了视图和状态的同步问题。

为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是Virtual DOM出现了。

Virtual DOM的好处是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOM,Virtual  DOM内部将弄清除如何有效(diff)的更新DOM。

虚拟DOM可以维护程序的状态,跟踪上一次的状态,通过比较前后两次状态的差异更新真实DOM。

Virtual DOM的作用

1、减少对真实DOM的操作

真实DOM 因为浏览器厂商需要实现众多的规范(各种 HTML5 属性、DOM事件),即使创建一个空的 div  也要付出昂贵的代价。如以下代码,打印空的div属性一共298个。而这仅仅是第一层。真正的 DOM  元素非常庞大。直接操作DOM可能会导致频繁的回流和重绘。

const div = document.createElement('div'); const arr = []; for(key in div){arr.push(key)}; console.log(arr.length); // 298

对复杂的文档DOM结构(复杂视图情况下提升渲染性能),提供一种方便的工具,进行最小化地DOM操作。既然我们可以用JS对象表示DOM结构,那么当数据状态发生变化而需要改变DOM结构时,我们先通过JS对象表示的虚拟DOM计算出实际DOM需要做的最小变动(Virtual  DOM会使用diff算法计算出如果有效的更新dom,只更新状态改变的DOM),然后再操作实际DOM,从而避免了粗放式的DOM操作带来的性能问题,减少对真实DOM的操作。

2、无需手动操作 DOM,维护视图和状态的关系

我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,MVVM框架会根据虚拟 DOM 和  数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率。

3、跨平台

虚拟DOM是对真实的渲染内容的一层抽象,是真实DOM的描述,因此,它可以实现“一次编码,多端运行”,可以实现SSR(Nuxt.js/Next.js)、原生应用(Weex/React  Native)、小程序(mpvue/uni-app)等。

Virtual DOM有什么不足

上面我们也说到了在复杂视图情况下提升渲染性能。虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟DOM  无法进行针对性的极致优化。首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。

下方是尤大自己的见解。https://www.zhihu.com/question/31809713/answer/53544875

Virtual DOM库

virtual-dom

一个JavaScript DOM模型,支持元素创建,差异计算和补丁操作,以实现高效的重新渲染。

源代码库地址:https://github.com/Matt-Esch/virtual-dom.git

已经有五六年没有维护了

snabbdom

为什么要介绍Virtual DOM库Snabbdom

Snabbdom核心

Snabbdom搭建项目

第一步,初始化项目

npm init -y

or

yarn init -y

第二步,安装依赖

安装snabbdom

npm install snabbdom

or

yarn add snabbdom

安装parcel-bundler

npm install parcel-bundler

or

yarn add parcel-bundler

第三步,创建文件夹/文件,编辑文件

在根目录下创建一个名为src的文件目录,然后在里面创建一个main.js文件。最后,在根目录下创建一个index.html文件。

package.json文件可以编辑如下,更利于操作。

"scripts": {     "serve": "parcel index.html --open",     "build": "parcel build index.html"  },

第四步,编辑文件内容

index.html

<!DOCTYPE html> <html lang="en">   <head>     <meta charset="UTF-8" />     <meta http-equiv="X-UA-Compatible" content="IE=edge" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>snabbdomApp</title>   </head>   <body>     <div id="app"></div>     <script src="src/main.js"></script>   </body> </html>

main.js

主要介绍snabbdom中两个核心init()、h()。

示例1

import { h, init } from 'snabbdom';  // init函数参数:数组(模块) // 返回值:patch函数:作用是对比两个Vnode的差异更新到真实dom const patch = init([]); // h函数 // 第一个参数:标签+选择器; // 第二个参数:如果是字符串则是标签的内容 let vnode = h('div#container', 'Hello World');  const app = document.querySelector('#app'); // patch函数 // 第一个参数:可以是DOM元素(内部会把DOM元素转化为Vnode),也可以是Vnode; // 第二个参数:Vnode // 返回值:Vnode let oldVnode = patch(app, vnode);  // 新旧Vnode对比 vnode = h('div', 'Hello Snabbdom'); patch(oldVnode, vnode);

示例2

import { h, init } from 'snabbdom';  const patch = init([]); // 可放置子元素 let vnode = h('div#container', [h('h2', '1'), h('h3', '2')]);  const app = document.querySelector('#app');  const oldVnode = patch(app, vnode);  vnode = h('div', 'Hello Snabbdom'); patch(oldVnode, vnode);  setInterval(() => {   // 清除页面元素   patch(oldVnode, h('!')); }, 3000);

示例3

常用模块

设置DOM元素的特性,使用setAttribute添加和更新特性。

允许设置DOM元素的属性。

提供了一种动态切换元素上的类的简单方法。

允许在元素上设置CSS属性。请注意,如果样式属性作为样式对象的属性被移除,样式模块并不会移除它们。为了移除一个样式,应该将其设置为空字符串。

允许在DOM元素上设置自定义数据属性(data-*)。

提供了附加事件监听器的强大功能。

import {   h,   init,   classModule,   propsModule,   styleModule,   eventListenersModule, } from 'snabbdom';  const patch = init([   styleModule,   classModule,   propsModule,   eventListenersModule, ]);  let vnode = h(   'div#container',   {     style: {       color: '#000',     },     on: {       click: eventHandler,     },   },   [     h('p', 'p1', h('a', { class: { active: true, selected: true } }, 'Toggle')),     h('p', 'p2'),     h('a', { props: { href: '/' } }, 'Go to'),   ] );  function eventHandler() {   console.log('1'); } const app = document.querySelector('#app');  patch(app, vnode);

snabbdom源码浅析

源码地址:https://github.com/snabbdom/snabbdom.git以下分析snabbdom版本3.0.1。

源码核心文件目录及其文件

核心文件夹是**src目录。**里面包含了如下文件夹及其目录:

核心文件浅析

h.ts

h 函数最早见于 hyperscript,使用 JavaScript 创建超文本,Snabbdom 中的 h 函数不是用来创建超文本,而是创建  Vnode。 在使用 Vue2.x 的时候见过 h 函数,它的参数就是h函数,但是Vue加强了h函数,使其支持组件机制。

new Vue({   router,   store,   render:h => h(App) }).$mount('#app)

Virtual DOM作用是什么

以上是h.ts文件中的内容,可以看到它导出了多个h方法,这种方式叫做函数重载。在JS中暂时没有,目前TS支持这种机制(但也只是通过调整代码参数层面上,因为最终TS还是要转换为JS)。方法名相同,参数个数或类型不同的方法叫做函数重载。所以通过参数个数或类型不同来区分它们。

// 这里通过参数不同来区分不同的函数  function add(a, b) {   console.log(a + b); }  function add(a, b, c) {   console.log(a + b + c); }  add(1, 2); add(1, 2, 3);

从上面代码层面上我们知道了通过函数重载这种方法可以在通过参数个数或类型不同轻松地实现了相应情况调用相应参数的方法。

那么,我们来具体看下源码是怎么实现函数重载的。

Virtual DOM作用是什么

通过源码我们看到,通过传入不同的类型的参数调用对应的代码,最后将将参数传入到vnode方法中,创建一个Vnode,并返回这个方法。

那么接下来,我们看下vnode方法的实现。

vnode.ts

我们打开vnode.ts这个文件,这个文件主要是导出了一个vnode方法,并且定义了几个接口。我们看到以下代码中vnode中的参数含义就知道在h.ts文件中函数参数的意思,是相对应的。

Virtual DOM作用是什么

init.ts

在介绍init.ts文件之前的,我们需要知道这样的一个概念:

这个概念我们在上面已经阐述了。**init()**就是通过这个文件导出的。

在看init.ts源码之前,我们还需要了解Vnode是渲染到真实DOM的整体流程。这样,看源码才不会有误解。

整体流程:

Diff算法的作用是用来计算出 Virtual DOM 中被改变的部分,然后针对该部分进行原生DOM操作,而不用重新渲染整个页面。

同级对比

对比的时候,只针对同级的对比,减少算法复杂度。

Virtual DOM作用是什么

就近复用

为了尽可能不发生 DOM 的移动,会就近复用相同的 DOM 节点,复用的依据是判断是否是同类型的 dom 元素。  看到这里你可能就会想到Vue中列表渲染为什么推荐加上key,我们需要使用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。key的作用主要是为了高效的更新虚拟DOM。

我们先看下init.ts中的大体源码。

Virtual DOM作用是什么

我们先简单地来看下sameVnode方法。判断是否是相同的虚拟节点。

// 是否是相同节点 function sameVnode(vnode1: VNode, vnode2: VNode): boolean {   const isSameKey = vnode1.key === vnode2.key;   const isSameIs = vnode1.data?.is === vnode2.data?.is;   const isSameSel = vnode1.sel === vnode2.sel;    return isSameSel && isSameKey && isSameIs; }

是否是Vnode。

// 是否是vnode function isVnode(vnode: any): vnode is VNode {   return vnode.sel !== undefined; }

注册一系列的钩子,在不同的阶段触发。

// 定义一些钩子函数 const hooks: Array<keyof Module> = [   "create",   "update",   "remove",   "destroy",   "pre",   "post", ];

下面呢,主要看下导出的init方法。也是init.ts中最主要的部分,从68行到472行。

// 导出init函数 export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {   let i: number;   let j: number;   const cbs: ModuleHooks = {     create: [],     update: [],     remove: [],     destroy: [],     pre: [],     post: [],   };   // 初始化转化成虚拟节点的api   const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;   // 把传入的所有模块的钩子函数,统一存储到cbs对象中   // 最终构建的cbs对象的形式cbs = {create:[fn1,fn2],update:[],....}   for (i = 0; i < hooks.length; ++i) {     // cbs.create= [], cbs.update = []...     cbs[hooks[i]] = [];     for (j = 0; j < modules.length; ++j) {       // modules 传入的模块数组       // 获取模块中的hook函数       // hook = modules[0][create]...       const hook = modules[j][hooks[i]];       if (hook !== undefined) {         // 把获取到的hook函数放入到cbs 对应的钩子函数数组中         (cbs[hooks[i]] as any[]).push(hook);       }     }   }    function emptyNodeAt(elm: Element) {     const id = elm.id ? "#" + elm.id : "";     const c = elm.className ? "." + elm.className.split(" ").join(".") : "";     return vnode(       api.tagName(elm).toLowerCase() + id + c,       {},       [],       undefined,       elm     );   }    function createRmCb(childElm: Node, listeners: number) {     return function rmCb() {       if (--listeners === 0) {         const parent = api.parentNode(childElm) as Node;         api.removeChild(parent, childElm);       }     };   }       function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {     let i: any;     let data = vnode.data;     if (data !== undefined) {       // 执行init钩子函数       const init = data.hook?.init;       if (isDef(init)) {         init(vnode);         data = vnode.data;       }     }     // 把vnode转换成真实dom对象(没有渲染到页面)     const children = vnode.children;     const sel = vnode.sel;     if (sel === "!") {       // 如果选择器是!,创建注释节点       if (isUndef(vnode.text)) {         vnode.text = "";       }       vnode.elm = api.createComment(vnode.text!);     } else if (sel !== undefined) {       // 如果选择器不为空       // 解析选择器       // Parse selector       const hashIdx = sel.indexOf("#");       const dotIdx = sel.indexOf(".", hashIdx);       const hash = hashIdx > 0 ? hashIdx : sel.length;       const dot = dotIdx > 0 ? dotIdx : sel.length;       const tag =         hashIdx !== -1 || dotIdx !== -1           ? sel.slice(0, Math.min(hash, dot))           : sel;       const elm = (vnode.elm =         isDef(data) && isDef((i = data.ns))           ? api.createElementNS(i, tag, data)           : api.createElement(tag, data));       if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));       if (dotIdx > 0)         elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));       // 执行模块的create钩子函数       for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);       // 如果vnode中有子节点,创建子Vnode对应的DOM元素并追加到DOM树上       if (is.array(children)) {         for (i = 0; i < children.length; ++i) {           const ch = children[i];           if (ch != null) {             api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));           }         }       } else if (is.primitive(vnode.text)) {         // 如果vode的text值是string/number,创建文本节点并追加到DOM树         api.appendChild(elm, api.createTextNode(vnode.text));       }       const hook = vnode.data!.hook;       if (isDef(hook)) {         // 执行传入的钩子 create         hook.create?.(emptyNode, vnode);         if (hook.insert) {           insertedVnodeQueue.push(vnode);         }       }     } else {       // 如果选择器为空,创建文本节点       vnode.elm = api.createTextNode(vnode.text!);     }     // 返回新创建的DOM     return vnode.elm;   }    function addVnodes(     parentElm: Node,     before: Node | null,     vnodes: VNode[],     startIdx: number,     endIdx: number,     insertedVnodeQueue: VNodeQueue   ) {     for (; startIdx <= endIdx; ++startIdx) {       const ch = vnodes[startIdx];       if (ch != null) {         api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);       }     }   }    function invokeDestroyHook(vnode: VNode) {     const data = vnode.data;     if (data !== undefined) {       // 执行的destroy 钩子函数       data?.hook?.destroy?.(vnode);       // 调用模块的destroy钩子函数       for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);       // 执行子节点的destroy钩子函数       if (vnode.children !== undefined) {         for (let j = 0; j < vnode.children.length; ++j) {           const child = vnode.children[j];           if (child != null && typeof child !== "string") {             invokeDestroyHook(child);           }         }       }     }   }    function removeVnodes(     parentElm: Node,     vnodes: VNode[],     startIdx: number,     endIdx: number   ): void {     for (; startIdx <= endIdx; ++startIdx) {       let listeners: number;       let rm: () => void;       const ch = vnodes[startIdx];       if (ch != null) {         // 如果sel 有值         if (isDef(ch.sel)) {           invokeDestroyHook(ch);            // 防止重复调用           listeners = cbs.remove.length + 1;           // 创建删除的回调函数           rm = createRmCb(ch.elm!, listeners);           for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);           // 执行remove钩子函数           const removeHook = ch?.data?.hook?.remove;           if (isDef(removeHook)) {             removeHook(ch, rm);           } else {             // 如果没有钩子函数,直接调用删除元素的方法             rm();           }         } else {           // Text node           // 如果是文本节点,直接是调用删除元素的方法           api.removeChild(parentElm, ch.elm!);         }       }     }   }    function updateChildren(     parentElm: Node,     oldCh: VNode[],     newCh: VNode[],     insertedVnodeQueue: VNodeQueue   ) {     let oldStartIdx = 0;     let newStartIdx = 0;     let oldEndIdx = oldCh.length - 1;     let oldStartVnode = oldCh[0];     let oldEndVnode = oldCh[oldEndIdx];     let newEndIdx = newCh.length - 1;     let newStartVnode = newCh[0];     let newEndVnode = newCh[newEndIdx];     let oldKeyToIdx: KeyToIndexMap | undefined;     let idxInOld: number;     let elmToMove: VNode;     let before: any;      while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {       if (oldStartVnode == null) {         oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left       } else if (oldEndVnode == null) {         oldEndVnode = oldCh[--oldEndIdx];       } else if (newStartVnode == null) {         newStartVnode = newCh[++newStartIdx];       } else if (newEndVnode == null) {         newEndVnode = newCh[--newEndIdx];       } else if (sameVnode(oldStartVnode, newStartVnode)) {         patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);         oldStartVnode = oldCh[++oldStartIdx];         newStartVnode = newCh[++newStartIdx];       } else if (sameVnode(oldEndVnode, newEndVnode)) {         patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);         oldEndVnode = oldCh[--oldEndIdx];         newEndVnode = newCh[--newEndIdx];       } else if (sameVnode(oldStartVnode, newEndVnode)) {         // Vnode moved right         patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);         api.insertBefore(           parentElm,           oldStartVnode.elm!,           api.nextSibling(oldEndVnode.elm!)         );         oldStartVnode = oldCh[++oldStartIdx];         newEndVnode = newCh[--newEndIdx];       } else if (sameVnode(oldEndVnode, newStartVnode)) {         // Vnode moved left         patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);         api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);         oldEndVnode = oldCh[--oldEndIdx];         newStartVnode = newCh[++newStartIdx];       } else {         if (oldKeyToIdx === undefined) {           oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);         }         idxInOld = oldKeyToIdx[newStartVnode.key as string];         if (isUndef(idxInOld)) {           // New element           api.insertBefore(             parentElm,             createElm(newStartVnode, insertedVnodeQueue),             oldStartVnode.elm!           );         } else {           elmToMove = oldCh[idxInOld];           if (elmToMove.sel !== newStartVnode.sel) {             api.insertBefore(               parentElm,               createElm(newStartVnode, insertedVnodeQueue),               oldStartVnode.elm!             );           } else {             patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);             oldCh[idxInOld] = undefined as any;             api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);           }         }         newStartVnode = newCh[++newStartIdx];       }     }     if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {       if (oldStartIdx > oldEndIdx) {         before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;         addVnodes(           parentElm,           before,           newCh,           newStartIdx,           newEndIdx,           insertedVnodeQueue         );       } else {         removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);       }     }   }      function patchVnode(     oldVnode: VNode,     vnode: VNode,     insertedVnodeQueue: VNodeQueue   ) {     const hook = vnode.data?.hook;     // 首先执行prepatch钩子函数     hook?.prepatch?.(oldVnode, vnode);     const elm = (vnode.elm = oldVnode.elm)!;     const oldCh = oldVnode.children as VNode[];     const ch = vnode.children as VNode[];     // 如果新旧vnode相同返回     if (oldVnode === vnode) return;     if (vnode.data !== undefined) {       // 执行模块的update钩子函数       for (let i = 0; i < cbs.update.length; ++i)         cbs.update[i](oldVnode, vnode);       // 执行update钩子函数       vnode.data.hook?.update?.(oldVnode, vnode);     }      // 如果是vnode.text 未定义     if (isUndef(vnode.text)) {       // 如果是新旧节点都有 children       if (isDef(oldCh) && isDef(ch)) {         // 使用diff算法对比子节点,更新子节点         if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);       } else if (isDef(ch)) {         // 如果新节点有children,旧节点没有children         // 如果旧节点有text,清空dom 元素的内容         if (isDef(oldVnode.text)) api.setTextContent(elm, "");         // 批量添加子节点         addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);       } else if (isDef(oldCh)) {         // 如果旧节点有children,新节点没有children         // 批量移除子节点         removeVnodes(elm, oldCh, 0, oldCh.length - 1);       } else if (isDef(oldVnode.text)) {         // 如果旧节点有 text,清空 DOM 元素         api.setTextContent(elm, "");       }     } else if (oldVnode.text !== vnode.text) {       // 如果没有设置 vnode.text       if (isDef(oldCh)) {         // 如果旧节点有children,移除         removeVnodes(elm, oldCh, 0, oldCh.length - 1);       }       // 设置 DOM 元素的textContent为 vnode.text       api.setTextContent(elm, vnode.text!);     }     // 最后执行postpatch钩子函数     hook?.postpatch?.(oldVnode, vnode);   }    // init 内部返回 patch 函数,把vnode渲染成真实dom,并返回vnode   return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {     let i: number, elm: Node, parent: Node;     // 保存新插入的节点的队列,为了触发钩子函数     const insertedVnodeQueue: VNodeQueue = [];     // 执行模块的pre 钩子函数     for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();     // 如果oldVnode不是Vnode,创建Vnode并设置elm     if (!isVnode(oldVnode)) {       // 把Dom元素转化成空的Vnode       oldVnode = emptyNodeAt(oldVnode);     }     // 如果新旧节点是相同节点     if (sameVnode(oldVnode, vnode)) {       // 找节点的差异并更新DOM,这里的原理就是diff算法       patchVnode(oldVnode, vnode, insertedVnodeQueue);     } else {       // 如果新旧节点不同,vnode创建对应的DOM        // 获取当前的DOM元素       elm = oldVnode.elm!;       // 获取父元素       parent = api.parentNode(elm) as Node;       // 创建Vnode对应的DOM元素,并触发init/create 钩子函数       createElm(vnode, insertedVnodeQueue);        if (parent !== null) {         // 如果父元素不为空,把vnode对应的DOM插入到父元素中         api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));         // 移除旧节点         removeVnodes(parent, [oldVnode], 0, 0);       }     }     // 执行insert 钩子函数     for (i = 0; i < insertedVnodeQueue.length; ++i) {       insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);     }     // 执行模块的post 钩子函数     for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();     // 返回vnode 作为下次更新的旧节点     return vnode;   }; }

接下来,我们分开介绍init方法中的内容。

将各个模块的钩子方法,挂到统一的钩子上

初始化的时候,将每个 modules 下的相应的钩子都追加都一个数组里面

在进行 patch 的各个阶段,触发对应的钩子去处理对应的事情

这种方式比较方便扩展。新增钩子的时候,不需要更改到主要的流程

这些模块的钩子,主要用在更新节点的时候,会在不同的生命周期里面去触发对应的钩子,从而更新这些模块。

let i: number; let j: number; const cbs: ModuleHooks = {   create: [],   update: [],   remove: [],   destroy: [],   pre: [],   post: [], }; // 把传入的所有模块的钩子函数,统一存储到cbs对象中 // 最终构建的cbs对象的形式cbs = {create:[fn1,fn2],update:[],....} for (i = 0; i < hooks.length; ++i) {   // cbs.create= [], cbs.update = []...   cbs[hooks[i]] = [];   for (j = 0; j < modules.length; ++j) {     // modules 传入的模块数组     // 获取模块中的hook函数     // hook = modules[0][create]...     const hook = modules[j][hooks[i]];     if (hook !== undefined) {       // 把获取到的hook函数放入到cbs 对应的钩子函数数组中       (cbs[hooks[i]] as any[]).push(hook);     }   } }

patch方法

init 方法最后返回一个 patch 方法 。

主要的逻辑如下 :

// init 内部返回 patch 函数,把vnode渲染成真实dom,并返回vnode return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {   let i: number, elm: Node, parent: Node;   // 保存新插入的节点的队列,为了触发钩子函数   const insertedVnodeQueue: VNodeQueue = [];   // 执行模块的pre 钩子函数   for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();   // 如果oldVnode不是Vnode,创建Vnode并设置elm   if (!isVnode(oldVnode)) {     // 把Dom元素转化成空的Vnode     oldVnode = emptyNodeAt(oldVnode);   }   // 如果新旧节点是相同节点   if (sameVnode(oldVnode, vnode)) {     // 找节点的差异并更新DOM,这里的原理就是diff算法     patchVnode(oldVnode, vnode, insertedVnodeQueue);   } else {     // 如果新旧节点不同,vnode创建对应的DOM      // 获取当前的DOM元素     elm = oldVnode.elm!;     // 获取父元素     parent = api.parentNode(elm) as Node;     // 创建Vnode对应的DOM元素,并触发init/create 钩子函数     createElm(vnode, insertedVnodeQueue);      if (parent !== null) {       // 如果父元素不为空,把vnode对应的DOM插入到父元素中       api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));       // 移除旧节点       removeVnodes(parent, [oldVnode], 0, 0);     }   }   // 执行insert 钩子函数   for (i = 0; i < insertedVnodeQueue.length; ++i) {     insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);   }   // 执行模块的post 钩子函数   for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();   // 返回vnode 作为下次更新的旧节点   return vnode; };

patchVnode方法

主要的逻辑如下 :

这里在对比的时候,就会直接更新元素内容了。并不会等到对比完才更新 DOM 元素。

     function patchVnode(     oldVnode: VNode,     vnode: VNode,     insertedVnodeQueue: VNodeQueue   ) {     const hook = vnode.data?.hook;     // 首先执行prepatch钩子函数     hook?.prepatch?.(oldVnode, vnode);     const elm = (vnode.elm = oldVnode.elm)!;     const oldCh = oldVnode.children as VNode[];     const ch = vnode.children as VNode[];     // 如果新旧vnode相同返回     if (oldVnode === vnode) return;     if (vnode.data !== undefined) {       // 执行模块的update钩子函数       for (let i = 0; i < cbs.update.length; ++i)         cbs.update[i](oldVnode, vnode);       // 执行update钩子函数       vnode.data.hook?.update?.(oldVnode, vnode);     }      // 如果是vnode.text 未定义     if (isUndef(vnode.text)) {       // 如果是新旧节点都有 children       if (isDef(oldCh) && isDef(ch)) {         // 使用diff算法对比子节点,更新子节点         if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);       } else if (isDef(ch)) {         // 如果新节点有children,旧节点没有children         // 如果旧节点有text,清空dom 元素的内容         if (isDef(oldVnode.text)) api.setTextContent(elm, "");         // 批量添加子节点         addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);       } else if (isDef(oldCh)) {         // 如果旧节点有children,新节点没有children         // 批量移除子节点         removeVnodes(elm, oldCh, 0, oldCh.length - 1);       } else if (isDef(oldVnode.text)) {         // 如果旧节点有 text,清空 DOM 元素         api.setTextContent(elm, "");       }     } else if (oldVnode.text !== vnode.text) {       // 如果没有设置 vnode.text       if (isDef(oldCh)) {         // 如果旧节点有children,移除         removeVnodes(elm, oldCh, 0, oldCh.length - 1);       }       // 设置 DOM 元素的textContent为 vnode.text       api.setTextContent(elm, vnode.text!);     }     // 最后执行postpatch钩子函数     hook?.postpatch?.(oldVnode, vnode);   }

updateChildren 方法

patchVnode 里面最重要的方法,也是整个 diff 里面的最核心方法。

主要的逻辑如下:

不提供 key 的情况下,如果只是顺序改变的情况,例如第一个移动到末尾。这个时候,会导致其实更新了后面的所有元素。

// 更新子节点   function updateChildren(       parentElm: Node,       oldCh: Array<VNode>,       newCh: Array<VNode>,       insertedVnodeQueue: VNodeQueue   ) {       let oldStartIdx = 0,           newStartIdx = 0;        let oldEndIdx = oldCh.length - 1;        let oldStartVnode = oldCh[0];       let oldEndVnode = oldCh[oldEndIdx];        let newEndIdx = newCh.length - 1;        let newStartVnode = newCh[0];       let newEndVnode = newCh[newEndIdx];        let oldKeyToIdx: any;       let idxInOld: number;       let elmToMove: VNode;       let before: any;        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {           if (oldStartVnode == null) {               // 移动索引,因为节点处理过了会置空,所以这里向右移               oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left           } else if (oldEndVnode == null) {               // 原理同上               oldEndVnode = oldCh[--oldEndIdx];           } else if (newStartVnode == null) {               // 原理同上               newStartVnode = newCh[++newStartIdx];           } else if (newEndVnode == null) {               // 原理同上               newEndVnode = newCh[--newEndIdx];           } else if (sameVnode(oldStartVnode, newStartVnode)) {               // 从左对比               patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);               oldStartVnode = oldCh[++oldStartIdx];               newStartVnode = newCh[++newStartIdx];           } else if (sameVnode(oldEndVnode, newEndVnode)) {               // 从右对比               patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);               oldEndVnode = oldCh[--oldEndIdx];               newEndVnode = newCh[--newEndIdx];           } else if (sameVnode(oldStartVnode, newEndVnode)) {               // Vnode moved right               // 最左侧 对比 最右侧               patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);               // 移动元素到右侧指针的后面               api.insertBefore(                   parentElm,                   oldStartVnode.elm as Node,                   api.nextSibling(oldEndVnode.elm as Node)               );               oldStartVnode = oldCh[++oldStartIdx];               newEndVnode = newCh[--newEndIdx];           } else if (sameVnode(oldEndVnode, newStartVnode)) {               // Vnode moved left               // 最右侧对比最左侧               patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);               // 移动元素到左侧指针的后面               api.insertBefore(                   parentElm,                   oldEndVnode.elm as Node,                   oldStartVnode.elm as Node               );               oldEndVnode = oldCh[--oldEndIdx];               newStartVnode = newCh[++newStartIdx];           } else {               // 首尾都不一样的情况,寻找相同 key 的节点,所以使用的时候加上key可以调高效率               if (oldKeyToIdx === undefined) {                   oldKeyToIdx = createKeyToOldIdx(                       oldCh,                       oldStartIdx,                       oldEndIdx                   );               }               idxInOld = oldKeyToIdx[newStartVnode.key as string];                if (isUndef(idxInOld)) {                   // New element                   // 如果找不到 key 对应的元素,就新建元素                   api.insertBefore(                       parentElm,                       createElm(newStartVnode, insertedVnodeQueue),                       oldStartVnode.elm as Node                   );                   newStartVnode = newCh[++newStartIdx];               } else {                   // 如果找到 key 对应的元素,就移动元素                   elmToMove = oldCh[idxInOld];                   if (elmToMove.sel !== newStartVnode.sel) {                       api.insertBefore(                           parentElm,                           createElm(newStartVnode, insertedVnodeQueue),                           oldStartVnode.elm as Node                       );                   } else {                       patchVnode(                           elmToMove,                           newStartVnode,                           insertedVnodeQueue                       );                       oldCh[idxInOld] = undefined as any;                       api.insertBefore(                           parentElm,                           elmToMove.elm as Node,                           oldStartVnode.elm as Node                       );                   }                   newStartVnode = newCh[++newStartIdx];               }           }       }       // 新旧数组其中一个到达末尾       if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {           if (oldStartIdx > oldEndIdx) {               // 如果旧数组先到达末尾,说明新数组还有更多的元素,这些元素都是新增的,说以一次性插入               before =                   newCh[newEndIdx + 1] == null                       ? null                       : newCh[newEndIdx + 1].elm;               addVnodes(                   parentElm,                   before,                   newCh,                   newStartIdx,                   newEndIdx,                   insertedVnodeQueue               );           } else {               // 如果新数组先到达末尾,说明新数组比旧数组少了一些元素,所以一次性删除               removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);           }       }   }

addVnodes方法

主要功能就是添加 Vnodes 到 真实 DOM 中。

function addVnodes(   parentElm: Node,   before: Node | null,   vnodes: VNode[],   startIdx: number,   endIdx: number,   insertedVnodeQueue: VNodeQueue ) {   for (; startIdx <= endIdx; ++startIdx) {     const ch = vnodes[startIdx];     if (ch != null) {       api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);     }   } }

removeVnodes方法

主要逻辑如下:

function invokeDestroyHook(vnode: VNode) {   const data = vnode.data;   if (data !== undefined) {     // 执行的destroy 钩子函数     data?.hook?.destroy?.(vnode);     // 调用模块的destroy钩子函数     for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);     // 执行子节点的destroy钩子函数     if (vnode.children !== undefined) {       for (let j = 0; j < vnode.children.length; ++j) {         const child = vnode.children[j];         if (child != null && typeof child !== "string") {           invokeDestroyHook(child);         }       }     }   } }
//创建一个删除的回调,多次调用这个回调,直到监听器都没了,就删除元素 function createRmCb(childElm: Node, listeners: number) {     return function rmCb() {       if (--listeners === 0) {         const parent = api.parentNode(childElm) as Node;         api.removeChild(parent, childElm);       }     };   }
function removeVnodes(   parentElm: Node,   vnodes: VNode[],   startIdx: number,   endIdx: number ): void {   for (; startIdx <= endIdx; ++startIdx) {     let listeners: number;     let rm: () => void;     const ch = vnodes[startIdx];     if (ch != null) {       // 如果sel 有值       if (isDef(ch.sel)) {         invokeDestroyHook(ch);          // 防止重复调用         listeners = cbs.remove.length + 1;         // 创建删除的回调函数         rm = createRmCb(ch.elm!, listeners);         for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);         // 执行remove钩子函数         const removeHook = ch?.data?.hook?.remove;         if (isDef(removeHook)) {           removeHook(ch, rm);         } else {           // 如果没有钩子函数,直接调用删除元素的方法           rm();         }       } else {         // Text node         // 如果是文本节点,直接是调用删除元素的方法         api.removeChild(parentElm, ch.elm!);       }     }   } }

createElm方法

将 vnode 转换成真正的 DOM 元素。

主要逻辑如下:

  function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {    let i: any;    let data = vnode.data;    if (data !== undefined) {      // 执行init钩子函数      const init = data.hook?.init;      if (isDef(init)) {        init(vnode);        data = vnode.data;      }    }    // 把vnode转换成真实dom对象(没有渲染到页面)    const children = vnode.children;    const sel = vnode.sel;    if (sel === "!") {      // 如果选择器是!,创建注释节点      if (isUndef(vnode.text)) {        vnode.text = "";      }      vnode.elm = api.createComment(vnode.text!);    } else if (sel !== undefined) {      // 如果选择器不为空      // 解析选择器      // Parse selector      const hashIdx = sel.indexOf("#");      const dotIdx = sel.indexOf(".", hashIdx);      const hash = hashIdx > 0 ? hashIdx : sel.length;      const dot = dotIdx > 0 ? dotIdx : sel.length;      const tag =        hashIdx !== -1 || dotIdx !== -1          ? sel.slice(0, Math.min(hash, dot))          : sel;      const elm = (vnode.elm =        isDef(data) && isDef((i = data.ns))          ? api.createElementNS(i, tag, data)          : api.createElement(tag, data));      if (hash < dot) elm.setAttribute("id", sel.slice(hash + 1, dot));      if (dotIdx > 0)        elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " "));      // 执行模块的create钩子函数      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);      // 如果vnode中有子节点,创建子Vnode对应的DOM元素并追加到DOM树上      if (is.array(children)) {        for (i = 0; i < children.length; ++i) {          const ch = children[i];          if (ch != null) {            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));          }        }      } else if (is.primitive(vnode.text)) {        // 如果vode的text值是string/number,创建文本节点并追加到DOM树        api.appendChild(elm, api.createTextNode(vnode.text));      }      const hook = vnode.data!.hook;      if (isDef(hook)) {        // 执行传入的钩子 create        hook.create?.(emptyNode, vnode);        if (hook.insert) {          insertedVnodeQueue.push(vnode);        }      }    } else {      // 如果选择器为空,创建文本节点      vnode.elm = api.createTextNode(vnode.text!);    }    // 返回新创建的DOM    return vnode.elm;  }

到此,关于“Virtual DOM作用是什么”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     801人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     348人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     311人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     432人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     220人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-前端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯