在讲正文之前,大家先把官网中有关自定义指令的内容再看一遍,带着疑问看解析。
自定义指令的源码牵扯到了前面没有讲到的知识点,所以先对这些需要的知识点做下补充。
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 = {}// 从参数中取出 modulesconst { modules, nodeOps } = backend// 遍历 hooks 数组,hooks 数组的内容是虚拟 DOM 的钩子函数字符串for (i = 0; i }
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 遍历 newDirsfor (key in newDirs) {// 使用遍历对象的 key 从 oldDirs 和 newDirs 中获取 oldDir 和 diroldDir = 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 {for (let i = 0; i }// 调用情形例如: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,结语
好了,到这就是自定义指令解析的全部内容。接下来,我会继续解析剩下没有讲到的指令。