Snabbdom 源码

下载 Snabbdom 源码

拉取代码

使用 v2.1.0 作为示例:

git 复制代码
git clone https://github.com/snabbdom/snabbdom.git
git checkout v2.1.0

安装依赖

对于 node 版本有要求,我用的 v12,可以成功安装。

js 复制代码
npm i

编译

因为测试案例中使用了编译之后的目录,所以在运行测试代码之前需要先编译:

js 复制代码
npm run compile

运行查看测试案例

需要先在 vscode 中安装 Live Server 插件,然后在 snabbdom\examples\reorder-animation\index.html 页面右键,选择 Open with Live Server,就会在5500端口打开这个测试界面,我们可以发现测试界面的删除和排序是不起作用的,这是因为 demo 中使用的 onclick 写法是被废除的,需要修改为正确的写法:

js 复制代码
- h('div.btn.rm-btn', { on: { click: [remove, movie] } }, 'x'),
+ h('div.btn.rm-btn', { on: { click: () => { remove(movie) } } }, 'x'),
js 复制代码
- h('a.btn.rank', { class: { active: sortBy === 'rank' }, on: { click: [changeSort, 'rank'] } }, 'Rank'),
- h('a.btn.title', { class: { active: sortBy === 'title' }, on: { click: [changeSort, 'title'] } }, 'Title'),
- h('a.btn.desc', { class: { active: sortBy === 'desc' }, on: { click: [changeSort, 'desc'] } }, 'Description'),
+ ('a.btn.rank', { class: { active: sortBy === 'rank' }, on: { click: () => {changeSort('rank')} } }, 'Rank'),
+ h('a.btn.title', { class: { active: sortBy === 'title' }, on: { click: () => {changeSort('title')} } }, 'Title'),
+ ('a.btn.desc', { class: { active: sortBy === 'desc' }, on: { click: () => {changeSort('desc')} } }, 'Description')

模拟核心

  1. init() 设置模块,建立 patch 函数
  2. 使用 h() 函数创建 js 对象(vnode)描述真实 DOM
  3. patch 比较新旧 2 个 vnode,如果第一个参数是真实 DOM,会转换为 vnode
  4. 把变化的内容更新到真实 DOM

h

h 函数的核心就是处理参数 ,调用 vnode 函数创建一个 vnode 对象返回。

h 函数中定义了 4 种 h 函数的重载,ts 支持重载,js 不支持重载,当 ts 代码被编译成 js 后,只有一个 h 函数,所以参数的差异是通过在函数内部判断类型来支持的

js 复制代码
// 定义了 4 种 h 函数的重载
export function h (sel: string): VNode
export function h (sel: string, data: VNodeData | null): VNode
export function h (sel: string, children: VNodeChildren): VNode
export function h (sel: string, data: VNodeData | null, children: VNodeChildren): VNode
// 真正的 h 函数
export function h (sel: any, b?: any, c?: any): VNode {
  ...
  // 参数处理
  if (c !== undefined) {
    // 三个参数的情况:sel(选择器),data,children/text
    ...
  }
  ...
  // h 函数的核心就是处理参数,调用 vnode 函数创建一个 vnode 对象返回
  return vnode(sel, data, children, text, undefined)
};

vnode

接收 5 个参数,返回 VNode 类型的对象。

js 复制代码
export function vnode (sel, data, children, text, elm) {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}

init

参数

  1. modules: 模块数组
  2. domApi: 把 vnode 对象转换为其他平台下的元素,不传值时默认是浏览器 Dom 操作的 Api,传递 domApi 就可以实现跨平台

返回值

init 作为一个高阶函数,返回 patch 函数。

这样做的好处是本来调用 patch 要传递4个参数:modules, domApi, oldVnode, vnode。因为 patch 函数的使用频率很高,所以先通过 init 初始化前2个参数,并将前2个参数缓存,以后再调用 patch 函数时只需传入 oldVnodevnode 即可。

方法体

js 复制代码
// 定义了一些辅助函数和一些类型
type ...
function ...

// 定义了 hooks 数组,存储的是钩子函数的名称,在 init 的时候被初始化,然后在特定的时间执行
const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']

