Vue2与Vue3响应式原理对比

Vue2.x 响应式原理

Vue2.x 响应式:

  •  实现原理
    • 对象类型:通过 Object.defineProperty() 对属性的读取、修改进行拦截( 数据劫持 )
    • 数组类型:通过重写数组方法,并作为拦截器挂载到数组对象与数组原型之间,来实现拦截。
  •  存在问题
    • 对于对象类型,直接操作对象新增属性或删除属性,界面不会更新,因为Vue无法监听到
    • 对于数组类型,直接通过下标修改数组元素,界面不会更新。直接修改数组长度,也不会生效。Vue 同样无法监听到
  • 解决办法:
    • 通过 this.$set() 或 Vue.set() 来实现对于对象类型的属性新增。通过 this.$delete 来删除对象类型中的属性。
    • 通过可以通过 this.$set() 或 Vue.set() 来实现对于数组类型的数组元素修改。也可以通过 调用拦截器中的数组方法来改变数组元素,例如:splice

Vue3.x的响应式

因为Vue3重新改写了底层响应式原理,那我们大胆猜测一下,Vue3 应该修复了 Vue2.x 版本的问题,也就是说 Vue3 能够直接监听到对于对象的新增属性或删除属性,同时还能直接通过数组下标来操作数组,或通过 length 属性直接改变数组长度,而不用依赖于 $set 方法、$delete 方法或 splice 方法。

新增或删除对象中的属性:

<template>
  <button @click="addSex">点击新增 sex 属性</button>
  <button @click="deleteName">点击删除 name 属性</button>
</template>

let person = reactive({
  name: "al",
});

function addSex() {
  person.sex = '男' // Proxy(Object){name:'al',sex:'男'}
}

function deleteName() {
  delete person.name  // Proxy(Object){sex:'男'}
}

新增或删除数组中的元素:由此可以证明,只要是能改变原数组的方法,在操作数组之后,都会被 Vue3 所监听到

let person = reactive({
  hobby:['吃饭','睡觉']
});

// 新增第三个元素
function updateHobby() {
  person.hobby[2] = '打豆豆'  // Proxy(Object) {hobby: ['吃饭', '睡觉']}
}

//删除第一个元素
function deleteArr() {
  // 通过shift 删除第一个元素
  person.hobby.shift() // Proxy(Object) {hobby: ['睡觉', '打豆豆']}
  
  // 通过splice 删除第一个元素
  person.hobby.splice(0,1) // Proxy(Object) {hobby: ['睡觉', '打豆豆']}

  // 通过pop删除最后一个元素
 person.hobby.pop() // Proxy(Object) {hobby: ['吃饭','睡觉']}

}

通过数组下标改变数组元素

let person = reactive({
  hobby:['吃饭','睡觉']
});

//通过数组下标改变数组元素
function changeHobby (){
  person.hobby[0] = '学习'  // Proxy(Object) {hobby: ['学习', '睡觉']}
}

通过length直接改变数组长度

function changeLength() {
  person.hobby.length = 1    //["吃饭"] 数组 length 变为1
}

function changeLength() {
  person.hobby.length = 4 //[ "吃饭", "睡觉", null, null ] 数组 length 变为4
}

Vue3响应式方式区分

上面说了这么多Vue3响应式的使用方式,我们发现 Vue3 针对于 Vue2 的痛点,Vue3给出了解决办法,那我们现在来看一下,Vue3 是怎么解决这些问题的。

首先,Vue3 存在两种响应式转化的方式,分别是 ref()函数 和 reactive()函数。

ref() 函数能接收基本数据类型,也能接收引用类型的数据,所以Vue3推荐使用 ref() 函数来实现响应式。

reactive() 函数只能接受引用类型数据,不能转化基本类型数据。

所以 ref() 函数和 reactive()函数进行响应式转化的底层原理其实是不一样的

