snabbdom1.0.1新版本试水

引言

最近在读snabbdom源码,在学习途中发现下载的GitHub源码结构和别人博客上的有一些差别,查看提交记录发现最近几周 snabbdom 做了一点点变动。我们到npm网站上也可以看到,18天前版本从0.7.4升级到了1.0.1。
在这里插入图片描述
通过以下链接看到snabbdom的npm相关信息:npm地址
所以如果想跑一下目前博客上的Demo就只能指定snabbdom包的版本了

npm i snabbdom@0.7.4

更新后源码变化

简单比对了下,比较明显的就是:

  • modules、helpers文件夹合并到了package中

  • snabbdom.ts改为=>init.ts

文件目录结构

结构简单,就三个文件夹:

  • examples:官方案例,下载后先使用npm安装依赖包,在运行

npm run compile

命令打包后即可运行

  • pref:Snabbdom benchmarks(暂时没啥用)
  • src: 存放snabbdom源码

核心函数

h.ts

其实在Vue实例中已经见过h()了,它的作用就是创建一个虚拟节点VNode。

new Vue({  
	router,  
	store,  
	render: h => h(App) 
}).$mount('#app')

在h.ts文件中,h函数通过函数重载的方式定义,再通过if语句判断变量是否是undefined,来处理三个参数、两个参数时的情况。

// 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
export function h (sel: any, b?: any, c?: any): VNode {
  var data: VNodeData = {}
  var children: any
  var text: any
  var i: number
  if (c !== undefined) {
    if (b !== null) {
      data = b
    }
    if (is.array(c)) {
      children = c
    } else if (is.primitive(c)) {
      text = c
    } else if (c && c.sel) {
      children = [c]
    }
  } else if (b !== undefined && b !== null) {
    if (is.array(b)) {
      children = b
    } else if (is.primitive(b)) {
      text = b
    } else if (b && b.sel) {
      children = [b]
    } else { data = b }
  }
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      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] === '#')
  ) {
    addNS(data, children, sel)
  }
  return vnode(sel, data, children, text, undefined)
};

vnode.ts

VNode 就是用一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是 Virtual DOM。
结构比较简单,定义了VNode的接口和函数实现,该函数参数分别表示的是:

  1. sel: 选择器;
  2. data:节点数据VNodeData (属性/样式/事件等 )
  3. children:子节点,要么是VNode,要么是text;
  4. elm:记录 vnode 对应的真实 DOM
  5. text :节点中的内容,和 children 只能互斥
  6. key: 该参数不在接口中,值由data所影响
import { Hooks } from './hooks'
import { AttachData } from './helpers/attachto'
import { VNodeStyle } from './modules/style'
import { On } from './modules/eventlisteners'
import { Attrs } from './modules/attributes'
import { Classes } from './modules/class'
import { Props } from './modules/props'
import { Dataset } from './modules/dataset'
import { Hero } from './modules/hero'

export type Key = string | number

export interface VNode {
  sel: string | undefined
  data: VNodeData | undefined
  children: Array<VNode | string> | undefined
  elm: Node | undefined
  text: string | undefined
  key: Key | undefined
}

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?: any[] // for thunks
  [key: string]: any // for any other 3rd party module
}

export function vnode (sel: string | undefined,
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}

init.ts(原snabbdom.ts)

核心代码,调用init函数时会返回一个patch函数,用于比较两个vnode差异并更新,这也是虚拟DOM的原理。
大致结构如下:

  • 开头部分先定义了所需的类型以及所需的辅助函数
  • 第46行定义了常量hooks,存放钩子函数名称的一个数组(不同的钩子函数在不同时期执行)
  • 最后就是导出部分了,将init导出(这一块与0.7.4版本不同,老版导出了h,thunk,init核心三件套)

从48行到开始到最后都是init的实现,首先init传入两个参数:

  1. modules:模块,处理元素的样式、属性、事件(都存放在package/modules中)
  2. domapi:执行一些DOM操作(定义在htmldomapi.ts中),其实就是调用了ducoment下dom操作的api

代码实现上,先初始化转换虚拟节点的domapi

const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi

再把传入的所有modules的钩子函数,通过遍历全部存储到一个cbs对象中

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 any[]).push(hook)
     }
   }
 }

接下来就是在函数内部定义了很多的辅助函数,它们主要是用到patch函数内部,也就是init返回值,它会在合适的时机调用:
在这里插入图片描述
最后就是返回的patch函数了,参数传入新旧节点,比较后进行更新。首先定义的insertedVnodeQueue是一个队列,保存新插入的节点,它是为了触发节点的钩子函数。之后for循环先执行了cbs对象中pre的钩子函数,再判断oldVnode是真实DOM还是VNode,如果是真实DOM则转为空的VNode。转化后,判断新旧节点是不是相同的节点,是则比较新旧节点差异并更新到DOM中,不是则需要把新节点渲染成DOM,再触发init/create的钩子函数。最后两个for循环分别执行用户的insert和模块的post钩子函数,执行完后把新节点返回,作为更新后的旧节点,以便于下次更新比较。

return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()

    if (!isVnode(oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode)
    }

    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      elm = oldVnode.elm!
      parent = api.parentNode(elm) as Node

      createElm(vnode, insertedVnodeQueue)

      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        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
  }
}
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值