Vue 数据响应式原理 ★★

110 篇文章 1 订阅
104 篇文章 1 订阅

文章目录

Vue 数据响应式原理

Vue2.0 对象

完整流程图
在这里插入图片描述

Observe类:将正常的object转换为被检测的object

  1. Observer类的作用将正常的object转换为被侦测的object,通过getter和setter的形式来追踪。
  2. 在每个属性的getter中会生成对应的Dep对象。属性1 --> Dep对象1 属性2–> Dep对象2 …
  3. 在每个属性的setter中会通知Dep对象里的watcher对象数据发生变化了

Watcher类

  1. 在解析el模板中的指令的时候,通过get获取到数据,创建对应的watcher对象,将watcher对象存入该数据对应的Dep对象中
  2. 当数据发生变化时,在每个属性的setter中会通知Dep对象,调用notify()遍历通知watcher调用update()更新函数更新视图

过程详述

在这里插入图片描述

问题1:数据被修改后,Vue内部如何监听message数据的改变的? --> Object.defineProperty -> 监听对象属性的改变
问题2:当数据发生改变,Vue如何知道需要通知哪些地方更新界面? -->发布订阅者模式

  1. Object.defineProperty -> 监听对象属性的改变
    defineReactive函数定义一个响应式数据

    function defineReactive(data,key,val){
    Objcet.defineProperty(data,key,{//代码①
    enumerable:true,
    configurable:true,
    get:function(){
    return val;
    },
    set:function(){
    if(val===newVal)return;
    val = newVal;
    }
    })
    }

  2. 定义Observer类将一个正常的object转换成被侦测的object,循环给一个数据内的所有属性(包括子属性)都转换成getter/setter形式,

    //{a:“x”,b:{c:“y”,d:“z”}}
    //Observer类的作用将正常的object转换为被侦测的object
    class Observer{
    constructor(data){
    this.data = data;
    if(!Array.isArray(data)){ //这里数组和对象都会进来,只处理对象
    Object.keys(data).forEach(key=>{
    defineReactive(this.data,key,data[key]);
    })
    }
    }
    }
    function defineReactive(data,key,val){
    //如果val是对象,比如b:{c:“y”,d:“z”},说明不是最里层,还需要对{c:“y”,d:“z”}进行转换getter/setter
    if(typeof val === ‘object’)new Observer(val);//递归的目的是每一层的属性都应该被绑定响应式
    //…代码①
    }

  3. 现在对数据绑定了监听,但是当数据发生变化时,我们通知谁?我们怎么知道那些地方使用了这个数据?
    对于模板来说使用了name数据,也就是调用了name.get()方法,所以我们可以在get方法中存储用到了name属性的地方,我们可以先把使用了name属性的地方,叫做name属性的依赖。

    {{name}}

定义Dep类,Dep类的目的是存储使用了某数据的依赖,同时可以删除依赖、给依赖发更新通知等等,那么Dep类与某数据的关系应该是一一对应,所以我们在绑定响应式时,可以为每个属性绑定一个dep对象。在get的时候,将使用了name的地方(依赖)存进dep中,在set数据改变的时候,通知dep中的依赖数据发生改变了,Object在getter中收集依赖,在stter中触发依赖

