CommanderXL / Biu-blog

个人博客
432 stars 39 forks source link

Vue 2.0 v-model实现 #18

Open CommanderXL opened 5 years ago

CommanderXL commented 5 years ago

v-model的实现

v-modelVue内部实现的一个指令。它所提供的基本功能是在一些表单元素上实现数据的双向绑定。基本的使用方法就是:


<div id="app">
  <input v-model="val">
</div>

new Vue({
  el: '#app',
  data () {
    return {
      val: 'default val'
    }
  },
  watch: {
    val (newVal) {
      console.log(newVal)
    }
  }
})

页面初次渲染完成后,input输入框的值为default val,当你改变input输入框的值的时候,你会发现控制台也打印出了输入框当中新的值。接下来我们就来看下v-model是如何完成数据的双向绑定的。

首先第一步,在这个vue实例化开始后,首先将data属性数据变成响应式的数据。接下来完成页面的渲染工作的时候,首先编译html模板:

(function() {
  with (this) {
    return _c('div', {
      attrs: {
          "id": "app"
      }
    }, [_c('input', {
      directives: [{
        name: "model",
        rawName: "v-model",
        value: (val),
        expression: "val"
      }],
      attrs: {          // 最终绑定到input的type属性上
        "type": "text"
      },
      domProps: {       // 最终绑定到input的value属性上,设定input的value初始值
        "value": (val)
      },
      on: {             // 最终会给input元素添加的dom事件
        "input": function($event) {
          if ($event.target.composing)
            return;
          val = $event.target.value   // 响应input事件,同时获取到输入到Input输入框当中的值,并修改val的值
        }
      }
    })])
  }
}
)

接下来我们深入细节的看下整个绑定的过程,以及在页面当中修改input输入框中的值后,如何使得模型数据也发生变化。

示例当中是在input元素上绑定的v-model指令,它是属于built in elements,因此不同于自定义component创建VNode过程中还需要进行获取props属性,自定义事件,初始化钩子函数等,而attrsdomPropson属性最终都会绑定到dom元素上。

当调用_c方法完成后,即VNode都已经生成完毕,开始将VNode渲染成真实的dom节点并挂载到document中去:

function mountComponent (
  vm,
  el,
  hydrating
) {
  ...
  updateComponent = function () 
    // vm._render首先构建完成vnode
    // 然后调用vm._update方法,更vnode挂载到真实的DOM节点上
    vm._update(vm._render(), hydrating);
  };
  ...
  new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);
  ...
}

在页面初始化的阶段:

function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,  // 父节点
  refElm,
  nested,
  ownerArray,
  index
) {

  ... 
  var data = vnode.data;          // 描述VNode属性的数据
  var children = vnode.children;  // VNode的子节点
  var tag = vnode.tag;            // VNode标签

  // 实例化自定义component vnode
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    return
  }

  ...
  vnode.elm = vnode.ns
    ? nodeOps.createElementNS(vnode.ns, tag)
    : nodeOps.createElement(tag, vnode);
  setScope(vnode);

  /* istanbul ignore if */
  {
    // 挂载子节点,vnode为父级vnode
    createChildren(vnode, children, insertedVnodeQueue);
    // 触发内部的create钩子函数
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue);
    }
    // 将vnode生成的dom节点插入到真实的dom节点当中
    insert(parentElm, vnode.elm, refElm);
  }
  ...
}

// TODO: 递归如何描述

在渲染VNode过程当中,如果是自定义的component VNode,那么首先完成componentvm实例化,接下来递归的对子节点进行实例化

注意当子 VNode 全部渲染成真实的 dom 节点,并挂载到父节点后,开始调用invokeCreateHooks方法,触发dom节点create阶段所包含的钩子函数来完成对dom节点添加attrsdomPropsdom事件等:(具体的可参见 createPatchFunction 方法中对于 create 阶段所有的回调函数的初始化)

function invokeCreateHooks (vnode, insertedVnodeQueue) {
  for (var i$1 = 0; i$1 < cbs.create.length; ++i$1) {
    cbs.create[i$1](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); }
  }
}

在这里我们只关心和v-model相关的domPropsdom事件的钩子函数,首先来看下更新domProps的钩子函数:

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__)) {
    props = vnode.data.domProps = extend({}, props);
  }

  for (key in oldProps) {
    if (isUndef(props[key])) {
      elm[key] = '';
    }
  }
  for (key in props) {
    cur = props[key];
    ...
    // 如果是input的value属性
    if (key === 'value') {
      // 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)) {
        // 更新dom对应的value值
        elm.value = strCur;
      }
    } else {
      elm[key] = cur;
    }
  }
}

dom初次创建的过程中,通过updateDOMProps方法完成domvalue的初始化。

接下来看下是如何绑定dom事件的:

// 更新dom绑定的事件
function updateDOMListeners (oldVnode, vnode) {
  if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
    return
  }
  var on = vnode.data.on || {};
  var oldOn = oldVnode.data.on || {};
  // 设置全局的dom target$1对象
  target$1 = vnode.elm;
  normalizeEvents(on);
  updateListeners(on, oldOn, add$1, remove$2, vnode.context);
  target$1 = undefined;
}

// 注意add$1方法,它完成了向dom target$1绑定事件的功能。相应的remove$2方法是将对应的事件从dom节点上删除
function add$1 (
  event,
  handler,
  once$$1,
  capture,
  passive
) {
  handler = withMacroTask(handler);
  if (once$$1) { handler = createOnceHandler(handler, event, capture); }
  target$1.addEventListener(
    event,
    handler,
    supportsPassive
      ? { capture: capture, passive: passive }
      : capture
  );
}

在本例当中,即向input节点绑定input事件:

input.addEventListener('input', function ($event) {
  if ($event.target.composing)
    return;
  val = $event.target.value 
})

当改变input输入框的内容时,触发input事件执行对应的回调函数,这个时候便会改变响应式数据val的值,即调用valsetter方法。因为之前在创建 input 的 VNode 的时候,val 收集到了这个 VNode 对应的 render watcher。所以当 val 的 setter 被触发的时候,会让 input 对应的 render watcher 重新执行,这样也就会触发这个 dom 节点的 diff 和渲染的工作。

// 创建VNode环节 // 渲染的环节 // 绑定dom

// TODO: 总体的的概述 // 如何绑定/更新domProps // 绑定原生的dom事件 // 和dom相关的attrs、domProps、原生的dom事件、style等,都是在将vnode渲染成真实的dom元素后,并关在到父dom节点后完成的。