Vue 自定义指令源码解析(基于V2.6.8)

最近经常使用指令来实现一些功能,觉得甚是好用。然而却遇到一个问题:v-if和我自定义指令在同一个标签里使用的时候,我自定义的指令传的值通过bind.value没获取到值。后来换成v-show是我期待的结果,但是当我把v-if换成v-else(v-if 和 v-else指令顺序颠倒),也能获取到binding.value的值。这就让我对指令很感兴趣,想知道vue内部又是怎么去实现指令的。

入口文件

指令在官方文档中属于全局API,所以从src\core\global-api\index.js中查找指令。

.....
  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue) //初始化指令
......

于是我查看了文件src\core\global-api\assets.js,发现了这段代码:

ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
       //省略其它代码
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })

该函数的参数有两个,其中id就是指令的名字,definition就是我们平时自定义指令传的对象或者函数。这段代码的意思也就很清楚了:如果是指令,且第二个参数是函数,则会新定义一个对象,并把definition赋值给bind和update属性。最后,会把指令的定义赋给Vue.options.directives[指令名]。感兴趣的可以自己再vue官网打印一下。

上面是全局API的实现方式。但是其实指令是加在模板的标签上的,所以模板编译部分也做了处理。

模板编译

模板编译的入口文件src\compiler\parser\index.js,你会很容易看到有个函数parseHTML(template, {}),代码如下:

parseHTML(template, {
     //......省略代码
      if (inVPre) {
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        processFor(element)  //解析v-for
        processIf(element)     //解析v-if
        processOnce(element)  //解析v-once
      }
     //......省略
    if (!unary) {
        currentParent = element
        stack.push(element)
      } else {
       //自定义指令走这里 
        closeElement(element)
      }
      //......省略
})

一步一步向下找,会找到函数processElement(),进而发现除了上述代码中直接解析的指令,剩下的指令和属性统一都交给了processAttrs()函数处理,比如v-bind、v-on以及普通属性等。截取部分该函数的代码:

      //省略部分代码
      else { // normal directives
        name = name.replace(dirRE, '')
        // parse arg
        const argMatch = name.match(argRE)
        let arg = argMatch && argMatch[1]
        isDynamic = false
        if (arg) {
          name = name.slice(0, -(arg.length + 1))
          if (dynamicArgRE.test(arg)) {
            arg = arg.slice(1, -1)
            isDynamic = true
          }
        }
        addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])

在该函数中处理了要传给addDirective的参数对应的值,最终在addDirective函数中把自定义指令添加到el上。然后我们在文件src\compiler\codegen\index.js中会找到constructor里的代码:

constructor (options: CompilerOptions) {
    this.options = options
    this.warn = options.warn || baseWarn
    this.transforms = pluckModuleFunction(options.modules, 'transformCode')
    this.dataGenFns = pluckModuleFunction(options.modules, 'genData')
    this.directives = extend(extend({}, baseDirectives), options.directives)
    //省略代码
  }

可以继续在该文件中找到genDirectives函数,可以看到添加到directives属性中的对象的生成过程如下:

function genDirectives (el: ASTElement, state: CodegenState): string | void {
  const dirs = el.directives
    //省略代码
    if (needRuntime) {
      hasRuntime = true
      res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
      }${
        dir.arg ? `,arg:${dir.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
      }${
        dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
      }},`
    }
  }

对于添加到directives上的数据,则是在patch中通过添加到cbs中的钩子函数处理的。具体过程见src/core/vdom/modules/directives.js文件中。

export default {
  create: updateDirectives,
  update: updateDirectives,
  destroy: function unbindDirectives (vnode: VNodeWithData) {
    updateDirectives(vnode, emptyNode)
  }
}

可以看到,这三个钩子函数,最终调用的都是updateDirectives方法。

function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
  if (oldVnode.data.directives || vnode.data.directives) {
    _update(oldVnode, vnode)
  }
}

如果updateDirectives()函数接受到的两个参数data中有directives属性,则调用_update方法来进行处理。_update方法的主要操作,其实就是调用指令的各种钩子函数。

