VUE2数据响应式原理

前言

在vue2的官方文档上可以看到它对响应式原理的解释是:
image.png
总结来说分为两步:

  1. 数据劫持:通过Object.defineProperty方法实现vue中data选项数据的监听
  2. 订阅-发布者模式:通过Watcher和Dep采用观察者模式实现依赖收集和派发更新的过程

gitHub代码地址:https://github.com/seapack-hub/webpack-loader-plugin

Object.defineProperty介绍

Object.defineProperty() 静态方法会直接在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。

参数及用法

Object.defineProperty()接受三个参数。
obj:要定义属性的对象。
prop:要定义或修改的属性的名称
descriptor:要定义或修改的属性描述符,分为数据描述符和访问器描述符。

Object.defineProperty(obj, prop, descriptor);
数据描述符
  1. configurable:是否可删除
  2. enumerable:是否可枚举
  3. value:属性值
  4. writable:是否可编辑

实例测试下,

let obj = {};
Object.defineProperty(obj,'a',{
    value:5,
    //是否可枚举
    enumerable:false,
    //是否可删除
    configurable:false,
    //是否可编辑
    writable:false
})
obj.a = 10;
delete obj.a;
console.log(obj);  //obj={a:5},设置了不可修改和删除,a的值不会变化。
访问器描述符

get:用作属性 getter 的函数,如果没有 getter 则为 undefined。当访问该属性时,将不带参地调用此函数,并将 this 设置为通过该属性访问的对象(因为可能存在继承关系,这可能不是定义该属性的对象)。返回值将被用作该属性的值。默认值为 undefined。 访问对象上的属性时会自动调用这个方法,方法的返回值作为对象上属性的值,不可与value和writable同时使用。
set:用作属性 setter 的函数,如果没有 setter 则为 undefined。当该属性被赋值时,将调用此函数,并带有一个参数(要赋给该属性的值),并将 this 设置为通过该属性分配的对象。默认值为 undefined。修改对象上的属性时会调用此方法,不可与value和writable同时使用。
实例如下:

let obj = {};
let bValue = 5
Object.defineProperty(obj,'a',{
    //是否可枚举
    enumerable:true,
    //是否可删除
    configurable:true,
    get(){
        console.log('访问a元素')
        return bValue;
    },
    set(value){
        console.log("更改后a的值为:",value);
        bValue = value;
    }
})
console.log(obj.a);  //访问a元素  5
obj.a = 10;         //更改后a的值为:10
console.log(obj);   //{a:10}

如上所示,根据get获取属性值,通过set设置属性值,如果想让set赋值的属性值通过get显示出来,可以在全局设置一个变量,来实现此种效果。
平时我们给对象赋值不需要这么麻烦,直接添加属性赋值就可以。obj.xxx=xxx 。如果想查看对象上某个属性的属性描述符,可以通过Object.getOwnPropertyDescriptor()方法获取。如下示例,我们可以看到,通过此种方式添加的属性其属性描述符默认为true。

let obj = {};
obj.c = 15
let prop = Object.getOwnPropertyDescriptor(obj,'c');
//输出结果:
// {
//   "value": 15,
//   "writable": true,
//   "enumerable": true,
//   "configurable": true
// }
小结

以上介绍,我们可以看到可以通过三种方式来实现在对象上添加一个属性。

  1. 直接赋值:obj.xxx = xxx
  2. 使用Object.getOwnPropertyDescriptor()的value和writable数据描述符
  3. 使用Object.getOwnPropertyDescriptor()的getter和setter的访问描述符。

方式一和方式二在某种程度上是等同的,区别在于方式二可以动态的设置数据描述符的值而方式一将其默认设置为true,方式三与前两者的区别在于其使用自定义的get和set方法来实现读取与赋值,可以在数据变动时做一些其他的操作,达到数据监控的效果。

二次封装-defineReactive

通过前面的内容我们可以了解到,使用Object.defineProperty()方法可以实现对对象上属性的监听与赋值操作,但其需要在方法之外设置一个公共变量来传递数据。按此种方式,如果需要监听多个对象,就要多设置几个参数,比较麻烦。
为此,我们可以做一下简单的封装。创建一个defineReactive.js文件,将方法封装在其中

//defineReactive.js
/**
 * 封装Object.defineProperty()
 * @param data 对象
 * @param key 添加的属性
 * @param val 属性值
 */
