Vue.js 框架源码与进阶

导学

Vue.js 框架源码与进阶_第1张图片

课程目标

  • 了解什么是虚拟DOM,以及虚拟DOM的作用
  • Snabbdom的基本使用
  • Snabbdom的源码解析

什么是Virtual DOM

  • Virtual DOM(虚拟DOM),是由普通的JS对象来描述DOM对象,因为不是真实的DOM对象,所以叫Virtual DOM
  • 真实DOM成员

Vue.js 框架源码与进阶_第2张图片

  • 可以使用Virtual DOM来描述真实DOM,示例
{ 
    sel: "div", 
    data: {}, 
    children: undefined, 
    text: "Hello Virtual DOM", 
    elm: undefined, 
    key: undefined 
}

注意:创建一个虚拟DOM比创建一个真实的DOM成本低很多

为什么使用虚拟DOM

  • 手动操作DOM比较麻烦,还需要考虑浏览器兼容性问题,虽然有jQuery等库简化DOM操作,但是随着项目的复杂DOM操作复杂提升
  • 为了简化DOM的复杂操作于是出现了各种MVVM框架,MVVM框架解决了视图和状态的同步问题
  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是Virtual DOM出现了
  • Virtual DOM的好处是当状态改变时不需要立即更新DOM,只需要创建一个虚拟树来描述DOM,Virtual DOM内部将弄清楚如何有效(diff)的更新DOM
  • 参考 github上 virtual-dom的描述

           1. 虚拟DOM可以维护程序的状态,跟踪上一次的状态

           2. 通过比较前后两次状态的差异更新真实DOM

Vue.js 框架源码与进阶_第3张图片

虚拟DOM的作用

  • 维护视图和状态的关系
  • 复杂视图情况下提升渲染性能
  • 除了渲染DOM以外,还可以实现SSR(Nuxt.js/Next.js)、原生应用(weex/React Native)、小程序(mpvue/uni-app)等

Vue.js 框架源码与进阶_第4张图片

Virtual DOM库

  1. Snabbdom
    1. Vue 2.x内部使用的Virtual DOM就是改造的Snabbdom
    2. 大约200SLOC(single line of code)
    3. 通过模块扩展
    4. 源码使用TypeScript开发
    5. 最快的Virtual DOM之一
  2. virtual-dom

Snabbdom基本使用

创建项目

  • 打包工具为了方便使用parcel
  • 创建项目,并安装parcel
# 创建项目目录
md snabbdom-demo
# 进入项目目录
cd snabbdom-demo
# 创建 package.json
yarn init -y
# 本地安装 parcel
yarn add parcel-bundler
  • 配置package.json的scripts
"scripts": {
    "dev": "parcel index.html --open",
    "build": "parcel build index.html"
}
  • 创建目录结构
index.html
package.json
01-basicuage.js

导入Snabbdom

中文文档:https://github.com/coconilu/Blog/issues/152

yarn add snabbdom
  • Snabbdom的官网demo中导入使用的是commonjs模块化语法,我们使用更流行的ES6模块化的语法import
  • 关于模块化的语法请参考阮一峰老师的Module的语法
  • ES6模块与commonJS模块的差异(https://www.cnblogs.com/raind/p/9536600.html)
import {init, h, thunk} from 'snabbdom'
  • Snabbdom的核心仅提供最基本的功能,只导出了三个函数init()、h()、thunk()
    • init()是一个高阶函数,返回patch()
    • h()返回虚拟节点VNode,这个函数我们在使用Vue.js的时候见过
new Vue({
    router,
    store,
    render: h => h(App)
}).$mount('#app')
    • thunk()是一种优化策略,可以在处理不变数据时使用
    • thunk()是一种优化策略,可以在处理不变数据时使用
  • 注意:导入时候不能使用import snabbdom from 'snabbdom'
    • 原因: node_modules/src/snabbdom.ts末尾导出使用的语法是export导出API,没有使用export default导出默认输出
export {h} from './h'
export {thunk} from './thunk'

export function init(modules: Array>, domApi?: DOMAPI)
}

Snabbdom的基本用法

两个案例来展示init()和h()这两个函数的功能

01-basicusage.js:

import { h, init} from 'snabbdom'


