Vue3 响应式系统的作用与实现(一)

38 阅读5分钟

我正在参加「掘金·启航计划」

响应式系统是 Vuejs 的重要组成部分,我们接下来会深入的探究 Vue3 的响应式系统的实现原理,希望本篇文章对你学习 Vue3 有帮助。

实现简单的响应式系统

我们在学习响应式系统之前,我们需要了解什么是响应式,什么是副作用函数,以及如何实现响应式。

什么是响应式呢?我们看下下面的代码

// 创建一个变量age,赋值为18
let age = 18;
// 将变量通过innerText渲染到页面上
document.body.innerText = age;
// 我们更改变量
age = 30;

上面的代码,我们将变量插入到页面上之后,对变量进行了更改,但是这时候,页面上的视图并没有进行更改,我们需要重新对视图进行渲染。

// 重新将变量进行渲染到页面上
document.body.innerText = age;

那有没有什么办法当我们一修改变量,视图就能更改呢?很明显是有的,我们在平时使用 Vue 的时候,已经体会到了。那么这样的响应式是如何实现的呢?

很简单,我们只需要在每次更改变量的时候,对变量进行拦截,然后进行一些处理即可,在 Vue2 当中,我们通过Object.defineProperty() 来拦截对对象的一些操作(get和set),但是这个 API 只能对对象进行拦截,对原始值无法拦截,所以我们在书写 Vue2 的时候需要data最终返回的是一个对象。而现在 Vue3 使用的是Proxy对变量进行拦截。

那什么是副作用函数呢?副作用函数就是回对外界产生影响的函数。

let age = 18;
// 像这样直接修改外部变量的,就是副作用函数的一种
function effectFn1() {
  age = 20;
}

// 与副作用函数相对应的是纯函数,我们在Vue的Computed当中定义的通常是纯函数

function effectFn2(age) {
  return age + 1;
}

接下来我们来简单实现一下 Vue3 的响应式系统

const obj = {
  age: 18,
};

// 定义一个用于存储副作用函数集合
const bucker = new Set();

const proxyObj = new Proxy(obj, {
  get(target, key) {
    // 在get方法当中对获取的读取操作进行拦截
    bucker.add(effect);
    // 返回对应数据
    return target[key];
  },
  set(target, key, newValue) {
    // 在set方法当中对修改操作进行拦截
    target[key] = newValue;
    // 当对变量进行修改的时候,我们就将依赖于这个变量的副作用函数执行
    bucker.forEach((effect) => effect());
  },
});
function effect(params) {
  console.log(proxyObj.age); //18
}
proxyObj.age = 30; // 30

实现较为完善的响应式系统

上面我们基于Proxy实现了一个简单的响应式系统,但是这个系统的缺陷还有很多,首先,它的副作用函数名称是固定的effect,并且它无法将代理对象的每个属性和各自的副作用函数一一对应起来,我们需要设计一种存储结构,将每个代理对象的每个属性和各自的副作用函数相对应。

IWaQn.png

我们首先创建一个WeakMap的数据结构,用来存放以target为键的值,为什么我们这里使用WeakMap而不用Map呢?因为weakMap对键的引用为弱引用,当WeakMap的键被垃圾回收器回收的时候,我们无法再通过对应的targetkey 回去到对应的数据。也就是说,WeakMap不会影响垃圾回收器对于键的回收,当其对应的 key 被回收以后,key 对应的资源也就无法再访问到了。因此WeakMap通常用来存储那些只有当 key 所引用的对象存在时(没有被回收)才有价值的信息。

既然我们已经把副作用函数与依赖之间的依赖关系存储结构定义好了,接下来就响应式系统进行升级。

/**
 * 实现较为完善的响应系统
 */

// 定义一个WeakMap的数据结构
const bucker = new WeakMap();
// 定义一个存放副作用函数的地方
const activeEffect = null;

// 实现一个函数,专门接收副作用函数
function effect(fn) {
  // 将副作用函数存放到指定的地方
  activeEffect = fn;
}

const data = {
  age: 18,
  name: "Jack",
  sex: "男",
  class: "安卓1802",
};

const proxyData = new Proxy(data, {
  get(target, key) {
    // ,没有activeEffect,直接返回目标值
    if (!activeEffect) return target[key];
    // 判断当前目标的依赖列表
    let depsMap = bucker.get(target);
    if (!depsMap) {
      bucker.set(target, (depsMap = new Map()));
    }
    // 判断有没有当前键所对应的依赖集合
    let deps = depsMap.get(key);
    if (!deps) {
      depsMap.set(key, (deps = new Set()));
    }
    // 将副作用函数添加到对应的依赖集合当中
    deps.add(activeEffect);
    // 返回值
    return target[key];
  },
  set(target, key, newValue) {
    target[key] = newValue;
    // 根据当前的target从依赖关系当中取出target的依赖表 bucker -> desMap -> deps -> effects
    const depsMap = bucker.get(target);
    if (!depsMap) {
      return;
    }
    const deps = depsMap.get(key);

    deps && deps.forEach((effect) => effect());
  },
});

封装 track 和 tigger 函数

上面我们已经实现了一个比较完善的响应式系统了,我们但是在 Proxy 的 get 和 set 方法当中,存在很多的代码,我们可以考虑将其进行抽取,以便后面进行复用。

/**
 * 封装track和tigger函数
 */

// 定义一个WeakMap的数据结构
const bucker = new WeakMap();
// 定义一个存放副作用函数的地方
let activeEffect = null;

// 实现一个函数,专门接收副作用函数
function effect(fn) {
  // 将副作用函数存放到指定的地方
  activeEffect = fn;
  fn();
}

const data = {
  age: 18,
  name: "Jack",
  sex: "男",
  class: "安卓1802",
};

function track(target, key) {
  // ,没有activeEffect,直接返回目标值
  if (!activeEffect) return target[key];
  // 判断当前目标的依赖列表
  let depsMap = bucker.get(target);
  if (!depsMap) {
    bucker.set(target, (depsMap = new Map()));
  }
  // 判断有没有当前键所对应的依赖集合
  let deps = depsMap.get(key);
  if (!deps) {
    depsMap.set(key, (deps = new Set()));
  }
  // 将副作用函数添加到对应的依赖集合当中
  deps.add(activeEffect);
}

function tigger(target, key) {
  // 根据当前的target从依赖关系当中取出target的依赖表 bucker -> desMap -> deps -> effects
  const depsMap = bucker.get(target);
  if (!depsMap) {
    return;
  }
  const deps = depsMap.get(key);

  deps && deps.forEach((effect) => effect());
}

const proxyData = new Proxy(data, {
  get(target, key) {
    track(target, key);
    // 返回值
    return target[key];
  },
  set(target, key, newValue) {
    l;
    target[key] = newValue;
    tigger(target, key);
  },
});

effect(() => {
  console.log(proxyData.age);
});

proxyData.age = 30;

总结

上面的代码已经实现看一个基本的响应式系统,但是还是有很多的功能我们还没有实现,比如分支切换以及删除副作用函数等等,剩下的功能将在下一篇文章进行解答,感兴趣的同学也可以去阅读霍春阳编写的《Vue.js 设计与实现》,写的真的很好。