Vue.js 源码分析(二十三) 高级应用 自定义指令详解
Posted greatdesert
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 源码分析(二十三) 高级应用 自定义指令详解的主要内容,如果未能解决你的问题,请参考以下文章
Vue.js 源码分析(二十九) 高级应用 transition-group组件 详解
Spring源码分析(二十三)BeanFactory的后处理
头秃系列,二十三张图带你从源码分析Spring Boot 启动流程~