目录
js中两种侦测对象变化方法:
该方法定义一个响应式数据,当数据的属性发生变化的时候,通知依赖更新,即向使用到它的地方发送通知
Object可以通过Object.defineProperty将属性转换成getter/setter的形式来追踪变化。读取数据时会触发getter,修改数据时会触发setter
Object.defineProperty(obj, key, {
get() {
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal
}
}
})
这样,修改数据时,我们可以主动通知依赖进行更新,那么问题来了:
如何收集依赖?什么时机收集依赖?
思路:
总结:在getter中收集依赖,在setter中触发依赖
依赖收集在哪里?
定义一个Dep类,用来存储当前key的依赖。DEP类主要负责对依赖的收集、删除、发送通知。
依赖是谁?
依赖就是用到数据的地方,用到这个数据的地方可能是视图中对应的坑({{name}}),也可能是开发者写的一个watch,于是抽象一个类Watcher
至此,问题告一段落
//数据劫持,添加依赖追踪
defineReactive(obj, key, val) {
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
//只有Watcher触发的getter才会收集依赖
Dep.target && dep.depend();
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
// 数据发生变更通知所有的观察者
dep.notify()
}
}
})
//递归树
this.observer(val);
}
// Dep发布者相当于vue data 对象中的某一个属性如:name
class Dep {
constructor(vm, key) {
this.subs = [];
}
depend() {
if (Dep.target) {
this.addSub(Dep.target)
}
}
addSub(sub) {
this.subs.push(sub);
}
removeSub(sub) {
remove(this.subs, sub);
}
notify() {
//只负责通知更新,具体是否更新以及更新操作由watcher做
this.subs.forEach(sub => sub.update())
}
}
当new Watcher()实例时,会自动将依赖收集到Dep中
// watcher 相当于视图中对应的坑({{name}}),一个坑对应有一个观察者,监听此处的数据变化
class Watcher {
constructor(vm, key, cb) {
this.vm = vm;
this.key = key;
this.cb = cb;
//这段代码会自动将Watcher添加到Dep中
//先将Watcher赋值给Dep.target,然后读取一下值,会触发get,这时依赖就被收集了,再将Dep.target置空
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
update() {
//每个watcher所做的更新大不一样,所以将具体的更新操作放到回调里面去做
this.cb.call(this.vm, this.vm[this.key])
}
};
}
完整代码:
class Observer{
constructor(obj){
this.obj = obj
if(!Array.isArray(obj)){
this.walk(obj)
}
}
walk(obj){
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
function defineReactive(obj, key, val) {
// 递归子属性
if(typeof val === 'object'){
new Observer(val)
}
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
//如果有创建了一个观察者,那么Dep.target就会被它的实例赋值,此时就会通过访问实例的值(见Watcher 构造函数)将其add到dep
//同时这里的判断是为了防止重复添加
Dep.target && dep.depend();
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
// 数据发生变更通知所有的观察者
dep.notify()
}
}
})
}
tip: 至于何时new Watcher() ,我们在模板编译的时候进行说明
通过defineProperty方法定义obj之后, 对Obj新增属性和删除属性的操作,将无法追踪到这个变化。
原因: ES6之前,JS没有提供元编程能力
//新增属性
this.obj.name = 'xxx'
//删除属性
delete this.obj.name
为了解决这个问题,vue.js提供的两个API——vm. s e t 和 v m . set和vm. set和vm.delete
用法:vm.$set(target, key, value)
import {set} from './observer/index' Vue.prototype.$set = set
代码中数组的部分可以在读了《vue源码之Array》之后再了解,源码及解读:
function set (target, key, val) {
// 当target是一个数组并且key是一个有效的索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key);
target.splice(key, 1, val);
return val
}
// 如果key 已经存在于对象中,直接修改值即可
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val
}
// 源码中,在Observer一个对象时,会给对象添加__ob__属性,标识这是一个响应式对象
var ob = (target).__ob__;
// 如果target上并没有__ob__属性,说明它本身就不是一个响应式的对象,不做处理
if (!ob) {
target[key] = val;
return val
}
//否则,将属性转换成响应式,并向依赖发出更新通知
defineReactive(ob.value, key, val);
ob.dep.notify();
return val
}
用法:vm.$delete(target, key)
import {del} from './observer/index' Vue.prototype.$delete = del
源码解读:
function del (target, key) {
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.splice(key, 1);
return
}
var ob = (target).__ob__;
// 不存在此属性时,直接返回
if (!hasOwn(target, key)) {
return
}
delete target[key];
if (!ob) {
return
}
// 删除属性后,像依赖发送通知
ob.dep.notify();
}
1. 用法
vm.$watch( expOrFn, callback, [options] )
用于观察一个表达式或computed函数的变化,来达到todo something的目的。
返回一个取消观察的函数,用于停止触发回调
var unwatch = vm.$watch('a.b.c', (newVal, oldVal) => {})
unwatch()
2. watch的实现原理
vm. w a t c h 实 际 上 是 对 W a t c h e r 的 一 种 封 装 。 无 论 是 视 图 中 的 多 个 依 赖 ( 如 n a m e 出 现 的 地 方 ) , 还 是 使 用 watch实际上是对Watcher的一种封装。无论是视图中的多个依赖(如{{name}}出现的地方),还是使用 watch实际上是对Watcher的一种封装。无论是视图中的多个依赖(如name出现的地方),还是使用watch监听值的变化,都可以看做是观察者。
实质:当用户使用vm.$watch()来监听一个数据变化时,实际跟我们在视图中新增一个坑{{name}}
,所做的事情是一样的。
源码解读:
Vue.prototype.$watch = function (
expOrFn,
cb,
options
) {
var vm = this;
options = options || {};
// 创建一个依赖,并收集到Dep
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn () {
// 依赖将自己从被收集到的所有Dep中移除,终止订阅数据变更通知
watcher.teardown();
}
};
}
//Watcher会收集name和age两个的Dep
// 同时这两个Dep中也会收集Watcher
// 任意一个数据发生变化,Watcher都会收到通知
this.$watch(function(){
return this.name + this.age
},(newVal, oldVal) => {})
3. deep实现原理
思路:
初始化实例时,判断是否deep,注意代码在之前执行
Watcher类的修改:
Dep.target = this;
this.vm[this.key]; //收集到当前数据Dep
// 新增
if(!!options.deep){
traverse(value) // 递归收集到当前数据子值的Dep
}
Dep.target = null;
添加到子值Dep
var seenObjects = new Set();
function traverse (val) {
_traverse(val, seenObjects);
seenObjects.clear();
}
function _traverse (val, seen) {
var i, keys;
if (val.__ob__) {
var depId = val.__ob__.dep.id;
if (seen.has(depId)) {
return
}
seen.add(depId);
}
// 重点看这里
// 如果值是一个对象,则递归
keys = Object.keys(val);
i = keys.length;
while (i--) {
// 其中val[keys[i]] 会触发getter,也就是此处会进行依赖收集
_traverse(val[keys[i]], seen); }
}
_traverse(val[keys[i]], seen)
其中val[keys[i]]会触发getter,也就是此处会进行依赖收集。
至此,所有值的变化,都会触发cb。
为了便于理解,文章中的代码都是源码的删减版