vue 底层原理(5-7)

320 阅读11分钟

第五章 深入响应式原理(下).

5-10 原理图

【基于做个原理图vue响应式原理做到数据变化,自动刷新的一个超强能力。】 Snipaste_2021-05-23_21-12-35.png

5-11 props

在初始化 props 之前,首先会对 props 做一次 normalize,它发生在 mergeOptions 的时候,在 src/core/util/options.js 中

export function function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // ...
  normalizeProps(child, vm)
  // ...
}

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
   //走到数组
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        //转为驼峰匿名法
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
    //走到对象
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  //遍历完后赋值  转为对象的一个形式
  options.props = res
}

.如果 props 既不是数组也不是对象,就抛出一个警告

举个例子:

  1.数组
  props: ['name', 'nick-name']
  //经过 normalizeProps 后,会被规范成
  options.props = {
  name: { type: null },
  nickName: { type: null }
}
 2.对象
 props: {
    name: String,
    nickName: {
      type: Boolean
    }
  }
   //经过 normalizeProps 后,会被规范成
   options.props = {
  name: { type: String },
  nickName: { type: Boolean }
}

好处:做规范化让组件变得灵活,可将props写成各种形式数组和对象可以节省代码达到想实现的目的,不用写成规范化后的形式,Vue相当于提供了语法糖让你书写props 达成它想要的格式。

二.初始化

Props 的初始化主要发生在 new Vue 中的 initState 阶段,在 src/core/instance/state.js 中:

initProps 主要做 3 件事情:校验、响应式和代理。

1.校验 在 src/core/util/props.js 中 validateProp

export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  const prop = propOptions[key]
  //判断看看有没有传递对应key的值,没有返回undefined
  const absent = !hasOwn(propsData, key)
  //拿到value的值
  let value = propsData[key]
  // boolean casting
  //getTypeIndex 作比对 判断prop.type有没有Boolean布尔类型
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false
    } else if (value === '' || value === hyphenate(key)) {
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      const stringIndex = getTypeIndex(String, prop.type)
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true
      }
    }
  }
  // check default value
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    // since the default value is a fresh copy,
    // make sure to observe it.
    const prevShouldObserve = shouldObserve
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    // skip validation for weex recycle-list child component props
    !(__WEEX__ && isObject(value) && ('@binding' in value))
  ) {
   //校验 函数的目的是断言这个 prop 是否合法。
    assertProp(prop, key, value, vm, absent)
  }
  //校验完成 返回
  return value
}

检测如果 prop 没有定义 default 属性,那么返回 undefined,通过这块逻辑我们知道除了 Boolean 类型的数据,其余没有设置 default 属性的 prop 默认值都是 undefined。

接着是开发环境下对 prop 的默认值是否为对象或者数组类型的判断,如果是的话会报警告,因为对象和数组类型的 prop,他们的默认值必须要返回一个工厂函数。 已下例子为正确。

接下来的判断是如果上一次组件渲染父组件传递的 prop 的值是 undefined,则直接返回 上一次的默认值 vm._props[key],有构造函数返回构造函数 这样可以避免触发不必要的 watcher 的更新 优化避免不必要更新。

props: {
  
    nickName: {
      type: object,
      //有默认值
      default(){
      return {
         ball:"111",
         game:"222"
      }
      },
     //自定义校验器 父组件传递必须只能是已下值
     validator(value){
      return value === "male" || value === "female" 
     },
     //required 为ture属性但父组件没有传递这个 prop 数据的话会报一个警告。
     required :true
  }

assertProp 函数的目的是断言这个 prop 是否合法。 在 src/core/util/props.js 中 validateProp

首先判断如果 prop 定义了 required 属性但父组件没有传递这个 prop 数据的话会报一个警告。

接着判断如果 value 为空且 prop 没有定义 required 属性则直接返回。

function assertProp (
  prop: PropOptions,
  name: string,
  value: any,
  vm: ?Component,
  absent: boolean
) {
  if (prop.required && absent) {
    warn(
      'Missing required prop: "' + name + '"',
      vm
    )
    return
  }
  // 没有值,不校验
  if (value == null && !prop.required) {
    return
  }
  //获取类型
  let type = prop.type
  //初始值
  let valid = !type || type === true
  const expectedTypes = []
  if (type) {
  //判断是不是数组,不是数组就转换为数组
    if (!Array.isArray(type)) {
      type = [type]
    }
    //然后再去对 prop 的类型做校验,先是拿到 prop 中定义的类型 type,并尝试把它转成一个类型数组,
    然后依次遍历这个数组,执行 assertType(value, type[i]) 去获取断言的结果,
    直到遍历完成或者是 valid 为 true 的时候跳出循环。
    for (let i = 0; i < type.length && !valid; i++) {
    //执行 assertType(value, type[i]) 去获取断言的结果,
      const assertedType = assertType(value, type[i])
      expectedTypes.push(assertedType.expectedType || '')
      valid = assertedType.valid
    }
  }
  //如果valid为false 就提示报错它想要的类型
  if (!valid) {
    warn(
      getInvalidTypeMessage(name, value, expectedTypes),
      vm
    )
    return
  }
  //自定义校验器 父组件传递必须只能是已下值
  //最后判断当 prop 自己定义了 validator 自定义校验器,
  //则执行 validator 校验器方法,如果校验不通过则输出警告信息
  const validator = prop.validator
  if (validator) {
    if (!validator(value)) {
      warn(
        'Invalid prop: custom validator check failed for prop "' + name + '".',
        vm
      )
    }
  }
}

执行 assertType(value, type[i]) 去获取断言的结果 主要值和类型是否匹配

在 src/core/util/props.js 中 assertType

const simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/
function assertType (value: any, type: Function): {
  valid: boolean;
  expectedType: string;
} {
  let valid
  //获取的字符串
  const expectedType = getType(type)
  //判断是否满足simpleCheckRE生成表达式
  if (simpleCheckRE.test(expectedType)) {
    const t = typeof value
    valid = t === expectedType.toLowerCase()
    // for primitive wrapper objects
    if (!valid && t === 'object') {
      valid = value instanceof type
    }
  } else if (expectedType === 'Object') {
    valid = isPlainObject(value)
  } else if (expectedType === 'Array') {
    valid = Array.isArray(value)
  } else {
    valid = value instanceof type
  }
  return {
    valid,
    expectedType
  }
}

2.响应式

defineReactive 我们之前已经介绍过,这里要注意的是,在开发环境中我们会校验 prop 的 key 是否是 HTML 的保留属性,并且在 defineReactive 的时候会添加一个自定义 setter,当我们直接对 prop 赋值的时候会输出警告:

在 src/core/instance/state.js 中 initProps

if (process.env.NODE_ENV !== 'production') {
  const hyphenatedKey = hyphenate(key)
  if (isReservedAttribute(hyphenatedKey) ||
      config.isReservedAttr(hyphenatedKey)) {
    warn(
      `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
      vm
    )
  }
  defineReactive(props, key, value, () => {
    if (!isRoot && !isUpdatingChildComponent) {
      warn(
        `Avoid mutating a prop directly since the value will be ` +
        `overwritten whenever the parent component re-renders. ` +
        `Instead, use a data or computed property based on the prop's ` +
        `value. Prop being mutated: "${key}"`,
        vm
      )
    }
  })
}  else {
    defineReactive(props, key, value)
  }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
    //父组件 proxy
      proxy(vm, `_props`, key)
    }

