Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue 2.0 v-model实现 #18

Open
CommanderXL opened this issue Jan 4, 2019 · 0 comments
Open

Vue 2.0 v-model实现 #18

CommanderXL opened this issue Jan 4, 2019 · 0 comments
Labels
Vue.js Extra attention is needed

Comments

@CommanderXL
Copy link
Owner

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节点后完成的。

@CommanderXL CommanderXL added the Vue.js Extra attention is needed label Jan 6, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Vue.js Extra attention is needed
Projects
None yet
Development

No branches or pull requests

1 participant