本文可能需要對vue,從編譯模板到生成dom的流程具有一定的熟悉程度,可能才能夠明白。同時不排除作者有理解出錯的地方,大家在學習的過程中可以進行參考。
簡單流程
從一個簡單的例子入手
<div class="login-register" @click="testClick"></div>
假如我們在模板上定義了一個事件,那么我們知道,vue會對我們寫的模板進行解析,生成AST。如果你在模板上綁定了事件,那么AST上會有一個叫做events或者nativeEvents的屬性。。大致長這個樣子
{ 'click': { value: '事件綁定函數名稱', modifiers: {} } }
根據不同的修飾符,他會長成不同的形式,上面只是其中一個種形式。然后到生成代碼階段。vue會解析這個AST樹。最終經過代碼的處理會變成vnode。從模板編譯到生成vnode的詳細過程,本文不進行介紹。
vnode解析
本文着重關注vnode解析階段對事件的解析。我們知道首次渲染,或者是更新。都會發生在createPatchFunction,我們已Web平台為例。這個函數在vue源碼的src/core/vdom/patch.js下。它大致長這樣
export function createPatchFunction (backend) { let i, j
const cbs = {} const { modules, nodeOps } = backend for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } } //省略若干 return function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { //首次渲染 isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { ... } }
} }
當首次渲染時候調用createPatchFunction。調用createPatchFunction本質上就是調用patch。然后進入patch后就進入調用createElm准備生成真正的dom結點。我們可以先來看看哪里調用了這個createPatchFunction。他在platform/web/runtime/patch.js中被調用。
import * as nodeOps from 'web/runtime/node-ops' import { createPatchFunction } from 'core/vdom/patch' import baseModules from 'core/vdom/modules/index' import platformModules from 'web/runtime/modules/index' // the directive module should be applied last, after all // built-in modules have been applied. const modules = platformModules.concat(baseModules) //module // [ // attrs, // klass, // events, // domProps, // style, // transition // ] //baseModules // [ // ref, // directives // ] export const patch: Function = createPatchFunction({ nodeOps, modules })
可以看到createPatchFunction執行了之后再賦值給patch。這個patch就是上面返回的patch函數。它在其他地方被用在渲染視圖上,這里不講述。
那么為什么我們要看這個函數的出生地呢?因為它的參數十分重要。他有兩個參數,第一個參數是存放操作dom節點的方法,終點關注modules。從上面的impot大家可以找下module的出處。它由兩個數字拼接起來,其中關注一下數組有一個元素叫做events。我們的事件添加就發生在這里。
我們繼續尋找一下events的出生地,別的屬性和事件關聯不大,我們重點看events。根據import我們找到events的出生地。然后我們關注events文件最后它導出了一些東西。好了我們記住它導出了一個對象,然后對象有一個屬性叫做create。那么modules經過進一步解析長成下面這樣,我們回到createPatchFunction解析
export default { create: updateDOMListeners, update: updateDOMListeners }
//modules
[ ...
{
create: updateDOMListeners,
update: updateDOMListeners
}
...
]
我們注意createPatchFunction。createPatchFunction接受一個參數叫做backend。然后在函數開頭,對backend進行解構,就是上面代碼的nodeOps和modules參數。
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } }
解構完之后進入for循環。在createPatchFunction開頭定義了一個cbs對象。for循環遍歷一個叫hooks的數組。hooks是這樣的,它定義在本文件開頭
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
我們看下這個for循環意圖就是在cbs上定義一系列和hooks元素相同的屬性,然后鍵值是一個數組,然后數組內容是modules里面的一些內容。最后cbs大致是這樣的。
cbs: {'create': [], 'update': []...}
結合modules的結構看,create的鍵值里面會有updateDOMListeners方法,這個方法是真正添加事件的方法,那么他在哪里被調用我們繼續看。好了我們回到createPatchFunction的return值patch函數看。
當我們進入首次渲染的時候,會執行到patch函數里面的createElm方法。
return function patch (oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { //首次渲染 isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { ... } }
}
我們看看createElm做了什么事情。
function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { ... createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { //這里是處理事件系統的 invokeCreateHooks(vnode, insertedVnodeQueue) } ...
}
為了讓大家看清晰,我刪掉了很多,大家可以自己打開一份源碼對比着看。我們關注一個叫invokeCreateHooks函數。這里就是真正准備進行原生事件綁定的入口!!
我們看看invokeCreateHooks函數做了什么。它的代碼比較短。
function invokeCreateHooks (vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) } i = vnode.data.hook // Reuse variable if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, vnode) if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } }
我們關注第一個for循環。我看可以看到他再遍歷cb.create數組里面的內容。然后把cbs.create里面的函數全部都執行一次,我們回憶一下cbs.create里面有什么內容,其中一個函數就是updateDOMListeners。
在這里開始執行updateDOMListeners。我們現在看updateDOMListeners做了什么。這個方法定義在了platform/web/runtime/modules/events.js中,
//events.js
....
let target: any
....
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) { if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { return } const on = vnode.data.on || {} const oldOn = oldVnode.data.on || {} //這里把target指向dom結點 target = vnode.elm normalizeEvents(on) updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context) target = undefined }
第一個if是根據vnode判斷是否有定義一個點擊事件。有的話就繼續執行,沒有就return。
然后給on進行賦值。on大致會長成這樣
然后進行一些賦值操作。其中關注target。vue把vnode.elm賦值給target,我們知道elm這個屬性就是指向vnode所對應的真實dom結點,所以這里就是把我們要綁定事件的dom結點進行緩存。
然后執行normalizeEvents,他是對on繼續進行一些處理,我們暫不關心他做什么,這對於我們理解事件綁定流程影響不大。
接下來執行updateListeners方法。看看它做了什么
export function updateListeners ( on: Object, oldOn: Object, add: Function, remove: Function, createOnceHandler: Function, vm: Component ) { let name, def, cur, old, event for (name in on) { def = cur = on[name] old = oldOn[name] event = normalizeEvent(name) /* istanbul ignore if */ if (__WEEX__ && isPlainObject(def)) { cur = def.handler event.params = def.params } 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)) { // // { // 'click': invoker() // } cur = on[name] = createFnInvoker(cur, vm) } if (isTrue(event.once)) { cur = on[name] = createOnceHandler(event.name, cur, event.capture) } 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(event.name, oldOn[name], event.capture) } } }
重點關注add方法,它還是在platform/web/runtime/modules/events.js中
function add ( name: string, handler: Function, capture: boolean, passive: boolean ) { if (useMicrotaskFix) { const attachedTimestamp = currentFlushTimestamp //handle, original值 //ƒ invoker() // fns: ƒ () const original = handler handler = original._wrapper = function (e) { if ( // no bubbling, should always fire. // this is just a safety net in case event.timeStamp is unreliable in // certain weird environments... e.target === e.currentTarget || // event is fired after handler attachment e.timeStamp >= attachedTimestamp || // bail for environments that have buggy event.timeStamp implementations // #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState // #9681 QtWebEngine event.timeStamp is negative value e.timeStamp <= 0 || // #9448 bail if event is fired in another document in a multi-page // electron/nw.js app, since event.timeStamp will be using a different // starting reference e.target.ownerDocument !== document ) { return original.apply(this, arguments) } } } //這里的target指向dom結點, //執行到這里的時候target已經被賦值了 target.addEventListener( name, handler, supportsPassive ? { capture, passive } : capture ) }
這個方法最后就通過addEventListener把事件綁定到dom上。
最后
這里很多的細節其實都沒有提及,只是大概的把整個流程進行了梳理。可能了解不深的讀者可能還是看不懂。大家可以根據自己的情況對本文進行參考。大家也可以自己創建一個vue項目,然后在谷歌瀏覽器中對vue下斷點,一步一步執行,那么整個流程會更加清晰。