高频面试题:
v-model?
答案:v-model是语法糖。
组件的渲染都大底会经历通过编译进行render函数的获取、虚拟DOM的获取和视图渲染过程这三个主要流程。
// main.js
const baseCheckbox = {
template: `<input type="checkbox" v-bind:checked="checked" v-on:change="$emit('change', $event.target.checked)">`,
model: {
prop: "checked",
event: "change"
},
props: {
checked: Boolean
}
};
new Vue({
el: "#app",
template: `<base-checkbox v-model="lovingVue"></base-checkbox>`,
components: {
baseCheckbox
},
data() {
return {
lovingVue: true
};
}
});
1、父组件的编译
(1)ast的获取
在const ast = parse(template.trim(), options)的过程中,会通过正则的方式去将template转换成ast树,整个过程中如果遇到闭合标签在ast树管理过程中会执行closeElement(element)中的element = processElement(element, options),其中有属性管理的方法processAttrs(element)。过程中会执行到addDirective:
export function addDirective (
el: ASTElement,
name: string,
rawName: string,
value: string,
arg: ?string,
isDynamicArg: boolean,
modifiers: ?ASTModifiers,
range?: Range
) {
(el.directives || (el.directives = [])).push(rangeSetItem({
name,
rawName,
value,
arg,
isDynamicArg,
modifiers
}, range))
el.plain = false
}
执行结果中包含属性directives,其中有属性为:
{
// ...
name: "model",
value: "lovingVue"
// ...
}
(2)code的获取
在genData的过程中,针对组件会执行genComponentModel(el, value, modifiers):
/**
* Cross-platform code generation for component v-model
*/
export function genComponentModel (
el: ASTElement,
value: string,
modifiers: ?ASTModifiers
): ?boolean {
const { number, trim } = modifiers || {}
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: JSON.stringify(value),
callback: `function (${baseValueExpression}) {${assignment}}`
}
}
/**
* Cross-platform codegen helper for generating v-model value assignment code.
*/
export function genAssignmentCode (
value: string,
assignment: string
): string {
const res = parseModel(value)
if (res.key === null) {
return `${value}=${assignment}`
} else {
return `$set(${res.exp}, ${res.key}, ${assignment})`
}
}
执行结果会使el中包含属性model,其中有属性为:
{
callback: "function ($$v) {lovingVue=$$v}",
expression: "\"lovingVue\"",
value: "(lovingVue)",
}
字符串拼接时,如果存在model会执行以下逻辑:
// component v-model
if (el.model) {
data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
}
最后生成的_render结果为:
with(this) {
return _c('base-checkbox', {
model: {
value: (lovingVue),
callback: function ($$v) {
lovingVue = $$v
},
expression: "lovingVue"
}
})
}
2、父组件的vNode
父组件执行_c将base-checkbox作为标签,将model对象作为data开始执行,最终会执行到_createElement中:
if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
vnode = createComponent(Ctor, data, context, children, tag)
}
这里的解析到Ctor组件为:
baseCheckbox {
template: `<input type="checkbox" v-bind:checked="checked" v-on:change="$emit('change', $event.target.checked)">`,
model: {
prop: "checked",
event: "change"
},
props: {
checked: Boolean
}
}
再看createComponent:
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
const baseCtor = context.$options._base
// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}
// ...
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}
// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)
// ...
// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn
// ...
// return a placeholder vnode
const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
// ...
return vnode
}
(1)transformModel
// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data) {
var prop = (options.model && options.model.prop) || 'value';
var event = (options.model && options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value;
var on = data.on || (data.on = {});
var existing = on[event];
var callback = data.model.callback;
if (isDef(existing)) {
if (
Array.isArray(existing)
? existing.indexOf(callback) === -1
: existing !== callback
) {
on[event] = [callback].concat(existing);
}
} else {
on[event] = callback;
}
}
注释中清晰解释,这里是将v-model的value和callback转换成prop和event handler。
如果没有定义model中的prop,默认为value;如果没有定义model中的event,默认为input。当前例子中定义了:
model: {
prop: "checked",
event: "change"
},
transform执行后data为:
{
attrs: {
checked: true
},
model: {
value: true,
expression: 'lovingVue',
callback: function ($$v) {lovingVue=$$v}
},
on: {
change: function ($$v) {lovingVue=$$v}
},
}
(2)extractPropFromVNodeData
export function extractPropsFromVNodeData (
data: VNodeData,
Ctor: Class<Component>,
tag?: string
): ?Object {
// we are only extracting raw values here.
// validation and default values are handled in the child
// component itself.
const propOptions = Ctor.options.props
if (isUndef(propOptions)) {
return
}
const res = {}
const { attrs, props } = data
if (isDef(attrs) || isDef(props)) {
for (const key in propOptions) {
const altKey = hyphenate(key)
if (process.env.NODE_ENV !== 'production') {
const keyInLowerCase = key.toLowerCase()
if (
key !== keyInLowerCase &&
attrs && hasOwn(attrs, keyInLowerCase)
) {
tip(
`Prop "${keyInLowerCase}" is passed to component ` +
`${formatComponentName(tag || Ctor)}, but the declared prop name is` +
` "${key}". ` +
`Note that HTML attributes are case-insensitive and camelCased ` +
`props need to use their kebab-case equivalents when using in-DOM ` +
`templates. You should probably use "${altKey}" instead of "${key}".`
)
}
}
checkProp(res, props, key, altKey, true) ||
checkProp(res, attrs, key, altKey, false)
}
}
return res
}
function checkProp (
res: Object,
hash: ?Object,
key: string,
altKey: string,
preserve: boolean
): boolean {
if (isDef(hash)) {
if (hasOwn(hash, key)) {
res[key] = hash[key]
if (!preserve) {
delete hash[key]
}
return true
} else if (hasOwn(hash, altKey)) {
res[key] = hash[altKey]
if (!preserve) {
delete hash[altKey]
}
return true
}
}
return false
}
当前例子中extractPropsFromVNodeData(data, Ctor, tag)在执行完checkProp(res, attrs, key, altKey, false)后,将checked: true返回,并删除attrs中的checked: true,最后将返回值赋值给propsData。此时data的值为:
{
attrs: {},
model: {
value: true,
expression: 'lovingVue',
callback: function ($$v) {lovingVue=$$v}
},
on: {
change: function ($$v) {lovingVue=$$v}
},
}
(3)listeners = data.on
通过const listeners = data.on的方式将其中事件赋值给listeners,再通过data.on = data.nativeOn将原生事件赋值给data.on。此时data的值为:
{
attrs: { },
model: {
value: true,
expression: 'lovingVue',
callback: function ($$v) {lovingVue=$$v}
},
on: undefined,
}
最后将{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }作为参数componentOptions在new VNode实例化vNode的时候传入。
3、子组件的createComponentInstanceForVnode
子组件会执行到init钩子函数中的:
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
在createComponentInstanceForVnode的过程中会执行new vnode.componentOptions.Ctor(options),进而执行继承于Vue中的this._init:
(1)initEvents
initEvents最终会执行到:
Vue.prototype.$on = function (event, fn) {
var vm = this;
if (Array.isArray(event)) {
for (var i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn);
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn);
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm
};
如果vm._events[event]不存在,将其赋值为[],并将当前处理后的事件推入其中。
(2)initProps
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
通过var value = validateProp(key, propsOptions, propsData, vm)获取到value,并通过defineReactive(props, key, value, () => {/* */})的方式将props处理成响应式。
通过initEvents和initProps就为当前vm实例上增加了_props和_events属性。
4、子组件的$mount
子组件在执行child.$mount(hydrating ? vnode.elm : undefined, hydrating)时,先通过编译过程获得_render:
with(this) {
return _c('input', {
attrs: {
"type": "checkbox"
},
domProps: {
"checked": checked
},
on: {
"change": function ($event) {
return $emit('change', $event.target.checked)
}
}
})
}
获得的vNode中包含data值为:
{
"attrs": {
"type": "checkbox"
},
"domProps": {
"checked": true
},
"on": {
change: ƒunction($event) {
return $emit('change', $event.target.checked)
},
}
}
在执行到invokeCreateHooks:
function invokeCreateHooks (vnode, insertedVnodeQueue) {
for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
cbs.create[i$1](emptyNode, vnode);
}
i = vnode.data.hook; // Reuse variable
if (isDef(i)) {
if (isDef(i.create)) { i.create(emptyNode, vnode); }
if (isDef(i.insert)) { insertedVnodeQueue.push(vnode); }
}
}
这里会通过updateAttrs(oldVnode, vnode)为elm设置属性type="checkbox";
再通过updateDOMListeners为elm绑定change事件;
最后通过updateDOMProps为elm设置elm['checked'] = true。
至此就完成了首次的渲染。
5、再次渲染
再次渲染会执行到$emit('change', $event.target.checked)将$event.target.checked进行触发。
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
tip(
`Event "${lowerCaseEvent}" is emitted in component ` +
`${formatComponentName(vm)} but the handler is registered for "${event}". ` +
`Note that HTML attributes are case-insensitive and you cannot use ` +
`v-on to listen to camelCase events when using in-DOM templates. ` +
`You should probably use "${hyphenate(event)}" instead of "${event}".`
)
}
}
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
}
这里通过let cbs = vm._events[event]获取到在initEvents阶段定义的事件,再通过invokeWithErrorHandling进行执行。相当于执行了ƒ ($$v) {lovingVue=$$v}回调函数,将父级中的数据进行改变,达到子组件修改父组件的目的。
总结:
组件中v-model通过prop和回调函数的方式进行实现。