You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// 生成render代码入口exportfunctiongenData(el: ASTElement,state: CodegenState): string{letdata='{'// directives first.// directives may mutate the el's other properties before they are generated.constdirs=genDirectives(el,state)if(dirs)data+=dirs+','// ...}
来看下genDirectives函数的定义:
functiongenDirectives(el: ASTElement,state: CodegenState): string|void{constdirs=el.directivesif(!dirs)returnletres='directives:['lethasRuntime=falseleti,l,dir,needRuntimefor(i=0,l=dirs.length;i<l;i++){dir=dirs[i]needRuntime=trueconstgen: 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=trueres+=`{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){returnres.slice(0,-1)+']'}}
exportdefaultfunctionmodel(el: ASTElement,dir: ASTDirective,_warn: Function): ?boolean{warn=_warnconstvalue=dir.valueconstmodifiers=dir.modifiersconsttag=el.tagconsttype=el.attrsMap.typeif(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.`,el.rawAttrsMap['v-model'])}}if(el.component){genComponentModel(el,value,modifiers)// component v-model doesn't need extra runtimereturnfalse}elseif(tag==='select'){genSelect(el,value,modifiers)}elseif(tag==='input'&&type==='checkbox'){genCheckboxModel(el,value,modifiers)}elseif(tag==='input'&&type==='radio'){genRadioModel(el,value,modifiers)}elseif(tag==='input'||tag==='textarea'){genDefaultModel(el,value,modifiers)}elseif(!config.isReservedTag(tag)){genComponentModel(el,value,modifiers)// component v-model doesn't need extra runtimereturnfalse}elseif(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.',el.rawAttrsMap['v-model'])}// ensure runtime directive metadatareturntrue}
functiononCompositionStart(e){e.target.composing=true}functiononCompositionEnd(e){// prevent triggering an input event for no reasonif(!e.target.composing)returne.target.composing=falsetrigger(e.target,'input')}functiontrigger(el,type){conste=document.createEvent('HTMLEvents')e.initEvent(type,true,true)el.dispatchEvent(e)}
在Vue中我们可以用
v-model
指令来使表单的值和状态进行双向绑定,当表单的值改变时绑定的值也会变化。其实,v-model
是Vue提供的props
和事件的语法糖,现在我们通过源码分析下这其中的原理。表单元素绑定
我们先来看一下
v-model
的例子:编译解析
对于
v-model
和其他指令一样,在模版的编译解析阶段会走src/compiler/parser/index.js
文件的processAttrs
方法,这个方法是对ast节点的attrsList属性进行处理。因为这个指令不是v-bind
和v-on
等特殊指令,所以该方法会走下面逻辑:这个方法就是处理普通指令并调用
addDirective
方法在ast节点的directives
属性上增加指令对象,对于我们的例子,执行完的结果:现在对
v-model
的编译解析阶段就完成了,接下来是进行编译代码生成阶段。代码生成
在编译代码生成阶段,会在
src/compiler/codegen/index.js
文件对于data代码生成入口函数genData
中处理指令代码的相关逻辑,这部分逻辑都在genDirectives
函数处理:来看下
genDirectives
函数的定义:这个方法循环遍历ast节点的
directives
属性的每个指令,对于每个指令会调用state.directives[dir.name]
返回的函数。这里的state是指Vue编译相关的一些配置,这些配置和平台有关,它的入口在src/platforms/web/compiler/options.js
:和指令相关配置定义在
src/platforms/web/compiler/directives/index.js
中:很明显Vue对这3个特殊的指令编译都有特殊处理。所以上面的
gen
函数就是指src/platforms/web/compiler/directives/model.js
文件中定义的model
方法:这个方法主要是处理
v-model
绑定在不同表单或者组件的处理。在我们例子是绑定在input
,所以会调用genDefaultModel
方法:这个方法先获取
v-model
指令的修饰符,接下来是根据不同修饰符对事件类型event
和表达式的值valueExpression
的处理。然后调用genAssignmentCode
方法生成我们回调函数的code
:这个方法主要是要处理指令表达式是类似
test[test1[key]]
,test["a"][key]
等情况。我们例子直接返回${value}=${assignment}
。因为我们没设置lazy,所以最终我们的code为if($event.target.composing)return;message=$event.target.value
。对于composing为真直接返回这段逻辑我们稍后分析。接下来就是v-model
指令的关键逻辑:它会往ast节点上增加一个
props
和绑定一个事件event,这就是Vue语法糖实现的核心。执行完这段逻辑看下ast节点结果:执行完平台的model方法后返回
true
,再回到genDirectives
方法,如果needRuntime
为true
,就把指令相关属性就行字符串代码拼接并最终返回。这里我们看下genData
函数有一细节,就是函数最开始就处理指令,这是因为处理指令时候可能会在节点上新增其他一些属性,例如我们v-model
指令会增加props
和事件。最后,来看下
render
生成的代码结果:指令钩子
在上面分析后,我们的例子其实等价于:
但是这里面有一个细微的差别我们可能没注意,那就是对于中文输入的处理。使用
v-model
输入中文过程中我们状态message是不会更着变化的,而等价的写法就会,那这中间的处理Vue是怎么实现的呢?我们知道Vue的自定义指令存在钩子函数,并且在绑定的元素的插入或者更新阶段触发。其实,Vue也内置了
v-model
的钩子函数来处理我们上面说的中文输入的场景。现在来看下它的定义。在我们虚拟节点的
patch
过程中会触发一系列的钩子函数,对于指令会在create
,update
和destory
钩子都会有处理,它的入口定义在src/core/vdom/modules/directives.js
:很明显,在上面的三个时期都会调用
_update
函数:这个方法用
isCreate
表示当前vnode是否是新建的节点,isDestroy
表示当前节点是否销毁。normalizeDirectives
方法是获取格式化指令对象,把指令的钩子函数进行整合到def
。接着循环新节点的指令数组newDirs
,对于每个指令对象dir
在老的指令对象oldDirs
不存在,这会调用指令的bind
钩子,如果有定义insert
钩子,则push到dirsWithInsert
队列中,这样能保证所有的指令执行完bind
钩子才去执行insert
钩子。如果老的指令对象
oldDir
存在,则调用指令的update
钩子,并把componentUpdated
钩子存到dirsWithPostpatch
中,这样能保证所有的指令执行完update
钩子才去执行componentUpdated
钩子。最后把执行指令insert
钩子数组函数合并到虚拟节点的自身的insert
钩子,把执行指令componentUpdated
钩子数组函数合并到虚拟节点的自身的postpatch
钩子,这样就会更新虚拟节点在patch
过程的对应阶段执行。如果不是新建的节点,并且老的指令数组
oldDirs
如果有newDirs
中不存在的,则证明该指令已经废弃,会调用响应的unbind
钩子函数。回到我们上面的问题,看看
v-model
内置的insert
钩子的实现,它定义在src/platforms/web/runtime/directives/model.js
中:上面代码在处理绑定
input
和textarea
类型的绑定时,在元素插入DOM后会另外绑定compositionstart
和compositionend
事件,它们分别会在中文输入过程和输入完成触发。来看下对应的回调函数:在中文输入过程中,设置
e.target.composing
为true
,这个时候我们再来看下v-model
绑定事件的函数体:当中文输入过程中触发的
input
事件,$event.target.composing
为true
直接返回,这样状态就会不更着改变了。当中文输入完成执行onCompositionEnd
函数会把e.target.composing
设置为false
,这个时候执行函数体就会修改状态message了。组件绑定
v-model
也可以用到组件上,先看一个例子:在组件上使用
v-model
也会在编译模版时进行处理,不同的是在gen
函数中会走下面的逻辑:因为组件不是平台保留的标签,调用
genComponentModel
方法进行处理并且返回false
:这个方法主要在ast节点上添加
model
属性来表示指令相关数据,我们例子中执行完的结果为:然后返回
genData
函数,这里返回的dirs为undefined
,因为组件使用v-model
单纯是个语法糖,不需要在运行时进行相关处理。另外,这个函数要把节点上的model
赋值给data
属性:最后我们看下生成的
render
代码:很明显,在
Child
的data
增加了model
属性,并且会在创建组件构造器时进行处理。在src/core/vdom/create-component.js
文件的createComponent
函数有下面一段逻辑:来看下
transformModel
的定义:这个方法向组件虚拟节点
data
属性增加一个key为prop
的属性,并且在on
增加事件event
,这样就实现了v-model
的功能。总结
那么至此,
v-model
的实现就分析完了,我们了解到它是 Vue 双向绑定的真正实现,但本质上就是一种语法糖,它即可以支持原生表单元素,也可以支持自定义组件。在组件的实现中,我们是可以配置子组件接收prop
名称,以及派发的事件名称。The text was updated successfully, but these errors were encountered: