深入源码理解Vue3 reactive

我们先来看官方对于reactive的解释,官方的解释也非常简单

返回对象的响应式副本

但从这句话我们可以得到以下信息

  1. reactive接受一个对象作为参数
  2. 其返回值是经reactive函数包装过后的数据对象,这个对象具有响应式

但同样会有一些疑问
比如,reactive的参数只能传递一个对象吗,如果传递其他值会怎么样?
比如,返回的响应式数据的本质是什么,为啥就能让数据变成响应式?
比如,"副本"是不是意味着响应式数据与原始数据没有关联?
比如,返回的响应式副本里头的数据是深度响应式吗,即是否递归监听对象的所有属性?等等

带着这些疑问我们一起来看
首先,通过reactive创建一个响应数据

import { reactive } from "vue";
export default {
  setup() {  
    const state = reactive({
      count: 0,
    });
  },
};

如上代码就可以创建一个响应式数据state,我具体来看一下这个

console.log(state)

可以看见,返回的响应副本state其实就是Proxy对象。所以reactive实现响应式就是基于ES2015 Proxy的实现的。那我们知道Proxy有几个特点:

  1. 代理的对象是不等于原始数据对象
  2. 原始对象里头的数据和被Proxy包装的对象之间是有关联的。即当原始对象里头数据发生改变时,会影响代理对象;代理对象里头的数据发生变化对应的原始数据也会发生变化。
    需要记住:是对象里头的数据变化,并不能将原始变量的重新赋值,那是大换血了

因此,既然reactive实现响应式是基于Proxy的实现的,那我们大胆猜测,原始数据与相应数据也是有关联的。那我们来测试一下

<template>
  <button @click="change">
    {{ state.count }}
  </button>
</template>
<script>
import { reactive } from "vue";
export default {
  setup() {
    const obj = {
      count: 0,
    };
    const state = reactive(obj);
    function change(){
        ++state.count
        console.log(obj);
        console.log(state);
    }
    return { state,change};
  },
};
</script>

以上代码测试结果如下

验证,确实当响应式对象里头数据变化的时候原始对象的数据也会变化
如果反过来,结果也是一样

 // ++state.count
++obj.count;

当响应式对象里头数据变化的时候原始对象的数据也会变化
那问题来了,我们操作数据的时候通过谁来操作呢?
官方的建议是

建议只使用响应式代理,避免依赖原始对象

再来解决另外一个问题看看reactive是否会深度监听每一层呢?

const state = reactive({
    a:{
        b:{
            c:{name:'c'}
        }
    }
});    
console.log(state);  
console.log(state.a);
console.log(state.a.b);  
console.log(state.a.b.c); 

可以看到结果reactive是递归会将每一层包装成Proxy对象的,深度监听每一层的property

最后测试一下如果reactive传递是非对象而是原始值会怎么样

const state = reactive(0);  
console.log(state)

结果是,原始值并不会被包装,所以也没有响应式特点

下面,我们看看reactive的源码吧
源码目录位置:vue-next\packages\reactivity\src\reactive.ts
直接找到reactive的类型声明:

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

可以看到reactive接受一个参数targettarget的类型是泛型T,而T类型是extends object,简单来说接受的参数target的类型是object类型或者时继承自object类的子类类型
返回值的类型的UnwrapNestedRefs<T>
看看UnwrapNestedRefs<T>类型

type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

使用type关键字声明类型UnwrapNestedRefs<T>,这里有个三目运算符,用于进一步判断T;如果传入的T属于Refs类或者其子类,那么返回传入的T,否者就是UnwrapRef<T>

下面具体看看reactive方法的定义

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

接受一个类型为object参数,当传入对象是只读,返回本身。这里的as关键字是断言,表示传入的值一定是Target类型,里头有个ReactiveFlags.IS_READONLY,用于判断是否是只读的属性

export interface Target {
  [ReactiveFlags.SKIP]?: boolean
  [ReactiveFlags.IS_REACTIVE]?: boolean
  [ReactiveFlags.IS_READONLY]?: boolean
  [ReactiveFlags.RAW]?: any
}

如果传递的对象是普通对象(不是readonly),则执行创建响应式对象函数createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers)
该方法比较长,是reactive的核心方法,所以还是得读一下源码

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    if (__DEV__) {
      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[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // target already has corresponding Proxy
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only a whitelist of value types can be observed.
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

可以看到除了几种特殊情况返回target本身之外,就返回proxyproxy就是通过new Proxy构造函数构建出来的。这里也进一步证明了reactive的响应式功能确实是通过Proxy实现的
可以看一样Proxy的定义

interface ProxyHandler<T extends object> {
    getPrototypeOf? (target: T): object | null;
    setPrototypeOf? (target: T, v: any): boolean;
    isExtensible? (target: T): boolean;
    preventExtensions? (target: T): boolean;
    getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined;
    has? (target: T, p: PropertyKey): boolean;
    get? (target: T, p: PropertyKey, receiver: any): any;
    set? (target: T, p: PropertyKey, value: any, receiver: any): boolean;
    deleteProperty? (target: T, p: PropertyKey): boolean;
    defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean;
    enumerate? (target: T): PropertyKey[];
    ownKeys? (target: T): PropertyKey[];
    apply? (target: T, thisArg: any, argArray?: any): any;
    construct? (target: T, argArray: any, newTarget?: any): object;
}
interface ProxyConstructor {
    revocable<T extends object>(target: T, handler: ProxyHandler<T>): { proxy: T; revoke: () => void; };
    new <T extends object>(target: T, handler: ProxyHandler<T>): T;
}
declare var Proxy: ProxyConstructor;

里面的具体实现方法,在createReactiveObject传参的时候就传入进来了
mutableHandlers和mutableCollectionHandlers,具体可以去`vue-next\packages\reactivity\src\baseHandlers.ts文件中看

经过上面的了解,我们可以总结和回答一下最开始几个疑问了
1. reactive的参数可以传递对象也可以传递原始值。但是原始值并不会包装成响应式数据
2. 返回的响应式数据的本质Proxy对象
3. 返回的响应式"副本"与原始数据有关联,当原始对象里头的数据或者响应式对象里头的数据发生,会彼此相互影响。两种都可以触发界面更新,操作时建议只使用响应式代理对象
4. 返回的响应式对象里头时深度递归监听每一层的,每一层都会被包装成Proxy对象

以上就是Vue3中reactive基本内容
注:本文示例代码可在github查阅
https://github.com/jCodeLife/learn-vue3/tree/master/learn-vue3-reactive

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容