一步步从Vue3.x源码上理解ref和reactive的区别

   2023-03-08 学习力0
核心提示:目录前言使用原理refreactive总结前言对于 ref 和 reactive, 应该说是只要用了Vue3.x就会接触到因为想要触发响应式,就必须通过 ref 和 reactive 来实现但,对于他们理解,大家也是众说纷纭那本文将从源码层面,带你去理解一下 ref 和 reactive 的区别⚠️

前言

对于 refreactive, 应该说是只要用了Vue3.x就会接触到

因为想要触发响应式,就必须通过 refreactive 来实现

但,对于他们理解,大家也是众说纷纭

那本文将从源码层面,带你去理解一下 refreactive 的区别

⚠️ 此文章基于 Vue 3.2.47 进行分析

使用

ref 可以使用 基本对象 和 引用类型 对象,如:

ref({ num: 1 })

ref(1)

而,reactive 只能使用 引用类型

reactive({ num: 1 })

原理

ref

从源码上看,ref 方法,就是返回 createRef 的函数调用

// 位置在 /core-3.2.47/packages/reactivity/src/ref.ts 第 82 行
export function ref(value?: unknown) {
  return createRef(value, false)
}

createRef方法就是创建 RefImpl 对象的实例

// 位置在 /core-3.2.47/packages/reactivity/src/ref.ts 第 99 行
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

那么,很好理解了,为什么我们打印 ref(1) 会是这样

一步步从Vue3.x源码上理解ref和reactive的区别

那,RefImpl 对象的功能是什么呢?

首先来看 constructor 构造函数

// 位置在 /core-3.2.47/packages/reactivity/src/ref.ts 第 106 行
class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }
  // 省略部分代码...
}

在创建 RefImpl 的实例过程中, 由于在 ref 函数中调用 createRef 传入第二参数为 false,可以直接理解为

this._rawValue = toRaw(value)

this._value = toReactive(value)

toRaw 在递归中检查对象上是否有 __v_raw,可以理解为是返回原始数据

// 位置在 /core-3.2.47/packages/reactivity/src/reactive.ts 第 239 行
export function toRaw<T>(observed: T): T {
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

// 位置在 /core-3.2.47/packages/reactivity/src/reactive.ts 第 16 行
export const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  IS_SHALLOW = '__v_isShallow',
  RAW = '__v_raw'
}

toReactive 判断如果他是引用类型的对象,那就使用 reactive 返回对象,如果不是,那就原值返回

// 位置在 /core-3.2.47/packages/reactivity/src/reactive.ts 第 251 行
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value
  
// 位置在 /core-3.2.47/packages/shared/src/index.ts 第 63 行
export const isObject = (val: unknown): val is Record<any, any> =>
  val !== null && typeof val === 'object'

至此我们就是知道了,ref 如果传入 引用类型的对象底层还是调用 reactive

但是乍一想,好像不对?那 ref 如何进行做响应式的呢?

其实原理在

  // 位置在 /core-3.2.47/packages/reactivity/src/ref.ts 第 118 行
  get value() {
     trackRefValue(this)
     return this._value
   }

  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    newVal = useDirectValue ? newVal : toRaw(newVal)
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal)
    }
  }

我们都知道,如果想要触发 ref 的值更新,必须使用 .value,例如:

const num = ref(1)
console.log(num.value)
num.value = 2

我们知道,所谓的响应式其实就是依赖收集派发更新的过程

对于 console.log(num.value) 我们会触发 get value 函数,进行依赖收集

对于 num.value = 2 我们会触发 set value 函数,进行派发更新

所以

  • ref 对于简单类型是通过 get value 和 set value 进行依赖收集和派发更新
  • ref 对于引用类型是通过 reactive 进行依赖收集和派发更新

但,我们依旧需要注意:

const count = ref({ num: 1 })

count.value.num = 2 // 不会触发 set value

reactive

从源码上看,reactive 方法,就是返回 createReactiveObject 的函数调用

// 位置在 /core-3.2.47/packages/reactivity/src/reactive.ts 第 90 行
export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (isReadonly(target)) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

createReactiveObject 方法,使用了 proxy 对值创建的代理对象,并返回代理对象

// 位置在 /core-3.2.47/packages/reactivity/src/reactive.ts 第 181 行
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, 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 existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // only specific 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
}

// 位置在 /core-3.2.47/packages/reactivity/src/reactive.ts 第 58 行
function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}

// 位置在 /core-3.2.47/packages/reactivity/src/reactive.ts 第 43 行
function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

// 位置在 /core-3.2.47/packages/reactivity/src/reactive.ts 第 37 行
const enum TargetType {
  INVALID = 0,
  COMMON = 1,
  COLLECTION = 2
}

现在,我们应该聚焦一下 baseHandlers,因为根据运行上下文可以知道,我们当前的 targetType1, 所以传入baseHandlers对象

baseHandlers 是从createReactiveObject进行传入,也就是 mutableHandlers

// 位置在 /core-3.2.47/packages/reactivity/src/baseHandlers.ts 第 225 行
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

// 位置在 /core-3.2.47/packages/reactivity/src/baseHandlers.ts 第 48 行
const get = /*#__PURE__*/ createGetter()

// 位置在 /core-3.2.47/packages/reactivity/src/baseHandlers.ts 第 158 行
const set = /*#__PURE__*/ createSetter()

get是通过 createGetter 方法创建

set是通过 createGetter 方法创建

对于 createGetter 返回了一个 get 函数

