Vue源码阅读(19):自定义指令的源码解析

 我的开源库:

在讲正文之前,大家先把官网中有关自定义指令的内容再看一遍,带着疑问看解析。

 自定义指令的源码牵扯到了前面没有讲到的知识点,所以先对这些需要的知识点做下补充。

1,知识补充

1-1,虚拟DOM在渲染时,会触发钩子函数

在前面的文章 Vue源码阅读(17):patch() 方法 中,我主要讲了组件在重新渲染时,对 DOM 内容的更新。其实 Vue 除了对 DOM 内容进行了更新外,还做了其他的操作,其中之一就是虚拟 DOM 在渲染时会触发对应的钩子函数。没错,每一个虚拟 DOM 都有钩子函数,在渲染的不同时机会触发执行,虚拟 DOM 的钩子函数以及触发时机如下表所示。

名称触发时机
init在 patch 期间发现新的虚拟节点时触发
create已经基于 VNode 创建了真实的 DOM 元素
activatekeepAlive组件被创建
insertvnode 对应的 DOM 元素被插入到视图中
prepatchvnode 在进行更新节点操作之前
updatevnode 进行更新节点操作时
postpatchvnode 完成了更新节点操作
destroyvnode 对应的 DOM 元素从页面中移除,或者 vnode 对应的 DOM 元素的父元素从页面中移除
removevnode 对应的 DOM 元素从页面中移除时触发

1-2,虚拟 DOM 重新渲染时,除了更新显示的内容,还会更新什么?

DOM 元素除了其显示的内容外,还有很多其他的东西,例如:directives、ref、attrs、class、events、style 等,DOM 的这些东西在组件重新渲染的时候,也需要同步进行更新,这样更新出来的新页面才是我们想要的结果。否则,如果只是更新了 DOM 中显示的内容,DOM 上的 class、style 和 events 之类的东西还是保留之前的,这样的组件更新只是更新了其外表,内在没有进行更新,更新的页面结果自然也就是错误的

Vue 将这些内容的操作都封装到了一个个的对象之中,这些对象的 key 是上一小节所说的钩子函数的名称,value 是当对应的钩子函数执行时对应内容操作的函数,例如:

src/core/vdom/modules/directives.js

export default {
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives (vnode: VNodeWithData) {
    updateDirectives(vnode, emptyNode)
  }
}

src/platforms/web/runtime/modules/class.js

export default {
  create: updateClass,
  update: updateClass
}

src/platforms/web/runtime/modules/events.js

export default {
  create: updateDOMListeners,
  update: updateDOMListeners
}

上面导出的三个对象分别对应 directives、class、events,当虚拟 DOM 的钩子函数触发时,就会执行这些导出对象中对应的函数。例如:虚拟 DOM 触发了 update 钩子函数,Vue 的底层就会执行上面三个对象中的三个 update 函数,对 DOM 的 directives、class、events 进行更新。

1-3,上面两小节对应源码的整合解析

1-3-1,src/core/vdom/modules/index.js

import directives from './directives'
import ref from './ref'

export default [
  ref,
  directives
]

导出一个数组,数组中的内容是一个个的对象,对象的内容看上面的 1-2 小节。

1-3-2,src/platforms/web/runtime/modules/index.js

import attrs from './attrs'
import klass from './class'
import events from './events'
import domProps from './dom-props'
import style from './style'
import transition from './transition'

export default [
  attrs,
  klass,
  events,
  domProps,
  style,
  transition
]

导出一个数组,数组中的内容是一个个的对象,对象的内容看上面的 1-2 小节。

1-3-3,src/platforms/web/runtime/patch.js

/* @flow */

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'

import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// 将上面两小节导出的数组拼接成一个数组
const modules = platformModules.concat(baseModules)

// 将 { nodeOps, modules } 作为参数,执行 createPatchFunction 方法,创建出 patch 函数
export const patch: Function = createPatchFunction({ nodeOps, modules })

1-3-4,src/core/vdom/patch.js

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) {
  let i, j
  // 很重要的一个变量
  const cbs = {}

  // 从参数中取出 modules
  const { modules, nodeOps } = backend

  // 遍历 hooks 数组,hooks 数组的内容是虚拟 DOM 的钩子函数字符串
  for (i = 0; i < hooks.length; ++i) {
    // 在 cbs 对象中,创建每个钩子函数对应的数组,效果如下所示:
    //cbs = {
    //   create: [],
    //   activate: [],
    //   update: [],
    //   remove: [],
    //   destroy: []
    // }
    cbs[hooks[i]] = []
    // 遍历 modules 数组
    for (j = 0; j < modules.length; ++j) {
      // 如果当前遍历的对象中存在当前遍历的钩子函数对应的函数时,
      // 将对应的函数 push 到 cbs[hooks[i]] 中
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }

    // 最终的 cbs 如下所示:
    //cbs = {
    //   create: [ref-create-f, directives-create-f, attrs-create-f, ......],
    //   activate: [transition-activate-f],
    //   update: [directives-update-f, ......],
    //   remove: [transition-remove-f],
    //   destroy: [directives-destroy-f,......]
    // }
  }

  // 更新节点的方法
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { 
    ......
    if (isDef(data) && isPatchable(vnode)) {
      // 触发 update 钩子函数,
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    }
    ......
  }

  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    ......
    patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
    ......
  }
}

1-4,小总结

自定义指令就是让被指令注册的 DOM 节点在不同的时期执行自定义指令中对应的函数,以此让被指令注册的 DOM 节点具有相应的功能。在该小节中,我们知道了 src/core/vdom/modules/directives.js 导出对象中的函数是如何被触发执行的,接下来,我们开始详细看看 src/core/vdom/modules/directives.js 文件中的源码。

2,自定义指令源码解析

