likes
comments
collection
share

[Vue 源码] v-model 逻辑分析

作者站长头像
站长
· 阅读数 39

v-model

v-model 和前面分析过的 v-on 一样,都是 Vue 提供的指令,所以 v-model 的分析流程和 v-on 相似。围绕模板的编译、 render 函数的生成,到最后的真实节点的挂载。 v-model 无论什么使用场景,本质上都是一个语法糖。

基础使用

v-model 和表单脱离不了关系,之所以视图能影响数据,本质上这个视图是可交互的,因此表单是实现这一交互的前提。表单的使用以 <input> <textarea> <select> 为核心,来看下具体的使用方式

// 普通输入框
<input type="text" v-model="value1">
// 多行文本框
<textarea v-model="value2" cols="30" rows="10"></textarea>
// 单选框
<div class="group">
  <input type="radio" value="one" v-model="value3"> one
  <input type="radio" value="two" v-model="value3"> two
</div> 

先来回顾一下模版到真实节点的过程。

    1. 模版解析成 AST
    1. AST 树生成可执行的 render 函数的生成
    1. render 函数转换成虚拟 DOM 对象
    1. 根据虚拟 DOM 对象生成真实 DOM 节点

模版解析

通过前面的分析已经知道,模版编译阶段,会调用 const ast = parse(template.trim(), options) 生成 AST 树, 而对于 v-model 的处理, 集中在 processAttrs 函数上。

processAttrs 的处理过程中,对模版的属性处理分成两部分,一部分是对普通 html 标签属性的处理,一部分是对 vue 指令的处理。而对于 vue 指令的处理中,又对 v-on v-bind 进行了特殊的处理,其他的 Vue 指令都会执行 addDirective 过程进行处理。

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)) {
      // 对 Vue 指令的处理
      // mark element as dynamic
      el.hasBindings = true
      // modifiers
      modifiers = parseModifiers(name.replace(dirRE, ''))
      
      if (bindRE.test(name)) { // v-bind
       // v-bind 指令处理 过程
      } else if (onRE.test(name)) { // v-on
        // v-on 指令处理过程
      } else { // normal directives
        // 对于非 v-bind v-on 的 vue 指令处理过程
        name = name.replace(dirRE, '')
        // parse arg
        const argMatch = name.match(argRE)
        let arg = argMatch && argMatch[1]
        isDynamic = false
        if (arg) {
          name = name.slice(0, -(arg.length + 1))
          if (dynamicArgRE.test(arg)) {
            arg = arg.slice(1, -1)
            isDynamic = true
          }
        }
        addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
        if (process.env.NODE_ENV !== 'production' && name === 'model') {
          checkForAliasModel(el, value)
        }
      }
    } else {
      // literal attribute
      // 普通 html 标签属性处理过程
    }
  }
}

在对事件机制的分析过程中,我们知道, Vuev-on 指令的处理是为 AST 树添加 events 属性,类似的,普通指令会在 AST 树上添加 directives 属性。

export function addDirective (
  el: ASTElement,
  name: string,
  rawName: string,
  value: string,
  arg: ?string,
  isDynamicArg: boolean,
  modifiers: ?ASTModifiers,
  range?: Range
) {
  (el.directives || (el.directives = [])).push(rangeSetItem({
    name,
    rawName,
    value,
    arg,
    isDynamicArg,
    modifiers
  }, range))
  el.plain = false
}

最终 AST 树上多了一个 directives 属性,如下图所示,其中 modifiers 代表模版中添加的修饰符,如 .lazy .number

[Vue 源码] v-model 逻辑分析

render 函数的生成

render 函数的生成阶段,也就是前面分析过的 genData 逻辑,其中 genData 会对模版的诸多属性进行处理,并返回最终拼接好的字符串模版,而对指令的处理会进入 genDirectives 流程

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

  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','
  // ... 
}

genDirectives 的逻辑并不复杂, 通过遍历 directives 数组,最终以 directives:[ 包裹的字符串返回

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
  for (i = 0, l = dirs.length; i < l; i++) {
    dir = dirs[i]
    needRuntime = true
    // 对 AST 树重新处理
    const gen: DirectiveFunction = state.directives[dir.name]
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      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.isDynamicArg ? dir.arg : `"${dir.arg}"`}` : ''
      }${
        dir.modifiers ? `,modifiers:${JSON.stringify(dir.modifiers)}` : ''
      }},`
    }
  }
  if (hasRuntime) {
    return res.slice(0, -1) + ']'
  }
}

