前几篇文章我们分析了computed、watch以及双向绑定的原理,有了前面的基础我们继续分析v-model的原理。
基础代码内容如下:
// App.vue
{{a}}
// test.vue
点我
在App.vue中执行render函数的时候代码如下:
// App.vue template
{{a}}
被编译为:
var render = function render() {
var _vm = this,
_c = _vm._self._c
return _c(
"div",
[
_vm._v("\n " + _vm._s(_vm.a) + "\n "),
_c("test", {
model: {
value: _vm.a,
callback: function ($$v) {
_vm.a = $$v
},
expression: "a",
},
}),
],
1
)
}
可以看出v-model="a"被编译为一个model对象,同时还有一个回调函数callback将收到的值赋给变量a。学过前面章节的小伙伴可能知道接下来要开始收集依赖了。触发变量a的getter方法将当前组件的watcher放入变量a的subs中(当前发布者有多少watcher监听),当前的watcher也会在deps中放入发布者a(当前watcher有多少个发布者)。接下来执行_c方法创建组件:
function createComponent(Ctor, data, context, children, tag) {
...
if (isDef(data.model)) {
// @ts-expect-error
transformModel(Ctor.options, data);
}
// extract props
// @ts-expect-error
var propsData = extractPropsFromVNodeData(data, Ctor, tag);
...
var listeners = data.on;
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn;
...
// install component management hooks onto the placeholder node
installComponentHooks(data);
// return a placeholder vnode
// @ts-expect-error
var name = getComponentName(Ctor.options) || tag;
var vnode = new VNode(
// @ts-expect-error
"vue-component-".concat(Ctor.cid).concat(name ? "-".concat(name) : ''), data, undefined, undefined, undefined, context,
// @ts-expect-error
{ Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }, asyncFactory);
return vnode;
}
首先执行transformModel方法去处理我们在test组件中定义的model对象:
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 (isArray(existing)
? existing.indexOf(callback) === -1
: existing !== callback) {
on[event] = [callback].concat(existing);
}
}
else {
on[event] = callback;
}
}
返回值如下:
设置了attrs对象,key为model.prop的值,value为model.value。设置了on对象,key为model.event的值,value为model.callback。执行完毕后继续执行extractPropsFromVNodeData处理test组件中的props对象:
function extractPropsFromVNodeData(data, Ctor, tag) {
...
var res = {};
var attrs = data.attrs, props = data.props;
if (isDef(attrs) || isDef(props)) {
for (var key in propOptions) {
var altKey = hyphenate(key);
...
checkProp(res, props, key, altKey, true) ||
checkProp(res, attrs, key, altKey, false);
}
}
return res;
}
该函数会根据我们在model中定义的prop的key值去props查找我们定义的prop值,如果找到了就会校验类型是否正确。所以我们在test组件中定义的props中必须要有个key与model中对应。至此test的vnode实例创建完毕。继续往下执行到实例化test的vm实例,接着执行initProps$1(vm, opts.props)方法主要执行 _loop_1(key):
function initProps$1(vm, propsOptions) {
var propsData = vm.$options.propsData || {};
var props = (vm._props = shallowReactive({}));
...
var keys = (vm.$options._propKeys = []);
var isRoot = !vm.$parent;
// root instance props should be converted
if (!isRoot) {
toggleObserving(false);
}
var _loop_1 = function (key) {
keys.push(key);
var value = validateProp(key, propsOptions, propsData, vm);
/* istanbul ignore else */
{
var hyphenatedKey = hyphenate(key);
...
defineReactive(props, key, value, function () {
if (!isRoot && !isUpdatingChildComponent) {
...
}
});
}
...
if (!(key in vm)) {
proxy(vm, "_props", key);
}
};
// propsOptions=clickText: {type: ƒ}
for (var key in propsOptions) {
_loop_1(key);
}
toggleObserving(true);
}
执行validateProp方法校验传入的参数是否符合类型规定并返回初始值undefined,通过defineReactive设置props数据clickText的get和set。_loop_1(key)执行完毕,校验了数据的类型、设置default初始值,然后对clickText设置了get和set方法。至此test的initState执行完毕。生成vm实例后开始执行render方法渲染组件test:
var render = function render() {
var _vm = this,
_c = _vm._self._c
return _c("div", [
_c(
"div",
{
on: {
click: function ($event) {
return _vm.$emit("click", 1234)
},
},
},
[_vm._v("点我")]
),
])
}
生成vnode后便执行_update方法,那么有人会纳闷我在test注册的事件什么时候定义呢,是在init在_update方法这里会执行invokeCreateHooks方法对当前dom定义click事件:
function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
...
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
if (data && data.pre) {
creatingElmInVPre--;
}
}
else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
...
function invokeCreateHooks(vnode, insertedVnodeQueue) {
for (var i_2 = 0; i_2 < cbs.create.length; ++i_2) {
cbs.create[i_2](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[i_2](emptyNode, vnode)的时候会执行updateDOMListeners方法:
function updateDOMListeners(oldVnode, vnode) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return;
}
var on = vnode.data.on || {};
var oldOn = oldVnode.data.on || {};
// vnode is empty when removing all listeners,
// and use old vnode dom element
target = vnode.elm || oldVnode.elm;
normalizeEvents(on);
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context);
target = undefined;
}
...
接下来执行updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
// on = click: ƒ ($event)
function updateListeners(on, oldOn, add, remove, createOnceHandler, vm) {
var name, cur, old, event;
for (name in on) {
cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
...
else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm);
}
...
add(event.name, cur, event.capture, event.passive, event.params);
}
else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
}
...
}
...
function createFnInvoker(fns, vm) {
function invoker() {
var fns = invoker.fns;
if (isArray(fns)) {
var cloned = fns.slice();
for (var i = 0; i < cloned.length; i++) {
invokeWithErrorHandling(cloned[i], null, arguments, vm, "v-on handler");
}
}
else {
// return handler return value for single handlers
return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler");
}
}
invoker.fns = fns;
return invoker;
}
主要执行createFnInvoker返回一个函数。随后执行add(event.name, cur, event.capture, event.passive, event.params)方法:
function add$1(event, fn) {
target$1.$on(event, fn);
}
...
Vue.prototype.$on = function (event, fn) {
var vm = this;
if (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);
...
if (hookRE.test(event)) {
vm._hasHookEvent = true;
}
}
return vm;
};
该函数在vm._event中的event就是我们定义的model.event,fn就是invoker回调函数。当我们执行$emit的时候就会在_event对象中查找我们需要的函数。接下来还会再次执行add方法,不过执行的是另外一个add方法,添加dom的监听事件。第一次执行add方法是test组件初始化执行initEvents的时候,第二次执行是_update方法中执行createElm方法的时候:
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_1 = currentFlushTimestamp;
var original_1 = handler;
//@ts-expect-error
handler = original_1._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_1 ||
// 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_1.apply(this, arguments);
}
};
}
// target = 当前dom name = 'click' original_1=ƒ invoker()
target.addEventListener(name, handler, supportsPassive ? { capture: capture, passive: passive } : capture);
}
执行target.addEventListener(name, handler, supportsPassive ? { capture: capture, passive: passive } : capture)方法设置click事件。至此从处理v-model到设置事件已经执行完毕,接下来就是触发该函数,我们看看触发该函数发生了什么。
当点击按钮的时候会触发 _vm.$emit事件,并传入要设置的值,该函数主要触发invokeWithErrorHandling从而执行invoker方法:
var render = function render() {
var _vm = this,
_c = _vm._self._c
return _c("div", [
_c(
"div",
{
on: {
click: function ($event) {
return _vm.$emit("click", 1234)
},
},
},
[_vm._v("点我")]
),
])
}
...
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 \"".concat(event, "\"");
for (var i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info);
}
}
return vm;
};
...
// handler= ƒ invoker() args= [1234] context=vm=test vm实例
function invokeWithErrorHandling(handler, context, args, vm, info) {
var res;
try {
res = args ? handler.apply(context, args) : handler.call(context);
...
}
...
return res;
}
...
function invoker() {
var fns = invoker.fns;
if (isArray(fns)) {
var cloned = fns.slice();
for (var i = 0; i < cloned.length; i++) {
invokeWithErrorHandling(cloned[i], null, arguments, vm, "v-on handler");
}
}
else {
// return handler return value for single handlers
return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler");
}
}
这个invoker是我们定义updateListeners的时候设置的on的click执行函数。该函数绑定了v-model执行的callback函数。接下来执行invokeWithErrorHandling函数,此时的handler是v-model的callback函数,args是[1234],执行_vm.a = 1234。触发变量a的set方法给变量设置新值,并触发依赖更新视图。
总结