vlambda博客
学习文章列表

vue3响应式原理源码简单剖析

vue3响应式原理源码简单剖析

讲Vue3的响应式原理之前,我们先回顾一下vue2的数据响应式原理

Vue2的响应式原理:

  1. 对象的响应式原理

    Vue内部定义了一个defineReactive函数,函数内容是通过Object的defineProperty函数对对象的属性遍历进行拦截

    function walk (value) {
       for (let key of Object.keys(value)) {
         defineReactive(value, key)
      }
    }
    /* 定义响应式操作 */
     function defineReactive (obj, key, val) {
       /* 闭包
        get和set函数想访问外部变量val
        get和set函数引用了defineReactive函数中的val变量
        */
       /* 属性拦截 */
       /* 后面想访问obj对象里面的属性的时候,通过get函数返回对应的数据
        后面想设置obj对象里面的属性的时候,通过set函数进行设置值
        此时就可以理解为obj中的key就被拦截了,从而在get和set函数中
        做我们想做的事情
      */
       Object.defineProperty(obj, key, {
         get () {
           console.log('get:', key);
           return val
        },
         set (v) {
           console.log('set:', key);
           if (v !== val) {
             val = v
          }
        }
      })
    }
  2. 数组的响应式原理:

    重写了数组的7个方法。大致流程是:


    简单参考一下(具体代码还是查看原文进行查看)

    import { def } from './utils.js'

    /* 获取数组中的原型 */
    const arrayProperty = Array.prototype;
    /* 以Array.prototype为原型创建arrayMethods对象 */
    export const arrayMethods = Object.create(arrayProperty)

    const methodsNeedChange = [
     'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
    ];

    /* 其实这里做的是,给arrayMethods对象新增这7个方法,用户访问数组中的这7个方法的时候,会优先使用我们定义的这7个方法
    而不会使用数组的中那7个方法。
    原型链---->
    其实就是重写了数组中的那7个方法
    */
    methodsNeedChange.forEach(methodName => {
     // 备份原来的方法
     const original = arrayProperty[methodName];
     // 定义新的方法
     /* 给对象定义一个不可枚举的函数 */
     def(arrayMethods, methodName, function () {
       const args = [...arguments];
       /* 恢复原来的功能(原有数组的中的方法功能不变) */
       const result = original.apply(this, args);
       /* 把这个数组身上的__ob__取出来 */
       const ob = this.__ob__;
       /* 三个特殊的方法,特殊处理的目的是,这些数据也需要是可侦测的 */
       let inserted = [];
       switch (methodName) {
         case 'push':
         case 'unshift':
           inserted = args;
           break;
         case 'splice':
           inserted = [...args].slice(2);
           break;
      }

       if (inserted) {
         /* 需要是插入的数据也是响应式的 */
         ob.observeArray(inserted);
         /* 此时为什么只需要取出当前调用对象的ob实例,而不是新的Observes对象的实例?
          因为当前对象已经是可侦测的了
        */
      }
       /* 在这里可以干其他操作 */

       return result;
    }, false)
    })
    console.log(arrayMethods);


    • 获取数组中的原型

    • 以Array.propertype(数组的原型)为原型创建一个对象

    • 对数组的7个方法进行遍历,先执行数组中原方法的功能,而后再去做一些想做的操作(更新页面)

回顾完Vue2的数据响应式,我们看看Vue3的数据响应式原理

之前我在学Vue3 的响应式原理的时候,给我的第一印象就是,Vue3的响应式原理就是Proxy,不使用Object.defineProperty方法。然而并非如此


Vue3定义响应式对象的时候,我们通常会使用到 ref 和reactive 这两个函数

那我们就看看ref是怎么定义一个数据为响应式的

// 定义了一个ref函数
function ref(value) {
   return createRef(value, false);
}

// 创建ref
function createRef(rawValue, shallow) {
 //判断数据是不是已经是一个响应式对象,是就直接返回
   if (isRef(rawValue)) {
       return rawValue;
  }
//不是的话,就返回一个RefImpl实例对象
   return new RefImpl(rawValue, shallow);
}

// RefImpl类
class RefImpl {
   constructor(value, __v_isShallow) {
       this.__v_isShallow = __v_isShallow;
       this.dep = undefined;
       this.__v_isRef = true;
       this._rawValue = __v_isShallow ? value : toRaw(value);
    // 这里是我们需要关注的
       this._value = __v_isShallow ? value : toReactive(value);
  }
   get value() {
       trackRefValue(this);
       return this._value;
  }
   set value(newVal) {
       newVal = this.__v_isShallow ? newVal : toRaw(newVal);
       if (hasChanged(newVal, this._rawValue)) {
           this._rawValue = newVal;
           this._value = this.__v_isShallow ? newVal : toReactive(newVal);
           triggerRefValue(this, newVal);
      }
  }
}

// 判断value值是是否是对象,如果是对象,需要调reactive的方法(这个后面看),否则就返回自己
const toReactive = (value) => isObject(value) ? reactive(value) : value;


从上往下看代码,在这个ref的对数据初始化进行响应式操作的流程中,我们并没有看到Proxy吧。我们看到的是创建了一个RefImpl类的实例对象,RefImpl类中对value属性进行了get和set操作,即对对象的value的属性进行读取或者修改的时候,进行拦截该属性的行为。由此可见,ref对数据的响应式操作是本质上还是对象的属性拦截,和Object.defineProperty方法是一样的


在RefImpl类的构造器中 判断了value值是否是对象,之前分析的值不是对象,是基本类型,那么就要看一下对象的情况,啊哈,我们看到了调用的是reactive函数,那么正好可以reactive也一起分析了。

//定义一个reactive的函数
function reactive(target) {
   // if trying to observe a readonly proxy, return the readonly version.
   if (isReadonly(target)) {
       return target;
  }
   return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

// 创建Reactive对象
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 传入的值必须是一个引用数据类型,不能是基本数据类型
   if (!isObject(target)) {
       if ((process.env.NODE_ENV !== 'production')) {
           console.warn(`value cannot be made reactive: ${String(target)}`);
      }
       return target;
  }
// 不需要代理两次
   // target is already a Proxy, return it.
   // exception: calling readonly() on a reactive object
   if (target["__v_raw" /* RAW */] &&
       !(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
       return target;
  }
   // target already has corresponding Proxy
   const existingProxy = proxyMap.get(target);
   if (existingProxy) {
       return existingProxy;
  }
   // only a whitelist of value types can be observed.
   const targetType = getTargetType(target);
   if (targetType === 0 /* INVALID */) {
       return target;
  }
 
//创建一个代理对象
   const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
   proxyMap.set(target, proxy);
   return proxy;
}

在这里我们终于看到了 Proxy 了,证明 对象的响应式操作是通过Proxy来实现的。

总结:

  1. 调用ref函数定义基本数据类型使之成为响应式对象的时候,它还是通过Object.defineProperty的方式进行的。

  2. 调用ref函数定义引用数据类型使之成为响应式对象的时候,通过Object.defineProperty的方式进行的,而里面的值是 再 通过Proxy代理的方式进行的。

  3. 调用reactive函数定义基本数据类型使之成为响应式对象的时候,直接返回,打印错误

  4. 调用reactive函数定义引用数据类型使之成为响应式对象的时候,它是通过Proxy代理的方式进行的。

  5. 数组也是引用类型的数据,故直接使用reactive函数进行调用即可


简单的实现vue2的响应式代码,可以点击 查看原文