likes
comments
collection
share

Vue v-model 从源码角度解析双向绑定原理

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

关于Vue的双向绑定,是面试过程中经常遇到的一个问题,Vuev-model指令为什么能实现双向绑定,本小节我们来分析一下双向绑定的过程:

Vue双向绑定原理 v-model源码分析

下面这行代码是Vue官网上双向绑定的源代码,下面我们以这个例子来分析v-model的原理实现:

<input v-model="message" placeholder="edit me"> 
<p>Message is: {{ message }}</p>

同样我们从编译的过程开始分析(不了解编译过程可以点击这里)。

parse

编译阶段首先会扫描我们的节点开始标签,当节点中含有v-model指令,会执行processAttrs方法对指令进行解析生成AST节点:

export const onRE = /^@|^v-on:/
export const dirRE = /^v-|^@|^:/
export const bindRE = /^:|^v-bind:/
const argRE = /:(.*)$/

function processAttrs (el) {
  const list = el.attrsList
  let i, l, name, rawName, value, modifiers, isProp
  for (i = 0, l = list.length; i < l; i++) {
    name = rawName = list[i].name
    value = list[i].value
    // 匹配v-|@|:开头的标签,本案例为v-model
    if (dirRE.test(name)) { 
      // mark element as dynamic
      el.hasBindings = true
      // modifiers
      // 如果命令中有修饰符,解析修饰符,v-model指令是支持修饰符的,本案例中没有
      modifiers = parseModifiers(name) 
      if (modifiers) {
        // 如果有修饰符,去掉修饰符后缀
        name = name.replace(modifierRE, '')
      }
     
      if (bindRE.test(name)) { // v-bind 匹配:,本案例跳过
        ......
      } else if (onRE.test(name)) { // v-on 匹配@,本案例跳过
        ......
      } else { // normal directives // 普通指令,本案例匹配
        // 普通指令 去掉'v-', 这时候 name='model'
        name = name.replace(dirRE, '')
        ......
        
        // 执行addDirective,添加指令,参数 name:'model', rawName:'v-model', value:'message'
        addDirective(el, name, rawName, value, arg, modifiers)
        if (process.env.NODE_ENV !== 'production' && name === 'model') {
          // 检查是否与v-for语句一起使用,是的话会报警告
          checkForAliasModel(el, value)
        }
      }
    }
  }
}

processAttrs遇到v-model指令会解析出指令上的修饰符,并执行addDirective函数,给元素节点添加指令。

export function addDirective (
  el: ASTElement,
  name: string, // 'model'
  rawName: string, // 'v-model'
  value: string, // 'message'
  arg: ?string,
  modifiers: ?ASTModifiers
) {
  // 给当前的ASTElement节点添加directives属性数组,并将当前传入的指令参数作为对象push进去
  (el.directives || (el.directives = [])).push({ name, rawName, value, arg, modifiers })
  el.plain = false
}

addDirective函数就是在AST的节点上生成了directives属性数组,并将当前的指令参数存入这个数组。 到这里v-model指令编译的parse阶段就结束了。接下来看代码生成过程。

genCode

genCode的过程中会执行genData解析v-model指令生成代码:

export function genData (el: ASTElement, state: CodegenState): string {
  let data = '{'
  // 遇到指令会先解析,因为指令可能会对其他属性代码的生成有影响
  // directives first.
  // directives may mutate the el's other properties before they are generated.
  // 执行genDirectives函数
  const dirs = genDirectives(el, state)
  // 将指令解析生成的代码拼接返回
  if (dirs) data += dirs + ','
  ......
  
  return data
}

这个过程遇到directives,会使用genDirectives函数对其进行解析:

function genDirectives (el: ASTElement, state: CodegenState): string | void {
  // 拿到parse阶段生成的directives数组,这里面有一个v-modle指令对象
  const dirs = el.directives
  // 空数组则直接返回
  if (!dirs) return
  
  // 开始生成结果
  let res = 'directives:['
  
  // 设置一个hasRuntime变量为false
  let hasRuntime = false
  let i, l, dir, needRuntime
  
  for (i = 0, l = dirs.length; i < l; i++) {
    // 依次拿到directives数组的指令
    dir = dirs[i]
    needRuntime = true
    // state.directives[dir.name]是web平台上定义的一个方法,这儿是一个model函数,见下文
    const gen: DirectiveFunction = state.directives[dir.name]
    // 若果gen存在,执行gen,这儿是model函数
    if (gen) {
      // compile-time directive that manipulates AST.
      // returns true if it also needs a runtime counterpart.
      // 对于v-model而言,执行model函数,这儿最终返回true
      needRuntime = !!gen(el, dir, state.warn)
    }
    // 给每个指令构造一个对象
    if (needRuntime) {
      // hasRuntime赋值为true
      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) + ']'
  }
}

