vue的原⽣事件绑定流程
本⽂可能需要对vue,从编译模板到⽣成dom的流程具有⼀定的熟悉程度,可能才能够明⽩。同时不排除作者有理解出错的地⽅,⼤家在学习的过程中可以进⾏参考。
简单流程
从⼀个简单的例⼦⼊⼿
假如我们在模板上定义了⼀个事件,那么我们知道,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 } = backendfor (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下断点,⼀步⼀步执⾏,那么整个流程会更加清晰。