高频面试题:自定义指令?
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令。在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。举个聚焦输入框的例子,如下:
一、全局自定义指令
Vue.directive("focus", {
inserted: function(el) {
el.focus();
}
});
new Vue({
el: "#app",
template: `<input v-focus>`
});
以上例子通过Vue.directive的方式进行全局
1、directive的注册
其中的Vue.directive是在全局执行initGlobalAPI(Vue)的时候通过initAssetRegisters进行定义:
const ASSET_TYPES = [
'component',
'directive',
'filter'
]
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
ASSET_TYPES中有component、directive和filter,这里先看directive逻辑,如果传入的typeof definition === 'function',那么还会将其通过definition = { bind: definition, update: definition }的方式进行,最后,再通过this.options[type + 's'][id] = definition的方式定义到当前的this.options中,在全局进行调用。
2、编译过程
(1)ast
在ast的构建过程中,在closeElement阶段会针对directive进行属性的处理processAttrs(element),其中有addDirective:
export function addDirective (
el: ASTElement,
name: string,
rawName: string,
value: string,
arg: ?string,
isDynamicArg: boolean,
modifiers: ?ASTModifiers,
range?: Range
) {
(el.directives || (el.directives = [])).push(rangeSetItem({
name,
rawName,
value,
arg,
isDynamicArg,
modifiers
}, range))
el.plain = false
}
通过addDirective为ast添加directives属性。
(2)code
在执行genData中进行code字符串拼接的过程中会执行const dirs = genDirectives(el, state):
function genDirectives (el: ASTElement, state: CodegenState): string | void {
const dirs = el.directives
if (!dirs) return
let res = 'directives:['
let hasRuntime = false
let i, l, dir, needRuntime
for (i = 0, l = dirs.length; i < l; i++) {
dir = dirs[i]
needRuntime = true
const gen: DirectiveFunction = state.directives[dir.name]
if (gen) {
// compile-time directive that manipulates AST.
// returns true if it also needs a runtime counterpart.
needRuntime = !!gen(el, dir, state.warn)
}
if (needRuntime) {
hasRuntime = true
res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
}${
dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
}${
dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
}},`
}
}
if (hasRuntime) {
return res.slice(0, -1) + ']'
}
}
在state.directives并未找到focus,故而执行到needRuntime为true的逻辑,拼接中会存在"directives:[{name:"focus",rawName:"v-focus"}]"。
3、vNode
生成的render的结果为:
with(this) {
return _c('input', {
directives: [{
name: "focus",
rawName: "v-focus"
}]
})
}
在生成的vNode中会包含data:
{
"directives": [
{
"name": "focus",
"rawName": "v-focus"
}
]
}
4、patch
(1)invokeCreateHooks
在patch过程中,会执行到createElm的逻辑,其中会invokeCreateHooks(vnode, insertedVnodeQueue):
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, vnode)
}
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) i.create(emptyNode, vnode)
if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
}
}
cbs.create[i](emptyNode, vnode)有updateDirectives的逻辑:
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
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)
const dirsWithInsert = []
const dirsWithPostpatch = []
let key, oldDir, dir
for (key in newDirs) {
oldDir = oldDirs[key]
dir = newDirs[key]
if (!oldDir) {
// new directive, bind
callHook(dir, 'bind', vnode, oldVnode)
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir)
}
} else {
// existing directive, update
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
callHook(dir, 'update', vnode, oldVnode)
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(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) {
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind
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) {
// $flow-disable-line
return res
}
let i, dir
for (i = 0; i < dirs.length; i++) {
dir = dirs[i]
if (!dir.modifiers) {
// $flow-disable-line
dir.modifiers = emptyModifiers
}
res[getRawDirName(dir)] = dir
dir.def = resolveAsset(vm.$options, 'directives', dir.name, true)
}
// $flow-disable-line
return res
}
通过resolveAsset(vm.$options, 'directives', dir.name, true)解析出directives中的focus,即为inserted: function(el) { el.focus(); }。
当前例子会满足dir.def && dir.def.inserted为true,进而执行dirsWithInsert.push(dir)。接着判断dirsWithInsert.length的条件存在,定义callInsert为遍历执行callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)的函数。当前const isCreate = oldVnode === emptyNode计算得出isCreate为true,接着执行mergeVNodeHook(vnode, 'insert', callInsert):
export function mergeVNodeHook (def: Object, hookKey: string, hook: Function) {
if (def instanceof VNode) {
def = def.data.hook || (def.data.hook = {})
}
let invoker
const oldHook = def[hookKey]
function wrappedHook () {
hook.apply(this, arguments)
// important: remove merged hook to ensure it's called only once
// and prevent memory leak
remove(invoker.fns, wrappedHook)
}
if (isUndef(oldHook)) {
// no existing hook
invoker = createFnInvoker([wrappedHook])
} else {
/* istanbul ignore if */
if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
// already a merged invoker
invoker = oldHook
invoker.fns.push(wrappedHook)
} else {
// existing plain hook
invoker = createFnInvoker([oldHook, wrappedHook])
}
}
invoker.merged = true
def[hookKey] = invoker
}
function createFnInvoker (fns, vm) {
function invoker () {
var arguments$1 = arguments;
var fns = invoker.fns;
if (Array.isArray(fns)) {
var cloned = fns.slice();
for (var i = 0; i < cloned.length; i++) {
invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler");
}
} else {
// return handler return value for single handlers
return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler")
}
}
invoker.fns = fns;
return invoker
}
通过mergeVNodeHook将其中的callInsert进行wrappedHook的包裹,以便在后续执行的过程中可以清除wrappedHook,以免发生内存泄漏(memory leak)。再通过createFnInvoker的方式,为[wrappedHook]的执行增加了错误处理。
在invokeCreateHooks逻辑中,会通过i = vnode.data.hook获取data中的钩子,并且有inserted,执行到insertedVnodeQueue.push(vnode)。
(2)invokeInsertHook
等到页面中的节点渲染完成以后,会执行invokeInsertHook调用insert钩子。
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue
} else {
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
}
在当前例子中,会遍历获取到queue[i].data.hook.insert,并将参数queue[i]传入,最终会执行到callInsert的逻辑:
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
其中的callHook如下:
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook]
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
最后执行到res = args ? handler.apply(context, args) : handler.call(context),最后接着绕,会绕到el.focus()的逻辑,让当前的input进行聚焦。
总结
整个过程中,从编译,
vNode的获取,再到patch过程,再通过invokeCreateHooks和invokeInsertHook完成了对v-focus的处理。