// 1. hello world
// 参数: 数组,模块
// 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM
let patch = init([])
// 第一个参数: 标签+ 选择器
// 第二个参数: 如果是字符串的话就是标签中的内容
let vnode = h('div#container.cls','Hello World')

let app = document.querySelector('#app')

// 第一个参数: 可以是DOM元素,内部会把DOM元素转换成VNode
// 第二个参数: VNode
// 返回值:VNode
let  oldVNode = patch(app, vnode)

// 假设的时刻
vnode = h('div', 'Hello Snabbdom')

patch(oldVNode, vnode)

02-basicusage.js:

// 2. div中放置子元素 h1,p
import { h , init } from 'snabbdom'

let patch = init([])

let vnode = h('div#container',[
  h('h1', 'Hello Snabbdom'),
  h('p', '这是一个p标签')
])

let app = document.querySelector('#app')

let oldVnode = patch(app, vnode)

setTimeout(() => {
  vnode = h('div#container',[
    h('h1', 'Hello World'),
    h('p', 'Hello P')
  ])
  patch(oldVnode, vnode)

  // 清空页面元素 -- 错误
  // patch(oldVnode, null)
  patch(oldVnode, h('!'))
}, 2000);

Snabbdom的模块

Snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块

常用模块

  1. 官方提供了6个模块
    1. attributes
      1. 设置DOM元素的属性,使用setAttribute()
      2. 处理布尔类型的属性
    2. props
      1. 和attributes模块相似,设置DOM元素的属性element[attr] = value
      2. 不处理布尔类型的属性
    3. class
      1. 切换类样式
      2. 注意:给元素设置类样式是通过sel选择器
    4. dataset
      1. 切换类样式
      2. 注意:给元素设置类样式是通过 sel 选择器
    5. eventlisteners
      1. 注册和移除事件
    6. style
      1. 设置行内样式,支持动画
      2. delayed/remove/destroy

模块使用

  1. 模块使用步骤
    1. 导入需要的模块
    2. init() 中注册模块
    3. 使用 h() 函数创建VNode的时候,可以把第二个参数设置为对象,其他参数往后移

代码演示:

import { init, h } from 'snabbdom'
// 1. 导入模块
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'
// 2. 注册模块
let patch = init([
  style,
  eventlisteners
])
// 3. 使用h() 函数的第二个参数传入模块需要的数据(对象)
let vnode = h('div', {
  style: {
    backgroundColor: 'red'
  },
  on: {
    click: eventHandler
  }
},[
  h('h1', 'Hello Snabbdom'),
  h('p', '这是p标签')
])

function eventHandler () {
  console.log('点击我了')
}

let app = document.querySelector('#app')

patch(app, vnode)

Snabbdom源码解析

概述

如何学习源码

  • 先宏观了解
  • 带着目标看源码
  • 看源码的过程要求不求甚解
  • 调试
  • 参考资料

Snabbdom的核心

  • 使用h()函数创建JavaScript对象(VNode)描述真实DOM
  • init()设置模块,创建patch()
  • patch()比较新旧两个VNode
  • 把变化的内容更新到真实DOM树上

Snabbdom源码

  • 源码地址
    • https://github.com/snabbdom/snabbdom
  • src目录结构

Vue.js 框架源码与进阶_第5张图片

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(1, 2, 3)

源码位置: src/h.ts (作用:调用vnode函数返回虚拟节点)

import { vnode, VNode, VNodeData } from './vnode'
import * as is from './is'

export type VNodes = VNode[]
export type VNodeChildElement = VNode | string | number | undefined | null
export type ArrayOrElement = T | T[]
export type VNodeChildren = ArrayOrElement

function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
  data.ns = 'http://www.w3.org/2000/svg'
  if (sel !== 'foreignObject' && children !== undefined) {
    for (let i = 0; i < children.length; ++i) {
      const childData = children[i].data
      if (childData !== undefined) {
        addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel)
      }
    }
  }
}

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)
};

必备快捷键

  • F12、Ctrl+ 单击鼠标左键,快速定位到引入的位置
  • Alt + 左键,返回刚才的定位
  • 选中array,F12查看array可以看见is下array的实现

vnode

export interface VNode {
  sel: string | undefined
  data: VNodeData | undefined
  children: Array | 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 | undefined,
  text: string | undefined,
  elm: Element | Text | undefined): VNode {
  const key = data === undefined ? undefined : data.key
  return { sel, data, children, text, elm, key }
}

