vue3响应式、浅响应式、去响应式

11,246 阅读4分钟

ref、reactive

基本数据类型使用ref

    import { ref } from "vue";
    let s = ref('');
    let b = ref(true);
    let n = ref(0);
    
    s.value = 'zzz';
    b.value = false;

引用类型使用

    import { reactive } from "vue";
    let obj = reactive({});
    let arr = reactive([]);
    
    obj.name = '小刘同学'
    arr.push('小刘同学')
    

为什么两种数据的定义响应式的方式不同?因为基本数据类型没有办法用proxy实现数据劫持,所以内部应该是包了一层成为了对象,所以使用的时候.value获取修改,在模板中不用.value获取,框架编译的时候进行了处理。

引用类型数据可以用ref嘛?可以,但没必要。ref转成对象之后会直接再调用reactive实现响应式。用ref属于是脱了裤子放屁😀。

关于响应式,vue2就是把data定义的对象递归对每一个属性用defineProperty进行数据劫持。vue3 源码没有读过,大概就是类似vue2的递归data属性绑定响应式的方法进行封装成了reactive, 劫持方式变成了proxy。

customRef

创建一个自定义的 ref,可以对你定义的变量的跟踪及更新进行显示控制,返回一个有get和set的对象,用法很像computed,但是功能更强大,因为它更像你封装的一个可复用的工具方法。

官方😏:通过v-model 实现 debounce的示例

<input v-model="text" />

function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

export default {
  setup() {
    return {
      text: useDebouncedRef('hello')
    }
  }
}

isRef、unref、isReactive、isProxy

  • isRef判断变量是否是用ref定义过
  • unref 是val = isRef(val) ? val.value : val 的语法糖, const cur = unref(val)
  • isReactive 判断变量是否被reactive定义过
  • isProxy 判断变量是否由reactive或readonly创建的的

toRef、toRefs、toRow

  • toRef 对一个响应式对象,可以单独摘取其中一个属性单独作为一个响应式变量,并保持数据同步、响应同步
const state = reactive({
  foo: 1,
  bar: 2
})
const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2
console.log(fooRef.value) // 2

state.foo++
console.log(fooRef.value) // 3
console.log(state.foo) // 3
  • toRefs 批量执行toRef的功能
const state = reactive({
  foo: 1,
  bar: 2
})
// 两种方式
const fooRefs = toRefs(state)
fooRefs.foo.value

const {foo, bar} = toRefs(state)
foo.value

思考:刚开始看到这俩api,我是有一些迷惑性的,用起来管理不会太乱嘛,不知道什么场景会用到。

  • 官方给的例子说传递组件props的时候,传递复合函数的时候比较有用,即使原property不存在的时候也会返回一个可用的ref。toRefs不会处理之前没有定义的属性
  • 这样是不是就是将数据变成了一个双向数据流了?这个问题的验证在总结组件传值的时进行验证吧😏
export default {
  setup(props) {
    const foo = toRef(props, 'foo')
    useSomeFeature(foo)
  }
}
export default {
  setup(props) {
    const {name, age} = toRefs(props)
  }
}

shallowRef、triggerRef、shallowReactive

  • 如果你又想记录数据的变化,又不想响应式的修改视图,你就可以用shallowRef去定义数据
  • 到了你想要去更新视图的时候,你可以使用triggerRef
  • shallowReactive 只响应变量的第一次数据、深层次的就不会响应。也没看到triggerReactive,triggerRef也只能用于ref定义的数据
const shallow = shallowRef({
  greet: 'Hello, world'
})

// 第一次运行时记录一次 "Hello, world"
watchEffect(() => {
  console.log(shallow.value.greet)
})

// 这不会触发作用 (effect),因为 ref 是浅层的
shallow.value.greet = 'Hello, universe'

// 触发更新响应式
toTrigger = () => {
    triggerRef(shallow)
}

const state = shallowReactive({
  foo: 1,
  nested: {
    bar: 2
  }
})

// 改变 state 本身的性质是响应式的
state.foo++
// ...但是不转换嵌套对象
isReactive(state.nested) // false
state.nested.bar++ // 非响应式

总结:通过观察其实shallowRef和shallowReactive的处理方式很相似,都是浅层代理,只响应第一层数据, 不响应式深层数据。shallowRef传的也是个对象,内部处理还会套一层value。

toRaw、markRaw、readonly

  • readonly 对响应式或纯对象或ref对象深度遍历所有属性变为都是只读的,ref不会被解包还是.value使用,修改会警告
const obj = reactive({ count: 0 })
const s = reactive('1')
const copyObj = readonly(obj)
const copyS = readonly(s)
  • toRaw 让返回reactive或readonly代理的原始对象,会解包,对ref变量无效
const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo, reactiveFoo) // true {}
  • markRaw 标记一个对象,使其永远不会转换为 proxy。
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// 嵌套在其他响应式对象中时也可以使用
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

总结: 官方对浅响应式(shallowXXX)、去响应式的(toRaw markRaw)的解读是在某方面可以处理我们不想响应式的业务场景。更重要是在一些情况下可以优化性能,比如一个大量数据的大列表,我们只关注数据的新增和删除不关心数据内部的变化就可以用浅响应式,只用于渲染的不会修改的就可以完全去响应式。这个处理就真的🐂🍺了👍