Virtual DOM

虚拟 DOM: 由JavaScript对象来描述真实 DOM。

  • VUE 内部的虚拟 DOM 是改造了一个开源库 Snabbdom
  • 真实 DOM 的操作耗能太大了。
  • 虚拟DOM的最终目标是将 虚拟节点渲染到视图:

虚拟DOM的好处:

  • 维护视图和状态的关系。跟踪状态的变化。状态改变后不需要立即更新真实DOM。而是创建虚拟DOM树 最终来映射到真实DOM。
  • 复杂视图情况下提升渲染性能
  • 跨平台:
    • 浏览器平台渲染dom
    • 服务端渲染 ssr
    • 原生应用(Weex / React Native )
    • 小程序( mpvue/ uni-app )

虚拟 DOM 实现的核心是 diff 算法:

  • diff 的过程就是【调用名为 patch 的函数,】比较新旧节点,将差异更新到真实 DOM。
  • 通过 h 函数来创建虚拟 DOM。
  • demo:
    假如有个数组[1,2,3],然后在开始位置插入100.
    没有设置key:diff算法先从新旧开始节点对比,因为sel和key(undefined)一样,它们被认为是相同节点,那么会对比他们的子节点,1和100不同,更新text。重复这一对比过程更新直到旧的数组遍历结束,此时新节点还剩一项3,做插入操作
    设置key:diff算法先从新旧开始节点对比,因为key不一样(1和100不等),从最后往前开始对比,因为sel和key一样,他们的text也一样,那么不更新。重复这一对比过程更新直到旧的数组遍历结束,此时新节点还剩一项100,做插入操作
patch( oldVnode: Element | VNode,newVnode:VNode ) {// patch 函数的核心就是比较新旧节点,将差异更新到真实 dom,更新 DOM。返回新节点。
    return newVnode
}

Snabbdom 中源码分析:

  • init 注册模块,返回 patch 函数
  • h 函数用来创建节点Vnode . Vnode 的本质就是一个 JavaScript 对象。
    函数重载:
    • 参数个数或参数类型不同的函数
    • 函数名相同
    • JavaScript 中没有重载的概念
    • typescript 中有重载。
  • patch 函数比较新旧节点,状态变化时将差异更新到真实 DOM 树。返回 newVnode
import {
  init,
  classModule, // toggle classes
  propsModule, // setting properties on DOM elements
  styleModule, // handles styling on elements with support for animations
  eventListenersModule, // attaches event listeners
  // 以上这些模块只能处理vnode节点,在 h 函数中使用,不能直接操作DOM。属于 snabbdom 的扩展。
  h,
} from "snabbdom";

const patch = init([ // init() 函数会返回一个 patch 函数
    classModule,
    propsModule,
    styleModule,    
    eventListenersModule,
])

let vnode = h('div#aaa.myclass','hi虚拟dom');
let dom = document.querySelector('#app');
let oldVnode = patch(dom , vnode); //比较新旧节点,将差异更新到真实 dom 。并将当前返回的节点保存起来。供下一次比较时使用
vnode = h('div#bbb.mybbbclass',[
    h('h1','创建子节点h1'),
    h('p','创建子节点p')
]);
patch(oldVnode,vnode)

//清空div内容 用注释节点来替换已有的节点内容
patch(oldvalue,h('!'))//感叹号用来创建注释节点

// h函数创建有事件绑定或者有样式的节点
let my_vnode = h('div',[
    h('h1',
       { 
        style: { backgroundColor: 'red' }, 
        'hello h1' 
       }
    ),
    h('p',
      { 
        on: { click: Handle}, 
        'hello p' 
      }
    )
])
function Handle() {
    console.log('点击p');
}

h 函数:创建Vnode节点

export interface VNode {
    sel: String | undefined,// 选择器
    data: VNode | undefined,
    children: Array <VNode | String> | undefined,//
    text: String | undefined,//文本
    key: Key | Number, //唯一标识节点
    // elm: Element | Text | undefined
}

patch 的内部实现:

  • 比较新旧节点,状态变化时将差异更新到真实 DOM 树。返回 newVnode
    实现思路:
  • 对比新旧vnode是否是相同节点【key节点唯一标识, sel节点选择器 】
  • 如果当前vnode不是相同节点,就没有必要继续比较下面的子节点,直接删除旧节点对应的DOM元素,将新节点内容渲染到真实DOM。【比较的是虚拟DOM 和真实 DOM 之间存在映射关系的两个节点】
  • 如果是相同节点:[节点标识和节点选择器都相同]
    • 判断新的vnode是否有text,有并且与旧vnode的text不同,更新文本内容
    • 如果新的vnode有children【children text互斥】,判断子节点是否有变化。⭐
// h 函数创建vnode对象。
// pacth 函数比较新旧节点。替换更新dom.
// init 函数注册扩展

