Vue事件绑定原理

前端开发 作者: 2024-08-21 16:10:01
Vue事件绑定原理 Vue中通过v-on或其语法糖@指令来给元素绑定事件并且提供了事件修饰符,基本流程是进行模板编译生成AST,生成render函数后并执行得到VNode,VNode生成真实DOM节点

Vue事件绑定原理

修饰符

  • .stop: 调用event.stopPropagation(),即阻止事件冒泡。
  • .prevent: 调用event.preventDefault(),即阻止默认事件。
  • .capture: 添加事件侦听器时使用capture模式,即使用事件捕获模式处理事件。
  • .self: 只当事件是从侦听器绑定的元素本身触发时才触发回调。
  • .{keyCode | keyAlias}: 只当事件是从特定键触发时才触发回调。
  • .native: 监听组件根元素的原生事件,即注册组件根元素的原生事件而不是组件自定义事件的。
  • .once: 只触发一次回调。
  • .left(2.2.0): 只当点击鼠标左键时触发。
  • .right(2.2.0): 只当点击鼠标右键时触发。
  • .middle(2.2.0): 只当点击鼠标中键时触发。
  • .passive(2.3.0): 以{ passive: true }模式添加侦听器,表示listener永远不会调用preventDefault()

普通元素

<!-- 方法处理器 -->
<button v-on:click="doThis"></button>

<!-- 动态事件 (2.6.0+) -->
<button v-on:[event]="doThis"></button>

<!-- 内联语句 -->
<button v-on:click="doThat('param',$event)"></button>

<!-- 缩写 -->
<button @click="doThis"></button>

<!-- 动态事件缩写 (2.6.0+) -->
<button @[event]="doThis"></button>

<!-- 停止冒泡 -->
<button @click.stop="doThis"></button>

<!-- 阻止默认行为 -->
<button @click.prevent="doThis"></button>

<!-- 阻止默认行为,没有表达式 -->
<form @submit.prevent></form>

<!--  串联修饰符 -->
<button @click.stop.prevent="doThis"></button>

<!-- 键修饰符,键别名 -->
<input @keyup.enter="onEnter">

<!-- 键修饰符,键代码 -->
<input @keyup.13="onEnter">

<!-- 点击回调只会触发一次 -->
<button v-on:click.once="doThis"></button>

<!-- 对象语法 (2.4.0+) -->
<button v-on="{ mousedown: doThis,mouseup: doThat }"></button>

组件元素

<!-- 自定义事件 -->
<my-component @my-event="handleThis"></my-component>

<!-- 内联语句 -->
<my-component @my-event="handleThis('param',$event)"></my-component>

<!-- 组件中的原生事件 -->
<my-component @click.native="onClick"></my-component>

编译阶段

// dev/src/compiler/parser/index.js line 23
export const onRE = /^@|^v-on:/
export const dirRE = process.env.VBIND_PROP_SHORTHAND
  ? /^v-|^@|^:|^\.|^#/
  : /^v-|^@|^:|^#/
// ...
const dynamicArgRE = /^\[.*\]$/
// ...
export const bindRE = /^:|^\.|^v-bind:/
  
