响应系统的作用与实现-1

86 阅读7分钟

我们都知道,vue3.x 是采用 Proxy 实现响应数据的,本篇文章也将围绕 vue3.x 的响应机制开始,展开对响应式数据和副作用函数的实现

副作用函数

副作用函数就是指有副作用的函数,例如下面例子:

const obj = { text: 'hello world' }; // 全局变量 obj

function effect() {
  obj.text = 'hello vue';
}

function elseFn() {
  console.log(obj.text); // 本来是打印 hello world 的,但是被effect影响了
}

effect 函数 执行时,会设置 obj.text,但除了 effect 函数之外,其他函数也可以读取或者设置 obj.text,也就是说 effect 函数会影响其他函数的执行。这时我们就可以认为 effect 产生了副作用。

响应式数据

了解了什么叫副作用函数,那么再看看什么是响应式数据,假设某个副作用函数读取了对象的属性:

const obj = { text: 'hello world' };
function effect() {
  console.log(obj.text);
}
effect()

如上面所示,effect 执行的时候会读取 obj.text,打印 hello world,当 obj.text 发生变化的时候,我们希望 effect 重新执行:

obj.text = 'hello vue'; // 修改text,同时希望effect重新执行

这句代码修改了 obj.text 的值,我们希望当值变化的时候对应的副作用函数能够随之执行并且打印 hello vue,如果能够执行,那么就可以称 obj 为响应式数据了,但是从目前代码来看还不能做到这点,因为 obj 仅仅是一个普通对象。接下来看看如何实现响应式数据的

响应式数据基本实现

接着上文思考,如何将 obj 成为响应式数据,了解过 vue2 的人都知道,vue2 的响应式数据是用 Object.defineProperty 拦截 getset 的,那么一样的,最主要的就是要拦截到对象的读取设置vue3 采用 ES2015+Proxy 代替 Object.defineProperty 拦截,关于 Proxy 如何代理可以阅读相关文章: {% post_link 知识点/Proxy对象 Proxy对象 %}

思路是这样的:

  • 当读取 obj.text 时,将副作用函数 effect 收集到一个 '桶'
  • 当设置 obj.text 时,将副作用函数 effect'桶' 里取出执行

思路有了,那么开始写代码:

const bucket = new Set();
const obj = { text: 'hello world' };
const proxy = new Proxy(obj, {
  get(target, key) {
    bucket.add(effect);
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue;
    bucket.forEach(fn => fn());
    return true;
  }
})

function effect() {
  console.log(proxy.text);
}
effect(); // 第一次执行打印 hello world
proxy.text = 'hello vue'; // 修改后 effect第二次执行打印 hello vue

解释一下这段代码,obj 利用 Proxy 创建了一个代理对象 proxy,在 effect 第一次执行的时候会读取代理对象属性 proxy.text ,拦截函数 get 从而收集 effect 副作用函数,当执行到 proxy.text = 'hello vue' 时,拦截函数 set 将收集到的副作用函数 effect 取出并执行,并打印出修改后的 proxy.text,这样我们就完成了一个简单的响应式系统了,当然还有很多要完善的地方,下面再继续完善。

完善响应式系统

上文中之所以说还不够完善,因为它有很多缺点,比如 如果副作用函数不叫 effect 了,那么就需要修改 get() 函数里的代码。想办法优化该代码:

let activeEffect = null;
const bucket = new Set();
const obj = { text: 'hello world', name: 'foo' };
function effect(fn) {
  activeEffect = fn;
  fn();
}

const proxy = new Proxy(obj, {
  get(target, key) {
    if (activeEffect) {
      bucket.add(activeEffect);
    }  
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue;
    bucket.forEach(fn => fn());
    return true;
  }
})

effect(() => {
  console.log(proxy.text);
});

proxy.text = 'hello vue';

从上面代码看,增加了 activeEffect 全局变量来存放副作用函数,提供 effect 函数来注册副作用函数,当 effect 执行时,fn 存放到 activeEffect 中,接着执行 fn,读取 proxy.textget() 收集副作用函数到'桶'里,这样 effect 可以执行多次并且与函数名无关,就解决了硬编码问题。但是仔细思考,还是有缺陷,比如,去修改一个 proxy 不存在或副作用函数没有读取到的属性:

proxy.noExist = ''; 
// proxy.name = 'bar';

执行上面的代码可以发现:

effect(() => {
  console.log(proxy.text);
});

这段代码也执行了,明明副作用函数读取的属性并没有包括 noExistname,却可以触发匿名副作用函数,这明显不是我们想要的。可以看出匿名副作用函数并没有读取 noExist,所以理论上 proxy.noExist 即使发生变化,匿名副作用函数也不会重新执行。这就要回到我们所谓的 '桶' 的数据结构了。原因是读取的属性并没有和副作用函数联系在一起,改进一下 bucket 的数据结构,如图:

bucket 数据结构

effect-structure1.png

代码实现:

let activeEffect = null;
const bucket = new Map();
const obj = { text: 'hello world', name: 'foo' };

function effect(fn) {
  activeEffect = fn;
  fn();
}

