IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 虚拟DOM原理 -> 正文阅读

[JavaScript知识库]虚拟DOM原理

在Vue2渲染层做了根本性的改动,那就是引入了虚拟DOM。vue的虚拟dom是基于 snabbdom 改造过来的。了解 snabbdom的原理之后再回过头来看 vue的虚拟DOM结构的实现。就难度不大了!

一、背景

为什么需要 Virtual DOM

在前端刀耕火种的时代,jquery 可谓是一家独大。然而慢慢地人们发现,在我们的代码中布满了一系列操作 DOM 的代码。这些代码难以维护,又容易出错,最重要的是,DOM操作非常耗费性能。
而Vue和React是数据驱动视图,那么它们如何有效控制DOM操作呢?
当项目达到一定的复杂度,想减少计算次数比较难。能不能把计算更多地转移为JS计算?因为JS执行速度很快,JS执行速度和DOM更新速度完全不是一个数量级的。于是有人想到用JS模拟DOM结构,计算出最小的变更,然后操作一次DOM即可。
实现 Virtual DOM
Virtual DOM 主要包括以下三个方面:

  1. 使用 js 数据对象 表示 DOM 结构 -> VNode
  2. 比较新旧两棵 虚拟 DOM 树的差异 -> diff
  3. 将差异应用到真实的 DOM 树上 -> patch

下面开始来研究 snabbdom 是如何实现这些方面的

目录

项目链接:https://github.com/snabbdom/snabbdom
首先看一下整体的目录结构,源码主要是在 src 里面,其他的目录:test 、examples 分别是测试用例以及例子。这里我们先关注源码部分:

── h.ts   创建vnode的函数
── helpers
 └── attachto.ts
── hooks.ts  定义钩子
── htmldomapi.ts   操作dom的一些工具类
── is.ts   判断类型
── modules  模块
 ├── attributes.ts
 ├── class.ts
 ├── dataset.ts
 ├── eventlisteners.ts
 ├── hero.ts
 ├── module.ts
 ├── props.ts
 └── style.ts
── snabbdom.bundle.ts 入口文件
── snabbdom.ts  初始化函数
── thunk.ts  分块
── tovnode.ts   dom元素转vnode
── vnode.ts  虚拟节点对象

snabbdom.bundle.ts 入口文件

我们先从入口文件开始看起:

import { init } from './snabbdom';
import { attributesModule } from './modules/attributes'; // for setting attributes on DOM elements
import { classModule } from './modules/class'; // makes it easy to toggle classes
import { propsModule } from './modules/props'; // for setting properties on DOM elements
import { styleModule } from './modules/style'; // handles styling on elements with support for animations
import { eventListenersModule } from './modules/eventlisteners'; // attaches event listeners
import { h } from './h'; // helper function for creating vnodes

// 入口文件

// 初始化,传入需要更新的模块。
var patch = init([
    // Init patch function with choosen modules
    attributesModule,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule
]) as (oldVNode: any, vnode: any) => any;

// 主要导出 snabbdomBundle , 主要包含两个函数,一个是 修补函数 , 一个是 h 函数
export const snabbdomBundle = { patch, h: h as any };
export default snabbdomBundle;

我们可以看到,入口文件主要导出两个函数:

  1. patch函数 , 由 snabbdom.ts 的 init 方法,根据传入的 module 来初始化
  2. h函数 ,在 h.ts 里面实现

看起来h函数比 patch 要简单一些,我们去看看到底做了些什么。

二、h函数

介绍

这里是 typescript 的语法,定义了一系列的重载方法。
h 函数主要根据传进来的参数,返回一个 vnode 对象

  • 在使用 Vue 的时候的 h() 函数和snabbdom中的h函数功能一样都是用来穿件VNode对象,但是Vue中增强了 h
    函数,实现了组件的机制:
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
  • h() 函数最早见于 hyperscript,使用 JavaScript 创建超文本,也就是 html 字符串
  • Snabbdom 中的 h() 函数源于 hyperscript,但是不是用来创建超文本,而是创建 VNode

源码

源码位置:src/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;
    // 如果 c 是数组
    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
    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;

h 函数比较简单,主要是提供一个方便的工具函数,方便创建 vnode 对象

三、vnode对象

vnode 是一个对象,用来表示相应的 dom 结构
代码位置 :./src/vnode.ts

定义 vnode 类型

/**
 * 定义VNode类型
 */
export interface VNode {
    // 选择器
    sel: string | undefined;
    // 数据,主要包括属性、样式、数据、绑定时间等
    data: VNodeData | undefined;
    // 子节点
    children: Array<VNode | string> | undefined;
    // 关联的原生节点
    elm: Node | undefined;
    // 文本
    text: string | undefined;
    // key , 唯一值,为了优化性能
    key: Key | undefined;
}

定义 VNodeData 的类型

/**
 * 定义VNode 绑定的数据类型
 */