// dev/src/compiler/parser/index.js line 757
function processAttrs (el) {
  const list = el.attrsList
  let i,l,name,rawName,value,modifiers,syncGen,isDynamic
  for (i = 0,l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value
    if (dirRE.test(name)) { // 匹配指令属性
      // mark element as dynamic
      el.hasBindings = true
      // modifiers
      modifiers = parseModifiers(name.replace(dirRE,'')) // 将修饰符解析
      // support .foo shorthand syntax for the .prop modifier
      if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) {
        (modifiers || (modifiers = {})).prop = true
        name = `.` + name.slice(1).replace(modifierRE,'')
      } else if (modifiers) {
        name = name.replace(modifierRE,'')
      }
      if (bindRE.test(name)) { // v-bind // 处理v-bind的情况
        // ...
      } else if (onRE.test(name)) { // v-on // 处理事件绑定
        name = name.replace(onRE,'') // 将事件名匹配
        isDynamic = dynamicArgRE.test(name) // 动态事件绑定
        if (isDynamic) { // 如果是动态事件
          name = name.slice(1,-1) // 去掉两端的 []
        }
        addHandler(el,false,warn,list[i],isDynamic) // 处理事件收集
      } else { // normal directives // 处理其他指令
        // ...
      }
    } else {
      // literal attribute // 处理文字属性
      // ...
    }
  }
}
// dev/src/compiler/helpers.js line 69
export function addHandler (
  el: ASTElement,name: string,value: string,modifiers: ?ASTModifiers,important?: boolean,warn?: ?Function,range?: Range,dynamic?: boolean
) {
  modifiers = modifiers || emptyObject
  // passive 和 prevent 不能同时使用,具体是由passive模式的性质决定的
  // 详细可以参阅 https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
  // warn prevent and passive modifier
  /* istanbul ignore if */
  if (
    process.env.NODE_ENV !== 'production' && warn &&
    modifiers.prevent && modifiers.passive
  ) {
    warn(
      'passive and prevent can\'t be used together. ' +
      'Passive handler can\'t prevent default event.',range
    )
  }
  // 标准化click.right和click.middle,因为它们实际上不会触发。
  // 从技术上讲,这是特定于浏览器的,但是至少目前来说,浏览器是唯一具有右键/中间点击的目标环境。
  // normalize click.right and click.middle since they don't actually fire
  // this is technically browser-specific,but at least for now browsers are
  // the only target envs that have right/middle clicks.
  if (modifiers.right) { // 将鼠标右键点击标准化 右键点击默认的是 contextmenu 事件
    if (dynamic) { // 如果是动态事件
      name = `(${name})==='click'?'contextmenu':(${name})` // 动态确定事件名
    } else if (name === 'click') { // 如果不是动态事件且是鼠标右击
      name = 'contextmenu' // 则直接替换为contextmenu事件
      delete modifiers.right // 删除modifiers的right属性
    }
  } else if (modifiers.middle) { // 同样标准化处理鼠标中键点击的事件
    if (dynamic) { // 如果是动态事件
      name = `(${name})==='click'?'mouseup':(${name})` // 动态确定事件名
    } else if (name === 'click') { // 如果不是动态事件且是鼠标中键点击
      name = 'mouseup' // 处理为mouseup事件
    }
  }
  // 下面是对捕获、一次触发、passive模式的modifiers处理,主要是为事件添加 !、~、& 标记
  // 这一部分标记可以在Vue官方文档中查阅 
  // https://cn.vuejs.org/v2/guide/render-function.html#%E4%BA%8B%E4%BB%B6-amp-%E6%8C%89%E9%94%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6
  // check capture modifier
  if (modifiers.capture) {
    delete modifiers.capture
    name = prependModifierMarker('!',dynamic)
  }
  if (modifiers.once) {
    delete modifiers.once
    name = prependModifierMarker('~',dynamic)
  }
  /* istanbul ignore if */
  if (modifiers.passive) {
    delete modifiers.passive
    name = prependModifierMarker('&',dynamic)
  }
  
  // events 用来记录绑定的事件
  let events
  if (modifiers.native) { // 如果是要触发根元素原生事件则直接取得nativeEvents
    delete modifiers.native
    events = el.nativeEvents || (el.nativeEvents = {})
  } else { // 否则取得events
    events = el.events || (el.events = {})
  }
    
  // 将事件处理函数作为handler
  const newHandler: any = rangeSetItem({ value: value.trim(),dynamic },range)
  if (modifiers !== emptyObject) {
    newHandler.modifiers = modifiers
  }

 // 绑定的事件可以多个,回调也可以多个,最终会合并到数组中
  const handlers = events[name]
  /* istanbul ignore if */
  if (Array.isArray(handlers)) {
    important ? handlers.unshift(newHandler) : handlers.push(newHandler)
  } else if (handlers) {
    events[name] = important ? [newHandler,handlers] : [handlers,newHandler]
  } else {
    events[name] = newHandler
  }

  el.plain = false
}

代码生成

// dev/src/compiler/codegen/index.js line 42
export function generate (
  ast: ASTElement | void,options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast,state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,// 即render字符串
    staticRenderFns: state.staticRenderFns
  }
}

