vue3 之 响应式 ref 、computed 、reactive的区别

前言

我们都知道vue3.0版本对所有的底层代码做了一次更新,尤其是响应式跟2.0的变化最大;

在2.0的时候使用的是Object.defineproperty()做的数据劫持, 不过Object.defineproperty是对所有的属性做的数据劫持不是目标对象,而且对数组是无法进行劫持的,也就是数组的变化监听实际上是,在原有的数组方法上进行的改造实现的;

但是3.0不一样,是使用proxy代理模式进行的数据劫持监听,proxy有个好的地方就是可以监听整个Object对象,不用单独去监听单个对象属性就可以检测到数据的变化,比之前的单个属性监听减少了性能上的开销,还有就是可以监听数组,只不过穿的参数是一个数组但是返回的却是对象形式;

举个例子:

import { reactive } from 'vue';

const arr = reactive([1,2,3,4]);

console.log(arr); // 输出的是代理之后的 Proxy {0: 1, 1: 2, 2: 3, 3: 4}

ref 原理和使用方式

ref官方文档说明是:接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value。那它又是怎么实现的呢

// 可以是对象的属性
export function ref<T extends object>(
  value: T
): T extends Ref ? T : Ref<UnwrapRef<T>>
// 也可以是一个任意值
export function ref<T>(value: T): Ref<UnwrapRef<T>>

export function ref<T = any>(): Ref<T | undefined>
// ref的值是可选的,非必填项
export function ref(value?: unknown) {
  return createRef(value)
}

从上边代码我们可以看到ref的参数可以是一个任意值,返回的值是当前的参数,或者是一个undefined;
并且如果你什么也不传也可以,会返回一个响应式对象但是value是undefined;

举个例子:

import { ref } from 'vue';
const refParam = ref();

console.log(refParam) // 返回是一个RefImpl {_rawValue: undefined, _shallow: false, __v_isRef: true, _value: undefined}

我们接着往下分析

// 如果是一个对象,则使用reactive做深度代理,否则直接返回
const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val
// shallow 用来表示是浅层代理还是深度代理
function createRef(rawValue: unknown, shallow = false) {
  if (isRef(rawValue)) {
    return rawValue
  }
  let value = shallow ? rawValue : convert(rawValue) // convert 如果是一个对象,则使用reactive做深度代理,否则直接返回
  const r = {
    __v_isRef: true,
    get value() {
      // 方法追踪
      track(r, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newVal) {
      // 判断新值和旧值是否是一致的,如果不是一致的就进行更新操作
      if (hasChanged(toRaw(newVal), rawValue)) {
        rawValue = newVal
        value = shallow ? newVal : convert(newVal)
        trigger(r, TriggerOpTypes.SET, 'value', newVal)
      }
    }
  }
  return r
}

从上边的代码我们可以分析出来,我们传进来的参数会被判断解析;
1.看第一条判断,如果是被代理过的不会再做更多操作,将直接返回这个值;
2.如果shallow表示浅代理value则直接使用这个参数当做返回值;
3.如果是深度代理会执行convert方法,这个时候会做判断是不是一个对象,是的话就使用reactive进行递归深度代理,否则使用当前值,reactive下边会进行详细分析;
4.从返回值就能看的出来,无论是对象还是数组亦或者其他的任意值,在ref处理的过程中都会当做是一个对象返回回来

computed 计算机属性3.0实现源码


// 返回的值是只读属性,并且是响应式
export function computed<T>(getter: ComputedGetter<T>): ComputedRef<T>
// 增加了计算机属性的读写操作
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>

// 参数可以是一个对象形式,也可以是一个方法
export function computed<T>(
  // 参数是一个方法或者是一个对象参数,如果是方法执行只读,如果是对象可以操作读写
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  // 如果是方法的话,不可以修改属性值,读取,如果是一个对象的话,则可以操作读写功能
  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  let dirty = true
  let value: T
  let computed: ComputedRef<T>

  // effect 传进来一个getter,这个getter = 如果当前参数是一个方法,值直接赋值当前的function,否则赋值这个对象的get方法
  const runner = effect(getter, {
    lazy: true, // 懒执行
    scheduler: () => {
      if (!dirty) {
        dirty = true
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  computed = {
    // 标记为这个是需要代理的响应式
    __v_isRef: true,
    // 是否是只读标记
    [ReactiveFlags.IS_READONLY]:
      isFunction(getterOrOptions) || !getterOrOptions.set,

    // expose effect so computed can be stopped
    effect: runner,
    get value() {
      if (dirty) {
        value = runner()
        dirty = false
      }
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}

从上述代码其实可以看出来,computed支持读写操作,如果我们在使用写操作的时候比如下边这个例子,当我们给computed传递的是一个方法的时候默认就是只读模式,不可以进行修改操作,但是我们如果想要修改这个值的话就需要使用对象形式的参数,注意{get() =>{}, set() => {}}get和set是搭配使用的,否则会抛出错误;
看下边这个例子:

// 只读模式操作
const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误!
// 读写模式操作
const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: (val) => {
    count.value = val - 1
  },
})

plusOne.value = 1
console.log(count.value) // 0

reactive

接收一个普通对象然后返回该普通对象的响应式代理,响应式转换是“深层的”:会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象。
下边直接看源码中怎么写的,然后咱们分析一下

// only unwrap nested ref
// 解嵌套
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
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
  )
}

上边代码我们可以看到reactive 接收的实际上是个对象,当然也可以接收一个数组,如果目标对象存在并且是一个只读的会直接返回这个对象,否则会创建一个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 reactiveFlag = isReadonly
    ? ReactiveFlags.READONLY
    : ReactiveFlags.REACTIVE
  if (hasOwn(target, reactiveFlag)) {
    return target[reactiveFlag]
  }
  // only a whitelist of value types can be observed.

  if (!canObserve(target)) {
    return target
  }
  const observed = new Proxy(
    target,
    collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers
  )
  def(target, reactiveFlag, observed)
  return observed
}

从上边代码不难看出,reactive接收的是一个对象形式,如果不是对象形式就会抛出一个错误,可能大家到这会有个疑问为什么数组也可以呢,这个是因为isObject不是强校验,看下边;

export const isObject = (val: unknown): val is Record<any, any> =>
  val !== null && typeof val === 'object'

通过上述的分析,我们不难看出,ref和reactive,computed的区别还是蛮大的;

首先ref对比reactive,reactive只能接收一个引用对象,不可以传递普通类型的数据,比如字符串、数字等,但是ref可以,因为ref返回的是一个响应式代理对象,这个对象value值就是我们的传递参数;所以如果是想代理一个对象或者是数组还是使用reactive更合适,如果我们想要代理一个普通类型的值就需要使用ref去代理更合理;

computed接收一个响应式的对象或者值,并且还可以对这个接收的目标进行操作,但是他本身并不是一个响应式的信息,只不过是对这个响应式的参数进行了追踪,获取和修改的操作;

今天的分享就到这了,如果有哪些地方说的不对,还望在下边的评论区发表出来,大家一起讨论;
如果想要体验vue3.0的同学也可以参考一下我的这篇文章vue3 学习 之 vue3使用

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

推荐阅读更多精彩内容