v-model 使用场景和源码学习
栏目: JavaScript · 发布时间: 5年前
内容简介:原理:通过使用v-model指令在元素的输入元素上创建双向数据绑定,它会根据控件类型自动选取正确的方法来更新元素。v-model 本质上不过是语法糖。它负责监听用户的输入事件以更新数据。 当input输入框值改变/复选框值改变时,text/checkbox值也会同时改变,它负责监听用户的输入事件以更新数据。 radio和checkbox用法当我们选中时,会用之前设定好的值对于input,select,textarea原生都有input事件,值更改时,input 事件会同步触发。
<input v-model="text" type="text"> <input v-model="checkbox" type="checkbox"> data() { return { text: '', checkbox: true } } 复制代码
原理:
通过使用v-model指令在元素的输入元素上创建双向数据绑定,它会根据控件类型自动选取正确的方法来更新元素。v-model 本质上不过是语法糖。它负责监听用户的输入事件以更新数据。 当input输入框值改变/复选框值改变时,text/checkbox值也会同时改变,它负责监听用户的输入事件以更新数据。 radio和checkbox用法当我们选中时,会用之前设定好的值
<input type="radio" v-model="radio" value="radio"> {{radio}} 复制代码
<input v-model="checkbox" type="checkbox" true-value="yes" false-value="no"> {{checkbox}} 复制代码
input事件
对于input,select,textarea原生都有input事件,值更改时,input 事件会同步触发。
methods: { // event是原生dom事件 onValueInput(event) { // srcElement Event.target 属性用来区分元素的 console.log(`${event.srcElement.type}变成了${event.target.value}`) } } 复制代码
修饰符
.lazy 取代 input 监听 change 事件因为change事件触发的条件是值改变失去焦点时触发,而input是实时,加上lazy修饰符后等于多了一个失去焦点才能触发的条件。
.number - 输入字符串转为有效的数字如果原值的转换结果为 NaN 则返回原值
注意:
- 修饰符不能限制输入内容仅仅是把用户输入的内容尝试转换一下
- 如输入1+1结果为1 它不会去计算只是碰到1是数字,碰到+就停止了
- .trim - 输入首尾空格过滤
自定义组件的v-model
父组件中在子组件上使用v-model,默认会用value的prop来接受父组件v-model绑定的值,然后子组件通过input事件将更新后的值传递给父组件
child组件中 <input :value="value" @input="onChildClick($event.target.value)"> props:{ value: { type: String, default: '' } }, methods: { onChildClick(value) { // 需要将更新后的值传递给父组件 this.$emit('input', value) } } 复制代码
父组件中 // 相当于<child :value="name" @input="name = arguments[0]"></child> <child v-model="name"></child> data(){ return{ name:"" } } 复制代码
源码学习
vscode中安装了Search node_modules后查找依赖包中的vue,或者直接去 vue 官网 将项目pull下来。
vue/src/compiler/codegen/index.js中 先看第一个函数,这个书写格式跟我们的习惯不太一样。
function genDirectives (el: ASTElement, state: CodegenState): string | void { // 省略内容 } 复制代码
这种书写方式是flow的语法。首先我们需要了解下什么是flow
flow
- 它是JavaScript 静态类型检查工具。
- 使用的原因:js 是动态类型语言,太灵活容易出现非常隐蔽的隐患代码,在运行阶段各种 bug,类型检查是当前动态类语言的发展趋势。
- 所谓类型检查,就是在编译期尽早发现(由类型错误引起的)bug,又不影响代码运行(不需要运行时动态检查类型)。
- 使用场景: 项目越复杂就越需要通过 工具 的手段来保证项目的维护性和增强代码的可读性。
flow 常用的类型注释语法
- 借助类型注释来指明期望的类型。类型注释是以冒号 : 开头
// x,y期待类型为number add函数的返回值期待值为number function add(x: number, y: number): number { return x + y } 复制代码
- 类型注释的使用场景:在函数参数,返回值,变量声明。
class Bar { x: string; // x 是字符串 y: string | number; // y 可以是字符串或者数字 bar(): string { // bar返回值为string return this.foo; } } 复制代码
- 标记为可选参数
是在定义函数的参数后面加一个 ?,标记为可选参数 function foo(x?) { if (x != undefined) { } } 复制代码
- 数组类型注释
// 数组类型注释的格式是 Array<T>,T 表示数组中每项的数据类型。在上述代码中,arr 是每项均为数字的数组 var arr: Array<number> = [1, 2, 3] 复制代码
- callable对象 callable 对象 (可调用的) 函数也是一个对象,也可以拥有属性,于是函数拥有一个 callable 属性
function makeCallable(): { (x: number): string; foo: number } { function callable(x) { return number.toFixed(2); } callable.foo = 123; return callable; } 复制代码
上面的代码可以拆成两部分看,下面的函数返回一个callable函数,并在返回之前给这个函数添加了foo属性。
function makeCallable() { function callable(x) { return number.toFixed(2); } callable.foo = 123; return callable; } 复制代码
然后分析: { (x: number): string; foo: number }这段,(x:number):string对应的就是callable函数,意思是callable的入参必须是一个number类型,并且返回值是一个string类型。
foo:number对应的就是callable.foo必须为number类型
- null和void JavaScript 有 null 和 undefined,Flow 中, null(值) 有 null 类型, undefined 有 void 类型
genDirectives函数
- 在了解了flow语法后我们继续来看vue源码,打开github上拉下来的项目,examples/commits/index.html
<input type="radio" :id="branch" :value="branch" name="branch" v-model="currentBranch"> 复制代码
- js部分先从编译阶段分析,首先是 parse 阶段, v-model 被当做普通的指令解析到 el.directives 中,然后在 codegen 阶段定义在 src/compiler/codegen/index.js 中
function genDirectives (el: ASTElement, state: CodegenState): string | void { const dirs = el.directives } 复制代码
有了folw语法的了解,我们知道:之后的是对前面变量的期望类型。但是在编译过程是el和state到底是什么呢?
可以看到el.directives是一个数组,它的子项包括(arg: null modifiers: undefined name: "model" rawName: "v-model" value: "currentBranch")
state.directives是一个对象,他的子项都是函数包括( bind: ƒ (e,t) cloak: ƒ O(e,t,n) html: ƒ (e,t) model: ƒ (e,t,n) on: ƒ (e,t) text: ƒ (e,t))这些函数 了解了这些参数是什么,继续看下面的代码
function genDirectives (el: ASTElement, state: CodegenState): string | void { const dirs = el.directives // 判断有无指令 if (!dirs) return let hasRuntime = false let i, l, dir, needRuntime // dirs.length表示指令的个数,这里就是将指令都遍历 for (i = 0, l = dirs.length; i < l; i++) { dir = dirs[i] needRuntime = true // 例如我们上面提到的model指令,在此将指令名字对应的函数赋值给gen变量,前面提到state.directives是一个包含(bind,model...)函数的对象 // :DirectiveFunction就是表示gen的类型是一个指令函数 const gen: DirectiveFunction = state.directives[dir.name] if (gen) { // gen函数返回一个Boolean之后我们会提到,这里将结果赋值给needRuntime来表示函数执行是否结束 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) + ']' } } } 复制代码
dir就是上面提到的数组下,res不过就是将这些参数拼接起来,让我们看看它最终长什么样子
加上后面的slice方法就是将res字符串的最后一位去掉然后拼接上']'组成一个完整的数组。
model函数
上面 const gen: DirectiveFunction = state.directives[dir. name]是拿出指令名对应的函数,拿model举例。定义在 src/platforms/web/compiler/directives/model.js
export default function model ( el: ASTElement, dir: ASTDirective, _warn: Function ): ?boolean{ // 就是needRuntime = !!gen(el, dir, state.warn)传递过来的参数 const value = dir.value const modifiers = dir.modifiers const tag = el.tag const type = el.attrsMap.type // process该对象表示Node所处的当前进程(全局变量)process.env属性返回一个包含用户环境信息的对象使用场景:在development和production不同环境上,配置会有些不同 if (process.env.NODE_ENV !== 'production') { 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.` ) } } if (el.component) { genComponentModel(el, value, modifiers) 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) return false } else if (process.env.NODE_ENV !== 'production') { warn() } return true } 复制代码
这段代码比较简单,判断下用户环境是不是production,如果是判断下tag(标签名)然后执行不同的函数
事件绑定和修饰符
1.由于index.html中input的type为radio不太常用,我将其改为如下
<input :id="branch" :value="branch" name="branch" v-model.lazy.number.trim="currentBranch"> 复制代码
将html改为如上后在model函数中经过判断后会执行genDefaultModel函数
function genDefaultModel( el: ASTElement, value: string, modifiers: ?ASTModifiers ): ?boolean { const type = el.attrsMap.type // 判断不是production的情况下执行的代码 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' ) } } // modify是一个对象判断,如果使用了lazy则{lazy:true}然后用对象结构赋值的方法取出Boolean作为判断 const { lazy, number, trim } = modifiers || {} const needCompositionGuard = !lazy && type !== 'range' // event是设置事件类型,如果是lazy则定义change类型,如果不是lazy再判断type是不是range,如果不是,则定义input事件类型 const event = lazy ? 'change' : type === 'range' ? RANGE_TOKEN : 'input' // valueExpression是一个字符串,$event.target.value代表原生的DOM事件获取到当前值 let valueExpression = '$event.target.value' if (trim) { // 如果使用trim修饰符,valueExpression字符串拼接.trim() valueExpression = `$event.target.value.trim()` console.log(valueExpression, 'trim') } if (number) { valueExpression = `_n(${valueExpression})` console.log(valueExpression, 'trim') } let 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()') } } 复制代码
genAssignmentCode函数
作用:返回code
按照我们分析的路线我们可以知道,函数接受值 value
就是我们在html定义的currentBranch,如果不清除,可以返回按照介绍的路线重新捋一遍。
assignment
就是genDefaultModel中的valueExpression变量是一个字符串
function genAssignmentCode(value, assignment) { var res = parseModel(value); if (res.key === null) { return (value + "=" + assignment) } else { return ("$set(" + (res.exp) + ", " + (res.key) + ", " + assignment + ")") } } 复制代码
parseModel就是对value也就是currentBranch值做了很多情况的处理,可以来看下具体的返回值如下
然后我们得到 ${value}=${assignment}结构赋值的结果就是message=$event.target.value
回到genDefaultModel函数中code = 'message=$event.target.value'
code生成完又执行了 2 句非常关键的代码
addProp(el, 'value', `(${value})`) addHandler(el, event, code, null, true) 复制代码
这实际上就是 input 实现 v-model 的精髓,通过修改 AST 元素,给 el 添加一个 prop,相当于我们在 input 上动态绑定了 value,又给 el 添加了事件处理,相当于在 input 上绑定了 input 事件,其实转换成模板如下:
<input v-bind:value="currentBranch" v-on:input="currentBranch=$event.target.value"> 复制代码
其实就是动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message 设置为目标值,这样实际上就完成了数据双向绑定了,所以说 v-model 实际上就是语法糖。
组件
从编译阶段说起,对于父组件而言,在编译阶段会解析 v-modle 指令,依然会执行 genData 函数中的 genDirectives 函数,接着执行 src/platforms/web/compiler/directives/model.js 中定义的 model 函数
genComponentModel(el, value, modifiers) 复制代码
genComponentModel 函数定义在 src/compiler/directives/model.js 中
export function genComponentModel( el: ASTElement, value: string, modifiers: ?ASTModifiers ): ?boolean { const { number, trim } = modifiers || {} // 给baseValueExpression赋值一个默认的字符串 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) el.model = { value: `(${value})`, expression: `"${value}"`, callback: `function (${baseValueExpression}) {${assignment}}` } } 复制代码
这个函数最终得到的结果是
el.model = { callback:'function ($$v) {currentBranch=$$v}', expression:'"currentBranch"', value:'(currentBranch)' } 复制代码
在创建vnode阶段会执行createComponent 函数定义在 src/core/vdom/create-component.js
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // 当v-mode值发生差异时,执行 transformModel if (isDef(data.model)) { transformModel(Ctor.options, data) } } 复制代码
transformModel函数
function transformModel (options, data: any) { // 设置安全模式首先判断options.model存在,如果存在prop属性存在,就使用prop对应的名字,否则在不设置的情况下默认使用value做完prop接收 const prop = (options.model && options.model.prop) || 'value' // 和上面同理 const event = (options.model && options.model.event) || 'input' // 给data设置值,如果之前定义了options.model.prop则使用,如果没有则使用data.props.value = data.model.value ;(data.props || (data.props = {}))[prop] = data.model.value const on = data.on || (data.on = {}) if (isDef(on[event])) { on[event] = [data.model.callback].concat(on[event]) } else { on[event] = data.model.callback } } 复制代码
以上代码效果如下
data.props = { value: (message), } data.on = { input: function ($$v) { message=$$v } } 复制代码
其实就相当于我们在这样编写父组件:
let vm = new Vue({ el: '#app', template: '<div>' + '<child :value="message" @input="message=arguments[0]"></child>' + '<p>Message is: {{ message }}</p>' + '</div>', data() { return { message: '' } }, components: { Child } }) 复制代码
注意点:子组件的 prop 和 input 事件名是可以自定义的在定义子组件的时候通过 model 选项配置子组件接收的 prop 名以及派发的事件名
const prop = (options.model && options.model.prop) || 'value' const event = (options.model && options.model.event) || 'input' 复制代码
我们可以做如下修改,也可以达到同样的效果
props: ['msg'], model: { prop: 'msg', event: 'change' }, methods: { updateValue(e) { this.$emit('change', e.target.value) } } 复制代码
以上所述就是小编给大家介绍的《v-model 使用场景和源码学习》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- fishhook使用场景&源码分析
- SharePreference源码学习和多进程的场景
- Laravel 全局异常错误处理源码解析及使用场景
- HBase场景 | 对比MySQL,一文看透HBase的能力及使用场景
- 容器的应用场景
- Redis 应用场景汇总
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Mission Python
Sean McManus / No Starch Press / 2018-9-18 / GBP 24.99
Launch into coding with Mission Python, a space-themed guide to building a complete computer game in Python. You'll learn programming fundamentals like loops, strings, and lists as you build Escape!, ......一起来看看 《Mission Python》 这本书的介绍吧!