Vue.js源碼分析之自定義指令詳解
前言
除了核心功能默認(rèn)內(nèi)置的指令 (v-model 和 v-show),Vue 也允許注冊自定義指令。
官網(wǎng)介紹的比較抽象,顯得很高大上,我個人對自定義指令的理解是:當(dāng)自定義指令作用在一些DOM元素或組件上時,該元素在初次渲染、插入到父節(jié)點、更新、解綁時可以執(zhí)行一些特定的操作(鉤子函數(shù)()
自定義指令有兩種注冊方式,一種是全局注冊,使用Vue.directive(指令名,配置參數(shù))注冊,注冊之后所有的Vue實例都可以使用,另一種是局部注冊,在創(chuàng)建Vue實例時通過directives屬性創(chuàng)建局部指令,局部自定義指令只能在當(dāng)前Vue實例內(nèi)使用
自定義指令可以綁定如下鉤子函數(shù):
·bind ;只調(diào)用一次,元素渲染成DOM節(jié)點后,執(zhí)行directives模塊的初始化工作時調(diào)用,在這里可以進(jìn)行一次性的初始化設(shè)置。
·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(對應(yīng)的DOM節(jié)點引用)、binding(一些關(guān)于指令的擴(kuò)展信息,是個對象)、vnode(該節(jié)點對應(yīng)的虛擬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é)點,并將當(dāng)前元素處于聚焦?fàn)顟B(tài)
})
var app = new Vue({el:"#d"})
</script>
</body>
</html>
輸出如下:

可以看到input元素自動獲得焦點了,控制臺輸出如下:

可以看到對于bind()鉤子來說,它的父節(jié)點是獲取不到的,因為Vue內(nèi)部會在執(zhí)行bind()鉤子后才會將當(dāng)前元素插入到父元素的子節(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行 指令相關(guān),給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í)行到這里時對應(yīng)的AST對象如下:

接下來在generate生成rendre函數(shù)的時候,會執(zhí)行g(shù)enDirectives()函數(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屬性保存到對應(yīng)的Vnode的data上面,當(dāng)該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; //設(shè)置def的hookKey屬性指向新的invoker
}
createFnInvoker就是v-on指令對應(yīng)的那個函數(shù),用到了同一個API,執(zhí)行完后,我們就把invoker插入到input對應(yīng)的VNode.data.hook里了,如下:

最后等到該VNode插入到父節(jié)點后就會執(zhí)行invokeCreateHooks()函數(shù),該函數(shù)會遍歷VNode.hook.insert,依次執(zhí)行每個函數(shù),也就執(zhí)行到我們自定義定義的inserted鉤子函數(shù)了。
總結(jié)
到此這篇關(guān)于Vue.js源碼分析之自定義指令的文章就介紹到這了,更多相關(guān)Vue.js自定義指令內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue2.0 與 bootstrap datetimepicker的結(jié)合使用實例
本篇文章主要介紹了vue2.0 與 bootstrap datetimepicker的結(jié)合使用實例,非常具有實用價值,需要的朋友可以參考下2017-05-05
基于Vue實現(xiàn)鼠標(biāo)滾動輪控制頁面橫向滑動效果
這篇文章主要介紹了如何基于Vue實現(xiàn)鼠標(biāo)滾動輪控制頁面橫向滑動效果,文中通過代碼示例和圖文結(jié)合的方式給大家講解的非常詳細(xì),對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-09-09
vueJs實現(xiàn)DOM加載完之后自動下拉到底部的實例代碼
這篇文章主要介紹了vueJs實現(xiàn)DOM加載完成之后自動下拉到底部的實例代碼,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-08-08
vue3+element 分片上傳與分片下載功能實現(xiàn)方法詳解
這篇文章主要介紹了vue3+element 分片上傳與分片下載功能實現(xiàn)方法,結(jié)合實例形式詳細(xì)分析了vue3+element 分片上傳與下載相關(guān)實現(xiàn)技巧與操作注意事項,需要的朋友可以參考下2023-06-06
Vue3+ElementUI 多選框中復(fù)選框和名字點擊方法效果分離方法
這篇文章主要介紹了Vue3+ElementUI 多選框中復(fù)選框和名字點擊方法效果分離方法,文中補充介紹了VUE-Element組件 CheckBox多選框使用方法,需要的朋友可以參考下2024-01-01
深入解析el-col-group強大且靈活的Element表格列組件
這篇文章主要為大家介紹了el-col-group強大且靈活的Element表格列組件深入解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-04-04
Vue表格首列相同數(shù)據(jù)合并實現(xiàn)方法
這篇文章主要介紹了Vue實現(xiàn)表格首列相同數(shù)據(jù)合并的方法,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-04-04