// 位置在 /core-3.2.47/packages/reactivity/src/baseHandlers.ts 第 94 行
function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.IS_SHALLOW) {
      return shallow
    } else if (
      key === ReactiveFlags.RAW &&
      receiver ===
        (isReadonly
          ? shallow
            ? shallowReadonlyMap
            : readonlyMap
          : shallow
          ? shallowReactiveMap
          : reactiveMap
        ).get(target)
    ) {
      return target
    }

    const targetIsArray = isArray(target)

    if (!isReadonly) {
      if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
        return Reflect.get(arrayInstrumentations, key, receiver)
      }
      if (key === 'hasOwnProperty') {
        return hasOwnProperty
      }
    }

    const res = Reflect.get(target, key, receiver)

    if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
      return res
    }

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isRef(res)) {
      // ref unwrapping - skip unwrap for Array + integer key.
      return targetIsArray && isIntegerKey(key) ? res : res.value
    }

    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

对于 createGetter 返回了一个 set 函数

// 位置在 /core-3.2.47/packages/reactivity/src/baseHandlers.ts 第 161 行
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    let oldValue = (target as any)[key]
    if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
      return false
    }
    if (!shallow) {
      if (!isShallow(value) && !isReadonly(value)) {
        oldValue = toRaw(oldValue)
        value = toRaw(value)
      }
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }

    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

其说白了,reactive 依赖 Proxy将值作为被代理对象,创建代理对象,也是通过getset,进行依赖收集派发更新

此时,我们也能理解了打印reactive({num: 1})为什么是Proxy 对象

一步步从Vue3.x源码上理解ref和reactive的区别

总结

  • ref其实就是创建RefImpl的实例对象,对于 简单类型 直接通过get valueset value进行依赖收集和派发更新 ,而对于 引用类型 直接调用 reactive方法
  • reactive底层用了 Proxy对象,创建出代理对象,进行依赖收集和派发更新
原文地址:https://juejin.cn/post/7196227448286429221
 
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • vue3+TS 自定义指令:长按触发绑定的函数
    vue3+TS 自定义指令:长按触发绑定的函数而然间看到一个在vue2中写的长按触发事件的自定义指定,想着能不能把他copy到我的vue3项目中呢。编写自定义指令时遇到的几个难点1.自定义指令的类型在ts中写任何东西都要考虑到类型的问题,自定义指令的类型问题依然存
    03-08
  • Vue生命周期 vue生命周期几个阶段
    Vue生命周期 vue生命周期几个阶段
    官网解释一、Vue的生命周期Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom - 渲染、更新 - 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。Vue2生命周期:1.beforeCreate(创建前)数据观测和初始化事件还未开始,此时 dat
    03-08
  • vue 中安装并使用echart
    vue 中安装并使用echart
    本文为博主原创,转载请注明出处:1.安装echart 依赖:  安装命令: npm install echarts --save  在vscode 的终端窗口进行执行,如图所示:   执行完之后,查看 项目中的echart 版本依赖是否添加成功:  package-lock.json 中有具体的echart 依赖
    03-08
  • day04-Vue01
    day04-Vue01
    Vue011.Vue是什么?Vue(读音/vju:/,类似于view)是一个前端框架,依据构建用户界面Vue的核心库只关注视图层,不仅易于上手,还便于与第三方库或者项目整合支持和其他类库结合使用开发复杂的单页应用非常方便Vue是Vue.js的简称官网:Vue.js - 渐进式 JavaScr
    03-08
  • Vue + Element 自定义上传封面组件
    Vue + Element 自定义上传封面组件
    前一段时间做项目,频繁使用到上传图片组件,而且只上传一个封面,于是想着自定义一个图片封面上传组件。先来看一下效果:                      第一张图片是上传之前,第二张图片是上传成功后,第3张图片是鼠标放上去之后的效果! 首先整理需
    03-08
  • 基于ZR.VUE 前端的改造,页面刷新报错
     问题描述:前后端分离开发,分开部署. 页面刷新 直接报404 错误的解决办法提示:  先在 .env.development 中 配置 VUE_APP_BASE_API , 将 '/' 替换为 后端地址 'http://localhost:8888/'如果是对应的发布的正式环境,也要修改  .env.production 的VUE_APP_
    03-08
  • Vue3 企业级优雅实战 - 组件库框架 - 9 实现组
    上文搭建了组件库 cli 的基础架子,实现了创建组件时的用户交互,但遗留了 cli/src/command/create-component.ts 中的 createNewComponent 函数,该函数要实现的功能就是上文开篇提到的 —— 创建一个组件的完整步骤。本文咱们就依次实现那些步骤。(友情提示
    03-08
  • vue-3 this概念
    一、this概念官方是这样说的:在 setup()内部,this 不会是该活跃实例的引用因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同这在和其它选项式 API 一起使用 setup() 时可能会导致混淆啥意思呢
    03-08
  • vue3和百度地图关键字检索 定位 点击定位
    vue3和百度地图关键字检索 定位 点击定位
    效果图在index.html中引入百度地图开放平台  去申请你的ak 非常的简单可以自己百度 一下!-- 这个用官网给的有好多警告 更具百度的把 https://api.map.baidu.com/getscript?v=2.0ak=xxxxxxxxxxxxxxxxxxxxx 换为这个就没有那么多 报错了 --scripttype="text/j
    03-08
  • Vue集成lodop插件实现打印功能 vue打印console
    Vue集成lodop插件实现打印功能 vue打印console
    目录VUE简单使用lodop1.创建LodopFuncs.js文件2.在打印功能vue页面引入LodopFuncs3.执行打印方法4.打印接口函数官网样例说明5.完整页面示例VUE 集成LODOP插件打印Lodop、C-Lodop使用说明及样例http://www.lodop.net/LodopDemo.htmlVUE简单使用lodop1.创建Lodo
    03-08
点击排行