Biu-blog
Biu-blog copied to clipboard
Vue 2.0 v-model实现
v-model的实现
v-model是Vue内部实现的一个指令。它所提供的基本功能是在一些表单元素上实现数据的双向绑定。基本的使用方法就是:
<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属性,自定义事件,初始化钩子函数等,而attrs,domProps,on属性最终都会绑定到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,那么首先完成component的vm实例化,接下来递归的对子节点进行实例化
注意当子 VNode 全部渲染成真实的 dom 节点,并挂载到父节点后,开始调用invokeCreateHooks方法,触发dom节点create阶段所包含的钩子函数来完成对dom节点添加attrs,domProps,dom事件等:(具体的可参见 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相关的domProps和dom事件的钩子函数,首先来看下更新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方法完成dom的value的初始化。
接下来看下是如何绑定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的值,即调用val的setter方法。因为之前在创建 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节点后完成的。