export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
  let i: number
  let j: number
  // 回调函数,存储的就是模块的钩子函数,会在特定时间执行
  const cbs: ModuleHooks = {create: [],update: [],remove: [],destroy: [],pre: [],post: []}
  // domApi不传值时默认是浏览器Dom操作的Api,传递domApi就可以实现跨平台
  const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi

  // 嵌套循环
  // 第一层循环初始化 cbs,这里上面已经在定义的时候就初始化了,这段就没有意义了
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    // 第二层循环遍历 modules 模块,将模块的钩子函数添加到对应的 cbs 数组中
    // cbs => { create: [fn1, fn2], update: [fn1, fn2],...}
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]]
      if (hook !== undefined) {
        (cbs[hooks[i]] as any[]).push(hook)
      }
    }
  }

  // 定义很多内部函数
  function ...

  // 返回 patch 函数
  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    ...
  }
}

patch

patch 整体过程:

  1. 判断新节点是否为 vnode 对象(有 sel 选择器属性),如果是 Dom 对象则转换为 vnode 对象
  2. 对比新旧 VNode 是否相同节点(节点的 key 标识和 sel 选择器相同)
  3. 如果是相同节点,对比这两个 vnode 节点的内容是否有变化,调用 patchVnode 方法
  4. 如果不是相同节点,调用 createElm 创建新的 Vnode 对应的 Dom 元素,并将新创建的 dom 元素插入到 dom 树上,还要在 dom 树上将旧的 vnode 节点对应的元素移除
  5. 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
js 复制代码
function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node
    // 存储新插入节点的队列,为了触发这些节点上的 insert 钩子函数
    const insertedVnodeQueue: VNodeQueue = []
    // 触发 pre 预处理钩子函数
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

    // 判断一个对象是否为 vnode 对象,如果一个对象有 sel 选择器属性,就认作 vnode 对象
    if (!isVnode(oldVnode)) {
      // emptyNodeAt 把 Dom 对象转换为 vnode 对象
      oldVnode = emptyNodeAt(oldVnode)
    }

    // 判断新旧 vnode 是否是相同节点,key 和 sel 相同即判定为相同节点
    if (sameVnode(oldVnode, vnode)) {
      // 对比这两个 vnode 节点的内容是否有变化,而 vnode 对应的 Dom 元素是不需要重新创建的,这样会提升我们的效率
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      // 新旧 vnode 不是同一个节点,会创建新的 Vnode 对应的 Dom 元素
      // 并将新创建的 dom 元素插入到 dom 树上
      // 还要在 dom 树上将旧的 vnode 节点对应的元素移除
      elm = oldVnode.elm!    // elm 为 oldVnode 对应的 dom 元素
      parent = api.parentNode(elm) as Node  // 获取 elm 的父元素,使用的就是 node.parentNode

      // 创建 vnode 节点对应的 dom 元素,同时触发一些钩子函数
      createElm(vnode, insertedVnodeQueue)

      if (parent !== null) {
        // 如果有 parent 父元素的话,将新 vnode 对应的 dom 元素插入到旧元素的后一个节点之前,也就是插入到旧节点之后
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        // 把旧节点从 parent 中移除
        removeVnodes(parent, [oldVnode], 0, 0)
      }
    }

    // 触发对应的钩子函数
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    // 返回新节点作为下一次处理的旧节点
    return vnode
  }
相关推荐
仍然探索未知中20 分钟前
前端扫盲HTML
前端·html
Brilliant Nemo1 小时前
Vue2项目中使用videojs播放mp4视频
开发语言·前端·javascript
酷爱码1 小时前
Linux实现临时RAM登录的方法汇总
linux·前端·javascript
LuckyLay1 小时前
Vue百日学习计划Day16-18天详细计划-Gemini版
前端·vue.js·学习
想要飞翔的pig2 小时前
uniapp+vue3页面滚动加载数据
前端·vue.js·uni-app
HarryHY2 小时前
git提交库常用词
前端
SoraLuna2 小时前
「Mac畅玩AIGC与多模态41」开发篇36 - 用 ArkTS 构建聚合搜索前端页面
前端·macos·aigc
霸王蟹2 小时前
React Fiber 架构深度解析:时间切片与性能优化的核心引擎
前端·笔记·react.js·性能优化·架构·前端框架
benben0442 小时前
Unity3D仿星露谷物语开发44之收集农作物
前端·游戏·unity·游戏引擎
会功夫的李白2 小时前
uniapp自动构建pages.json的vite插件
前端·uni-app·vite