function _update (oldVnode, vnode) {
  // 第一次实例化组件时,oldVnode是emptyNode
  const isCreate = oldVnode === emptyNode
  // 销毁组件时,vnode是emptyNode
  const isDestroy = vnode === emptyNode
  //normalizeDirectives函数是从组件的vm.$options.directives中获取指令的定义
  const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
  const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)

  const dirsWithInsert = []
  const dirsWithPostpatch = []

  let key, oldDir, dir
  for (key in newDirs) {
    //循环新vnode上绑定的指令
    oldDir = oldDirs[key]
    dir = newDirs[key]
    if (!oldDir) {
      // new directive, bind   => 如果第一次绑定,则直接调用bind钩子函数
      callHook(dir, 'bind', vnode, oldVnode)
      if (dir.def && dir.def.inserted) {
        //若同时还添加了inserted钩子,则会先把它添加到dirsWithInsert数组中。
        dirsWithInsert.push(dir)
      }
    } else {
      // existing directive, update   =>  如果不是第一次绑定,则调用update钩子函数
      dir.oldValue = oldDir.value
      dir.oldArg = oldDir.arg
      callHook(dir, 'update', vnode, oldVnode)
      if (dir.def && dir.def.componentUpdated) {
         //若同时定义了componentUpdated钩子,则会先把它添加到dirsWithPostpatch数组中。
        dirsWithPostpatch.push(dir)
      }
    }
  }

  if (dirsWithInsert.length) {
    const callInsert = () => {
      for (let i = 0; i < dirsWithInsert.length; i++) {
        callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
      }
    }
    if (isCreate) {
       //如果是vnode是第一次创建,
      //则会把dirsWithInsert数组中的回调追加到vnode.data.hook.insert中执行
      mergeVNodeHook(vnode, 'insert', callInsert)
    } else {
      callInsert()
    }
  }

  if (dirsWithPostpatch.length) {
    mergeVNodeHook(vnode, 'postpatch', () => {
      for (let i = 0; i < dirsWithPostpatch.length; i++) {
        callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode)
      }
    })
  }

  if (!isCreate) {
    for (key in oldDirs) {
      if (!newDirs[key]) {
        // no longer present, unbind  
        // 如果不是第一次创建,就调用旧vnode中新vnode不存在的指令的unbind钩子函数
        callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
      }
    }
  }
}

指令使用

Vue.directive('my-directive', {
  bind: function () {},
  inserted: function () {},
  update: function () {},
  componentUpdated: function () {},
  unbind: function () {}
})

上面的_update 函数对应着这几个钩子函数,其中:
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
unbind:只调用一次,指令与元素解绑时调用。

指令钩子函数会被传入以下参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM 。
  • binding:一个对象,包含以下属性:
  • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

看完源代码,在回过头来看官网注释,会发现自己对官网的说明印象更加深刻,也更加深自己的理解。

答案

关于开头提到的问题的答案:其实还是和v-if有关,v-if在编译过程中会被转化成三元表达式,条件不满足的时候,是不会渲染此节点的,自然值也没拿到了。

谢谢阅读,有问题可以互相交流哦

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容

  • vue概述 在官方文档中,有一句话对Vue的定位说的很明确:Vue.js 的核心是一个允许采用简洁的模板语法来声明...
    li4065阅读 7,181评论 0 25
  • 这篇笔记主要包含 Vue 2 不同于 Vue 1 或者特有的内容,还有我对于 Vue 1.0 印象不深的内容。关于...
    云之外阅读 5,043评论 0 29
  • 在第五章中我们已经介绍了需要Vue内置的指令,比如v-if、v-show等,这些丰富的内置指令能满足我们的...
    六个周阅读 1,672评论 2 9
  • ## 框架和库的区别?> 框架(framework):一套完整的软件设计架构和**解决方案**。> > 库(lib...
    Rui_bdad阅读 2,884评论 1 4
  • 一:什么是闭包?闭包的用处? (1)闭包就是能够读取其他函数内部变量的函数。在本质上,闭包就 是将函数内部和函数外...
    xuguibin阅读 9,476评论 1 52