export function defineReactive(data,key,val){
    Object.defineProperty(data,key,{
        //可配置,如删除
        configurable:true,
        //可枚举
        enumerable:true,
        get(){
            console.log(key,"被访问了");
            return val;
        },
        set(value){
            console.log(`${key}属性被赋值了,新值为:${value}`);
            if(val === value){
                return;
            }
            val = value;
        }
    })
}

实例:封装后每次属性的封装和赋值都可以监听到。

//index.js
import {defineReactive} from './defineReactive';
let obj = {};
defineReactive(obj,'a',5);
defineReactive(obj,'b',10);
console.log(obj.a);    //a 被访问了 5
console.log(obj.b);    //b 被访问了 10
obj.a = 8;             //a属性被赋值了,新值为:8

深度监听对象属性

Object.defineProperty的局限

通过上面的操作实例操作,我们已经可以监听对象上的属性,但如果我们添加的属性值是一个对象,能否监听属性值对象里面的属性呢?测试一下。

//index.js
import {defineReactive} from './defineReactive';
let obj = {};
let student = {
    name:"张三",
    age:18,
    subject:{
        Chinese:'98',
        English:'80'
    }
}
defineReactive(obj,'student',student);
console.log(obj.student); 
//student 被访问了
// {
//   "name": "张三",
//   "age": 18,
//   "subject": {
//     "Chinese": "98",
//     "English": "80"
//   }
// }
console.log(obj.student.name);
//student 被访问了
//张三
console.log(obj.student.subject.Chinese);
//student 被访问了
//98

通过上面的例子可以看到,通过Object.defineProperty方法只能监听object对象上第一层级的属性,对于深层对象里面的属性是无法监听的。
接下来的目标就是将一个正常的object转换为每个层级的属性都是响应式(可以被侦测到的)object。
先对defineReactive方法做一下优化,方便之后的操作

//defineReactive.js
export function defineReactive(data,key,val){
    //若没有传入属性值,且对象中含有这个属性的属性值时,直接将其赋值给val
    if(arguments.length == 2){
        val = data[key];
    }
    ...
}

之后,我们可以利用这个方法对object对象上的第一层属性设置监听。

//index.js
import {defineReactive} from './defineReactive';
let student = {
    name:"张三",
    age:18,
    subject:{
        Chinese:'98',
        English:'80'
    }
}
for(let key in student){
    defineReactive(student,key);
}
console.log(student.name); //name 被访问了  张三
student.age = 20;  //age属性被赋值了,新值为:20

之后的开发思路类似,通过递归的思想给对象的每一层属性都设置监听。大致分为以下三步

  1. 创建Observer类进行递归给对象设置监听
  2. 创建observe函数辅助调用Observe类
  3. 创建utils.js辅助方法集合帮助实现监听
实现对象第一层属性监听

通过上面的方法,我们已经可以实现第一层的监听,但还需要一些辅助方法,以及将具体的功能分类抽取。
新建 utils.js文件,创建def方法,为对象添加不可枚举属性。

//utils.js
/**
 * 设置属性值是否可以被枚举
 * @param data 对象
 * @param key 属性
 * @param value 属性值
 * @param enumerable 是否可被枚举
 */
export function def(data,key,value,enumerable){
    Object.defineProperty(data,key,{
        value,
        enumerable,
        configurable:true,
        writable:true
    })
}

创建Observer类,为对象第一层属性设置监听

//Observer.js
import { def } from './utils'
import {defineReactive} from "./defineReactive";

export default class Observer{
    //创建构造器
    constructor(value){
        //1.给对象上添加__ob__属性
        //__ob__ 一般都会设置为不可枚举的属性,它本身只是存储Observer类实例的
        // 构造函数中的this不是表示类本身,而是表示类的实例
        def(value,'__ob__',this,false);
        this.walk(value);
    }
    //遍历对象,为每个属性添加监听
    walk(value){
        for(let key in value){
            defineReactive(value,key);
        }
    }
}

创建observe方法,用来判断属性值类型,并存储Observer实例。作为数据响应式的入口。

//observe.js
import Observer from "./Observer";
/**
 * 存储Observer类的实例
 * @param value 监控的对象
 */
