记一次Vue的v-on指令中关于回调函数的坑

220 阅读2分钟

缘起

某日在一次翻译react到vue的过程中,有如下语句

<Tabs onChange={this.tabsChange(3)}></Tabs>
...
tabsChange = (key) => (value)=>{ /*some code*/ };

非常amazing啊,我之前写vue的v-on时咋没想到这么写,于是代码写起来

<Tabs @change="tabsChange(3)"></Tabs>
...
methods:{
    tabsChange: (key) => value => { /* some code */ }
}

nice,写完,运行,duang!代码并没有按照我想的那样执行,WHY?

遇事不决,console.log,于是

<Tabs @change="tabsChange(3)"></Tabs>
...
methods:{
    tabsChange(key){
        console.log(1)
        return value => {
            console.log(2)
            /* some code */
        }
    }
}

运行结果:1。WTF? 为毛2不出来,妹理由啊,原来直接写 @change="(value) => handleChange(value, 3)" 这样也是可以的,那估计就是v-on本身的问题,不支持这样的调用了。

解决

vue的文档查了一波 API — Vue.js (vuejs.org) 没有相关的介绍,这就蛋疼了,那么某度?似乎也查不到相关信息,没办法只能看看源码了

vue的源码还是相对好看的,组织比较清晰,先搜了一波v-on相关定义,查到在compiler\directives\on.js有相关介绍,但是只有一个包装函数,没啥用,继续查

这时候就需要想一下了,一般的前端框架,都是解析,编译,运行这三步,那么我们知道调用v-on的实际上是vm.$emit方法,那么咱们就看看相关定义看看是如何执行的。

core\instance\events.js中查找到了相关定义,最终执行的是invokeWithErrorHandling方法,点进去之后,关键语句是res = args ? handler.apply(context, args) : handler.call(context) 也就是调用v-on绑定的回调函数,执行之。

那么会不会是这个函数有问题,导致无法执行回调呢?不多说,测试之

function test(a) {
    return function (b) {
        console.log('a:[' + a + '], b:[' + b + ']' )
    }
}

test(3).apply(null, [4])

执行结果:a:[3], b:[4],证明了它的清白之余,咱们只能继续往下找了。既然调用这里没啥看头,咱们只能找找编译的时候生成的最终产物是啥。
那么,就从compiler\index.js找起,其中发现了关键函数generate,点进去,查找到关键函数genData最终定位到genHandler即生成最终代码。

if (!handler.modifiers) {
    if (isMethodPath || isFunctionExpression) {
      return handler.value
    }
    /* istanbul ignore if */
    if (__WEEX__ && handler.params) {
      return genWeexHandler(handler.params, handler.value)
    }
    return `function($event){${
      isFunctionInvocation ? `return ${handler.value}` : handler.value
    }}` // inline statement
  }

关键在于最后,如果输入的是test(3),实际上生成的是带有包装函数的最终执行体。按照上面的例子改写

function test(a) {
  return function (b) {
      console.log('a:[' + a + '], b:[' + b + ']' )
  }
}
//test(3).apply(null, [4])
(function($event){ return test(3) }).apply(null, [4])

执行结果:ƒ (b) { console.log('a:[' + a + '], b:[' + b + ']' ) }

而如果输入的是 (value) => test(3, value)那么 genHandler函数最终会直接原样输出,也就是最终执行时大概会是((b) => test(a)).apply(null, arguments)。那么,@change="(value) => handleChange(value, 3) 这样写可以但是@change="handleChange(3) 这样写就不行也就得出结论了

总结

这个坑,其实很容易忽略过去,毕竟这个写法不行,那个写法ok就好,何必纠结那么多。但是呢往往咱们在了解框架运行机制或者看源码的时候就需要有这些小困惑,让你能够沉下心去,有目的性的去看框架源码,不然看源码的时候很容易就会陷入枯燥的为看而看,坚持不下去。而且在找问题的过程中并不是一帆风顺,我在查找定义的时候也曾经陷入过死胡同里不知道下一步应该往哪找。那么这个时候就需要有那些小困惑支持着你一步步地,耐心地往下追代码,最终找到根源,解除疑惑。
最后记得输出文档,输出才是你最终解决问题的最后一步。

最后的最后,vue的源码还是挺容易懂的,建议大家时不时啃一下,说不定就精通Vue,月薪50K了呢
我是南宁老林,混迹于广西南宁的微中年前端程序员

image.png