• Vue源码分析之虚拟DOM


    虚拟Dom

    关于虚拟Dom的概念可以从一个简单的小例子出发,如下代码所示:

    let div = document.querySelector('#container');
    let s = '';
    for (let k in div){s += k + ','}
    

    运行后结果如下

    可见创建一个Dom元素开销的有多大,一般对数据进行操作而改变后渲染在页面,做法就是直接删除所有旧的Dom,渲染出新的Dom

    因为dom元素无法跟踪和感知数据的变化,以及上一次的数据是什么。从而采用重渲染笨重的方式来改变试图。

    而虚拟dom解决的就是数据跟踪的问题,将dom所需要渲染的数据单独提取放在js对象 vnode 当中,对数据修改时,通过比较新旧虚拟

    节点的差异,来决定如何以最高效的方式修改已存在的Dom元素。

    • vnode数据结构
    interface VNode {
        sel: string | undefined;
        data: VNodeData | undefined;
        children: Array<VNode | string> | undefined;
        elm: Node | undefined;
        text: string | undefined;
        key: Key | undefined;
    }
    
    
    interface VNodeData {
        props?: Props;
        attrs?: Attrs;
        class?: Classes;
        style?: VNodeStyle;
        dataset?: Dataset;
        on?: On;
        hero?: Hero;
        attachData?: AttachData;
        hook?: Hooks;
        key?: Key;
        ns?: string;
        fn?: () => VNode;
        args?: Array<any>;
        [key: string]: any;
    }
    
    字段 描述
    sel 选择器字符串
    data 描述对象属性,类似vue的 @click :props
    children 孩子虚拟节点数组
    elm 存放真实Dom
    text 文本节点与children不可共存
    key 区分不同vnode,类似vue的 v-for=... :key="idx" 中的key

    patchVnode

    概述

    对两个不同的vnode进行打补丁,使用diff算法比较新旧Dom元素的差异,并只对差异部分进行更新,原本使用的是直接全删除重新创建新的Dom

    为方便描述, vnode 代表新节点, oldVnode 代表旧节点, ch 代表孩子节点数组, text代表文本节点

    流程描述

    • 初始化
    1. 因为用户仅对 vnode 虚拟节点进行操作,而非真实的Dom,因此对于 vnode.elm 还未赋予真实的Dom
    2. 通过sameVnode比较后,可知新旧的虚拟节点是对同一Dom进行修改,因此直接将旧dom赋予到新dom方便操作
    const elm = vnode.elm = (oldVnode.elm as Node);
    
    • hook钩子

    流程最初和最末尾会触发用户传递的 prepatchpostpatch 两个不同的钩子函数,分别代表补丁前,和补丁后的生命周期

    • 比较虚拟dom

    直接对比新旧vnode对象指针地址来判断是否为同一个对象,若对象相同,无需补丁,直接返回

    • 触发update钩子

    如果 vnode.data != undefined 触发模块和用户的update钩子

    • 对比算法

    根据流程图,对比新旧节点的 textch 来分以下5种情况操作, 注意同节点下 textch 不可共存。

    情况1. vnode.text != undefined

    新节点存在 text 文本节点,若 oldCh 存在则移除 oldCh,最终

    api.setTextContent(elm, vnode.text as string);
    

    情况2. ch && oldCh

    updateChildren 后面介绍

    if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
    

    情况3. isDef(ch)

    oldText 可能有也可能没有,无论如何执行 addVnodes,添加虚拟节点和Dom节点

    if (isDef(oldVnode.text)) api.setTextContent(elm, '');
    addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue);
    

    情况4. isDef(oldCh)

    存在 oldCh ,移除虚拟节点和Dom节点

    removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
    

    情况5. isDef(oldVnode.text)

    新节点既没有text也没有ch,若存在oldText,清空文本即可

    createElm

    /****
     * @params vnode 需要生成Dom的虚拟节点
     * @params insertedVnodeQueue 用于用户传递 insert 钩子回调,这里不关注
     * @return Dom
     */
    
    function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
        let i: any, data = vnode.data;
        //触发用户传递 init 钩子
        if (data !== undefined) {
          if (isDef(i = data.hook) && isDef(i = i.init)) {
            i(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 as string);
        } 
    
        //选择器不为空时
        else if (sel !== undefined) {
          // 解析和提取选择器中 tag 和 #id 和 .class 
          const hashIdx = sel.indexOf('#');
          const dotIdx = sel.indexOf('.', hashIdx);
    
          // 这里不考虑0是因为用法不允许省略标签名,如 h('#app', 'no div tags');
          const hash = hashIdx > 0 ? hashIdx : sel.length;
    
          // 如果不存在 #id 或 .class,则赋予长度,处理选择器出现 #id 和 .class 顺序不同的情况
          const dot = dotIdx > 0 ? dotIdx : sel.length;
          const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel;
    
          // 生成Dom,ns为域名空间,针对svg标签情况,一般都是调用 api.createElement(tag) 情况,
          const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag)
                                                                                   : api.createElement(tag);
    
          // 设置 id 和 class
          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);
    
          // 递归创建孩子节点,并追加到 children[] 中
          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)) {
            api.appendChild(elm, api.createTextNode(vnode.text));
          }
          i = (vnode.data as VNodeData).hook; // Reuse variable
    
          //触发用户传递的 create hook 并未 insert钩子做数据铺垫
          if (isDef(i)) {
            if (i.create) i.create(emptyNode, vnode);
            if (i.insert) insertedVnodeQueue.push(vnode);
          }
        } else {
          // sel 选择器不传递的情况,直接创建文本节点
          vnode.elm = api.createTextNode(vnode.text as string);
        }
        return vnode.elm;
      }
    

    removeVnodes

    这里强调一下 invokeDestroyHook 和 createRmCb 的作用,一是防止过河拆桥,而是防止重复remove

    invokeDestroyHook 用于会触发用户和模块中 destroy钩子函数,该函数会深度优先递归子节点。

    createRmCb 闭包,因为要求模块的 remove钩子执行到最后一个模块,才对element进行删除,否则会出现过河拆桥的情况

    listeners = cbs.remove.length + 1;   //listeners 记录有几个需要执行remove的模块
    rm = createRmCb(ch.elm as Node, listeners);
    for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm);  //每执行一次模块的remove,rm就调用一次,listeners--
    
      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) {
            if (isDef(ch.sel)) {
              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);
              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);
            }
          }
        }
      }
    
      function invokeDestroyHook(vnode: VNode) {
        let i: any, j: number, data = vnode.data;
        if (data !== undefined) {
          if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode);
          for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode);
          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 createRmCb(childElm: Node, listeners: number) {
        return function rmCb() {
          if (--listeners === 0) {
            const parent = api.parentNode(childElm);
            api.removeChild(parent, childElm);
          }
        };
      }
    
  • 相关阅读:
    27.TreeMap
    26.HashCode
    25.HashTable
    myeclipse快捷键
    spring 配置
    jdbcType和javaType对应关系
    Ajax表单提交
    ajax
    JQuery及Form插件使用
    jsp标准数据库
  • 原文地址:https://www.cnblogs.com/demonxian3/p/13535611.html
Copyright © 2020-2023  润新知