export function observe(value){
    if(typeof value != 'object'){
        return;
    }
    //定义ob
    let ob;
    //__ob__ 属性的作用就是存储Observer类的实例,
    //前后都加__ 主要是不想与其他常见的属性重名
    if(typeof value.__ob__ !== 'undefined'){
        ob = value.__ob__;
    }else{
        ob = new Observer(value);
    }
    return ob;
}

至此,创建了Observer类,def和observe方法,实现了对象第一层属性的监听。先进行下测试
测试代码:

//index.js
//引入observe
import {observe} from './observe'
let student = {
    name:"张三",
    age:18,
    subject:{
        Chinese:'98',
        English:'80'
    }
}
observe(student);
console.log(student);

结果:
image.png
添加了__ob__这个属性,age,name,subject三个属性也都设置了监听。但subject属性值对象没有设置监听
image.png

递归实现深度监听

上面的代码实现了对象第一层的监听,接下来实现更深层的监控。
在defineReactive函数中调用observe函数,实现对子对象的递归。

//defineReactive.js
import {observe} from "./observe";

/**
 * 封装Object.defineProperty()
 * @param data 对象
 * @param key 添加的属性
 * @param val 属性值
 */
export function defineReactive(data,key,val){
    //若没有传入属性值,且对象中含有这个属性的属性值时,直接将其赋值给val
    if(arguments.length == 2){
        val = data[key];
    }
    //子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用
    let childOb = observe(val);

    Object.defineProperty(data,key,{
        //可配置,如删除
        configurable:true,
        //可枚举
        enumerable:true,
        get(){
            console.log(key,"被访问了");
            return val;
        },
        set(value){
            console.log(`${key}属性被赋值了,新值为:${value}`);
            if(val === value){
                return;
            }
            val = value;
            //当设置了新值,新值也需要调用observe
            childOb = observe(val);
        }
    })
}

//测试效果

import {observe} from './observe'
let student = {
    name:"张三",
    age:18,
    subject:{
        Chinese:'98',
        English:'80'
    }
}
observe(student);
console.log(student);
console.log(student.subject.English);

image.png
可以看出,所有的所有的属性值都被设置了监控。
总结,整理下具体的实现过程,在这次递归中主要是三方起到了递归作用。分别是:

  1. observe: 入口,主要作用是为对象创建并存储Observer类实例,
  2. Observer类:主要作用是遍历对象的每个属性并调用defineReactive方法,
  3. defineReactive:为每个属性设置监听。

其调用执行过程如下图:
image.png
其中,红色的箭头表示循环递归的方向,绿色的箭头表示入口和出口(结束递归),椭圆表示类或方法里面的内容。两个判断应该是在observe函数里面,将其拿出来是为了方便理解。此次递归主要由observe函数,Observer类,defineReactive函数循环调用来实现,而不是一般的递归那种函数调用函数本身。

深度监听数组

数组与对象

之前的代码我们考虑了属性值为对象的情况,是可以实现深度监听,如果属性值为数组,那效果能否实现呢?测试下。

//index.js
import {observe} from './observe'
let student = {
    name:"张三",
    age:18,
    subject:{
        Chinese:'98',
        English:'80'
    },
    grade:[99,100,85,76,60]
}
observe(student);
console.log(student.grade)
student.grade.push(88);

结果如下图:
image.png
当我们访问数组元素的时候可以被监听到,但是当使用push向数组里面添加元素时只能触发访问监听,不能触发修改监听。

为什么要重写数组方法?

通过Object.defineProperty方法监听的数组,只有在数组被覆盖或其引用被改变时才能监听到,对于数组元素内索引的改变是无法监听到的。

import {observe} from './observe'
let student = {
    name:"张三",
    age:18,
    subject:{
        Chinese:'98',
        English:'80'
    },
    grade:[99,100,85,76,60]
}
observe(student);
//此时通过push添加数组元素只能监听到数组被访问了。不能监听到数组被修改了。
student.grade.push(100);
//grade 被访问了

//只有直接替换整个数组才能通过Object.defineProperty监听到
student.grade = [101,102];
//grade属性被赋值了,新值为:101,102

总结:数组被修改大致分两类

  1. 直接替换数组。此种方式是可以被Object.defineProperty方法监听到的。
  2. 数组内元素的变动。这种修改是无法被监听到的。

涉及修改数组内元素的方法有7种,所以需要重写这七种方法,当数组内元素变动时,可以被监听到。

