根据调试工具看Vue源码之组件通信(一)## 根据调试工具看Vue源码之组件通信(一)在平时的业务开发中,相信在座的各位没少用过组件通信。然而,对于一些新手/业务熟手来说,不懂技术原理往往知其然而不知其所以然,用得一脸懵逼。看完本文可以帮助你了解Vue组件的通信方式及原理,从而进一步加深对Vue的理解,远离CV工程师的行列。Vue常用的组件通信方式通过$emit在子组件传参给父组件,同时触发对应的父组件函数,以此达到父子组件通信的目的通过eventbus的$emit 和$on方法传递数据,以此实现父子组件/兄弟组件之间的通信通过Vuex将页面数据划分模块,更好更方便的管理数据父子组件通信原理????示例代码:// 父组件<template> <div id=“app”> <img alt=“Vue logo” src="./assets/logo.png"> <HelloWorld msg=“Welcome to Your Vue.js App” @test=“test”/> </div></template><script>import HelloWorld from ‘./components/HelloWorld.vue’export default { name: ‘app’, methods: { test (param) { debugger console.log(‘param–>’, param); } }, components: { HelloWorld }}</script>// 子组件<template> <div class=“wrapper”> <button @click=“test”>按钮</button> </div></template><script>export default { name: ‘HelloWorld’, methods: { test () { debugger this.$emit(’test’, ‘666’) } }}</script>我们可以看到,父子组件的test方法中各打了一个debugger。运行程序,进入第一个断点Vue.prototype.$emit = function (event) { var vm = this; … var cbs = vm._events[event]; if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs; var args = toArray(arguments, 1); var info = “event handler for "” + event + “"”; for (var i = 0, l = cbs.length; i < l; i++) { invokeWithErrorHandling(cbs[i], vm, args, vm, info); } } return vm};看完上面的代码我们知道,vm._events[event]拿到了一个方法,然后调用invokeWithErrorHandling。当然,vm._events[event]的方法应该是从template上拿到的,接下来我们可以带着这几个疑问继续往下看:vm._events是什么时候赋值的?invokeWithErrorHandling方法是怎么执行的?vm._events是什么时候赋值的?在子组件的test方法中打下一个断点,选中调用堆栈中的最后一个以后可以看到add$1函数,在这里再下一个断点,重新刷新页面以后断点停在了add$1这个函数上,同时调用堆栈列表刷新,大概有这些:add$1updateListenersupdateDomListenersinvokeCreateHookscreateElm…试探性的点进updateListeners以后,我们看到:function updateListeners ( on, oldOn, add, remove$$1, createOnceHandler, vm) { var name, def$$1, cur, old, event; // 看到这里初步猜测会遍历所有的方法 // 在chrome的断点下可以看到一个click属性,这里不知道为什么没有test方法 for (name in on) { def$$1 = cur = on[name]; old = oldOn[name]; event = normalizeEvent(name); // 判断当前的方法的调用器(invoker)是否是undefined,在开发环境下则会有报错提示 if (isUndef(cur)) { process.env.NODE_ENV !== ‘production’ && warn( “Invalid handler for event "” + (event.name) + “": got " + String(cur), vm ); } else if (isUndef(old)) { // 判断之前是否已存在 if (isUndef(cur.fns)) { // 判断实际上调用的函数是否是undefined cur = on[name] = createFnInvoker(cur, vm); } if (isTrue(event.once)) { // 可能是挂载在一次性节点上,这里也做出判断 cur = on[name] = createOnceHandler(event.name, cur, event.capture); } // 断点没打在这里之前,event.name一直是“click” add(event.name, cur, event.capture, event.passive, event.params); } else if (cur !== old) { old.fns = cur; on[name] = old; } } for (name in oldOn) { if (isUndef(on[name])) { event = normalizeEvent(name); remove$$1(event.name, oldOn[name], event.capture); } }}整理完上面这个函数的逻辑以后,将断点打在add上,刷新页面后断点停在这里,步进这个函数:function add (event, fn) { target.$on(event, fn);}显然target是全局变量,但是这里先不深究。再次步进之后可以看到断点停在这里:Vue.prototype.$on = function (event, fn) { var vm = this; if (Array.isArray(event)) { for (var i = 0, l = event.length; i < l; i++) { vm.$on(event[i], fn); } } else { (vm._events[event] || (vm._events[event] = [])).push(fn); // optimize hook:event cost by using a boolean flag marked at registration // instead of a hash lookup if (hookRE.test(event)) { vm._hasHookEvent = true; } } return vm };可见父子组件通信过程中,尽管$on对开发者不可见,但是最终还是要走$on函数,这里感觉跟使用eventbus大同小异。至此,刚才提出的第一个疑问已经解决:)invokeWithErrorHandling方法是怎么执行的?在一开始的基础上,直接步进invokeWithErrorHandling方法:function invokeWithErrorHandling ( handler, context, args, vm, info) { var res; try { // 判断是否有参数,然后分情况调用 res = args ? handler.apply(context, args) : handler.call(context); // 处理异步函数的情况 if (res && !res._isVue && isPromise(res)) { // issue #9511 // reassign to res to avoid catch triggering multiple times when nested calls res = res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)”); }); } } catch (e) { handleError(e, vm, info); } return res}最后重新梳理下父子组件通信的实现逻辑:赋值vm._events[event]页面初始化时,Vue调用updateListeners函数(当然,在那之前会生成虚拟dom,也就是vnode,这里暂不深究),在函数里面调用createFnInvoker方法,给模板上的方法再套一层调用器(invoker)调用target.$on方法递归处理event为数组的情况给vm._events[event]赋值invokeWithErrorHandling方法是怎么执行的?判断是否有参数,然后分情况调用处理异步函数的情况⚠️注意:由于Vue会在方法上再封装一层调用器(invoker),所以在在调用堆栈这里往往会出现两个invokeWithErrorHandling方法