从源码看vue(v2.7.10)中的v-model(双向绑定)之组件的原理

前几篇文章我们分析了computed、watch以及双向绑定的原理,有了前面的基础我们继续分析v-model的原理。

基础代码内容如下:

// App.vue




// test.vue






解析v-model

在App.vue中执行render函数的时候代码如下:

// App.vue template

被编译为:
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;
    }
}

返回值如下:
从源码看vue(v2.7.10)中的v-model(双向绑定)之组件的原理_第1张图片
设置了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到设置事件已经执行完毕,接下来就是触发该函数,我们看看触发该函数发生了什么。

触发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方法给变量设置新值,并触发依赖更新视图。

总结

  1. 双向绑定首先会解析v-model获取回调函数和初始值,test组件会从parent获取v-model解析的初始值并给当前的props设置的值。
  2. 在渲染的时候会给dom绑定点击事件并将$emit方法解析为_vm.$emit方法,点击时会执行该方法然后执行invoker函数,invoker函数绑定了v-model的callback函数从而执行_vm.a = 1234,触发变量a的set方法更新视图。
  3. v-model的本质就是在test子组件中去执行v-model的回调函数并将最新的值传递过去。其中组件定义的props作用是校验model中prop的数据类型。model中的prop是用来接收父组件v-model中的初始值,model中定义的event会在初始化组件的时候放到vm._events里面,执行$emit方法的时候会去该对象里面查找event,找到了就会执行该函数。

你可能感兴趣的:(vue2.7.10原理,vue.js,javascript,前端)