Vue3响应式原理解读(Ref)

前言

最近在学习vue的相关知识,之前是做iOS开发,对相应式感到好奇,和iOS的kvo模式有点像,所以就研究了下vue的Ref,再次做一个简单的总结

Ref & ShallowRef

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

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

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

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

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

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

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

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

源码实现

  • @issue1 如果是对象和数组,则调用 reactive方法 转化为响应式对象(ref会转换,shallowRef不会转换)
  • @issue2 getter 取值的时候收集依赖
  • @issue3 setter 设置值的时候触发依赖
kotlin 复制代码
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)
}

测试代码

scss 复制代码
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 的值,反之亦然。

scss 复制代码
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() 创建的。

scss 复制代码
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

源码实现

typescript 复制代码
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 复制代码
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方法

源码实现

typescript 复制代码
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)
      }
    },
  })
}

测试代码

ini 复制代码
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)
相关推荐
_codeOH1 天前
Vue 3 vs React 19:框架还在卷,核心原理就这些
前端·vue.js
英勇无比的消炎药1 天前
新手必看玩转TinyRobot一定要避开这些坑
前端·vue.js
英勇无比的消炎药1 天前
别再盲目混用AI组件库和传统组件库差距原来这么大
前端·vue.js
英勇无比的消炎药1 天前
前端提效神器全新AI组件库TinyRobot改写日常开发模式
前端·vue.js
英勇无比的消炎药1 天前
前端提效神器TinyRobot
前端·vue.js
CDwenhuohuo1 天前
uni 背景色渐变 全屏
前端·javascript·vue.js
爱怪笑的小杰杰1 天前
Vue 项目交付第三方开发,如何隐藏核心 JS 源码?
前端·javascript·vue.js
小二·1 天前
Vue 3 组合式 API 进阶实战
前端·javascript·vue.js
rising start1 天前
九、vue3 组件通信:全场景详解
前端·vue.js·typescript
编程技术手记1 天前
Vue Scoped CSS 与动态创建 DOM 的兼容性问题
前端·css·vue.js