一、数据代理
实现数据代理核心就是 Object.defineProperty()方法,而IE8及以下是不支持该方法的,所以vue不能用在IE8及以下。
回归正题,data里的属性可以直接vm.xxx,那是因为用defineProperty方法将data中的属性增加到vm上了,主要代码如下:
Object.keys(data).forEach(function (key) {
var me = this;
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
//vm.xxx获取属性值时调用该方法
get: function proxyGetter() {
return me.data[key];
},
//当监听到vm.xxx属性值发生改变时,同时更新data中的属性值
set: function proxySetter(newVal) {
me.data[key] = newVal;
}
})
})
二、模板解析
模板解析用到了DocumentFragment
- 将原生节点拷贝到fragment中
- 对含有大括号的文本节点、指令层层处理
- 将处理完的fragment添加回页面,渲染完成
初始化:
//事件处理(解析事件指令)
eventHandler: function(node, vm, exp, dir) {
var eventType = dir.split(':')[1],
fn = vm.$options.methods && vm.$options.methods[exp];
if(eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
}
//根据属性名获取data中的值(解析普通指令及大括号表达式)
_getVal: function(vm, exp) {
var val = vm._data;
exp = exp.split('.');
exp.forEach(function(k) {
val = val[k];
})
return val;
}
//更新节点,以v-text也是{{}}为例
textUpdater: function (node, value) {
node.textContent = typeof(value) == 'undefined' ? '' : value;
}
三、数据绑定
数据绑定通过数据劫持的方式来更新页面
首先看下面一张图:
如何进行数据劫持:
利用Object.defineProperty方法将data中的属性重新配置,加上getter和setter方法
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知订阅者
dep.notify();
}
});
Dep是一个构造函数,每一个属性对应一个dep,数据劫持时创建实例
function Dep() {
this.id = uid++;
this.subs = [];
}
watcher也是一个构造函数,每一个表达式对应一个watcher,监视数据变化来更新页面
function Watcher(vm, exp, cb) {
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.depIds = {};
this.value = this.get();
}
模板解析时每次创建watcher对象,都会去获取data的属性值,这样会触发getter中的dep.depend();,继而会触发addDep方法
addDep: function(dep) {
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this); //属性值改变时,dep会通知订阅者watcher
this.depIds[dep.id] = dep; //这句话加上if条件判断主要是为了防止属性值改变时重复建立联系,只允许初始化的时候建立联系
}
}
这样dep和watcher就建立起了联系
举个例子:
<div id="app">
<p>{{name}}</p>
<p v-text="name"></p>
<p>{{frends.age}}</p>
<button v-on:click="update">更新</button>
</div>
new Mvvm ({
data:{
name: 'xiaoming',
frends: {
name: 'xiaohong',
age: 18
}
},
methods: {
update () {
this.name = 'xinxin';
this.frends.age = 20;
}
}
})
数据劫持时创建的Dep实例有:
{id: 0, sub[]} {id: 1, sub[]} {id: 2, sub[]} {id: 3, sub[]}
初始化视图模板解析时,每创建一个watcher实例,都要和dep建立联系:
//watcher<0>加尖括号是为了区分每次创建的watcher实例
第一个<p>{{name}}</p>:
name属性对应的dep={id:0, sub[watcher<0>]}
watcher<0>.depIds={0: {id:0, sub[watcher<0>]}}
第二个<p v-text="name"></p>
name属性对应的dep={id:0, sub[watcher<0>, watcher<1>]}
watcher<1>.depIds={0: {id:0, sub[watcher<0>, watcher<1>]}}
第三个<p>{{frends.age}}</p>
frends属性对应的dep={id:1, sub[watcher<2>]}
watcher<2>.depIds={1: {id:1, sub[watcher<2>]}}
age属性对应的dep={id:3, sub[watcher<2>]}
watcher<2>.depIds={1: {id:1, sub[watcher<2>]}, 3: {id:3, sub[watcher<2>]}}
当name属性变化时->setter会被调用->通知dep->调用watcher里的更新页面的方法->getter方法会被调用->通过watcher的depIds属性防止重复创建联系
四、双向绑定
双向绑定中的一个方向model–>view的绑定就是上面说的模板解析,另一个方向就是绑定input事件,监听到input标签value值变化,则更新data,然后更新页面
model: function (node, vm, exp) {
var me = this;
var val = this._getVMVal(vm, exp);
node,addEventListener('input', function (e) {
var newValue = e.target.value;
if(val === newValue) {
return;
}
me._setVMVal(vm, exp, newValue);
val = newValue;
})
}
_setVMVal: function(vm, exp, value) {
var val = vm._data;
exp = exp.split('.');
exp.forEach(function(k, i) {
// 非最后一个key,更新val的值
if (i < exp.length - 1) {
val = val[k];
} else {
val[k] = value;
}
});
}