• 售前

  • 售后

热门帖子
入门百科

Vue.js源码分析之自定义指令详解

[复制链接]
爸证欢 显示全部楼层 发表于 2021-10-26 12:38:10 |阅读模式 打印 上一主题 下一主题
前言

除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也答应注册自界说指令。
官网先容的比力抽象,显得很高大上,我个人对自界说指令的明白是:当自界说指令作用在一些DOM元素或组件上时,该元素在初次渲染、插入到父节点、更新、解绑时可以实行一些特定的操作(钩子函数()
自界说指令有两种注册方式,一种是全局注册,使用Vue.directive(指令名,配置参数)注册,注册之后所有的Vue实例都可以使用,另一种是局部注册,在创建Vue实例时通过directives属性创建局部指令,局部自界说指令只能在当前Vue实例内使用
自界说指令可以绑定如下钩子函数:
  1. ·bind      ;只调用一次,元素渲染成DOM节点后,执行directives模块的初始化工作时调用,在这里可以进行一次性的初始化设置。
  2. ·inserted       ;被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
  3. ·update       ;所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。
  4. ·componentUpdated ;指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  5. ·unbind       ;只调用一次,指令与元素解绑时调用。
复制代码
每个钩子函数可以有四个参数,分别是el(对应的DOM节点引用)、binding(一些关于指令的扩展信息,是个对象)、vnode(该节点对应的假造VN哦的)和oldVnode(之前的VNode,仅在update和componentUpdated钩子中可用)
bind钩子函数实行的时间该DOM元素被渲染出来了,但是并没有插入到父元素中,例如:
  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4.     <meta charset="UTF-8">
  5.     <title>Document</title>
  6.     <script src="vue.js"></script>
  7. </head>
  8. <body>
  9.     <div id="d"><input type="" name="" v-focus></div>
  10.     <script>
  11.         Vue.directive('focus', {      
  12.             bind:function(el){console.log(el.parentElement);},                      //打印父节点
  13.             inserted: function (el) {console.log(el.parentElement);el.focus()}      //打印父节点,并将当前元素处于聚焦状态
  14.         })
  15.         var app = new Vue({el:"#d"})
  16.     </script>
  17. </body>
  18. </html>
复制代码
输出如下:

可以看到input元素自动得到焦点了,控制台输出如下:

可以看到对于bind()钩子来说,它的父节点是获取不到的,因为Vue内部会在实行bind()钩子后才会将当前元素插入到父元素的子节点里
源码分析

在解析模板将DOM转换成AST对象的时间会实行processAttrs()函数,如下:
  1. function processAttrs (el) {                     //解析Vue的属性
  2.   var list = el.attrsList;
  3.   var i, l, name, rawName, value, modifiers, isProp;
  4.   for (i = 0, l = list.length; i < l; i++) {             //遍历每个属性
  5.     name = rawName = list[i].name;
  6.     value = list[i].value;
  7.     if (dirRE.test(name)) {                                 //如果该属性以v-、@或:开头,表示这是Vue内部指令
  8.       // mark element as dynamic
  9.       el.hasBindings = true;
  10.       // modifiers
  11.       modifiers = parseModifiers(name);
  12.       if (modifiers) {
  13.         name = name.replace(modifierRE, '');
  14.       }
  15.       if (bindRE.test(name)) { // v-bind                          //bindRD等于/^:|^v-bind:/ ,即该属性是v-bind指令时
  16.         /*v-bind的分支*/
  17.       } else if (onRE.test(name)) { // v-on
  18.         /*v-on的分支*/
  19.       } else { // normal directives
  20.         name = name.replace(dirRE, '');                         //去掉指令前缀,比如v-show执行后等于show
  21.         // parse arg
  22.         var argMatch = name.match(argRE);
  23.         var arg = argMatch && argMatch[1];
  24.         if (arg) {
  25.           name = name.slice(0, -(arg.length + 1));
  26.         }
  27.         addDirective(el, name, rawName, value, arg, modifiers); //执行addDirective给el增加一个directives属性
  28.         if ("development" !== 'production' && name === 'model') {
  29.           checkForAliasModel(el, value);
  30.         }
  31.       }
  32.     } else {
  33.       /*非Vue指令的分支*/
  34.     }
  35.   }
  36. }
复制代码
addDirective会给AST对象上增长一个directives属性保存指令信息,如下:
  1. function addDirective (                         //第6561行 指令相关,给el这个AST对象增加一个directives属性,值为该指令的信息
  2.   el,
  3.   name,
  4.   rawName,
  5.   value,
  6.   arg,
  7.   modifiers
  8. ) {
  9.   (el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });
  10.   el.plain = false;
  11. }
