vue的原生事件綁定流程


本文可能需要對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下斷點,一步一步執行,那么整個流程會更加清晰。

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM