根據除錯工具看Vue原始碼之元件通訊(一)
根據除錯工具看Vue原始碼之元件通訊(一)## 根據除錯工具看Vue原始碼之元件通訊(一)
在平時的業務開發中,相信在座的各位沒少用過元件通訊。然而,對於一些新手/業務熟手來說,不懂技術原理往往知其然而不知其所以然,用得一臉懵逼。看完本文可以幫助你瞭解Vue
元件的通訊方式及原理,從而進一步加深對Vue
的理解,遠離CV
工程師的行列。
Vue
常用的元件通訊方式
-
通過
$emit
在子元件傳參給父元件,同時觸發對應的父元件函式,以此達到父子元件通訊的目的 -
通過
eventbus
的$emit
和$on
方法傳遞資料,以此實現父子元件/兄弟元件之間的通訊 -
通過
Vuex
將頁面資料劃分模組,更好更方便的管理資料
父子元件通訊原理
:chestnut:示例程式碼:
// 父元件 <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$1 updateListeners updateDomListeners invokeCreateHooks createElm
試探性的點進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
方法是怎麼執行的?
- 判斷是否有引數,然後分情況呼叫
- 處理非同步函式的情況
:warning:注意:由於Vue
會在方法上再封裝一層呼叫器(invoker
),所以在在呼叫堆疊這裡往往會出現兩個invokeWithErrorHandling
方法