Vue.js 源码分析(二十四) 高级应用 自定义指令详解
除了核心功能默认内置的指令 (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钩子函数了。
推荐阅读
-
Vue.js 源码分析(二十二) 指令篇 v-model指令详解
-
Vue.js 源码分析(二十五) 高级应用 插槽 详解
-
Vue.js 源码分析(十六) 指令篇 v-on指令详解
-
Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解
-
Vue.js 源码分析(二十四) 高级应用 自定义指令详解
-
Vue.js 源码分析(十五) 指令篇 v-bind指令详解
-
Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解
-
Vue.js 源码分析(十八) 指令篇 v-for 指令详解
-
Vue.js 源码分析(十七) 指令篇 v-if、v-else-if和v-else 指令详解
-
Vue.js 源码分析(二十六) 高级应用 作用域插槽 详解