关于vue2双向绑定原理的源码解析。
vue2双向绑定现象是这样的:
图1
如图所示,是一个简单的tab页组件,有一个双向绑定的属性id,用于标识当前展示的标签页。
- 当id变化的时候,模板会更新;
- 当切换标签页的时候,id会同步更新
这就是双向绑定。还有就是input组件,当value变化的时候,模板会更新;当输入值的时候,value会同步变化。
不论是双向绑定还是单向绑定,都用到了vue的响应式原理,也叫数据劫持,就是当获取或者设置值的时候,插入了其他操作,导致去执行其他操作。
双向绑定还用了一个就是发布订阅模式,也就是当切换标签页的时候,触发事件从而更新id的操作;
下面说一下双向绑定的过程:
- id变化时,触发id的set方法,依赖更新,触发重新渲染模板的逻辑;
- 当切换标签页的时候,触发click事件,click事件执行$emit,emit的参数一般都是input,也就是会触发input事件。
- 因为v-model这个指令会在生成渲染函数时生成一个对象,该对象就会有一个事件callback,事件中就是给id赋值的逻辑。
- 当渲染函数生成VNode的时候,会执行一个转换函数,用于把callback事件注册到data.on,且会把事件名称转换成input
- 当hy-tabs组件执行初始化_init的时候,执行$on把input事件注册到组件上
- 所以最后通过$emit订阅input事件就可以触发id被赋值。
- 当id变化时,又会触发set方法中的依赖更新,就会重新渲染模板。
如图所示,是图1生成的渲染函数,有一个model属性,callback就是给id赋值
转换且注册callback的方法如下:
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;
}
}
下面重点说一下$on注册input事件,也就是组件初始化事件方法,首先看一下初始化事件:
Vue.prototype._init = function(options) {
...
initLifecycle(vm);
initEvents(vm);
initRender(vm);
...
}
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);
}
}
- 首先定义_events属性存储事件
- 接着会获取父组件的事件,就会得到input事件(parentListeners就是父组件的listeners,是在生成VNode赋值过去的)
- 然后更新当前组件的事件,执行updateComponentListeners
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)) {
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);
}
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
}
function createFnInvoker(fns, vm) {
function invoker() {
var arguments$1 = arguments;
var fns = invoker.fns;
if (Array.isArray(fns)) {
var cloned = fns.slice();
for (var i = 0; i < cloned.length; i++) {
invokeWithErrorHandling(cloned[i], null, arguments$1, 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
}
function invokeWithErrorHandling(handler, context, args, vm, info) {
var res;
try {
res = args ? handler.apply(context, args) : handler.call(context);
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(function(e) {
return handleError(e, vm, info + " (Promise/async)");
});
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true;
}
} catch (e) {
handleError(e, vm, info);
}
return res
}
- 可以看到,把input这个函数通过createFnInvoker创建,返回一个函数存储这个方法;然后通过add方法注册到vm中就完成了。
- 最后当执行$emit input方法时,就会去执行_events上的方法了。
Vue.prototype.$emit = function(event) {
var vm = this;
{
var 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 + "\".");
}
}
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
}