【源码系列#05】Vue3响应式原理(Ref)

专栏分享:vue2源码专栏,vue3源码专栏,vue router源码专栏,玩具项目专栏,硬核💪推荐🙌

欢迎各位ITer关注点赞收藏🌸🌸🌸

Ref & ShallowRef

ref: 接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性 .value

可以将 ref 看成 reactive 的一个变形版本,这是由于 reactive 内部采用 Proxy 来实现,而 Proxy 只接受对象作为入参,这才有了 ref 来解决值类型的数据响应,如果传入 ref 的是一个对象,内部也会调用 reactive 方法进行深层响应转换

javascript 复制代码
const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

shallowRef: ref() 的浅层作用形式。和 ref() 不同,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。只有对 .value 的访问是响应式的。

javascript 复制代码
const state = shallowRef({ count: 1 })

// 不会触发更改
state.value.count = 2

// 会触发更改
state.value = { count: 2 }

源码实现

  • @issue1 如果是对象和数组,则调用 reactive方法 转化为响应式对象(ref会转换,shallowRef不会转换)
  • @issue2 getter 取值的时候收集依赖
  • @issue3 setter 设置值的时候触发依赖
javascript 复制代码
/**
 * @desc 如果是对象和数组,则转化为响应式对象
 */
function toReactive(value) {
  return isObject(value) ? reactive(value) : value
}

/**
 * @desc RefImpl
 * @issue1 如果是对象和数组,则转化为响应式对象
 */
class RefImpl {
  // ref标识
  public __v_isRef = true
  // 存储effect
  public dep = new Set()
  public _value
  constructor(public rawValue, public _shallow) {
    // @issue1
    this._value = _shallow ? rawValue : toReactive(rawValue)
  }
  get value() {
    // 取值的时候收集依赖
    trackEffects(this.dep)
    return this._value
  }
  set value(newValue) {
    // 新旧值不相等
    if (newValue !== this.rawValue) {
      // @issue1
      this._value = this._shallow ? newValue : toReactive(newValue)
      this.rawValue = newValue
      // 设置值的时候触发依赖
      triggerEffects(this.dep)
    }
  }
}


// 接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value。
export function ref(value) {
  return new RefImpl(value)
}

测试代码

javascript 复制代码
/**
 * 1. Ref
 **/
const person = ref({
  name: '柏成',
  age: 25,
})
effect(() => {
  app.innerHTML = person.value.name
})

setTimeout(() => {
  person.value.name = '柏成2号' // 会触发更改
}, 1000)

/**
 * 2. shallowRef
 */
const person = shallowRef({
  name: '柏成',
  age: 25,
})
effect(() => {
  app.innerHTML = person.value.name
})

setTimeout(() => {
  person.value.name = '柏成2号' // 不会触发更改
}, 1000)

setTimeout(() => {
  person.value = {
    name: '柏成9号' // 会触发更改
  }
}, 2000)

toRef & toRefs

toRef: 基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。

javascript 复制代码
const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

// 更改该 ref 会更新源属性
fooRef.value++
console.log(state.foo) // 2

// 更改源属性也会更新该 ref
state.foo++
console.log(fooRef.value) // 3

toRefs: 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。

javascript 复制代码
const state = reactive({
  foo: 1,
  bar: 2
})

const stateAsRefs = toRefs(state)

// 这个 ref 和源属性已经 "链接上了"
state.foo++
console.log(stateAsRefs.foo.value) // 2

stateAsRefs.foo.value++
console.log(state.foo) // 3

源码实现

javascript 复制代码
class ObjectRefImpl {
  // 只是将.value属性代理到原始类型上
  constructor(public object, public key) {}
  
  get value() {
    return this.object[this.key]
  }
  
  set value(newValue) {
    this.object[this.key] = newValue
  }
}

// 基于响应式对象上的一个属性,创建一个对应的 ref。这样创建的 ref 与其源属性保持同步:改变源属性的值将更新 ref 的值,反之亦然。
export function toRef(object, key) {
  return new ObjectRefImpl(object, key)
}

// 将一个响应式对象转换为一个普通对象,这个普通对象的每个属性都是指向源对象相应属性的 ref。每个单独的 ref 都是使用 toRef() 创建的。
export function toRefs(object) {
  const result = isArray(object) ? new Array(object.length) : {}

  for (let key in object) {
    result[key] = toRef(object, key)
  }

  return result
}

测试代码

javascript 复制代码
// 对象
const person = reactive({
  name: '柏成',
  age: 18
})
// 数组
const numbers = reactive([1, 2, 3, 4, 5])

// 注意!直接解构后会丢失响应性的特点!!!
// let { name, age } = person

const {
  name,
  age
} = toRefs(person)
const [first] = toRefs(numbers)

effect(() => {
  app.innerHTML = `${name.value},${age.value}岁。第一个数字为${first.value}。`
})

setTimeout(() => {
  name.value = '柏成9号'
  first.value = 999
}, 1000)

自动脱ref

在js中访问ref时需要.value获取,但是在模版中却可以直接取值,不需要加.value!这里就用到了 proxyRefs 自动脱ref方法

源码实现

javascript 复制代码
export function proxyRefs(object) {
  return new Proxy(object, {
    // 代理的思想,如果是ref 则取ref.value
    get(target, key, recevier) {
      let r = Reflect.get(target, key, recevier)
      return r.__v_isRef ? r.value : r
    },
    // 设置的时候如果是ref,则给ref.value赋值
    set(target, key, value, recevier) {
      let oldValue = target[key]
      if (oldValue.__v_isRef) {
        oldValue.value = value
        return true
      } else {
        return Reflect.set(target, key, value, recevier)
      }
    },
  })
}

测试代码

javascript 复制代码
const name = ref('柏成')
const age = ref('24')

const person = proxyRefs({
  name,
  age,
  sex: '男'
})

effect(() => {
  app.innerHTML = `${person.name},${person.age}岁。性别${person.sex}。`
})

setTimeout(() => {
  name.value = '柏成9号'
}, 1000)
相关推荐
Dream-Y.ocean1 小时前
鸿蒙PC平台宅匠 3D 室内设计适配实战:基于 Electron 壳方案的 Three.js 双渲染引擎与参数化户型编辑器开发
javascript·3d·harmonyos
Csvn2 小时前
性能监控
前端
Highcharts.js2 小时前
Highcharts Gantt使用符号的自定义数据标签开发甘特图示例
javascript·甘特图·highcharts·图表开发·实战代码·响应式图表
Csvn2 小时前
构建工具
前端
FRing033 小时前
vue2干货总结3(computed计算属性、watch监听器)
开发语言·前端·vue.js·vue2
Java小卷3 小时前
JuanForm vs FormCreate!Vue3 表单引擎联动配置 PK!
vue.js·低代码
kyriewen5 小时前
Skills 榜前 10 我全装了:只有 3 个能直接用
前端·javascript·程序员
IT_陈寒5 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端
工控丫丫6 小时前
帆软看板中经常使用的公式函数及经常搭的组合
前端·javascript·数据库
jerrywus6 小时前
我用 GPT-6.1-sol 做了台会讲解的 3D 售货机:60 罐饮料,一口没喝上
前端·agent·claude