3.代理

在 src/core/instance/state.js 中 proxy

在经过响应式处理后,我们会把 prop 的值添加到 vm._props 中,比如 key 为 name 的 prop,它的值保存在 vm._props.name 中,但是我们在组件中可以通过 this.name 访问到这个 prop,这就是代理做的事情。

onst sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

3.更新

在 src/core/vdom/patch.js 中:

首先,prop 数据的值变化在父组件,我们知道在父组件的 render 过程中会访问到这个 prop 数据,所以当 prop 数据变化一定会触发父组件的重新渲染,那么重新渲染是如何更新子组件对应的 prop 的值呢?

在父组件重新渲染的最后,会执行 patch 过程,进而执行 patchVnode 函数,patchVnode 通常是一个递归过程,当它遇到组件 vnode 的时候,会执行组件更新过程的 prepatch 钩子函数,

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // ...

  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }
  // ...
}

prepatch 函数定义在 src/core/vdom/create-component.js 中:

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
    child,
    options.propsData, // updated props
    options.listeners, // updated listeners
    vnode, // new parent vnode
    options.children // new children
  )
}

4.子组件重新渲染

其实子组件的重新渲染有 2 种情况,一个是 prop 值被修改,另一个是对象类型的 prop 内部属性的变化。

先来看一下 prop 值被修改的情况,当执行 props[key] = validateProp(key, propOptions, propsData, vm) 更新子组件 prop 的时候,会触发 prop 的 setter 过程,只要在渲染子组件的时候访问过这个 prop 值,那么根据响应式原理,就会触发子组件的重新渲染。

再来看一下当对象类型的 prop 的内部属性发生变化的时候,这个时候其实并没有触发子组件 prop 的更新。但是在子组件的渲染过程中,访问过这个对象 prop,所以这个对象 prop 在触发 getter 的时候会把子组件的 render watcher 收集到依赖中,然后当我们在父组件更新这个对象 prop 的某个属性的时候,会触发 setter 过程,也就会通知子组件 render watcher 的 update,进而触发子组件的重新渲染。

以上就是当父组件 props 更新,触发子组件重新渲染的 2 种情况。

第六章 编译(上).

一.编译入口(上)

之前我们分析过模板到真实 DOM 渲染的过程,中间有一个环节是把模板编译成 render 函数,这个过程我们把它称作编译。

虽然我们可以直接为组件编写 render 函数,但是编写 template 模板更加直观,也更符合我们的开发习惯。

Vue.js 提供了 2 个版本,一个是 Runtime + Compiler 的,一个是 Runtime only 的,前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,需要借助 webpack 的 vue-loader 事先把模板编译成 render函数。

这一章我们就来分析编译的过程,对编译过程的了解会让我们对 Vue 的指令、内置组件等有更好的理解。不过由于编译的过程是一个相对复杂的过程,我们只要求理解整体的流程、输入和输出即可,对于细节我们不必抠太细。有些细节比如对于 slot 的处理我们可以在之后去分析插槽实现的时候再详细分析。

一.编译入口(下) compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns,它的定义在 src/platforms/web/compiler/index.js 中:

import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

可以看到 compileToFunctions 方法实际上是 createCompiler 方法的返回值,该方法接收一个编译配置参数,接下来我们来看一下 createCompiler 方法的定义,在 src/compiler/index.js 中:

// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export 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
  }
})