重写数组方法的实现逻辑

对于一般对象,vue提供了Vue.set(object,propertyName,value) 方法来使对象添加的属性是响应式的。
数组是一种特殊的对象,有很多操作数组的方法,如push,shift,sort等等,vue重写了一些方法来实现数组的响应式。
常见的数组方法

方法名称作用是否更改原数组
concat用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组
sort对数组的元素进行排序,并返回对相同数组的引用
reverse反转数组中的元素,并返回同一数组的引用。
join将一个数组的所有元素连接成一个字符串并返回这个字符串,不更改原数组。
push指定的元素添加到数组的末尾,并返回新的数组长度。
pop从数组中删除最后一个元素,并返回该元素的值。此方法会更改数组的长度。
shift从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。
unshift将指定元素添加到数组的开头,并返回数组的新长度。
slice返回一个新的数组对象,这一对象是一个由 start 和 end 决定的原数组的浅拷贝
splice通过移除或者替换已存在的元素和/或添加新元素就地改变一个数组的内容

数组还有很多其他的方法,但常用的就这些,其中有七个方法涉及到了更改原数组,vue对这些方法进行了重写。
重写七个数组方法
一般我们定义数组时使用 new Array() ,这样写有些麻烦,之后有一些简写语法

let arr = new Array();
//简写
let att = [];

平时我们使用的数组方法是在数组原型上的(Array.prototype);

let arr = Array.prototype;
console.log(arr);

image.png
声明一个数组后,Array原型上的方法会放到该数组的原型上,因此,我们用重写后的新方法覆盖掉原来的方法来实现

let arr = [];
console.log('数组:',arr);

image.png
大致思路如下:
1,备份原型的方法,
2,创建新方法实现监控操作,并调用原方法,保证新方法保持原有方法的功能
3,用新的方法覆盖掉数组原型上的方法(不是Array.prototype)
原理示意图如下所示:
image.png
代码实现:
新建array.js文件,具体操作如下:
1.以Array.prototype为原型创建arrayMethods对象
2.将重写的七个方法放到arrayMethods对象上(不是放到arrayMethods的原型上)
3.将arrayMethods对象暴露出去

//array.js
import {def} from './utils'
//获取Array.prototype 原型
const arrayPrototype = Array.prototype;

// 以Array.prototype为原型创建arrayMethods对象,并暴露
export const arrayMethods = Object.create(arrayPrototype);

//要被改写的七个数组方法
const methodsNeedChange = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
];

//重写七个数组方法‘’
methodsNeedChange.forEach(name=>{
    //复制原方法
    let original = arrayPrototype[name];
    //在arrayMethods上添加新方法,并在其中调用原方法保留其功能
    def(arrayMethods,name,function(){
        console.log(`${name}方法重写了`);
        original.apply(this,arguments)
    },false)
})

代码中关于apply的用法,是有关this指向的,这里就不多概述了,想进一步了解的可以看这位大佬的文章,里面讲的比较详细。
https://juejin.cn/post/7128233572380442660
在Observer类中替换数组类型的原型

//Observer.js
import { def } from './utils'
import {defineReactive} from "./defineReactive";

export default class Observer{
    //创建构造器
    constructor(value){
        //1.给对象上添加__ob__属性
        //__ob__ 一般都会设置为不可枚举的属性,它本身只是存储Observer类实例的
        // 构造函数中的this不是表示类本身,而是表示类的实例
        def(value,'__ob__',this,false);
        //判断类型是否是数组,如是数组,使用arrayMethods替换数组的原型
        if(Array.isArray(value)){
            Object.setPrototypeOf(value,arrayMethods);
        }
        this.walk(value);
    }
    //遍历对象,为每个属性添加监听
    walk(value){
        for(let key in value){
            defineReactive(value,key);
        }
    }
}

测试数据

import {observe} from './observe'
let student = {
    name:"张三",
    age:18,
    subject:{
        Chinese:'98',
        English:'80'
    },
    grade:[99,100,85,76,60]
}
observe(student);
student.grade.push(101);
console.log(student.grade);

结果如下:数组的push方法已经用新的方法替换了。新方法也实现了原方法的功能。
image.png
接下来就是重要的部分了,为数组变动的元素设置监听。
替换数组原型并设置方法监听

1.设置返回值

