Vue.js源碼分析之自定義指令詳解
前言
除了核心功能默認內(nèi)置的指令 (v-model 和 v-show),Vue 也允許注冊自定義指令。
官網(wǎng)介紹的比較抽象,顯得很高大上,我個人對自定義指令的理解是:當自定義指令作用在一些DOM元素或組件上時,該元素在初次渲染、插入到父節(jié)點、更新、解綁時可以執(zhí)行一些特定的操作(鉤子函數(shù)()
自定義指令有兩種注冊方式,一種是全局注冊,使用Vue.directive(指令名,配置參數(shù))注冊,注冊之后所有的Vue實例都可以使用,另一種是局部注冊,在創(chuàng)建Vue實例時通過directives屬性創(chuàng)建局部指令,局部自定義指令只能在當前Vue實例內(nèi)使用
自定義指令可以綁定如下鉤子函數(shù):
·bind ;只調(diào)用一次,元素渲染成DOM節(jié)點后,執(zhí)行directives模塊的初始化工作時調(diào)用,在這里可以進行一次性的初始化設置。
·inserted ;被綁定元素插入父節(jié)點時調(diào)用 (僅保證父節(jié)點存在,但不一定已被插入文檔中)。
·update ;所在組件的 VNode 更新時調(diào)用,但是可能發(fā)生在其子 VNode 更新之前。指令的值可能發(fā)生了改變,也可能沒有。
·componentUpdated ;指令所在組件的 VNode 及其子 VNode 全部更新后調(diào)用。
·unbind ;只調(diào)用一次,指令與元素解綁時調(diào)用。
每個鉤子函數(shù)可以有四個參數(shù),分別是el(對應的DOM節(jié)點引用)、binding(一些關于指令的擴展信息,是個對象)、vnode(該節(jié)點對應的虛擬VN哦的)和oldVnode(之前的VNode,僅在update和componentUpdated鉤子中可用)
bind鉤子函數(shù)執(zhí)行的時候該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);}, //打印父節(jié)點 inserted: function (el) {console.log(el.parentElement);el.focus()} //打印父節(jié)點,并將當前元素處于聚焦狀態(tài) }) var app = new Vue({el:"#d"}) </script> </body> </html>
輸出如下:
可以看到input元素自動獲得焦點了,控制臺輸出如下:
可以看到對于bind()鉤子來說,它的父節(jié)點是獲取不到的,因為Vue內(nèi)部會在執(zhí)行bind()鉤子后才會將當前元素插入到父元素的子節(jié)點里
源碼分析
在解析模板將DOM轉(zhuǎn)換成AST對象的時候會執(zhí)行processAttrs()函數(shù),如下:
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內(nèi)部指令 // 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執(zhí)行后等于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); //執(zhí)行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元素執(zhí)行到這里時對應的AST對象如下:
接下來在generate生成rendre函數(shù)的時候,會執(zhí)行genDirectives()函數(shù),將AST轉(zhuǎn)換成一個render函數(shù),如下:
with(this){return _c('div',{attrs:{"id":"d"}},[_c('input',{directives:[{name:"focus",rawName:"v-focus"}],attrs:{"type":"","name":""}})])}
最后等渲染完成后會執(zhí)行directives模塊的create鉤子函數(shù),如下:
var directives = { //第6173行 directives模塊 create: updateDirectives, //創(chuàng)建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); //調(diào)用normalizeDirectives$1()函數(shù)規(guī)范化參數(shù) 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); //調(diào)用callHook$1()函數(shù),參數(shù)2為bind,即執(zhí)行v-focus指令的bind函數(shù) if (dir.def && dir.def.inserted) { //如果有定義了inserted鉤子函數(shù) dirsWithInsert.push(dir); //則保存到dirsWithInsert數(shù)組里 } } 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鉤子函數(shù)) var callInsert = function () { //定義一個callInsert函數(shù),該函數(shù)會執(zhí)行dirsWithInsert里的每個函數(shù) for (var i = 0; i < dirsWithInsert.length; i++) { callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode); } }; if (isCreate) { //如果是初始化 mergeVNodeHook(vnode, 'insert', callInsert); //則調(diào)用mergeVNodeHook()函數(shù) } 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); } } } }
writer by:大沙漠 QQ:22969969
對于bind鉤子函數(shù)來說是直接執(zhí)行了,而對于inserted鉤子函數(shù)則是把函數(shù)保存到dirsWithInsert數(shù)組里,再定義了一個callInsert函數(shù),該函數(shù)內(nèi)部通過作用域訪問dirsWithInsert變量,并遍歷該變量依次執(zhí)行每個inserted鉤子函數(shù)
mergeVNodeHook()鉤子函數(shù)的作用是把insert作為一個hooks屬性保存到對應的Vnode的data上面,當該Vnode插入到父節(jié)點后會調(diào)用該hooks,如下:
function mergeVNodeHook (def, hookKey, hook) { //第2074行 合并VNode的鉤子函數(shù) def:一個VNode hookKey:(事件名,比如:insert) hook:回調(diào)函數(shù) if (def instanceof VNode) { //如果def是一個VNode def = def.data.hook || (def.data.hook = {}); //則將它重置為VNode.data.hook,如果VNode.data.hook不存在則初始化為一個空對象 注:普通節(jié)點VNode.data.hook是不存在的。 } var invoker; var oldHook = def[hookKey]; function wrappedHook () { hook.apply(this, arguments); //先執(zhí)行hook函數(shù) // important: remove merged hook to ensure it's called only once // and prevent memory leak remove(invoker.fns, wrappedHook); //然后把wrappedHook從invoker.fns里remove掉,以且包只執(zhí)行一次 } if (isUndef(oldHook)) { //如果oldHook不存在,即之前沒有定義hookKey這個鉤子函數(shù) // no existing hook invoker = createFnInvoker([wrappedHook]); //直接調(diào)用createFnInvoker()返回一個閉包函數(shù),參數(shù)為執(zhí)行的回調(diào)函數(shù) } 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指令對應的那個函數(shù),用到了同一個API,執(zhí)行完后,我們就把invoker插入到input對應的VNode.data.hook里了,如下:
最后等到該VNode插入到父節(jié)點后就會執(zhí)行invokeCreateHooks()函數(shù),該函數(shù)會遍歷VNode.hook.insert,依次執(zhí)行每個函數(shù),也就執(zhí)行到我們自定義定義的inserted鉤子函數(shù)了。
總結(jié)
到此這篇關于Vue.js源碼分析之自定義指令的文章就介紹到這了,更多相關Vue.js自定義指令內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
vue2.0 與 bootstrap datetimepicker的結(jié)合使用實例
本篇文章主要介紹了vue2.0 與 bootstrap datetimepicker的結(jié)合使用實例,非常具有實用價值,需要的朋友可以參考下2017-05-05vueJs實現(xiàn)DOM加載完之后自動下拉到底部的實例代碼
這篇文章主要介紹了vueJs實現(xiàn)DOM加載完成之后自動下拉到底部的實例代碼,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-08-08vue3+element 分片上傳與分片下載功能實現(xiàn)方法詳解
這篇文章主要介紹了vue3+element 分片上傳與分片下載功能實現(xiàn)方法,結(jié)合實例形式詳細分析了vue3+element 分片上傳與下載相關實現(xiàn)技巧與操作注意事項,需要的朋友可以參考下2023-06-06Vue3+ElementUI 多選框中復選框和名字點擊方法效果分離方法
這篇文章主要介紹了Vue3+ElementUI 多選框中復選框和名字點擊方法效果分離方法,文中補充介紹了VUE-Element組件 CheckBox多選框使用方法,需要的朋友可以參考下2024-01-01深入解析el-col-group強大且靈活的Element表格列組件
這篇文章主要為大家介紹了el-col-group強大且靈活的Element表格列組件深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-04-04Vue表格首列相同數(shù)據(jù)合并實現(xiàn)方法
這篇文章主要介紹了Vue實現(xiàn)表格首列相同數(shù)據(jù)合并的方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04