除了核心功能默認內置的指令 (v-model
和 v-show
),Vue 也容許註冊自定義指令。html
官網介紹的比較抽象,顯得很高大上,我我的對自定義指令的理解是:當自定義指令做用在一些DOM元素或組件上時,該元素在初次渲染、插入到父節點、更新、解綁時能夠執行一些特定的操做(鉤子函數()vue
自定義指令有兩種註冊方式,一種是全局註冊,使用Vue.directive(指令名,配置參數)註冊,註冊以後全部的Vue實例均可以使用,另外一種是局部註冊,在建立Vue實例時經過directives屬性建立局部指令,局部自定義指令只能在當前Vue實例內使用node
自定義指令能夠綁定以下鉤子函數:數組
·bind ;只調用一次,元素渲染成DOM節點後,執行directives模塊的初始化工做時調用,在這裏能夠進行一次性的初始化設置。
·inserted ;被綁定元素插入父節點時調用 (僅保證父節點存在,但不必定已被插入文檔中)。
·update ;所在組件的 VNode 更新時調用,可是可能發生在其子 VNode 更新以前。指令的值可能發生了改變,也可能沒有。
·componentUpdated ;指令所在組件的 VNode 及其子 VNode 所有更新後調用。
·unbind ;只調用一次,指令與元素解綁時調用。閉包
每一個鉤子函數能夠有四個參數,分別是el(對應的DOM節點引用)、binding(一些關於指令的擴展信息,是個對象)、vnode(該節點對應的虛擬VN哦的)和oldVnode(以前的VNode,僅在update和componentUpdated鉤子中可用)app
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元素自動得到焦點了,控制檯輸出以下:post
能夠看到對於bind()鉤子來講,它的父節點是獲取不到的,由於Vue內部會在執行bind()鉤子後纔會將當前元素插入到父元素的子節點裏this
源碼分析
在解析模板將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鉤子函數了。