Vue2源碼解析之自定義指令
基本使用
自定義指令,其實(shí)就是在vue提供的鉤子中寫代碼,而這個(gè)鉤子的執(zhí)行是在dom渲染的不同階段執(zhí)行不同的鉤子;
自定義指令的兩種方式
// 全局 // 注冊一個(gè)全局自定義指令 `v-focus` Vue.directive('focus', { // 當(dāng)被綁定的元素插入到 DOM 中時(shí)…… inserted: function (el) { // 聚焦元素 el.focus() } }) // 組件內(nèi) directives: { focus: { // 指令的定義 inserted: function (el) { el.focus() } } } <input v-focus>
自定義指令中可以使用的鉤子函數(shù):
一個(gè)指令定義對象可以提供如下幾個(gè)鉤子函數(shù) (均為可選):
`bind`:只調(diào)用一次,指令第一次綁定到元素時(shí)調(diào)用。在這里可以進(jìn)行一次性的初始化設(shè)置。
`inserted`:被綁定元素插入父節(jié)點(diǎn)時(shí)調(diào)用 (僅保證父節(jié)點(diǎn)存在,但不一定已被插入文檔中)。
`update`:所在組件的 VNode 更新時(shí)調(diào)用,**但是可能發(fā)生在其子 VNode 更新之前**。
指令的值可能發(fā)生了改變,也可能沒有。但是你可以通過比較更新前后的值來忽略不必要的模板
更新 (詳細(xì)的鉤子函數(shù)參數(shù)見下)。
`componentUpdated`:指令所在組件的 VNode **及其子 VNode** 全部更新后調(diào)用。
`unbind`:只調(diào)用一次,指令與元素解綁時(shí)調(diào)用。
鉤子函數(shù)的參數(shù):
`el`:指令所綁定的元素,可以用來直接操作 DOM。
`binding`:一個(gè)對象,包含以下 property:
`name`:指令名,不包括 `v-` 前綴
`value`:指令的綁定值,例如:`v-my-directive="1 + 1"` 中,綁定值為 `2`.
`oldValue`:指令綁定的前一個(gè)值,僅在 `update` 和 `componentUpdated` 鉤子中可用。
無論值是否改變都可用。
`expression`:字符串形式的指令表達(dá)式。例如 `v-my-directive="1 + 1"` 中,
表達(dá)式為 `"1 + 1"`。
`arg`:傳給指令的參數(shù),可選。例如 `v-my-directive:foo` 中,參數(shù)為 `"foo"`。
`modifiers`:一個(gè)包含修飾符的對象。例如:`v-my-directive.foo.bar` 中,修飾符對象為 `{ foo: true, bar: true }`。
`vnode`:Vue 編譯生成的虛擬節(jié)點(diǎn)。
`oldVnode`:上一個(gè)虛擬節(jié)點(diǎn),僅在 `update` 和 `componentUpdated` 鉤子中可用。
eg:
<div id="hook-arguments-example" v-demo:foo.a.b="message"></div> Vue.directive('demo', { bind: function (el, binding, vnode) { var s = JSON.stringify el.innerHTML = 'name: ' + s(binding.name) + '<br>' + 'value: ' + s(binding.value) + '<br>' + 'expression: ' + s(binding.expression) + '<br>' + 'argument: ' + s(binding.arg) + '<br>' + 'modifiers: ' + s(binding.modifiers) + '<br>' + 'vnode keys: ' + Object.keys(vnode).join(', ') } }) new Vue({ el: '#hook-arguments-example', data: { message: 'hello!' } })
動(dòng)態(tài)指令:
指令的參數(shù)可以是動(dòng)態(tài)的。例如,在 v-mydirective:[argument]="value"
中,argument
參數(shù)可以根據(jù)組件實(shí)例數(shù)據(jù)進(jìn)行更新!這使得自定義指令可以在應(yīng)用中被靈活使用;
源碼解析
模板解析階段
編譯模板解析每個(gè)標(biāo)簽,會(huì)把標(biāo)簽上的屬性解析到一個(gè)對象的attrs屬性上,解析到閉合標(biāo)簽之后,會(huì)調(diào)用閉合標(biāo)簽的回調(diào)方法,方法中會(huì)針對屬性進(jìn)行處理,先處理v-bind,v-on,再處理自定義指令,會(huì)把自定義指令處理之后放在抽象語法樹的directives屬性上;
抽象語法樹生成render函數(shù)階段
解析抽象語法樹生成render函數(shù),directives指令會(huì)被作為render函數(shù)的屬性參數(shù);
render函數(shù)生成虛擬dom階段
執(zhí)行render函數(shù),把自定義指令屬性掛載到虛擬節(jié)點(diǎn)的data下的directives上
生成真實(shí)dom階段
creatElem創(chuàng)建真實(shí)dom的函數(shù)內(nèi)部在創(chuàng)建完真實(shí)dom之后,會(huì)去調(diào)用invokeCreateHooks方法執(zhí)行create相關(guān)的鉤子函數(shù),create函數(shù)內(nèi)部就會(huì)執(zhí)行_update方法;下面進(jìn)行解析_update方法;
// 源碼位置: src/core/vdom/modules/directives.js 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) { // 舊節(jié)點(diǎn)不存在就表示是新創(chuàng)建的 const isCreate = oldVnode === emptyNode // 新節(jié)點(diǎn)不存在表示是需要?jiǎng)h除的 const isDestroy = vnode === emptyNode // 舊節(jié)點(diǎn)中的指令集合 const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context) // 新節(jié)點(diǎn)中的指令集合 const newDirs = normalizeDirectives(vnode.data.directives, vnode.context) // 保存需要執(zhí)行inserted鉤子的指令 const dirsWithInsert = [] // 保存需要執(zhí)行update鉤子的指令 const dirsWithPostpatch = [] let key, oldDir, dir // 遍歷新節(jié)點(diǎn)的指令 for (key in newDirs) { // 獲取到舊節(jié)點(diǎn)中對應(yīng)的指令 oldDir = oldDirs[key] dir = newDirs[key] // 如果舊節(jié)點(diǎn)中的指令不存在 表示是新添加的,通過bind進(jìn)行綁定 if (!oldDir) { // new directive, bind callHook(dir, 'bind', vnode, oldVnode) // 如有inserted就進(jìn)行添加 if (dir.def && dir.def.inserted) { dirsWithInsert.push(dir) } } else { // 如果舊節(jié)點(diǎn)中的指令存在 表示是需要更新的,執(zhí)行update // 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) } } } // 如果dirsWithInsert有值 if (dirsWithInsert.length) { // 創(chuàng)建一個(gè)函數(shù),函數(shù)內(nèi)部遍歷dirsWithInsert并且執(zhí)行inserted鉤子 const callInsert = () => { for (let i = 0; i < dirsWithInsert.length; i++) { callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode) } } // 如果是新建的,就和insert鉤子進(jìn)行合并執(zhí)行 if (isCreate) { mergeVNodeHook(vnode, 'insert', callInsert) } else { // 否則執(zhí)行指令中的inserted鉤子 callInsert() } } // dirsWithPostpatch有值 就合并componentUpdated和postpatch鉤子在dom更新的時(shí)候執(zhí)行 if (dirsWithPostpatch.length) { mergeVNodeHook(vnode, 'postpatch', () => { for (let i = 0; i < dirsWithPostpatch.length; i++) { callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode) } }) } // 如果不是新建的 if (!isCreate) { // 遍歷舊節(jié)點(diǎn)中的指令 for (key in oldDirs) { // 如果新節(jié)點(diǎn)中不存在此指令,表示是要解綁的,調(diào)用unbind鉤子 if (!newDirs[key]) { // no longer present, unbind callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy) } } } }
指令的創(chuàng)建,更新和銷毀都是同一個(gè)函數(shù)updateDirectives,而updateDirectives函數(shù)內(nèi)部,判斷就節(jié)點(diǎn)上的指令或新節(jié)點(diǎn)上的指令存在就執(zhí)行_update方法;
export default { create: updateDirectives, update: updateDirectives, destroy: function unbindDirectives (vnode: VNodeWithData) { updateDirectives(vnode, emptyNode) } } function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) { // 舊節(jié)點(diǎn)的指令或新節(jié)點(diǎn)的指令存在 if (oldVnode.data.directives || vnode.data.directives) { _update(oldVnode, vnode) } }
_update函數(shù)內(nèi)部首先聲明了6個(gè)變量;
// 舊節(jié)點(diǎn)不存在就表示是新創(chuàng)建的 const isCreate = oldVnode === emptyNode // 新節(jié)點(diǎn)不存在表示是需要?jiǎng)h除的 const isDestroy = vnode === emptyNode // 舊節(jié)點(diǎn)中的指令集合 const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context) // 新節(jié)點(diǎn)中的指令集合 const newDirs = normalizeDirectives(vnode.data.directives, vnode.context) // 保存需要執(zhí)行inserted鉤子的指令 const dirsWithInsert = [] // 保存需要執(zhí)行update鉤子的指令 const dirsWithPostpatch = []
接著遍歷新節(jié)點(diǎn)中的指令,并且保存新舊節(jié)點(diǎn)中對應(yīng)的指令的值;
let key, oldDir, dir // 遍歷新節(jié)點(diǎn)的指令 for (key in newDirs) { // 獲取到舊節(jié)點(diǎn)中對應(yīng)的指令 oldDir = oldDirs[key] dir = newDirs[key]
如果舊節(jié)點(diǎn)中的此指令不存在,表示是新添加的,執(zhí)行bind鉤子進(jìn)行綁定,并且判斷是否傳遞了inserted鉤子,如果傳遞了就把當(dāng)前指令存到dirsWithInsert中;
// 如果舊節(jié)點(diǎn)中的指令不存在 表示是新添加的,通過bind進(jìn)行綁定 if (!oldDir) { // new directive, bind callHook(dir, 'bind', vnode, oldVnode) // 如有inserted就進(jìn)行添加 if (dir.def && dir.def.inserted) { dirsWithInsert.push(dir) } }
如果舊節(jié)點(diǎn)中的此指令存在,表示是需要更新,把舊指令的參數(shù)和值保存到新指令對象上,并且執(zhí)行update鉤子,判斷是否傳遞了componentUpdated鉤子,傳遞了就保存到dirsWithPostpatch中;
else { // 如果舊節(jié)點(diǎn)中的指令存在 表示是需要更新的,執(zhí)行update // 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) } }
如果dirsWithInsert有值,就創(chuàng)建一個(gè)函數(shù),函數(shù)內(nèi)部遍歷這個(gè)dirsWithInsert,通過callback調(diào)用每一個(gè)指令的inserted鉤子;判斷當(dāng)前節(jié)點(diǎn)是否是新建的,是新創(chuàng)建的就和insert鉤子進(jìn)行合并執(zhí)行,如果不是新創(chuàng)建的直接執(zhí)行inserted鉤子;(為什么需要合并鉤子?是因?yàn)閕nserted鉤子的執(zhí)行時(shí)機(jī)是dom已經(jīng)插入到頁面中,而新創(chuàng)建的節(jié)點(diǎn)被插入到頁面中就會(huì)執(zhí)行insert鉤子,因此把它們進(jìn)行合并,再插入的時(shí)候再去執(zhí)行)
// 如果dirsWithInsert有值 if (dirsWithInsert.length) { // 創(chuàng)建一個(gè)函數(shù),函數(shù)內(nèi)部遍歷dirsWithInsert并且執(zhí)行inserted鉤子 const callInsert = () => { for (let i = 0; i < dirsWithInsert.length; i++) { callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode) } } // 如果是新建的,就和insert鉤子進(jìn)行合并執(zhí)行 if (isCreate) { mergeVNodeHook(vnode, 'insert', callInsert) } else { // 否則執(zhí)行指令中的inserted鉤子 callInsert() } }
如果dirsWithPostpatch有值,就進(jìn)行遍歷執(zhí)行componentUpdated鉤子;
// dirsWithPostpatch有值 就合并componentUpdated和postpatch鉤子在dom更新的時(shí)候執(zhí)行 if (dirsWithPostpatch.length) { mergeVNodeHook(vnode, 'postpatch', () => { for (let i = 0; i < dirsWithPostpatch.length; i++) { callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode) } }) }
如果不是新創(chuàng)建的,就遍歷舊節(jié)點(diǎn)的指令,判斷新節(jié)點(diǎn)上是否有此指令,沒有就表示需要解綁,那么就執(zhí)行unbind鉤子;
// 如果不是新建的 if (!isCreate) { // 遍歷舊節(jié)點(diǎn)中的指令 for (key in oldDirs) { // 如果新節(jié)點(diǎn)中不存在此指令,表示是要解綁的,調(diào)用unbind鉤子 if (!newDirs[key]) { // no longer present, unbind callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy) } } }
normalizeDirectives函數(shù),把指令的值進(jìn)行格式化處理;
const emptyModifiers = Object.create(null) /** * 獲取到鉤子的值,并且進(jìn)行統(tǒng)一的格式化操作 * @param {*} dirs * @param {*} vm * @returns 格式化之后的結(jié)果 { * v-focus: { * name: 'focus', // 指令名稱 * value: '', // 指令值 * arg: '', // 參數(shù) * modifiers: {}, // 修飾符 * def: { inserted: fn } // 回調(diào) * } * } */ function normalizeDirectives ( dirs: ?Array<VNodeDirective>, vm: Component ): { [key: string]: VNodeDirective } { // 創(chuàng)建一個(gè)對象 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] // 如果修飾符屬性不存在就創(chuàng)建一個(gè)空的對象 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 } function getRawDirName (dir: VNodeDirective): string { return dir.rawName || `${dir.name}.${Object.keys(dir.modifiers || {}).join('.')}` }
callHook函數(shù),內(nèi)部直接執(zhí)行對應(yīng)的hook函數(shù);
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`) } } }
小結(jié):
- _update函數(shù)內(nèi)部,主要通過新舊的判斷是新建還是刪除節(jié)點(diǎn),并且通過一個(gè)方法把指令進(jìn)行格式化處理得到新舊節(jié)點(diǎn)上的新舊指令;
- 遍歷新指令集合如果舊指令集合中不存在此指令,表示新建,就執(zhí)行bind鉤子,并且把指令存儲(chǔ)到插入指令的數(shù)組中;如果舊指令集合中存在,表示更新,執(zhí)行update鉤子,并且把此指令存入到更新的數(shù)組中;
- 接著判斷插入指令數(shù)組如果有值,如果當(dāng)前節(jié)點(diǎn)是新建的,那么就合并執(zhí)行insert和inserted鉤子;否則不是新建,直接執(zhí)行inserted鉤子
- 判斷更新的數(shù)組有值,合并執(zhí)行postpatch和componentUpdated鉤子
- 最后判斷如果不是新建的節(jié)點(diǎn),并且遍歷舊指令集合,如果在新指令中沒有找到此指令,表示是刪除的,執(zhí)行unbind鉤子;
- 以上就完成了自定義指令中的各個(gè)鉤子的執(zhí)行,并且_update函數(shù)的執(zhí)行是在dom插入到文檔中之前執(zhí)行的,并且在子組件創(chuàng)建完成之后;
總結(jié)
自定義指令是在模板編譯節(jié)點(diǎn)進(jìn)行解析收集自定義指令屬性的,自定義指令在此階段都是作為屬性被添加到ast的directives上;在render函數(shù)生成虛擬節(jié)點(diǎn)的時(shí)候被作為虛擬節(jié)點(diǎn)的data下的directives屬性;在創(chuàng)建真實(shí)dom之后,就會(huì)執(zhí)行自定義指令的各個(gè)鉤子方法,通過新舊節(jié)點(diǎn)來判斷是新建的還是刪除的還是更新的(舊虛擬dom不存在表示新建,新虛擬dom不存在表示刪除),如果是新建的并且新的指令集合中存在,舊的指令集合中不存在的指令就執(zhí)行bind方法,表示新建并且把當(dāng)前指令添加到插入數(shù)組中;如果舊指令集合中存在此指令,表示更新,執(zhí)行update方法并且插入到更新數(shù)組中;如果插入數(shù)組有值,并且是新增的節(jié)點(diǎn)就合并執(zhí)行insert和inserted方法;如果不是新增就執(zhí)行inserted方法;如果更新數(shù)組有值,直接合并執(zhí)行postpatch和componentUpdated方法;最后遍歷舊指令集合如果舊的存在新的不存在那么表示刪除,直接調(diào)用unbind方法;(自定義指令其實(shí)就是在對應(yīng)的dom創(chuàng)建之后還未插入到文檔中之前,通過新舊虛擬節(jié)點(diǎn)和新舊指令判斷是新建的還是更新的還是刪除的,從而執(zhí)行不同的鉤子函數(shù))
到此這篇關(guān)于Vue2源碼解析之自定義指令的文章就介紹到這了,更多相關(guān)Vue2自定義指令內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue iview實(shí)現(xiàn)動(dòng)態(tài)新增和刪除
這篇文章主要為大家詳細(xì)介紹了vue iview實(shí)現(xiàn)動(dòng)態(tài)新增和刪除,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-06-06webpack vue項(xiàng)目開發(fā)環(huán)境局域網(wǎng)訪問方法
下面小編就為大家分享一篇webpack vue項(xiàng)目開發(fā)環(huán)境局域網(wǎng)訪問方法,具有很好的參考價(jià)值,希望對大家有所幫助,一起跟隨小編過來看看吧2018-03-03Vue使用vue-draggable 插件在不同列表之間拖拽功能
這篇文章主要介紹了使用vue-draggable 插件在不同列表之間拖拽,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-03-03Vue3播放m3u8視頻的兩種實(shí)現(xiàn)方式示例
這篇文章主要介紹了Vue3播放m3u8視頻的兩種實(shí)現(xiàn)方式,兩種視頻播放器組件分別是vue3-video-play和chimee-player,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-01-01vue之?dāng)?shù)據(jù)交互實(shí)例代碼
本篇文章主要介紹了vue之?dāng)?shù)據(jù)交互實(shí)例代碼,vue中也存在像ajax和jsonp的數(shù)據(jù)交互,實(shí)現(xiàn)向服務(wù)器獲取數(shù)據(jù),有興趣的可以了解一下2017-06-06