初识Javascript设计模式之发布-订阅模式到分析Vue之数据响应式原理

目录

环境搭建

在开始分析代码之前先了解一下JavaScript的设计模式之发布-订阅模式,观察者模式

什么是发布-订阅模式

代码分析大致分为三部分:让数据变成响应式、依赖收集 和 派发更新。

让数据变成响应式

什么是数据响应式?

Object.defineProperty()

中间调用层(调度中心):Observer类——递归侦测对象全部属性 

observe是干什么的?

defineReactive函数

如何观测数组?—— 数组的响应式处理

依赖收集

此处涉及到了两个重要的部分——依赖到底是什么?依赖要存放在哪里?

Dep类

为什么depend()的时候,不直接把Dep.target加入dep.subs,而是调用了Dep.target.addDep呢?

派发更新

Wacher类

总结:


一开始学习的时候还得有JS高级语法:

  • 函数上下文的了解
  • 简单了解webpack和webpack-dev-server
  • VueVue2.xVue3.x均可)

环境搭建

npm init

npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3

配置webpack.config.js文件

// 从https://www.webpackjs.com/官网照着配置
const path = require('path');

module.exports = {
    // 入口
    entry: './src/index.js',
    // 出口
    output: {
        // 虚拟打包路径,就是说文件夹不会真正生成,而是在8080端口虚拟生成
        publicPath: 'xuni',
        // 打包出来的文件名,不会真正的物理生成
        filename: 'bundle.js'
    },
    devServer: {
        // 端口号
        port: 8080,
        // 静态资源文件夹
        contentBase: 'www'
    }
};

项目结构

index.html(虚拟途径)

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <script src="/xuni/bundle.js"></script>
</body>

</html>

在开始分析代码之前先了解一下JavaScript的设计模式之发布-订阅模式,观察者模式

什么是发布-订阅模式

1. 定义

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。

订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

 

2. 例子

比如我们很喜欢看某个公众号号的文章,但是我们不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。

上面一个看似简单的操作,其实是一个典型的发布订阅模式,公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。

人的日常生活离不开各种人际交涉,比如你的朋友有很多,这时候你要结婚了,要以你为发布者,打开你的通讯录,挨个打电话通知各个订阅者你要结婚的消息。抽象一下,实现发布-订阅模式需要:

  1. 发布者(你)
  2. 缓存列表(通讯录,你的朋友们相当于订阅了你的所有消息)
  3. 发布消息的时候遍历缓存列表,依次触发里面存放的订阅者的回调函数(挨个打电话)
  4. 另外,回调函数中还可以添加很多参数,,订阅者可以接收这些参数,比如你会告诉他们婚礼时间,地点等,订阅者收到消息后可以进行各自的处理。

代码分析大致分为三部分:让数据变成响应式依赖收集派发更新

  1. 任何一个 Vue Component 都有一个与之对应的 Watcher 实例。
  2. Vue 的 data 上的属性会被添加 getter 和 setter 属性。
  3. 当 Vue Component render 函数被执行的时候, data 上会被 触碰(touch), 即被getter 方法会被调用, 此时 Vue 会去记录此 Vue component 所依赖的所有 data。(这一过程被称为依赖收集)
  4. data 被改动时(主要是用户操作), 即被setter 方法会被调用, 此时 Vue 会去通知所有依赖于此 data 的组件去调用他们的 render 函数进行更新

让数据变成响应式

什么是数据响应式?

Vue.js 一个核心思想是数据驱动。所谓数据驱动,是指视图是由数据驱动生成的,我们对视图的修改,不会直接操作 DOM,而是通过修改数据

Object.defineProperty()

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

为什么要用Object.defineProperty

可以为属性设置很多特性,例如 configurable,enumerable,但是现在不过多解释,重点只放在 get 和 set

 
export const def = function (obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
        value,
        enumerable,
        writable: true,
        configurable: true
    });
};

 


中间调用层(调度中心):Observer类——递归侦测对象全部属性 

