一、实现原理
1. 从数据到视图的更新,是需要对数据进行监听劫持,这里我们设置一个监听器Observer来实现对所有数据的监听;
2. 设置一个订阅者Watcher,收到属性的变化通知并执行相应的函数,从而更新视图;
3. 设置一个解析器Compiler,解析视图DOM中所有节点的指令,并将模板中的数据进行初始化,然后初始化对应的订阅器。
二、Observer
监听器
Observer是一个数据监听器,核心是Object.defineProperty()
,对所有属性监听,利用递归来遍历所有的属性值,对其进行Object.defineProperty()
操作。
// observer.js文件
function Observer(data) { // 监听器
this.walk(data)
}
Observer.prototype = {
walk(data) {
if( !data || typeof data !== 'object') {
return false
}
Object.keys(data).forEach(key => { // 遍历操作
this.defineProperty(data, key, data[key])
})
},
defineProperty(data, key, val) { // set/get方法
this.walk(val) // key 对应的又是 字典对象时
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
return val
},
set(newVal) {
if(val !== newVal) {
console.log('新值: '+ newVal);
val = newVal
}
}
})
}
}
// 测试数据
var zhangsan = {
name: '张三',
age: 18,
score:{
math: 100,
english:90
}
}
// 添加监听
new Observer(zhangsan)
zhangsan.age = 20
zhangsan.score.english = 99
确保都数据中的每个属性都转为getter/setter方法!
三、Dep
订阅器
在最开始的结构中,通过Observer
劫持并监听数据,当属性发生改变时,即通知调用到Watcher
执行界面的更新。
这先建立一个简单的概念,Observer
相当于作家,Watcher
相当于读者(订阅者),现在是属于直接的关系。
但假如有新的Watcher
订阅者时,怎么办呢?
这就还需要一个订阅器Dep
,订阅器Dep
是用来收集所有订阅者的。
Dep
相当于是杂志社,Watcher
作为订阅者,首先需要向杂志社订阅杂志,这样当有新的杂志(消息)产生时,Dep
才会通知Watcher
,如下图所示:
// dep.js文件
function Dep(){ // 订阅器
this.subs = []
}
Dep.prototype = {
addSub(watcher){ // 添加订阅者
this.subs.push(watcher)
},
notify(){ // 通知订阅者
this.subs.forEach(watcher=>{
watcher.run()
})
}
}
四、Dep
订阅器如何使用
Dep
主要的功能是添加新的订阅者,以及通知订阅者。
但什么时候添加订阅者,什么是通知订阅者呢?
- 当数据发生改变时,即set的时就要通知订阅者; [新杂志发布(数据更新set),这就是通知订阅者]
- 在获取数据时,即get时添加订阅者; [想要阅读杂志,这要先获取到杂志(获取数据get)]
杂志社和订阅者的示例,进行对比才知道为什么放在set或get方法中!
在获取的时(get)就需要加上条件限制,并不是所有获取的都是添加订阅者
操作。
// Observer.prototype中的defineProperty
defineProperty(data, key, val) { // set/get方法
this.walk(val) // key 对应的又是 字典对象时
// 订阅器
var dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
if(条件){ // 符合某个条件说明是新的订阅者,才进行添加操作
dep.addSub(watcher);
}
return val;
},
set(newVal) {
if(val !== newVal) {
console.log('新值: '+ newVal);
val = newVal;
// 通知订阅者
dep.notify()
}
}
})
}
五、Watcher
订阅者
Watcher
订阅者,在开始的时就要是添加到Dep
订阅器中(杂志社当有新的杂志产生时,才知道通知谁。
其实也就是在Observer
监听器的get
方法中执行时,添加Watcher
订阅者操作。但get
方法会被调用多次,这就可以在Dep
订阅器中添加一个Dep.target
标识是否为新订阅者,添加成功后再将其去掉。
记住: 只有是新的订阅者,才是添加操作,否则不添加操作!
// watcher.js文件
// vm Vue的实例
// exp data中的key
// cb 回调函数
function Watcher(vm, exp, cb){
this.vm = vm
this.exp = exp
this.cb = cb
// 获取key对应的值,同时将watcher添加到Dep的队列中
this.value = this.get()
}
Watcher.prototype = {
get(){ // 获取数据时,添加订阅者
// 添加一个标识的意思
Dep.target = this
// 获取值,即触发get方法 [添加订阅者]
var val = this.vm[this.exp]
// 已经添加完成
Dep.target = null
return val
},
update(){ // 更新界面
this.run()
},
run(){
var val = this.vm[this.exp]
if(val != this.value){ // 新值和旧值判断
var oldValue = this.value
this.value = val
this.cb.call(this.vm, val, oldValue) // 回调函数
}
}
}
Observer
监听器中,条件就有了,添加上即可:
// observer.js文件的 Observer.prototype
defineProperty(data, key, val) {
this.walk(val) // key 对应的又是 字典对象时
// 订阅器
var dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() { // Dep.target存储的就是Watcher实例
if(Dep.target){ // 判断是否要添加(新订阅者)
dep.addSub(Dep.target)
}
return val;
},
set(newVal) {
if(val !== newVal) {
console.log('新值: '+ newVal)
val = newVal
// 通知订阅者
dep.notify()
}
}
})
}
基本流程操作,参考下图:
六、效果
从上图中可以看到,现阶段只是实现一个简单的从数据到视图的更新,后面我们再完善解析器Compiler(备注: 此时效果还是有问题的)。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<div id="app">
<h3>名字: {{name}}</h3>
<h3>技能: {{skill}}</h3>
</div>
<div>
请输入你的名字:<input type="text" class="text" value="" />
<input type="button" class="bt" value="确定" />
</div>
<script src="dep.js" type="text/javascript" charset="utf-8"></script>
<script src="watcher.js" type="text/javascript" charset="utf-8"></script>
<script src="observer.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
function Vue(options) {
this.el = options.el // 元素
this.data = options.data // 数据
this.watcher = {} // 属性、数据、元素 的关联
Object.keys(this.data).forEach(key => { // 数据代理
this.proxyKeys(key);
})
// 监听器
new Observer(this.data);
// 解析DOM
this.compile()
}
Vue.prototype = {
proxyKeys(key) {
var self = this
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get() {
return self.data[key]
},
set(newVal) {
self.data[key] = newVal
}
})
},
compile() { // 解析DOM即更新数据
// 获取到对象绑定的元素
var ele = document.querySelector(this.el);
// 所有子元素
var childEls = ele.childNodes;
// 创建fragment
var fragment = document.createDocumentFragment();
// 获取到第一个子元素
var child = ele.firstChild;
while(child) {
// 将Dom元素移入fragment中
// appendChild: 如果被插入的节点已经存在于当前文档的文档树中,则那个节点会首先从原先的位置移除,然后再插入到新的位置
fragment.appendChild(child)
// 再获取第一个(其实就是下一个下一个的操作)
child = ele.firstChild
}
// 遍历所有子元素
[].slice.call(fragment.childNodes).forEach(el => {
var reg = /\{\{(.*)\}\}/;
var text = el.textContent;
if(reg.test(text)) {
var key = reg.exec(text)[1];
el.textContent = this.data[key];
// 新的订阅者
new Watcher(this, key, value=>{
ele.innerHTML = value;
})
}
})
// 添加
ele.appendChild(fragment)
}
}
// 创建Vue对象
var myVue = new Vue({
el: '#app',
data: {
name: '阿童木',
skill: 'web前端开发',
}
})
// 修改名字
document.querySelector('.bt').onclick = function() {
myVue.data.name = document.querySelector('.text').value
}
</script>
</body>
</html>