function patch(oldVnode: Vnode | Element, vnode:VNode) {
    // 1. 对比新旧节点之前判断传进来的是 vnode 还是 element。不是vnode就转成vnode.
    // 【因为传进来的可能是 vnode,也可能是 Element.】
    if(!isVnode(oldVnode)) {
        oldVnode = emptyNodeAt(oldVnode);// 判断:如果不是 vnode 节点【说明是 Element 】就把它处理成 vnode 节点
    }
    // 2. 对比新旧节点,先判断节点是否相同
    if(sameVnode(oldVnode,vnode)) {
        // 2-1. 如果节点相同,就对比节点的差异,更新真实 DOM
        patchVnode(oldVnode,vnode,insertedVnodeQueue); // 判断是否有text,是否有children。再对比
    }
    else {
    // 2-2. 如果节点不相同。那么子节点就没必要继续比较了。
    // 直接删除旧节点对应的DOM元素,将新节点内容渲染到真实DOM:
        // 2.2-1 获取到旧节点的 dom 元素。
        olddom = oldVnode.elm!;// 【! 属于typescript语法,标识这个属性一定有值】
        // 2.2-2 获取到旧节点 dom 元素的父元素
        parentdom = api.parentNode(olddom) as Node;
        // 2.2-3 创建新节点 vnode 对应的 DOM 元素
        createElm(vnode, insertedVnodeQueue);

        if(parentdom !== null) {
            // 2.2-4 将步骤‘3’创建的【新节点 vnode 对应的 】dom 元素 挂载到 parentdom 父元素。插入位置是:旧节点dom元素的兄弟节点元素之前
            api.insertBefore(parentdom, vnode.elm!, api.nextSibling(olddom));// 往父元素中插入新创建的 dom 元素。插入到真实DOM树种
            // api.nextSibling(elm) 表示旧节点dom元素的下一个兄弟节点元素
            // 2.2-5 移除 parentdom 下的旧节点所对应的 dom 元素
            removeVnodes(parentdom, [oldVnode], 0, 0);// 0 0 表示要删除的元素在数组中的开始和结束位置
        }
    }
    // 3.  遍历钩子函数
    for(i = 0;i<insertedVnodeQueue.length; ++i) {
        insertedVnodeQueue[i].data!.hook!.inser!(insertedVnodeQueue[i])
    }
    for(i=0;i < cbs.post.length; ++i) cbs.post([i]() i = 0;
    // 4. 
    return vnode;
}
function isVnode(oldVnode) {//判断是否是 vnode
    return oldVnode.sel; // vnode.sel 存在说明是vnode对象 
}
function emptyNodeAt(elm: Element) {// 将 element 转 Vnode
    const id = elm.id? '#'+id :'';
    const cs = elm.className? '.'+ elm.className.split(' ').join('.') : '';
    return vnode(api.tagName(elm).toLowerCase()+id+cs, {}, [], undefined, elm);// elm 表示当前转换后的vnode所对应的 dom 元素
    // vnode(tagname#idname.classname,data,children,text,element);  children 与 text 互斥。即两个只允许一个存在
}
function sameVnode(oldVnode,vnode) {// 判断两个节点是否相同:[节点标识和节点选择器都相同] key && sel
    return oldVnode.key === vnode.key && oldVnode.sel === vnode.sel;
}
function patchVnode(oldVnode: VNode|Element,vnode:VNode) {// 在新旧节点相同的情况下:比较新旧节点。将差异更新到真实 dom
    if(oldvalue === vnode) return;
    if(oldvnode.text !== vnode.text) {
        // 如果老节点有children,直接移除老节点children对应的DOM。
        // 新节点对应dom 的 textContent
    }
    if(oldVnode.children !== vnode.children) {// 新老节点都有children且不相等,就去对比子节点。并更新子节点的差异
        updateChilren();// 将差异更新到 DOM
    }
}
function createElm(vnode, insertedVnodeQueue): Node {// 将 vnode 转换成对应的 dom 元素,并把创建的 dom 元素存储到 vnode.elm 
    // 1. 执行 init 钩子函数

    // 2. 将 vnode 转换成 真实 DOM [还没有去渲染到视图],存储到 vnode.elm
    const children = vnode.children;
    const sel = vnode.sel;// 节点选择器
    if(sel === '!') { // h('!')
        // 创建注释节点
        if(vnode.text === undefined) {
            vnode.text = ''
        }
        vnode.elm = api.createComment(vnode.text!);// 调用 DOM 中的api来创建注释节点
    }
    else if(sel !== undefined) {
        // 如果选择器不为空,需要解析选择器,转换为真实dom
        if(vnode.children) {
            for(i = 0; i< vnode.children.length; ++i) {
                if(vnode.children[i] !== null) {
                    // 将当前子节点转换成真实 DOM 并追加到 elm
                    api.appendChild(elm, createElm(vnode.children[i] as Vnode, insertedVnodeQueue))
                }
            }
        }else if( typeof vnode.text === 'string' || typeof vnode.text === 'number' ) {
            api.appendChild(elm, api.createTextNode(vnode.text))
        }
        // 。。。判断是否有 hook,有的话处理钩子函数
    }else {// 选择器为空时,创建文本节点
        vnode.elm = api.createTextNode(vnode.text!);// ! 表示这个属性一定有值
    }

    // 最终返回创建的真实 om
    return vnode.elm

}
function removeVnodes(parentElm: Node, vnodes:VNode[], startIdx:Number, endIdx: Number): void {// 移除parent 下的 oldVnode 对应的 dom元素
    for(; startIdx <= endIdx; ++startIdx) {//删除 Idx 区间内的节点
        const ch = vnodes[startIdx];
        if(ch!==null) {
            if(ch.sel) {
                // 元素节点

            }else {
                // 文本节点
                api.removeChild(parentElm, ch.elm!)
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值