Observer类是将每个目标对象(即data)的键值转换成getter/setter形式(这个使用闭包,封装在defineReactive),用于进行依赖收集以及调度更新。

 

import { def } from './utils.js';
import defineReactive from './defineReactive.js';
import { arrayMethods } from './array.js';
import observe from './observe.js';
import Dep from './Dep.js';

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类的实例,而且就只会实例化一次,并且需要数据是可配置的时候才会实例化Observer类实例。

import Observer from './Observer.js';
export default function (value) {
    // 如果value不是对象,什么都不做
    if (typeof value != 'object') return;
    // 定义ob
    var ob;
    if (typeof value.__ob__ !== 'undefined') {
        ob = value.__ob__;
    } else {
        ob = new Observer(value);
    }
    return ob;
}

defineReactive函数

defineReactive 是真正为数据添加 get 和 set 属性方法的方法,它将 data 中的数据定义一个响应式对象,并给该对象设置 get 和 set 属性方法,其中 get 方法是对依赖进行收集, set 方法是当数据改变时通知 Watcher 派发更新。

这里采用闭包的写法,那么闭包的妙用:上述代码里Object.defineProperty()里的get/set方法相对于var dep = new Dep()形成了闭包,从而很巧妙地保存了dep实例

那么 get 和 set 方法有什么用?

  • get 值是一个函数,当属性被访问时,会触发 get 函数

在get方法中:

  1. 先为每个data声明一个 Dep 实例对象,被用于getter时执行dep.depend()进行收集相关的依赖;
  2. 根据Dep.target来判断是否收集依赖,还是普通取值。Dep.target是在什么时候,如何收集的后面再说明,先简单了解它的作用,
  • set 值同样是一个函数,当属性被赋值时,会触发 set 函数

在set方法中:

  1. 获取新的值并且进行observe,保证数据响应式;
  2. 通过dep对象通知所有观察者去更新数据,从而达到响应式效果。
import observe from './observe.js';
import Dep from './Dep.js';

export default function defineReactive(data, key, val) {
    const dep = new Dep();
    // console.log('我是defineReactive', key);
    if (arguments.length == 2) {
        val = data[key];
    }

    // 子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用
    let childOb = observe(val);

    Object.defineProperty(data, key, {
        // 可枚举
        enumerable: true,
        // 可以被配置,比如可以被delete
        configurable: true,
        // getter
        get() {
            console.log('你试图访问' + key + '属性');
            // 如果现在处于依赖收集阶段
            if (Dep.target) {
                dep.depend();
                if (childOb) {
                    childOb.dep.depend();
                }
            }
            return val;
        },
        // setter
        set(newValue) {
            console.log('你试图改变' + key + '属性', newValue);
            if (val === newValue) {
                return;
            }
            val = newValue;
            // 当设置了新值,这个新值也要被observe
            childOb = observe(newValue);
            // 发布订阅模式,通知dep
            dep.notify();
        }
    });
};

如何观测数组?—— 数组的响应式处理

思路仍然是一样的:

  • 保留数组原来的操作
  • pushunshiftsplice这些方法,会带来新的数据元素,而新带来的数据元素,我们是有办法得知的(即为传入的参数)
  • 那么新增的元素也是需要被配置为可观测数据的,这样子后续数据的变更才能得以处理。所以要对新增的元素调用observer实例上的observeArray方法进行一遍观测处理
  • 由于数组变更了,那么就需要通知观察者Observer类,所以通过ob.dep.notify()对数组的观察者watchers进行通知
  • 针对当前的数据对象新建一个订阅器;
  • 为每个数据的 value 都添加一个__ob__属性,该属性不可枚举并指向自身;

  • 针对数组类型的数据进行单独处理(包括赋予其数组的属性和方法,以及 observeArray 进行的数组类型数据的响应式);

  • this.walk(value),遍历对象的 key 调用 defineReactive 方法;


import { def } from './utils.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);
});
 