重写的七个方法中reverse,push,pop…等都是有返回值的,重写的方法需要有原方法的功能,所以需要设置返回值。

//array.js
...
//重写七个数组方法‘’
methodsNeedChange.forEach(name=>{
    //复制原方法
    let original = arrayPrototype[name];
    //在arrayMethods上添加新方法,并在其中调用原方法保留其功能
    def(arrayMethods,name,function(){
        console.log(`${name}方法重写了`);
        const result = original.apply(this,arguments);
        
        return result;
    },false)
})
2,理解重写方法中this的指向

最简单的理解方法就是:谁调用了这个方法,方法里面的this就指向谁。根据原理示意图,重写后的方法放到arrayMethods对象上,arrayMethods对象又替换了arr数组的原型。最后是数组调用重写的方法,所以方法里面的this应该指向数组。
image.png
测试下

//array.js
...
//重写七个数组方法‘’
methodsNeedChange.forEach(name=>{
    //复制原方法
    let original = arrayPrototype[name];
    //在arrayMethods上添加新方法,并在其中调用原方法保留其功能
    def(arrayMethods,name,function(){
        console.log(`${name}方法重写了`);
        const result = original.apply(this,arguments);
        console.log("this是谁?",this)
        return result;
    },false)
})
//index.js
import {observe} from './observe'
let student = {
    name:"张三",
    age:18,
    subject:{
        Chinese:'98',
        English:'80'
    },
    grade:[99,100,85,76,60]
}
observe(student);
student.grade.push(101);
console.log(student.grade);

结果如下:this指向 数组student.grade
image.png

3,获取Observer类,给数组替换原型。

在Observer类中遍历数组元素,递归为元素为数组的值替换原型

//Observer.js
import { def } from './utils'
import {defineReactive} from "./defineReactive";

import {arrayMethods} from "./array";
import {observe} from "./observe";
export default class Observer{

    //创建构造器
    constructor(value){
        //1.给对象上添加__ob__属性
        //__ob__ 一般都会设置为不可枚举的属性,它本身只是存储Observer类实例的
        // 构造函数中的this不是表示类本身,而是表示类的实例
        def(value,'__ob__',this,false);

        //判断类型是否是数组,如是数组,使用arrayMethods替换数组的原型
        if(Array.isArray(value)){
            Object.setPrototypeOf(value,arrayMethods);
            // 如果数组里面还包含数组 需要递归判断
            this.observeArray(value)
        }else{
            this.walk(value);
        }
    }
    //遍历对象,为每个属性添加监听
    walk(value){
        for(let key in value){
            defineReactive(value,key);
        }
    }
    //遍历数组,为每个数组项添加监控
    observeArray(arr){
        //先获取数组长度,pop,shift等方法调用后会更改数组长度
        for(let i = 0,l=arr.length;i<l;i++){
            //逐项进行observe
            observe(arr[i]);
        }
    }
}

在array.js中为新添加的项设置监控,如果新加的项为数组也需要替换原型。

//array.js
...
//重写数组方法
methodsNeedChange.forEach(name=>{
    //复制原方法
    let original = arrayPrototype[name];
    //在arrayMethods上添加新方法,并在其中调用原方法保留其功能
    def(arrayMethods,name,function(){
        console.log(`${name}方法重写了`);
        const result = original.apply(this,arguments);
        //获取Observer类,为监听数据做准备
        const ob = this.__ob__;

        //将类数组对象转变为数组,之后会调用数组的方法,类数组对象上没有
        //获取七个方法的参数
        const args = [...arguments];

        //获取数组新增项 push,unshift,splice 方法都可以向数组中添加新项
        let inserted = [];
        switch (name){
            case "push":
            case "unshift":
                inserted = args;
                break;
            case "splice":
                // splice格式是splice(下标, 数量, 插入的新项)
                inserted = args.slice(2);
                break;
        }
        //判断是否有新项插入,调用Observer实例的observeArray对数组每一项进行观测。
        if(inserted){
            ob.observeArray(inserted);
        }
        //数组改变时通知更新
        ob.dep.notify();
        return result;
    },false)
})

测试结果:

import {observe} from './observe'
let student = {
    name:"张三",
    age:18,
    subject:{
        Chinese:'98',
        English:'80'
    },
    grade:[99,100,85,76,60]
}
observe(student);
student.grade.push(101);
console.log(student.grade);