ref()函数实现响应式

  1. 在调用 ref() 函数时,会先调用 createRef() 函数。
    export function ref(value) {
      return createRef(value, false);
    }
  2. 然后在 createRef() 函数中 判断当前接收的参数是否为 ref 对象,如果是,则直接返回该响应式数据,避免重复转化,如果不是,则调用 new RefImpl() 构造函数生成 RefImpl 引用对象
    // 接收两个参数,
    // 第一个参数 rawValue 是需要转化为 ref 的原始值,
    // 第二个参数 shallow 是一个布尔值,标识是否是浅层响应,如果为 true,则只处理表面层次的响应式,而不会递归处理嵌套对象
    
    function createRef(rawValue, shallow) {
      // 判断需要转化的数据是否已经是 ref 对象,如果是,则直接返回该数据,避免重复转化
      // 但是 isRef 函数并不会进行深度响应式判断,如果对一个深度响应式对象再次使用 ref 或 reactive可能会导致嵌套的代理对象,
      if (isRef(rawValue)) {
        return rawValue;
      }
    
      // 如果不是 ref 对象,则调用 RefImpl 构造函数生成新的 RefImpl 引用对象
      // 同时传递 rawValue 和 shallow 来初始化响应式数据以及确定相应深度
      return new RefImpl(rawValue, shallow);
    }
  3. RefImpl 类:是 ref 对象的实际实现。主要包括

    1. 存储原始值以及响应值:_rawValue 存储原始值,_value存储响应值

    2. 响应式处理:通过 shallow 来决定是否进行深层响应式数据处理

    3. 依赖收集与分发:在 get 函数中通过 trackRefValue 函数来收集依赖。在set函数中通过 triggerRefValue 函数通知依赖更新。

    4. RefImpl 类解析

      class RefImpl {
        private _value: any;    // 用来存储响应值
        private _rawValue: any;    // 用来存储原始值
        public dep?: Dep = undefined;    // 用来收集分发依赖
        public readonly __v_isRef = true;    //是否只读,暂不考虑
      
        // 接收 new RefImpl() 传递过来的 rawValue 和 shallow  
        constructor(value, public readonly __v_isShallow: boolean) {
          // 判断是否需要深层响应,如果不用,直接返回 Value 值,如果需要深层响应,则调用 toRaw 函数解除 value 的响应式,将其转化为原始值,以保证后续的深层响应
          this._rawValue = __v_isShallow ? value : toRaw(value);
      
          // 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应
          this._value = __v_isShallow ? value : reactive(value);
        }
      
        get value() {
          // 收集依赖
          trackRefValue(this);
      
          // 返回响应式数据
          return this._value;
        }
      
        set value(newVal) {
      
          // 将 newVal 转化为原始值,并于初始原始值比较,若不同,则准备更新数据,渲染页面,分发依赖
          if (hasChanged(toRaw(newVal), this._rawValue)) {
      
            //判断是否需要深层响应,如果不用,直接返回 newVal 值,如果需要深层响应,则调用 toRaw 函数解除 newVal 的响应式,将其转化为原始值,以保证后续的深层响应
            this._rawValue = this.__v_isShallow ? newVal : toRaw(newVal);
      
            // 判断是否需要深层响应,如果不用,则直接返回Value,不做响应式处理。如果需要深层响应,则调用 reactive 函数进行深层响应
            this._value = this.__v_isShallow ? newVal : reactive(newVal);
      
            // 分发依赖,通知更新
            triggerRefValue(this);
          }
        }
      }
    5. trackRefValue() 函数:用来收集依赖

      // 接收参数 ref ,也就是当前 refImpl 引用实例对象
      function trackRefValue(ref) {
      
        // 判断当前是否处于依赖收集状态,在 Vue2.x 中,相当于 window.target 。一般用来判断当前是否有活跃的响应式副作用正在运行
        if (isTracking()) {
      
          // ref.dep 是 RefImpl 实例对象上的一个属性,相当于 Vue2.x中的 Dep 类,用来收集或分发依赖
      
          // 判断 ref.dep 是否存在。若存在则直接使用,若不存在,则通过 createDep 函数创建一个新的依赖集合并赋值给 ref.dep ,然后使用
      
          // 将当前活跃的副作用(effect)添加到 ref.dep 中,以便在将来 ref 值变化时能够触发这些副作用。
          trackEffects(ref.dep || (ref.dep = createDep()));
        }
      }
    6. triggerRefValue()函数:用来分发依赖

      function triggerRefValue(ref) {
        // ref.dep:依赖集合。如果存在依赖集合,则继续进行触发操作。
        if (ref.dep) {
          
          // 遍历并执行 ref.dep 中的所有副作用(effect),以响应 ref 值的变化。这个函数会通知所有依赖于 ref 值的副作用重新运行。类似于 Vue2.x中的 nofiny() 
          triggerEffects(ref.dep);
        }
      }
      
    7. 如果需要深层响应转化,则需要用到 reactive() 函数,这里需要重点说明一下,

      reactive()函数的设计是单例模式,也就是说:对同一个对象多次调用 reactive() 函数,返回的都是同一个代理对象。对一个代理对象调用reactive() 函数,总会返回代理对象自身。所以如果 ref 函数接收的是一个 Proxy代理对象的话,调用 reactive 函数之后,返回的还是本身的 Proxy 代理对象,并不会重复转化一次。

      这个规则对嵌套对象也适用。依靠深层响应性,响应式对象内的嵌套对象依然是代理对象。

