何为变化侦测:
变换侦测的类型:
- “推” push
- “拉” pull
Vue.js的变化侦测属于"推"。当状态变化的时候vue就马上会知道。变化侦测也是vue响应式系统的核心。由于vue使用的是push
类型,所以每个状态的依赖就会越多,所以需要的内存开销越大,因此vue引入了虚拟DOM,即一个状态所绑定的依赖不在是具体的DOM节点而是一个组件,状态发生改变就会通知到组件,由组件内部使用虚拟DOM进行比对。
如何追踪变化:
两种方式Object.defineProperty
和Proxy
。因为ES6浏览器支持问题目前vueJS还是用的是Object.defineProperty
。
追踪实现:
// obj: 要定义属性的对象,这里就是要监听的对象
// prop: 要配置或修改属性的名称或Symbol
// descriptor: 要定义或修改的属性描述
// enumerable: 默认为false,为 true 时该属性才会在对象的枚举属性中
// configurable:默认为false,为 true 时该属性的描述才能被改变,
// 同时该属性也可以从对应的对象上删除
function defindeReactive (obj, prop, descriptor) {
Object.defineProperty(obj, prop, {
enumerable: true, // 是否可枚举
configurable: true, // 是否可配置
get: function () {
return descriptor;
},
set: function (newVal) {
if(descriptor === newVal) {
return;
}
descriptor = newVal;
}
});
}
每当从obj的prop中读取数据时,get函数被触发,每当往obj的prop中设置数据的时候,set函数会被触发。
如何收集依赖:
为什么要收集依赖?
因为当数据的属性发生变化时,可以根据依赖通知那些使用了该数据的位置,即时更新数据,根据上述的定义的方法,可以在getter中收集依赖,在setter中触发依赖。
收集的依赖存放到哪里:
我们可以为把prop放到一个数组中进行统一管理。如果这个依赖是一个函数,保存在window.rely上。可以把上述方法修改一下
function defindeReactive (obj, prop, descriptor) {
let dep = [];
Object.defineProperty(obj, prop, {
enumerable: true,
configurable: true,
get: function () {
dep.push(window.rely);
return descriptor;
},
set: function (newVal) {
if(descriptor === newVal) {
return;
}
for (let i = 0; i < dep.length; i++) {
dep[i](newVal, descriptor);
}
descriptor = newVal;
}
});
}
这里的dep数组,就可以把所有的依赖收集起来,在setter函数被触发时就可以循环遍历所有的依赖。为降低代码的耦合度,可以封装一个Dep类,为我们专门收集和管理依赖
export default class Dep {
constructor () {
this.subs = [];
}
addSub (sub) {
this.subs.push(sub);
}
removeSub () {
remove(this.subs, sub);
}
depend () {
if(window.rely) {
this.addSub(window.rely);
}
}
notify () {
const subs = this.subs.slice();
for (let i = 0; i < subs.length; i++) {
subs[i].update();
}
}
function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item);
if(index > -1) {
return arr.splice(index, 1);
}
}
}
}
之后再次修改defineReactive,将参数语义化
function defindeReactive (data, key, val) {
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend();
return val;
},
set: function (newVal) {
if(val === newVal) {
return;
}
val = newVal;
dep.notify();
}
});
}
属性变化之后通知谁:
要通知用到数据的地方,使用这个数据的地方很多、而且类型可能不同,既有可能是模板也能是用户自己写的一个watch。这时就需要一个可以处理这些情况的类。然后可以在依赖收集阶段只收集这个封装好的类的实例进来,通知也只通知它一个。然后由它通知其他地方。这就是Watcher
。可以根据Watcher
侦听属性变化,再由Watcher发出通知。
Watcher:
可以把Watcher看成一个中介的角色,数据发生变化的时候会通知它,它会通知其他地方。当属性变化时,会触发第二个参数中的函数。
怎么实现?
把watcher
实例添加到数据属性的Dep中,当数据属性值发生变化的时候,通知Watcher
。然后Watcher
执行参数中的回调函数
export default class Wathcer {
constructor (vm, expOrFn, callBack) {
this.vm = vm;
this.getter = parsePath(expOrFn);
this.callBack = callBack;
this.value = this.get();
}
get () {
window.rely = this;
let value = this.getter.call(this.vm, this.vm);
wndow.rely = undefined;
return value;
}
update () {
const oldValue = this.value;
this.value = this.get();
this.callBack.call(this.vm, this.value, oldValue);
}
}
这样就可以把Watcher
的实例添加到数据的属性中。
因为在get
方法中先把window.rely
设置为this
,当前Watcher
实例,这样就可以在读取数据属性值的时候触发getter
函数。触发getter
,就会开始收集依赖,从Window.rely
中读取依赖并添加到Dep
中,只要先在window.rely
赋一个this
,然后再去读这个值,去触发getter
,就可以把this主动添加到keypath
的Dep
中,当依赖注入到Dep
中,每当数据属性的值发生变化的时候,就会触发update
方法就会执行回调函数,将value
和oldValue
传入到参数中。这样不管是vm.$watch('add', (value, oldValue) => {})
,还是在data
定义的数据属性,都可以通过Watcher
来通知自己是否需要发生变化。
那么parsePath是如何读取一个字符串的keypath的?
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;
}
}
先将keypath分割成数组,然后循环数组一层一层去读数据,最后拿到就是keypath中想要的数据。
递归侦测所有的key:
目前已经基本完成变化侦测功能了,但目前代码只能侦测数据中的某一个属性,如果把数据中所有属性(包括子属性)都侦测到,岂不美哉!!!,这里需要封装一个类,将数据内的所有属性包括子属性都转换成getter/setter形式,然后去追踪其变化。
/*
Observer类会附加到每个被侦测的object上
一旦被附加上,Observer会将Object的所有属性转化为getter/setter的形式
来收集属性的依赖,并且当属性发生变化时会通知这些依赖
*/
export class Observer {
constructor (value) {
this.value = value;
if (!Array.isArray(value)) {
this.walk(value);
}
}
/*
walk会将每个属性都转换成getter/setter形式侦测变化,只有数据类型为Object被调用
*/
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 (typeof val === 'object') {
new Observer(val)
}
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
dep.depend();
return val;
},
set: function (newVal) {
if(val === newVal) {
return;
}
val = newVal;
dep.notify();
}
});
}
定义的Observer
类可以将一个正常的object
转换成被侦测的object
。然后判断数据的类型、只有object
类型的数据才会调用walk
将每个属性转化成getter/setter
的形式来侦测变化。最后在defineReactive
中新增new Observer(val)
来递归子属性,这样我们就可以把data
中的所有属性包括子属性都转换为getter/setter
的形式来侦测变化。当data
中的属性发生变化的时候,就会通知相关依赖进行变化。
有关Object变换侦听的问题key:
经上述介绍数据的变化是通过getter/setter来追踪的,由于这种追踪方式导致一些语法中即便是数据发生了变化,Vue.js也追踪不到
例子:
let vm = new Vue({
el: '#app',
template: '#demo-template',
methods: {
action () {
this.obj.name = 'berwin';
}
},
data: {
obj: {}
}
});
在action
方法中,在obj中新增了name
属性,Vue.js无法侦测到这一变化,所以不会向依赖发送通知。Vue.js通过Object.definProperty
来将对象的key
转换成getter/setter
的形式来追踪变化,但是它们只能追踪到一个数据是否被修改,无法追踪新增或删除属性。在Es6之前JavaScript没有提供元编程的能力,无法侦测到对象的添加或删除属性。Vue.js提供了两个API vm.$set
和vm.$delete