image.png
调用数组方法时数组被监听到了。至此我们实现了对象和数组的监听。但和对象有所区别的是,对数组的监听只是能监控到数组本身,不能具体监控到是数组中的哪个元素。

依赖收集和触发

什么是依赖?简而言之,需要监控的数据是依赖。
在vue中,每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
简而言之,就是在getter中收集依赖,在setter中触发依赖。vue通过创建Dep类和Watcher类来实现这个过程。
其中Dep类主要是来管理依赖的,vue将
收集依赖的代码
封装为Dep类来方便管理依赖。每个Observer类的实例,成员中都有一个Dep类的实例。
Watcher是一个中介,数据发生变化时通过Watcher中转,通知组件。

开始创建Dep类收集依赖。
//Dep.js
let uid = 0;
export default class Dep{
    constructor(){
        //做一个实例标记
        this.id = uid++;
        //用数组存储自己的订阅者,subs是subscribes订阅者的意思
        //这个数组里存放watcher的实例
        this.subs = [];
        //存储id,保证唯一
        this.newWatcherIds = new Set();
    }

    //通知更新
    notify(){
        //浅克隆一份
        const subs = this.subs;
        //循环遍历
        for(let i=0,l=subs.length; i<l; i++){
            subs[i].update()
        }
    }

    //添加订阅
    addSub(sub){
        const id = sub.id;
        if(!this.newWatcherIds.has(id)){
            this.newWatcherIds.add(id);
            this.subs.push(sub);
        }
    }
    // 添加依赖
    depend() {
        // Dep.target就是一个我们自己指定的全局的位置,你用window.target也行,只要是全剧唯一,没有歧义就行
        if (Dep.target) {
            this.addSub(Dep.target);
        }
    }
}

新建Watcher类

依赖就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。

//Watcher.js
import Dep from './Dep'
let uid = 0;
export default class Watcher{
    /**
     * 构造函数
     * @param target 监控的对象
     * @param name  监控的属性
     * @param callback 回调函数
     */
    constructor(target,name,callback) {
        this.id = uid++;
        //将监控对象放入target中
        this.target = target;
        //存储回调函数
        this.callback = callback;
        this.getter = parsePath(name);
        //监控项初始值放入value中
        this.value = this.get();
    }
    update(){
        this.run();
    }

    get(){
        //进入依赖收集阶段,让全局的Dep.target值为Watcher实例
        Dep.target = this;
        //获取对象
        const obj = this.target;

        let value;
        try{
            //访问对象上的元素,会触发监控的get方法,将watcher存入Dep中
            value = this.getter(obj);
        }finally{
            Dep.target = null;
        }
        return value;
    }
    //运行
    run(){
        this.getAndInvoke(this.callback);
    }
    getAndInvoke(ob){
        //获取新值
        const value = this.get();
        //如果新值不等于旧值,或新值为一个对象
        if(value !== this.value||typeof value == 'object'){
            //存储旧值
            const oldValue = this.value;
            //将新值存储在this.value中
            this.value = value;
            //执行回调函数
            ob.call(this.target,value,oldValue);
        }
    }
}

/**
 * 辅助函数,帮助拿到对象监控的值
 * @param str
 * @returns {function(*): *}
 */
function parsePath(str){
    let argument = str.split(".");
    return (obj)=>{
        for(let i = 0,l=argument.length;i<l;i++){
            obj = obj[argument[i]];
        }
        return obj;
    }
}

Watcher类中的parsePath是一个辅助函数,帮助我们具体监控项的值。举个简单的例子,我需要监控vue中data中的某个数据项:subject.Chinese 。vue中的语法是用 "."来分隔的。在获取数据的时候需要将这种语法解析出来。parsePath方法的作用就是找到"subject.Chinese"对应的数据98

data(){
    return {
        name:"张三",
        age:18,
        subject:{
            Chinese:'98',
            English:'80'
        },
        grade:[99,100,85,76,60]
    }
}
watch:{
    "subject.Chinese":function(val){...}
}

测试下

function parsePath(str){
    let argument = str.split(".");
    return (obj)=>{
        for(let i = 0,l=argument.length;i<l;i++){
            obj = obj[argument[i]];
        }
        return obj;
    }
}
let student = {
    name:"张三",
    age:18,
    subject:{
        Chinese:'98',
        English:'80'
    },
    grade:[99,100,85,76,60]
}