class Dep {//Dep类存储依赖,添加依赖, 删除依赖,通知依赖等
	constructor(){
		this.subs = [];//subs存储依赖
	}
	addSub(sub){
		this.subs.push(sub);
	}
	depend(){
		//if(依赖){
		//	this.addSub(依赖);
		//}
		if(window.target){
			this.addSub(window.target);
		}
	}
	notify(){
		this.subs.forEach(sub =>{
			sub.update(); //通知这个属性的所有依赖数据更新
		})
	}
}
function defineReactive(data,key,val){
	if(typeof val === 'object')new Observer(val);
	let dep = new Dep(); //绑定dep对象
	Objcet.defineProperty(data,key,{
		enumerable:true,
		configurable:true,
		get:function(){
			dep.depend();//修改dep对象,这里应该添加依赖
			return val;
		},
		set:function(){
			if(val===newVal)return;
			val = newVal;
			dep.notify();//通知依赖,数据发生了修改
		}
	})
}
  1. 依赖是什么?之前我们把使用了数据的地方,叫做依赖,那么依赖描述成数据结构应该是什么样子的?
    定义一个Watcher类,一个Watcher对象就是一个依赖
    当属性变化时,就会调用dep的notify属性循环通知watcher调用update()方法,修改模板中的数据。

    class Watcher{
    constructor(vm,name,node){
    this.node = node;
    this.vm = vm;
    this.name = name;
    window.target = this, //我们给依赖命名为window.target, = watcher对象,所以依赖就是watcher对象
    this.update();
    window.target = null; //并没有绑定在实例上,全局仅有一个,数据更新后会重新调用get,防止一个watcher对象被多次加入dep.sub数组中
    }
    update(){//将{{name}}的name更新为vm里面的值,这里是在vm上代理了_data的值
    this.node.nodeValue = this.vm[this.name];
    //从vm中取某个属性,相当于调用该属性的getter方法,此时window.target是有值的,值为watcher对象,所以这个watcher对象会被存储进dep.sub数组里面
    }
    }
    //watcher对象在解析模板中的指令的时候会被创建new Watcher

不足以及解决办法

新增属性、删除属性、界面不会更新
解决:通过vm.$set(obj,key.val)/vm.$delete(obj,key)新增属性/删除属性

Vue2.0 数组

数组是通过改写数组的pushpopshiftunshiftsplicesortreverse

思路
定义一个拦截器对象,拦截器对象的__proto__隐式原型指向Array.prototype,拦截器对象重写上述的7个方法,重写的目的是增加响应式,但是最终调用的函数原型上的方法
为数组增加响应式的办法:让数组的__proto__隐式原型指向拦截器对象
在这里插入图片描述

