vue指令本质
指令本质上是装饰器,是vue对HTML元素的扩展,给HTML元素增加自定义功能,语义化HTML标签。vue编译DOM时,会执行与指令关联的JS代码,即找到指令对象,执行指令对象的相关方法。
自定义指令生命周期
自定义指令有五个生命周期(也叫钩子函数),分别是bind、inserted、update、componentUpdated、unbind
钩子函数作用介绍
- bind:只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个绑定时执行一次的初始化动作。
- inserted:被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于document中)。
- update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
- componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
- unbind:只调用一次,指令与元素解绑时调用。
实现过程
// 版本2.6.10
export default {
create: updateDirectives,
update: updateDirectives,
destroy: function unbindDirectives (vnode: VNodeWithData) {
updateDirectives(vnode, emptyNode)
}
}
function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode)
}
}
function _update (oldVnode, vnode) {
const isCreate = oldVnode === emptyNode // 判断虚拟节点是否是一个新创建的节点
const isDestroy = vnode === emptyNode // 当新的虚拟节点不存在,在旧虚拟节点存在时,为true
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context) // 旧指令集合
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context) // 新指令集合
const dirsWithInsert = [] // 保存需要触发inserted指令钩子的列表
const dirsWithPostpatch = [] // 保存需要触发componentUpdated指令钩子的列表
let key, oldDir, dir
for (key in newDirs) {
oldDir = oldDirs[key]
dir = newDirs[key]
if (!oldDir) { // 判断oldDir是否存在,如果不存在,则首次绑定到元素中
// 调用bind
callHook(dir, 'bind', vnode, oldVnode)
// 判断指令是否有inserted方法,有则添加到dirsWithInsert,保证执行完指令的bind方法后执行inserted方法
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir)
}
} else {
// oldDir存在,则更新指令
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
callHook(dir, 'update', vnode, oldVnode)
// 判断指令是否有componentUpdated方法,有则添加到dirsWithPostpatch,
// 保证指令所在的vnode及自vnode更新完后(执行完指令的update方法后),执行componentUpdated方法
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir)
}
}
}
if (dirsWithInsert.length) {
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
if (isCreate) {
// 如果是新创建的节点,使用mergeVNodeHook将一个钩子函数与虚拟节点现有的钩子函数合并在一起
// 可以将钩子函数的执行推迟到被绑定的元素插入到父节点之后进行
mergeVNodeHook(vnode, 'insert', callInsert)
} else {
callInsert()
}
}
if (dirsWithPostpatch.length) {
mergeVNodeHook(vnode, 'postpatch', () => {
for (let i = 0; i < dirsWithPostpatch.length; i++) {
callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
}
})
}
// 先判断当前虚拟节点是否是新创建
if (!isCreate) {
// 循环旧指令集合,找出不存在的,则该指令是废弃的,并执行指令的unbind方法
for (key in oldDirs) {
if (!newDirs[key]) {
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
}
}
}
}
const emptyModifiers = Object.create(null)
function normalizeDirectives (
dirs: ?Array<VNodeDirective>,
vm: Component
): { [key: string]: VNodeDirective } {
const res = Object.create(null)
if (!dirs) {
return res
}
let i, dir
for (i = 0; i < dirs.length; i++) {
dir = dirs[i]
if (!dir.modifiers) {
dir.modifiers = emptyModifiers
}
res[getRawDirName(dir)] = dir
dir.def = resolveAsset(vm.$options, 'directives', dir.name, true)
}
return res
}
function getRawDirName (dir: VNodeDirective): string {
return dir.rawName || `${dir.name}.${Object.keys(dir.modifiers || {}).join('.')}`
}
function callHook (dir, hook, vnode, oldVnode, isDestroy) {
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`)
}
}
}