createCompiler 方法实际上是通过调用 createCompilerCreator 方法返回的,该方法传入的参数是一个函数,真正的编译过程都在这个 baseCompile 函数里执行,那么 createCompilerCreator 又是什么呢,它的定义在 src/compiler/create-compiler.js 中:

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []
      finalOptions.warn = (msg, tip) => {
        (tip ? tips : errors).push(msg)
      }

      if (options) {
        // merge custom modules
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        // merge custom directives
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // copy other options
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      const compiled = baseCompile(template, finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        errors.push.apply(errors, detectErrors(compiled.ast))
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

可以看到该方法返回了一个 createCompiler 的函数,它接收一个 baseOptions 的参数,返回的是一个对象,包括 compile 方法属性和 compileToFunctions 属性,这个 compileToFunctions 对应的就是 $mount 函数调用的 compileToFunctions 方法,它是调用 createCompileToFunctionFn 方法的返回值,我们接下来看一下 createCompileToFunctionFn 方法,它的定义在 src/compiler/to-function/js 中:

export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an ' +
            'environment with Content Security Policy that prohibits unsafe-eval. ' +
            'The template compiler cannot work in this environment. Consider ' +
            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
            'templates into render functions.'
          )
        }
      }
    }

    // check cache
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // compile //编译流程
    const compiled = compile(template, options)

    // check compilation errors/tips
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        warn(
          `Error compiling template:\n\n${template}\n\n` +
          compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
          vm
        )
      }
      if (compiled.tips && compiled.tips.length) {
        compiled.tips.forEach(msg => tip(msg, vm))
      }
    }

    // turn code into functions
    const res = {}
    const fnGenErrors = []
    res.render = createFunction(compiled.render, fnGenErrors)
    //字符串转换动态方法
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\n\n` +
          fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
          vm
        )
      }
    }
    // 报错编译结果 走完一遍后面就不不需要在走一次编译流程了
    return (cache[key] = res)
  }
}

至此我们总算找到了 compileToFunctions 的最终定义,它接收 3 个参数、编译模板 template,编译配置 options 和 Vue 实例 vm。核心的编译过程就一行代码:

//编译流程
const compiled = compile(template, options)

compile 函数在执行 createCompileToFunctionFn 的时候作为参数传入,它是 createCompiler 函数中定义的 compile 函数,如下:

function compile (
  template: string,
  options?: CompilerOptions
): CompiledResult {
  const finalOptions = Object.create(baseOptions)
  const errors = []
  const tips = []
  //有问题遍历后会push到对应数组中,最后返回出去
  finalOptions.warn = (msg, tip) => {
    (tip ? tips : errors).push(msg)
  }

  if (options) {
    // merge custom modules
    if (options.modules) {
      finalOptions.modules =
        (baseOptions.modules || []).concat(options.modules)
    }
    // merge custom directives
    if (options.directives) {
      finalOptions.directives = extend(
        Object.create(baseOptions.directives || null),
        options.directives
      )
    }
    // copy other options
    for (const key in options) {
      if (key !== 'modules' && key !== 'directives') {
        finalOptions[key] = options[key]
      }
    }
  }
  //compile 函数执行的逻辑是先处理配置参数,真正执行编译过程就一行代码:
  const compiled = baseCompile(template, finalOptions)
  if (process.env.NODE_ENV !== 'production') {
    errors.push.apply(errors, detectErrors(compiled.ast))
  }
  compiled.errors = errors
  compiled.tips = tips
  return compiled
}

baseCompile 在执行 createCompilerCreator 方法时作为参数传入,如下:核心内容

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
 //解析模板字符串生成 AST
  const ast = parse(template.trim(), options)
  //优化语法树
  optimize(ast, options)
  //生成代码
  const code = generate(ast, options)
  //编译后的结果
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

二.parse

编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。

整体流程

首先来看一下 parse 的定义,在 src/compiler/parser/index.js 中:

//Html 转换AST
export function parse (
//模板中定义的字符串
  template: string,
  options: CompilerOptions
): ASTElement | void {
  getFnsAndConfigFromOptions(options)
//对于 template 模板的解析主要是通过 parseHTML 函数,它的定义在 src/compiler/parser/html-parser 中:
  parseHTML(template, {
    // options ...
    start (tag, attrs, unary) {
      let element = createASTElement(tag, attrs)
      processElement(element)
      treeManagement()
    },

    end () {
      treeManagement()
      closeElement()
    },

    chars (text: string) {
      handleText()
      createChildrenASTOfText()
    },
    comment (text: string) {
      createChildrenASTOfComment()
    }
  })
  return astRootElement
}

已下只拷贝部分代码 细节可以在界面中寻找

//html:模板
export function parseHTML (html, options) {
  let lastTag
  //html不断循环
  while (html) {
    if (!lastTag || !isPlainTextElement(lastTag)){
    //找到<括号代码中
      let textEnd = html.indexOf('<')
      if (textEnd === 0) {
      //注释节点的判断
         if(matchComment) {
         //在匹配的过程中会利用 advance 函数不断前进整个模板字符串,直到字符串末尾
           advance(commentLength)
           continue
         }
         if(matchDoctype) {
           advance(doctypeLength)
           continue
         }
         if(matchEndTag) {
           advance(endTagLength)
           parseEndTag()
           continue
         }
         if(matchStartTag) {
           parseStartTag()
           handleStartTag()
           continue
         }
      }
      handleText()
      advance(textLength)
    } else {
       handlePlainTextElement()
       parseEndTag()
    }
  }
}

由于 parseHTML 的逻辑也非常复杂,因此我也用了伪代码的方式表达,整体来说它的逻辑就是循环解析 template ,用正则做各种匹配,对于不同情况分别进行不同的处理,直到整个 template 被解析完毕。 在匹配的过程中会利用 advance 函数不断前进整个模板字符串,直到字符串末尾。最终html解析完毕

function advance (n) {
  index += n
  html = html.substring(n)
}

源码1png.png

匹配的过程中主要利用了正则表达式,如下:

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const ncname = '[a-zA-Z_][\\w\\-\\.]*'
const qnameCapture = `((?:${ncname}\\:)?${ncname})`
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const startTagClose = /^\s*(\/?)>/
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--/
const conditionalComment = /^<!\[/通过这些正则表达式,我们可以匹配注释节点、文档类型节点、开始闭合标签等。注释节点、文档类型节点对于注释节点和文档类型节点的匹配,如果匹配到我们仅仅做的是做前进即可。if (comment.test(html)) {  const commentEnd = html.indexOf('-->')  if (commentEnd >= 0) {    if (options.shouldKeepComment) {      options.comment(html.substring(4, commentEnd))    }    advance(commentEnd + 3)    continue  }}if (conditionalComment.test(html)) {  const conditionalEnd = html.indexOf(']>')  if (conditionalEnd >= 0) {    advance(conditionalEnd + 2)    continue  }}const doctypeMatch = html.match(doctype)if (doctypeMatch) {  advance(doctypeMatch[0].length)
  continue
}

对于注释和条件注释节点,前进至它们的末尾位置;对于文档类型节点,则前进它自身长度的距离。

开始标签

const startTagMatch = parseStartTag()
if (startTagMatch) {
  handleStartTag(startTagMatch)
  if (shouldIgnoreFirstNewline(lastTag, html)) {
    advance(1)
  }
  continue
}

首先通过 parseStartTag 解析开始标签:

function parseStartTag () {
// 前面<括号被截断了
  const start = html.match(startTagOpen)
  if (start) {
    const match = {
      //获取标签名
      tagName: start[1],
      attrs: [],
      start: index
    }
    //前进开始索引值 匹配属性用正则表达式
    advance(start[0].length)
    let end, attr
    // 正则表达式 attribute
    // 正则表达式 startTagClose  例:<br /> 一个/标签也称为一元标签
    while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
    //匹配完就前进到下个索引值
      advance(attr[0].length)
      match.attrs.push(attr)
    }
    //结束标签
    if (end) {
     // 一元标签会赋值unarySlash
      match.unarySlash = end[1]
      advance(end[0].length)
      match.end = index
      return match
    }
  }
}

对于开始标签,除了标签名之外,还有一些标签相关的属性。函数先通过正则表达式 startTagOpen 匹配到开始标签,然后定义了 match 对象,接着循环去匹配开始标签中的属性并添加到 match.attrs 中,直到匹配的开始标签的闭合符结束。如果匹配到闭合符,则获取一元斜线符,前进到闭合符尾,并把当前索引赋值给 match.end。

parseStartTag 对开始标签解析拿到 match 后,紧接着会执行 handleStartTag 对 match 做处理:

//获取到了前面闭合如<ul v-show="isShow" >
function handleStartTag (match) {
  const tagName = match.tagName
  const unarySlash = match.unarySlash
  //首先 true
  if (expectHTML) {
    if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
      parseEndTag(lastTag)
    }
    if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
      parseEndTag(tagName)
    }
  }
    //首先 false
  const unary = isUnaryTag(tagName) || !!unarySlash
  
  const l = match.attrs.length
  const attrs = new Array(l)
  for (let i = 0; i < l; i++) {
    const args = match.attrs[i]
    if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
      if (args[3] === '') { delete args[3] }
      if (args[4] === '') { delete args[4] }
      if (args[5] === '') { delete args[5] }
    }
    const value = args[3] || args[4] || args[5] || ''
    const shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
      ? options.shouldDecodeNewlinesForHref
      : options.shouldDecodeNewlines
  //获取到键和值 如[{name:":class",value:"111"}]
    attrs[i] = {
      name: args[1],
      value: decodeAttr(value, shouldDecodeNewlines)
    }
  }
  //最后判断如果非一元标签,则往 stack 里 push 一个对象,并且把 tagName 赋值给 lastTag
  if (!unary) {
    stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs })
    //相当于ul
    lastTag = tagName
  }
  
  if (options.start) {
  //重要 创建这个过程 AST
    options.start(tagName, attrs, unary, match.start, match.end)
  }
}

//闭合标签

const endTagMatch = html.match(endTag)
if (endTagMatch) {
  const curIndex = index
  advance(endTagMatch[0].length)
  parseEndTag(endTagMatch[1], curIndex, index)
  continue
}

//先通过正则 endTag 匹配到闭合标签,然后前进到闭合标签末尾,然后执行 parseEndTag 方法对闭合标签做解析。

function parseEndTag (tagName, start, end) {
  let pos, lowerCasedTagName
  if (start == null) start = index
  if (end == null) end = index
  
  if (tagName) {
    lowerCasedTagName = tagName.toLowerCase()
  }
  
  if (tagName) {
    for (pos = stack.length - 1; pos >= 0; pos--) {
      if (stack[pos].lowerCasedTag === lowerCasedTagName) {
        break
      }
    }
  } else {
    pos = 0
  }
  
  if (pos >= 0) {
    for (let i = stack.length - 1; i >= pos; i--) {
      if (process.env.NODE_ENV !== 'production' &&
        (i > pos || !tagName) &&
        options.warn
      ) {
        options.warn(
          `tag <${stack[i].tag}> has no matching end tag.`
        )
      }
      if (options.end) {
        options.end(stack[i].tag, start, end)
      }
    }
    stack.length = pos
    lastTag = pos && stack[pos - 1].tag
  } else if (lowerCasedTagName === 'br') {
    if (options.start) {
      options.start(tagName, [], true, start, end)
    }
  } else if (lowerCasedTagName === 'p') {
    if (options.start) {
      options.start(tagName, [], false, start, end)
    }
    if (options.end) {
      options.end(tagName, start, end)
    }
  }
}

parseEndTag 的核心逻辑很简单,在介绍之前我们回顾一下在执行 handleStartTag 的时候,对于非一元标签(有 endTag)我们都把它构造成一个对象压入到 stack 中.
那么对于闭合标签的解析,就是倒序 stack,找到第一个和当前 endTag 匹配的元素。如果是正常的标签匹配,那么 stack 的最后一个元素应该和当前的 endTag 匹配,但是考虑到如下错误情况:

<div><span></div>

这个时候当 endTag 为 </div> 的时候,从 stack 尾部找到的标签是 <span>,就不能匹配,因此这种情况会报警告。匹配后把栈到 pos 位置的都弹出,并从 stack 尾部拿到 lastTag。

最后调用了 options.end 回调函数,并传入一些参数,这个回调函数的作用稍后我会详细介绍。

let text, rest, next
if (textEnd >= 0) {
  rest = html.slice(textEnd)
  while (
    !endTag.test(rest) &&
    !startTagOpen.test(rest) &&
    !comment.test(rest) &&
    !conditionalComment.test(rest)
  ) {
    next = rest.indexOf('<', 1)
    if (next < 0) break
    textEnd += next
    rest = html.slice(textEnd)
  }
  text = html.substring(0, textEnd)
  advance(textEnd)
}

if (textEnd < 0) {
  text = html
  html = ''
}

if (options.chars && text) {
  options.chars(text)
}

接下来判断 textEnd 是否大于等于 0 的,满足则说明到从当前位置到 textEnd 位置都是文本,并且如果 < 是纯文本中的字符,
就继续找到真正的文本结束的位置,然后前进到结束的位置。

再继续判断 textEnd 小于 0 的情况,则说明整个 template 解析完毕了,把剩余的 html 都赋值给了 text。

最后调用了 options.chars 回调函数,并传 text 参数,这个回调函数的作用稍后我会详细介绍。

因此,在循环解析整个 template 的过程中,会根据不同的情况,去执行不同的回调函数,下面我们来看看这些回调函数的作用。


当解析到开始标签的时候,最后会执行 start 回调函数,函数主要就做 3 件事情,创建 AST 元素,处理 AST 元素,AST 树管理。下面我们来分别来看这几个过程。

创建 AST 元素

// check namespace.
// inherit parent ns if there is one
const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)

// handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
  attrs = guardIESVGBug(attrs)
}

let element: ASTElement = createASTElement(tag, attrs, currentParent)
if (ns) {
  element.ns = ns
}

export function createASTElement (
  tag: string,
  attrs: Array<Attr>,
  parent: ASTElement | void
): ASTElement {
  return {
    type: 1,
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    parent,
    children: []
  }
}

通过 createASTElement 方法去创建一个 AST 元素,并添加了 namespace。可以看到,每一个 AST 元素就是一个普通的 JavaScript 对象,其中,type 表示 AST 元素类型,tag 表示标签名,attrsList 表示属性列表,attrsMap 表示属性映射表,parent 表示父的 AST 元素,children 表示子 AST 元素集合。

处理 AST 元素

if (isForbiddenTag(element) && !isServerRendering()) {
  element.forbidden = true
  process.env.NODE_ENV !== 'production' && warn(
    'Templates should only be responsible for mapping the state to the ' +
    'UI. Avoid placing tags with side-effects in your templates, such as ' +
    `<${tag}>` + ', as they will not be parsed.'
  )
}

// apply pre-transforms
for (let i = 0; i < preTransforms.length; i++) {
  element = preTransforms[i](element, options) || element
}

if (!inVPre) {
  processPre(element)
  if (element.pre) {
    inVPre = true
  }
}
if (platformIsPreTag(element.tag)) {
  inPre = true
}
if (inVPre) {
  processRawAttrs(element)
} else if (!element.processed) {
  // structural directives
  processFor(element)
  processIf(element) //有进入后返回多了些字段在后面代码中会使用到
  processOnce(element)
  // element-scope stuff
  processElement(element, options)
}

首先是对模块 preTransforms 的调用,其实所有模块的 preTransforms、 transforms 和 postTransforms 的定义都在 src/platforms/web/compiler/modules 目录中,这部分我们暂时不会介绍,之后会结合具体的例子说。接着判断 element 是否包含各种指令通过 processXXX 做相应的处理,处理的结果就是扩展 AST 元素的属性。这里我并不会一一介绍所有的指令处理,而是结合我们当前的例子,我们来看一下 processFor 和 processIf:

export function processFor (el: ASTElement) {
  let exp
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    const res = parseFor(exp)
    if (res) {
      extend(el, res)
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        `Invalid v-for expression: ${exp}`
      )
    }
  }
}

export const forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/
export const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
export function parseFor (exp: string): ?ForParseResult {
//表达式做一个解析
  const inMatch = exp.match(forAliasRE)
  if (!inMatch) return
  const res = {}
  //inMatch 获取后面所以拿到值 data
  res.for = inMatch[2].trim()a
  const alias = inMatch[1].trim().replace(stripParensRE, '')
  const iteratorMatch = alias.match(forIteratorRE)
  if (iteratorMatch) {
    res.alias = alias.replace(forIteratorRE, '')
    res.iterator1 = iteratorMatch[1].trim()
    if (iteratorMatch[2]) {
      res.iterator2 = iteratorMatch[2].trim()
    }
  } else {
    res.alias = alias
  }
  return res
}

processFor 就是从元素中拿到 v-for 指令的内容,然后分别解析出 for、alias、iterator1、iterator2 等属性的值添加到 AST 的元素上。就我们的示例 v-for="(item,index) in data" 而言,解析出的的 for 是 data,alias 是 item,iterator1 是 index,没有 iterator2。

function processIf (el) {
//删除有v-if的
  const exp = getAndRemoveAttr(el, 'v-if')
  if (exp) {
    el.if = exp
    addIfCondition(el, {
      exp: exp,
      block: el
    })
  } else {
    if (getAndRemoveAttr(el, 'v-else') != null) {
      el.else = true
    }
    const elseif = getAndRemoveAttr(el, 'v-else-if')
    if (elseif) {
      el.elseif = elseif
    }
  }
}
export function addIfCondition (el: ASTElement, condition: ASTIfCondition) {
  if (!el.ifConditions) {
    el.ifConditions = []
  }
  el.ifConditions.push(condition)
}

processIf 就是从元素中拿 v-if 指令的内容,如果拿到则给 AST 元素添加 if 属性和 ifConditions 属性;否则尝试拿 v-else 指令及 v-else-if 指令的内容,如果拿到则给 AST 元素分别添加 else 和 elseif 属性。

AST 树管理

我们在处理开始标签的时候为每一个标签创建了一个 AST 元素,在不断解析模板创建 AST 元素的时候,我们也要为它们建立父子关系,就像 DOM 元素的父子关系那样。

AST 树管理相关代码如下:

function checkRootConstraints (el) {
  if (process.env.NODE_ENV !== 'production') {
    if (el.tag === 'slot' || el.tag === 'template') {
      warnOnce(
        `Cannot use <${el.tag}> as component root element because it may ` +
        'contain multiple nodes.'
      )
    }
    if (el.attrsMap.hasOwnProperty('v-for')) {
      warnOnce(
        'Cannot use v-for on stateful component root element because ' +
        'it renders multiple elements.'
      )
    }
  }
}


// tree management
//AST 树的一个根节点
if (!root) {
// root第一次没有进来element,赋值给了root
  root = element
  checkRootConstraints(root)
} else if (!stack.length) {
  // allow root elements with v-if, v-else-if and v-else
  if (root.if && (element.elseif || element.else)) {
    checkRootConstraints(element)
    addIfCondition(root, {
      exp: element.elseif,
      block: element
    })
  } else if (process.env.NODE_ENV !== 'production') {
    warnOnce(
      `Component template should contain exactly one root element. ` +
      `If you are using v-if on multiple elements, ` +
      `use v-else-if to chain them instead.`
    )
  }
}
// 第一次没有先跳过
if (currentParent && !element.forbidden) {
  if (element.elseif || element.else) {
    processIfConditions(element, currentParent)
  } else if (element.slotScope) { // scoped slot
    currentParent.plain = false
    const name = element.slotTarget || '"default"'
    ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element
  } else {
  // 第二次进入话到这里父push子节点
    currentParent.children.push(element)
    element.parent = currentParent
  }
}
// 第一次为false
if (!unary) {
  currentParent = element
  stack.push(element)
} else {
  closeElement(element)
}

AST 树管理的目标是构建一颗 AST 树,本质上它要维护 root 根节点和当前父节点 currentParent。为了保证元素可以正确闭合,这里也利用了 stack 栈的数据结构,和我们之前解析模板时用到的 stack 类似。

当我们在处理开始标签的时候,判断如果有 currentParent,会把当前 AST 元素 push 到 currentParent.chilldren 中,同时把 AST 元素的 parent 指向 currentParent。

接着就是更新 currentParent 和 stack ,判断当前如果不是一个一元标签,我们要把它生成的 AST 元素 push 到 stack 中,并且把当前的 AST 元素赋值给 currentParent。

stack 和 currentParent 除了在处理开始标签的时候会变化,在处理闭合标签的时候也会变化,因此整个 AST 树管理要结合闭合标签的处理逻辑看。

#处理闭合标签

end () {
  treeManagement()
  closeElement()
}

当解析到闭合标签的时候,最后会执行 end 回调函数:

// remove trailing whitespace
const element = stack[stack.length - 1]
const lastNode = element.children[element.children.length - 1]
if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {
  element.children.pop()
}
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)

首先处理了尾部空格的情况,然后把 stack 的元素弹一个出栈,并把 stack 最后一个元素赋值给 currentParent,这样就保证了当遇到闭合标签的时候,可以正确地更新 stack 的长度以及 currentParent 的值,这样就维护了整个 AST 树。

最后执行了 closeElement(element):

function closeElement (element) {
  // check pre state
  if (element.pre) {
    inVPre = false
  }
  if (platformIsPreTag(element.tag)) {
    inPre = false
  }
  // apply post-transforms
  for (let i = 0; i < postTransforms.length; i++) {
    postTransforms[i](element, options)
  }
}

closeElement 逻辑很简单,就是更新一下 inVPre 和 inPre 的状态,以及执行 postTransforms 函数,这些我们暂时都不必了解

处理文本内容 对应伪代码:

chars (text: string) {
  handleText()
  createChildrenASTOfText()
}

除了处理开始标签和闭合标签,我们还会在解析模板的过程中去处理一些文本内容:

const children = currentParent.children
text = inPre || text.trim()
  ? isTextTag(currentParent) ? text : decodeHTMLCached(text)
  // only preserve whitespace if its not right after a starting tag
  : preserveWhitespace && children.length ? ' ' : ''
if (text) {
  let res
  if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
  // 表达式的AST  前面伟元素AST
    children.push({
      type: 2,
      expression: res.expression,
      tokens: res.tokens,
      text
    })
  } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
   // 纯文本的AST 
    children.push({
      type: 3,
      text
    })
  }
}

文本构造的 AST 元素有 2 种类型,一种是有表达式的,type 为 2,一种是纯文本,type 为 3。在我们的例子中,文本就是 :,是个表达式,通过执行 parseText(text, delimiters) 对文本解析,它的定义在 src/compiler/parser/text-parser.js 中:

const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
const regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g

const buildRegex = cached(delimiters => {
  const open = delimiters[0].replace(regexEscapeRE, '\\$&')
  const close = delimiters[1].replace(regexEscapeRE, '\\$&')
  return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})

export function parseText (
  text: string,
  delimiters?: [string, string]
): TextParseResult | void {
  const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
  if (!tagRE.test(text)) {
    return
  }
  const tokens = []
  const rawTokens = []
  let lastIndex = tagRE.lastIndex = 0
  let match, index, tokenValue
  while ((match = tagRE.exec(text))) {
    index = match.index
    // push text token
    if (index > lastIndex) {
      rawTokens.push(tokenValue = text.slice(lastIndex, index))
      tokens.push(JSON.stringify(tokenValue))
    }
    // tag token
    const exp = parseFilters(match[1].trim())
    tokens.push(`_s(${exp})`)
    rawTokens.push({ '@binding': exp })
    lastIndex = index + match[0].length
  }
  if (lastIndex < text.length) {
    rawTokens.push(tokenValue = text.slice(lastIndex))
    tokens.push(JSON.stringify(tokenValue))
  }
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  }
}

parseText 首先根据分隔符(默认是 {{}})构造了文本匹配的正则表达式,然后再循环匹配文本,遇到普通文本就 push 到 rawTokens 和 tokens 中,如果是表达式就转换成 _s(${exp}) push 到 tokens 中,以及转换成 {@binding:exp} push 到 rawTokens 中。

对于我们的例子 :,tokens 就是 [_s(item),'":"',_s(index)];rawTokens 就是 [{'@binding':'item'},':',{'@binding':'index'}]。那么返回的对象如下:

return {
 expression: '_s(item)+":"+_s(index)',
 tokens: [{'@binding':'item'},':',{'@binding':'index'}]
}

2.png

那么至此,parse 的过程就分析完了,看似复杂,但我们可以抛开细节理清它的整体流程。parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。其实这里我觉得源码写的不够友好,这种是典型的魔术数字,如果转换成用常量表达会更利于源码阅读。

当 AST 树构造完毕,下一步就是 optimize 优化这颗树。

第七章 编译(下).

7-1 optimize

当我们的模板 template 经过 parse 过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化,optimize 的逻辑是远简单于 parse 的逻辑,所以理解起来会轻松很多。

为什么要有优化过程,因为我们知道 Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的比对。

来看一下 optimize 方法的定义,在 src/compiler/optimizer.js 中:

/**
 * Goal of the optimizer: walk the generated template AST tree
 * and detect sub-trees that are purely static, i.e. parts of
 * the DOM that never needs to change.
 *
 * Once we detect these sub-trees, we can:
 *
 * 1. Hoist them into constants, so that we no longer need to
 *    create fresh nodes for them on each re-render;
 * 2. Completely skip them in the patching process.
 */
export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  //标记静态节点
  markStatic(root)
  // second pass: mark static roots.
 // 标记静态根节点
  markStaticRoots(root, false)
}

function genStaticKeys (keys: string): Function {
  return makeMap(
    'type,tag,attrsList,attrsMap,plain,parent,children,attrs' +
    (keys ? ',' + keys : '')
  )
}

我们在编译阶段可以把一些 AST 节点优化成静态节点,所以整个 optimize 的过程实际上就干 2 件事情,markStatic(root) 标记静态节点 ,markStaticRoots(root, false) 标记静态根。

#标记静态节点

function markStatic (node: ASTNode) {
  node.static = isStatic(node)
  if (node.type === 1) {
    // do not make component slot content static. this avoids
    // 1. components not able to mutate slot nodes
    // 2. static slot content fails for hot-reloading
    if (
      !isPlatformReservedTag(node.tag) &&
      node.tag !== 'slot' &&
      node.attrsMap['inline-template'] == null
    ) {
      return
    }
    for (let i = 0, l = node.children.length; i < l; i++) {
      const child = node.children[i]
      markStatic(child)
      if (!child.static) {
        node.static = false
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        const block = node.ifConditions[i].block
        markStatic(block)
        if (!block.static) {
          node.static = false
        }
      }
    }
  }
}

function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // expression
    return false
  }
  if (node.type === 3) { // text
    return true
  }
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey)
  ))
}

首先执行 node.static = isStatic(node)

isStatic 是对一个 AST 元素节点是否是静态的判断,如果是表达式,就是非静态;如果是纯文本,就是静态;对于一个普通元素,如果有 pre 属性,那么它使用了 v-pre 指令,是静态,否则要同时满足以下条件:没有使用 v-if、v-for,没有使用其它指令(不包括 v-once),非内置组件,是平台保留的标签,非带有 v-for 的 template 标签的直接子节点,节点的所有属性的 key 都满足静态 key;这些都满足则这个 AST 节点是一个静态节点。

如果这个节点是一个普通元素,则遍历它的所有 children,递归执行 markStatic。因为所有的 elseif 和 else 节点都不在 children 中, 如果节点的 ifConditions 不为空,则遍历 ifConditions 拿到所有条件中的 block,也就是它们对应的 AST 节点,递归执行 markStatic。在这些递归过程中,一旦子节点有不是 static 的情况,则它的父节点的 static 均变成 false。

#标记静态根

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
    // For a node to qualify as a static root, it should have children that
    // are not just static text. Otherwise the cost of hoisting out will
    // outweigh the benefits and it's better off to just always render it fresh.
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true
      return
    } else {
      node.staticRoot = false
    }
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

markStaticRoots 第二个参数是 isInFor,对于已经是 static 的节点或者是 v-once 指令的节点,node.staticInFor = isInFor。 接着就是对于 staticRoot 的判断逻辑,从注释中我们可以看到,对于有资格成为 staticRoot 的节点,除了本身是一个静态节点外,必须满足拥有 children,并且 children 不能只是一个文本节点,不然的话把它标记成静态根节点的收益就很小了。

接下来和标记静态节点的逻辑一样,遍历 children 以及 ifConditions,递归执行 markStaticRoots。

总结

那么至此我们分析完了 optimize 的过程,就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM 永远不需要改变,这对运行时对模板的更新起到极大的优化作用。

我们通过 optimize 我们把整个 AST 树中的每一个 AST 元素节点标记了 static 和 staticRoot,它会影响我们接下来执行代码生成的过程。

7-3 codegen

编译的最后一步就是把优化后的 AST 树转换成可执行的代码,这部分内容也比较多,我并不打算把所有的细节都讲了,了解整体流程即可。部分细节我们会在之后的章节配合一个具体 case 去详细讲。

为了方便理解,我们还是用之前的例子:

<ul :class="bindCls" class="list" v-if="isShow">
    <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>

它经过编译,执行 const code = generate(ast, options),生成的 render 代码串如下:

with(this){
  return (isShow) ?
  // _c 就是执行 createElement 去创建 VNode
    // tag ul
    _c('ul', {
      //data 值
        staticClass: "list",
        class: bindCls
      },
      //_l 对应 renderList 渲染列表
      // children 后面都是子节点
      _l((data), function(item, index) {
        return _c('li', {
          on: {
            "click": function($event) {
              clickItem(index)
            }
          }
        },
        //_v 对应 createTextVNode 创建文本 VNode
        [_v(_s(item) + ":" + _s(index))])
      })
    ) :
    //_e 对于 createEmptyVNode创建空的 VNode。
    _e()
}

那么接下来我们就重点关注一下这个 render 代码串的生成过程

const code = generate(ast, options)

generate 函数的定义在 src/compiler/codegen/index.js 中:

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

generate 函数首先通过 genElement(ast, state) 生成 code,再把 code 用 with(this){return ${code}}} 包裹起来。这里的 state 是 CodegenState 的一个实例,稍后我们在用到它的时候会介绍它。先来看一下 genElement:

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      //genData 监听
      const data = el.plain ? undefined : genData(el, state)
      //genChildren 监听
      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      // tag
      code = `_c('${el.tag}'${
      //data
        data ? `,${data}` : '' // data
      }${
      //children
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}

基本就是判断当前 AST 元素节点的属性执行不同的代码生成函数,在我们的例子中,我们先了解一下 genFor 和 genIf。

#genIf

export function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}

