文章目录
Vue 数据响应式原理
Vue2.0 对象
完整流程图
Observe类:将正常的object转换为被检测的object
- Observer类的作用将正常的object转换为被侦测的object,通过getter和setter的形式来追踪。
- 在每个属性的getter中会生成对应的Dep对象。属性1 --> Dep对象1 属性2–> Dep对象2 …
- 在每个属性的setter中会通知Dep对象里的watcher对象数据发生变化了
Watcher类
- 在解析el模板中的指令的时候,通过get获取到数据,创建对应的watcher对象,将watcher对象存入该数据对应的Dep对象中
- 当数据发生变化时,在每个属性的setter中会通知Dep对象,调用notify()遍历通知watcher调用update()更新函数更新视图
过程详述
问题1:数据被修改后,Vue内部如何监听message数据的改变的? --> Object.defineProperty -> 监听对象属性的改变
问题2:当数据发生改变,Vue如何知道需要通知哪些地方更新界面? -->发布订阅者模式
-
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;
}
})
} -
定义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);//递归的目的是每一层的属性都应该被绑定响应式
//…代码①
} -
现在对数据绑定了监听,但是当数据发生变化时,我们通知谁?我们怎么知道那些地方使用了这个数据?
{{name}}
对于模板来说使用了name数据,也就是调用了name.get()方法,所以我们可以在get方法中存储用到了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();//通知依赖,数据发生了修改
}
})
}
-
依赖是什么?之前我们把使用了数据的地方,叫做依赖,那么依赖描述成数据结构应该是什么样子的?
定义一个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 数组
数组是通过改写数组的push
、pop
、shift
、unshift
、splice
、sort
、reverse
思路
定义一个拦截器对象,拦截器对象的__proto__
隐式原型指向Array.prototype
,拦截器对象重写上述的7个方法,重写的目的是增加响应式,但是最终调用的函数原型上的方法
为数组增加响应式的办法:让数组的__proto__
隐式原型指向拦截器对象
过程详述
-
我们需要创建拦截器对象,重写这7个方法,重写的目的是增加响应式,但是最终调用的函数原型上的方法
比如调用push方法时,实际调用的时arrayMethods.push,也就是mutator函数,最终调用的是原型上的push方法 --> 加一层mutator的目的是可以在mutator函数做一些其他事情,比如通知依赖const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto); //arrayMethods.proto = Array.prototypeconst 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; //可以被删除
})}
-
我们现在要为数组增加响应式,方法是让数组的
__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身上的改写方法设置到被侦测的数组上
-
还是需要考虑一个问题,数组改变了去通知谁?如何收集依赖?在哪里触发依赖?
需要注意的一个问题是data是一个对象,如果有数组也是存在对象中的,所以数组是在getter中收集依赖,在拦截器中触发依赖。
数组中元素的修改时通过拦截器重写的方法,那么如果触发了重写方法说明数据改变了,此时我们就需要通知依赖,所以是在拦截器中触发依赖。 -
拦截器怎么能看见依赖?我们要把依赖放在哪里?
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;
})
})
-
在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类转化为响应式数据
-
如果数组里面套对象怎么办?侦察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实例
}}
-
还有一点是如果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用新值替换该下标值