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

 

 

 

 

 

posted on 2020-08-07 12:08  余圣源  阅读(1554)  评论(0编辑  收藏  举报