高频面试题:
v-model?
答案:v-model是语法糖。
节点或者组件的渲染都大底会经历通过编译进行render函数的获取、虚拟DOM的获取和视图渲染过程这三个主要流程。
一、input
new Vue({
el: "#app",
data() {
return {
msg: ""
};
},
template: `<div>
<input v-model="msg" placeholder="edit me">
<p>msg is: {{ msg }}</p>
</div>`
});
1、编译过程
(1)ast的获取
在const ast = parse(template.trim(), options)的过程中,会通过正则的方式去将template转换成ast树,整个过程中如果遇到input闭合标签在ast树管理过程中会执行closeElement(element)中的element = processElement(element, options),其中有属性管理的方法processAttrs(element)。
function processAttrs (el) {
// ...
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])
// ...
addAttr(el, name, JSON.stringify(value), list[i])
// ...
}
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
}
function addAttr (el, name, value, range, dynamic) {
var attrs = dynamic
? (el.dynamicAttrs || (el.dynamicAttrs = []))
: (el.attrs || (el.attrs = []));
attrs.push(rangeSetItem({ name: name, value: value, dynamic: dynamic }, range));
el.plain = false;
}
在当前例子中,如果满足正则条件,会执行addDirective的方式处理其中的v-model="msg",处理结果为:
如果满足条件,会执行addAttr的方式处理其中的placeholder="edit me",处理结果为:
从上面通过debugger断点可以看出,当前的el上会多出directives和attrs两个属性。
(2)code码的生成
在生成code码的过程中会不断的调用genElement进行ast树的递归操作,其中会通过data = genData(el, state)的方式构建当前el上的属性:
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// directives first.
// directives may mutate the el's other properties before they are generated.
const dirs = genDirectives(el, state)
if (dirs) data += dirs + ','
// key
if (el.key) {
data += `key:${el.key},`
}
// ref
if (el.ref) {
data += `ref:${el.ref},`
}
if (el.refInFor) {
data += `refInFor:true,`
}
// pre
if (el.pre) {
data += `pre:true,`
}
// record original tag name for components using "is" attribute
if (el.component) {
data += `tag:"${el.tag}",`
}
// module data generation functions
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
// attributes
if (el.attrs) {
data += `attrs:${genProps(el.attrs)},`
}
// DOM props
if (el.props) {
data += `domProps:${genProps(el.props)},`
}
// event handlers
if (el.events) {
data += `${genHandlers(el.events, false)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true)},`
}
// slot target
// only for non-scoped slots
if (el.slotTarget && !el.slotScope) {
data += `slot:${el.slotTarget},`
}
// scoped slots
if (el.scopedSlots) {
data += `${genScopedSlots(el, el.scopedSlots, state)},`
}
// component v-model
if (el.model) {
data += `model:{value:${
el.model.value
},callback:${
el.model.callback
},expression:${
el.model.expression
}},`
}
// inline-template
if (el.inlineTemplate) {
const inlineTemplate = genInlineTemplate(el, state)
if (inlineTemplate) {
data += `${inlineTemplate},`
}
}
data = data.replace(/,$/, '') + '}'
// v-bind dynamic argument wrap
// v-bind with dynamic arguments must be applied using the same v-bind object
// merge helper so that class/style/mustUseProp attrs are handled correctly.
if (el.dynamicAttrs) {
data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
}
// v-bind data wrap
if (el.wrapData) {
data = el.wrapData(data)
}
// v-on data wrap
if (el.wrapListeners) {
data = el.wrapListeners(data)
}
return data
}
可以看出,会对el上各个节点属性进行处理,当执行到genDirectives的时候当var gen = state.directives[dir.name]存在时会执行gen(el, dir, state.warn),在当前例子中(tag === 'input'成立,进而执行genDefaultModel(el, value, modifiers):
function genDefaultModel (
el,
value,
modifiers
) {
var type = el.attrsMap.type;
// warn if v-bind:value conflicts with v-model
// except for inputs with v-bind:type
if (process.env.NODE_ENV !== 'production') {
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]
);
}
}
var ref = modifiers || {};
var lazy = ref.lazy;
var number = ref.number;
var trim = ref.trim;
var needCompositionGuard = !lazy && type !== 'range';
var event = lazy
? 'change'
: type === 'range'
? RANGE_TOKEN
: 'input';
var valueExpression = '$event.target.value';
if (trim) {
valueExpression = "$event.target.value.trim()";
}
if (number) {
valueExpression = "_n(" + valueExpression + ")";
}
var code = genAssignmentCode(value, valueExpression);
if (needCompositionGuard) {
code = "if($event.target.composing)return;" + code;
}
addProp(el, 'value', ("(" + value + ")"));
addHandler(el, event, code, null, true);
if (trim || number) {
addHandler(el, 'blur', '$forceUpdate()');
}
}
其中addProp(el, 'value', ("(" + value + ")"))和addHandler(el, event, code, null, true)就是将v-model = msg转换成prop和events的核心逻辑。
之后通过prop和event的处理逻辑,处理el上在的prop和events属性。
最后获得包含domProps和on的data值为:
"{directives:[{name:"model",rawName:"v-model",value:(msg),expression:"msg"}],attrs:{"placeholder":"edit me"},domProps:{"value":(msg)},on:{"input":function($event){if($event.target.composing)return;msg=$event.target.value}}}"
2、vNode
获取到的vNode结果中,会有属性directives、on、attrs和domProps。
3、patch阶段
在patch过程中,如果vNode中存在data属性,会执行到处理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); }
}
}
在执行的过程中,cbs.create中会包含updateAttrs、updateClass、updateAttrs、updateDOMListeners、updateDOMProps、updateStyle和updateDirectives等钩子函数,当前例子中主要涉及到的方法如下所示:
执行完以后,如果vnode.data.hook中有insert钩子,会将其推入到insertedVnodeQueue数组中,在所有父子节点完成节点的创建以后,会在patch逻辑的最后执行invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch):
function invokeInsertHook (vnode, queue, initial) {
// delay insert hooks for component root nodes, invoke them after the
// element is really inserted
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data.pendingInsert = queue;
} else {
for (var i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i]);
}
}
}
function inserted (el, binding, vnode, oldVnode) {
if (vnode.tag === 'select') {
// #6903
if (oldVnode.elm && !oldVnode.elm._vOptions) {
mergeVNodeHook(vnode, 'postpatch', function () {
directive.componentUpdated(el, binding, vnode);
});
} else {
setSelected(el, binding, vnode.context);
}
el._vOptions = [].map.call(el.options, getValue);
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
el._vModifiers = binding.modifiers;
if (!binding.modifiers.lazy) {
el.addEventListener('compositionstart', onCompositionStart);
el.addEventListener('compositionend', onCompositionEnd);
// Safari < 10.2 & UIWebView doesn't fire compositionend when
// switching focus before confirming composition choice
// this also fixes the issue where some browsers e.g. iOS Chrome
// fires "change" instead of "input" on autocomplete.
el.addEventListener('change', onCompositionEnd);
/* istanbul ignore if */
if (isIE9) {
el.vmodel = true;
}
}
}
},
这里在input输入的时候,处理了compositionstart和compositionend,并且,在iOS Chrome浏览器中通过change替代input方法。
小结
input中的v-model,最终通过target.addEventListener处理成在节点上监听input事件function($event){msg=$event.target.value}}的形式,当input值发生变化时,msg也改变。
二、组件
const childCom = {
template: `<div>
<button @click="changeCount">数据修改</button>
</div>`,
data() {
return {
count: 1
};
},
methods: {
changeCount() {
this.count++;
this.$emit("input", this.count);
}
}
};
Vue.component("childCom", childCom);
new Vue({
el: "#app",
data() {
return {
msg: ""
};
},
template: `<div>
<childCom v-model="msg"></childCom>
<p>{{msg}}</p>
</div>`
});
1、编译过程
(1)ast的获取
和普通input的ast获取过程一样,也会通过addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i])的方式对其中的v-model属性进行处理,结果为:
(2)code码的生成
在对el上directives进行处理的时候,会执行到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}}`
}
}
在这里为el定义model,然后再执行:
// component v-model
if (el.model) {
data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
}
执行完获取的结果为:
'{model:{value:(msg),callback:function ($$v) {msg=$$v},expression:"msg"}}'
2、vNode
在子组件vNode获取过程中,会执行createComponent,如果存在mode,会执行:
// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data);
}
// 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;
}
}
这里为data定义了attrs和on属性,执行结果如下:
然后又通过以下方式将data.on赋值给listeners作为组件事件,并将原生事件data.nativeOn赋值给data.on
// extract listeners, since these needs to be treated as child component listeners instead of DOM listeners
var listeners = data.on;
// replace with listeners with .native modifier so it gets processed during parent component patch.
data.on = data.nativeOn;
3、patch
在子组件patch的过程中,会执行到以下逻辑:
// createElm方法中的代码片段
{
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
// 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); }
}
}
cbs.create中有处理组件事件的方法updateDOMListeners (oldVnode, vnode)
function updateDOMListeners (oldVnode, vnode) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
target$1 = vnode.elm;
normalizeEvents(on);
updateListeners(on, oldOn, add$1, remove$2, createOnceHandler$1, vnode.context);
target$1 = undefined;
}
function updateListeners (
on,
oldOn,
add,
remove$$1,
createOnceHandler,
vm
) {
var name, def$$1, cur, old, event;
for (name in on) {
def$$1 = cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
if (isUndef(cur)) {
process.env.NODE_ENV !== 'production' && warn(
"Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
vm
);
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm);
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture);
}
add(event.name, cur, event.capture, event.passive, event.params);
} else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name);
remove$$1(event.name, oldOn[name], event.capture);
}
}
}
function add (
name,
handler,
capture,
passive
) {
// async edge case #6566: inner click event triggers patch, event handler
// attached to outer element during patch, and triggered again. This
// happens because browsers fire microtask ticks between event propagation.
// the solution is simple: we save the timestamp when a handler is attached,
// and the handler would only fire if the event passed to it was fired
// AFTER it was attached.
if (useMicrotaskFix) {
var attachedTimestamp = currentFlushTimestamp;
var original = handler;
handler = original._wrapper = function (e) {
if (
// no bubbling, should always fire.
// this is just a safety net in case event.timeStamp is unreliable in
// certain weird environments...
e.target === e.currentTarget ||
// event is fired after handler attachment
e.timeStamp >= attachedTimestamp ||
// bail for environments that have buggy event.timeStamp implementations
// #9462 iOS 9 bug: event.timeStamp is 0 after history.pushState
// #9681 QtWebEngine event.timeStamp is negative value
e.timeStamp <= 0 ||
// #9448 bail if event is fired in another document in a multi-page
// electron/nw.js app, since event.timeStamp will be using a different
// starting reference
e.target.ownerDocument !== document
) {
return original.apply(this, arguments)
}
};
}
target.addEventListener(
name,
handler,
supportsPassive
? { capture: capture, passive: passive }
: capture
);
}
最终可以看出,当前节点会通过原生方法target.addEventListener进行事件的绑定name为click,其中的handler是经过处理的changeCount事件:
changeCount() {
this.count++;
this.$emit("input", this.count);
}
4、callback执行逻辑
(1)在子组件事件实例化过程中initEvents:
function initEvents (vm) {
vm._events = Object.create(null);
vm._hasHookEvent = false;
// init parent attached events
var listeners = vm.$options._parentListeners;
if (listeners) {
updateComponentListeners(vm, listeners);
}
}
function updateComponentListeners (
vm,
listeners,
oldListeners
) {
target = vm;
updateListeners(listeners, oldListeners || {}, add, remove$1, createOnceHandler, vm);
target = undefined;
}
function updateListeners (
on,
oldOn,
add,
remove$$1,
createOnceHandler,
vm
) {
var name, def$$1, cur, old, event;
for (name in on) {
def$$1 = cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
if (isUndef(cur)) {
process.env.NODE_ENV !== 'production' && warn(
"Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
vm
);
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm);
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture);
}
add(event.name, cur, event.capture, event.passive, event.params);
} else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
}
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name);
remove$$1(event.name, oldOn[name], event.capture);
}
}
}
function add (event, fn) {
target.$on(event, fn);
}
在add的过程中,会触发$on方法:
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]不存在的时候,会将其置为空数组,并且将当前处理后的事件fn推入其中,并且,这里的fn本质上还是执行到了回调函数ƒunction input($$v) {msg=$$v}。
(2)$emit
当点击子元素的按钮的时候,会触发this.$emit方法:
Vue.prototype.$emit = function (event) {
var vm = this;
// ...
var cbs = vm._events[event];
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs;
var args = toArray(arguments, 1);
var info = "event handler for \"" + event + "\"";
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
return vm
};
}
首先获取到vm._events获取到处理后的回调函数,当执行到ƒunction input($$v) {msg=$$v}时,父组件中的msg发生改变,进而触发发布者dep的notify方法,视图开始重新渲染。
小结
v-model在组件中则通过给点击事件绑定原生事件,当触发到this.$emit的时候,再进行回调函数ƒunction input($$v) {msg=$$v}的执行,进而达到子组件修改父组件中数据msg的目的。