VNode渲染真实DOM

snabbdom

  • patch(oldVnode, newVnode)
  • 打补丁,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
  • 对比新旧VNode是否相同节点(节点的key和sel相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的VNode是否有text,如果有并且和oldVnode的text不同,直接更新文本内容
  • diff过程只进行同层级比较

Vue.js 框架源码与进阶_第6张图片

 

 

init函数

设置模块,传入modules和domApi;没有传第二个参数domApi时,使用htmlDomApi(把虚拟DOM转换成真实DOM)

export interface DOMAPI {
  createElement: (tagName: any) => HTMLElement
  createElementNS: (namespaceURI: string, qualifiedName: string) => Element
  createTextNode: (text: string) => Text
  createComment: (text: string) => Comment
  insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void
  removeChild: (node: Node, child: Node) => void
  appendChild: (node: Node, child: Node) => void
  parentNode: (node: Node) => Node | null
  nextSibling: (node: Node) => Node | null
  tagName: (elm: Element) => string
  setTextContent: (node: Node, text: string | null) => void
  getTextContent: (node: Node) => string | null
  isElement: (node: Node) => node is Element
  isText: (node: Node) => node is Text
  isComment: (node: Node) => node is Comment
}

function createElement (tagName: any): HTMLElement {
  return document.createElement(tagName)
}

function createElementNS (namespaceURI: string, qualifiedName: string): Element {
  return document.createElementNS(namespaceURI, qualifiedName)
}

function createTextNode (text: string): Text {
  return document.createTextNode(text)
}

function createComment (text: string): Comment {
  return document.createComment(text)
}

function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node | null): void {
  parentNode.insertBefore(newNode, referenceNode)
}

function removeChild (node: Node, child: Node): void {
  node.removeChild(child)
}

function appendChild (node: Node, child: Node): void {
  node.appendChild(child)
}

function parentNode (node: Node): Node | null {
  return node.parentNode
}

function nextSibling (node: Node): Node | null {
  return node.nextSibling
}

function tagName (elm: Element): string {
  return elm.tagName
}

function setTextContent (node: Node, text: string | null): void {
  node.textContent = text
}

function getTextContent (node: Node): string | null {
  return node.textContent
}

function isElement (node: Node): node is Element {
  return node.nodeType === 1
}

function isText (node: Node): node is Text {
  return node.nodeType === 3
}

function isComment (node: Node): node is Comment {
  return node.nodeType === 8
}

export const htmlDomApi: DOMAPI = {
  createElement,
  createElementNS,
  createTextNode,
  createComment,
  insertBefore,
  removeChild,
  appendChild,
  parentNode,
  nextSibling,
  tagName,
  setTextContent,
  getTextContent,
  isElement,
  isText,
  isComment,
}
// 初始化转换虚拟节点的api
 const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi

// 把传入的所以模块的钩子函数,统一存储到cbs对象中
// 最终构建的cbs对象的形式 cbs = { crete: [fn1, fn2], update: [], ...}
  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)
      }
    }
  }

module:

import { PreHook, CreateHook, UpdateHook, DestroyHook, RemoveHook, PostHook } from '../hooks'

export type Module = Partial<{
  pre: PreHook
  create: CreateHook
  update: UpdateHook
  destroy: DestroyHook
  remove: RemoveHook
  post: PostHook
}>

 

patch函数

// init 内部返回 patch函数,把 vnode 渲染成真实 dom,并返回vnode
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]()
    
    // 如果 oldVnode不是VNode,创建VNode 并设置 elm
    if (!isVnode(oldVnode)) {
        // 把DOM元素转换成空的 VNode
      oldVnode = emptyNodeAt(oldVnode)
    }
    // 如果新旧节点是相同节点(key 和sel 相同)
    if (sameVnode(oldVnode, vnode)) {
        // 找节点的差异并更新 DOM
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
        // 如果新旧节点不同,vnode创建对应的 DOM
        // 获取当前的 DOM元素
      elm = oldVnode.elm!
      parent = api.parentNode(elm) as Node
      // 创建 vnode对应的DOM元素,并触发 init/reate 钩子函数
      createElm(vnode, insertedVnodeQueue)

      if (parent !== null) {
        // 如果父节点不为空,把 vnode对应的 DOM插入到文档中
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        // 移除老节点
        removeVnodes(parent, [oldVnode], 0, 0)
      }
    }
    // 执行用户设置的insert 钩子函数
    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
    }
    // 执行模块的post钩子函数
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
    // 返回 vnode
    return vnode
  }

