数据响应式原理(vue2)
MVVM
非侵入
Object.defineProperty() 数据劫持/数据代理
let obj ={};
Object.defineProperyty(obj,'property1',{
value: 1,
//是否可写 修改
wirtable: false,
//是否可以被枚举 for in
enumerable:false
})
getter/setter
Objecct.defineProperty(obj,'property1',{
get(){
console.log('property1被劫持,访问')
}
set(){
console.log('property1被劫持,改变')
}
})
get和value不能同时设定,因为get的返回值作为属性值
因此setter后修改需要共同的变量,给getter返回
let obj={};
let temp;
Objecct.defineProperty(obj,'property1',{
get(){
console.log('property1被劫持,访问');
return temp;
}
set(newValue){
console.log('property1被劫持,改变');
temp = newValue;
}
})
可以发现上面的temp是外层变量,可以改造成函数来形成闭包 defineReative
function defineReactive(data,key,val){
Objecct.defineProperty(data,key,{
enumerable:true,
//可配置,删除
configurable:true,
get(){
console.log('key访问');
return val;
}
set(newValue){
console.log('改变');
val = newValue;
}
})
}
问题:对象的深层无法检测,所以需要递归observe
递归侦测对象的全部属性
调用过程 :
外层 index 直接observe(obj)
observe 检测有无Observer类实例 没有则new一个赋给_ ob _
Observer构造函数 调用 walk 静态方法 对obj的内部每个属性进行 defineReactive 调用 (设置getter setter)
在difineReactive每个属性调用observe 这里就实现了递归
注意的是,其中setter的newValue也要observe(设置的值如果是新对象也要进行递归侦测属性)
Observer 类:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
主要流程: observe(obj) -> obj有无_ ob _ 没有new Observer() 添加到_ ob _ 上 -> 遍历下一层属性,逐个defineReactive
utils类: 工具类, def函数将‘__ ob __’ 设置为无法枚举遍历
//设置enumerable
export const def = function (obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
});
};
数组的响应式处理
vue中为了处理数组的响应式,改写 七个方法
push pop shift unshift splice sort reverse
静态方法都在Array.prototype 上
我们构造一个对象arrayMethods proto指向这个数组原型
实现: arrayMethods = Object.creat(Array.prototype)
在Observer类内部,如果value是数组,需要将这个数组原型指向这个arrayMethods,
使用Object.setPrototypeOf(value,arrayMethods)
//array.js 处理数组
// 得到Array.prototype
const arrayPrototype = Array.prototype;
// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype);
// 要被改写的7个数组方法
const methodsNeedChange = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
];
methodsNeedChange.forEach(methodName => {
// 备份原来的方法,因为push、pop等7个函数的功能不能被剥夺
const original = arrayPrototype[methodName];
// 定义新的方法
def(arrayMethods, methodName, function () {
// 恢复原来的功能
const result = original.apply(this, arguments);
// 把类数组对象变为数组
const args = [...arguments];
// 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性(就是这个数组)添加了__ob__属性。
const ob = this.__ob__;
// 有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变为observe的
let inserted = [];
switch (methodName) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
// splice格式是splice(下标, 数量, 插入的新项)
inserted = args.slice(2);
break;
}
// 判断有没有要插入的新项,让新项也变为响应的
if (inserted) {
ob.observeArray(inserted);
}
console.log('啦啦啦');
ob.dep.notify();
return result;
}, false);
});
export default class Observer {
constructor(value) {
// 每一个Observer的实例身上,都有一个dep
this.dep = new Dep();
// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例
def(value, '__ob__', this, false);
// console.log('我是Observer构造器', value);
// 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object
// 检查它是数组还是对象
if (Array.isArray(value)) {
// 如果是数组,要非常强行的蛮干:将这个数组的原型,指向arrayMethods
Object.setPrototypeOf(value, arrayMethods);
// 让这个数组变的observe
this.observeArray(value);
} else {
this.walk(value);
}
}
// 遍历
walk(value) {
for (let k in value) {
defineReactive(value, k);
}
}
// 数组的特殊遍历
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
// 逐项进行observe
observe(arr[i]);
}
}
};
对象的侦测: observe - observer实例 - walk遍历 - 对每个属性observe并侦听
这个时候能做到 : 对象的所有属性的get和set都能侦听到
数组的侦测: observe - observer实例 - 将数组原型连上 arrayMethods(arrayMethods引入时就开始运行,arrayMethods的原型连上Array.prototype,目的是获取到原型方法并在arrayMethods中defineProperty定义改写的方法,其中push,unshift,splice会增添新元素需要额外进行侦听处理,这个处理即为对增添的元素依次observe ) - 对这个数组的元素进行依次observe
改写方法实现: this为这个数组,通过拿到的原型方法调用得到结果最后返回,中间处理是上文中对增添元素进行依次observe
能做到: 数组调用这些方法能实际生效并对增添的元素(对象或者数组)能够监听,但对于数组内部的项get和set都不能侦听到
tip: 这里是性能权衡后的结果,对于数组的侦测只限于对象和数组,基础数据项都不能侦测
依赖收集
数据需要的地方——依赖
vue1 细粒度 用到数据的DOM都是依赖
vue2 中等粒度 用到数据的组件都是依赖
在getter中收集依赖 ,setter中触发依赖
Dep类和Watcher类
Dep 意味着dependence 依赖,它是专门用来管理依赖的一个类,每个Observer的实例,成员中都有一个Dep的实例
Watcher是中介类,当数据发生变化通过Watcher中转,通知组件
Watcher就是依赖,只有watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。
Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
每一个Observer的实例中,都有一个dep实例
//像下面数据就会newObserver4次, 4个dep实例
let obj = {
a:{
b:{
c:1
}
},
g: [2,3]
}
//Dep类
//闭包全局uid
let uid = 0;
class Dep {
constructor(){
this.id = uid++;
//subscribes 订阅者,数组储存自己的订阅者 ——即Watcher的实例,watcher是依赖
this.subs = [];
}
addSub(sub){
this.subs.push(sub);
}
depend(){
if (Dep.target){
this.addSub(Dep.target);
}
}
notify(){
//浅克隆
const subs = this.subs.slice();
for (let i = 0,l = subs.length;i<l;i++){
subs[i].update();
}
}
}
//Watcher类
var uid = 0;
export default class Watcher {
constructor(target, expression, callback) {
this.id = uid++;
this.target = target;
this.getter = parsePath(expression);
this.callback = callback;
this.value = this.get();
}
update() {
this.run();
}
get() {
// 进入依赖收集阶段。让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集阶段
Dep.target = this;
const obj = this.target;
var value;
// 只要能找,就一直找
try {
value = this.getter(obj);
} finally {
Dep.target = null;
}
return value;
}
run() {
this.getAndInvoke(this.callback);
}
getAndInvoke(cb) {
const value = this.get();
if (value !== this.value || typeof value == 'object') {
const oldValue = this.value;
this.value = value;
cb.call(this.target, value, oldValue);
}
}
};
function parsePath(str) {
var segments = str.split('.');
return (obj) => {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
obj = obj[segments[i]]
}
return obj;
};
}
总结
vue2内部数据响应式实现的核心是Object.defineProperty(obj,key,{....}),对要监听的每个属性进行setter和getter的设置。
对于依赖收集这一块由于牵扯到了整个生命周期,目前还不是很清楚,现水平的理解是:
在每个vue实例化后(至于在什么时候实例化目前不清楚),会遍历vue实例的data,使用的是observe(data),这个时候就会进行一系列操作(后文细说),这个时候就已经对data中大部分的属性进行监控了(get,set),说是大部分是因为对数组则没有监控每一项,而是采用重写几个常用的方法来弥补监控(具体下文)。然后模板引擎进行渲染时候render,会touch这些数据(应该是执行new Watcher类似的操作),会将依赖(watcher实例)存储至各个Dep实例中去,当被监听的数据set触发时,会通知(notify)watcher进行相应的回调,处理完后就会重新渲染(后续diff等等)。
具体实现两部分:数据侦测和依赖收集
数据侦测是调用observe(obj),(如果不是对象直接返回)会进行Observer类的实例检测,如果有直接返回这个ob实例,没有则创建Observer实例并挂载到_ob _ 属性上(这个属性使用def工具函数声明不可枚举遍历)。创建Observer实例首先会new Dep,把dep实例挂载到ob实例的dep属性上,然后会分两种情况进行处理:
- 如果是对象调用walk遍历子属性,调用defineReactive。内部会先创建dep实例,然后再observe(val)——这个时候就完成了递归,对象的所有属性都会遍历到并且所有属性有dep实例,其中对象属性会有dep属性。接着就是一个拥有val值闭包的difineProperty函数:里面的get会进行依赖收集,调用dep.depend(),set调用dep.notify()并对新值也进行observe(newValue)
- 如果是数组则引入arrayMethods并把proto属性指向arrayMethods,然后对数组每项进行observe。其中arrayMethods的实现是基于AOP思想,不修改Array.prototype上的原型,只是拿过来进行改写:arrayMethods是Object.creat(Array.prototype)得来,然后对arrayMethods里面的push,pop,shift,unshift,slice,sort,reverse使用def工具函数进行改写def(arrayMethods,'method',function(){})。实现方法具体是 拿到原型方法,并apply调用得到结果最后返回(arguments的数组转换),内部则对会增添元素的方法进行额外的observeArray(addEleArray)。这个时候也会进行通知 dep.notify()进行处理。
依赖收集则依靠两个类:Dep和Watcher类
Dep类是管理依赖的类,每个监听的属性都有一个dep实例,里面存储了sub数组(watcher实例,订阅者),还有depend方法:存在一个全局变量Dep.target,用于储存当前的订阅者watcher,内部则是如果存在这个订阅者则将这个订阅者推入sub数组。notify方法:触发通知,通知自身所有订阅者(遍历sub数组),每个调用update方法
Watcher类是中介类,用于接收通知和主动触发收集依赖,它的构造函数需要传入要监听的对象,属性表达式,和属性修改触发的回调函数。关于属性表达式,就会有相对应的解析函数——split('.') 并遍历,最终返回值,parsePath这个函数是返回解析函数。在new Watcher类时候,会在构造函数中获取初始值,过程中就会调用这个parsePath,因为读取了属性值,就很自然的触发了属性的get函数,就进入了依赖收集(属性的dep将存储watcher实例),最后把value,cb等属性值存储在watcher实例中。dep触发的update方法则会储存旧值并把value重新get一次,最后call方法调用cb,传入新值和旧值。
一句话说:这两部分分别意味着:数据劫持和发布订阅模式的实现