[Vue源码]当我们谈及v-model,我们在讨论什么?

748 阅读4分钟

前言

当提及到v-model时,我们都会想到它时一个语法糖,相当于在使用该指令的元素或者组件上绑定了props中的value值以及注册了input事件。但其实深入挖掘其中,可以学习到更多的东西,下面就看一下相关的Vue源码吧。

从编译出发

以下面的例子出发分析:

new Vue({
    el:'#app',
    template:'<input v-model="value">'
})

在初始化时,Vue构造函数会执行this._init,然后this._init内部通过vm.$mount()调用Vue.prototype.$mount的方法。Vue.prototype.$mount的方法会因Vue构建版本而不同。

Vue构建版本分完整版运行时的版本完整版包括编译器运行时的版本。如果需要在代码编译模板 (比如传入一个字符串给 template 选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就将需要加上编译器,即完整版:

// 需要编译器
new Vue({
  template: '<div>{{ hi }}</div>'
})

// 不需要编译器
new Vue({
  render (h) {
    return h('div', this.hi)
  }
})

对于我们现在例子的情况,需要用到完整版Vue.prototype.$mount(定义在src\platforms\web\entry-runtime-with-compiler.js中)来作分析,当render未定义但template已定义时,会通过compileToFunction解析template以获取render,其内部会调用baseCompile作为主要的解析函数,如下所示:

function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 1.把模板转换成ast抽象语法树
  // 抽象语法树,用来以树形的方式描述代码结构
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 2.优化抽象语法树
    optimize(ast, options)
  }
  // 3.把抽象语法树生成字符串形式的js代码
  const code = generate(ast, options)
  return {
    ast,
    // 渲染函数
    render: code.render,
    // 静态渲染函数,生成静态VNode树
    staticRenderFns: code.staticRenderFns
  }
}

至此,我们可以知道编译的三个步骤:

  • parse : 解析模板字符串生成 AST语法树
  • optimize : 优化语法树,主要时标记静态节点,提高更新页面的性能
  • codegen : 生成js代码,主要是render函数和staticRenderFns函数

我们接着开头的例子去代入到这些步骤中:

parse

parse过程中,会对模板使用大量的正则表达式去进行解析。开头的例子会被解析成以下AST节点:

ast={
    'type': 1, // type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本
    'tag': 'input',
    'directives': [
        {
            'name': 'model',
            'value': 'value',
            'modifier': undefined
        }
    ],
    attrsMap:{
        'v-model':'value'
    },
    // ...还有很多属性省略...
}

optimize过程在此不做分析,因为本例子没有静态节点

codegen

首先,查看codegen过程中const code = generate(ast, options)中,generate源码如下所示:

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  /** CodegenState构造函数是根据options生成一个处理AST语法树的代码生成器
  * CodegenState类的构造函数中有以下逻辑
  * class CodegenState{
      constructor(){
          this.directives = extend(extend({}, baseDirectives), options.directives)
      }
  * }
  * 其中baseDirectives用于处理v-on,v-bind,v-cloak指令
  * options为编译相关的配置,不同的平台下option不同,
  * 在web环境下,option会来自src\platforms\web\compiler\options.js,
  * 而option['directives']则引用自src\platforms\web\compiler\directives\index.js
  * 里面包括对v-model,v-text,v-html指令的处理
  */
  const state = new CodegenState(options)
  /**
  * 
  */
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

genElement方法中有大量处理当前AST节点属性的逻辑,根据节点属性生成各种生成函数,在我们的例子中,只需要执行里面的genData,而genData中针对el.directives会调用genDirectives处理。genDirectives源码如下所示:

function genDirectives (el: ASTElement, state: CodegenState): string | void {
  const dirs = el.directives
  if (!dirs) return
  let res = 'directives:['
  let hasRuntime = false
  let i, l, dir, needRuntime
  // 遍历el.directives
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i]
    needRuntime = true
    // 调用CodegenState实例的directives对应的指令处理函数进行处理
    const gen: DirectiveFunction = state.directives[dir.name]
    if (gen) {
      needRuntime = !!gen(el, dir, state.warn)
    }
    if (needRuntime) {
      hasRuntime = true
      res += `{name:"${dir.name}",rawName:"${dir.rawName}"${
        dir.value ? `,value:(${dir.value}),expression:${JSON.stringify(dir.value)}` : ''
      }${
        dir.arg ? `,arg:"${dir.arg}"` : ''
      }${
        dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
      }},`
    }
  }
  if (hasRuntime) {
    return res.slice(0, -1) + ']'
  }
}

现在可以知道,处理v-modelCodegenState实例中的directives['model'],接下来我们看一下其相关代码:

rc\platforms\web\compiler\directives\model.js

export default function model (
  el: ASTElement,
  dir: ASTDirective,
  _warn: Function
): ?boolean {
  warn = _warn
  const value = dir.value
  const modifiers = dir.modifiers
  const tag = el.tag
  const type = el.attrsMap.type
  // 以分情况来处理v-model
  // 1. 如果节点是组件实例
  if (el.component) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  // 2. 如果节点的html标签为select
  } else if (tag === 'select') {
    genSelect(el, value, modifiers)
  // 3. 如果节点的html标签为input且type属性为'checkbox'
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers)
  // 4. 如果节点的html标签为input且type属性为'radio'
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers)
  // 5. 如果节点的html标签为input且type属性为'textarea'
  } else if (tag === 'input' || tag === 'textarea') {
    genDefaultModel(el, value, modifiers)
  // 6. 检查标记是否被保留,以便它不能被注册为组件。这是依赖于平台的,在web端此值恒为false
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  } 
  // ensure runtime directive metadata
  return true
}

继续以我们的例子做分析,此时执行genDefaultModel函数,该函数源码如下:

function genDefaultModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const type = el.attrsMap.type
  const { lazy, number, trim } = modifiers || {}
  const needCompositionGuard = !lazy && type !== 'range'
  // 如果v-model.lazy则选择监听change事件,
  // input事件的触发机制是,当输入框内容发生变化时
  // change事件的触发机制是,当输入框失去焦点后且输入框内容发生变化时
  const event = lazy
    ? 'change'
    : type === 'range'
      ? RANGE_TOKEN
      : 'input'
  // valueExpression是用于event事件触发后,把valueExpression赋值到v-model绑定的值上,也就是value
  let valueExpression = '$event.target.value'
  // 如果v-model.trim,则事件触发把event.target.value赋值前调用trim()
  if (trim) {
    valueExpression = `$event.target.value.trim()`
  }
  // 如果v-model.number,则事件触发把event.target.value赋值前先调用parseFloat转换
  // target._n = toNumber;
  // function toNumber (val) {
  //   var n = parseFloat(val);
  //   return isNaN(n) ? val : n
  // }
  if (number) {
    valueExpression = `_n(${valueExpression})`
  }
  
  /**
  * genAssignmentCode (value: string,assignment: string): string {
      const res = parseModel(value)
      // 如果v-model="value"
      if (res.key === null) {
        return `${value}=${assignment}`
      // 如果v-model="value",即绑定的是一个对象中的属性
      } else {
        return `$set(${res.exp}, ${res.key}, ${assignment})`
      }
    }
  */
  let code = genAssignmentCode(value, valueExpression)
  if (needCompositionGuard) {
    code = `if($event.target.composing)return;${code}`
  }
  // 添加到prop中
  addProp(el, 'value', `(${value})`)
  // 添加到on事件中
  addHandler(el, event, code, null, true)
  // 以上两个处理过程相当于把节点处理为<input :value="value" @input="value=$event.target.value">
  
  if (trim || number) {
    addHandler(el, 'blur', '$forceUpdate()')
  }
}

以上针对lazy,number,trim修饰符的处理在其余五种情况下也几乎一样。

genDirectivesmodel函数执行完后,接下来的操作会生成directives数据。directives数据将会作为生成VNode需要的参数传入到render函数内部调用的createElement函数中。

直至codegen过程结束,生成的render代码如下所示:

function render() {
  with(this) {
    return _c('input', {
      directives: [{
        name: "model",
        rawName: "v-model",
        value: (value),
        expression: "value"
      }],
      domProps: {
        "value": (value)
      },
      on: {
        "input": function ($event) {
          if ($event.target.composing) return;
          value = $event.target.value
        }
      }
    })
  }
}

model函数中的多种情况

这里我直接说一下其他情况下,v-bindv-on会对应哪些值和事件,这些情况对应的函数的源码我就不做分析了,基本和genDefaultModel都大同小异。

1. 如果节点是组件实例

此时,v-model会转换成绑定(v-bind)value值和监听(v-on)input事件。另外,如果在组件里定义了model属性,则会绑定和监听其中设置的值,例如:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
      // ...省略
  `
})

此时该组件以<base-checkbox v-model="lovingVue"></base-checkbox>的方式调用时,v-model会转换成绑定checked值和监听change事件。

2. 如果节点为普通元素

节点类型绑定值(v-bind)监听事件(v-on)
input[type="radio"]checkedchange
selectvaluechange
input[type="checkbox"]checkedchange
inputvalueinput
textareavalueinput

后记

支支吾吾想了很久,后记还是写 "希望大家多多支持" 吧。