reactive 函数的响应式原理

上面说到了 在 ref函数中如果接收了一个对象,且需要深层响应的话,就会调用 reactive 函数来进行响应式转化,那我们现在来看看 reactive 函数转化响应式数据的原理

首先,在了解 reactive 函数的原理时,我们需要了解reactive 函数的基本概念,可以参考我的上一篇博文--reactive()函数。在这篇博文中,我大概讲了一下 reactive 函数是怎么通过 Proxy 代理对象以及 Reflect 对象来实现响应式操作的。现在,让我们完善一下 reactive函数的响应式原理吧。

import { isObject, toRawType } from '@vue/shared';
import { mutableHandlers } from './baseHandlers';
import { ReactiveFlags, reactiveMap } from './reactive';

export function reactive(target) {
  // 判断 target 是否是一个对象
  if (!isObject(target)) {
    return target;
  }

  // 如果 target 已经是一个响应式对象,直接返回它
  if (target[ReactiveFlags.IS_REACTIVE]) {
    return target;
  }

  // 如果已经存在对应的 Proxy 对象,直接返回缓存的 Proxy 对象
  const existingProxy = reactiveMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }

  // 否则创建一个新的 Proxy 对象
  const proxy = new Proxy(target, mutableHandlers);

  // 缓存创建的 Proxy 对象,避免重复创建,这就是单例模式--reactive函数对于Proxy代理对象返回的是其本身
  reactiveMap.set(target, proxy);

  return proxy;
}

到了这一步,已经完成了数据代理,通过对Proxy代理对象的操作,可以同步影响源对象。这时我们就需要进行数据监测了,而这一步其实就是在  mutableHandlers 对象之中。

  const proxy = new Proxy(target, mutableHandlers);

mutableHandlersProxy 代理的核心,它定义了各种操作的拦截器,如 getsethasdeleteProperty 等。在 Vue 3 中,getset 是最重要的两个拦截器:为了方便解释,我把 mutableHandlers 中的所有属性方法全部抽离出来了,真正源码不是这样的。

import { track, trigger } from './effect';
import { toRaw, reactive, readonly } from './reactive';
import { isObject, hasOwn, isSymbol, hasChanged } from '@vue/shared';
import { ReactiveFlags, toReactive, toReadonly } from './reactive';

const get = createGetter();
const set = createSetter();
const deleteProperty = createDeleteProperty();
const has = createHas();
const ownKeys = createOwnKeys();

export const mutableHandlers = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
};

到这里我们能了解 mutableHandlers 对象中基本都有些什么,然后就需要对每个方法进行深入解析了。