genDirectives会对所有的指令进行遍历,并执行指令函数对指令进行解析,解析完成后拼接字符串代码,最终返回代码串。这儿的核心函数是gen函数,本案例也就是model函数:

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

  // 如果是input标签且为file类型会报警告,file类型是只读属性
  if (process.env.NODE_ENV !== 'production') {
    // inputs with type="file" are read only and setting the input's
    // value will throw an error.
    if (tag === 'input' && type === 'file') {
      warn(
        `<${el.tag} v-model="${value}" type="file">:\n` +
        `File inputs are read only. Use a v-on:change listener instead.`
      )
    }
  }

  // 下面是针对不同的类型进行不同的解析,本案例的tag为'input',会进入genDefaultModel函数
  if (el.component) {
    // 动态组件,不满足
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  } else if (tag === 'select') {
    genSelect(el, value, modifiers)
  } else if (tag === 'input' && type === 'checkbox') {
    genCheckboxModel(el, value, modifiers)
  } else if (tag === 'input' && type === 'radio') {
    genRadioModel(el, value, modifiers)
  } else if (tag === 'input' || tag === 'textarea') {
    // 本案例进入这个逻辑,我们分析一下
    genDefaultModel(el, value, modifiers)
  } else if (!config.isReservedTag(tag)) {
    genComponentModel(el, value, modifiers)
    // component v-model doesn't need extra runtime
    return false
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `<${el.tag} v-model="${value}">: ` +
      `v-model is not supported on this element type. ` +
      'If you are working with contenteditable, it\'s recommended to ' +
      'wrap a library dedicated for that purpose inside a custom component.'
    )
  }

  // ensure runtime directive metadata
  // 返回true
  return true
}

model函数会针对不同的条件做不同的处理,本案例最终是执行genDefaultModel函数:

function genDefaultModel (
  el: ASTElement,
  value: string,
  modifiers: ?ASTModifiers
): ?boolean {
  const type = el.attrsMap.type

  // warn if v-bind:value conflicts with v-model
  // except for inputs with v-bind:type
  // 绑定了值与v-model冲突会报警告
  if (process.env.NODE_ENV !== 'production') {
    const value = el.attrsMap['v-bind:value'] || el.attrsMap[':value']
    const typeBinding = el.attrsMap['v-bind:type'] || el.attrsMap[':type']
    if (value && !typeBinding) {
      const binding = el.attrsMap['v-bind:value'] ? 'v-bind:value' : ':value'
      warn(
        `${binding}="${value}" conflicts with v-model on the same element ` +
        'because the latter already expands to a value binding internally'
      )
    }
  }

  // 修饰符解构,本案例没有修饰符
  const { lazy, number, trim } = modifiers || {}
  // 非lazy type!=='range',本案例 needCompositionGuard为true
  const needCompositionGuard = !lazy && type !== 'range'
  // 根据修饰符判断事件是什么,本案例没有修饰符,event为'input'
  const event = lazy
    ? 'change'
    : type === 'range'
      ? RANGE_TOKEN
      : 'input'

  // 定义代码表达式
  let valueExpression = '$event.target.value'
  // 根据不同修饰符,对代码表达式做不同的修改,本案例没有修饰符,不做处理
  if (trim) {
    valueExpression = `$event.target.value.trim()`
  }
  if (number) {
    valueExpression = `_n(${valueExpression})`
  }

  // 调用genAssignmentCode生成代码
  // 本案例最终的code为`message=$event.target.value`
  let code = genAssignmentCode(value, valueExpression)
  // 本案例needCompositionGuard为true,进入下面逻辑
  if (needCompositionGuard) {
    // 拼接code,结果为`if($event.target.composing)return;message=$event.target.value`
    code = `if($event.target.composing)return;${code}`
  }

  // 这儿就是v-model实现的本质,其实就是一个语法糖
  // 添加prop属性value,value为'message'
  addProp(el, 'value', `(${value})`)
  // 添加事件 event为'input',
  //code为`if($event.target.composing)return;message=$event.target.value`
  addHandler(el, event, code, null, true)
  if (trim || number) {
    addHandler(el, 'blur', '$forceUpdate()')
  }
}
export function genAssignmentCode (
  value: string, // 'message'
  assignment: string // '$event.target.value'
): string {
  const res = parseModel(value)
  if (res.key === null) {
    // 本案例返回结果`message=$event.target.value`
    return `${value}=${assignment}`
  } else {
    return `$set(${res.exp}, ${res.key}, ${assignment})`
  }
}

parseModel的作用就是将下面这些格式的value解析出来
/**
 * Parse a v-model expression into a base path and a final key segment.
 * Handles both dot-path and possible square brackets.
 *
 * Possible cases:
 *
 * - test
 * - test[key]
 * - test[test1[key]]
 * - test["a"][key]
 * - xxx.test[a[a].test1[key]]
 * - test.xxx.a["asa"][test1[key]]
 *
 */

到这里我们就清楚了,其实v-model指令在编译阶段,最终生成代码的时候会给当前的节点添加value属性和input事件,本案例在解析完v-model后,添加了value:messageinput:message=$event.target.value这样的命令。这就是双向绑定原理的本质。

那么有个问题:

我们使用v-model指令与使用:value="message";@input="message=$event.target.value"

这两者有什么不同吗?下一节我们来分析。