某天夜晚,我在沉沉的睡梦中突然惊醒0.0,想到做了这么久Vue了,到底它是肿么帮助我实现双向绑定的呢?当我陷入了沉思之中时,困意再次来袭。。。
今天想到了这个问题,我必须对的起这个噩梦!
对于这种我不懂的问题,第一时间就是去翻看官方的api。哦吼,果然有介绍,但是呢,这些字我都认识,但是为什么就是看不懂呢。
归根到底,是因为Object.defineProperty到底是个啥我不知道。
一、Object.defineProperty是什么玩意
obj和prop都很好理解,descriptor:属性描述符就有点难理解了。其实它是一个JavaScript对象隐藏的一些属性。其中包括
configurable:(Boolean类型 )该属性描述符是否可以改变。默认为 false。
enumerable:(Boolean类型 )该属性描述符是否可以枚举。默认为 false。
value:(数值,对象,函数等)任何有效的 JavaScript 值,默认为 undefined。
writable:(Boolean类型 )是否可以赋值运算,默认为 false。
get:(函数类型 )方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象),默认为 undefined。
set:(函数类型 )当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值,默认为 undefined。
说白了,这个object.defineProperty的方法就是一个定义和修改对象的操作,而vue主要是用到这个方法中的set和get
我们来看一下,object.defineProperty里面的set和get都做了什么。
//正常情况下定义一个对象
let testObject={
id='1',
name='9527'
}
console.log(testObject.name) //'9527'
//如果我们用object.defineProperty
let testObject = {};
let name = '';
Object.defineProperty(testObject, 'name', {
set(val) {
name = val;
console.log("从此你就叫", name)
},
get() {
console.log('yes madam!');
return '我是' + name + '!';
}
});
//这里触发了对象的set方法,因此打印出来 ‘从此你就叫 9527’
testObject.name = '9527';
//这里触发了对象的get方法,因此打印出
//‘yes madam!
//我是9527!’
console.log(testObject.name);
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。
二、vue如何根据setter追踪数据的变化
通过vue追踪的路径图可以看出,双向绑定在三个步骤:
1、组件渲染VMTree (Compile)。
2、object.defineProperty中的getter/setter,来进行数据劫持 (Observer)。
3、组件渲染的过程中把“接触”过的数据属性getter记录为依赖。之后当依赖项的 setter 触发时,会通知**(watcher)**,从而使它关联的组件重新渲染。
大致的思路摸清楚了,一个MVVM的框架核心点在于三个:Compile编译模板,Observer数据监听,watcher数据订阅。
- Observer监听器,监听数据,一旦数据发生变化,通知Watcher
- Watcher观察者,监测属性的变化,收到变化通知后,修改页面视图
- Compile扫描和解析每个节点的相关指令,初始化页面的数据从而初始化Watcher。
有了 总体的思路,我们一步步来实现一个MVVM的框架。
三、动手实现一个MVVM
1、创造一个Observer监听器
//创建一个监听器
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
//轮询出对象内的属性名及属性值
Object.keys(data).forEach(function (key) {
defineReactive(data, key, data[key]);
});
}
//定义响应
function defineReactive(data, key, val) {
//如果属性内的值还是对象,继续往下轮询
observe(val);
//对象的属性添加getter和setter
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
return val;
},
set: function (newVal) {
if (val === newVal) {
return;
}
val = newVal;
console.log(key + '属性已经被监听,值为:' + newVal.toString());
}
});
}
//初始化一个对象
let policeman = {
dept: {
deptName: ''
},
id: '',
name: ''
};
observe(policeman);
policeman.dept.deptId = 'fh';//此处由于初始化的对象里没有,因此不被监听
policeman.dept.deptName = '飞虎队';//deptName属性已经被监听,值为:飞虎队
policeman.id = '9527';//id属性已经被监听,值为:9527
policeman.name = '周星星';//name属性已经被监听,值为:周星星
这样就创建了一个能够监听数据变化的监听器,下一步引入Watcher观察者,这位的作用在于,收集数据的依赖,并更新数据同时提醒数据发生变化。
2、引入Watcher观察者
在引入观察者之前,先定义一个Dept用来接收Observer传递过来的数据,并且初始化和通知观察者
//给依赖数据开辟内存
function Dept() {
this.watchers = []
}
//prototype方法用来给对象添加属性和方法,
//只作用在对象函数上
Dept.prototype = {
//添加观察者
addWatcher(watcher) {
this.watchers.push(watcher)
},
//通知每一个观察者更新数据
notify: function () {
this.watchers.forEach(function (watcher) {
watcher.update(); //通知每个观察者检查更新
})
}
};
有了存放观察者的地方了,开始创建观察者
function Watcher(vm, exp, cb) {
this.vm = vm; //指向vm的作用域
this.exp = exp; //绑定属性的key值
this.cb = cb; //闭包
this.value = this.get();
}
Watcher.prototype = {
//数据更新
update: function () {
this.run();
},
run: function () {
let value = this.vm.data[this.exp];
let oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function () {
Dep.target = this; // 缓存自己
let value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
};
修改之前Observer的get和set方法,在里面加入Dept
function defineReactive(data, key, val) {
//如果属性内的值还是对象,继续往下轮询
observe(val);
//添加Dept
let dept = new Dept();
//对象的属性添加getter和setter
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
//判断是否需要添加观察者
if (Dep.target) {
dept.addWatcher(Dep.target);
}
return val;
},
set: function (newVal) {
if (val === newVal) {
return;
}
val = newVal;
console.log(key + '属性已经被监听,值为:' + newVal.toString());
dept.notify(); // 如果数据变化,通知所有订阅者
}
});
};
这样整个数据层面的监听和观察就基本完成了,接下来研究如何实现页面数据的初始化已经编译解析。
3、dom解析器Compiler
function myMvvM(data,el,exp) {
let self = this;
this.data = data;
//Object.keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组
Object.keys(data).forEach(function(key) {
//绑定代理属性
self.proxyKeys(key);
});
//数据创建监听
observe(data);
// 初始化模板数据的值
el.innerHTML = this.data[exp];
//数据创建观察者
new Watcher(this,exp,function(value) {
//此处是一个回调函数,如何观察到数据变化时调用,如何返回到页面
el.innerHTML = value;
});
return this;
}
myMvvM.prototype = {
proxyKeys:function(key) {
let self = this;
Object.defineProperty(this,key,{
enumerable:false,
configurable:true,
get:function proxyGetter() {
return self.data[key];
},
set:function proxySetter(newVal) {
self.data[key] = newVal;
}
});
}
}
在html页面中使用myMvvM
<html>
<meta charset="utf-8">
<body>
<h1 id="name"></h1>
</body>
<script src="mvvm/observer.js"></script>
<script src="mvvm/Watcher.js"></script>
<script src="mvvm/index.js"></script>
<script>
//获取页面的name标签
var ele = document.querySelector('#name');//<h1 id="name"></h1>
var myMvvM = new myMvvM({
name: 'madam!,我是周星星'
}, ele, 'name');
window.setTimeout(function () {
console.log('模拟2s后更新数据');
myMvvM.name = '工号:9527';
}, 2000);
</script>
</html>
好了这样一个简单的双向绑定就实现了,但是和vue中的似乎还差了点。上面的例子中并没有涉及到,DOM的解析,而是固定了一个节点作为模拟的数据,下面来实现Compiler解析器的解析和绑定。
解析过程比较复杂,可以理解为:
- 解析模板并且替换掉模板的数据
- 将模板指令对应的节点绑定对应的更新函数,初始化相应的观察者
步骤为:
1、先把页面中的 DOM 暂存在DocumentFragment中
2、编译出元素节点(v-model、v-text…)和文本节点{{message}}
3、将编译好的内容放回到页面中
Vue模板渲染是一个很大的点,虽然在使用中不太能意识到他的存在。由于这一个方面可研究的比较多,准备再单独写一篇文章记录一下。