2-1,src/core/vdom/modules/directives.js 文件导出的对象

export default {
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives (vnode: VNodeWithData) {
    updateDirectives(vnode, emptyNode)
  }
}

directives.js 文件中导出的对象,监控了虚拟 DOM 的三个回调函数,分别是 create、update、destroy,当虚拟 DOM 中的 create、update、destroy 钩子函数执行时,该导出对象中的函数便会触发执行。

可以发现,无论上面哪个钩子被触发,最终处理的都是 updateDirectives 函数。

2-2,updateDirectives

function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode)
  }
}

只要 vnode 和 oldVnode 中有一个使用了自定义指令,就会执行 _update() 方法,否则什么都不做。

2-3,_update()

_update() 方法是自定义指令功能的核心方法,详细的解析我都写在了注释中,大家看注释即可理解。

function _update (oldVnode, vnode) {
  // 判断 vnode 是不是一个新建的节点
  const isCreate = oldVnode === emptyNode
  // 判断当前的处理,vnode 是不是被销毁移除
  const isDestroy = vnode === emptyNode
  // oldVnode 中的指令集合
  // 是一个对象,结构如下所示:
  // {
  //   v-focus: {
  //     def: {inserted: f},
  //     modifiers: {},
  //     name: "focus",
  //     rawName: "v-focus"
  //   }
  // }
  const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
  // vnode 中的指令集合,也是一个对象
  const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)

  // 保存需要触发 inserted 指令钩子函数的指令列表
  const dirsWithInsert = []
  // 保存需要触发 componentUpdated 指令钩子函数的指令列表
  const dirsWithPostpatch = []

  // 接下来要做的事情是对比 newDirs 和 oldDirs 两个指令集合并触发执行对应的钩子函数
  let key, oldDir, dir
  // 使用 for in 遍历 newDirs
  for (key in newDirs) {
    // 使用遍历对象的 key 从 oldDirs 和 newDirs 中获取 oldDir 和 dir
    oldDir = oldDirs[key]
    dir = newDirs[key]
    if (!oldDir) {
      // 如果 oldDir 不存在的话,说明当前循环的指令是首次绑定到元素
      // 此时需要触发执行 dir 指令中的 bind 函数
      callHook(dir, 'bind', vnode, oldVnode)
      // 如果 dir 指令中存在 inserted 方法的话,那么该指令将被添加到 dirsWithInsert 数组中,
      // 稍后再触发执行这些 inserted 方法,这样做的目的是:执行完所有的 bind 方法后,再执行 inserted 方法
      if (dir.def && dir.def.inserted) {
        dirsWithInsert.push(dir)
      }
    } else {
      // 如果 oldDir 存在的话,说明当前的指令已经被绑定过了,此时应该执行 dir 中的 update 方法
      dir.oldValue = oldDir.value
      // 触发执行 dir 中的 update 方法
      callHook(dir, 'update', vnode, oldVnode)
      // 判断 dir 中有没有定义 componentUpdated 方法,如果定义了的话,将其添加到 dirsWithPostpatch 数组中
      // 这样做的目的是保证:指令所在组件的 VNode 及其子 VNode 全部更新后调用
      if (dir.def && dir.def.componentUpdated) {
        dirsWithPostpatch.push(dir)
      }
    }
  }

  // 处理 inserted 方法
  if (dirsWithInsert.length) {
    // 创建一个新的函数 callInsert,在该函数中,真正的触发执行 inserted 方法,
    // 确保触发执行 inserted 方法是在被绑定元素插入到父节点之后。
    const callInsert = () => {
      for (let i = 0; i < dirsWithInsert.length; i++) {
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
      }
    }
    // isCreate 用于判断 vnode 是不是一个新建的节点
    if (isCreate) {
      // 如果 vnode 是新创建的节点,那么就应该等到元素被插入到父节点之后再触发执行指令的 inserted 方法
      // 在这里,通过 mergeVNodeHook 将 callInsert 添加到虚拟节点的 insert 钩子函数列表中,将 inserted 方法
      // 的执行推迟到元素插入到父节点之后
      mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', callInsert)
    } else {
      // 如果被绑定元素已经被插入到父节点,则直接触发执行 callInsert 函数
      callInsert()
    }
  }

  // 处理 componentUpdated 方法
  if (dirsWithPostpatch.length) {
    // 这里和上面的 inserted 同理。
    // dir 中的 componentUpdated 方法需要在指令所在组件的 VNode 及其子 VNode 全部更新之后触发执行
    mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'postpatch', () => {
      for (let i = 0; i < dirsWithPostpatch.length; i++) {
        callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
      }
    })
  }

  // 处理 unbind 方法
  if (!isCreate) {
    for (key in oldDirs) {
      if (!newDirs[key]) {
        // 如果 oldDirs 中的指令在 newDirs 中不存在的话,说明该指令已经被解除了,此时触发执行 oldDirs[key] 中的 unbind 方法
        callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
      }
    }
  }
}

// 调用情形例如:callHook(dir, 'bind', vnode, oldVnode),dir 对象的结构如下所示:
// {
//   def: {bind: f},
//   modifiers: {},
//   name: "focus",
//   rawName: "v-focus"
// }
function callHook (dir, hook, vnode, oldVnode, isDestroy) {
  // 获取 dir 指令中指定的 hook 函数,然后触发执行
  const fn = dir.def && dir.def[hook]
  if (fn) {
    try {
      fn(vnode.elm, dir, vnode, oldVnode, isDestroy)
    } catch (e) {
      handleError(e, vnode.context, `directive ${dir.name} ${hook} hook`)
    }
  }
}

3,结语

好了,到这就是自定义指令解析的全部内容。接下来,我会继续解析剩下没有讲到的指令。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值