Vue v-model 从源码角度解析双向绑定原理
关于Vue
的双向绑定,是面试过程中经常遇到的一个问题,Vue
的v-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:message
,input:message=$event.target.value
这样的命令。这就是双向绑定原理的本质。
那么有个问题:
我们使用v-model
指令与使用:value="message"
;@input="message=$event.target.value"
这两者有什么不同吗?下一节我们来分析。
转载自:https://juejin.cn/post/7097993770360111117