逐行分析v-model源码,助你工做面试排雷解难

欢迎你

文章写做: Markdown Nice
做者:wangly
发布地址:掘金,语雀
声明:转载注明做者以及地址
此次也时髦的对文章样式进行了一些更改。但愿你们可以喜欢绿绿的。javascript

哈喽,你们好呀。我是wangly。 一名一年经验的前端老倒霉蛋了,前两篇文章很是感谢你们的支持,为了感谢你们,此次给掘友们带来了一篇关于Vue中常用到 v-model 指令的源码分析,充分的给你们说说,碰到相似的面试题和工做上碰到的问题扫盲。但愿看完以后能对你有帮助。本篇文章须要有必定的基础,若是看不懂的话,反复品读,你会有一个成长。前端

为何要看源码?

不看源码,咱们只会知晓它表面的工做流程,而不知晓其内部的运转原理。就会有一种,知其然,而不知其因此然的感受。当某天面试官问你这个东西的时候,你只能回答出它的使用流程,而 get 不到深度,就会给人一种模棱两可的感受。千里执行,始于足下,跟着我一块儿探索它的奥秘吧。vue

劝退三连

  • v-model 作了什么?
  • v-model 在什么场景下能用,什么场景下不能用?
  • v-model 解决了什么问题?

开始发车咯

1.入口函数

v-model自己是一个指令语法糖,来为input 和 指定的变量作一个双向绑定的过程,下面咱们来看下model指令,它获得了什么东西。请看源码(这里使用打包后的代码, 更加清晰)java

// model 函数
function model ( el, dir, _warn ) {
  console.log(el)
  console.log(dir)
  console.log(_warn)
}
复制代码

打印结果以下web

打印出现的结果给各位截个图,其中:面试

  • el 为 ASTElement AST语法元素
  • dir 为 ASTDirection AST指令
  • _warn 为 一个警告函数

2.获取v-model元素须要用到的一些属性

下面的代码,主要是用来v-model绑定的元素获取一些基本信息。express

  • value: 绑定 data的属性名称。
  • modifiers: 修饰符对象,如 v-model.lazy="msg"的修饰符会生成一个对象, { lazy: true }表示 lazy修饰符存在。
  • tag: v-model 绑定的标签名称。
  • type: 元素的 attribute中的type类型
// 绑定`data`的属性名称
var value = dir.value;
// 修饰符列表
var modifiers = dir.modifiers; 
//标签名称, 
var tag = el.tag; 
// 元素的类型
var type = el.attrsMap.type; // 标签类型
console.log(value, modifiers, tag, type)
复制代码

3.当 input类型为file的时候

这里作了个判断,当input且类型是file文件的话,则抛出一个警告。用来警示开发者。数组

{
  // 类型为file的input是只读的,设置input的值可能会致使错误
  if (tag === 'input' && type === 'file') {
    warn$1(
      // error 信息
      "<" + (el.tag) + " v-model=\"" + value + "\" type=\"file\">:\n" +
      "File inputs are read only. Use a v-on:change listener instead.",
      el.rawAttrsMap['v-model']
    );
  }
}
复制代码

当咱们作一个file去使用v-model的时候,控制台就直接打印了一条错误。app

4.根据不一样形式,作不一样的处理

在Vue中,v-model先判断,当前元素是标签仍是组件,若是是组件,就调用genComponentModel来去处理这个问题。组件v-model额外运行时,就返回。先对组件判断,在而后对原生标签作处理。如inputselectcheckbox等标签的双向绑定。下面给你们整理一下对应的处理方式吧。我想拆开来你们都能看懂。编辑器

  • 组件: genComponentModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
  • select下拉选择框:getSelect( el: ASTElement, value: string, modifiers: ?ASTModifiers)
  • checkbox多选框: genCheckboxModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
  • Radio单选按钮:genRadioModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
  • input & textarea (默认Model处理):genDefaultModel( el: ASTElement, value: string, modifiers: ?ASTModifiers)
  • 绑定的元素不支持v-model,则会提示错误。v-model不支持该元素。以下图
// 判断 el 是不是组件
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') {
  // 处理单选
  genRadioModel(el, value, modifiers);
} else if (tag === 'input' || tag === 'textarea') {
  // 默认的输入框 针对于 输入框 和 多行输入框
  genDefaultModel(el, value, modifiers);
} else if (!config.isReservedTag(tag)) {
  // 不须要额外去额外运行时
  genComponentModel(el, value, modifiers);
  return false
} else {
  warn$1(
    // 若是不在处理范内,提示错误。v-model不支持该元素
    "<" + (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.',
    el.rawAttrsMap['v-model']
  );
}
复制代码

默认处理方式genDefaultModel

genDefaultModel 主要是用来处理基本文本框和多选文本框。 处理实例: genDefaultModel的第一句话,就是将elattributetype值。由于其中有一个新加入的range与其余的值是不同的。须要额外作出处理

var type = el.attrsMap.type;
复制代码

其次,须要判断v-bind:值与v-model是否冲突,若是冲突就会将错误添加到堆栈当中。因此咱们在控制台能够看到冲突的提示

// 若是v-bind:值与v-model冲突,则发出警告
// 除了带有v-bind的输入:type
{
  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$1(
      binding +
        '="' +
        value$1 +
        '" conflicts with v-model on the same element ' +
        'because the latter already expands to a value binding internally',
      el.rawAttrsMap[binding]
    )
  }
}
复制代码