export interface VNodeData {
    // 属性 能直接用 . 访问的
    props?: Props;
    // 属性
    attrs?: Attrs;
    // 样式类
    class?: Classes;
    // 样式
    style?: VNodeStyle;
    // 数据
    dataset?: Dataset;
    // 绑定的事件
    on?: On;

    hero?: Hero;
    attachData?: AttachData;
    // 钩子
    hook?: Hooks;
    key?: Key;
    ns?: string; // for SVGs
    fn?: () => VNode; // for thunks
    args?: Array<any>; // for thunks
    [key: string]: any; // for any other 3rd party module
}

创建 VNode 对象

// 根据传入的 属性 ,返回一个 vnode 对象
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: sel,
        data: data,
        children: children,
        text: text,
        elm: elm,
        key: key
    };
}
export default vnode;

四、patch方法

在开始解析这块源码的时候,先给大家补一个知识点。关于 两颗 Virtual Dom 树对比的策略

diff策略

1、同级比较
在这里插入图片描述
对比的时候,只比较同一层级,不跨级比较,减少算法复杂度。
2、就近复用
为了尽可能不发生 DOM 的移动,会就近复用相同的 DOM 节点,复用的依据是判断是否是同类型的 dom 元素
3、tag不相同,则直接删掉重建,不再深度比较
4、tag和key两者都相同,则认为是相同节点,不再深度比较

init 方法

在 ./src/snabbdom.ts 中,主要是 init 方法。
init 方法主要是传入 modules ,domApi , 然后返回一个 patch 方法
注册钩子:

// 钩子 ,
const hooks: (keyof Module)[] = [
    'create',
    'update',
    'remove',
    'destroy',
    'pre',
    'post'
];

这里主要是注册一系列的钩子,在不同的阶段触发
将各个模块的钩子方法,挂到统一的钩子上
这里主要是将每个 modules 下的 hook 方法提取出来存到 cbs(也就是callbacks回调函数集) 里面

  • 初始化的时候,将每个 modules 下的相应的钩子都追加都一个数组里面。create、update…
  • 在进行 patch 的各个阶段,触发对应的钩子去处理对应的事情
  • 这种方式比较方便扩展。新增钩子的时候,不需要更改到主要的流程
    // 循环 hooks , 将每个 modules 下的 hook 方法提取出来存到 cbs 里面
    // 返回结果 eg : cbs['create'] = [modules[0]['create'],modules[1]['create'],...];
    for (i = 0; i < hooks.length; ++i) {
        cbs[hooks[i]] = [];
        for (j = 0; j < modules.length; ++j) {
            const hook = modules[j][hooks[i]];
            if (hook !== undefined) {
                (cbs[hooks[i]] as Array<any>).push(hook);
            }
        }
    }

这些模块的钩子,主要用在更新节点的时候,会在不同的生命周期里面去触发对应的钩子,从而更新这些模块。
例如元素的 attr、props、class 之类的!

sameVnode

判断是否是相同的虚拟节点

/**
 *  判断是否是相同的虚拟节点
 */
function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
    return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}

patch

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

patch 方法主要的逻辑如下 :

  • 触发 pre 钩子
  • 如果老节点非 vnode, 则新创建空的 vnode
  • 新旧节点为 sameVnode 的话,则调用 patchVnode 更新 vnode , 否则创建新节点
  • 触发收集到的新元素 insert 钩子
  • 触发 post 钩子
    /**
     * 修补节点
     */
    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]();

        // 如果老节点非 vnode , 则创建一个空的 vnode
        if (!isVnode(oldVnode)) {
            oldVnode = emptyNodeAt(oldVnode);
        }

        // 如果是同个节点,则进行修补
        if (sameVnode(oldVnode, vnode)) {
            patchVnode(oldVnode, vnode, insertedVnodeQueue);
        } else {
            // 不同 Vnode 节点则新建
            elm = oldVnode.elm as Node;
            parent = api.parentNode(elm);

            createElm(vnode, insertedVnodeQueue);

            // 插入新节点,删除老节点
            if (parent !== null) {
                api.insertBefore(
                    parent,
                    vnode.elm as Node,
                    api.nextSibling(elm)
                );
                removeVnodes(parent, [oldVnode], 0, 0);
            }
        }

        // 遍历所有收集到的插入节点,调用插入的钩子,
        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]();

        return vnode;
    };

整体的流程大体上是这样子,接下来我们来关注更多的细节!

patchVnode 方法

它的作用是对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM。当新旧节点时同一个节点时,就会调用patchVnode对新旧节点差异进行局部更新。首先我们研究 patchVnode 了解相同节点是如何更新的。

patchVnode 方法主要的逻辑如下 :
第一个过程:触发prepatch和update钩子函数

  • 判断新旧节点是否相同,如果相同就返回