总结起来,就是:

  • Observer类的实例挂载在__ob__属性上,提供后续观测数据使用,以及避免被重复实例化。然后,实例化Dep类实例,并且将对象/数组作为value属性保存下来
  • 如果value是个对象,就执行walk()过程,遍历对象把每一项数据都变为可观测数据(调用defineReactive方法处理)
  • 如果value是个数组,就执行observeArray()过程,递归地对数组元素调用observe(),以便能够对元素还是数组的情况进行处理

 


 

 

依赖收集

依赖收集的原理是:当视图被渲染时,会触发渲染中所使用到的数据的 get 属性方法,通过 get 方法进行依赖收集。

此处涉及到了两个重要的部分——依赖到底是什么?依赖要存放在哪里?

  • 这两部分刚好对应 Vue 中两个类,一个是 Watcher 类,而依赖就是 Watcher 类的实例;

数据对象中的 get 方法主要使用 depend 方法进行依赖收集(收集依赖实例Watcher),depend 是 Dep 类中的属性方法

  • 另一个是 Dep 类,而依赖就是存放在 Dep 实例的 subs 属性(数组类型)中进行管理的。

Dep.target 对象是一个 Watcher 类的实例,调用 Dep 类的 addSub方法

 

Dep类

  • Dep 类中有三个属性:target、uid 和 subs,分别表示当前全局唯一的静态数据依赖的监听器 Watcher、该属性的 uid 以及订阅这个属性数据的订阅者列表[a*],其中 subs 其实就是存放了所有订阅了该数据的订阅者们。另外还提供了将订阅者添加到订阅者列表的 add 方法、从订阅者列表删除订阅者的 remove方法(我没写)。
  • Dep.target 是当前全局唯一的订阅者,这是因为同一时间只允许一个订阅者被处理。
  • addDep 这个属性方法做了什么?可以看到入参是一个 Dep 类实例,这个实例实际上是当前全局唯一的订阅者,这个方法主要的逻辑就是调用当前数据依赖 dep 的类方法 addSub,而这个方法在上面 Dep 类方法中可以看到,就是将当前全局唯一的 watcher 实例放入这个数据依赖的订阅者列表中。

为什么depend()的时候,不直接把Dep.target加入dep.subs,而是调用了Dep.target.addDep呢?


      这是因为,我们不能无脑地直接把当前watcher塞入dep.subs里,我们要保证dep.subs里的每个watcher都是唯一的。

var uid = 0;
export default class Dep {
    constructor() {
        console.log('我是DEP类的构造器');
        this.id = uid++;

        // 用数组存储自己的订阅者。subs是英语subscribes订阅者的意思。
        // 这个数组里面放的是Watcher的实例
        this.subs = [];
    }
    // 添加订阅
    addSub(sub) {
        this.subs.push(sub);
    }
    // 添加依赖
    depend() {
        // Dep.target就是一个我们自己指定的全局的位置,你用window.target也行,只要是全剧唯一,没有歧义就行
        if (Dep.target) {
            this.addSub(Dep.target);
        }
    }
    // 通知更新
    notify() {
        console.log('我是notify');
        // 浅克隆一份
        const subs = this.subs.slice();
        // 遍历
        for (let i = 0, l = subs.length; i < l; i++) {
            subs[i].update();
        }
    }
};

派发更新

Wacher类

Watcher 是一个中介,数据发生变化时通过 Watcher 中转,通知组件
import Dep from "./Dep";

var uid = 0;
export default class Watcher {
    constructor(target, expression, callback) {
        console.log('我是Watcher类的构造器');
        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;
    };
}

 

总结:

其实在 Vue 中初始化渲染时,视图上绑定的数据就会实例化一个 Watcher,依赖收集就是是通过属性的 getter 函数完成的,文章一开始讲到的 ObserverWatcherDep 都与依赖收集相关。其中 ObserverDep 是一对一的关系, DepWatcher 是多对多的关系,Dep 则是 ObserverWatcher 之间的纽带。依赖收集完成后,当属性变化会执行被 Observer 对象的 dep.notify() 方法,这个方法会遍历订阅者(Watcher)列表向其发送消息, Watcher 会执行 run 方法去更新视图,我们再来看一张图总结一下:

 

 

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值