你知道vue中@click="onClick"和@click="onClick()"有什么区别么?

683 阅读2分钟

深挖日常遇到的每个问题,积跬步,至千里

最近在回顾react文档的时候发现react在事件这块着重强调了一下,在react的事件中,需要写函数表达式而不要写函数调用方式即:

<div onClick={onClick}></div>

众所周知哈,react内部自己有一套事件机制,所以我们必须写成官方推荐的这种形式。(如果大家有兴趣,后面可能也会涉及react源码的分析)但是vue中的事件似乎没有这种特殊说明,我们可以写成@click="onClick"@click="onClick()"都能顺利执行。

那么@click="onClick"@click="onClick()"到底有什么区别呢?按照国际惯例,我们就从源码入手,分析一下。

vue@2.6.10 版本web端为例

编译template

我们知道,我们在vue文件中写的template都是会先经过编译的,所以先找到最全的打包入口文件entry-runtime-with-compiler.js,只保留了部分需要理解的代码:

Vue.prototype.$mount = function() {
    if (template) {
        const { render, staticRenderFns } = compileToFunctions(template, {
            outputSourceRange: process.env.NODE_ENV !== 'production',
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
        }, this)
        options.render = render
        options.staticRenderFns = staticRenderFns
    }
}

我们可以看到,当$mount(el)时,如果template存在,则会进行编译然后生成render函数。然后再看看compileToFunctions里具体都做了什么:

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
    // 生成ast
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

很显然,generate这个函数应该和我们今天的问题有很大关系,再看下 generate这个函数做了什么

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}}`,
    staticRenderFns: state.staticRenderFns
  }
}
// 优化掉了部分代码
export function genElement (el: ASTElement, state: CodegenState): string {
    // component or element
    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
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  
}
// 优化掉了部分代码
export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  return data
}

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`)
}

export function genHandlers (
  events: ASTElementHandlers,
  isNative: boolean
): string {
  const prefix = isNative ? 'nativeOn:' : 'on:'
  let staticHandlers = ``
  let dynamicHandlers = ``
  for (const name in events) {
    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
  }
}
// 优化部分代码
function genHandler (handler: ASTElementHandler | Array<ASTElementHandler>): string {
  if (!handler) {
    return 'function(){}'
  }

  if (Array.isArray(handler)) {
    return `[${handler.map(handler => genHandler(handler)).join(',')}]`
  }

  const isMethodPath = simplePathRE.test(handler.value)
  const isFunctionExpression = fnExpRE.test(handler.value)
  const isFunctionInvocation = simplePathRE.test(handler.value.replace(fnInvokeRE, ''))

  if (!handler.modifiers) {
    if (isMethodPath || isFunctionExpression) {
      return 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) {
      if (modifierCode[key]) {
        genModifierCode += modifierCode[key]
        // left/right
        if (keyCodes[key]) {
          keys.push(key)
        }
      } else if (key === '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)
      }
    }
    if (keys.length) {
      code += genKeyFilter(keys)
    }
    // 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
    return `function($event){${code}${handlerCode}}`
  }
}

我们可以看到事件是在 genHandler这个函数中组装的,判断是否有modifierCode里定义的修饰符,如果有则增加修饰符定义的代码。如果是 isMethodPath@click="onClick"这种写法,则直接返回内容,如果是isFunctionInvocation@click="onClick()"这种写法,则返回function($event){ return ${handler.value} }。同理,如果有修饰符写法,则会将修饰符map中的特定代码添加到事件中,然后再根据我们写的事件格式进行返回。

下面是不带修饰符和带修饰符的几种不同事件写法的事件编译结果:

  • @click="onClick"
{on:{"click":onClick}
  • @click="onClick()"
{on: { "click": "function($event){return onClick()}" }}
  • @click.stop="onClick"
{ on: { "click": "function($event){$event.stopPropagation();return onClick($event)}" } }
  • @click.stop="onClick()"
{ on: { "click": "function($event){$event.stopPropagation();return onClick()}" } }

现在知道为什么当我们需要给事件手动传递参数时,如果想要使用该事件的默认事件参数,需要手动传递$event参数了吧!相信也对vue中的事件有了进一步了解~