当前位置:首页 > 网站技术 > 前端技术 > 正文内容

Vue.js 框架最为核心的 Virtual DOM 是如何实现的?

小彬2021-01-15前端技术151

1、虚拟DOM

Virtual DOM :由普通的js对象来描述DOM对象,因为不是真实的DOM对象,所以叫虚拟DOM

Snabbdom 就是Vue使用的虚拟DOM框架


1)真实DOM成员

 let element =document.querySelector('#app')
 let s=''
 for(let key in element){
    s += key +','
 }
//输出的就是真实的DOM
 console.log(s)

Vue.js 框架最为核心的 Virtual DOM 是如何实现的?

 2)可以使用Virtual DOM 来描述真实DOM

示例:
{
    sel:"div",
    data:{},
    children:undefined,
    text:"Hello Virtual DOM",
    elm:undefined,
    key:undefined
}

如上对比,创建一个虚拟的DOM的成本远远小于真实的DOM 


2、为什么使用Virtual DOM 

Vue.js 框架最为核心的 Virtual DOM 是如何实现的?


虚拟DOM的作用  Vue.js 框架最为核心的 Virtual DOM 是如何实现的?


Virtual DOM的开源库  Vue.js 框架最为核心的 Virtual DOM 是如何实现的?



3、Snabbdom的基本使用 

Vue.js 框架最为核心的 Virtual DOM 是如何实现的?    Vue.js 框架最为核心的 Virtual DOM 是如何实现的?  Vue.js 框架最为核心的 Virtual DOM 是如何实现的?



因为我使用的是npm 
所以和教程步骤不一样

npm install parcel-bundler

//snabbdom要安装指定版本才能使用本教程demo
npm i snabbdom@0.7.4

安装完成后,根据图示添加
"scripts": {
    "dev": "parcel index.html --open",  //--open 浏览器自动打开
    "build": "parcel build index.html"
  },



下面是两个解决snabbdom更新后兼容问题
https://www.jianshu.com/p/cff844f497a4
https://blog.csdn.net/tiandao451/article/details/107163507/


Snabbdom的使用

01-basicusage.js

// 导入snabbdom
import { h,init } from 'snabbdom'

// 参数:数组 模块
// 返回值:patch 函数,作用对比两个VNode的差异更新到真实DOM
let patch=init([])
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串的话就是标签中的内容
let vnode =h("div#container.cls",'Hello world') //自己创建的vnode
let app=document.querySelector('#app')  //初始的vnode

// 第一个参数:可以是DOM 元素,内部会把DOM元素转换成vnode
// 第二个参数:Vnode
// 返回值:Vnode
let oldVnode =patch(app,vnode)


// 假设时刻
vnode=h('div','hello snabbdom')

patch(oldVnode,vnode)
console.log(patch,vnode,oldVnode)
02-basicusage.js

// 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')
// 使用patch把虚拟DOM更新至真实DOM
let oldNode=patch(app,vnode)

setTimeout(()=>{
   vnode=h("div#container",[
     h("h1","3秒后的更新"),
     h('p',"hello new text")
   ])

   patch(oldNode,vnode)


  //  清空页面元素  
  // 1 错误做法
  // patch(oldNode,null)

  // 2 创建一个注释节点 h('!')
  patch(oldNode,h('!'))
},3000)


4、Snabbdom的模块

 Vue.js 框架最为核心的 Virtual DOM 是如何实现的?Vue.js 框架最为核心的 Virtual DOM 是如何实现的?

03-modules.js

// 1、导入模块
import {h,init} from 'snabbdom'
import style from 'snabbdom/modules/style'
import eventlistenters from 'snabbdom/modules/eventlisteners'
// 2、注册模块
let patch=init([
  style,
  eventlistenters
])

// 3、使用h()函数的第二个参数传入模块需要的数据(对象)
// 设置行内样式和注册事件
let vnode=h('div#container',{
  style:{
    backgroundColor:'red'
  },
  on:{
    click:eventHandler
  }
},[
  h('h1','hello snabbdom'),
  h('p','这是一个p标签')
])

function eventHandler(){
  console.log('点击我了')
}

let app=document.querySelector('#app')

// 使用patch把虚拟DOM更新至真实DOM
patch(app,vnode)



3、Snabbdom的源码解析


如何学习源码

先宏观了解

带着目标看源码

看源码的过程中要不求甚解

调试

参考资料


Snabbdom的核心

1)使用h()函数创建Javascript对象(VNode)描述真实DOM

2)init()设置模块,创建patch()

3)patch()比较新旧两个VNode (旧的VNode在前,新的VNode在后)

4)把变化的内容更新到真实DOM树上


Snabbdom源码地址

https://github.com/snabbdom/snabbdom

Vue.js 框架最为核心的 Virtual DOM 是如何实现的? 

重载的意思就是可以存在同名函数,根据传入参数的个数不同调用不同的函数。

c或者c++才有重载,js没有,ts的重载就和此例子一样的实现方法

function add(a,b,c){
    if(c == undefined){
        console.log(a+b)
    }else{
        console.log(a+b+c) 
    }
}
add(1,2) //3
add(1,2,3)//6

h.ts

//h()函数核心是 调用vnode函数,创建虚拟节点
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, 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
    data = b;
    // 如果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是数组
    if (is.array(b)) { children = b; }
    // 如果b是字符串或者数字
    else if (is.primitive(b)) { text = b; }
    // 如果b是vnode,vnode有sel属性
    else if (b && b.sel) { children = [b]; }
    else { data = b; }
  }
  if (children !== undefined) {
    // 处理children中的原始值(string/number)
    for (i = 0; i < children.length; ++i) {
      // 如果child是strin/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);
  }
  return vnode(sel, data, children, text, undefined);
};


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};
  // vnode最后返回的就是一个描述虚拟DOM的对象
}