在介绍方法之前,我们了解一些概念

  • 副作用函数(Effect):使用了响应式数据的函数,例如组件的渲染函数。当数据发生变化时,这些函数需要重新执行以更新视图或计算新的值。
  • 依赖关系:在 Vue 3 中,每个响应式属性都可能有多个副作用函数依赖于它。依赖关系在 track 函数中被收集,而在 trigger 函数中被使用。
  • 全局状态(activeEffect):Vue 3 的响应式系统中有一个全局状态(activeEffect),用于保存当前正在执行的副作用函数。当我们读取响应式数据时,Vue 会检查是否存在当前副作用函数,并将其与数据的依赖关联起来。在组件渲染过程中,activeEffect 会指向当前的渲染函数,从而实现对所有使用到的响应式数据的依赖收集。

get方法:get拦截器负责处理对对象属性的读取操作。这是 Vue 响应式系统中最重要的部分之一,因为它涉及到依赖追踪。

function createGetter() {
  return function get(target, key, receiver) {
    // Reflect.get(target, key, receiver): 使用 Reflect.get 来获取源对象属性的值。
    // 这是现代 JavaScript 中获取属性值的标准方式,可以避免一些特殊情况下的错误。
    // 可以参考上一篇博文--reactive函数
    const res = Reflect.get(target, key, receiver);

    // 依赖收集 track(target, 'get', key): 调用 track 函数来进行依赖收集。
    // 这使得 Vue 能够追踪哪些组件依赖于这个属性,以便在属性变化时触发重新渲染。
    track(target, 'get', key);
    
    // 如果属性值是对象类型,则递归地将其转化为响应式对象,实现深度响应式。
    if (isObject(res)) {
      return reactive(res); // 深度响应式
    }
    
    // 如果属性值是基本类型,则直接返回
    return res;
  };
}

set方法set 拦截器负责处理对象属性的修改操作,这是触发依赖更新的关键部分。

function createSetter() {
  return function set(target, key, value, receiver) {
    // 获取上一次的属性值
    const oldValue = target[key];

    // Reflect.set(): 使用 Reflect.set 来设置对象属性的值,与get一致,返回Boolean值来判断是否设置成功
    const result = Reflect.set(target, key, value, receiver);
    
    // 检查新值与旧值是否不同,只有在值确实发生变化时才触发更新。
    if (hasChanged(value, oldValue)) {

      // 调用 trigger 函数触发响应式更新,通知所有依赖该属性的副作用函数(如渲染函数)重新运行。
      trigger(target, 'set', key, value);
    }
    
    // 返回设置的结果状态,true or false
    return result;
  };
}

deleteProperty :拦截 delete 操作,在删除对象属性时触发响应式更新。

function deleteProperty(target, key) {
    // 检查 target 是否具有 key 属性
    const hadKey = hasOwn(target, key); 

    // 删除 target 的 key 属性,与get 和 set 方法类似
    const result = Reflect.deleteProperty(target, key); 

    // 如果删除成功,并且 key 属性确实存在
    if (result && hadKey) { 

      // 触发响应式系统的更新通知
      trigger(target, 'delete', key); 
    }

    // 返回删除操作的结果(true 或 false)
    return result; 
},

has:用于检查一个对象是否拥有某个属性,相当于in 操作符 。

has(target, key) {
    // 判断 target 对象中是否存在属性key,返回 Boolean 值
    const result = Reflect.has(target, key);

    // 收集依赖
    track(target, 'has', key);

    // 返回结果
    return result;
  },

ownKeys Reflect.ownKeys 方法返回一个由目标对象自身的属性键组成的数组。它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))

  function ownKeys(target) {
    // 依赖收集,标记为数组类型
    track(target, 'iterate', 'array');

    // 返回所有键--是一个数组
    return Reflect.ownKeys(target);
  }

上面是操作对象的方法,其中 track 则是收集依赖的方法,trigger则是分发依赖通知更新的方法,下面简单介绍一下这两个方法是如何工作的。