createElm

function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
    let i: any
    let data = vnode.data
    if (data !== undefined) {
      const init = data.hook?.init
      if (isDef(init)) {
        init(vnode)
        data = vnode.data
      }
    }
    // 把 vnode 转换成真实 DOM对象(没有渲染到页面)
    const children = vnode.children
    const sel = vnode.sel
    if (sel === '!') {
        // 如果选择器是!,创建注释节点
      if (isUndef(vnode.text)) {
        vnode.text = ''
      }
      vnode.elm = api.createComment(vnode.text!)
    } 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
      const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
        ? api.createElementNS(i, tag)
        : api.createElement(tag)
      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)
      // 如果 vnode中有子节点,创建子vnode对应的DOM元素并追加到 DOM 树上
      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)) {
       // 如果 vnode的text值是 string/number,创建文本节点并追加到 DOM 树
        api.appendChild(elm, api.createTextNode(vnode.text))
      }
      const hook = vnode.data!.hook
      if (isDef(hook)) {
        // 执行用户传入的钩子 create
        hook.create?.(emptyNode, vnode)
        if (hook.insert) {
          // 把vnode添加到队列中,为后续执行 insert 钩子做准备
          insertedVnodeQueue.push(vnode)
        }
      }
    } else {
      // 如果选择器为空,创建文本节点
      vnode.elm = api.createTextNode(vnode.text!)
    }
    // 返回新创建的 DOM
    return vnode.elm
  }

Vue.js 框架源码与进阶_第7张图片

addVnodes和removeVnodes

 

function addVnodes (
    parentElm: Node,
    before: Node | null,
    vnodes: 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)
      }
    }
  } 

 

// 批量删除节点
   function removeVnodes (parentElm: Node,
    vnodes: VNode[],
    startIdx: number,
    endIdx: number): void {
    for (; startIdx <= endIdx; ++startIdx) {
      let listeners: number
      let rm: () => void
      const ch = vnodes[startIdx]
      if (ch != null) {
        if (isDef(ch.sel)) {
          invokeDestroyHook(ch)
          listeners = cbs.remove.length + 1
          rm = createRmCb(ch.elm!, listeners)
          for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
          const removeHook = ch?.data?.hook?.remove
          if (isDef(removeHook)) {
            removeHook(ch, rm)
          } else {
            rm()
          }
        } else { // Text node
          api.removeChild(parentElm, ch.elm!)
        }
      }
    }
  }

patchVnode

Vue.js 框架源码与进阶_第8张图片

 

function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
    const hook = vnode.data?.hook
    hook?.prepatch?.(oldVnode, vnode)
    const elm = vnode.elm = oldVnode.elm!
    const oldCh = oldVnode.children as VNode[]
    const ch = vnode.children as VNode[]
    if (oldVnode === vnode) return
    if (vnode.data !== undefined) {
        // 执行模块的 update钩子函数
      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      // 执行用户设置的 update 钩子函数
      vnode.data.hook?.update?.(oldVnode, vnode)
    }
    // 如果vnode.text 未定义
    if (isUndef(vnode.text)) {
      // 如果新老节点都有 children
      if (isDef(oldCh) && isDef(ch)) {
        // 使用diff算法对比子节点,老节点没有children
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
      } else if (isDef(ch)) {
        // 如果新节点有children,老节点没有children
        // 如果老节点有text,清空dom元素的内容
        if (isDef(oldVnode.text)) api.setTextContent(elm, '')
        // 批量添加子节点
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
        // 如果没有设置vnode.text
      } else if (isDef(oldCh)) {
          // 如果老节点有chaldren移除
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      }
      // 设置DOM元素的 textContent 为vnode.text
      api.setTextContent(elm, vnode.text!)
    }
    // 最后执行用户设置的 postpatch 钩子函数
    hook?.postpatch?.(oldVnode, vnode)
  }

 

你可能感兴趣的:(前端,vue源码,vue)