最近開了一個讀者回饋表單郵箱,無論是對文章的感想或是對部落格的感想,有什麼想回饋的都可以發郵箱跟我說:i_kkkp@163.com

Vue.js 响应式系统的作用与实现

引言

响应式这一个概念应该不难理解,就是js在对某一个对象或者某一个值进行操作时,我们希望通过实现一个响应式系统达到触发某些事件,也就是对操作的相应。

响应式数据与副作用函数

副作用函数指的是会产生副作用的函数,如下面的代码所示:

function effect() {
  document.body.innerText = 'hello vue3'
}

当 effect 函数执行时,它会设置 body 的文本内容,但除了effect 函数之外的任何函数都可以读取或设置 body 的文本内容。也就是说,effect 函数的执行会直接或间接影响其他函数的执行,这时我们说 effect 函数产生了副作用。

其实副作用函数并不少见,我们前面在讨论webpack的 tree shaking 话题里面涉及到的是否进行数据流处理对 tree shaking 的效果是不可忽略的。这边不再赘述。

副作用很容易产生,例如一个函数修改了全局变量,这其实也是一个副作用,如下面的代码所示:

// 全局变量
let val = 1
function effect() {
  val = 2 // 修改全局变量,产生副作用
}

理解了什么是副作用函数,再来说说什么是响应式数据。假设在一个副作用函数中读取了某个对象的属性:

const obj = { text: 'hello world' }
function effect() {
  // effect 函数的执行会读取 obj.text
  document.body.innerText = obj.text
}

副作用函数 effect 会设置 body 元素的 innerText 属性,其值为 obj.text,当 obj.text 的值发生变化时,我们希望副作用函数 effect 会重新执行。

那这样我们的思路就变成了:通过一些手段,在读取 obj.text 值的时候可以将effect函数储存进一个bucket里面,而在设置obj.text 值的时候可以在bucket里面把effect拿出来执行。

响应式数据的基本实现

如何才能让 obj 变成响应式数据呢?通过观察我们能发现:

  • 当副作用函数 effect 执行时,会触发字段 obj.text 的读取操作;
  • 当修改 obj.text 的值时,会触发字段 obj.text 的设置操作。

如何拦截一个对象属性的读取和设置操作。

在 ES2015 之前,只能通过 Object.defineProperty 函数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,我们可以使用代理对象 Proxy 来实现,这也是 Vue.js 3 所采用的方式。

function createReactiveObject(target, proxyMap, baseHandlers) {
  // 核心就是 proxy
  // 目的是可以侦听到用户 get 或者 set 的动作
  const existingProxy = proxyMap.get(target);
  
  if (existingProxy) {
    return existingProxy;
  }

  const proxy = new Proxy(target, baseHandlers);

  // 把创建好的 proxy 给存起来,
  proxyMap.set(target, proxy);
  return proxy;
}

这样我们就基于 proxy 创建了一个用于存储副作用函数,并且我们使用了一个proxyMap,这是一个可以把各类型的proxy储存起来的容器。我们分别设置了 get 和 set 拦截函数,用于拦截读取和设置操作。

设计一个完善的响应系统

从上面的示例不难看出,一个响应系统的工作流程如下:

  • 当读取操作发生时,将副作用函数收集到“桶”中;

  • 当设置操作发生时,从“桶”中取出副作用函数并执行。

下面通过一个简单的响应式系统实现来讲解原理:

我们知道proxy对象是可以传入一个具有getter和setter的对象进行get或set操作时处理的函数,因此我们可以创建一个baseHandlers,进行getter和setter的管理。

//baseHandlers
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {

    //Reflect.get方法允许你从一个对象中取属性值。就如同属性访问器语法,但却是通过函数调用来实现
    const res = Reflect.get(target, key, receiver);

    if (!isReadonly) {
      // 在触发 get 的时候进行依赖收集
      track(target, "get", key);
    }

    return res;
  };
}

function createSetter() {
  return function set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);

    // 在触发 set 的时候进行触发依赖
    trigger(target, "set", key);

    return result;
  };
}

我们还需要在副作用函数与被操作的目标字段之间建立明确的联系。
例如当读取属性时,无论读取的是哪一个属性,其实都一样,都会把副作用函数收集到“桶”里;当设置属性时,无论设置的是哪一个属性,也都会把“桶”里的副作用函数取出并执行。副作用函数与被操作的字段之间没有明确的联系。解决方法很简单,只需要在副作用函数与被操作的字段之间建立联系即可,这就需要我们重新设计“桶”的数据结构,而不能简单地使用一个Set 类型的数据作为“桶”了。

我们通过WeakMap来实现刚才上面我们说的储存effect的bucket。不了解WeakMap类的特性的可以看下WeakMap 对象是一组键/值对的集合

WeakMap 的键是原始对象 target,WeakMap 的值是一个Map 实例,而 Map 的键是原始对象 target 的 key,Map 的值是一个由副作用函数组成的 Set。

WeakMap

//Map存放不同类型的代理
export const reactiveMap = new WeakMap();
export const readonlyMap = new WeakMap();
export const shallowReadonlyMap = new WeakMap();

定义好了上面的几种bucket,我们开始实现响应式系统最核心的部分,也就是proxy的实现:

function createReactiveObject(target, proxyMap, baseHandlers) {
  // 核心就是 proxy
  // 目的是可以侦听到用户 get 或者 set 的动作

  const existingProxy = proxyMap.get(target);
  
  if (existingProxy) {
    return existingProxy;
  }
  
  const proxy = new Proxy(target, baseHandlers);

  // 把创建好的 proxy 给存起来,
  proxyMap.set(target, proxy);
  return proxy;
}

对于前面的trick和trigger:

export function track(target, type, key) {
  if (!isTracking()) {
    return;
  }
  console.log(`触发 track -> target: ${target} type:${type} key:${key}`);
  // 1. 先基于 target 找到对应的 dep
  // 如果是第一次的话,那么就需要初始化
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    // 初始化 depsMap 的逻辑
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  let dep = depsMap.get(key);

  if (!dep) {
    dep = createDep();
    depsMap.set(key, dep);
  }

  trackEffects(dep);
}

export function trigger(target, type, key) {
  // 1. 先收集所有的 dep 放到 deps 里面,
  // 后面会统一处理
  let deps: Array<any> = [];

  const depsMap = targetMap.get(target);

  if (!depsMap) return;
  // 暂时只实现了 GET 类型
  // get 类型只需要取出来就可以
  const dep = depsMap.get(key);

  // 最后收集到 deps 内
  deps.push(dep);

  const effects: Array<any> = [];
  deps.forEach((dep) => {
    // 这里解构 dep 得到的是 dep 内部存储的 effect
    effects.push(...dep);
  });
  // 这里的目的是只有一个 dep ,这个dep 里面包含所有的 effect
  // 这里的目前应该是为了 triggerEffects 这个函数的复用
  triggerEffects(createDep(effects));
}
嵌套的 effect 与 effect 栈 Webpack-自定义 loader/plugin

評論