let ob = parsePath("subject.Chinese");
let value = ob(student);
console.log(value)//98
在defineReactive的get与set中进行依赖的收集与触发
//defineReactive.js
import {observe} from "./observe";
import Dep from "./Dep"

/**
 * 封装Object.defineProperty()
 * @param data 对象
 * @param key 添加的属性
 * @param val 属性值
 */
export function defineReactive(data,key,val){
    //val闭包中的dep
    const dep = new Dep();
    //若没有传入属性值,且对象中含有这个属性的属性值时,直接将其赋值给val
    if(arguments.length == 2){
        val = data[key];
    }
    //子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用
    let childOb = observe(val);

    Object.defineProperty(data,key,{
        //可配置,如删除
        configurable:true,
        //可枚举
        enumerable:true,
        get(){
            //数据被访问时收集依赖
            //这里需要用Dep类上的target,全局变量,不是Dep的实例dep
            //Dep类的实例dep,在这里的作用是收集监听元素的Watcher实例
            //Watcher实例存放到公共变量 Dep上
            console.log(key,"被访问了");
            if(Dep.target){
                dep.depend()
                if(childOb){
                    childOb.dep.depend();
                }
            }
            return val;
        },
        set(value){
            if(val === value){
                return;
            }
            console.log(`${key}属性被赋值了,新值为:${value}`);
            val = value;
            //当设置了新值,新值也需要调用observe
            childOb = observe(val);

            dep.notify();
        }
    })
}

在Observer类的构造函数中创造Dep类的实例。

//Observer.js
export default class Observer{

    //创建构造器
    constructor(value){
        //给每个对象上添加Dep
        this.dep = new Dep();
       ....
    }
    ...
}
执行流程分析

测试查看结果

//index.js
import Watcher from "./Watcher";
import {observe} from './observe'
let student = {
    name:"张三",
    age:18,
    subject:{
        Chinese:'98',
        English:'80'
    },
    grade:[99,100,85,76,60]
}
//设置对象元素为响应式
observe(student);
//设置依赖,监控studeng.subject.Chinese元素,当元素更改后执行回调函数
new Watcher(student,'subject.Chinese',(value,oldValue)=>{
    console.log(`更改后的新值为:${value} 原本的值为:${oldValue}`)
});
student.subject.Chinese = 100;

控制台输出:监控元素被更改后,执行了设置的回调函数。类似于vue中的watcher
image.png

执行过程分析:

  1. 通过observe方法使student对象变为响应式。在这个过程中,在每个对象的__ob__实例(Observer类)上添加一个Dep类的实例成员,在对象的每个属性中闭包引用一个Dep类的实例。总共创建了9个Dep类的实例,其中属性类Dep的实例是在defineReactive里面创建的,在get和set函数中引用。对象的Dep实例是在Observer类的构造函数创建的,放在Observer类实例成员dep上。

image.png

  1. 创建一个Watcher类的实例,传入三个参数:总对象(student),需要监控的数据项(‘subject.Chinese’),回调函数。
  2. 执行Watcher类的构造函数,将总对象放入target上,回调函数放到callback上,调用get方法读取对象上监控项的值。
//Watcher.js
constructor(target,name,callback) {
        this.id = uid++;
        //将监控对象放入target中
        this.target = target;
        //存储回调函数
        this.callback = callback;
        this.getter = parsePath(name);
        //监控项初始值放入value中
        this.value = this.get();
    }
  1. get方法执行过程,将Watcher实例放到全局变量Dep.target上(Watcher类中构造函数和方法中的this不是表示类本身,而是表示类的实例)。访问student对象上subject.Chinese元素(此时会触发student.subject.Chinese的访问监听get函数)。
//Watcher.js
get(){
        //进入依赖收集阶段,让全局的Dep.target值为Watcher实例
        Dep.target = this;
        //获取对象Student
        const obj = this.target;

        let value;
        try{
            //访问student对象上的subject.Chinese元素,
            //会触发监控的get方法,将watcher存入Dep中
            value = this.getter(obj);
        }finally{
            Dep.target = null;
        }
        return value;
    }

5 触发访问监听后,调用student.subject.Chinese的get函数。此时Dep.traget不为空,调用Chinese属性对应的Dep实例dep5上的添加依赖方法depend,将Watcher类实例添加到dep5的subs数组中,至此,依赖收集完成。

