最近經(jīng)常使用指令來實(shí)現(xiàn)一些功能泡态,覺得甚是好用族展。然而卻遇到一個問題:v-if和我自定義指令在同一個標(biāo)簽里使用的時候,我自定義的指令傳的值通過bind.value沒獲取到值。后來換成v-show是我期待的結(jié)果吓坚,但是當(dāng)我把v-if換成v-else(v-if 和 v-else指令順序顛倒),也能獲取到binding.value的值灯荧。這就讓我對指令很感興趣礁击,想知道vue內(nèi)部又是怎么去實(shí)現(xiàn)指令的。
入口文件
指令在官方文檔中屬于全局API漏麦,所以從src\core\global-api\index.js中查找指令客税。
.....
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue) //初始化指令
......
于是我查看了文件src\core\global-api\assets.js,發(fā)現(xiàn)了這段代碼:
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
//省略其它代碼
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
該函數(shù)的參數(shù)有兩個撕贞,其中id就是指令的名字更耻,definition就是我們平時自定義指令傳的對象或者函數(shù)。這段代碼的意思也就很清楚了:如果是指令捏膨,且第二個參數(shù)是函數(shù)秧均,則會新定義一個對象,并把definition賦值給bind和update屬性号涯。最后目胡,會把指令的定義賦給Vue.options.directives[指令名]。感興趣的可以自己再vue官網(wǎng)打印一下链快。
上面是全局API的實(shí)現(xiàn)方式誉己。但是其實(shí)指令是加在模板的標(biāo)簽上的,所以模板編譯部分也做了處理域蜗。
模板編譯
模板編譯的入口文件src\compiler\parser\index.js巨双,你會很容易看到有個函數(shù)parseHTML(template, {}),代碼如下:
parseHTML(template, {
//......省略代碼
if (inVPre) {
processRawAttrs(element)
} else if (!element.processed) {
// structural directives
processFor(element) //解析v-for
processIf(element) //解析v-if
processOnce(element) //解析v-once
}
//......省略
if (!unary) {
currentParent = element
stack.push(element)
} else {
//自定義指令走這里
closeElement(element)
}
//......省略
})
一步一步向下找霉祸,會找到函數(shù)processElement()筑累,進(jìn)而發(fā)現(xiàn)除了上述代碼中直接解析的指令,剩下的指令和屬性統(tǒng)一都交給了processAttrs()函數(shù)處理丝蹭,比如v-bind慢宗、v-on以及普通屬性等。截取部分該函數(shù)的代碼:
//省略部分代碼
else { // normal directives
name = name.replace(dirRE, '')
// parse arg
const argMatch = name.match(argRE)
let arg = argMatch && argMatch[1]
isDynamic = false
if (arg) {
name = name.slice(0, -(arg.length + 1))
if (dynamicArgRE.test(arg)) {
arg = arg.slice(1, -1)
isDynamic = true
}
}
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
在該函數(shù)中處理了要傳給addDirective的參數(shù)對應(yīng)的值奔穿,最終在addDirective函數(shù)中把自定義指令添加到el上镜沽。然后我們在文件src\compiler\codegen\index.js中會找到constructor里的代碼:
constructor (options: CompilerOptions) {
this.options = options
this.warn = options.warn || baseWarn
this.transforms = pluckModuleFunction(options.modules, 'transformCode')
this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
this.directives = extend(extend({}, baseDirectives), options.directives)
//省略代碼
}
可以繼續(xù)在該文件中找到genDirectives函數(shù),可以看到添加到directives屬性中的對象的生成過程如下:
function genDirectives (el: ASTElement, state: CodegenState): string | void {
const dirs = el.directives
//省略代碼
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)}` : ''
}},`
}
}
對于添加到directives上的數(shù)據(jù)贱田,則是在patch中通過添加到cbs中的鉤子函數(shù)處理的淘邻。具體過程見src/core/vdom/modules/directives.js文件中。
export default {
create: updateDirectives,
update: updateDirectives,
destroy: function unbindDirectives (vnode: VNodeWithData) {
updateDirectives(vnode, emptyNode)
}
}
可以看到湘换,這三個鉤子函數(shù)宾舅,最終調(diào)用的都是updateDirectives方法统阿。
function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode)
}
}
如果updateDirectives()函數(shù)接受到的兩個參數(shù)data中有directives屬性,則調(diào)用_update方法來進(jìn)行處理筹我。_update方法的主要操作扶平,其實(shí)就是調(diào)用指令的各種鉤子函數(shù)。
function _update (oldVnode, vnode) {
// 第一次實(shí)例化組件時蔬蕊,oldVnode是emptyNode
const isCreate = oldVnode === emptyNode
// 銷毀組件時结澄,vnode是emptyNode
const isDestroy = vnode === emptyNode
//normalizeDirectives函數(shù)是從組件的vm.$options.directives中獲取指令的定義
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) {
//循環(huán)新vnode上綁定的指令
oldDir = oldDirs[key]
dir = newDirs[key]
if (!oldDir) {
// new directive, bind => 如果第一次綁定,則直接調(diào)用bind鉤子函數(shù)
callHook(dir, 'bind', vnode, oldVnode)
if (dir.def && dir.def.inserted) {
//若同時還添加了inserted鉤子岸夯,則會先把它添加到dirsWithInsert數(shù)組中麻献。
dirsWithInsert.push(dir)
}
} else {
// existing directive, update => 如果不是第一次綁定,則調(diào)用update鉤子函數(shù)
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
callHook(dir, 'update', vnode, oldVnode)
if (dir.def && dir.def.componentUpdated) {
//若同時定義了componentUpdated鉤子猜扮,則會先把它添加到dirsWithPostpatch數(shù)組中勉吻。
dirsWithPostpatch.push(dir)
}
}
}
if (dirsWithInsert.length) {
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
if (isCreate) {
//如果是vnode是第一次創(chuàng)建,
//則會把dirsWithInsert數(shù)組中的回調(diào)追加到vnode.data.hook.insert中執(zhí)行
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
// 如果不是第一次創(chuàng)建旅赢,就調(diào)用舊vnode中新vnode不存在的指令的unbind鉤子函數(shù)
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
}
}
}
}
指令使用
Vue.directive('my-directive', {
bind: function () {},
inserted: function () {},
update: function () {},
componentUpdated: function () {},
unbind: function () {}
})
上面的_update 函數(shù)對應(yīng)著這幾個鉤子函數(shù)齿桃,其中:
bind:只調(diào)用一次,指令第一次綁定到元素時調(diào)用煮盼。在這里可以進(jìn)行一次性的初始化設(shè)置短纵。
inserted:被綁定元素插入父節(jié)點(diǎn)時調(diào)用 (僅保證父節(jié)點(diǎn)存在,但不一定已被插入文檔中)僵控。
update:所在組件的 VNode 更新時調(diào)用香到,但是可能發(fā)生在其子 VNode 更新之前。指令的值可能發(fā)生了改變报破,也可能沒有养渴。但是你可以通過比較更新前后的值來忽略不必要的模板更新
componentUpdated:指令所在組件的 VNode 及其子 VNode 全部更新后調(diào)用。
unbind:只調(diào)用一次泛烙,指令與元素解綁時調(diào)用。
指令鉤子函數(shù)會被傳入以下參數(shù):
-
el
:指令所綁定的元素翘紊,可以用來直接操作 DOM 蔽氨。 -
binding
:一個對象,包含以下屬性: -
vnode
:Vue 編譯生成的虛擬節(jié)點(diǎn)帆疟。移步 VNode API 來了解更多詳情鹉究。 -
oldVnode
:上一個虛擬節(jié)點(diǎn),僅在update
和componentUpdated
鉤子中可用踪宠。
看完源代碼自赔,在回過頭來看官網(wǎng)注釋,會發(fā)現(xiàn)自己對官網(wǎng)的說明印象更加深刻柳琢,也更加深自己的理解绍妨。
答案
關(guān)于開頭提到的問題的答案:其實(shí)還是和v-if有關(guān)润脸,v-if在編譯過程中會被轉(zhuǎn)化成三元表達(dá)式,條件不滿足的時候他去,是不會渲染此節(jié)點(diǎn)的毙驯,自然值也沒拿到了。
謝謝閱讀灾测,有問題可以互相交流哦