function genIfConditions (
  conditions: ASTIfConditions,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  if (!conditions.length) {
    return altEmpty || '_e()'
  }

  const condition = conditions.shift()
  if (condition.exp) {
    return `(${condition.exp})?${
      genTernaryExp(condition.block)
    }:${
      genIfConditions(conditions, state, altGen, altEmpty)
    }`
  } else {
    return `${genTernaryExp(condition.block)}`
  }

  // v-if with v-once should generate code like (a)?_m(0):_m(1)
  function genTernaryExp (el) {
    return altGen
      ? altGen(el, state)
      : el.once
        ? genOnce(el, state)
        : genElement(el, state)
  }
}

genIf 主要是通过执行 genIfConditions,它是依次从 conditions 获取第一个 condition,然后通过对 condition.exp 去生成一段三元运算符的代码,: 后是递归调用 genIfConditions,这样如果有多个 conditions,就生成多层三元运算逻辑。这里我们暂时不考虑 v-once 的情况,所以 genTernaryExp 最终是调用了 genElement。

在我们的例子中,只有一个 condition,exp 为 isShow,因此生成如下伪代码:

return (isShow) ? genElement(el, state) : _e()

export function genFor (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altHelper?: string
): string {
  const exp = el.for
  const alias = el.alias
  const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
  const iterator2 = el.iterator2 ? `,${el.iterator2}` : ''

  if (process.env.NODE_ENV !== 'production' &&
    state.maybeComponent(el) &&
    el.tag !== 'slot' &&
    el.tag !== 'template' &&
    !el.key
  ) {
    state.warn(
      `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` +
      `v-for should have explicit keys. ` +
      `See https://vuejs.org/guide/list.html#key for more info.`,
      true /* tip */
    )
  }

  el.forProcessed = true // avoid recursion
  return `${altHelper || '_l'}((${exp}),` +
    `function(${alias}${iterator1}${iterator2}){` +
      `return ${(altGen || genElement)(el, state)}` +
    '})'
}