其次,在获取当前修饰符的状态去生成表达式,下面对modifiers 进行获取,若是modifiersundefined的话,那么它就是一个空对象。

// 获取修饰符列表
var ref = modifiers || {}
// 懒加载修饰符
var lazy = ref.lazy
// 数字修饰符
var number = ref.number
// 空格过滤修饰符
var trim = ref.trim
// 在未打包下是这样的
const { lazy, number, trim } = modifiers || {}
复制代码

当获取到了修饰符的状态后,下一步开始生成event事件,由于其中有一些事件是vue本身定义的,好比:

// RANGE
export const RANGE_TOKEN = '__r'
// CHECK & RADIO
export const CHECKBOX_RADIO_TOKEN = '__c'
复制代码

经过event,生成code代码模板。这里会对修饰符进行一个断定。默认的eventinput,若是是lazy的话就使用change事件。不是的话对range作判断。若是type是range的话就使用RANGE_TOKEN反之则就是input了。当生成了事件名后,根据不一样的修饰符生成对应的value表现模板,经过genAssignmentCode方法,获取代码字符串。

// 非懒加载进度条时候
const needCompositionGuard = !lazy && type !== 'range'
// event 事件名称
const event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input'
// value模板。默认状况下,做为
let valueExpression = '$event.target.value'
if (trim) {
  // trim事件
  valueExpression = `$event.target.value.trim()`
}
if (number) {
  // _n($event.target.value)
  valueExpression = `_n(${valueExpression})`
}
// 获取code
let code = genAssignmentCode(value, valueExpression)
// 若是是range,那么须要对range的composing进行判断。
if (needCompositionGuard) {
  code = `if($event.target.composing)return;${code}`
}
复制代码

给出一个默认的实例,genAssignmentCode默认两个参数,value assignment,咱们能够看一下,它作了什么,有什么用?

