文章目录
Vue中的观察者模式
各位看官有兴趣的可以先从 Vue中的代理模式以及JS中简单实现观察者模式这两篇文章看起,这样子各位看官对Vue中的观察者模式理解起来就会简单而且深刻很多。
初始化Vue
/**
* 初始化vue
* @param {*} vm vue对象
*/
function initState(vm) {
let opt = vm.$options;
// 初始化data对象
if (opt.data) {
initData(vm);
}
// 初始化watch
if (opt.watch) {
initWatch(vm);
}
}
看过 Vue中的代理模式的有没有发现上面部分代码和那篇文章比是不是有什么不同。
初始化Data
/**
* 初始化Data
* @param {*} vm vue对象
*/
function initData(vm) {
// 获取data
let data = vm.$options.data
// vue对象中创建 $data 方法
// data || {} 表示如果data为null则直接返回{}空对象
vm.$data = data || {};
if (typeof data != 'object' || data == null) {
return;
}
else {
new Observe(data, vm);
}
}
初始化观察者类
// 开始watch之旅
/**
* 初始化watch
* @param {*} vm
*/
function initWatch(vm) {
// 获取vue中的watch对象
let watch = vm.$options.watch;
// 循环获取watch中的key
for (const key in watch) {
// 根据key获取watch中的方法
handler = watch[key];
// 创建watch
createWatch(vm, key, handler);
}
}
为目标类Target属性赋予观察者的实例,类似于依赖关系
function pushTarget(watcher) {
Dep.target = watcher;
}
function popTarget() {
Dep.target = null;
}
目标类
看过JS中简单实现观察者模式这篇文章的再次看到下面的代码是不是有种似曾相识感觉,在这里可比那块的方便多了,可以通过depend方法自动传入观察者类的构造函数中的集合中,并同时将该观察者添加到目标类中的观察者实例容器中
// dep编号
let uid = 0;
/**
* 目标类(看作哨兵)
*/
class Dep {
constructor() {
this.id = uid++;
// 存储观察者(士官)的容器
this.watchers = [];
}
/**
* 在哨兵的笔记本中记录需要通知的士官
* @param {*} watcher
*/
addWatch(watcher) {
this.watchers.push(watcher);
}
/**
* Dep.target此时经过了pushTarget方法,已经是 watcher对象了
*
* 作用是吧dep(哨兵)自动传递给士官。而不是之前举得例子,需要先实例化士官,再把哨兵通过士官的构造函数传入
*
*/
depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}
notify() {
this.watchers.forEach(w => {
w.update();
})
}
}
观察者类
- 这里的观察者类(Watcher)在Vue中只能实现简单的监听(简单的对Watch里面key的属性值进行监听),如果需要达到对对象类型以及其他的深度监听就需要你们自己去完善了。
- 其实可以从构造函数就能看出,watch通过获取Vue对象中获取每一个key,并没有为对象类型进行更深层次的递归遍历。
- 其中的addDep方法在上面也说到了为观察者类和目标类互相添加依赖,将各自的实例添加到对方的容器中去。
- 大家也可以在Update方法里面其实就是在监听到Vue中属性值发生改变时通知给watcher并对 watch中的方法进行重新赋值
/**
* 观察者类(理解成士官类)
*/
class Watcher {
constructor(vm, key, handler) {
this.vm = vm;
this.key = key;
this.handler = handler;
this.deps = [];
// Set对象允许存储任何类型的唯一值,无论是原始值或者对象引用
this.depsId = new Set();
this.getter = function () {
let keys = key.split(".");
return keys.reduce((total, current) => {
// 这一步相当于从vue对象中获取例如是userName的键
return total[current];
}, vm)
}
this.oldValue = this.get();
}
/**
* 根据watch中的key或者data中的变量
* @returns 返回data中与watch的key相对应的变量(旧值)
*/
get() {
// 渲染watcher到Dep.target上
pushTarget(this);
let value = this.getter.call();
popTarget();
return value;
}
/**
* 触发更新
* 当data中对应watch的值发生变化后触发该法
*/
update() {
// 获取data对应key改变后的新值
let newValue = this.get();
if (this.oldValue != newValue) {
this.handler(newValue, this.oldValue);
}
}
addDep(dep) {
let id = dep.id;
if (!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep);
dep.addWatch(this);
}
}
}
创建观察者类并为Vue实例添加$watch扩展方法
/**
* 创建watch
* @param {*} vm vue对象
* @param {*} key watch中监听方法的key
* @param {*} handler watch中的监听方法
*/
function createWatch(vm, key, handler) {
vm.$watch(key, handler);
}
/**
* 在vm对象中扩展watch方法
* @param {*} key
* @param {*} handler
*/
Vue.prototype.$watch = function (key, handler) {
let vm = this;
// 实例化watch对象。用于监听watch需要监听的数据
new Watcher(vm, key, handler);
}
创建哨兵(目标类)
你要是看过 Vue中的代理模式这篇文章那你就不会对下面的代码感到陌生。
- 当然当Vue初始化时首先还是会初始化Data对象以及watch(观察者)。
- 在调用get代理方法时会为目标类Dap以及观察者类Watcher添加依赖(将彼此的实例添加到对方的容器中)
- 在data中监听到Vue属性值发生改变时通过实例化的目标类在代理方法set中为其观察者对象的容器中的所有观察者的监听handler(方法)重新赋值。
// 实例化Dep(哨兵)
let dep = new Dep();
// 给data中的变量配置get/set属性用于监听
// 或者可以理解成把指定key挂载到data中
// ★这里其实有问题,userName和user.userName其实是会相互干扰的
Object.defineProperty(data, key, {
get: function () {
if (Dep.target) {
dep.depend();
}
return value;
},
set: function (newValue) {
if (newValue != value) {
// 验证newValue是否是对象
if (typeof newValue != 'object' || newValue == null) {
// 啥都不做
}
else {
new Observe(newValue, vm);
}
value = newValue;
dep.notify();
// 这里只是做一个日志输出,如果是一个正常的vue,则应该是更新视图方法
console.log(`我现在是【${value}】啦!`);
}
}
})
上HTML展示一下效果
不要在意{{userName}}没有渲染上,让我们看控制台哈哈哈
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!-- <script src="https://cdn.bootcss.com/vue/2.5.2/vue.min.js"></script> -->
<script src="./myVue.js"></script>
</head>
<body>
<div id="app">
{{userName}}
</div>
<script>
var vm = new Vue({
el:"#app",
data:{
userName:"qq",
user:{
age:16,
name:""
},
age:16
},
watch:{
age(newValue,oldValue){
console.log("现在的新值是:"+newValue+",而以前是"+oldValue);
},
userName(newValue){
console.log("现在的新值是:"+newValue)
}
}
})
setTimeout(function(){
vm.userName="sa"
vm.age=18;
},2000)
</script>
</body>
</html>