Vue2 响应式系统之分支切换

 更新时间:2022年4月12日 23:48  点击:1051 作者:windliang

场景

我们考虑一下下边的代码会输出什么。

import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
    text: "hello, world",
    ok: true,
};
observe(data);

const updateComponent = () => {
    console.log("收到", data.ok ? data.text : "not");
};

new Watcher(updateComponent); // updateComponent 执行一次函数,输出 hello, world

data.ok = false; // updateComponent 执行一次函数,输出 not

data.text = "hello, liang"; // updateComponent 会执行吗?

我们来一步一步理清:

observer(data)

拦截了 中 和 的 ,并且各自初始化了一个 实例,用来保存依赖它们的 对象。datatextokget、setDepWatcher

image-20220331073954801

new Watcher(updateComponent)

这一步会执行 函数,执行过程中用到的所有对象属性,会将 收集到相应对象属性中的 中。updateComponentWatcherDep

image-20220331074904131

当然这里的 其实是同一个,所以用了指向的箭头。Watcher

data.ok = false

这一步会触发 ,从而执行 中所有的 ,此时就会执行一次 。setDepWatcherupdateComponent

执行 就会重新读取 中的属性,触发 ,然后继续收集 。updateComponentdatagetWatcher

image-20220331080258402

重新执行 函数 的时候:updateComponent

const updateComponent = () => {
    console.log("收到", data.ok ? data.text : "not");
};

因为 的值变为 ,所以就不会触发 的 , 的 就不会变化了。data.okfalsedata.textgettextDep

而 会继续执行,触发 收集 ,但由于我们 中使用的是数组,此时收集到的两个 其实是同一个,这里是有问题,会导致 重复执行,一会儿我们来解决下。data.okgetWatcherDepWacherupdateComponent

data.text = "hello, liang"

执行这句的时候,会触发 的 ,所以会执行一次 。但从代码来看 函数中由于 为 , 对输出没有任何影响,这次执行其实是没有必要的。textsetupdateComponentupdateComponentdata.okfalsedata.text

之所以执行了,是因为第一次执行 读取了 从而收集了 ,第二次执行 的时候, 虽然没有读到,但之前的 也没有清除掉,所以这一次改变 的时候 依旧会执行。updateComponentdata.textWatcherupdateComponentdata.textWatcherdata.textupdateComponent

所以我们需要的就是当重新执行 的时候,如果 已经不依赖于某个 了,我们需要将当前 从该 中移除掉。updateComponentWatcherDepWatcherDep

image-20220331081754535

问题

总结下来我们需要做两件事情。

  • 去重, 中不要重复收集 。DepWatcher
  • 重置,如果该属性对 中的 已经没有影响了(换句话就是, 中的 已经不会读取到该属性了 ),就将该 从该属性的 中删除。DepWacherWatcherupdateComponentWatcherDep

去重

去重的话有两种方案:

  • Dep 中的 数组换为 。subsSet
  • 每个 对象引入 , 对象中记录所有的 的 ,下次重新收集依赖的时候,如果 的 已经存在,就不再收集该 了。DepidWatcherDepidDepidWatcher

Vue2 源码中采用的是方案 这里我们实现下:2

Dep 类的话只需要引入 即可。id

/*************改动***************************/
let uid = 0;
/****************************************/
export default class Dep {
    static target; //当前在执行的函数
    subs; // 依赖的函数
  	id; // Dep 对象标识
    constructor() {
      /**************改动**************************/
        this.id = uid++;
      /****************************************/
        this.subs = []; // 保存所有需要执行的函数
    }

    addSub(sub) {
        this.subs.push(sub);
    }
    depend() {
        if (Dep.target) {
            // 委托给 Dep.target 去调用 addSub
            Dep.target.addDep(this);
        }
    }

    notify() {
        for (let i = 0, l = this.subs.length; i < l; i++) {
            this.subs[i].update();
        }
    }
}

Dep.target = null; // 静态变量,全局唯一

在 中,我们引入 来记录所有的 。Watcherthis.depIdsid

import Dep from "./dep";
export default class Watcher {
  constructor(Fn) {
    this.getter = Fn;
    /*************改动***************************/
    this.depIds = new Set(); // 拥有 has 函数可以判断是否存在某个 id
    /****************************************/
    this.get();
  }

  /**
     * Evaluate the getter, and re-collect dependencies.
     */
  get() {
    Dep.target = this; // 保存包装了当前正在执行的函数的 Watcher
    let value;
    try {
      value = this.getter.call();
    } catch (e) {
      throw e;
    } finally {
      this.cleanupDeps();
    }
    return value;
  }

  /**
     * Add a dependency to this directive.
     */
  addDep(dep) {
    /*************改动***************************/
    const id = dep.id;
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
    /****************************************/

  }

  /**
     * Subscriber interface.
     * Will be called when a dependency changes.
     */
  update() {
    this.run();
  }

