vue2数据响应式原理——依赖收集和发布订阅

简介: vue2数据响应式原理——依赖收集和发布订阅

在这里插入图片描述

前言

本系列查阅顺序:

  1. [vue2数据响应式原理——数据劫持(初始篇)]
  2. [vue2数据响应式原理——数据劫持(对象篇)]
  3. [vue2数据响应式原理——数据劫持(数组篇)]
  4. [vue2数据响应式原理——依赖收集和发布订阅]

前几篇我们已经研究过了数据劫持,并多次提到依赖这个词,这一篇我们就将针对依赖来进行深入探讨:什么是依赖,以及收集依赖和发布订阅。

收集依赖,发布订阅

在这里插入图片描述

依赖是谁?

需要用到数据的地方成为依赖!

前面说了在getter中收集依赖,在setter中触发依赖。

那依赖是谁呢?我们需要在访问数据时收集谁,更新数据时触发谁呢?

这个依赖其实就是我们定义的一个类:Watcher

什么是Watcher?

Watcher就是需要用到数据的地方!

Watcher是个中介角色,数据发生变化时通知它,它再通知其他地方。

关于Wather,可以先看vue2watch侦听的使用方式:

vm.$watch('a.b.c',function (newVal,oldVal) {
    //....
})

关于vm.$watch的vue2官方文档

看上面的描述可能会开始懵了,别急,我们根据代码来看,新建一个Watcher.js

Watcher.js

export default class Watcher {
    //vm:监听对象;expOrFn监听的表达式或函数,如a.m;cb:回调函数;
    constructor(vm, expOrFn, cb) {
        this.vm = vm;
        //执行this.getter(),就可以读取a.m的内容
        this.getter = parsePath(expOrFn);
        this.cb = cb;
        this.value = this.get();
    }
    get() {
        //进入依赖收集阶段
        window.target = this;
        // call(上下文,参数1,参数2,..),这里值将this.getter方法放到this.vm里执行,后面的this.vm是传入this.getter方法的参数

        //这里执行this.getter()函数的时候获取了响应式数据的值,触发了响应式数据的getter,即defineReactive中defineProperty的get
        //则defineProperty的get就可以通过window.target收集这个依赖
        let value = this.getter.call(this.vm, this.vm);
        window.target = undefined;
        return value;
    }
    update() {
        const oldValue = this.value;
        this.value = this.get();
        this.cb.call(this.vm, this.value, oldValue);
    }
}

function parsePath(str) {
    const segments = str.split(".");
    return (obj) => {
        for (let i = 0; i < segments.length; i++) {
            if (!obj) return;
            obj = obj[segments[i]];
        }
        return obj;
    };
}

看一下这个Watcher.js的使用方式:

import Watcher from "./Watcher.js";
let obj = {
    a: {
        m: 99,
    },
};
observe(obj);
new Watcher(obj, "a.m", (newVal, oldVal) => {
    console.log("新的值--》", newVal);
    console.log("老的值--》", oldVal);
});

结合上面Watcher类的用法来看Watcher.js的内容:

使用Watcher类时需要传入三个参数:

  • 一个我们想要监听的对象obj
  • 一个监听值在obj中的表达式a.m
  • 一个接收两参数的回调函数

根据用法再看Watcher.js内容就大致能够明白的差不多了,其中parsePath是我们写的一个工具函数,作用是根据传入的第二个参数表达式获取到指定的值,例如下列用法:

let fn = parsePath("a.m");
let value = fn({
    a: {
        m: 6,
    },
});
console.log(value); //打印结果6

Watcher.js最重要的部分是:

    get() {
        //进入依赖收集阶段
        window.target = this;
        // call(上下文,参数1,参数2,..),这里值将this.getter方法放到this.vm里执行,后面的this.vm是传入this.getter方法的参数

        //这里执行this.getter()函数的时候获取了响应式数据的值,触发了响应式数据的getter,即defineReactive中defineProperty的get
        //则defineProperty的get就可以通过window.target收集这个依赖
        let value = this.getter.call(this.vm, this.vm);
        window.target = undefined;
        return value;
    }

在调用get()获取数据的时候,Watcher先是将自己绑定到了 window.target上,而window.target是全局的一个唯一的地方

之后调用了 this.getter()函数,根据前面可以知道this.getter()其实就是parsePath返回的一个函数,作用就是获取对应的值

因为在使用Watcher类之前,数据肯定是响应式的(即已经被observe),所以Watcher在获取值的时候肯定会触发响应式数据的getter

所以我们就可以在响应式数据的getter里根据window.target这个全局唯一值来获取到当前正在访问数据的依赖,并对其进行收集

Watcher收集到哪呢?

我们可以把依赖收集的代码封装成一个Dep类,它专门用来管理依赖,每个Observer的实例成员中都有一个Dep的实例。

说到这你会问,为什么每个Observer的实例都要有一个Dep的实例呢?

因为每一个响应式数据都有一个Observer的实例,而每一个响应式数据都需要一个Dep实例来存放哪些地方用到了自己(即Watcher),并且当数据变化发生set后,需要通过Dep实例去遍历通知储存在Dep实例中的所有依赖,所以可以知道:每个响应式数据和ObserverDep都是是一对一的关系,而它们对Watcher是一对多的关系。

Dep.js

