变化侦测
# 变化侦测
[TOC]
# 前言
变化侦测:追踪状态,或者说当数据发生了变化的时候察觉到变化
- 在Angular中,通过脏值检查来实现变化监测
- 在React中,通过对比Virtual DOM来实现变化侦测
- 在Vue中,也存在一套机制实现变化监测
我们知道,Vue最大的特点就是数据驱动视图,当数据(状态)也就是state发生变化之后,所对应的视图也就是UI相应的改变
UI = render(state)
当state(输入)发生变化的时候,UI(输出)也对应的发生变化,但这都是用户定义的,其中公式的规则是不变的,也就是render是不变的,而Vue也就是充当了这个render的角色
接下来,有一些问题?
为什么会有Object和Array两种变化监测?
这是因为对于Object
数据我们使用的是JS
提供的对象原型上的方法Object.defineProperty
,而这个方法是对象原型上的,所以Array
无法使用这个方法,所以我们需要对Array
型数据设计一套另外的变化侦测机制。
# Object的变化监测
# Object.defineProperty
对象定义属性。数据绑定,数据劫持
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Object.defineProperty() 方法直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,返回此对象。
说白了就是,给对象增加、修改属性的一个方法,
Object.defineProperty(obj, prop, descriptor)
let car = {}
let val = 3000
//默认 不可修改 不可重写 不可枚举
Object.defineProperty(car, 'price', {
enumerable: true,
configurable: true,
get(){
console.log('price属性被读取了')
return val
},
set(newVal){
console.log('price属性被修改了')
val = newVal
}
})
Vue怎么知道state发生了改变?
通过getter和setter对数据进行监测,当数据发生改变的时候,触发 get() 和 set()
源码:src/core/observer/index.js
Observer类通过递归的方式把一个对象的所有属性都转化成可观测对象
并且给value新增一个__ob__属性,值为该value的 Observer 实例。这个操作相当于为 value 打上标记,表示它已经被转化成响应式了,避免重复操作
然后判断数据的类型,只有 object 类型的数据才会调用walk将每一个属性转换成 getter/setter 的形式来侦测变化。
最后,在 defineReactive 中当传入的属性值还是一个 object 时使用 new observer(val)来递归子属性,这样我们就可以把 obj 中的所有属性(包括子属性)都转换成 getter/seter 的形式来侦测变化。
# 依赖收集
数据变化监测的目的已经达到了,那么,接下来一个问题
当数据发生变化的时候,到底通知谁发生变化呢?
当数据发生变化的时候,不可能说全部都重新渲染吧,谁使用了那么久更新谁,所以问题就变成了
怎么知道到底是谁使用了当前有变化的数据呢?
给每个数据都创建一个依赖数组,也就是说当一个UI依赖了当前的数据的时候就把当前的数据放到依赖数组中去,当数据发生变化的时候,通知对应的依赖数组。——这个过程就是依赖收集
可观测的数据被获取时会触发getter属性,那么就可以在getter中收集这个依赖。同样,当这个数据变化时会触发setter属性,那么就可以在setter中通知依赖更新
所谓的依赖数组到底是怎么存在的?
在Vue的源码中,并不是单纯的通过一个数组去实现的依赖的存储,而是存在一个Dep依赖管理器,
源码:src/core/observer/dep.js
- 初始化subs数组,用来存放依赖
- 定义实例方法对依赖进行添加移除等操作
- 通知(notify)所有的依赖进行更新
回到defineReactive,可以看到,在getter中调用了dep.depend()方法收集依赖,在setter中调用dep.notify()方法通知所有依赖更新。
究竟谁是这个依赖?
在Vue中还实现了一个叫做Watcher的类,而Watcher类的实例就是我们上面所说的那个"谁"。换句话说就是:谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher实例。watcher相当于一个中间人。数据发生了变化,通知依赖对应的watcher,再由watcher再去通知真正的视图
Watcher究竟是怎样把自己添加到数据对应的依赖管理器中的?
watcher实现逻辑:
当实例化Watcher类时,会先执行其构造函数;
在构造函数中调用了this.get()实例方法;
在get()方法中,首先通过window.target = this把实例自身赋给了全局的一个唯一对象window.target上,
然后通过let value = this.getter.call(vm, vm)获取一下被依赖的数据,获取被依赖数据的目的是触发该数据上面的getter
上文我们说过,在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中,在get()方法最后将window.target释放掉。
而当数据变化时,会触发数据的setter,在setter中调用了dep.notify()方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖的update()方法,也就是Watcher类中的update()实例方法,在update()方法中调用数据变化的更新回调函数,从而更新视图。
# 存在的问题
Object.defineProperty虽然实现了对object数据的监测,但是他只能观测到object的取值和设置值,当添加或者删除键值对的时候,是无法观测到的。
解决方案:Vue增加了两个全局的API,Vue.set 和 Vue.delete
# 总结
首先,我们通过Object.defineProperty
方法实现了对object
数据的可观测,并且封装了Observer
类,让我们能够方便的把object
数据中的所有属性(包括子属性)都转换成getter/seter
的形式来侦测变化。
接着,我们学习了什么是依赖收集?并且知道了在getter
中收集依赖,在setter
中通知依赖更新,以及封装了依赖管理器Dep
,用于存储收集到的依赖。
最后,我们为每一个依赖都创建了一个Watcher
实例,当数据发生变化时,通知Watcher
实例,由Watcher
实例去做真实的更新操作。
其整个流程大致如下:
Data
通过observer
转换成了getter/setter
的形式来追踪变化。- 当外界通过
Watcher
读取数据时,会触发getter
从而将Watcher
添加到依赖中。 - 当数据发生了变化时,会触发
setter
,从而向Dep
中的依赖(即Watcher)发送通知。 Watcher
接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
# Array的变化监测
# 依赖收集
https://vue-js.com/learn-vue/reactive/array.html#_4-%E5%86%8D%E8%B0%88%E4%BE%9D%E8%B5%96%E6%94%B6%E9%9B%86
依赖是在什么地方收集的?
其实Array
型数据的依赖收集方式和Object
数据的依赖收集方式相同,都是在getter
中收集,在使用data中的array数据时,需要先通过object对象中获取array数据,然后在获取array的时候就触发了getter,所以就可以在getter中收集依赖了
当Array型数据发生变化时如何得知?
在碰到array类型的时候,在内部的方法的原型链上修改其方法,也就是说,在内部重写了内置的几个数组方法,主要包括:'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'方式,同时,在新的方法中,还可以实现一些其他的方法
源码:src/core/observer/array.js
首先创建了继承自
Array
原型的空对象arrayMethods
,接着在
arrayMethods
上使用object.defineProperty
方法将那些可以改变数组自身的7个方法遍历逐个进行封装。最后,当我们使用
push
方法的时候,其实用的是arrayMethods.push
,而arrayMethods.push
就是封装的新函数mutator
也就是说,实标上执行的是函数
mutator
,而mutator
函数内部执行了original
函数,这个original
函数就是Array.prototype
上对应的原生方法。那么,接下来我们就可以在
mutato
r函数中做一些其他的事,比如说发送变化通知。
使用拦截器
回到源码的observe部分,将拦截器挂载到数组实例与Array.prototype
之间
如何通知依赖?
要想通知依赖,首先要能访问到依赖。
要访问到依赖也不难,因为我们只要能访问到被转化成响应式的数据
value
即可因为
vaule
上的__ob__
就是其对应的Observer
类实例,有了
Observer
类实例我们就能访问到它上面的依赖管理器然后只需调用依赖管理器的
dep.notify()
方法,让它去通知依赖更新即可
# 深度侦测
也就是说当在Array上进行操作的时候,只是对基本的增加和删除的元素,而在Vue
中,不论是Object
型数据还是Array
型数据所实现的数据变化侦测都是深度侦测,所谓深度侦测就是不但要侦测数据自身的变化,还要侦测数据中所有子数据的变化
实现的方式就是将数组中所有的元素都转化成可以监测的对象。当有数组中有新元素添加的时候,也需要将其转化成可侦测的数据
思路就是:往数组中添加元素的有三种方是push, unshift,splice,当时前两者时,也就是传入的参数就是新增的元素,在splice中,下标为2的才是新增的元素
# 不足之处
上述的原理其实还是根据数组对应的方法进行改造的,也就是在原型链上进行了改造,之后如果直接在数组上操作数组下标其实还是会导致侦测不到的情况的
# 总结
- 首先我们分析了对于
Array
型数据也在getter
中进行依赖收集; - 其次我们发现,当数组数据被访问时我们轻而易举可以知道,但是被修改时我们却很难知道,为了解决这一问题,我们创建了数组方法拦截器,从而成功的将数组数据变的可观测。
- 接着我们对数组的依赖收集及数据变化如何通知依赖进行了深入分析;
- 最后我们发现
Vue
不但对数组自身进行了变化侦测,还对数组中的每一个元素以及新增的元素都进行了变化侦测,我们也分析了其实现原理。