第二个过程:对比新旧节点,更新差异

  • 如果 vnode.text(文本节点) 存在
    • 如果 oldVnode.children 和 vnode.children 都有值
      • 调用 updateChildren()使用 diff 算法对比子节点,更新子节点
    • 如果 vnode.children 有值, oldVnode.children 无值
      • 清空 DOM 元素,然后调用 addVnodes() ,批量添加子节点
    • 如果 oldVnode.children 有值, vnode.children 无值
      • 调用 removeVnodes() ,批量移除子节点
    • 如果 oldVnode.text 有值
      • 清空 DOM 元素的内容
  • 如果 vnode.text存在并且和 oldVnode.text 不等
    • 如果老节点有子节点,全部移除
      • 设置 DOM 元素的 textContent 为 vnode.text

第三个过程:触发postpatch钩子函数

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

// 更新节点
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
  let i: any, hook: any;
  // 第一个过程:触发prepatch和update钩子函数
  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;
  // 调用 cbs 中的所有模块的update回调更新对应的实际内容
  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为假,继续判断新老节点是否都有children
  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);
        
    // 如果新节点有 children,老节点没有 children
    } else if (isDef(ch)) {
?
        
      // 如果老节点有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)) {  // 如果老节点有children,新节点没有children
?
      // 批量移除老节点的子节点
      removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).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 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);
  }
}

updateChildren 方法

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

updateChildren 主要的逻辑如下:

1、优先处理特殊场景,先对比两端。也就是

  • 旧 vnode 头 vs 新 vnode 头
  • 旧 vnode 尾 vs 新 vnode 尾
  • 旧 vnode 头 vs 新 vnode 尾
  • 旧 vnode 尾 vs 新 vnode 头

2、首尾不一样的情况,寻找 key 相同的节点,找不到则新建元素
3、如果找到 key,但是,元素选择器变化了,也新建元素
4、如果找到 key,并且元素选择没变, 则移动元素
5、两个列表对比完之后,清理多余的元素,新增添加的元素

不提供 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 方法

addVnodes 就比较简单了,主要功能就是添加 Vnodes 到 真实 DOM 中

/**
 * 添加 Vnodes 到 真实 DOM 中
 */
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
            );
        }
    }
}

removeVnodes 方法

删除 VNodes 的主要逻辑如下:

  • 循环触发 destroy 钩子,递归触发子节点的钩子
  • 触发 remove 钩子,利用 createRmCb , 在所有监听器执行后,才调用 api.removeChild,删除真正的DOM节点
/**
 * 创建一个删除的回调,多次调用这个回调,直到监听器都没了,就删除元素
 */
function createRmCb(childElm: Node, listeners: number) {
    return function rmCb() {
        if (--listeners === 0) {
            const parent = api.parentNode(childElm);
            api.removeChild(parent, childElm);
        }
    };
}
/**
 * 删除 VNodes
 */
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);
            }
        }
    }
}

createElm 方法

将 vnode 转换成真正的 DOM 元素

主要逻辑如下:

  • 触发 init 钩子
  • 处理注释节点
  • 创建元素并设置 id , class
  • 触发模块 create 钩子
  • 处理子节点
  • 处理文本节点
  • 触发 vnodeData 的 create 钩子
/**
*  VNode ==> 真实DOM
*/
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any,
        data = vnode.data;

    if (data !== undefined) {
        // 如果存在 data.hook.init ,则调用该钩子
        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) {
        // 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;

        // 根据 tag 创建元素
        const elm = (vnode.elm =
            isDef(data) && isDef((i = (data as VNodeData).ns))
                ? api.createElementNS(i, tag)
                : api.createElement(tag));

        // 设置 id
        if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));

        // 设置 className
        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 ,则创建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));
        }

        // 执行 vnode.data.hook 中的 create 钩子
        i = (vnode.data as VNodeData).hook; // Reuse variable
        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;
}

五、钩子

源码路径 : ./src/hooks.ts

这个文件主要是定义了 Virtual Dom 在实现过程中,在其执行过程中的一系列钩子。方便外部做一些处理:

// 钩子
export interface Hooks {
    // 在 `patch` 开始执行的时候调用
    pre?: PreHook;

    // 在 `createElm`,进入的时候调用init
    // vnode转换为真实DOM节点时触发
    init?: InitHook;

    // 创建真实DOM的时候,调用 create
    create?: CreateHook;

    // 在`patch`方法接近完成的时候,才收集所有的插入节点,遍历调用响应的钩子
    // 可以认为插入到DOM树时触发
    insert?: InsertHook;

    // 在两个节点开始对比前调用
    prepatch?: PrePatchHook;

    // 更新过程中,调用update
    update?: UpdateHook;

    // 两个节点对比完成时候调用
    postpatch?: PostPatchHook;

    // 删除节点的时候调用,包括子节点的destroy也会被触发
    destroy?: DestroyHook;

    // 删除当前节点的时候调用。元素从父节点删除时触发,和destory略有不同,remove只影响到被移除节点中最顶层的节点
    remove?: RemoveHook;

    // 在`patch`方法的最后调用,也就是patch完成后触发
    post?: PostHook;
}

六、模块

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-05-06 10:58:50  更:2022-05-06 11:01:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 22:51:55-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码