Vue3响应式系统原理(上)

响应式的基本概念

响应式是指当数据发生变化时,系统会自动更新与数据相关的 DOM 结构。

在 Vue2 中,响应式系统的实现基于 Object.defineProperty。然而,Object.defineProperty 有一些局限,如:无法监听数组的变化、需要遍历对象的每个属性进行监听、性能开销较大。

在 Vue3 中,响应式系统的实现基于 ES6 的 Proxy 对象。Proxy 可以直接监听对象和数组的变化,而无需对每个属性进行监听,从而大大提高性能。同时,Proxy 也可以解决 Object.defineProperty 无法监听数组的问题。

响应式的关键在于vue的依赖收集机制。

简化模型

为了更直观的理解vue依赖收集的模型,我们先来看一个"简单"的功能描述:

已知watcher函数,调用了一些"外部函数":

arduino 复制代码
function watcher () {
    console.log('watcher start')
    函数1(); // 这里调用了"外部函数"
    函数2();
    console.log('watcher end')
}

能否设计一个依赖收集系统 ,使这些"外部函数"运行时,watcher也会随之运行?

关键:如何判断函数间的调用关系?

看似有点难,实际一点也不简单,我们需要知道函数间调用关系。我们先看个例子:

lua 复制代码
function A() { console.log('A') }
function B() { console.log('B') }
function C() { console.log('C') }
...

function watcher () {
    console.log('watcher start!')
    /* *这里调用了上面的某些函数* */
    console.log('watcher end!')
}
/* *这里运行了某些函数* */
watcher();

----------- 运行结果如下 ----------

- watcher start!
- A
- B
- wathcer end! 
- C

运行结果我们可以看出watcher内部一定调用了A、B函数:

  • 为啥?js是单线程的。

  • C函数一定在watcher外面吗?不一定。例如:

    function watcher () { console.log('start') A() B() setTimeout(()=>{ C() }) console.log('end') } watcher();

  • C函数这种咋办?不管!我们只管肯定没问题的!

我们由此可以确定

函数watcher执行期间,凡是运行过的函数,一定是watcher内部调用过的函数

根据这个原理,我们设计依赖收集系统如下:

ini 复制代码
// 当前的监听函数
let activeEffect = null
// 副作用函数
function effect (watcher) {
    activeEffect = watcher
    // watcher执行的期间就是依赖收集的阶段
    watcher(true);
    activeEffect= null;
}
// isTracking:是否是依赖收集阶段
function A (isTracking = false) {
    if (isTracking) {
        // 依赖收集阶段,effects就是A的监听函数集合
        A.effects = A.effects || new Set();
        A.effects.add(activeEffect);
    } else {
        // 依赖运行阶段
        console.log('A触发了');
        A.effects.forEach(fn => fn(true));
    }
}
function B (isTracking = false) {
    /*** 与A类似 ***/
} 

测试一下效果

看起来达到了要求。

将上面代码优化一下,最终如下

php 复制代码
// 监听器的注册函数
let activeEffect = null;
function effect (watcher) {    
    activeEffect = watcher;    
    watcher(true);
    // 这里及时清理,避免非必要收集
    activeEffect = null;
}

/* *** A.effects / B.effects 改为WeakMap存储 *** */
const bucket = new WeakMap();
// 依赖收集函数
function track (target) {
    const effects = bucket.get(target) || new Set();
    activeEffect && effects.add(activeEffect);
    bucket.set(target, effects);
}

// 依赖触发函数
function trigger (target) {
    bucket.get(target)?.forEach?.(fn => fn(true));
}
function A (isTracking = false) {
    if (isTracking) {
        // 依赖收集流程,这里将
        track(A);
    } else {
        console.log('A触发了')
        // 依赖运行流程
        trigger(A);
    }
}
function B (isTracking = false) {
    /**** 与A类似 ***/
}

这里将之前 A.effects = A.effects || new Set();依赖收集流程提取成track函数,监听函数的触发流程抽离为trigger函数;这样,我们实现了一个简单的依赖收集系统。

Vue依赖收集模型

我们知道Vue3是通过Proxy实现的依赖收集流程,Proxy示例:

1. Proxy对象get监听,set触发

Vue3中,Proxy代理数据在被读取时"依赖收集",在被赋值时会"触发依赖";我们试一下上面完成的依赖收集系统,看下效果:

scss 复制代码
const data = {
    value: 1,
}
const proxyData = new Proxy(data, {
    get(target, key) {
        //收集
        track(target);
        return target[key];
    },
    set(target, key, value) {
        // 触发
        trigger(target);
        target[key] = value;
    }
})

测试一下

测试代码如下:

终端运行结果:

看起来效果不错!但是下面的例子里有问题:

一个无关的属性key的赋值也会触发监听函数!这不是我们想要的。为了精确监听,还需要细化依赖收集系统。

2. "key"级依赖

我们可以将对象的属性作为基本单位进行依赖收集。改造如下:

vbnet 复制代码
// 依赖收集函数,这里精确到keyfunction track (target, key) {    const effects = bucket.get(target) || new Map();    const keyMap = effects.get(key) || new Set();    effects.set(key, keyMap);    bucket.set(target, effects);    activeEffect && keyMap.add(activeEffect);}// 依赖触发函数,这里精确到keyfunction trigger (target, key) {    const effects = bucket.get(target);    if (!effects) return;    const keyMap = effects.get(key);    if (!keyMap) return;    keyMap.forEach(effect => effect());}
const data = {    value: 1}const proxyData = new Proxy(data, {    get(target, key) {
        // 具体到key进行收集        track(target, key);        return target[key]    },    set(target, key, value) {
        // 触发到key        trigger(target, key);        target[key] = value    }})

这里试一下效果

这样就实现了精确到属性的监听系统。看到这里,似乎完成的很不错了,但是看到下面的例子:

这里value属性由false变为true后,属性data的就已不再参与监听函数内的逻辑了 ;监听函数不应该再响应data属性,但实际上并没有。因为依赖关系已经固化,data属性只要变化就一定会触发监听,不管是否真的需要:

3. 分支切换

为了优化这一点,应将依赖关系实时更新,将多余的监听去除。为此,vue采取的策略是:

每次监听函数运行前,都要将自己的依赖关系清除;然后在运行期间重建依赖关系。

详情在下篇继续更新.....

相关推荐
惜.己4 分钟前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称27 分钟前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色44 分钟前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2341 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河1 小时前
CSS总结
前端·css
BigYe程普1 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H2 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
花花鱼2 小时前
@antv/x6 导出图片下载,或者导出图片为base64由后端去处理。
vue.js
程序员-珍2 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai2 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端