前言

Vue3用久了,你会不会对自己手下的代码发生一些疑问?比如说:

  • 有了reactive,那还需求 ref 干啥?
  • 同样是呼应式署理,为什么只需ref能够署理原始数据,reactive却不可?
  • 为什么运用ref的值要加上一个麻烦的.value
  • 为什么ref能够一同署理原始数据和方针数据?
  • 为什么只需修正呼应式数据,视图就主动改动了呢?Vue它怎样就知道我修正了数据呢?

诸如此类的问题,是否令你感到有些困惑?不必忧虑,这些问题其实是多是因为Vue3的新特性和语法带来的改动导致的。下面会将上述问题一一捋顺,让咱们开发的时分能够做到知其所以然。

本文旨在收拾思路,让读者了解的整体流程,帮助读者对 Vue 的作业原理树立一个整体的知道和了解。而并非谈论详细的源码。因此,本文不会涉及到实际中大段的 Vue 源码。请安心阅览(恶作剧的!)

运用差异

reactive 的运用

在Vue3中,reactive是一个用于创立呼应式方针的API。它承受一个一般的 JavaScript方针 作为参数,并回来一个呼应式的署理。只需运用简略的几行代码就能够创立一个呼应式数据,并轻松的运用它,一般版运用如下:

<script setup>
  // 引入
  import { reactive } from 'vue';
  // 创立呼应式方针
  let reactiveInfo = reactive({
    name: '饿肚子游侠'
  });
</script>
<template>
  <div>
    // 页面运用
    {{ reactiveInfo }}
  </div>
</template>

好,很棒,完全没有问题。但当咱们想往reactive里装一个原始数据类型的时分,就出问题了。Vue会给出提示:它无法取得呼应式

Vue3呼应式:refvsreactive,5分钟消除运用困惑

这个提示是什么意思呢?其实也便是:后续假如再想对reactiveInfo的值进行更新赋值的时分,从头赋值后的值现已不能再呼应式的更新到页面上了。那该怎样办呢?假定现在有个参数,一定要界说为number类型或者其它任何的原始数据类型怎样办?凡是有运用Vue3经历的小伙伴必定都知道,该ref上场了。

ref 的运用

在Vue3中,ref是一个创立呼应式的API,它能够将一般的 JavaScript数据 变为呼应式数据。ref函数承受一个初始值作为参数,并回来一个呼应式的引证方针。与reactive一样,ref运用起来也很简略。区别就在于,在script标签里对其进行获取的时分,需求加上一个.value,而在template里运用的时分则不必:

<script setup>
  // 引入
  import { ref } from 'vue';
  // 创立呼应式方针
  let refTest = ref(0);
  // 修正测验
  setTimeout(() => {
    refTest.value = 2;
  }, 1000);
</script>
<template>
  <div>
    // 页面运用
    {{ refTest }}
  </div>
</template>

差异背面

在简略了解了reactiveref的运用之后,现在,不论是想要完成方针仍是原始数据类型的呼应式转换,都能够轻松做到了。但是却不由会对它们的运用差异发生一些猎奇,为什么!到底为什么ref能够署理原始数据,reactive不可!为什么运用ref的值还非要加上.value!为什么修正了我的呼应式方针,视图也跟着变!不必忧虑,下面就来把这些问题一个一个的解决。

前置常识了解(简略了解)

在JavaScript中,Proxy是一个内置的方针,用于创立一个署理方针,能够阻拦并定制方针方针上的操作。Proxy的构造函数承受两个参数:方针方针(Target)和处理器方针(Handler)。

参数解说如下:

  1. 方针方针(Target):即被署理的方针,能够是任何JavaScript方针(包括数组、函数、一般方针等)。Proxy会在方针方针上创立一个署理,阻拦对方针方针的操作。
  2. 处理器方针(Handler):一个包含各种阻拦办法的方针。处理器方针能够界说一系列阻拦器(也称为圈套或署理办法),这些阻拦器在对署理方针进行操作时会被主动调用。处理器方针能够重写或定制这些操作,以完成自界说的行为。

处理器方针的常见阻拦器办法包括:

  • get(target, property, receiver):阻拦对署理方针特点的读取操作。
  • set(target, property, value, receiver):阻拦对署理方针特点的赋值操作。
  • has(target, property):阻拦in操作符的操作。
  • deleteProperty(target, property):阻拦delete操作符的操作。
  • apply(target, thisArg, argumentsList):阻拦对署理方针的函数调用操作。
  • construct(target, argumentsList, newTarget):阻拦对署理方针的new操作符的操作。

