最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Virtual DOM 的实现原理

    正文概述 掘金(Ada)   2020-12-27   246

    概述

    • 了解什么是虚拟 DOM,以及虚拟DOM的作用
    • Snabbdom 的基本使用
    • Snabbdom的源码解析

    什么是虚拟 DOM

    • Virtual DOM(虚拟DOM),是由普通的JS对象来描述DOM对象,因为不是真实的DOM对象,所以叫Virtual DOM
    • 真实的DOM成员
    let element = document.querySelector('#app') 
    let s = ''
    for (var key in element) {
    s += key + ',' }
    console.log(s)
    // 打印结果 align,title,...
    ...
    

    可以发现一个DOM对象它的成员非常多,所以创建一个DOM对象的成本是非常高的。

    • 可以使用Virtual DOM来描述真实DOM,示例
    {
      sel: "div",
      data: {},
      children: undefined,
      text: "Hello Virtual DOM",
      elm: undefined,
      key: undefined
    }
    

    可见创建虚拟DOM的开销较小。

    为什么使用虚拟DOM

    • 手动操作DOM比较麻烦,还需要考虑浏览器兼容性问题,虽然有jQuery等库简化DOM操作,但是随着项目的复杂DOM操作复杂提升
    • 为了简化DOM的复杂操作于是出现了各种MVVM框架,MVVM框架解决了视图和状态的同步问题,数据变视图变,视图变数据变。
    • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题(数据变化后无法获取上一次的状态,只好删除重建),于是VirtualDOM 出现了
    • Virtual DOM的好处是当状态改变时不需要立即更新DOM,只需创建一个虚拟树来描述DOM,Virtual DOM内部将弄清楚如何有效(diff)的更新DOM
    • 参考github上的 virtual dom的描述
      • 虚拟DOM可以维护程序的状态,跟踪上一次的状态
      • 通过比较前后两次状态的差异更新真实的 DOM

    虚拟DOM的作用

    • 维护视图和状态的关系
    • 复杂视图情况下提升渲染性能
    • 除了渲染DOM以外,还可以实现SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等

    Virtual DOM 的实现原理

    Virtual DOM库

    • Snabbdom
      • Vue 2.x 内部使用的 Virtual DOM就是改造的Snabdom
      • 大约 200 SLOC (single line of code)
      • 通过模块可扩展
      • 源码使用 TypeScript 开发
      • 最快的 Virtual DOM 之一
    • virtual-dom
      • 最早的虚拟DOM开源库

    Snabbdom 基本使用

    创建项目

    • 打包工具为了方便使用 parcel
    • 创建项目,并安装 parcel
    1 # 创建项目目录
    2 md snabbdom-demo
    3 # 进入项目目录
    4 cd snabbdom-demo
    5 # 创建 package.json
    6 yarn init -y
    7 # 本地安装 parcel
    8 yarn add parcel-bundler
    
    • 配置 package.json 的 script
    "scripts": {
      "dev": "parcel index.html --open",
      "build": "parcel build index.html"
    }
    
    • 创建目录结构
    | index.html
    | package.json
    └─src
        01-basicusage.js
    

    导入Snabbdom

    Snabbdom 文档

    • 看文档的意义
      • 学习任何一个库都要先看文档
      • 通过文档了解库的作用
      • 看文档中提供的示例,自己快速实现一个demo
      • 通过文档查看API的使用
    • 文档地址

    github.com/snabbdom/sn…

    中文翻译

    安装 Snabbdom

    yarn add snabbdom
    

    导入 Snabbdom

    • Snabbdom 的官网 demo 中导入使用的是 commonjs 模块化语法,我们使用更流行的 ES6 模块化的语法 import
    • 关于模块化的语法请参考阮一峰老师的Module 的语法
    • ES6 模块与 CommonJS 模块的差异
    import { init, h, thunk } from 'snabbdom'
    
    • init()是一个高阶函数,返回patch()
    • h()返回虚拟节点 VNode,这个函数我们在使用Vue.js的时候见过
    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app')
    
    • thunk()是一种优化策略,可以在处理不可变数据时使用
    • 注意:导入时不能使用 import anabbdom from 'snabbdom'
      • 原因: node_modules/src/snabbdom.ts末尾导出使用的语法是export导出API,没有使用 export default导出默认输出

    Virtual DOM 的实现原理

    代码演示

    1. hello world
    import { h, init } from 'snabbdom'
    
    // 1. hello world
    // 参数:数组,模块
    // 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM
    let patch = init([])
    // 第一个参数:标签+选择器
    // 第二个参数:如果是字符串的话就是标签中的内容
    // 返回值:VNode
    let vnode = h('div#container.cls', { 
      hook: {
        init (vnode) {
          console.log(vnode.elm)
        },
        create (emptyVnode, vnode) {
          console.log(vnode.elm)
        }
      }
    }, 'Hello World')
    
    let app = document.querySelector('#app')
    // 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
    // 第二个参数:VNode
    // 返回值:VNde
    let oldVnode = patch(app, vnode)
    
    // 假设的时刻
    vnode = h('div', 'Hello Snabbdom')
    
    patch(oldVnode, vnode)
    
    1. div中放置子元素 h1,p
    // 2. div中放置子元素 h1,p
    import { h, init } from 'snabbdom'
    
    let patch = init([])
    
    let vnode = h('div#container', [
      h('h1', 'Hello Snabbdom'),
      h('p', '这是一个p标签')
    ])
    
    let app = document.querySelector('#app')
    
    let oldVnode = patch(app, vnode)
    
    setTimeout(() => {
      vnode = h('div#container', [
        h('h1', 'Hello World'),
        h('p', 'Hello P')
      ])
      patch(oldVnode, vnode)
    
      // 清空页面元素 -- 错误
      // patch(oldVnode, null)
      // h('!')创建注释节点
      patch(oldVnode, h('!'))
    }, 2000);
    

    模块

    Snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块

    常用模块

    • 官方提供了6个模块
      • attributes
        • 设置DOM元素的属性,使用 setAttribute()
        • 处理布尔类型的属性
      • props
        • 和attribute模块相似,设置DOM元素的属性 element[attr] = value
        • 不处理布尔类型的属性
      • class
        • 切换类样式
        • 注意:给元素设置类样式是通过 sel 选择器
      • dataset
        • 设置 data-* 的自定义属性
      • eventlisteners
        • 注册和移除事件
      • style
        • 设置行内样式,支持动画
        • delayed/remove/destroy

    模块使用

    • 模块使用步骤
      • 导入需要的模块
      • init() 中注册模块
      • 使用 h() 函数创建VNode的时候,可以把第二个参数设置为对象,其他参数往后移
    • 代码演示
    import { init, h } from 'snabbdom'
    // 1. 导入模块
    import style from 'snabbdom/modules/style'
    import eventlisteners from 'snabbdom/modules/eventlisteners'
    // 2. 注册模块
    let patch = init([
      style,
      eventlisteners
    ])
    // 3. 使用 h() 函数的第二个参数传入模块需要的数据(对象)
    let vnode = h('div', {
      style: {
        backgroundColor: 'red'
      },
      on: {
        click: eventHandler
      }
    }, [
      h('h1', 'Hello Snabbdom'),
      h('p', '这是p标签')
    ])
    
    function eventHandler () {
      console.log('点击我了')
    }
    
    let app = document.querySelector('#app')
    
    let oldVnode = patch(app, vnode)
    
    
    vnode = h('div', 'hello')
    patch(oldVnode, vnode)
    

    Snabbdom 源码解析

    概述

    如何学习源码

    • 先宏观了解(核心执行过程)
    • 带着目标看源码
    • 看源码的过程要不求甚解(围绕主线)
    • 调试
    • 参考资料

    Snabbdom 的核心

    • 使用h()函数创建Javascript对象(VNode)描述真实的DOM
    • init() 设置模块,创建patch()
    • patch() 比较新旧两个VNode
    • 把变化的内容更新到真实的DOM树上

    Snabbdom 源码

    • 源码地址

      • github.com/snabbdom/sn…
    • src 目录结构

    │ h.ts	h() 函数,用来创建 VNode
    │ hooks.ts	所有钩子函数的定义
    │ htmldomapi.ts	对 DOM API 的包装
    │ is.ts	判断数组和原始值的函数
    │ jsx-global.d.ts	jsx 的类型声明文件
    │ jsx.ts	处理 jsx
    │ snabbdom.bundle.ts	入口,已经注册了模块
    │ snabbdom.ts	初始化,返回 init/h/thunk
    │ thunk.ts	优化处理,对复杂视图不可变值得优化
    │ tovnode.ts	DOM 转换成 VNode
    │ vnode.ts	虚拟节点定义
    │ 
    ├─helpers
    │ 	attachto.ts	定义了 vnode.ts 中 AttachData 的数据结构
    │
    └─modules	所有模块定义
    	attributes.ts 
    	class.ts
        	dataset.ts
    	eventlisteners.ts
    	hero.ts example 中使用到的自定义钩子 
        	module.ts 定义了模块中用到的钩子函数 
        	props.ts
    	style.ts
    

    h 函数

    • h()函数介绍
      • 在使用Vue的时候见过h()函数
      new Vue({
        router,
        store,
        render: h => h(App)
      }).$mount('#app') 
      
      • h()函数最早见于 hypescript,使用 JavaScript 创建超文本
      • Snabbdom中的h()函数不是用来创建超文本,而是创建VNode
    • 函数重载
      • 概念
        • 参数个数或类型不同的函数
        • JavaScript中没有重载的概念
        • TypeScript中有重载,不过重载的实现还是通过代码调整参数
      • 重载的示例
      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) 
      
      • 源码位置:src/h.ts
      // h 函数的重载
      export function h(sel: string): VNode;
      export function h(sel: string, data: VNodeData | null): VNode; 
      export function h(sel: string, children: VNodeChildren): VNode; 
      export function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNode;
      export function h(sel: any, b?: any, c?: any): VNode {
        var data: VNodeData = {}, children: any, text: any, i: number; // 处理参数,   实现重载的机制
        if (c !== undefined) {
          // 处理三个参数的情况 
          // sel、data、children/text
          if (b !== null) { data = b; }
          if (is.array(c)) { children = c; }
          // 如果 c 是字符串或者数字
          else if (is.primitive(c)) { text = c; } // 如果 c 是 VNode
          else if (c && c.sel) { children = [c]; }
        } else if (b !== undefined && b !== null) { 
          // 处理两个参数的情况
          // 如果 b 是数组
          if (is.array(b)) { children = b; }
          // 如果 b 是字符串或者数字
          else if (is.primitive(b)) { text = b; } // 如果 b 是 VNode
          else if (b && b.sel) { children = [b]; } else { data = b; }
        }
        if (children !== undefined) {
        // 处理 children 中的原始值(string/number) 
        for (i = 0; i < children.length; ++i) {
          // 如果 child 是 string/number,创建文本节点
          if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
          } 
        }
        if (
        sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' && (sel.length === 3 ||   sel[3] === '.' || sel[3] === '#')
        ){
          // 如果是 svg,添加命名空间
          addNS(data, children, sel);
        }
        // 返回 VNode
        return vnode(sel, data, children, text, undefined);
      };
      // 导出模块
      export default h;
      
      核心:调用vnode创建虚拟节点并返回

    vnode 函数

    • 一个VNode就是一个虚拟节点用来描述一个DOM元素,如果这个VNode有childen就是Virtual DOM
    • 源码位置:src/vnode.ts
    export interface VNode { 
      // 选择器
      sel: string | undefined;
      // 节点数据:属性/样式/事件等
      data: VNodeData | undefined;
      // 子节点,和 text 只能互斥
      children: Array<VNode | string> | undefined; 
      // 记录 vnode 对应的真实 DOM
      elm: Node | undefined;
      // 节点中的内容,和 children 只能互斥
      text: string | undefined;
      // 优化用
      key: Key | undefined;
    }
    
    export function vnode(sel: string | undefined,
                          data: any | undefined,
                          children: Array<VNode | string> | undefined,
                          text: string | undefined,
                          elm: Element | Text | undefined): VNode {
      let key = data === undefined ? undefined : data.key;
      return {sel, data, children, text, elm, key};
    }
    export default vnode;
    

    VNode 渲染真实DOM

    snabbdom

    • patch(oldVnode, newVnode)
      • 打补丁,把新节点中变化的内容渲染到真实的DOM,最后返回新节点作为下一次处理的旧节点

    patch整体执行过程

    • 对比新旧VNode是否是相同节点(节点的key和sel相同)
    • 如果不是相同节点,删除之前的内容,重新渲染
    • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVNode的text不同,直接更新文本内容
    • 如果新的VNode有children,判断子节点是否有变化,判断子节点的过程使用的就是diff算法
    • diff过程只进行同层级比较

    Virtual DOM 的实现原理

    init()

    • 功能:init(modules, domApi),返回patch()函数(高阶函数)
    • 为什么使用高阶函数
      • 因为patch()函数在外部会调用多次,每次调用依赖一些参数,比如:modules/domApi/cbs
      • 通过高阶函数让init()内部形成闭包,返回的patch()可以访问到modules/domApi/cbs,而不需要重新创建
    • init()在返回patch()之前,首先收集了所有模块中的钩子函数存储到cbs对象中
    • 源码位置:src/snabbdom.ts
    const hooks: (keyof Module)[] = ['create', 'update', 'remove',
    'destroy', 'pre', 'post'];
    export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
      let i: number, j: number, cbs = ({} as ModuleHooks);
      // 初始化 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[hooks[i]] = [];
        for (j = 0; j < modules.length; ++j) {
          // const hook = modules[0]['create']
          const hook = modules[j][hooks[i]];
          if (hook !== undefined) {
            (cbs[hooks[i]] as Array<any>).push(hook); }
          } 
        }
      }
    ......
    ......
    ......
      return function patch(oldVnode: VNode | Element, vnode: VNode): VNode
    {}
    }
    

    patch()

    • 功能:
      • 传入新旧VNode,对比差异,把差异渲染到DOM
      • 返回新的VNode,作为下一次patch()的oldVnode
    • 执行过程:
    1. 首先执行模块中的钩子函数 pre
    2. 如果oldVnode和vnode相同(key和sel相同)
      • 调用patchVnode(),找节点的差异并更新DOM
    3. 如果 oldVnode 是DOM元素
      • 把DOM元素转换成oldVnode
      • 调用createElm()把vnode转换成真实DOM,记录到vnode.elm
      • 把刚创建的DOM元素插入到parent中
      • 移除老节点
      • 触发用户设置的create钩子函数
    • 源码位置: src/snabbdom.ts
    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);
      }
      // 如果新旧节点是相同节点(key 和 sel 相同) 
      if (sameVnode(oldVnode, vnode)) {
        // 找节点的差异并更新 DOM
        patchVnode(oldVnode, vnode, insertedVnodeQueue);
      } else {
        // 如果新旧节点不同,vnode 创建对应的 DOM 
        // 获取当前的 DOM 元素
        elm = oldVnode.elm!;
        parent = api.parentNode(elm);
        // 触发 init/create 钩子函数,创建 DOM 
        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;
    };
    

    createElm

    • 功能
      • createElm(vnode, insertedVnodeQueue),返回创建的DOM元素
      • 创建vnode对应的DOM元素
    • 执行过程
      • 首先触发用户设置的init钩子函数
      • 如果选择器是!,创建注释节点
      • 如果选择器为空,创建文本节点
      • 如果选择器不为空
        • 解析选择器,设置标签的id和class属性
        • 执行模块的create钩子函数
        • 如果vnode有children,创建children的vnode对应的DOM,追加到DOM树
        • 如果vnode的text值是string/number,创建文本节点并追加到DOM树
        • 执行用户设置的create钩子函数
        • 如果有用户设置的insert函数,把vnode添加到队列中
    • 源码位置: src/snabbdom.ts
    function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
      let i: any, data = vnode.data; 
      if (data !== undefined) {
        // 执行用户设置的 init 钩子函数 
        const init = data.hook?.init; 
        if (isDef(init)) {
          init(vnode);
          data = vnode.data; 
        }
      }
      let children = vnode.children, 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)
          : api.createElement(tag);
       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)) {
          // 如果 vnode 的 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) {
            // 把 vnode 添加到队列中,为后续执行 insert 钩子做准备
            insertedVnodeQueue.push(vnode); 
          }
        }
      } else {
        // 如果选择器为空,创建文本节点
        vnode.elm = api.createTextNode(vnode.text!);
      }
      // 返回新创建的 DOM
      return vnode.elm; 
     }
    
    • 总结

    Virtual DOM 的实现原理

    addVnodes 和 removeVnodes

    patchVnode

    • 功能:
      • patchVnode(oldVnode, vnode, insertedVnodeQueue)
      • 对比oldVnode和vnode的差异,把差异渲染到DOM
    • 执行过程:
      • 首先执行用户设置的prepatch钩子函数
      • 执行create钩子函数
        • 首先执行模块的create钩子函数
        • 然后执行用户设置的create钩子函数
      • 如果vnode.text未定义
        • 如果oldVnode.children和vnode.children都有值
          • 调用updateChildern()
          • 使用diff算法对比子节点,更新子节点
        • 如果vnode.children有值,oldVnode.children无值
          • 清空DOM元素
          • 调用addVnodes(),批量移除子节点
        • 如果oldVnode.childern有值,vnode.children无值
          • 调用removeVnodes(),批量移除节点
        • 如果oldVonde.text有值
          • 清空DOM元素的内容
      • 如果设置了vnode.text并且和oldVnode.text不等
        • 如果老节点有子节点,全部移除
        • 设置DOM元素的textContent为vnode.text
      • 最后执行用户设置的postpatch钩子函数

    Virtual DOM 的实现原理

    • 源码位置: src/snabbdom.ts
    function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue:
    VNodeQueue) {
      const hook = vnode.data?.hook;
      // 首先执行用户设置的 prepatch 钩子函数 hook?.prepatch?.(oldVnode, vnode);
      const elm = vnode.elm = oldVnode.elm!; let oldCh = oldVnode.children as    VNode[]; 
      let 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

    • 功能:
      • diff算法的核心,对比新旧节点的children,更新DOM
    • 执行过程:
      • 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二棵树的每一个节点比较,但是这样的时间复杂度为O(n^3)
      • 在DOM操作的时候我们很少很少把一个父节点移动/更新到某一个子节点
      • 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的时间复杂度为O(n)
      Virtual DOM 的实现原理
      • 在进行同级别比较的时候,首先会对新老节点数组的开始和结尾设置编辑索引,遍历的过程中移动索引
      • 在对开始和结束节点比较的时候,总共有四种情况
        • oldStartVnode / newStartVnode(旧开始节点 / 新开始节点)
        • oldEndVnode / new EndVnode(旧结束节点 / 新结束节点)
        • oldStartVnode / newEndVnode(旧开始节点 / 新结束节点)
        • oldEndVnode / newStartVnode(旧结束节点 / 新开始节点)
      Virtual DOM 的实现原理
      • 开始节点和结束节点比较,这两种情况类似
        • oldStartVnode / newStartVnode(旧开始节点 / 新开始节点)
        • oldEndVnode / new EndVnode(旧结束节点 / 新结束节点)
      • 如果 旧开始节点 / 新开始节点是相同节点(key和sel相同)
        • 调用patchVnode()对比和更新节点
        • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++
        Virtual DOM 的实现原理
      • 旧开始节点 / 新结束节点是相同节点
        • 调用patchVnode()对比和更新节点
        • 把旧开始节点对应的DOM元素,移动到右边
          • 更新索引
          Virtual DOM 的实现原理
      • 旧结束节点 / 新开始节点 是相同节点
        • 调用patchVnode()对比和更新节点
        • 把旧结束节点对应的DOM元素,移动到左边
        • 更新索引
        Virtual DOM 的实现原理
      • 如果不是以上四种情况
        • 遍历新节点,使用新开始节点的key在老节点数组中找相同节点
        • 如果没有找到,说明新开始节点是新节点
          • 创建新节点对应的DOM元素,插入到DOM树中
        • 如果找到了
          • 判断新节点和找到的老节点的sel选择器是否相同
          • 如果不相同,说明节点被修改了
            • 重新创建对应的DOM元素,插入到DOM树中
          • 如果相同,把 elmToMove对应的DOM元素,移动到左边
        Virtual DOM 的实现原理
      • 循环结束
        • 当老节点的所有子节点先遍历完(oldStartIdx > oldEndIdx),循环结束
        • 新节点的所有子节点先遍历完(newStartIdx > newEndIdx),循环结束
      • 如果老节点的数组先遍历完,说明新节点有剩余,把剩余节点批量插入到右边
      Virtual DOM 的实现原理
      • 如果新节点的数组先遍历完,说明老节点有剩余,把剩余节点批量删除
      Virtual DOM 的实现原理
    • 源码位置:src/snabbdom.ts

    模块源码

    • patch() -> patchVnode() -> updateChildren()
    • Snabbdom 为了保证核心库的精简,把处理元素的属性/事件/样式等工作,放置到模块中
    • 模块可以按需引入
    • 模块使用可以查看官方文档
    • 模块实现的核心是基于 Hooks

    Hooks

    • 预定义的钩子函数的名称
    • 源码位置:src/hooks.ts
    export interface Hooks {
      // patch 函数开始执行的时候触发
      pre?: PreHook;
      // createElm 函数开始之前的时候触发
      // 在把 VNode 转换成真实 DOM 之前触发
      init?: InitHook;
      // createElm 函数末尾调用
      // 创建完真实 DOM 后触发
      create?: CreateHook;
      // patch 函数末尾执行
      // 真实 DOM 添加到 DOM 树中触发
      insert?: InsertHook;
      // patchVnode 函数开头调用
      // 开始对比两个 VNode 的差异之前触发
      prepatch?: PrePatchHook;
      // patchVnode 函数开头调用
      // 两个 VNode 对比过程中触发,比  prepatch 稍晚 
      update?: UpdateHook;
      // patchVnode 的最末尾调用
      // 两个 VNode 对比结束执行
      postpatch?: PostPatchHook;
      // removeVnodes -> invokeDestroyHook 中调用 
      // 在删除元素之前触发,子节点的 destroy 也被触发 
      destroy?: DestroyHook;
      // removeVnodes 中调用 
      // 元素被删除的时候触发 
      remove?: RemoveHook; 
      // patch 函数的最后调用 
      // patch 全部执行完毕触发 
      post?: PostHook;
    }
    

    Modules

    模块文件的定义
    Snabbdom 提供的所有模块在:src/modules 文件夹下,主要模块有:

    • attributes.ts

    使用 setAttribute/removeAttribute 操作属性 能够处理 boolean 类型的属性

    • class.ts

    切换类样式

    • dataset.ts

    操作元素的 data-* 属性

    • eventlisteners.ts

    注册和移除事件

    • module.ts

    定义模块遵守的钩子函数

    • props.ts

    和 attributes.ts 类似,但是是使用 elm[attrName] = value 的方式操作属性

    • style.ts

    操作行内样式 可以使动画更平滑

    • hero.ts

    自定义的模块,examples/hero 示例中使用


    起源地下载网 » Virtual DOM 的实现原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元