// dev/src/compiler/codegen/index.js line 55
export function genElement (el: ASTElement,state: CodegenState): string {
    // ...
    let code
    if (el.component) {
      code = genComponent(el.component,el,state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el,state)
      }

      const children = el.inlineTemplate ? null : genChildren(el,state,true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // ...
}

// dev/src/compiler/codegen/index.js line 219
export function genData (el: ASTElement,state: CodegenState): string {
  let data = '{'
  // ...
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events,false)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents,true)},`
  }
  // ...
  data = data.replace(/,$/,'') + '}'
  // ...
  return data
}

// dev/src/compiler/to-function.js line 12 
function createFunction (code,errors) {
  try {
    return new Function(code) // 将render字符串转为render函数
  } catch (err) {
    errors.push({ err,code })
    return noop
  }
}
// dev/src/compiler/codegen/events.js line 3
const fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
const fnInvokeRE = /\([^)]*?\);*$/
const simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/

// dev/src/compiler/codegen/events.js line 7
// KeyboardEvent.keyCode aliases
const keyCodes: { [key: string]: number | Array<number> } = {
  esc: 27,tab: 9,enter: 13,space: 32,up: 38,left: 37,right: 39,down: 40,'delete': [8,46]
}
// KeyboardEvent.key aliases
const keyNames: { [key: string]: string | Array<string> } = {
  // #7880: IE11 and Edge use `Esc` for Escape key name.
  esc: ['Esc','Escape'],tab: 'Tab',enter: 'Enter',// #9112: IE11 uses `Spacebar` for Space key name.
  space: [' ','Spacebar'],// #7806: IE11 uses key names without `Arrow` prefix for arrow keys.
  up: ['Up','ArrowUp'],left: ['Left','ArrowLeft'],right: ['Right','ArrowRight'],down: ['Down','ArrowDown'],// #9112: IE11 uses `Del` for Delete key name.
  'delete': ['Backspace','Delete','Del']
}
}

// dev/src/compiler/codegen/events.js line 37
// #4868: modifiers that prevent the execution of the listener
// need to explicitly return null so that we can determine whether to remove
// the listener for .once
const genGuard = condition => `if(${condition})return null;`
const modifierCode: { [key: string]: string } = {
  stop: '$event.stopPropagation();',prevent: '$event.preventDefault();',self: genGuard(`$event.target !== $event.currentTarget`),ctrl: genGuard(`!$event.ctrlKey`),shift: genGuard(`!$event.shiftKey`),alt: genGuard(`!$event.altKey`),meta: genGuard(`!$event.metaKey`),left: genGuard(`'button' in $event && $event.button !== 0`),middle: genGuard(`'button' in $event && $event.button !== 1`),right: genGuard(`'button' in $event && $event.button !== 2`)
}

// dev/src/compiler/codegen/events.js line 55
export function genHandlers (
  events: ASTElementHandlers,isNative: boolean
): string {
  const prefix = isNative ? 'nativeOn:' : 'on:'
  let staticHandlers = ``
  let dynamicHandlers = ``
  for (const name in events) { // 遍历AST解析后的事件属性
    const handlerCode = genHandler(events[name]) // 将事件对象转换成可拼接的字符串
    if (events[name] && events[name].dynamic) {
      dynamicHandlers += `${name},${handlerCode},`
    } else {
      staticHandlers += `"${name}":${handlerCode},`
    }
  }
  staticHandlers = `{${staticHandlers.slice(0,-1)}}`
  if (dynamicHandlers) {
    return prefix + `_d(${staticHandlers},[${dynamicHandlers.slice(0,-1)}])`
  } else {
    return prefix + staticHandlers
  }
}

// dev/src/compiler/codegen/events.js line 96
function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
  if (!handler) {
    return 'function(){}'
  }

  // 事件绑定可以多个,多个在解析AST树时会以数组的形式存在,如果有多个则会递归调用getHandler方法返回数组。
  if (Array.isArray(handler)) {
    return `[${handler.map(handler => genHandler(handler)).join(',')}]`
  }

  const isMethodPath = simplePathRE.test(handler.value) // 调用方法为 doThis 型
  const isFunctionExpression = fnExpRE.test(handler.value) // 调用方法为 () => {} or function() {} 型
  const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE,'')) // 调用方法为 doThis($event) 型

  if (!handler.modifiers) { // 没有修饰符
    if (isMethodPath || isFunctionExpression) { // 符合这两个条件则直接返回
      return handler.value
    }
    /* istanbul ignore if */
    if (__WEEX__ && handler.params) {
      return genWeexHandler(handler.params,handler.value)
    }
    return `function($event){${ // 返回拼接的匿名函数的字符串
      isFunctionInvocation ? `return ${handler.value}` : handler.value
    }}` // inline statement
  } else { // 处理具有修饰符的情况
    let code = ''
    let genModifierCode = ''
    const keys = []
    for (const key in handler.modifiers) {  // 遍历modifiers上记录的修饰符
      if (modifierCode[key]) {
        genModifierCode += modifierCode[key]  // 根据修饰符添加对应js的代码
        // left/right
        if (keyCodes[key]) {
          keys.push(key)
        }
      } else if (key === 'exact') { // 针对exact的处理
        const modifiers: ASTModifiers = (handler.modifiers: any)
        genModifierCode += genGuard(
          ['ctrl','shift','alt','meta']
            .filter(keyModifier => !modifiers[keyModifier])
            .map(keyModifier => `$event.${keyModifier}Key`)
            .join('||')
        )
      } else {
        keys.push(key) // 如果修饰符不是以上修饰符,则会添加到keys数组中
      }
    }
    if (keys.length) {
      code += genKeyFilter(keys) // 处理其他修饰符 即keyCodes中定义的修饰符
    }
    // Make sure modifiers like prevent and stop get executed after key filtering
    if (genModifierCode) {
      code += genModifierCode
    }
    // 根据三种不同的书写模板返回不同的字符串
    const handlerCode = isMethodPath
      ? `return ${handler.value}($event)`
      : isFunctionExpression
        ? `return (${handler.value})($event)`
        : isFunctionInvocation
          ? `return ${handler.value}`
          : handler.value
    /* istanbul ignore if */
    if (__WEEX__ && handler.params) {
      return genWeexHandler(handler.params,code + handlerCode)
    }
    return `function($event){${code}${handlerCode}}`
  }
}

// dev/src/compiler/codegen/events.js line 175
function genFilterCode (key: string): string {
  const keyVal = parseInt(key,10)
  if (keyVal) { // 如果key是数字,则直接返回$event.keyCode!==${keyVal}
    return `$event.keyCode!==${keyVal}`
  }
  const keyCode = keyCodes[key]
  const keyName = keyNames[key]
  // 返回_k函数,它的第一个参数是$event.keyCode,
  // 第二个参数是key的值,
  // 第三个参数就是key在keyCodes中对应的数字。
  return (
    `_k($event.keyCode,` +
    `${JSON.stringify(key)},` +
    `${JSON.stringify(keyCode)},` +
    `$event.key,` +
    `${JSON.stringify(keyName)}` +
    `)`
  )
}

事件绑定

// dev/src/core/vdom/patch.js line 33
const hooks = ['create','activate','update','remove','destroy']

// dev/src/core/vdom/patch.js line 125
function createElm (
    vnode,insertedVnodeQueue,parentElm,refElm,nested,ownerArray,index
  ) {
  // ...
  if (isDef(data)) {
    invokeCreateHooks(vnode,insertedVnodeQueue)
  }
  // ...
}

// dev/src/core/vdom/patch.js line 303
// 在之前cbs经过处理 
// 这里cbs.create包含如下几个回调:
// updateAttrs、updateClass、updateDOMListeners、updateDOMProps、updateStyle、update、updateDirectives
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)
    }
}
// dev/src/platforms/web/runtime/modules/events.js line 105
function updateDOMListeners (oldVnode: VNodeWithData,vnode: VNodeWithData) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {  // on是事件指令的标志
    return
  }
  // 新旧节点不同的事件绑定解绑
  const on = vnode.data.on || {}
  const oldOn = oldVnode.data.on || {}
  // 拿到需要添加事件的真实DOM节点
  target = vnode.elm
  // normalizeEvents是对事件兼容性的处理
  normalizeEvents(on)
  // 调用updateListeners方法,并将on作为参数传进去
  updateListeners(on,oldOn,add,remove,createOnceHandler,vnode.context)
  target = undefined
}

// dev/src/core/vdom/helpers/update-listeners.js line line 53
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)) { // createFunInvoker返回事件最终执行的回调函数
        cur = on[name] = createFnInvoker(cur,vm)
      }
      if (isTrue(event.once)) {  // 只触发一次的事件
        cur = on[name] = createOnceHandler(event.name,event.capture)
      }
      // 执行真正注册事件的执行函数
      add(event.name,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)
    }
  }
}

// dev/src/platforms/web/runtime/modules/events.js line 32
// 在执行完回调之后,移除事件绑定
function createOnceHandler (event,handler,capture) {
  const _target = target // save current target element in closure
  return function onceHandler () {
    const res = handler.apply(null,arguments)
    if (res !== null) {
      remove(event,onceHandler,capture,_target)
    }
  }
}
// dev/src/platforms/web/runtime/modules/events.js line 46
function add (
  name: string,handler: Function,capture: boolean,passive: boolean
) {
  // async edge case #6566: inner click event triggers patch,event handler
  // attached to outer element during patch,and triggered again. This
  // happens because browsers fire microtask ticks between event propagation.
  // the solution is simple: we save the timestamp when a handler is attached,// and the handler would only fire if the event passed to it was fired
  // AFTER it was attached.
  if (useMicrotaskFix) {
    const attachedTimestamp = currentFlushTimestamp
    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.addEventListener(
    name,supportsPassive
      ? { capture,passive }
      : capture
  )
}

// dev/src/platforms/web/runtime/modules/events.js line 92
function remove (
  name: string,_target?: HTMLElement
) {
  (_target || target).removeEventListener(
    name,handler._wrapper || handler,capture
  )
}
https://github.com/WindrunnerMax/EveryDay
https://cn.vuejs.org/v2/api/#v-on
https://juejin.im/post/6844903919290679304
https://juejin.im/post/6844904061897015310
https://juejin.im/post/6844904126250221576
https://segmentfault.com/a/1190000009750348
https://blog.csdn.net/weixin_41275295/article/details/100549145
https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
https://github.com/liutao/vue2.0-source/blob/master/%E4%BA%8B%E4%BB%B6%E5%A4%84%E7%90%86.md
原创声明
本站部分文章基于互联网的整理,我们会把真正“有用/优质”的文章整理提供给各位开发者。本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
本文链接:http://www.jiecseo.com/news/show_66177.html
Vue事件绑定原理