  /**
     * Scheduler job interface.
     * Will be called by the scheduler.
     */
  run() {
    this.get();
  }
}

重置

同样是两个方案:

  • 全量式移除,保存 所影响的所有 对象,当重新收集 的前,把当前 从记录中的所有 对象中移除。WatcherDepWatcherWatcherDep
  • 增量式移除,重新收集依赖时,用一个新的变量记录所有的 对象,之后再和旧的 对象列表比对,如果新的中没有,旧的中有,就将当前 从该 对象中移除。DepDepWatcherDep

Vue2 中采用的是方案 ,这里也实现下。2

首先是 类,我们需要提供一个 方法。DepremoveSub

import { remove } from "./util";
/*
export function remove(arr, item) {
    if (arr.length) {
        const index = arr.indexOf(item);
        if (index > -1) {
            return arr.splice(index, 1);
        }
    }
}
*/
let uid = 0;

export default class Dep {
    static target; //当前在执行的函数
    subs; // 依赖的函数
    id; // Dep 对象标识
    constructor() {
        this.id = uid++;
        this.subs = []; // 保存所有需要执行的函数
    }
		
    addSub(sub) {
        this.subs.push(sub);
    }
  /*************新增************************/
    removeSub(sub) {
        remove(this.subs, sub);
    }
  /****************************************/
    depend() {
        if (Dep.target) {
            // 委托给 Dep.target 去调用 addSub
            Dep.target.addDep(this);
        }
    }

    notify() {
        for (let i = 0, l = this.subs.length; i < l; i++) {
            this.subs[i].update();
        }
    }
}

Dep.target = null; // 静态变量,全局唯一

然后是 类,我们引入 来保存所有的旧 对象,引入 来保存所有的新 对象。Watcherthis.depsDepthis.newDepsDep

import Dep from "./dep";
export default class Watcher {
    constructor(Fn) {
        this.getter = Fn;
        this.depIds = new Set(); // 拥有 has 函数可以判断是否存在某个 id
      	/*************新增************************/
        this.deps = [];
        this.newDeps = []; // 记录新一次的依赖
        this.newDepIds = new Set();
      	/****************************************/
        this.get();
    }

    /**
     * Evaluate the getter, and re-collect dependencies.
     */
    get() {
        Dep.target = this; // 保存包装了当前正在执行的函数的 Watcher
        let value;
        try {
            value = this.getter.call();
        } catch (e) {
            throw e;
        } finally {
          	/*************新增************************/
            this.cleanupDeps();
          	/****************************************/
        }
        return value;
    }

    /**
     * Add a dependency to this directive.
     */
    addDep(dep) {
        const id = dep.id;
      /*************新增************************/
        // 新的依赖已经存在的话,同样不需要继续保存
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id);
            this.newDeps.push(dep);
            if (!this.depIds.has(id)) {
                dep.addSub(this);
            }
        }
      /****************************************/
    }

    /**
     * Clean up for dependency collection.
     */
  	/*************新增************************/
    cleanupDeps() {
        let i = this.deps.length;
        // 比对新旧列表,找到旧列表里有,但新列表里没有,来移除相应 Watcher
        while (i--) {
            const dep = this.deps[i];
            if (!this.newDepIds.has(dep.id)) {
                dep.removeSub(this);
            }
        }

        // 新的列表赋值给旧的,新的列表清空
        let tmp = this.depIds;
        this.depIds = this.newDepIds;
        this.newDepIds = tmp;
        this.newDepIds.clear();
        tmp = this.deps;
        this.deps = this.newDeps;
        this.newDeps = tmp;
        this.newDeps.length = 0;
    }
  	/****************************************/
    /**
     * Subscriber interface.
     * Will be called when a dependency changes.
     */
    update() {
        this.run();
    }

    /**
     * Scheduler job interface.
     * Will be called by the scheduler.
     */
    run() {
        this.get();
    }
}

测试

回到开头的代码

import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
    text: "hello, world",
    ok: true,
};
observe(data);

const updateComponent = () => {
    console.log("收到", data.ok ? data.text : "not");
};

new Watcher(updateComponent); // updateComponent 执行一次函数,输出 hello, world

data.ok = false; // updateComponent 执行一次函数,输出 not

data.text = "hello, liang"; // updateComponent 会执行吗?

此时 修改的话就不会再执行 了,因为第二次执行的时候,我们把 中 里的 清除了。data.textupdateComponentdata.textDepWatcher

总结

今天这个主要就是对响应式系统的一点优化,避免不必要的重新执行。所做的事情就是重新调用函数的时候,把已经没有关联的 去除。Watcher

不知道看到这里大家有没有一个疑问,我是一直没想到说服我的点,欢迎一起交流:

在解决去重问题上,我们是引入了 ,但如果直接用 其实就可以。在 类中是用 来存 ,用数组来存 对象,为什么不直接用 来存 对象呢?idsetWatcherSetidDepSetDep

