Vue 模版编译分析

81 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情

模版编译

Vue 实例在挂载之前,有相当多的工作时进行模版编译,将 template 进行编译,解析成 AST 树,在转换成 render 函数,有了 render 函数之后才会进入实例挂载流程。对于事件而言,我们经常会使用 v-on 或者 @ 在模版上绑定事件,所以对于事件的第一步处理,就是在编译阶段对事件指令进行收集

指令收集

先来看看 Vue 在模版中绑定事件的简单用法

<div id="app">
    <div v-on:click.stop="doThis">点击</div>
    <span>{{count}}</span>
</div>

var vm = new Vue({
  el: '#app',
  data() {
    return {
        count: 1
    }
  },
  methods: {
    doThis() {
        ++this.count
    }
  }
})

针对上面的例子,来看下模版编译的基本过程

const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  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
  }
})

在实例挂载中,已经对上面的代码进行了分析,而模版编译的过程就发生在 parse(template.trim(), options)parse 方法的实现比较复杂,里面分支众多,我们现在主要关注与事件有关的 processAttrs 方法。该方法会对 html 元素中的属性进行解析,其中就包括了 v-on 属性

var onRE = /^@|^v-on:/;
var dirRE = /^v-|^@|^:/;

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, name, value, modifiers, false, warn, list[i], isDynamic)
      } else { // normal directives
        
      }
    } else {
      
    }
  }
}

processAttrs 的逻辑较多,但相对容易理解,通过正则的方式, 拿到事件的类型、事件修饰符,并从属性列表中拿到事件回调。最终通过 addHandler 方法, 为 AST 树添加事件相关的属性,

function addHandler (
  el: ASTElement,
  name: string,
  value: string,
  modifiers: ?ASTModifiers,
  important?: boolean,
  warn?: ?Function,
  range?: Range,
  dynamic?: boolean
) {
  modifiers = modifiers || emptyObject
  // 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
    )
  }

  // 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) {
    if (dynamic) {
      name = `(${name})==='click'?'contextmenu':(${name})`
    } else if (name === 'click') {
      name = 'contextmenu'
      delete modifiers.right
    }
  } else if (modifiers.middle) {
    if (dynamic) {
      name = `(${name})==='click'?'mouseup':(${name})`
    } else if (name === 'click') {
      name = 'mouseup'
    }
  }

  // check capture modifier
  if (modifiers.capture) {
    delete modifiers.capture
    name = prependModifierMarker('!', name, dynamic)
  }
  if (modifiers.once) {
    delete modifiers.once
    name = prependModifierMarker('~', name, dynamic)
  }
  /* istanbul ignore if */
  if (modifiers.passive) {
    delete modifiers.passive
    name = prependModifierMarker('&', name, dynamic)
  }

  let events
  if (modifiers.native) {
    delete modifiers.native
    events = el.nativeEvents || (el.nativeEvents = {})
  } else {
    events = el.events || (el.events = {})
  }

  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
}

最终在 AST 中事件的表现形式如下

01-事件机制.png

代码生成

模版编译的最后一步是根据解析得到的 AST 树生成对应平台的渲染函数,也就是 render 函数,源码中调用的 generate 函数

const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 生成 render 函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
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
  }
}

generate 函数的核心处理逻辑在于 genElement 中, genElement 函数会根据不同指令类型处理不同的分支,对于普通模版的编译会进入 genData 函数中处理,我们暂时先只关注针对事件的处理逻辑,在生成 AST 树时, AST 树中多了 events 属性,因此 getHandlers 会对 events 属性进行处理

function genData (el: ASTElement, state: CodegenState): string {

  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false)},`
  }
  
  return data
}

getHandlers 会遍历解析好的 AST 树,拿到 events 对象属性,并根据属性上的属性对象拼接成字符串

function genHandlers (
  events: ASTElementHandlers,
  isNative: boolean
): string {
  const prefix = isNative ? 'nativeOn:' : 'on:'
  let staticHandlers = ``
  let dynamicHandlers = ``
  for (const name in events) {
    // 遍历 AST 树上的 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(){}'
  }

  // 事件绑定可以有多个,多个事件会在 AST 中以数组的形式存在,这里进行递归处理
  if (Array.isArray(handler)) {
    return `[${handler.map(handler => genHandler(handler)).join(',')}]`
  }

  // 事件的书写方式正则匹配
  const isMethodPath = simplePathRE.test(handler.value) // doThis   事件只想 methods 中的8属性
  const isFunctionExpression = fnExpRE.test(handler.value) // ()=>{} o 或者 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 {
    // 包含修饰符的场景
  }
}

上面代码中,三个正则匹配分别对应模版中事件的三种写法

    1. <div @click="test"></div
    1. <div @click="function(){}"></div> <div @click="()=>{}"></div>
    1. <div @click="test($event)"></div>

上述代码中,如果事件不带任何修饰符,并且满足正确的模版写法,则直接返回调用的事件名,如果不满足,则可能是 <div @click="console.log(11)"></div> , 写法,此时会封装到一个函数中。

对于包含修饰符的事件绑定,会通过事件修饰符获取到对应需要执行的脚本字符串,并添加到函数字符串中返回

let code = ''
let genModifierCode = ''
const keys = []
// 遍历 modifiers  上的修饰符
for (const key in handler.modifiers) {
  if (modifierCode[key]) {
    // 通过修饰符拿到需要执行的脚本字符串
    genModifierCode += modifierCode[key]
    // 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)
  }
}
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
/* istanbul ignore if */
if (__WEEX__ && handler.params) {
  return genWeexHandler(handler.params, code + handlerCode)
}
return `function($event){${code}${handlerCode}}`


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

在经过这一步骤之后,与事件相关的代码生成就分析完了