genFor 的逻辑很简单,首先 AST 元素节点中获取了和 for 相关的一些属性,然后返回了一个代码字符串。

在我们的例子中,exp 是 data,alias 是 item,iterator1 ,因此生成如下伪代码:

_l((data), function(item, index) {
  return genElememt(el, state)
})

genData & genChildren

再次回顾我们的例子,它的最外层是 ul,首先执行 genIf,它最终调用了 genElement(el, state) 去生成子节点,注意,这里的 el 仍然指向的是 ul 对应的 AST 节点,但是此时的 el.ifProcessed 为 true,所以命中最后一个 else 逻辑:

这里我们只关注 2 个逻辑,genData 和 genChildren

export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'

  // directives first.
  // directives may mutate the el's other properties before they are generated.
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

  // key
  if (el.key) {
    data += `key:${el.key},`
  }
  // ref
  if (el.ref) {
    data += `ref:${el.ref},`
  }
  if (el.refInFor) {
    data += `refInFor:true,`
  }
  // pre
  if (el.pre) {
    data += `pre:true,`
  }
  // record original tag name for components using "is" attribute
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
  // module data generation functions
  for (let i = 0; i < state.dataGenFns.length; i++) {
    data += state.dataGenFns[i](el)
  }
  // attributes
  if (el.attrs) {
    data += `attrs:{${genProps(el.attrs)}},`
  }
  // DOM props
  if (el.props) {
    data += `domProps:{${genProps(el.props)}},`
  }
  // event handlers
  if (el.events) {
    data += `${genHandlers(el.events, false, state.warn)},`
  }
  if (el.nativeEvents) {
    data += `${genHandlers(el.nativeEvents, true, state.warn)},`
  }
  // slot target
  // only for non-scoped slots
  if (el.slotTarget && !el.slotScope) {
    data += `slot:${el.slotTarget},`
  }
  // scoped slots
  if (el.scopedSlots) {
    data += `${genScopedSlots(el.scopedSlots, state)},`
  }
  // component v-model
  if (el.model) {
    data += `model:{value:${
      el.model.value
    },callback:${
      el.model.callback
    },expression:${
      el.model.expression
    }},`
  }
  // inline-template
  if (el.inlineTemplate) {
    const inlineTemplate = genInlineTemplate(el, state)
    if (inlineTemplate) {
      data += `${inlineTemplate},`
    }
  }
  data = data.replace(/,$/, '') + '}'
  // v-bind data wrap
  if (el.wrapData) {
    data = el.wrapData(data)
  }
  // v-on data wrap
  if (el.wrapListeners) {
    data = el.wrapListeners(data)
  }
  return data
}