genDirectives 函数中,会通过 state.directives[dir.name] 拿到对应指令的处理函数,而这些指令的处理函数针对不同的平台又有不同的实现。在编译过程中,通过偏函数的方式,分离了不同平台的不同编译过程,也为每一个平台每次提供相同的配置进行了选项合并,并进行了缓存。针对浏览器而言,有三个重要的指令选项

 var directives = {
  model: model$1,
  text: text,
  html: html
};

state.directives[dir.name] 也就是对应的 model 函数,来看下 model 函数的逻辑

function model$1 (
  el,
  dir,
  _warn
) {
  warn$2 = _warn;
  // 绑定的值
  var value = dir.value;
  var modifiers = dir.modifiers;
  var tag = el.tag;
  var type = el.attrsMap.type;

  {
    //  如果 input 元素的 type 是 file , 如果还使用 v-model 进行双向绑定则会发出警告
    if (tag === 'input' && type === 'file') {
      warn$2(
        "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
        "File inputs are read only. Use a v-on:change listener instead.",
        el.rawAttrsMap['v-model']
      );
    }
  }

  // 组件上的 v-model
  if (el.component) {
    genComponentModel(el, value, modifiers);
    // component v-model doesn't need extra runtime
    return false
  } else if (tag === 'select') {
    // select 表单
    genSelect(el, value, modifiers);
  } else if (tag === 'input' && type === 'checkbox') {
    // checkbox
    genCheckboxModel(el, value, modifiers);
  } else if (tag === 'input' && type === 'radio') {
    // radio
    genRadioModel(el, value, modifiers);
  } else if (tag === 'input' || tag === 'textarea') {
    // 普通的 input
    genDefaultModel(el, value, modifiers);
  } else {
    // 如果不是以上几种类型,则默认为组件上的双向绑定
    genComponentModel(el, value, modifiers);
    // component v-model doesn't need extra runtime
    return false
  }

  // ensure runtime directive metadata
  return true
}

显然,在 model 函数中会对 AST 树做进一步处理,我们知道表单有不同的类型,不同类型对应的事件响应机制也不同。因此需要针对不同的表单控件生成不同的 render 函数,这里我们重点分析 input 标签的处理, 也就是 getDefaultModel 方法。

function genDefaultModel (
  el,
  value,
  modifiers
) {
  var type = el.attrsMap.type;

  // 如果 v-bind 和 v-model 的值相同,则抛出错误
  {
    var value$1 = el.attrsMap['v-bind:value'] || el.attrsMap[':value'];
    var typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type'];
    if (value$1 && !typeBinding) {
      var binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value';
      warn$2(
        binding + "=\"" + value$1 + "\" conflicts with v-model on the same element " +
        'because the latter already expands to a value binding internally',
        el.rawAttrsMap[binding]
      );
    }
  }

  // 拿到 v-model 的修饰符
  var ref = modifiers || {};
  var lazy = ref.lazy;
  var number = ref.number;
  var trim = ref.trim;
  var needCompositionGuard = !lazy && type !== 'range';
  // lazy 修饰将触发同步的事件,从 input 改为 change
  var event = lazy
    ? 'change'
    : type === 'range'
      ? RANGE_TOKEN
      : 'input';

  var valueExpression = '$event.target.value';
  if (trim) {
    // 过滤输入的首尾空格
    valueExpression = "$event.target.value.trim()";
  }
  if (number) {
    // 将输入值转换成数字类型
    valueExpression = "_n(" + valueExpression + ")";
  }

  // 处理 v-model 的格式,允许使用如下格式 v-model=“a.b” v-mode="a[b]"
  var code = genAssignmentCode(value, valueExpression);
  if (needCompositionGuard) {
    // 确保不会在输入发组合文字过程中得到更新
    code = "if($event.target.composing)return;" + code;
  }

  // 添加 value
  addProp(el, 'value', ("(" + value + ")"));
  // 绑定事件
  addHandler(el, event, code, null, true);
  if (trim || number) {
    addHandler(el, 'blur', '$forceUpdate()');
  }
}


function genAssignmentCode (
  value,
  assignment
) {
  // 处理 v-model 的格式  v-model="a.b"  v-model="a[b]"
  var res = parseModel(value);
  if (res.key === null) {
    return (value + "=" + assignment)
  } else {
    return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")")
  }
}

getDefaultModel 的逻辑分为两部分,一部分是针对修饰符产生不同的事件处理字符串,而是为 v-model 产生的 AST 树添加属性和事件相关的属性,关键的两行代码就是

// 添加 value
addProp(el, 'value', ("(" + value + ")"));
// 绑定事件
addHandler(el, event, code, null, true);

回到 genData , 通过 genDirectives 处理之后,原先的 AST 新增了两个属性,因此在字符串处理过程中同样需要处理 propsevents 的分支, 最终 render 函数的结果为

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

生成真实 DOM

