我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
在讲正文之前,大家先把官网中有关自定义指令的内容再看一遍,带着疑问看解析。
自定义指令的源码牵扯到了前面没有讲到的知识点,所以先对这些需要的知识点做下补充。
1,知识补充
1-1,虚拟DOM在渲染时,会触发钩子函数
在前面的文章 Vue源码阅读(17):patch() 方法 中,我主要讲了组件在重新渲染时,对 DOM 内容的更新。其实 Vue 除了对 DOM 内容进行了更新外,还做了其他的操作,其中之一就是虚拟 DOM 在渲染时会触发对应的钩子函数。没错,每一个虚拟 DOM 都有钩子函数,在渲染的不同时机会触发执行,虚拟 DOM 的钩子函数以及触发时机如下表所示。
名称 | 触发时机 |
---|---|
init | 在 patch 期间发现新的虚拟节点时触发 |
create | 已经基于 VNode 创建了真实的 DOM 元素 |
activate | keepAlive组件被创建 |
insert | vnode 对应的 DOM 元素被插入到视图中 |
prepatch | vnode 在进行更新节点操作之前 |
update | vnode 进行更新节点操作时 |
postpatch | vnode 完成了更新节点操作 |
destroy | vnode 对应的 DOM 元素从页面中移除,或者 vnode 对应的 DOM 元素的父元素从页面中移除 |
remove | vnode 对应的 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,结语
好了,到这就是自定义指令解析的全部内容。接下来,我会继续解析剩下没有讲到的指令。