过程详述

  1. 我们需要创建拦截器对象,重写这7个方法,重写的目的是增加响应式,但是最终调用的函数原型上的方法
    比如调用push方法时,实际调用的时arrayMethods.push,也就是mutator函数,最终调用的是原型上的push方法 --> 加一层mutator的目的是可以在mutator函数做一些其他事情,比如通知依赖

    const arrayProto = Array.prototype;
    export const arrayMethods = Object.create(arrayProto); //arrayMethods.proto = Array.prototype

    const methodsNeedChange = [‘push’,‘pop’,‘shift’,‘unshift’,‘splice’,‘sort’,‘reverse’]; //需要被改写的七个方法
    methodsNeedChange.forEach(function(method){
    const original= arrayProto[method]; //缓存原来的方法,最终还是会被调用
    Object.defineProperty(arrayMethods, method, {//为拦截器对象增加7个方法
    //value:重写的方法
    value:function mutator(…args){
    return original.apply(this,args); //最终调用的是原型的方法,this指向拦截器
    }
    enumerable:false;//不可以被枚举
    writable:true; //可以被遍历
    configurable:true; //可以被删除
    })

    }

  2. 我们现在要为数组增加响应式,方法是让数组的__proto__隐式原型指向拦截器对象,Observer类的作用就是增加响应式,我们之前为对象增加了响应式。

    //引入arrayMethods
    class Observer{
    constructor(data){
    this.data = data;
    if(Array.isArray(data)){//为数组增加响应式
    data.proto = arrayMethods;
    }
    else{ //为对象增加响应式
    Object.keys(data).forEach(key=>{
    defineReactive(this.data,key,data[key]);
    })
    }
    }
    }

注意:有些浏览器不支持__proto__,如果不支持,Vue直接把arrayMethods身上的改写方法设置到被侦测的数组上

  1. 还是需要考虑一个问题,数组改变了去通知谁?如何收集依赖?在哪里触发依赖?
    需要注意的一个问题是data是一个对象,如果有数组也是存在对象中的,所以数组是在getter中收集依赖,在拦截器中触发依赖
    数组中元素的修改时通过拦截器重写的方法,那么如果触发了重写方法说明数据改变了,此时我们就需要通知依赖,所以是在拦截器中触发依赖。

  2. 拦截器怎么能看见依赖?我们要把依赖放在哪里?

object是从一个属性对应一个Dep数组,所以写在了defineReactive函数中,因为需要在getter中收集依赖,在setter中触发依赖,在getter、setter的时候需要看得见依赖。
同理,我们需要把依赖保存在getter和拦截器都能看见依赖的地方,也就是Observer实例中。
因为在getter中可以访问到Observer实例,在拦截器中也可以访问到Observer实例(这个地方在后面讲,先认定这个结论)

//引入arrayMethods
class Observer{
	constructor(data){
		//....
		this.dep = new Dep();//在Observer实例上新增dep
		if(Array.isArray(data)){//为数组增加响应式
			data.__proto__ = arrayMethods;
		}
		else{ //为对象增加响应式
			//....
		}	
	}
}

怎么在拦截器中访问到Oberver实例?给数组增加一个属性__ob__,这个__ob__指向Oberver实例

//工具函数给obj身上的key添加val
function def(obj,key,val,enumerable){
	Object.defineProperty(obj,key,{
		value:val,
		enumerable:!!enumerable,
		writeable:true,
		configurable:true
	})
}
class Observer{
	constructor(data){
		//....
		this.dep = new Dep();//在Observer实例上新增dep
		def(data,'__ob__',this); //拦截器可以通过数组身上的__ob__属性访问到Observer实例
		if(Array.isArray(data)){//为数组增加响应式
			data.__proto__ = arrayMethods;
		}
		else{ //为对象增加响应式
			//....
		}	
	}
}

现在我们在可以在拦截器里看见依赖了,也就是通过数组身上的__ob__属性,那么我们就可以在拦截器中通知依赖,告诉Watcher数据变啦。

[...].forEach(function(method){
	const original = arrayProto[method];//缓存Array原型上的方法
	def(arrayMethods,method,function mutator(...args){
		const result = original.apply(this,args);//this指向拦截器
		this.__ob__.dep.notify(); //向依赖发送数据!!!
		return result;
	})
})
  1. 在getter收集依赖,添加到observer实例的dep实例中

    function defineReactive(data,key,val){
    //if(typeof val === ‘object’)new Observer(val);
    let childOb = observe(val); //上面的判断也会在这个函数中判断
    let dep = new Dep(); //绑定dep对象
    Objcet.defineProperty(data,key,{
    enumerable:true,
    configurable:true,
    get:function(){
    dep.depend();//对象的依赖收集
    if(childOb){
    childOb.dep.depend(); //数组的依赖收集,收集在observer实例的Dep上
    }
    return val;
    },
    set:function(){
    if(val===newVal)return;
    val = newVal;
    dep.notify();//通知依赖,数据发生了修改
    }
    })
    }
    export function observe (value){ //observe函数:为数组和对象返回observe实例
    if(!isObject(value))return;
    let ob
    if(hasOwn(value,‘ob’) && value.ob_ instanceof Observer){//如果该数组已经有了__ob__,已经创建了observer实例,已经是响应式的了
    ob = value.ob;
    }else{ //数组没有observer实例 或者是对象则会创建observer实例
    ob = new Observer(value);
    }
    return ob;
    }

__ob__的作用
1.让拦截器可以访问到observer实例:通过将observer实例绑定在数组的__ob__属性上
2.用来标识当前value是否已经被Observer类转化为响应式数据

  1. 如果数组里面套对象怎么办?侦察Array中的每一项

    class Observer{
    constructor(data){
    //…
    if(Array.isArray(value)){
    this.observeArray(value); //侦察Array中的每一项
    //…
    }
    }
    //…
    observeArray(items){//循环侦察Array中的每一项
    for(let i=0,l=items.length;i<1;i++){
    observe(items[i]);//observe函数:为数组和对象返回observe实例
    }

    }
    
  2. 还有一点是如果push、unshift、splice新增数组元素进来,那么对于新增的元素我们也需要用Observe来侦测,让其变为响应式的。具体实现是把新增元素取出来,调用数组身上的observer实例的observeArray方法

对象和数组响应式原理的对比与整理

在哪里收集依赖在哪里通知依赖?

  • 对象:在getter中收集依赖,在setter中触发依赖
  • 数组:在getter中收集依赖,在拦截器arrayMethods中触发依赖

哪里创建Dep实例
在收集依赖的地方和触发依赖的地方都能看见

  • 对象:在defineReactive函数里创建Dep实例,每一个属性对应一个Dep实例
  • 数组:在Observer类里给Observer实例绑定Dep实例,每一个observer实例对应一个Dep实例

Observer类:侦察对象和数组的变化
主要目的:为数组和对象增加响应式
思路
1.为数组准备好Dep实例和为数组添加__ob__属性,该属性指向Observer实例,每一个数组都会绑定一个observer实例

__ob__的作用
1.让拦截器可以访问到observer实例:通过将observer实例绑定在数组的__ob__属性上
2.用来标识当前value是否已经被Observer类转化为响应式数据

2.循环侦察Array中的每一项,让数组的隐式原型指向拦截器对象

observeArray方法:循环侦察Array中的每一项,数组中的每一项调用observe函数
observe函数:为数组和对象返回observe实例

3.循环将对象中的每一个属性都转化为getter/setter模式

defineReactive:将对象的每一个属性转化为getter/setter,为对象的 对象的每一个属性对应一个Dep实例,对象的Dep实例在这个函数中创建。在getter收集依赖,在setter中触发依赖通知Dep
数组在getter中通过observer实例的Dep实例收集依赖

//工具函数给obj身上的key添加val
function def(obj,key,val,enumerable){
	Object.defineProperty(obj,key,{
		value:val,
		enumerable:!!enumerable,
		writeable:true,
		configurable:true
	})
}
class Observer{
	constructor(data){
		this.dep = new Dep();//在Observer实例上新增dep
		def(data,'__ob__',this); //为数组绑定observer实例
		if(Array.isArray(value)){//处理数组
			this.observeArray(value); //侦察Array中的每一项
			data.__proto__ = arrayMethods; //让数组的隐式原型指向拦截器对象
		}
		else{ //处理对象
			Object.keys(data).forEach(key=>{//循环将每一个属性转化为getter/setter
				defineReactive(this.data,key,data[key]);
			})
		}
    }
    observeArray(items){//循环侦察Array中的每一项
		for(let i=0,l=items.length;i<1;i++){
			observe(items[i]);//observe函数:为数组和对象返回observe实例
		}
}
function defineReactive(data,key,val){
		let childOb = observe(val); //返回observe实例
	    let dep = new Dep(); //绑定dep对象
		Objcet.defineProperty(data,key,{
			enumerable:true,
			configurable:true,
			get:function(){
				dep.depend();//对象的依赖收集
			if(childOb){
				childOb.dep.depend(); //数组的依赖收集,收集在observer实例的Dep上
			}
			return val;
		},
		set:function(){
			if(val===newVal)return;
			val = newVal;
			dep.notify();//通知依赖,数据发生了修改
		}
	})
}
}
export function observe (value){ //observe函数:为数组和对象返回observe实例
	if(!isObject(value))return;
	let ob
	if(hasOwn(value,'__ob__') && value.__ob___ instanceof Observer){//如果该数组已经有了__ob__,已经创建了observer实例,已经是响应式的了
		ob = value.__ob__;
	}else{ //数组没有observer实例 或者是对象则会创建observer实例
		ob = new Observer(value);
	}
	return ob;
}

数组专有:拦截器:重写七个方法,使其加入响应式

拦截器的作用
1.拦截数组的七个方法,将拦截器的__proto__隐式原型指向Array.prototype显式原型
2.通知依赖数据改变了,通过调用数组绑定的observer对象找到observer对象绑定的dep实例通知依赖

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto); //arrayMethods.__proto__ = Array.prototype

const methodsNeedChange = ['push','pop','shift','unshift','splice','sort','reverse']; //需要被改写的七个方法
methodsNeedChange.forEach(function(method){
	const original= arrayProto[method]; //缓存原来的方法,最终还是会被调用
	def(arrayMethods,method,function mutator(...args){
		const result = original.apply(this,args);//this指向拦截器,调用原型的方法
		this.__ob__.dep.notify(); //向依赖发送数据!!!
		return result;
	})
}

共有 Dep类:对依赖进行管理 没有对数组和对象分别处理

1.使用数据的时候收集依赖
2.通知依赖数据发生变化
3.对依赖进行管理,比如增加依赖、删除依赖、通知依赖等
Dep实例和数据的关系是一一对应的

class Dep {//Dep类存储依赖,添加依赖, 删除依赖,通知依赖等
	constructor(){
		this.subs = [];//subs存储依赖
	}
	addSub(sub){
		this.subs.push(sub);
	}
	depend(){
		if(window.target){
			this.addSub(window.target);
		}
	}
	notify(){
		this.subs.forEach(sub =>{
			sub.update(); //通知这个属性的所有依赖数据更新
		})
	}
}

共有 Watcher类:依赖 没有对数组和对象分别处理

中介,数据发生改变通知外界。外界通过Watcher类读取数据
当模板解析时会创建Watcher实例,此时调用update方法,从vm中取某个属性,相当于调用该属性的getter方法,此时window.target是有值的,值为watcher对象,所以这个watcher对象会被存储进dep.sub数组里面
Dep类通知依赖时也会调用update方法

export class Watcher{
	constructor(vm,name,node){
		this.node = node;
		this.vm = vm;
		this.name = name;
		window.target = this, //我们给依赖命名为window.target, = watcher对象,所以依赖就是watcher对象
		this.update();
		window.target = null; //并没有绑定在实例上,全局仅有一个,数据更新后会重新调用get,防止一个watcher对象被多次加入dep.sub数组中
	}
	update(){//将{{name}}的name更新为vm里面的值,这里是在vm上代理了_data的值
		this.node.nodeValue = this.vm[this.name]; 
		//从vm中取某个属性,相当于调用该属性的getter方法,此时window.target是有值的,值为watcher对象,所以这个watcher对象会被存储进dep.sub数组里面
	}
}
//watcher对象在解析模板中的指令的时候会被创建new Watcher
主要的实现方式描述/总结
  • 对象
    数据传给Observer类,该类的作用是把一个object中的所有数据(包括子属性)都转换成响应式的,它利用defineProperty方法,为对象中的每个属性)绑定getter和setter方法以及dep实例,dep实例和属性一一对应,它的作用是收集使用了该属性的地方(依赖watcher)。在getter方法里收集依赖,在setter方法里触发依赖,利用dep通知watcher数据发生改变,watcher再通知界面数据发生改变

在这里插入图片描述

  • 数组:加入拦截器
    在数组的身上绑定observer实例(数组的__ob__属性)和dep实例(子数组也会),dep实例是存储与管理使用该数组的地方(依赖),修改该数组的隐式原型__proto__指向拦截器,拦截器对象的隐式原型__proto__指向Array.prototype,在拦截器中拦截数组中的七个方法,进行一些处理后,再调用Array.prototype上的对应方法。这样调用数组中的七个方法会先调用拦截器中重写的方法,调用重写方法时说明数组发生了变化,在重写方法中会通知该数组oberver实例上dep数据修改了,dep在通知依赖(watcher)数据修改了,最后watcher通知界面数据修改了。依赖的收集是在getter中进行的,将依赖收集在oberver实例的dep上
    对于push、unshift、splice方法新增的元素,先将新增元素取出,然后使用observeArray新增数组进行变化监测
    在这里插入图片描述

不足以及解决办法

直接通过下标修改数组,界面不会自动更新

解决:vm.$set(arr,index.value),调用数组的splice方法,或者采用splice用新值替换该下标值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值