const proxy = new Proxy(obj, {
  get(target, key) {
    if (!activeEffect) {
      return target[key];
    }
    let effectFns = bucket.get(key);
    if (!effectFns) {
      bucket.set(key, (effectFns = new Set()))
    }
    effectFns.add(activeEffect);
    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    const effectFns = bucket.get(key);
    effectFns && effectFns.forEach(fn => fn());
    return true;
  }
})

上面代码我们将 bucket 的数据结构改成了 Map,让它可以记录每个属性所对应的副作用函数,接下来看看不同属性和副作用函数执行情况:

effect(() => {
  console.log(proxy.text);
});
proxy.text = 'hello vue'; // 正常执行匿名副作用函数
proxy.name = 'bar'; // 不执行
proxy.noExist = ''; // 不执行

这样我们就完成了属性和副作用函数之间的联系了,但是紧接着还有问题,不同对象之间,属性名可能相同,以现在的数据结构很明显还有缺陷,我们再继续改进数据结构,如图:

bucket 数据结构

effect-structure2.png

上图所示,我们增加了 WeakMap 一层数据结构,target 代表着被代理的对象。这样就很明确了,代码实现如下:

let activeEffect = null;
const bucket = new WeakMap();
const obj = { text: 'hello world', name: 'foo' };
function effect(fn) {
  activeEffect = fn;
  fn();
}

const proxy = new Proxy(obj, {
  get(target, key) {
    if (!activeEffect) {
      return target[key];
    }
    let depsMap = bucket.get(target);
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()))
    }
    let effectFns = depsMap.get(key);
    if (!effectFns) {
      depsMap.set(key, (effectFns = new Set()))
    }
    effectFns.add(activeEffect);
    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    const depsMap = bucket.get(target);
    if (depsMap) {
      const effectFns = depsMap.get(key);
      effectFns && effectFns.forEach(fn => fn());
    }
    return true;
  }
})
const proxy1 = new Proxy({text: 1}, ....);
const proxy2 = new Proxy({text: 2}, ....);

effect(() => {
  console.log(proxy1.text);
});


effect(() => {
  console.log(proxy2.text);
});

proxy1.text = 'hello vue'; // 不影响 proxy2.text 的副作用函数
proxy2.text = 'hello vue'; // 不影响 proxy1.text 的副作用函数

这样,proxy1 proxy2 就互不相干了,我们就可以去代理不同对象具有相同属性的情况了。分析上面的代码,最终,我们将 bucket 的数据结构改成了 WeackMap -> Map -> Set,当 访问 proxy.text 时,会先从 bucket 的键寻找原对象(obj),不存在则手动添加,否则取出键为 obj 的值,再从取出的值找到 keytext 的副作用函数集合,将当前活跃的副作用函数 activeEffect 加入这个集合中。当这个代理对象有设置操作时,经过 set 拦截,若 key 刚好为 text ,那么就会从 bucket 找到相对应的副作用函数集合并执行。

bucket 数据结构中出现了 WeakMap,那么为什么要用 WeakMap 而不用 Map ,两者有什么区别?

先看一段代码:

const weakMap = new WeakMap();
const map = new Map();
(function() {
  const bar = {text: 1};
  const foo = {text: 1};

  weakMap.set(bar, 1);
  map.set(foo, 2);
})()

我们都知道,js引擎的垃圾回收会根据数据的引用次数来回收,引用次数不为0就不回收,上面代码自执行函数执行后,foo 它没有被回收,因为它被 map 作为 key 引用着,导致没办法回收。而 bar 则可以被回收,这是因为,WeakMap 是弱引用,顾名思义,不计垃圾回收引用次数,也就是没有引用次数,垃圾回收器可以把 bar 的内存回收掉。

那回到我们的响应式系统,将 target (需要被代理的对象) 作为 WeakMapkey 意义何在?

它的价值所在其实就体现在,这个对象何时是需要响应式的,当用户侧没有引用时,那么它就不需要了。可以想象,我们打开一个弹窗组件,这个时候组件相当于一个闭包,内部的对象正好作为 WeakMapkey ,那么当弹窗销毁之后,组件内部的对象已经不需要响应式了,正好这个对象被垃圾回收器回收。如果换成 Map 那么会有内存泄漏的情况

最后我们为了代码的简洁,将 get()set()中收集和执行副作用函数的代码封装一下,完整代码如下:

function track(target, key) {
  let depsMap = bucket.get(target);
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let effectFns = depsMap.get(key);
  if (!effectFns) {
    depsMap.set(key, (effectFns = new Set()))
  }
  effectFns.add(activeEffect);
}

function trigger(tartget, key) {
  const depsMap = bucket.get(target);
  if (depsMap) {
    const effectFns = depsMap.get(key);
    effectFns && effectFns.forEach(fn => fn());
  }
}

let activeEffect = null;
const bucket = new WeakMap();
function effect(fn) {
  activeEffect = fn;
  fn();
}
const proxy = new Proxy(obj, {
  get(target, key) {
    if (!activeEffect) {
      return target[key];
    }
    track(target, key);
    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    trigger(target, key);
    return true;
  }
})

查看原文: 响应系统的作用与实现-1