track:收集依赖,track 函数 会将 targetkey 和当前的副作用函数关联起来,这样在 key 对应的属性值发生变化时,Vue 就能找到所有依赖这个属性的副作用函数,并触发它们重新执行。

function track(target, type, key) {
  // 首先检查当前是否有正在执行的副作用函数。如果没有,则不需要进行依赖收集。
  // 副作用函数一般在 Vue 的响应式系统运行时注册,比如在组件的渲染过程中,Vue 会把当前的渲染函数注册为全局的副作用函数。
  if (!isTracking()) return;

  // targetMap 是一个全局的 WeakMap,用于存储所有的响应式对象及其依赖关系。
  // 从 targetMap 中获取当前 target 对象的依赖图(即 depsMap),如果不存在,则创建一个新的 Map。
  //depsMap 是一个 Map,用于存储 target 对象中每个属性(即 key)的依赖集合(dep)
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }

  // dep 是一个 Set,用于存储依赖于某个特定属性 key 的所有副作用函数。
  // 从 depsMap 中获取 key 的依赖集合 dep,如果不存在,则创建一个新的 Set。
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = createDep()));
  }

  // 将当前的副作用函数添加到 dep 中
  // 这一步是将当前正在执行的副作用函数(如渲染函数)添加到依赖集合中,以便在这个 key 发生变化时,能够重新执行这些副作用函数。
  trackEffects(dep);
}

trigger:当响应式对象的属性值发生变化时,Vue 会通过调用 trigger 函数来通知依赖该属性的所有副作用函数(如组件渲染函数、计算属性等)重新执行,从而实现视图的自动更新。

function trigger(target, type, key, newValue, oldValue) {
  // targetMap 是全局存储所有响应式对象及其依赖关系的 WeakMap。targetMap 的键是响应式对象 target,值是一个 Map,这个 Map 存储了该对象每个属性 key 的依赖集合。
  // depsMap 是当前 target 对象的依赖图,里面存储了 key 与其依赖的副作用函数的映射关系。
  // 如果 depsMap 不存在,说明这个 target 对象没有被任何副作用函数依赖,直接返回。
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }

  // effects 是一个 Set,用于去重并存储所有需要触发的副作用函数。这样可以避免重复触发相同的副作用函数。
  const effects = new Set();

    // 如果是 SET 操作,收集相关副作用函数, 从 depsMap 中获取与 key 相关的副作用函数集合,并将其添加到 effects 中。
  if (key !== undefined) {
    addEffects(depsMap.get(key));
  }

  // addEffects 函数检查是否存在与当前 key 相关的副作用函数,如果存在,它们将被添加到 effects 集合中。
  const addEffects = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => effects.add(effect));
    }
  };


  // 如果操作类型是 'add'、'delete' 或 'set',而且对象是数组或其他特殊数据结构,还需要处理特殊的依赖关系。例如,操作数组的 length 属性时,需要触发依赖于 length 的副作用函数。
  if (type === 'add' || type === 'delete' || type === 'set') {
    addEffects(depsMap.get(Array.isArray(target) ? 'length' : ''));
  }

  // 遍历 effects 集合,并执行其中的每个副作用函数
  // 如果副作用函数有一个 scheduler 调度器(通常是用来调度执行顺序的),则调用调度器,否则直接执行副作用函数。
  effects.forEach(effect => {
    if (effect.options.scheduler) {
      effect.options.scheduler(effect);
    } else {
      effect();
    }
  });
}

trackEffects 收集依赖

通过源码我们可以知道,不论是ref还是reactive,在收集依赖时,最终都使用了 trackEffects 函数,其作用是将当前正在执行的副作用函数(例如渲染函数、计算属性等)添加到某个响应式数据的依赖集合中。这是依赖收集的一部分,目的是在数据变化时触发相关的副作用函数进行更新。