// @ Function
export function genAssignmentCode ( value: string, assignment: string ): string {
  const res = parseModel(value)
![](https://imgkr.cn-bj.ufileos.com/783a46d8-0d4a-4ea7-9880-562b99f36f9d.png)
 if (res.key === null) { // value = xxxx return `${value}=${assignment}` } else { // $set()方式 return `$set(${res.exp}, ${res.key}, ${assignment})` } } 复制代码

genAssignmentCode方法中,调用了一个parseModel方法。它的做用主要是作一个解析的过程,这里就不去作介绍了。和JSON.parse做用相同。转换前,转换后:

  • 单独msg
  • 对象中的msg

根据上图,我想你已经知道它的做用了。没错。用来获取当前绑定的数据模型。对属性和对象属性的作一个区分。由于咱们都知道,对象属性更改有可能会丢失响应式,为了以防万一,因此才使用$set()的方式。到了这里,我想你也应该知道genAssignmentCode是用来干吗的吧?一句话总结:

若是是属性,就返回value = assignment,若是是对象属性,就使用set('导出模型的exp', '导出模型的key', assignment)的方式。

导出后的code,除了range须要经历过needCompositionGuard的过滤。为code添加$event.target.composing,这个实际上是对输入法IME问题的解决。防止非必要的软更新问题。

什么是IME问题:查看

code生成完毕后,那就开始对el进行改造,改造的过程分为两个方法addPropaddHeader。咱们分别来看看下它作了什么吧。

addProp

addProp 方法主要是对elprops的属性添加,来看一下,addProp作了什么吧。

function addProp (el, name, value, range, dynamic) {
    (el.props || (el.props = [])).push(rangeSetItem({ name: name, value: value, dynamic: dynamic }, range));
    el.plain = false;
  }
复制代码

能够看到,它主要就是给props添加一些属性。看下图能够看到,el中props中数据更换为了传递进来的参数了。

addHeadle

addHeadle主要是将上面生成的code模板,添加给元素的event事件。以下图,能够看出,el的event的下面的事件值作一个处理。这样在el中就会绑定一个事件。咱们能够当作以下DOM:

// 转换前 <input type="text" v-model="msg"> 复制代码// 转换后 <input type="text" :value="msg" @input="if(event.target.composing)return;message =event.target.value"> 复制代码

genSelect下拉选择框

经过上面的默认事件,我想你对v-model的大体流程有了基本的概念,那么就来聊一聊下拉选择框的问题吧。相对于input默认的流程,select的话就少了addProp,只有一个addHeader的方法。在一开始有一个selectVal保存默认的val。能够根据下面代码,看下转换前,转换后的代码

// 源码
var number = modifiers && modifiers.number
// 默认数据
var selectedVal =
  'Array.prototype.filter' +
  '.call($event.target.options,function(o){return o.selected})' +
  '.map(function(o){var val = "_value" in o ? o._value : o.value;' +
  'return ' +
  (number ? '_n(val)' : 'val') +
  '})'
 // 生成后的代码 Array.prototype.filter .call($event.target.options, function (o) { return o.selected }) .map(function (o) { var val = '_value' in o ? o._value : o.value return val })  复制代码

其次是assignment的代码模板,根据$event.target.multiple来去判断到底是$$selectedVal 仍是 $$selectedVal[0]

var assignment = '$event.target.multiple ? $selectedVal : $selectedVal[0]';
复制代码

最后就是生成code,而且将code和el的methods绑定。

var code = "var $$selectedVal = " + selectedVal + ";";
code = code + " " + (genAssignmentCode(value, assignment));
addHandler(el, 'change', code, null, true);
复制代码

这是最后生成绑定的code:

var $selectedVal = Array.prototype.filter
  .call($event.target.options, function (o) {
    return o.selected
  })
  .map(function (o) {
    var val = '_value' in o ? o._value : o.value
    return val
  })
msg = $event.target.multiple ? $selectedVal : $selectedVal[0]
复制代码

贴上genSelect的代码

function genSelect ( el: ASTElement, value: string, modifiers: ?ASTModifiers ) {
  // 获取numver指令
  const number = modifiers && modifiers.number
  // selectVal函数模板
  const selectedVal = `Array.prototype.filter` +
    `.call($event.target.options,function(o){return o.selected})` +
    `.map(function(o){var val = "_value" in o ? o._value : o.value;` +
    `return ${number ? '_n(val)' : 'val'}})`
 // assignment获取值 const assignment = '$event.target.multiple ? $selectedVal : $selectedVal[0]' // 生成code let code = `var $selectedVal = ${selectedVal};` code = `${code} ${genAssignmentCode(value, assignment)}` // 添加事件并将code模板加入进去 addHandler(el, 'change', code, null, true) } 复制代码

genCheckboxModel多选框

多选框的v-model 有了一个新的方法getBindingAttr ,那么这个方法是用来干什么的呢? 其实主要是用来处理v-bind的数据。经过getAndRemoveAttr来去数据对val进行处理,其中主要是对v-bind: + msg两种方式的数据处理,以下图: getAndRemoveAttr 只会从数组attrsList中删除attr,不会被processAttrs处理。随后将el.attrsMap[name]拿出来,

function getBindingAttr(el, name, getStatic) {
  // 获取绑定的value(动态的)
  var dynamicValue =
    getAndRemoveAttr(el, ':' + name) || getAndRemoveAttr(el, 'v-bind:' + name)
  // 根据value进行处理
  if (dynamicValue != null) {
    return parseFilters(dynamicValue)
  } else if (getStatic !== false) {
    var staticValue = getAndRemoveAttr(el, name)
    if (staticValue != null) {
      console.log(JSON.stringify(staticValue))
      return JSON.stringify(staticValue)
    }
  }
}
复制代码

随后就是添加Prop Handle的操做,这个参考上面的处理方式,作一些处理,处理后的event会有一个change事件,做为值修改的方法:

var $a = msg,
  $el = $event.target,
  $c = $el.checked ? true : false
if (Array.isArray($a)) {
  var $v = '1',
    $i = _i($a, $v)
  if ($el.checked) {
    $i < 0 && (msg = $a.concat([$v]))
  } else {
    $i > -1 && (msg = $a.slice(0, $i).concat($a.slice($i + 1)))
  }
} else {
  msg = $c
}
复制代码

添加props和handle的源码,参考上面的分析。这里就很少作赘述,只要知道,往prop添加了什么?handle的方法是什么?内容是什么?

addProp(
  el,
  'checked',
  'Array.isArray(' +
    value +
    ')' +
    '?_i(' +
    value +
    ',' +
    valueBinding +
    ')>-1' +
    (trueValueBinding === 'true'
      ? ':(' + value + ')'
      : ':_q(' + value + ',' + trueValueBinding + ')')
)
addHandler(
  el,
  'change',
  'var $a=' +
    value +
    ',' +
    '$el=$event.target,' +
    '$c=$el.checked?(' +
    trueValueBinding +
    '):(' +
    falseValueBinding +
    ');' +
    'if(Array.isArray($a)){' +
    'var $v=' +
    (number ? '_n(' + valueBinding + ')' : valueBinding) +
    ',' +
    '$i=_i($a,$v);' +
    'if($el.checked){$i<0&&(' +
    genAssignmentCode(value, '$a.concat([$v])') +
    ')}' +
    'else{$i>-1&&(' +
    genAssignmentCode(value, '$a.slice(0,$i).concat($a.slice($i+1))') +
    ')}' +
    '}else{' +
    genAssignmentCode(value, '$c') +
    '}',
  null,
  true
)
复制代码

genRadioModel单选框

处理单选按钮的v-model就没有那么多的花花肠子,若是理解了上面checkbox和input的解析,对于radio,就是获取bangdingvalue。随后作修饰符的处理。而后按照套路通常添加Prop事件handle

function genRadioModel(el, value, modifiers) {
  // 获取修饰符
  var number = modifiers && modifiers.number
  // 绑定的value值
  var valueBinding = getBindingAttr(el, 'value') || 'null'
  // number修饰符和非number修饰符下的区别.生成value处理方式
  valueBinding = number ? '_n(' + valueBinding + ')' : valueBinding
  // 添加prop
  addProp(el, 'checked', '_q(' + value + ',' + valueBinding + ')')
  // 添加事件
  addHandler(el, 'change', genAssignmentCode(value, valueBinding), null, true)
}
复制代码

genComponentModel组件

最后一个就是组件的v-model的绑定的了。首先,须要知道如何实现组件的v-model,这里给一个基本的demo。 点击后:

<div id="app">
  <my-component v-model="title"></my-component>
</div>
<script src="./dist/vue.js"></script>
<script> Vue.component('my-component', { template: `<div> {{value}} <button @click="handleInput">提交input</button> </div>`, props: ['value'], methods: { handleInput() { this.$emit('input', '我触发了input emit'); //触发 input 事件,并传入新值 } } }); new Vue({ el: '#app', data: { title: '我是默认' } }) </script>
复制代码

能够看到,当在组件中定义prop存在value的时候,将修改的值经过$emit发布input事件发布。从而能够经过v-model来作一个双向绑定。那么咱们探究下组件内的v-model作了一些什么吧。

// 解构指令
const { number, trim } = modifiers || {}
 // 基本value模板 const baseValueExpression = '$v' let valueExpression = baseValueExpression // trim下的模板语法 if (trim) { valueExpression = `(typeof ${baseValueExpression} === 'string'` + `? ${baseValueExpression}.trim()` + `: ${baseValueExpression})` } // number下的模板,执行了_n的代理方法toNumber if (number) { valueExpression = `_n(${valueExpression})` } // 获取code模板 const assignment = genAssignmentCode(value, valueExpression) // 对el的model进行修改 el.model = { value: `(${value})`, expression: JSON.stringify(value), callback: `function (${baseValueExpression}) {${assignment}}`, } 复制代码

其大部分都是在渲染code模板,为下面lemodel的作准备。组件和元素标签不同,因此组件的模板就没有addProps, addHanndle这两个步骤。取而代之的是是对el.model的修改。

尾篇

本文所述的v-model只是单独的源码分析,其实不少内容在渲染后的模板仍是要从一开始开始,模板的渲染,若是不看其余的源码压根就不明白,举个例子: number修饰符下都会给默认的valueExpression添加一个_n()其实就是一个函数,那么这个函数是干什么的?

valueExpression = "_n(" + valueExpression + ")";
复制代码

咱们能够看到这个方法,其中_n指向了toNumber

function installRenderHelpers(target) {
  target._o = markOnce
  // _n
  target._n = toNumber
  ......
}
复制代码

toNumber只是作一个很简单的事情,将传入的字符串转换为Int也就是number,若是转换失败就返回原来的字符串。

function toNumber (val) {
  var n = parseFloat(val);
  return isNaN(n) ? val : n
}
复制代码

总结

vue的源码很长,很晦涩。不少人只是看了一些免费视频的分析,如:xxxxVue源码解析。其实内容无非就是讲了一些vue响应式MVVM浅显的概念,就以为vue不过如此。却不知,只是夜郎自大。精心啃读vue的源码,会对工做中使用vue出现的一些问题。快速的找到解决方案。本篇文章只是对v-model的简单的理解。若是面试官问到你,若是你看完,说不定可以吹半小时呢。固然,具体深刻,还须要去理解渲染的模板具体作了什么。原本是准备通篇详解。后面发现这样写的话就脱离了本文的范畴。属于离题,超纲。 因此,若是你以为技术停滞不前,不妨将vue反复细品。 若是对你有帮助能够评论 点赞 收藏三连。

有意换坑:
学历:专科
经验: 一年
目标地: 上海杭州深圳广州 薪资: 8K ~ 12K 欢迎远程boss拉我上岸。非外包。

相关文章
相关标签/搜索