genChildren 接下来我们再来看一下 genChildren,它的定义在 src/compiler/codegen/index.js 中:

export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  const children = el.children
  if (children.length) {
    const el: any = children[0]
    if (children.length === 1 &&
      el.for &&
      el.tag !== 'template' &&
      el.tag !== 'slot'
    ) {
      return (altGenElement || genElement)(el, state)
    }
    const normalizationType = checkSkip
      ? getNormalizationType(children, state.maybeComponent)
      : 0
    const gen = altGenNode || genNode
    return `[${children.map(c => gen(c, state)).join(',')}]${
      normalizationType ? `,${normalizationType}` : ''
    }`
  }
}

在我们的例子中,li AST 元素节点是 ul AST 元素节点的 children 之一,满足 (children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot') 条件,因此通过 genElement(el, state) 生成 li AST元素节点的代码,也就回到了我们之前调用 genFor 生成的代码,把它们拼在一起生成的伪代码如下

return (isShow) ?
    _c('ul', {
        staticClass: "list",
        class: bindCls
      },
      _l((data), function(item, index) {
        return genElememt(el, state)
      })
    ) : _e()

在我们的例子中,在执行 genElememt(el, state) 的时候,el 还是 li AST 元素节点,el.forProcessed 已为 true,所以会继续执行 genData 和 genChildren 的逻辑。由于 el.events 不为空,在执行 genData 的时候,会执行 如下逻辑:

if (el.events) {
  data += `${genHandlers(el.events, false, state.warn)},`
}

genHandlers 的定义在 src/compiler/codegen/events.js 中:

export function genHandlers (
  events: ASTElementHandlers,
  isNative: boolean,
  warn: Function
): string {
  let res = isNative ? 'nativeOn:{' : 'on:{'
  for (const name in events) {
    res += `"${name}":${genHandler(name, events[name])},`
  }
  return res.slice(0, -1) + '}'
}

genHandler 的逻辑就不介绍了,很大部分都是对修饰符 modifier 的处理,
感兴趣同学可以自己看,对于我们的例子,它最终 genData 生成的 data 字符串如下:

{
  on: {
    "click": function($event) {
      clickItem(index)
    }
  }
}

genChildren 的时候,会执行到如下逻辑:

export function genChildren (
  el: ASTElement,
  state: CodegenState,
  checkSkip?: boolean,
  altGenElement?: Function,
  altGenNode?: Function
): string | void {
  // ...
  const normalizationType = checkSkip
    ? getNormalizationType(children, state.maybeComponent)
    : 0
  const gen = altGenNode || genNode
  return `[${children.map(c => gen(c, state)).join(',')}]${
    normalizationType ? `,${normalizationType}` : ''
  }`
}

function genNode (node: ASTNode, state: CodegenState): string {
  if (node.type === 1) {
    return genElement(node, state)
  } if (node.type === 3 && node.isComment) {
    return genComment(node)
  } else {
    return genText(node)
  }
}

genChildren 的就是遍历 children,然后执行 genNode 方法,根据不同的 type 执行具体的方法。在我们的例子中,li AST 元素节点的 children 是 type 为 2 的表达式 AST 元素节点,那么会执行到 genText(node) 逻辑。

export function genText (text: ASTText | ASTExpression): string {
  return `_v(${text.type === 2
    ? text.expression
    : transformSpecialNewlines(JSON.stringify(text.text))
  })`
}

因此在我们的例子中,genChildren 生成的代码串如下:


[_v(_s(item) + ":" + _s(index))]

 return (isShow) ?
    _c('ul', {
        staticClass: "list",
        class: bindCls
      },
      _l((data), function(item, index) {
        return _c('li', {
          on: {
            "click": function($event) {
              clickItem(index)
            }
          }
        },
        [_v(_s(item) + ":" + _s(index))])
      })
    ) : _e()

总结

这一节通过例子配合解析,我们对从 ast -> code 这一步有了一些了解,编译后生成的代码就是在运行时执行的代码。由于 genCode 的内容有很多,所以我对大家的建议是没必要把所有的细节都一次性看完,我们应该根据具体一个 case,走完一条主线即可。

在之后的章节我们会对 slot 的实现做解析,我们会重新复习编译的章节,针对具体问题做具体分析,有利于我们排除干扰,对编译过程的学习有更深入的理解。