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


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

    基础代码内容如下:

    // App.vue
    
    
    
    
    // test.vue
    
    
    
    
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    解析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
      )
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    可以看出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;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    首先执行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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    返回值如下:
    在这里插入图片描述
    设置了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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    该函数会根据我们在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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    执行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("点我")]
        ),
      ])
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    生成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);
         }
     }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在遍历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;
    }
    ...
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    接下来执行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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    主要执行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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    该函数在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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    执行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");
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    这个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,找到了就会执行该函数。
  • 相关阅读:
    感受 OpenDNS
    国产管理软件勒索病毒大爆发
    2022 CCF BDCI 小样本数据分类任务 baseline
    Request和Response
    代码随想录算法训练营第53天 | ● 1143.最长公共子序列 ● 1035.不相交的线 ● 53. 最大子序和
    模糊系统与神经网络的区别,什么是模糊神经网络
    模拟退火算法
    LeetCode770之基本计算器IV(相关话题:波兰表达式,多项式运算)
    难以理解:摄像头APP,我测试好好的,发给别人就用不了
    边学边记——Java中 final 关键字的用法
  • 原文地址:https://blog.csdn.net/qq_35094120/article/details/127425815