为什么 reactive 不能监听原始数据类型?

要了解为什么reactive无法监听原始数据类型,咱们需求提到前置常识里的Proxy。实际上,reactive底层是经过Proxy来完成数据劫持的。因此,只需了解了Proxy的特性,就能明白为什么reactive不支持原始数据类型了(代码能够跟着敲一下,便于了解也能够加深印象)。

先来试验一下运用Proxy监听方针,能够留意看代码注释:

// 界说一个方针
let testObj = {
  name: '11'
};
// 将方针传入Proxy中,并传入处理器方针来重写它的get和set办法
const proxyObj = new Proxy(testObj, {
  get: () => {
    // 获取数据的时分输出‘get’
    console.log('get');
  },
  set: () => {
    // 设置数据的时分输出‘set’
    console.log("set");
    return 'set';
  }
});
// 试验数据是否正常被监听了
proxyObj.name = '22';
proxyObj.name

假如你运行了上述代码,会发现控制台正常输出了” set “和” get “,表明咱们成功运用Proxy对数据进行了监听。接下来,假如咱们将testObj改成原始数据类型,并再次测验,你会发现控制台输出了如下报错信息:无法运用非方针作为方针或处理程序创立署理。这意味着Proxy是无法对原始数据类型进行署理的。而reactive正是基于Proxy封装而成的,所以reactive不能监听原始数据类型也就不难了解了。

Vue3呼应式:refvsreactive,5分钟消除运用困惑

那么下一个问题紧接着就来了:ref,为什么能够监听原始数据类型?

为什么 ref 能够监听原始数据类型?

在探求这个问题之前,让咱们先来了解一下ref这个API的封装思路是什么,以及它与reactive之间的异同。一同也会解答为什么ref需求运用.value

其实经过前文的试验,咱们现已观察到:Proxy是只能监听方针,而无法直接处理原始数据类型的数据。考虑到这一点,咱们是不是能够测验将原始数据放置在方针中,以便Proxy进行监听呢?接下来咱们就按照这一思路,模拟完成对原始数据类型的监听。

ref函数的封装原理如下:

  1. 首要,创立一个方针,这个方针包含一个名为value的特点,用于存储咱们传入的初始值(方针类型或者原始数据类型皆可)。
  2. 接下来,就和reactiveAPI的封装根本共同了,运用Proxy来创立一个署理方针。署理方针会阻拦对其特点的读取和赋值操作。
  3. 在署理方针的阻拦器办法中,对于读取操作,会回来方针的value特点的值。而对于赋值操作,会将新的值赋给呼应式方针的value特点。
  4. 最后,将署理过的方针回来。这样,ref函数回来的值实际上便是由署理方针包装过的方针了。
function ref(value) {
  // 将传入的初始值存在reactiveObj中
  const reactiveObj = { value };
  // 回来一个Proxy署理
  return new Proxy(reactiveObj, {
    // 读取数据
    get(target, property, receiver) {
      console.log('触发了获取操作');
      return target.value;
    },
    // 更新数据
    set(target, property, value, receiver) {
      target.value = value;
      console.log('触发了更新操作');
      return 'set';
    },
  });
}
// 测验
const count = ref(100000);
count.value // 输出'get'
count.value = 1; // 输出'set'

这样操作之后,控制台现已能够正常输出了” 触发了获取操作 “和” 触发了更新操作 “了。

ref的封装进程中,咱们将值包装在一个方针的value特点中,这个进程就解说了为什么咱们需求运用.value来访问和修正ref封装回来的值。同样的,这儿不论传入的数据是方针类型仍是原始数据类型,都会经过这个包装将其放在一个方针里,然后再运用Proxy监听包装后的方针,所以才让ref具有一同署理原始数据和方针数据的才能。

为什么只需修正呼应式数据 视图就会主动改动

当修正数据后,视图会当即更新,这是Vue的常见特性。然而,你是否曾思考过其间的原理?Vue是如何知道咱们修正了哪个数据的呢?

实际在Vue源码中,这个进程涉及到了更多的内容和考虑,还要考虑模板编译等内容。因为这些内容并非本文的要点,咱们不会过多谈论。为了更明晰地展现原理而不涉及源码细节,本文采用了下面这样一个简化的办法来代替杂乱的进程。