复制代码
例子里的p元素实行到这里时对应的AST对象如下:

接下来在generate天生rendre函数的时间,会实行genDirectives()函数,将AST转换成一个render函数,如下:
  1. with(this){return _c('div',{attrs:{"id":"d"}},[_c('input',{directives:[{name:"focus",rawName:"v-focus"}],attrs:{"type":"","name":""}})])}
复制代码
末了等渲染完成后会实行directives模块的create钩子函数,如下:
  1. var directives = {                 //第6173行 directives模块
  2.   create: updateDirectives,             //创建DOM后的钩子
  3.   update: updateDirectives,
  4.   destroy: function unbindDirectives (vnode) {
  5.     updateDirectives(vnode, emptyNode);
  6.   }
  7. }
  8. function updateDirectives (oldVnode, vnode) {         //第6181行   oldVnode:旧的Vnode,更新时才有 vnode:新的VNode
  9.   if (oldVnode.data.directives || vnode.data.directives) {
  10.     _update(oldVnode, vnode);
  11.   }
  12. }
复制代码
_updat 就是处理指令初始化和更新的,如下:
  1. function _update (oldVnode, vnode) {                 //第6187行 初始化/更新指令
  2.   var isCreate = oldVnode === emptyNode;                                                     //是否为初始化
  3.   var isDestroy = vnode === emptyNode;
  4.   var oldDirs = normalizeDirectives$1(oldVnode.data.directives, oldVnode.context);         
  5.   var newDirs = normalizeDirectives$1(vnode.data.directives, vnode.context);                 //调用normalizeDirectives$1()函数规范化参数
  6.      
  7.   var dirsWithInsert = [];
  8.   var dirsWithPostpatch = [];
  9.   var key, oldDir, dir;
  10.   for (key in newDirs) {                                     //遍历newDirs
  11.     oldDir = oldDirs[key];                                         //oldVnode上的key指令信息
  12.     dir = newDirs[key];                                            //vnode上的key指令信息
  13.     if (!oldDir) {                                                 //如果oldDir不存在,即是新增指令
  14.       // new directive, bind
  15.       callHook$1(dir, 'bind', vnode, oldVnode);                     //调用callHook$1()函数,参数2为bind,即执行v-focus指令的bind函数
  16.       if (dir.def && dir.def.inserted) {                            //如果有定义了inserted钩子函数
  17.         dirsWithInsert.push(dir);                                     //则保存到dirsWithInsert数组里
  18.       }
  19.     } else {
  20.       // existing directive, update
  21.       dir.oldValue = oldDir.value;
  22.       callHook$1(dir, 'update', vnode, oldVnode);
  23.       if (dir.def && dir.def.componentUpdated) {
  24.         dirsWithPostpatch.push(dir);
  25.       }
  26.     }
  27.   }
  28.   if (dirsWithInsert.length) {                                    //如果dirsWithInsert存在(即有绑定了inserted钩子函数)
  29.     var callInsert = function () {                                  //定义一个callInsert函数,该函数会执行dirsWithInsert里的每个函数
  30.       for (var i = 0; i < dirsWithInsert.length; i++) {
  31.         callHook$1(dirsWithInsert[i], 'inserted', vnode, oldVnode);   
  32.       }
  33.     };
  34.     if (isCreate) {                                                 //如果是初始化  
  35.       mergeVNodeHook(vnode, 'insert', callInsert);                    //则调用mergeVNodeHook()函数
  36.     } else {
  37.       callInsert();
  38.     }
  39.   }
  40.   if (dirsWithPostpatch.length) {        
  41.     mergeVNodeHook(vnode, 'postpatch', function () {
  42.       for (var i = 0; i < dirsWithPostpatch.length; i++) {
  43.         callHook$1(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
  44.       }
  45.     });
  46.   }
  47.   if (!isCreate) {
  48.     for (key in oldDirs) {
  49.       if (!newDirs[key]) {
  50.         // no longer present, unbind
  51.         callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
  52.       }
  53.     }
  54.   }
  55. }
复制代码
writer by:大戈壁 QQ:22969969
对于bind钩子函数来说是直接实行了,而对于inserted钩子函数则是把函数保存到dirsWithInsert数组里,再界说了一个callInsert函数,该函数内部通过作用域访问dirsWithInsert变量,并遍历该变量依次实行每个inserted钩子函数
mergeVNodeHook()钩子函数的作用是把insert作为一个hooks属性保存到对应的Vnode的data上面,当该Vnode插入到父节点后会调用该hooks,如下:
  1. function mergeVNodeHook (def, hookKey, hook) {      //第2074行  合并VNode的钩子函数 def:一个VNode hookKey:(事件名,比如:insert) hook:回调函数
  2.   if (def instanceof VNode) {                           //如果def是一个VNode
  3.     def = def.data.hook || (def.data.hook = {});          //则将它重置为VNode.data.hook,如果VNode.data.hook不存在则初始化为一个空对象 注:普通节点VNode.data.hook是不存在的。
  4.   }
  5.   var invoker;
  6.   var oldHook = def[hookKey];
  7.   function wrappedHook () {     
  8.     hook.apply(this, arguments);                            //先执行hook函数        
  9.     // important: remove merged hook to ensure it's called only once
  10.     // and prevent memory leak
  11.     remove(invoker.fns, wrappedHook);                       //然后把wrappedHook从invoker.fns里remove掉,以且包只执行一次
  12.   }
  13.   if (isUndef(oldHook)) {                               //如果oldHook不存在,即之前没有定义hookKey这个钩子函数
  14.     // no existing hook
  15.     invoker = createFnInvoker([wrappedHook]);               //直接调用createFnInvoker()返回一个闭包函数,参数为执行的回调函数
  16.   } else {
  17.     /* istanbul ignore if */
  18.     if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
  19.       // already a merged invoker
  20.       invoker = oldHook;
  21.       invoker.fns.push(wrappedHook);
  22.     } else {
  23.       // existing plain hook
  24.       invoker = createFnInvoker([oldHook, wrappedHook]);
  25.     }
  26.   }
  27.   invoker.merged = true;
  28.   def[hookKey] = invoker;                               //设置def的hookKey属性指向新的invoker
  29. }
复制代码
createFnInvoker就是v-on指令对应的那个函数,用到了同一个API,实行完后,我们就把invoker插入到input对应的VNode.data.hook里了,如下:

末了比及该VNode插入到父节点后就会实行invokeCreateHooks()函数,该函数会遍历VNode.hook.insert,依次实行每个函数,也就实行到我们自界说界说的inserted钩子函数了。
总结

到此这篇关于Vue.js源码分析之自界说指令的文章就先容到这了,更多相关Vue.js自界说指令内容请搜刮脚本之家从前的文章或继续欣赏下面的相关文章盼望各人以后多多支持脚本之家!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x

帖子地址: 

回复

使用道具 举报

分享
推广
火星云矿 | 预约S19Pro,享500抵1000!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

草根技术分享(草根吧)是全球知名中文IT技术交流平台,创建于2021年,包含原创博客、精品问答、职业培训、技术社区、资源下载等产品服务,提供原创、优质、完整内容的专业IT技术开发社区。
  • 官方手机版

  • 微信公众号

  • 商务合作