vnode渲染真实DOM

Vue.js 框架最为核心的 Virtual DOM 是如何实现的?

init函数

const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
export {h} from './h';
export {thunk} from './thunk';
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.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 Array<any>).push(hook);
      }
    }
  }
 init 内部返回patch函数,把vnode渲染成真实DOM,并返回vnode
 return function patch(oldVnode: VNode | Element, vnode: VNode): VNode )


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);
    }
// 如果新旧节点是相同节点(key和sel相同)
    if (sameVnode(oldVnode, vnode)) {
      // 找节点的差异并更新DOM
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
    } else {
      // 如果新旧节点不同,vnode创建对应的DOM
      // 获取当前的DOM元素
      elm = oldVnode.elm as Node;
      parent = api.parentNode(elm);
      // 创建vnode对应的DOM元素,并触发init/create钩子函数
      createElm(vnode, insertedVnodeQueue);

      if (parent !== null) {
        // 如果父节点不为空,把vnode对应的DOM插入到文档中
        api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
        // 移除老节点
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }
 // 执行用户设置的insert钩子函数
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
    }
    // 执行模块的post钩子函数
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    // 返回vnode
    return vnode;
  };



createElm函数 

Vue.js 框架最为核心的 Virtual DOM 是如何实现的?

 function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any, data = vnode.data;
    if (data !== undefined) {
      // 执行用户设置的init钩子函数
      if (isDef(i = data.hook) && isDef(i = i.init)) {
        i(vnode);
        data = vnode.data;
      }
    }
    // 把vnode转换成真实DOM对象(没有渲染到页面)
    let children = vnode.children, sel = vnode.sel;
    if (sel === '!') {
        // 如果选择器是!,则创建注释节点
      if (isUndef(vnode.text)) {
        vnode.text = '';
      }
      vnode.elm = api.createComment(vnode.text as string);
    } 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 as VNodeData).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));
      }
      i = (vnode.data as VNodeData).hook; // Reuse variable
      if (isDef(i)) {
         // 执行用户传入的钩子create
        if (i.create) i.create(emptyNode, vnode);
        // 把vnode添加到队列中,为后续执行insert钩子做准备
        if (i.insert) insertedVnodeQueue.push(vnode);
      }
    } else {
      // 如果选择器为空,创建文本节点
      vnode.elm = api.createTextNode(vnode.text as string);
    }
     // 返回创建的DOM
    return vnode.elm;
  }

addVnodes和removeVnodes函数 

 
  function createRmCb(childElm: Node, listeners: number) {
    // 返回删除元素的回调函数
    return function rmCb() {
      if (--listeners === 0) {
        const parent = api.parentNode(childElm);
        api.removeChild(parent, childElm);
      }
    };
  }
  function addVnodes(parentElm: Node,
                     before: Node | null,
                     vnodes: Array<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) {
    let i: any, j: number, data = vnode.data;
    if (data !== undefined) {
      // 执行用户设置的destroy的钩子函数
      if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
      // 调用模块的destroy钩子函数
      for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
      // 执行子节点的destroy的钩子函数
      if (vnode.children !== undefined) {
        for (j = 0; j < vnode.children.length; ++j) {
          i = vnode.children[j];
          if (i != null && typeof i !== "string") {
            invokeDestroyHook(i);
          }
        }
      }
    }
  }

  function removeVnodes(parentElm: Node,
                        vnodes: Array<VNode>,
                        startIdx: number,
                        endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx];
      if (ch != null) {
        // 如果sel有值
        if (isDef(ch.sel)) {
          // 执行destroy钩子函数,会执行所有子节点的destroy钩子函数
          invokeDestroyHook(ch);
          listeners = cbs.remove.length + 1;
          // 创建删除的回调函数
          rm = createRmCb(ch.elm as Node, listeners);
          for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);
          // 执行用户设置的remove钩子函数
          if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
            i(ch, rm);
          } else {
            // 如果没有用户钩子函数,直接调用删除元素的方法
            rm();
          }
        } else { // Text node
          // 如果是文本节点,直接调用删除元素的方法
          api.removeChild(parentElm, ch.elm as Node);
        }
      }
    }
  }

patchVnode函数 

Vue.js 框架最为核心的 Virtual DOM 是如何实现的?

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


标签: javascriptvue
分享给朋友:

相关文章

手机端分享调用功能的实现(只能在真机上测试有效)

手机端分享调用功能的实现(只能在真机上测试有效)

html: <a href="javascript:void(0);" onclick="call('default')"> <...

移动webAPP开发必备基础知识汇总

移动webAPP开发必备基础知识汇总

HTML5存储1、localStorage和sessionStorage1相同的使用方法api使用setItem方法设置存储内容使用getItem方法设置获取内容使用removeItem方法删除存储内...

HTTP之安全威胁解析

HTTP之安全威胁解析

1、web安全攻击web应用的概念web应用是由动态脚本、编译过的代码等组合而成它通常架设在web服务器上,用户在web浏览器上发送请求这些请求使用http协议,由web应用和企业后台的数据库及其他动...

前端入门html、css、js知识汇总(1)

前端入门html、css、js知识汇总(1)

注:本文适合需要一点前端知识,如有不懂自行百度或去W3C官网、去菜鸟教程查询1.html不需要编译,直接由浏览器执行   html文件时一个文本文件   h...

es6入门之环境搭建

es6入门之环境搭建

话不多说,首先我也是先学习了然后再总结的。es6入门——es6环境的搭建(原文有个错误,把cmd说成是控制台,以至于评论区有个新手没理解到意思,控制台是浏览器的console,cmd是输入命令行执行的...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

分享:

支付宝

微信