从上面小节中现已了解到,在Vue中,咱们运用Proxy来署理数据方针,使其成为可观察的。也便是当咱们修正署理方针的特点时,能够在Proxy中感知到这种改动并触发相关操作,然后来做一些咱们想做的事的,比如:数据改动的时分,是不是能够将对应的视图也一同改掉?

所以接下来要对上面的监听做一点点改造,具体便是在setter函数中经过id获取到对应的DOM元素,并直接替换更新后的数据,然后到达修正数据后视图改动的效果:

  <div id="app"></div>
  <button onclick="count.value  ">加一</button>
  <div id="updateTest"></div>
  <script>
    function ref(value) {
      const reactiveObj = { value };
      return new Proxy(reactiveObj, {
        // 读取数据
        get(target, property, receiver) {
          console.log('get', property);
          return target.value;
        },
        // 更新数据
        set(target, property, value, receiver) {
          target.value = value;
          console.log('set');
          document.querySelector("#updateTest").innerHTML = value;
          return 'set';
        },
      });
    }
    const count = ref(100000);
    document.querySelector("#updateTest").innerHTML = count.value;
  </script>

现在,点击加一按钮,页面就能够跟着数据改动更新啦!接下来只需求增加一套依靠搜集来盯梢count特点的改动,就能够在在数据更新时主动更新相关的视图啦。

注释写的较全,所以代码不过多解说,能够多留意注释哦!

首要,咱们需求创立一个Dep类来办理依靠的搜集和告诉。

  // 创立依靠办理类 Dep
  class Dep {
    constructor() {
      // 用于存储依靠的订阅者
      this.subscribers = new Set();
    }
    // 增加订阅者
    depend() {
      if (activeWatcher) {
        this.subscribers.add(activeWatcher);
      }
    }
    // 告诉所有订阅者进行更新
    notify() {
      this.subscribers.forEach((watcher) => watcher.update());
    }
  }

然后,咱们需求将Dep实例与count特点相关起来,再对上面的ref函数进行一下改造。

// 先将activeWatcher设为null 确保在初始化阶段没有活动的Watcher方针
let activeWatcher = null;
// 创立 ref 函数
function ref(value) {
  const reactiveObj = { value };
  // 创立依靠办理实例
  const dep = new Dep();
  return new Proxy(reactiveObj, {
    // 读取数据
    get(target, property, receiver) {
      console.log('get', property);
      dep.depend(); // 把依靠搜集一下
      return target.value;
    },
    // 更新数据
    set(target, property, value, receiver) {
      target.value = value;
      console.log('set');
      dep.notify(); // 告诉依靠更新
      return 'set';
    },
  });
}

最后,咱们在按钮的点击事情监听器中创立了一个Watcher实例,在数据改动时更新视图。

// 调用ref函数
const count = ref(100000);
// 将初始值放到页面上
document.querySelector("#updateTest").innerHTML = count.value;
// 创立 Watcher 类
class Watcher {
  constructor(updateFn) {
    this.updateFn = updateFn;
  }
  // 执行更新操作
  update() {
    this.updateFn();
  }
}
activeWatcher = new Watcher(() => {
  // 更新视图
  document.querySelector("#updateTest").innerHTML = count.value;
});

至此,咱们现已完成了依靠的搜集。下面是整个依靠搜集触发的具体流程,能够跟着再收拾一下:

  1. 当用户点击按钮时,按钮的点击事情被触发。在点击事情中,每点击一次就将count的值加一。
  2. count的值改动了,在数据改动后更新的进程中,Proxy的监听起效了,countset函数被调用,而其间包括了告诉依靠更新的逻辑,于是触发了告诉依靠更新的操作。
  3. 而在告诉依靠更新的进程中,activeWatcher又被触发,调用了它的update办法。
  4. update办法中执行了更新视图的函数,将count.value的值更新到#updateTest元素中,然后更新了视图。

总结

实际在Vue3的源码中必定不会封装的如此简略,实际上要考虑的事情还有很多,比如Vue怎样知道哪里运用了数据,更新后的数据该更新到哪里?Vue是怎样把script里界说的呼应式数据正确的放到页面上的?等诸如此类的问题。本文这儿只是提供一个整体流程和思考路径,假如有对详尽的源码感兴趣的小伙伴,能够在谈论区提出,后续会连续更新数据劫持模板编译等相关的源码解读,到时也会本文留传的疑惑解开。