//defineReactive.js
export function defineReactive(data,key,val){
    //val闭包中的dep
    const dep = new Dep();
    //若没有传入属性值,且对象中含有这个属性的属性值时,直接将其赋值给val
    if(arguments.length == 2){
        val = data[key];
    }
    //子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用
    let childOb = observe(val);

    Object.defineProperty(data,key,{
        //可配置,如删除
        configurable:true,
        //可枚举
        enumerable:true,
        get(){
            //数据被访问时收集依赖
            //这里需要用Dep类上的target,全局变量,不是Dep的实例dep
            //Dep类的实例dep,在这里的作用是收集监听元素的Watcher实例
            //Watcher实例存放到公共变量 Dep上
            console.log(key,"被访问了");
            if(Dep.target){
                dep.depend()
                if(childOb){
                    childOb.dep.depend();
                }
            }
            return val;
        },
        ....
    })
}

6.访问完成后,将Dep.target置空。
7.修改监控项的值,触发set函数,dep5调用notify方法,发布更新信息。

//index.js
//修改监控项的值
student.subject.Chinese = 100;

//defineReactive.js
//触发set函数
set(value){
    if(val === value){
        return;
    }
    console.log(`${key}属性被赋值了,新值为:${value}`);
    val = value;
    //当设置了新值,新值也需要调用observe
    childOb = observe(val);
    dep.notify();
}

8.notify方法调用Watcher实例的update方法。

//Dep.js
//通知更新
notify(){
    //浅克隆一份
    const subs = this.subs;
    //循环遍历
    for(let i=0,l=subs.length; i<l; i++){
        subs[i].update()
    }
}

9.Watcher实例的update方法获取新值,并执行传入的回调函数callback。至此,依赖触发完成。

//Watcher.js
import Dep from './Dep'
let uid = 0;
export default class Watcher{
    /**
     * 构造函数
     * @param target 监控的对象
     * @param name  监控的属性
     * @param callback 回调函数
     */
    constructor(target,name,callback) {
        this.id = uid++;
        //将监控对象放入target中
        this.target = target;
        //存储回调函数
        this.callback = callback;
        this.getter = parsePath(name);
        //监控项初始值放入value中
        this.value = this.get();
    }
    update(){
        this.run();
    }

    get(){
        ...
    }
    run(){
        this.getAndInvoke(this.callback);
    }
    getAndInvoke(ob){
        //获取新值
        const value = this.get();
        //如果新值不等于旧值,或新值为一个对象
        if(value !== this.value||typeof value == 'object'){
            //存储旧值
            const oldValue = this.value;
            //将新值存储在this.value中
            this.value = value;
            //执行回调函数
            ob.call(this.target,value,oldValue);
        }
    }
}
...

在整个过程中,Watcher把自己设置到全局的一个指定位置(Dep.target),然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher 收集到Dep中。
依赖就是Watcher。只有Watcher触发的getter才会收集依赖,哪个Watcher触发了getter,就把哪个Watcher收集到Dep中。
Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的Watcher都通知一遍。
具体流程如图所示:
image.png
图片来源于:https://blog.csdn.net/Mikon_0703/article/details/111367773

总结

vue如何实现数据的响应式的?
通过Object.defineProperty方法实现对数据的响应式。

  1. 对于对象,它会循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依旧为对象,则递归为属性值上的每个 属性(key) 设置 getter、setter。
  • 访问数据时(obj.key)进行依赖收集,在 dep 中存储相关的 watcher。
  • 设置数据时由 dep 通知相关的 watcher 去更新
  1. 对于数组,重写数组中那 7 个可以更改自身的原型方法,然后拦截对这些方法的操作。
  • 添加新数据时进行响应式处理,然后由 dep 通知 watcher 去更新
  • 删除数据时,也要由 dep 通知 watcher 去更新

扩展

上面手写的代码放到gitHub上了,有兴趣的可以下载运行下。同时也可以对照下vue2源码,创建一个vue项目,在vue项目上的:node_modules/vue/src/core/observer 可以看到这部分响应式的源码。
image.png

参考文章:
https://juejin.cn/post/6950826293923414047
https://blog.csdn.net/Mikon_0703/article/details/111367773

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值