Vue2双向数据绑定原理

302 阅读1分钟

关于vue2双向绑定原理的源码解析。 vue2双向绑定现象是这样的: image.png

图1

如图所示,是一个简单的tab页组件,有一个双向绑定的属性id,用于标识当前展示的标签页。

  1. 当id变化的时候,模板会更新;
  2. 当切换标签页的时候,id会同步更新

这就是双向绑定。还有就是input组件,当value变化的时候,模板会更新;当输入值的时候,value会同步变化。 不论是双向绑定还是单向绑定,都用到了vue的响应式原理,也叫数据劫持,就是当获取或者设置值的时候,插入了其他操作,导致去执行其他操作。
双向绑定还用了一个就是发布订阅模式,也就是当切换标签页的时候,触发事件从而更新id的操作; 下面说一下双向绑定的过程:

  1. id变化时,触发id的set方法,依赖更新,触发重新渲染模板的逻辑;
  2. 当切换标签页的时候,触发click事件,click事件执行$emit,emit的参数一般都是input,也就是会触发input事件。
    1. 因为v-model这个指令会在生成渲染函数时生成一个对象,该对象就会有一个事件callback,事件中就是给id赋值的逻辑。
    2. 当渲染函数生成VNode的时候,会执行一个转换函数,用于把callback事件注册到data.on,且会把事件名称转换成input
    3. 当hy-tabs组件执行初始化_init的时候,执行$on把input事件注册到组件上
    4. 所以最后通过$emit订阅input事件就可以触发id被赋值。
    5. 当id变化时,又会触发set方法中的依赖更新,就会重新渲染模板。

如图所示,是图1生成的渲染函数,有一个model属性,callback就是给id赋值 image.png 转换且注册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);
    }
}
  1. 首先定义_events属性存储事件
  2. 接着会获取父组件的事件,就会得到input事件(parentListeners就是父组件的listeners,是在生成VNode赋值过去的)
  3. 然后更新当前组件的事件,执行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
}
  1. 可以看到,把input这个函数通过createFnInvoker创建,返回一个函数存储这个方法;然后通过add方法注册到vm中就完成了。
  2. 最后当执行$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
    }