Vue2 和 3 响应式的区别

首先我们要知道,不是 Vue2 变成了 Vue3,而是 Vue2 的主要思想选项式 API 到 Vue3 版本的时候在选项式 API 的基础上新增了组合式 API,拓展了一些函数的 API,开发者仍然可以选择使用选项 API 进行开发!
函数 API 的好处是可以把组件内部的逻辑进行抽离,不再非得把所有的数据和逻辑都写在组件内部,当该组件内部逻辑复杂的时候会导致文件特别长需要上下翻阅,比较麻烦。当我们使用函数 API 把逻辑抽离到外部的 JS 文件,这样对封装集成更加的友好。
这就是为什么很多人觉得 Vue3 的组合式 API 上手比较困难,是因为我们习惯了 Vue2 的架子模版,在选项式 API 时我们只要在对应的地方编写对应的逻辑即可,组合式 API 显的更加的灵活。

Vue2 的响应式

首先要知道什么是响应式?
响应式就是数据和视图直接的联动关系。说白了,就是数据更改的时候不需要开发者手动的去操作 DOM,而是让底层的 ViewModel 进行驱动,帮我们追踪数据依赖的变化,并更新视图。

通常,我们是这样定义一个对象:

  1. const obj = {
  2. a: 1,
  3. b: 2
  4. };
  5. obj.a = 2;

如果我们想要实现,当a属性被重新赋值的时候要去 update 更新视图,显然这样是啥都干不了的!
我们希望每次对a属性更改的时候都能去 update 视图,所以 Vue2 使用了Object.defineProperty()对对象进行了封装,对对象的属性进行拦截。

  1. Object.defineProperty(data, "a", {
  2. // getter 函数
  3. get() {
  4. return data.a;
  5. },
  6. // setter 函数
  7. set(newValue) {
  8. // update() 更新视图
  9. data.a = newValue;
  10. }
  11. });

每一个对象的属性都具备定义 getter/setter 的权限。

如果我们的数据是一个多层嵌套的关系,那么我们就需要封装一个方法然后进行递归:

  1. // 定义我们的数据
  2. const data = {
  3. a: 1,
  4. b: {
  5. c: 2
  6. },
  7. d: [1, 2, 3, 4, 5]
  8. };
  9. observe(data);
  10. function observe(data) {
  11. // 遍历 data 对象
  12. for (const key in data) {
  13. defineReactive(data, key, data[key]);
  14. }
  15. }
  16. function defineReactive(data, key, value) {
  17. // 如果属性的值是一个对象,那就进行递归
  18. if ({}.toString.call(value) === "[object Object]") {
  19. observe(value);
  20. }
  21. // 对属性进行拦截操作
  22. Object.defineProperty(data, key, {
  23. get() {
  24. console.log("GET", key);
  25. return value;
  26. },
  27. set(newVal) {
  28. console.log("SET", key);
  29. // update() 更新视图
  30. value = newVal;
  31. }
  32. });
  33. }

:::tips 🔔 提示
本案例只有数据拦截的大概原理,不涉及到update()更新视图的逻辑! ::: image.png
打开控制台,你会发现给每一个属性都设置了 getter/setter 的机制。

但是,你会发现当数组调用方法的时候,是无法触发 setter 机制的。这是因为Object.defineProperty()方法主要是给对象定义属性的,数组某些方法并不能被劫持,只能通过重新给数组赋值来触发。
如下,使用push()方法时候是无法触发 setter 机制的,但是data.d的数据确实发生了变化。
image.png
如果把data.d重新赋值就可以触发 setter 机制。
image.png

Vue2 想出的解决办法就是把不能触发 setter 机制的方法全部重新实现:

  1. // 定义一个 data 对象
  2. let data = {
  3. name: 'xiechen',
  4. age: [1, 2, 3]
  5. }
  6. // 执行观察者模式
  7. observer(data)
  8. // 专门用于劫持数据的
  9. function observer(target) {
  10. if (typeof target !== 'object' || typeof target == null) {
  11. return target
  12. }
  13. // 如果是数组
  14. if (Array.isArray(target)) {
  15. // 保存数组原本的原型
  16. let oldArrayPrototype = Array.prototype
  17. // 创建一个空对象,原型指向 oldArrayPrototype
  18. let proto = Object.create(oldArrayPrototype)
  19. Array.from(['push', 'shift', 'unshift', 'pop']).forEach(method => {
  20. // 函数劫持,把函数重写
  21. proto[method] = function () {
  22. // 执行数组原本的方法
  23. oldArrayPrototype[method].call(this, ...arguments)
  24. // 更新视图
  25. updateView()
  26. }
  27. })
  28. // 给数组新增一个原型,target.__proto__ = proto
  29. Object.setPrototypeOf(target, proto)
  30. }
  31. // 如果是对象直接执行响应式
  32. for (let key in target) {
  33. defineReactive(target, key, target[key])
  34. }
  35. }
  36. // 执行响应式
  37. function defineReactive(target, key, value) {
  38. // 递归执行
  39. observer(value)
  40. // Object.defineProperty 只能劫持对象
  41. Object.defineProperty(target, key, {
  42. get() {
  43. return value
  44. },
  45. set(newVal) {
  46. if (newVal !== value) {
  47. value = newVal
  48. updateView()
  49. }
  50. }
  51. })
  52. }
  53. function updateView() {
  54. console.log('更新视图');
  55. }

这样在数组执行push()方法时候,我们可以执行一些其他的操作,最后执行的还是Array.prototype.push方法。

以上就是 Vue2 做响应式的大致流程,也就是数据劫持的过程,在劫持的过程中进行数据更新。

Vue3 的响应式

Vue3 使用了 ES6 种的 Proxy API,因为当时编写 Vue2 的时候 Proyx 的兼容性不是很好所以就放弃使用 Proxy。
Proxy 和Object.defineProperty()做的事情很类似,但是又不相同,例如:

  1. function reactive(data) {
  2. return new Proxy(data, {
  3. get(target, key) {
  4. console.log("get", key);
  5. const value = Reflect.get(target, key);
  6. // 如果属性的值是一个对象那就进行递归
  7. return typeof value === "object" ? reactive(value) : value;
  8. },
  9. set(target, key, newVal) {
  10. console.log("set", key);
  11. // update()
  12. // 为什么不用 target[key] = newVal?
  13. // 所有的程序脱离语义化都是败笔,一会 obj.xxx 一会 obj.xx=xx 太乱
  14. return Reflect.set(target, key, newVal);
  15. }
  16. });
  17. }
  18. const data = {
  19. a: 1,
  20. b: {
  21. c: 2
  22. },
  23. d: [1, 2, 3, 4, 5]
  24. };
  25. const $data = reactive(data);
  26. console.log($data);

以上就是使用 Proxy 对属性进行拦截的大致流程,如果你还不会 Proxy 和 Reflect 对象,那你真的应该抓紧了!
经过 Proxy 的处理会得到一个代理对象:
image.png

Proxy 相比Object.defineproperty()的好处:
1、不用逐个属性的定义 getter/setter 函数,默认就是对第一层全部的属性进行劫持,可以进行递归达到深层次拦截的目的。
2、Proxy 的实例对象返回的是针对原对象的一个代理对象
3、可以监听到数组方法的操作

例如对数据进行更改:

  1. const $data = reactive(data);
  2. $data.b.c = 100;
  3. $data.d.push(6);

image.png
以上就是 Vue3响应式的大致原理!