Vue响应式系统的核心-----变化侦测(Object)
什么是变化侦测
变化侦测的作用是侦测数据的变化,当数据变化时,会通知视图进行相应的更新
在运行时应用内部的状态会不断发生变化,此时需要不停地重新渲染。这时如何确定状态中发生了什么变化?变化侦测主要用来解决这个问题。
Vue的变化侦测属推。当状态发生变化时,Vue立刻知道了,在一定程度上知道哪些状态变了。
有一个状态绑定多个依赖,每个依赖表示一个具体的DOM节点,当状态变化了,向这个状态的所有依赖发送通知,让他们进行DOM更新操作。每个状态绑定的依赖越多,依赖追踪在内存上的开销就会越大。
从vue.js 2.0开始引入了虚拟DOM,一个状态所绑定的依赖不再是具体的DOM节点,而是一个组件。当状态变化后,会通知到组件,组件内部在使用虚拟DOM进行比对。这样可以大大降低依赖数量,从而降低依赖追踪所消耗的内存。
Object的变化侦测

-
Data通过Observer转换成了getter/setter的形式来追踪变化
-
当外界通过watcher读取数据时,会触发getter从而将watcher添加到依赖中
-
当数据发生变化时,会触发setter,从而向Dep中的依赖发送通知。
-
watcher接收到通知后,会向外界发送通知,变化通知到外界后可能会触发视图更新,也有可能触发用户的某个回调函数等。
如何追踪变化
在vue中如何侦测一个对象的变化?使用Object.defineProperty和ES6的Proxy
function defineReactive(data,key,val){
Object.defineProperty(data,key,{
enumerable: true,
configuration: true,
get: function(){
return val
},
set: function(newVal){
if(val === newVal){
return
}
val = newVal
}
})
}
每当从data的key中读取数据时,get函数被触发;每当往data的key中设置数据时,set函数被触发。
如何收集依赖
观察数据目的是当数据的属性发生变化时,可以通知那些曾经使用该数据的地方。
<template>
<h1>{{name}}</h1>
</template>
该模板使用了数据name,所以当它发生变化时,要向使用了它的地方发送通知。
收集依赖:把用到数据name的地方收集起来,然后等属性发生变化时,把之前收集好的依赖循环触发一遍。
依赖收集在哪里
在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 < l; i++) {
subs[i].update()
}
}
}
function defineReactive (obj,key,val) {
const dep = new Dep() //实例化一个依赖管理器,生成一个依赖管理数组dep
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
dep.depend() // 在getter中收集依赖
return val;
},
set(newVal){
if(val === newVal){
return
}
val = newVal;
dep.notify() // 在setter中通知依赖更新
}
})
}
依赖是谁?
当属性发生变化后,需要通知谁
其实在Vue中还实现了一个叫做Watcher的类,而Watcher类的实例就是我们上面所说的那个"谁"。换句话说就是:谁用到了数据,谁就是依赖,我们就为谁创建一个Watcher实例。在之后数据变化时,我们不直接去通知依赖更新,而是通知依赖对应的Watch实例,由Watcher实例去通知真正的视图。
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn)
this.value = this.get()
}
get () {
window.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
/**
* Parse simple path.
* 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
* 例如:
* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
const bailRE = /[^\w.$]/
export function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
递归侦测所有key
前面的代码只能侦测数据中的某一个属性,我们希望把数据中的所有属性都侦测到,所以需要封装一个Observer类。这个类的作用将一个数据内的所有属性都转换成getter/setter的形式,然后再追踪它们的变化。
function Observer (value) {
this.value = value;
if (!Array.isArray(value)) {
this.walk(value);
}
};
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i],obj[keys[i]]);
}
};
function defineReactive (obj,key,val) {
//新增,递归子属性
if(typeof val === 'object'){
new Observer(val);
}
const dep = new Dep() //实例化一个依赖管理器,生成一个依赖管理数组dep
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get(){
dep.depend() // 在getter中收集依赖
return val;
},
set(newVal){
if(val === newVal){
return
}
val = newVal;
dep.notify() // 在setter中通知依赖更新
}
})
}
不足之处
虽然我们通过Object.defineProperty方法实现了对object数据的可观测,但是这个方法仅仅只能观测到object数据的取值及设置值,当我们向object数据里添加一对新的key/value或删除一对已有的key/value时,它是无法观测到的,导致当我们对object数据添加或删除值时,无法通知依赖,无法驱动视图进行响应式更新。
当然,Vue也注意到了这一点,为了解决这一问题,Vue增加了两个全局API:Vue.set和Vue.delete。

浙公网安备 33010602011771号