function trackEffects(dep) {
  // 获取当前正在执行的副作用函数
  let shouldTrack = shouldTrackEffect(dep);

  if (shouldTrack) {
    // 将当前的副作用函数 activeEffect 添加到 dep 中。这一步建立了属性与副作用函数之间的单向关联,即这个属性知道有哪些副作用函数依赖于它
    dep.add(activeEffect);

    // 同时将 dep 添加到 activeEffect 的 deps 数组中。这一步建立了副作用函数与属性之间的双向关联,即这个副作用函数知道它依赖于哪些属性。
    activeEffect.deps.push(dep);
  }
}


// 检查当前的副作用函数是否已经被添加到 dep 中,以避免重复添加。
function shouldTrackEffect(dep) {
  // 如果 dep 中已经存在 activeEffect,则返回 false,表示不需要重复添加;否则返回 true。
  return !dep.has(activeEffect);
}

 在 if 判断中,Vue3实现了 双向依赖管理,这样做的好处是

  • 避免重复添加:由于 dep 是一个 Set,它会自动去重,确保每个副作用函数只会被添加一次。
  • 双向清理:在副作用函数停止依赖某个属性时,可以通过清理 activeEffect.deps 中的 dep,从而移除双向关联,避免内存泄漏。

同样的,在这里我们也需要理解一些基本概念,那就是:dep 、 effect、activeEffect、activeEffect.deps

  • dep:是一个 Set 集合,用来存储依赖某个响应式属性的所有副作用函数。
  • effect:是一个包装了副作用逻辑的函数(例如渲染函数、计算属性等)。当 effect 函数被执行时,它会触发响应式数据的读取,从而进行依赖收集。
  • activeEffect:是一个全局变量,用于指向当前正在运行的副作用函数。每当副作用函数被执行时,activeEffect 就会被设置为当前的副作用函数,从而在依赖收集时可以将这个函数与相应的响应式属性关联起来。
  • activeEffect.deps:activeEffect.deps 是一个数组,用来存储当前副作用函数依赖的所有 dep 集合。也就是说,activeEffect.deps 中的每一个元素都是一个 dep,而每个 dep 都包含了当前副作用函数所依赖的响应式属性。

举一个栗子就是:假设我们有一个响应式对象和一个依赖于这个对象的副作用函数:

const state = reactive({ count: 0 });

const effectFn = effect(() => {
  console.log(state.count);
});

在执行 effectFn 的过程中,Vue 会执行以下操作:

  1. 依赖收集effectFn 会读取 state.count,因此 state.count 对应的 dep 集合会被收集,并且 effectFn 会被添加到这个 dep 集合中。

  2. 记录依赖:同时,Vue 还会将这个 dep 集合添加到 activeEffect.deps 中,以便将来进行依赖关系的清理。

  3. 触发更新:当 state.count 的值发生变化时,Vue 会遍历 dep 集合中的所有副作用函数,并触发它们重新执行。

  4. 清理旧的依赖:如果在下一次执行 effectFn 之前,某些依赖关系已经不再存在(例如副作用函数不再依赖某个属性),Vue 会通过遍历 activeEffect.deps 来清理这些不再需要的依赖。

总结

Vue2的响应式

  1. 原理:通过Object.definePropoty() 实现对数据每个属性的劫持,通过get和set实现了响应式
  2. 问题:对于数组数据无法通过数组下标直接操作,无法通过 length直接设置数组长度,无法             直接给对象或数组添加属性
  3. 解决:通过 vm.$set() 或 this.set() 来对对象或数组进行操作

Vue3响应式

  1. 原理:Ref响应式还是通过Object.definePropoty()对数据劫持,通过get和set实现响应式。reactive则是通过 Proxy对数据进行代理劫持,实现响应式。然后通过Reflect实现对源数据的操作
  2. 优势:Proxy是实现对象的监听,而不是对某个属性的监听。而且是惰性的,嵌套对象只有在被访问时才会被转化为响应式。这种方式避免了不必要的性能开销,尤其是在处理大型数据结构时。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值