到此这篇关于Vue2 响应式系统之分支切换的文章就介绍到这了,更多相关Vue2分支切换内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!

原文出处:https://vue.windliang.wang

[!--infotagslink--]

相关文章

  • python实现学生通讯录管理系统

    这篇文章主要为大家详细介绍了python实现学生通讯录管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-02-25
  • 解决idea check out 切换分支时找不到需要的分支问题

    这篇文章主要介绍了解决idea check out 切换分支时找不到需要的分支问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-02-09
  • 详解为什么现代系统需要一个新的编程模型

    如今高要求的分布式系统的建造者遇到了不能完全由传统的面向对象编程(OOP)模型解决的挑战,但这可以从Actor模型中获益。...2021-05-20
  • BootStrap栅格系统、表单样式与按钮样式源码解析

    这篇文章主要为大家详细解析了BootStrap栅格系统、表单样式与按钮样式源码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2017-01-23
  • 护卫神 主机管理系统使用说明(MSSQL管理)

    护卫神·主机管理系统该版本支持在Windows Server 200320082012,含32位和64位,直接开设配置WEB站、FTP站,以及SQL Server和MySQL,是您开设和管理虚拟主机的绝好帮手。但是对于新用户可能在使用上有一些困难,因此请仔细阅读如下说明文档...2016-01-27
  • 利用C#修改Windows操作系统时间

    这篇文章主要介绍了利用C#修改Windows操作系统时间,帮助大家更好的利用c#操作系统,感兴趣的朋友可以了解下...2020-12-08
  • vivo OriginOS新系统如何更新 originos系统更新方法

    vivo新系统更新的步骤是什么?如何更新到vivo的最新系统?vivo的最新系统太亮眼了,不少vivo的用户都在跃跃欲试想体验一下最新的系统。vivo新系统虽然做出来了不过我们想体验的话还是要等待一段时间。到时大家通过下面的方法就可以使用到新系统了...2020-12-08
  • C#实现影院售票系统

    这篇文章主要为大家详细介绍了C#实现影院售票系统,解析了售票系统的难点,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-06-25
  • ColorOS7.2好不好用 ColorOS7.2系统升级体验

    ColorOS7.2系统怎么样?好不好用?值不值得升级?下面小编带来ColorOS7.2系统升级体验...2020-06-29
  • C#多线程编程中的锁系统(四):自旋锁

    这篇文章主要介绍了C#多线程编程中的锁系统(四):自旋锁,本文讲解了基础知识、自旋锁示例、SpinLock等内容,需要的朋友可以参考下...2020-06-25
  • Unity实现换装系统

    这篇文章主要为大家详细介绍了Unity实现换装系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-04-11
  • php需登录的文件上传管理系统

    本文给大家介绍一个不错的需要登录的php 文件上传管理系统,功能简单有需要了解的同学可参考。 代码如下<&#63;php$admin_pw="admin";//管理密码$uploaddir="upload";//上传目录session_start();if($_GET['action']=="g...2015-10-30
  • C++学生信息管理系统

    这篇文章主要为大家想详细介绍了C++学生信息管理系统的实现代码,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-04-25
  • 源码揭秘为什么 Vue2 this 能够直接获取到 data 和 methods

    本篇文章主要介绍的是Vue2 this 能够直接获取到 data 和 methods,阅读本文将能学到如何学习调试 vue2 源码、data 中的数据为什么可以用 this 直接获取到、methods 中的方法为什么可以用 this 直接获取到,需要的朋友可以参考一下...2021-09-23
  • C语言学生成绩管理系统小设计

    这篇文章主要为大家详细介绍了C语言学生成绩管理系统小设计,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-04-25
  • C#毕业设计之Winform零压健身房管理系统

    本文介绍了个人的《零压健身房管理系统(扁平化)》的基本流程和功能点的介绍,虚心接受各位的意见,欢迎在提出宝贵的意见,大家一起探讨学习...2021-09-26
  • .NET Core如何获取操作系统中的各种信息

    .net core是最近讨论频率很高的话题,下面这篇文章主要给大家介绍了关于利用.NET Core如何获取操作系统中各种信息的相关资料,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面来一起看看吧...2021-09-22
  • 基于jQuery和CSS3制作响应式水平时间轴附源码下载

    我们经常看到有很多的垂直时间轴用来记录事件进展,而有朋友问我要求分享水平的时间轴,其实水平时间轴的难点在于自适应屏幕尺寸。那么 今天我要给大家分享的是一款支持响应式、支持触屏手势滑动的水平时间轴...2015-12-21
  • 20分钟成功编写bootstrap响应式页面 就这么简单

    这篇文章主要教大家如何在20分钟内成功编写bootstrap响应式页面,其实很简单,培养大家分分钟开发出一个高大上的页面能力,感兴趣的小伙伴们可以参考一下...2016-05-14
  • C++顺序表实现图书管理系统

    这篇文章主要为大家详细介绍了C++顺序表实现图书管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-10-13