在生成真实 DOM 之前需要先生成虚拟 DOM , 生成虚拟 DOM 的过程和之前相同,没有特别的地方。有了虚拟 DOM 之后,就开始生成真实 DOM , 也就是 patchVnode ,其中关键是 createElm 方法,在前面的到的指令相关的信息会保存在 vnodedata 属性中,所以所属性的处理会走 invokeCreateHooks 逻辑

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
  // ....
  if (isDef(data)) {
    invokeCreateHooks(vnode, insertedVnodeQueue)
  }
}

invokeCreateHooks 会调用定义好的钩子函数,对 vnode 上定义的属性、指令、事件等进行真实 DOM 的处理,包括一下步骤(部分)

    1. updateDOMProps 会利用 vnode data 上的 domProps 更新 input 标签的 value
    1. updateAttrs 会利用 vnode data 上的 attrs 属性更新节点的属性值
    1. updateDOMListeners 利用 vnode data 上的 on 属性添加事件监听

因此 v-model 语法糖最终反应的结果,是通过监听表单控件自身的 input 事件(其他类型有不同的监听事件类型),去影响自身的 value 值。

组件使用 v-model

组件上使用 v-model 本质上是父子组件通信的语法糖。 先来看一个简单的例子

 var child = {
    template: '<div><input type="text" :value="value" @input="emitEvent">{{value}}</div>',
  methods: {
      emitEvent(e) {
        this.$emit('input', e.target.value)
      }
    },
    props: ['value']
  }
 new Vue({
   data() {
     return {
       message: 'test'
     }
   },
   components: {
     child
   },
   template: '<div id="app"><child v-model="message"></child></div>',
   el: '#app'
 })

父组件上使用 v-model ,子组件默认会利用名为 value 的 prop 和名为 input 的事件, AST 生成阶段和普通表单控件的区别在于,当遇到 child 是,由于不是普通的 html 标签,会执行 getComponentModel 的过程,而 getComponentModel 的结果在 AST 树上添加 model 属性。

export function genComponentModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const { number, trim } = modifiers || {}

  const baseValueExpression = '$$v'
  let valueExpression = baseValueExpression
  if (trim) {
    valueExpression =
      `(typeof ${baseValueExpression} === 'string'` +
      `? ${baseValueExpression}.trim()` +
      `: ${baseValueExpression})`
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`
  }
  const assignment = genAssignmentCode(value, valueExpression)

  // 在 AST 树上添加 model 属性,其中有 value 、 expression 、 callback 属性
  el.model = {
    value: `(${value})`,
    expression: JSON.stringify(value),
    callback: `function (${baseValueExpression}) {${assignment}}`
  }
}

经过对 AST 树的处理后,回到 genData 的流程,由于又了 model 属性,父组件拼接的字符串会做进一步的处理。

function genData (el: ASTElement, state: CodegenState): string {
  const dirs = genDirectives(el, state)
  if (dirs) data += dirs + ','

  // ...
  // v-model 组件的 render 函数处理
  if (el.component) {
    data += `tag:"${el.tag}",`
  }
}

因此,父组件最终的 render 函数表现为

"_c('child',{model:{value:(message),callback:function ($$v) {message=$$v},expression:"message"}})"

子组件的创建阶段赵丽会执行 createComponent , 其中对 model 的逻辑需要特别说明

function createComponent() {
  // transform component v-model data into props & events
  if (isDef(data.model)) {
    // 处理父组件的v-model指令对象
    transformModel(Ctor.options, data);
  }
}
function transformModel (options, data) {
  // prop默认取的是value,除非配置上有model的选项
  var prop = (options.model && options.model.prop) || 'value';
  // event默认取的是input,除非配置上有model的选项
  var event = (options.model && options.model.event) || 'input'
  // vnode上新增props的属性,值为value
  ;(data.attrs || (data.attrs = {}))[prop] = data.model.value;
  // vnode上新增on属性,标记事件
  var on = data.on || (data.on = {});
  var existing = on[event];
  var callback = data.model.callback;
  if (isDef(existing)) {
    if (
      Array.isArray(existing)
        ? existing.indexOf(callback) === -1
        : existing !== callback
    ) {
      on[event] = [callback].concat(existing);
    }
  } else {
    on[event] = callback;
  }
}

从 transformModel 的逻辑可以看出, 子组件的 vnode 会为 data.props 添加 data.model.value , 并且给 data.on 添加 data.model.callback 。

显然,这种写法就是时间通信的写法,这个过程有回到了对事件指令的分析过程。在组件上使用 v-mode 本质上还是一个父子组件通信的语法糖。

转载自:https://juejin.cn/post/7203640843562106937
评论
请登录