阅读 900

超简单的vue3.0响应式原理

什么是响应式?

当数据变化时,视图也会进行更新

响应式也是我们常说的双向绑定数据->视图方向的流动.

为了加深大家对响应式理解。我做了一个小的demo。页面初始值data.name为bob。当我在控制台将data.name改为dani时,页面也更新为dani。

image.png

那么系统是如何知道我将data.name改为dani了呢?答案是---数据劫持

数据劫持

数据劫持就是当我们对数据进行读写操作时都能被某个函数拦截。

其实有两种方式能实现数据劫持: Object.definePropertyproxy.

1、Object.defineProperty

对于 data={name: 'boo', age: 18},我们可以使用Object.defineProperty(data, 'name', { get() {…}, set() {…}})对data.name进行劫持。我们把这种能被劫持的数据叫做响应式数据。

想要使data下所有属性都能被劫持,则需要循环data下所有的key并define property。这样就存在一个问题: 当我给data新增gender属性时data.gender = 'male',由于data.gender没有被define property,所以data.gender不能被劫持,也是不可相应的。

正是由于这种js的限制,defineProperty无法检测到对象新增属性,也无法检测到数组的变化!

vue对这些数据也做了优化:

对于对象,上面提到的data.gender,vue提供了$set方法主动调用defineProperty将其转化为响应式数据。

this.$set(this.someObject,‘b’,2)    // 新增响应式数据
复制代码

对于数组,vue重写了原型的push、 pop、 unshift、shift、splice、sort、reverse实现函数劫持。可能有小伙伴会说操作数组的方法不是10个吗,你这只写了7个呀? 因为只有这7个方法会修改到数组本身。也建议大家在开发过程中尽量使用这些方法对数组进行修改。

即使vue中做了一些努力,但是对于数组通过下标和length的方式进行修改,依旧是不能被劫持。

vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的
复制代码

有了缺陷我们就去优化它,优化不了就想着如何去重构了,刚好ES6提供了proxy。刚好proxy能解决上述definePropery的所有问题。所以vue3使用proxy进行数据劫持

2、Proxy
const target = { a: 1, b: 2 }
var proxy = new Proxy(target, {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    // 拦截【读取】
    return res;
  },
  set(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver);
    // 拦截【新增、修改】
    return res;
  }
})
复制代码

可以看到我们对数据进行读写操作时,会分别被proxy的get和set拦截。 image.png

那proxy劫持数据干嘛呢?

当然我们的最终目的是想要更新页面。

更新页面的前提是,我需要知道有哪些更新页面的函数依赖了响应式数据 —— 依赖收集

当数据变化时,找到对应的更新页面的函数(依赖函数),执行并更新页面 —— 触发依赖

那vue是如何进行依赖收集和触发的呢?让我们从源码中寻找答案吧!!

vue3如何实现响应式?

image.png

看下使用vue3的api如何实现上面的demo呢

const { reactive, effect } = VueReactivity;

// reactive将对象转化为响应式数据data
const data = reactive({name: 'bob'});
// effect副作用,使用effect的方式执行副作用。函数可以被收集到依赖中。
effect(() => {
  document.querySelector('#name').innerText = data.name;
});
复制代码

使用到了两个核心函数reactiveeffect

reactive: 将对象转化为响应式数据data。

effect: 副作用,表示通过effect的方式执行函数fn有一定的副作用,这个副作用就是fn可能被收集到依赖集合中。

effect入参(fn)中有使用到响应式数据data.name。所以函数会被收集到data.name对应的依赖集合中,当data.name='dani'时,这个依赖函数fn会被找到执行并更新页面

分别看下reactive和effect是如何实现的呢?

1、reactive
function reactive(target) {
  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      track(target, key);  // 收集依赖
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key); // 触发执行对应的依赖函数
      return res;
    }
  });
  return proxy;
}
复制代码

可以看到在proxy的get中调用track进行依赖收集。set中调用trigger进行依赖触发。依赖是怎么收集起来的呢?

vue3在内存定义了一个targetMaps的变量,来存储所有响应式数据对应的依赖函数。具体数据结构如下:

image.png

通过上面的数据结构,track(target, key)trigger(target, key)能很方便的通过target和key存放、找到执行对应的依赖集合。

回顾reactive:

image.png

那么track收集的依赖函数从而而来呢?

effect的副作用就是能将函数fn收集到依赖集合中。

2、effect
function effect(fn) {
  try {
    activeEffect = fn;  // 将当前依赖给activeEffect
    fn(); // 默认执行一次依赖函数
  } finally {
    activeEffect = null;
  }
}
复制代码

首先将fn赋值给全局变量activeEffect,方便在后续依赖收集中获取到fn。

然后执行了fn,这一步很关键,在fn的执行过程中,如果有使用读取到响应式数据,会被对应proxy的get劫持进行依赖收集。此时收集的依赖就是activeEffect,也就是正在被执行的函数fn。

换句话说:在fn的执行过程中如果有使用到响应式数据,fn就会被收集到改数据的依赖集合中。

image.png

上图可以看到effect执行fn,一个完整的依赖收集过程。当数据变化是,会被proxy的set劫持,触发对应的依赖函数执行。

3、流程

image.png

这就是vue3的响应式原理啦~~~~

文章分类
前端
文章标签