Vue源码解析-响应式原理之reactive

前言

在vue3中,通过reactive可以创建响应式数据,其实现核心是通过Proxy创建代理对象,使用或更改该代理对象会触发相应的方法如get,set,再通过get,set方法里的逻辑处理并与Reflect搭配使用实现响应式,要想明白其实现原理,需要先来了解一下Proxy和Reflect。

Proxy和Reflect介绍

代码示例:

javascript 复制代码
//const proxy = new Proxy(target, handler)
//例:
const handler = {
    // receiver指的是getter或setter的this值,也就是触发操作的对象,在这里指的是代理对象proxy_obj
    get(target,key,receiver){
        console.log('该值被读取了,读取的原对象是',target,'|读取的属性是',key,'|触发操作的对象是',receiver)
        return Reflect.get(target,key,receiver)
    },
    set(target,key,value,receiver){
        console.log('该值被设置了,设置的原对象是',target,'|设置的属性是',key,'|设置的值是',value,'|触发操作的对象是',receiver)
        const result = Reflect.set(target,key,value,receiver)
        return result
    }
}
const proxy_obj = new Proxy({a:1},handler)
proxy_obj.a ++
/*
输出:
该值被读取了,读取的原对象是 { a: 1 } |读取的属性是 a |触发操作的对象是 { a: 1 }
该值被设置了,设置的原对象是 { a: 1 } |设置的属性是 a |设置的值是 2 |触发操作的对象是 { a: 1 }
*/
  • Proxy是JavaScript中原生的构造函数,其会返回一个代理对象,我们在vue中执行const obj = reactive({name:'aaa'})时,obj就是这个代理对象 ,我们一般操作的也是obj,而不是直接操作其原对象{name: 'aaa'}。其固定传入两个参数,一个是target,也就是我们需要被代理的对象,在上面的例子中,被代理的对象就是{a:1};另一个是handler,是一个用于定义拦截代理对象的get,set操作后的执行逻辑的对象。
  • Reflect是ES6提供的一个内置对象,提供一组静态方法以标准化、函数式的方式操作对象,其与Porxy方法一一对应,所以大部分场景都是两者搭配使用。比如proxy中有get,set和deleteProperty等方法,对应的在Reflect中也有Reflect.get,Reflect.set和Reflect.deleteProperty,用来获取,设置,删除指定对象的某个属性。

以Reflect.get(target, key, receiver)为例,返回的是target对象的key属性的值,那为什么不直接使用target[key]呢,原因是Reflect.get中的receiver代表了属性访问的上下文,也就是其this指向,直接使用target[key]则其this只能是target。当然还有proxy和Reflect语义一致等其他好处,这里就不展开了。

reactive核心原理

设计模式

reactive的实现采用了发布-订阅 的设计模式,以我们生活中的例子为例,微信公众号中公众号就是一个发布者,当我们去订阅(关注)这个公众号时,我们就成了订阅者,公众号那边就会记录下是谁关注了它,这个过程称为依赖收集 ,当有文章更新时,它会把文章推送给所有订阅者,这个过程就称为派发更新。那我们将这个原理映射到代码中:

js 复制代码
const reactive_obj = reactive({
    name: 'watermelon',
    age: 18
})
function reactive(target){
    const handler = {
        get(target, key, receiver){
            // 收集依赖
            track(target, key)
            return Reflect.get(target, key, receiver)
        }
        set(target, key, value, receiver){
          const result = Reflect.set(target, key, value, receiver)
          // 派发更新
          trigger(target, key);
          return result;
        }
    }
    return new Proxy(target,handler)
}

数据存储结构

在探究track和trigger的具体实现前,先来了解一下实现响应式所使用的的数据存储结构。

  1. 首先最外层是通过一个targetMap(weakMap类型)存储所有响应式对象,其key是原对象target,也就是上面代码中的{ name: 'watermelon', age: 18 },其值是一个depsMap(Map类型)。
  2. depsMap的key是target里的属性,也就是示例中的name、age,其值是dep(set类型),用于存储某个属性的依赖列表。

targetMap使用weakMap类型是为了当其不再被引用时能够被垃圾回收,防止内存泄漏。

存储结构示意图

具体实现

副作用函数:在当前语境下,副作用函数是指依赖"依赖响应式数据"的函数,当数据发生改变时会重新执行,以实现自动更新,常见的有render(模板渲染函数),watch的回调函数等(在后面的代码实现中用effectFn模拟)。

  • 执行流程:当vue创建一个组件实例时,会先调用一个包装了render的副作用函数注册器effect,在其内部会有一个副作用函数的包裹函数wrapperedEffect,这个wrapperedEffect用于执行副作用函数以及一些逻辑处理,其会先记录下当前活跃的副作用函数(通过activeEffect),再执行副作用函数effectFn,由于用到了响应式数据,此时会触发其getter,并执行track,在track内则可将这个activeEffect进行依赖收集。随后,执行state.age++后,会触发其setter,并执行其trigger,遍历其依赖列表进行派发更新,此时会输出更新后的数据。
  • 依赖收集:按照前面的响应式的数据存储结构,将原对象target,访问的属性key,属性的依赖列表dep,按照(target,(key,dep))的结构存储在targetMap中。
  • 派发更新:派发更新会通过要修改的原对象和其属性,得到(target,(key,dep))中的dep,也就是依赖列表,然后遍历执行依赖列表里的副作用函数,从而实现自动更新的效果。
  • 在实际实现中,trigger里不是直接遍历执行的,为避免 effect 执行时又触发 trigger 导致无限循环,Vue 会用 Set 去重,将其推入异步队列并在微任务中执行。
  • activeEffect在实际实现中是栈结构,为支持嵌套 effect,避免父子effect覆盖。
