本文可能需要对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下断点,一步一步执行,那么整个流程会更加清晰。