在运行时,应用状态会不断的发生变化,页面需要反复渲染页面,如何确定其中的状态发生了哪些变化?
变化侦测就是用来解决该问题,变换侦测也是响应式系统的核心,没有它,就没有重新渲染。
js追踪变化的方法有两种,Object.defineProperty和Proxy,由于ES6在浏览器中的支持度并不理想,到目前为止,Vue.js还是使用Object.defineProperty。
Object.defineProperty是js中提供的一个方法,可以用于在对象中定义一个新属性或修改现有属性,并返回一个对象。当读取数据时,自动调用它里面的get方法,设置时,调用set方法。
function defineReactive(data,key,val){
Object.defineProperty(data,key,{
enumerable:true,
get:function(){
return val
}
set:function(newVal){
if(val==newVal){
return;
}
val = newVal;
}
})
}
defineReactive用来对Object.defineProperty进行封装,封装后只需要传递data、key和val。
当从data的key中读取数据时,get函数被触发,设置数据时,set函数被触发。
在getter中收集依赖,在setter中触发依赖
封装一个Dep类,用来管理依赖,可以收集依赖、删除依赖或者向依赖发送通知。
export default class Dep{
constructor(){
this.subs = [];
}
addSub(sub){ //收集
this.subs.push(sub);
}
removeSub(sub){ //删除
remove(this.subs,sub)
}
depend(){
if(window.target){
this.addSub(window.target);
}
}
notify(){ //通知
const subs = this.subs.slice();
for(let i=0,l=subs.length;i-1){
return arr.splice(index,1);
}
}
}
}
改造下defineReaactive
function defineReactive(data,key,val){
let dep = new Dep();//修改
Object.defineProperty(data,key,{
enumerable:true,
get:function(){
dep.depend()//修改
return val
}
set:function(newVal){
if(val==newVal){
return;
}
val = newVal;
dep.notify();//新增
})
}
知道状态发生变化不是最终结果,而是要通知使用了该状态的地方它们发生了变化。
用到了该状态的地方就是我们要收集的,也就是依赖。
但使用这个数据的地方很多,而且类型还不一样,有可能是模板,也可能是用户写的一个watch,所以需要抽象出一个能集中处理这些情况的类,在依赖收集阶段,只收集这个封装好的类的实例进来,通知也只通知它一个。给它取了一个好听的名字,叫Watcher,即依赖。
Watcher是一个中介的角色,数据发生变化的时候通知它,然后它再去通知其他地方。
经典的使用方式:
vm.$watch("name", function(newVal, oldVal) {
// something
});
把window.target赋一个this,再读一下值,触发getter,就可以将this主动添加到依赖数组的dep中。
之后,每当name的值发生变化,就会触发update方法,在其中执行参数中的回调函数。
(该部分来源https://blog.csdn.net/qq_16858683/article/details/101371154)
export default class Watcher {
constructor(vm, expOrFn, cb) {
// vm为vue实例
// expOrFn为表达式或函数(这个函数可能访问了多个数据,就会创建多个依赖),在上面的例子里,expOrFn为"name"
// cb为回调函数,callback的缩写
this.vm = vm;
// 解析expOfFn的路径,读取它的值(parsePath函数是一个闭包,返回的是一个函数,该函数参数应该传这个值所在对象,其实就是vm)
// 所以这里getter只是一个函数
this.getter = parsePath(expOfFn);
this.cb = cb;
// 每实例化一个Watcher,都自动触发get(),触发收集依赖的逻辑
this.value = this.get();
}
get() {
window.target = this;
// 读取expOrFn的值,从而触发了"name"的getter,从而触发了收集依赖:dep.depend();
// depend() {
// if (window.target) this.addSubs(window.target);
// }
// 而depend()中是将window.target存入依赖,此时windo.target指向了Watcher的this!,正好把这个Watcher存入依赖,妙啊
let value = this.getter.call(this.vm, this.vm);
// 用完window.target就置为undefined,备胎...
window.target = undefined;
return value;
}
// "name"数据改变时,触发它自己的setter,然后遍历依赖。触发了该watcher的update方法。
// 该方法调用回调函数
update() {
// 此时watcher中的value是老的值
const oldValue = this.value;
// 通过get()获取"name"最新的值,get中会判断这个watcher是否被加入到依赖中,防止重复添加依赖
this.value = this.get();
// callback中的this指向vm,然后传入newVal,oldVal,执行callback
this.cb.call(this.vm, this.value, oldValue);
}
}
到现在,已经实现了变化侦测的功能,但只能侦测数据中的某一个属性。
为了侦测到数据的所有属性,包括子属性,封装一个Observer类。这个类的作用是将一个数据里的所有属性都转换成getter/setter的形式,然后追踪它们的变化。
(该部分来源https://blog.csdn.net/qq_16858683/article/details/101371154)
export class Observer {
constructor(value) {
this.value = value;
// 数组和对象要分开处理
if (!Array.isArray(value)) {
this.walk(value);
}
}
// 循环为每个属性添加侦测
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]]);
}
}
}
function defineReactive(data, key, val) {
// 如果子属性还是个对象,则递归为这个对象的子属性添加侦测
if (type val === "object") new Observer;
let dep = new Dep();
Object.defineProperty (data, key, {
enumerable: true,
configurable: true,
get: function () {
// 在getter中收集依赖
dep.depend();
return val;
},
set: function (newVal) {
if (val === newVal) return;
val = newVal;
// 在setter中通知依赖
dep.notify();
}
})
}
Data通过Observer转换成getter/setter的形式来追踪变化。
当外界通过Watcher读取数据时,会触发getter将Watcher添加到依赖中。
当数据发生了变化,会触发setter,从而向Dep中的依赖(Watcher)发送通知。
Watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也可能触发用户的某个回调函数。
Vue.js通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性
但Vue提供了两个API解决这个问题