除了核心功能預設內置的指令 (v-model 和 v-show),Vue 也允許註冊自定義指令。 官網介紹的比較抽象,顯得很高大上,我個人對自定義指令的理解是:當自定義指令作用在一些DOM元素或組件上時,該元素在初次渲染、插入到父節點、更新、解綁時可以執行一些特定的操作(鉤子函數() 自定義指令有兩 ...
除了核心功能預設內置的指令 (v-model
和 v-show
),Vue 也允許註冊自定義指令。
官網介紹的比較抽象,顯得很高大上,我個人對自定義指令的理解是:當自定義指令作用在一些DOM元素或組件上時,該元素在初次渲染、插入到父節點、更新、解綁時可以執行一些特定的操作(鉤子函數()
自定義指令有兩種註冊方式,一種是全局註冊,使用Vue.directive(指令名,配置參數)註冊,註冊之後所有的Vue實例都可以使用,另一種是局部註冊,在創建Vue實例時通過directives屬性創建局部指令,局部自定義指令只能在當前Vue實例內使用
自定義指令可以綁定如下鉤子函數:
·bind ;只調用一次,元素渲染成DOM節點後,執行directives模塊的初始化工作時調用,在這裡可以進行一次性的初始化設置。
·inserted ;被綁定元素插入父節點時調用 (僅保證父節點存在,但不一定已被插入文檔中)。
·update ;所在組件的 VNode 更新時調用,但是可能發生在其子 VNode 更新之前。指令的值可能發生了改變,也可能沒有。
·componentUpdated ;指令所在組件的 VNode 及其子 VNode 全部更新後調用。
·unbind ;只調用一次,指令與元素解綁時調用。
每個鉤子函數可以有四個參數,分別是el(對應的DOM節點引用)、binding(一些關於指令的擴展信息,是個對象)、vnode(該節點對應的虛擬VN哦的)和oldVnode(之前的VNode,僅在update和componentUpdated鉤子中可用)
bind鉤子函數執行的時候該DOM元素被渲染出來了,但是並沒有插入到父元素中,例如:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <script src="vue.js"></script> </head> <body> <div id="d"><input type="" name="" v-focus></div> <script> Vue.directive('focus', { bind:function(el){console.log(el.parentElement);}, //列印父節點 inserted: function (el) {console.log(el.parentElement);el.focus()} //列印父節點,並將當前元素處於聚焦狀態 }) var app = new Vue({el:"#d"}) </script> </body> </html>
輸出如下:
可以看到input元素自動獲得焦點了,控制台輸出如下:
可以看到對於bind()鉤子來說,它的父節點是獲取不到的,因為Vue內部會在執行bind()鉤子後才會將當前元素插入到父元素的子節點里
源碼分析
在解析模板將DOM轉換成AST對象的時候會執行processAttrs()函數,如下:
function processAttrs (el) { //解析Vue的屬性 var list = el.attrsList; var i, l, name, rawName, value, modifiers, isProp; for (i = 0, l = list.length; i < l; i++) { //遍歷每個屬性 name = rawName = list[i].name; value = list[i].value; if (dirRE.test(name)) { //如果該屬性以v-、@或:開頭,表示這是Vue內部指令 // mark element as dynamic el.hasBindings = true; // modifiers modifiers = parseModifiers(name); if (modifiers) { name = name.replace(modifierRE, ''); } if (bindRE.test(name)) { // v-bind //bindRD等於/^:|^v-bind:/ ,即該屬性是v-bind指令時 /*v-bind的分支*/ } else if (onRE.test(name)) { // v-on /*v-on的分支*/ } else { // normal directives name = name.replace(dirRE, ''); //去掉指令首碼,比如v-show執行後等於show // parse arg var argMatch = name.match(argRE); var arg = argMatch && argMatch[1]; if (arg) { name = name.slice(0, -(arg.length + 1)); } addDirective(el, name, rawName, value, arg, modifiers); //執行addDirective給el增加一個directives屬性 if ("development" !== 'production' && name === 'model') { checkForAliasModel(el, value); } } } else { /*非Vue指令的分支*/ } } }
addDirective會給AST對象上增加一個directives屬性保存指令信息,如下:
function addDirective ( //第6561行 指令相關,給el這個AST對象增加一個directives屬性,值為該指令的信息 el, name, rawName, value, arg, modifiers ) { (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers }); el.plain = false; }
例子里的p元素執行到這裡時對應的AST對象如下:
接下來在generate生成rendre函數的時候,會執行genDirectives()函數,將AST轉換成一個render函數,如下:
with(this){return _c('div',{attrs:{"id":"d"}},[_c('input',{directives:[{name:"focus",rawName:"v-focus"}],attrs:{"type":"","name":""}})])}
最後等渲染完成後會執行directives模塊的create鉤子函數,如下:
var directives = { //第6173行 directives模塊 create: updateDirectives, //創建DOM後的鉤子 update: updateDirectives, destroy: function unbindDirectives (vnode) { updateDirectives(vnode, emptyNode); } } function updateDirectives (oldVnode, vnode) { //第6181行 oldVnode:舊的Vnode,更新時才有 vnode:新的VNode if (oldVnode.data.directives || vnode.data.directives) { _update(oldVnode, vnode); } }
_updat 就是處理指令初始化和更新的,如下:
function _update (oldVnode, vnode) { //第6187行 初始化/更新指令 var isCreate = oldVnode === emptyNode; //是否為初始化 var isDestroy = vnode === emptyNode; var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context); var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context); //調用normalizeDirectives$1()函數規範化參數 var dirsWithInsert = []; var dirsWithPostpatch = []; var key, oldDir, dir; for (key in newDirs) { //遍歷newDirs oldDir = oldDirs[key]; //oldVnode上的key指令信息 dir = newDirs[key]; //vnode上的key指令信息 if (!oldDir) { //如果oldDir不存在,即是新增指令 // new directive, bind callHook$1(dir, 'bind', vnode, oldVnode); //調用callHook$1()函數,參數2為bind,即執行v-focus指令的bind函數 if (dir.def && dir.def.inserted) { //如果有定義了inserted鉤子函數 dirsWithInsert.push(dir); //則保存到dirsWithInsert數組裡 } } else { // existing directive, update dir.oldValue = oldDir.value; callHook$1(dir, 'update', vnode, oldVnode); if (dir.def && dir.def.componentUpdated) { dirsWithPostpatch.push(dir); } } } if (dirsWithInsert.length) { //如果dirsWithInsert存在(即有綁定了inserted鉤子函數) var callInsert = function () { //定義一個callInsert函數,該函數會執行dirsWithInsert里的每個函數 for (var i = 0; i < dirsWithInsert.length; i++) { callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode); } }; if (isCreate) { //如果是初始化 mergeVNodeHook(vnode, 'insert', callInsert); //則調用mergeVNodeHook()函數 } else { callInsert(); } } if (dirsWithPostpatch.length) { mergeVNodeHook(vnode, 'postpatch', function () { for (var i = 0; i < dirsWithPostpatch.length; i++) { callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode); } }); } if (!isCreate) { for (key in oldDirs) { if (!newDirs[key]) { // no longer present, unbind callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy); } } } }
對於bind鉤子函數來說是直接執行了,而對於inserted鉤子函數則是把函數保存到dirsWithInsert數組裡,再定義了一個callInsert函數,該函數內部通過作用域訪問dirsWithInsert變數,並遍歷該變數依次執行每個inserted鉤子函數
mergeVNodeHook()鉤子函數的作用是把insert作為一個hooks屬性保存到對應的Vnode的data上面,當該Vnode插入到父節點後會調用該hooks,如下:
function mergeVNodeHook (def, hookKey, hook) { //第2074行 合併VNode的鉤子函數 def:一個VNode hookKey:(事件名,比如:insert) hook:回調函數 if (def instanceof VNode) { //如果def是一個VNode def = def.data.hook || (def.data.hook = {}); //則將它重置為VNode.data.hook,如果VNode.data.hook不存在則初始化為一個空對象 註:普通節點VNode.data.hook是不存在的。 } var invoker; var oldHook = def[hookKey]; function wrappedHook () { hook.apply(this, arguments); //先執行hook函數 // important: remove merged hook to ensure it's called only once // and prevent memory leak remove(invoker.fns, wrappedHook); //然後把wrappedHook從invoker.fns里remove掉,以且包只執行一次 } if (isUndef(oldHook)) { //如果oldHook不存在,即之前沒有定義hookKey這個鉤子函數 // no existing hook invoker = createFnInvoker([wrappedHook]); //直接調用createFnInvoker()返回一個閉包函數,參數為執行的回調函數 } 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; //設置def的hookKey屬性指向新的invoker }
createFnInvoker就是v-on指令對應的那個函數,用到了同一個API,執行完後,我們就把invoker插入到input對應的VNode.data.hook里了,如下:
最後等到該VNode插入到父節點後就會執行invokeCreateHooks()函數,該函數會遍歷VNode.hook.insert,依次執行每個函數,也就執行到我們自定義定義的inserted鉤子函數了。