export default class Dep {
    constructor() {
            //存储自己的订阅者,即Watcher实例
            this.subs = [];
        }
        //添加订阅
    addSub(sub) {
            this.subs.push(sub);
        }
        //添加依赖
    depend() {
            //window.target是我们指定的全局唯一的位置,当有依赖产生时Watcher会将依赖绑定到该位置
            if (window.target) {
                this.addSub(window.target);
            }
        }
        //通知更新
    notify() {
        //浅克隆:防止循环引用
        const subs = this.subs.slice();
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
}

只看WatcherDep还不能理解收集依赖以及发布订阅的完整过程,还需要继续看:

Observer.js

Observer内创建Dep实例。

//...
import Dep from "./Dep.js"; //增加
export default class Observer {
    constructor(value) {
            //每个Observer的实例上都有一个Dep
            this.dep = new Dep(); //增加
            //.......
        }
    //.......
}

defineReactive.js

defineReactive中也需要new一个Dep实例,目的是调用Dep中的dependnotify方法:

import observe from "./observe.js";
import Dep from "./Dep.js"; //新增
export default function defineReactive(data, key, val) {
    const dep = new Dep();  //新增
    if (arguments.length == 2) {
        val = data[key];
    }
    //子元素要进行observe,至此形成了递归调用。(多个函数循环调用)
    let childOb = observe(val);
    Object.defineProperty(data, key, {
        //可枚举
        enumerable: true,
        //可以被配置,比如delete
        configurable: true,
        // 读取key时触发 getter
        get: function() {
            console.log(`你试图访问${data}的${key}属性,它的值为:${val}`);
            //如果现在处于依赖收集阶段  //新增
            dep.depend();  //新增
            if (childOb) {  //新增
                childOb.dep.depend();  //新增
            }  //新增
            return val;
        },
        //更改key时触发 setter
        set: function(newVal) {
            console.log(`你试图改变${data}的${key}属性,它的新值为:${newVal}`);
            if (val === newVal) {
                return;
            }
            val = newVal;
            childOb = observe(newVal);

            dep.notify();  //新增
        },
    });
}

到这里就能完全明白,文章开头的那句话:

在getter中收集依赖,在setter中触发依赖。

Watcher监听了数据,Watcher内部会先调用this.get(),将自己赋值到window.target,之后访问了数据

在getter中收集依赖:当访问数据时触发getter,执行了 dep.depend(),并且如果该数据的也是响应式数据,即childOb存在,也即 observe(val)有返回值(这就解释了在数据劫持那部分为什么我们要让 observe返回ob,以及为什么要将observe(val)赋值给childOb)就执行childOb.dep.depend()(因为父元素改变了,如果子元素是响应式数据,那父子元素都要收集依赖),dep.depend()会将当前正在使用数据的依赖存放到subs中

在setter中触发依赖:当数据修改时触发setter,执行了dep.notify(),继而遍历了subs里的Watcher依赖逐个对Watcher执行updateWatcher调用update进行更新数据,并调用cb返回新值和旧值

综合以上,当用 Watcher监听某一数据后,只要该数据改变,就会通知到 Watcher,之后就可以在 Watcher中写一些操作让它去通知其它地方

其实对于数组而言,上述触发依赖的方式有所变化,因为数组的修改并不会触发setter,而是会触发我们对 “ 七个方法 ” 的封装,所以对数组而言是:

在getter中收集依赖,在拦截器中触发依赖。

在数组拦截器array.js中新增ob.dep.notify()

methodName.forEach(function(method) {
    const original = arrayProto[method];
    def(
        arrayMethods,
        method,
        function(...args) {
            //...
            //触发依赖
            ob.dep.notify(); // 新增
            console.log("调用了我们改写的7方法");
            return result;
        },
        false
    );
});

之后总结一下:
在这里插入图片描述

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

Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有Watcher都通知一遍。

Watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的Watcher,并把这个Watcher收集到Dep中。

每个响应式数据都有一个dep实例,里面的subs存放了用到该数据的Watcher依赖,让数据变化时发生set,触发dep.notify,然后去通知每一个用到该数据的Watcher

相关文章
|
17天前
Vue3 子传父 暴露数据 defineExpose
Vue3 子传父 暴露数据 defineExpose
Vue3 子传父 暴露数据 defineExpose
|
1天前
|
JavaScript 前端开发 开发者
Vue的响应式原理:深入探索Vue的响应式系统与依赖追踪
【4月更文挑战第24天】Vue的响应式原理通过JavaScript getter/setter实现,当数据变化时自动更新视图。它创建Watcher对象收集依赖,并通过依赖追踪机制精确通知更新。当属性改变,setter触发更新相关Watcher,重新执行操作以反映数据最新状态。Vue的响应式系统结合依赖追踪,有效提高性能,简化复杂应用的开发,但对某些复杂数据结构需额外处理。
|
14天前
|
存储 JavaScript 搜索推荐
vue如何实现登录数据的持久化
vue如何实现登录数据的持久化
|
14天前
|
JavaScript 前端开发 开发者
浅谈Vue 3的响应式对象: ref和reactive
浅谈Vue 3的响应式对象: ref和reactive
|
17天前
Vue3 响应式数据 reactive使用
Vue3 响应式数据 reactive使用
|
17天前
|
JavaScript
Vue 定义只读数据 readonly
Vue 定义只读数据 readonly
|
17天前
|
JavaScript
Vue 响应式数据的判断
Vue 响应式数据的判断
|
17天前
|
JavaScript
Vue 将响应式数据转为普通对象
Vue 将响应式数据转为普通对象
|
17天前
|
JavaScript
vue多条数据渲染(带图片)
vue多条数据渲染(带图片)
15 1
|
JavaScript 前端开发
模拟Vue数据的双向绑定
Vue的数据双向绑定功能一直为人称道, Vue数据的双向数据绑定主要依赖了Object.defineProperty,这里尝试用最简单的代码, 实现数据的双向绑定Demo MVVM ViewModel基本实现原理 Gi...
889 0