js 复制代码
let activeEffect = null
const targetMap = new WeakMap();

const state = reactive({
  name: 'watermelon',
  age: 18
});
function effect(fn) {
  const wrappedEffect = () => {
    try {
      activeEffect = wrappedEffect;
      return fn();
    } finally {
      activeEffect = null;
    }
  };
  wrappedEffect();
  return wrappedEffect;
}
function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  dep.add(activeEffect);
}
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effectFn => {
      effectFn();
    });
  }
}
const effectFn = () => {
  console.log(`${state.name} is ${state.age}`);
}
effect(effectFn);

补充

上面就是reactive实现的核心原理了,当然在reactive内部会多一些处理,比如边界处理等。下面是一些reactive源码实现的补充,可以当做一些拓展。

1. reactive只能代理对象

在调用reactive创建响应式对象后,vue会进行判断,如果传入的值不是对象,如number,string等,会直接返回该值(开发环境下会在控制台输出错误提示)。

js 复制代码
  if (!isObject(target)) {
    if (__DEV__) {
      warn(
        `value cannot be made reactive'}: ${String(target)}`,
      )
    }
    return target
  }

2.对shallow和readonly类型数据的处理

概念

  • shallow:只代理第一层属性,不递归处理嵌套属性
  • readonly:只读,禁止修改对象,尝试修改会警告

在reactive中的处理

  1. 在get函数中的处理(核心):
  • 对shllow类型的处理:在普通的对象中,如果其属性还嵌套着对象,还会对其递归的响应式处理,如{a:1,b:{c:2}},在reactive内部会继续对属性b执行reactive(b),但如果是shallow类型,则会直接返回b;此外,shallow类型还会影响到ref的解包,比如const b = ref(2);const obj = {a:1,b},如果obj是个普通对象,会直接返回b.value,但如果obj是shallow类型,则不会进行解包,而是直接返回ref类型的b。
  • 对readonly类型的处理:如果是reaonly类型,在get中不会进行依赖收集,因为其是只读的,不会被修改,也就不必收集依赖了;并且,在遇到嵌套对象时,如{a:1,b:{c:2}},在内部其会返回readonly(b),而不是reactive(b)。
js 复制代码
// baseHandlers.ts
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    //  1. 依赖收集:只有非 readonly 才 track
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    const res = Reflect.get(target, key, receiver)
    //  2. ref 解包:只有非 shallow 才自动解包
    if (isRef(res)) {
      return res.value
    }
    //  3. 浅层判断:如果是 shallow,直接返回,不递归
    if (shallow) {
      return res
    }
    //  4. 深层递归:如果不是 readonly,递归 reactive;否则递归 readonly
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

3. reactive的解构

在reactive中,是不支持响应式解构的,如果你将其属性解构出来,会使其失去响应式,需要通过toRefs方法恢复响应式。

js 复制代码
const state = reactive({ count: 0, name: 'watermelon' })
// 此时你直接使用state是属性是没有响应式的
// 需要用 toRefs 把每个属性变成 ref
const { count, name } = toRefs(state)
count.value++ //  这样才会触发更新

4.其他方法:deleteProperty,has,ownKeys

在handler中的方法除了上面提到的get和set两个方法外,还有deleteProperty,has和ownKeys。

  • deleteProperty:拦截 delete obj.key,会在方法内执行删除操作和trigger
  • has:拦截 key in obj,会在方法内部执行track
  • ownKeys:拦截 Object.keys()for...in,会在方法内部执行track

5.对于集合类数据类型的处理

像上面使用get/set进行拦截的处理方法适用于Object和Array,对于集合类的数据如Map Set WeakMap WeakSet,其内部是使用get劫持其原型方法实现响应式的。那为什么不能像上面一样,直接使用get/set呢?原因在于集合类是通过方法来执行操作的,比如const map = new Map();map.set('count', 1) ,这不是对map的属性进行修改,无法触发set拦截器。所以vue的处理思路是:

  • get 拦截所有属性访问
  • 当你访问 map.set 时,我返回一个包装过的 set 方法
  • 这个包装方法在调用前后插入 tracktrigger
相关推荐
胡八一3 分钟前
使用qianjkun uniapp 主应用 集成 vue微应用
前端·vue.js·uni-app
blueblood36 分钟前
在 Ant Design Vue 2 中隐藏 a-modal 右下角自带的确定按钮
前端·vue.js
那你能帮帮我吗1 小时前
el-tree过滤后的数据,选择父节点,仅选中过滤后的子节点
vue.js·element
玖伍贰零壹肆1 小时前
vue——解决跨层级组件通信难题
前端·javascript·vue.js
BillKu3 小时前
Vue3组件加载顺序
前端·javascript·vue.js
叫我阿柒啊3 小时前
Java全栈工程师的实战面试:从基础到微服务的全面解析
java·数据库·vue.js·spring boot·微服务·前端开发·全栈开发
萌萌哒草头将军4 小时前
Rspack 1.5 版本更新速览!🚀🚀🚀
前端·javascript·vue.js
苏琢玉4 小时前
RSA+AES 混合加密不复杂,但落地挺烦,我用 Vue+PHP 封装成了两个库
vue.js·npm·php·composer
计算机学姐4 小时前
基于SpringBoot的老年人健康数据远程监控管理系统【2026最新】
java·vue.js·spring boot·后端·mysql·spring·mybatis