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

上篇文章是分析双向绑定在组件上的用法,这篇文章我们来看看在普通dom上的用法。

前提

// app.vue



// render
var render = function render() {
  var _vm = this,
    _c = _vm._self._c
  return _c("div", [
    _vm._v("\n  " + _vm._s(_vm.a) + "---" + _vm._s(_vm.b) + "\n  "),
    _c("input", {
      directives: [
        { name: "model", rawName: "v-model", value: _vm.a, expression: "a" },
      ],
      domProps: { value: _vm.a },
      on: {
        input: function ($event) {
          if ($event.target.composing) return
          _vm.a = $event.target.value
        },
      },
    }),
  ])
}

可以看到v-model="b"被解析为三部分:第一部分是directives,我们知道这个是在自定义指令中用到过。v-model也是一个指令,初始值为a变量的值。第二部分是domProps,这个里面有个value,value的值会直接给dom。第三部分是on,这个是设置了一个oninput事件,并将当前的值赋给a变量。下面我们从源码具体来看看是如何使用这三部分的。

分析

看过前几篇文章的估计都知道执行完render函数后生成了vnode,然后调用_update方法。_update方法会遍历每一个vnode去执行createElm方法,我们重点讲解这个方法。生成的vnode如下图所示:
从源码看vue(v2.7.10)中的v-model(双向绑定)之input的原理_第1张图片

function createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {
   ...
   var data = vnode.data;
   var children = vnode.children;
   var tag = vnode.tag;
   if (isDef(tag)) {
       ...
       vnode.elm = vnode.ns
           ? nodeOps.createElementNS(vnode.ns, tag)
           : nodeOps.createElement(tag, vnode);
       setScope(vnode);
       createChildren(vnode, children, insertedVnodeQueue);
       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);
   }
}

我们重点看invokeCreateHooks方法:

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)方法,该方法有8个函数。data上的directives会调用updateDirectives方法去处理,data上的domProps会调用updateDomProps去处理,data上的on会调用updateDomListeners去处理。
从源码看vue(v2.7.10)中的v-model(双向绑定)之input的原理_第2张图片
先看第一个触发的函数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;
}

可以看出先获取on对象,然后调用 normalizeEvents(on)方法,在本次未触发,然后执行updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)方法:

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);
          if (isUndef(cur)) {
              warn$2("Invalid handler for event \"".concat(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);
          }
         ...
      }
      ...
  }

先取出回调函数,最后执行add方法:

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.addEventListener(name, handler, supportsPassive ? { capture: capture, passive: passive } : capture);
}

这里主要是vue对bug的一些兼容性处理,最后调用了addEventListener去给当前dom绑定我们定义的事件。
再看第二个触发的函数updateDomProps:

function updateDOMProps(oldVnode, vnode) {
      if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) {
          return;
      }
      var key, cur;
      var elm = vnode.elm;
      var oldProps = oldVnode.data.domProps || {};
      var props = vnode.data.domProps || {};
      // clone observed objects, as the user probably wants to mutate it
      if (isDef(props.__ob__) || isTrue(props._v_attr_proxy)) {
          props = vnode.data.domProps = extend({}, props);
      }
     ...
      for (key in props) {
          cur = props[key];
          ...
          if (key === 'value' && elm.tagName !== 'PROGRESS') {
              // store value as _value as well since
              // non-string values will be stringified
              elm._value = cur;
              // avoid resetting cursor position when value is the same
              var strCur = isUndef(cur) ? '' : String(cur);
              if (shouldUpdateValue(elm, strCur)) {
                  elm.value = strCur;
              }
          }
          ...
      }
  }

首先会取出我们的domProps,由于我们的key是value,所以当elm上的值和我们要给的值不一样的时候此时会执行elm.value = strCur重新给dom赋值。
第三个触发的就是updateDirectives方法:

function _update(oldVnode, vnode) {
      var isCreate = oldVnode === emptyNode;
      var isDestroy = vnode === emptyNode;
      var oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context);
      var newDirs = normalizeDirectives(vnode.data.directives, vnode.context);
      var dirsWithInsert = [];
      var dirsWithPostpatch = [];
      var key, oldDir, dir;
      for (key in newDirs) {
          oldDir = oldDirs[key];
          dir = newDirs[key];
          if (!oldDir) {
              // new directive, bind
              callHook(dir, 'bind', vnode, oldVnode);
              if (dir.def && dir.def.inserted) {
                  dirsWithInsert.push(dir);
              }
          }
          else {
              // existing directive, update
              dir.oldValue = oldDir.value;
              dir.oldArg = oldDir.arg;
              callHook(dir, 'update', vnode, oldVnode);
              if (dir.def && dir.def.componentUpdated) {
                  dirsWithPostpatch.push(dir);
              }
          }
      }
      if (dirsWithInsert.length) {
          var callInsert = function () {
              for (var i = 0; i < dirsWithInsert.length; i++) {
                  callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode);
              }
          };
          if (isCreate) {
              mergeVNodeHook(vnode, 'insert', callInsert);
          }
          else {
              callInsert();
          }
      }
      if (dirsWithPostpatch.length) {
          mergeVNodeHook(vnode, 'postpatch', function () {
              for (var i = 0; i < dirsWithPostpatch.length; i++) {
                  callHook(dirsWithPostpatch[i], 'componentUpdated', vnode, oldVnode);
              }
          });
      }
      if (!isCreate) {
          for (key in oldDirs) {
              if (!newDirs[key]) {
                  // no longer present, unbind
                  callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy);
              }
          }
      }
  }

normalizeDirectives方法会给当前对象新增def,这是hook方法:
从源码看vue(v2.7.10)中的v-model(双向绑定)之input的原理_第3张图片
首先执行dirsWithInsert.push(dir),然后执行mergeVNodeHook(vnode, ‘insert’, callInsert)方法,该方法会把回调函数给invoker,执行完invoker就会删除回调函数,相当于只会执行一次。然后执行invokeCreateHooks的insertedVnodeQueue.push(vnode)保存当前vnode。那么啥时候执行insertedVnodeQueue的函数呢?这个会在最开始执行patch函数返回最终的elm之前会执行invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)方法去执行里面的函数。我们来看看为啥要执行insertd函数:

inserted: function (el, binding, vnode, oldVnode) {
  ...
    else if (vnode.tag === 'textarea' || isTextInputType(el.type)) {
        el._vModifiers = binding.modifiers;
        if (!binding.modifiers.lazy) {
            el.addEventListener('compositionstart', onCompositionStart);
            el.addEventListener('compositionend', onCompositionEnd);
            // Safari < 10.2 & UIWebView doesn't fire compositionend when
            // switching focus before confirming composition choice
            // this also fixes the issue where some browsers e.g. iOS Chrome
            // fires "change" instead of "input" on autocomplete.
            el.addEventListener('change', onCompositionEnd);
            /* istanbul ignore if */
            if (isIE9) {
                el.vmodel = true;
            }
        }
    }
}

在执行input的insertd方法时会启用几个监听函数,其实这个监听就是在输入中文的时候不会还没输完就触发回调,而是输完拼音以后再触发。

总结

使用v-model会将当前的input设置oninput监听事件将最新的值赋给当前绑定的变量,而且会使用domProps给当前dom设置初始值。最后触发insertd的钩子设置composition事件,这里之所以用insertd钩子是因为监听事件只需要设置一次。

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