snabbdom 源码解析

382 阅读4分钟

1)snabbdom 源码解析

2)h 函数

  • h() 函数介绍

    • 在使用 Vue 的时候见过 h() 函数
    new Vue({
        router,
        store,
        render: h => h(App)
    }).$mount('#app')
    
    • h() 函数最早见于 hyperscript, 使用 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
    add(1, 2, 3) //调用第二个add
    

3)updateChildren

  • 功能:

    • diff 算法的核心,对比新旧节点的 children, 更新 DOM
  • 执行过程

    • 要对比两棵树的差异,我们可以取第一棵树的每一个节点一次和第二棵树的每一个节点比较,但是这样的时间复杂度为 O(n^3)
    • 在 DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
    • 因此只需要找 同级别 的子节点依次比较,然后再找下一基本的节点比较,这样算法的时间复杂度为 O(n)

  • 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历的过程中移动索引

  • 在对开始和结束节点比较的时候,总共有四种情况:

    • oldStartVNode / newStartVNode(旧开始节点 / 新开始节点)
    • oldEndVNode / newEndVNode(旧结束节点 / 新结束节点)
    • oldStartVNode / newEndVNode(旧开始节点 / 新结束节点)
    • oldEndVNode / newStartVNode(旧结束节点 / 新开始节点)

  • 开始节点和结束节点比较,这两种情况类似

    • oldStartVNode / newStartVNode(旧开始节点 / 新开始节点)
    • oldEndVNode / newEndVNode(旧结束节点 / 新结束节点)
  • 如果 oldStartVNode 和 newStartVNode 是 sameVnode(key 和 sel 相同)

    • 调用 patchVnode() 对比和更新节点
    • 把旧开始和新开始索引往后移动 oldStartdx++/oldEnddx++

  • oldStartVnode / newEndVnode(旧开始节点 / 新结束节点)相同

    • 调用 pathchVnode() 对比和更新节点
    • 把 oldStartVnode 对应的 DOM 元素,移动到右边
    • 更新索引

  • oldEndVNode / newStartVNode(旧结束节点 / 新开始节点)相同

    • 调用 pathchVnode() 对比和更新节点
    • 把 oldStartVnode 对应的 DOM 元素,移动到左边
    • 更新索引

  • 如果不是以上四种情况

    • 遍历新节点,使用 newStartVNode 的 key 在老节点数组中找相同节点

    • 如果没有找到,说明 newStartVNode 是新节点

      • 创建新节点对应的 DOM 元素,插入到 DOM 树中
    • 如果找到了

      • 判断新节点和找到的老节点的 sel 选择器是否相同
      • 如果不相同,说明节点被修改了
        • 创建新节点对应的 DOM 元素,插入到 DOM 树中
      • 如果相同,把 eleToMove 对应的 DOM 元素,移动到左边

    • 循环结束

      • 当老节点的所有子节点遍历完(oldStartldx > oldEndldx),循环结束
      • 新节点所有子节点先遍历完(newStartldx > newEndldx),循环结束
    • 如果老节点的数组先遍历完oldStartldx > oldEndldx),说明新节点有剩余,把剩余节点批量插入到右边

    • 如果新节点数组先遍历完(newStartldx > newEndldx),说明老节点有剩余,把剩余节点批量删除

4)Modules 源码

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

5)Hooks

  • 预定义的钩子函数的名称
  • 源码位置:src/package/hooks.ts
import { VNode } from './vnode'

// patch 函数开始的时候触发
export type PreHook = () => any
// createElm 函数开始之前的时候触发
// 再把 VNode 转换成真实 DOM 之前触发
export type InitHook = (vNode: VNode) => any
// createElm 函数末尾调用
// 创建完真实 DOM 后触发
export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any
// patch 函数末尾执行
// 真实 DOM 添加到 DOM 树中触发
export type InsertHook = (vNode: VNode) => any
// patchVNode 函数开头调用
// 开始对比两个 VNode 的差异之前触发 
export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any
// patchVNode 函数开头调用
// 两个 VNode 对比过程中触发,比 prepatch 稍晚
export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any
// patchVNode 的最末尾调用
// 两个 VNode 对比结束执行
export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any
// removeVnodes -> invokeDestroyHook 中调用
// 在删除元素之前触发,子节点的 destroy 也被触发
export type DestroyHook = (vNode: VNode) => any
// removeVNodes 中调用
// 元素被删除的时候触发
export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any
// patch 函数的最后调用
export type PostHook = () => any

export interface Hooks {
  pre?: PreHook
  init?: InitHook
  create?: CreateHook
  insert?: InsertHook
  prepatch?: PrePatchHook
  update?: UpdateHook
  postpatch?: PostPatchHook
  destroy?: DestroyHook
  remove?: RemoveHook
  post?: PostHook
}