Vue3-02 响应式API

文章目录

  • 响应式系统API
    • `reactive`
    • `ref`
    • `reactive` VS `ref`
    • `readonly`
    • `computed`
    • `watchEffect`
      • 停止监听
      • 清除副作用
      • 执行时机
      • 调试
    • `watch`
    • `watch` vs `watchEffect`
  • 响应式系统工具集
    • `unref`
    • `toRef`
    • `toRefs`
    • `isRef`
    • `isProxy`
    • `isReactive`
    • `isReadonly`
  • 高级响应式系统API
    • `customRef`
    • `toRaw`
    • `markRaw`
    • `shallowReactive`
    • `shallowReadonly`
    • `shallowRef`

响应式系统API

reactive

const obj = reactive({ count: 0 })

相当于Vue 2.x中的Vue.observable()API,返回一个普通对象的响应式代理,响应式转换是深层的,会影响对象内部嵌套的属性,基于ES2015的Proxy实现,返回的代理对象不等于原始对象,要避免使用原始对象

经过试验,Vue3中可以通过修改数组下标来响应式的更改数组成员的值了

reactive将自动解构所有深层次的refs,同时维持ref的响应性

const count = ref(1)
const obj = reactive({ count })

// ref 会被解构
console.log(obj.count === count.value) // true

// 它会更新 `obj.value`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2

ref

ref的引入是为了以变量形式传递响应式的值而不再依赖访问this:

const count = ref(0)

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

ref主要目的是保证基本类型值的响应式,如果传入的参数不是基本类型,会调用reative方法进行深层响应式转换

ref使用时:

  • ref的返回值setup中返回应用在模板中时,会自动解构,不需要书写.value
  • ref作为reactive对象的属性被修改或访问时,也会自动解构,不需要书写.value
  • 通过ArrayMap等原声集合类中范围ref时,不会自动解构,需要使用.value获取值

reactive VS ref

使用refreactive的区别可以通过如何撰写编撰的JavaScript逻辑比较

// 风格 1: 将变量分离
let x = 0
let y = 0

function updatePosition(e) {
  x = e.pageX
  y = e.pageY
}

// --- 与下面的相比较 ---

// 风格 2: 单个对象
const pos = {
  x: 0,
  y: 0,
}

function updatePosition(e) {
  pos.x = e.pageX
  pos.y = e.pageY
}

使用ref就是将将风格(1)转换为使用ref,让基础类型值也具有响应性,使用reactive和风格(2)一致

只使用reactive的问题是,使用组合函数的时候必须始终保持对这个组合函数返回对象的引用以保持响应性,这个对象不能够被解构或者展开

// 组合函数:
function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0,
  })

  // ...
  return pos
}

// 消费者组件
export default {
  setup() {
    // 这里会丢失响应性!
    const { x, y } = useMousePosition()
    return {
      x,
      y,
    }

    // 这里会丢失响应性!
    return {
      ...useMousePosition(),
    }

    // 这是保持响应性的唯一办法!
    // 你必须返回 `pos` 本身,并按 `pos.x` 和 `pos.y` 的方式在模板中引用 x 和 y。
    return {
      pos: useMousePosition(),
    }
  },
}

解决方法是使用toRefs将响应式对象的每个对象都转换为响应的ref

function useMousePosition() {
  const pos = reactive({
    x: 0,
    y: 0,
  })

  // ...
  return toRefs(pos)
}

// x & y 现在是 ref 形式了!
const { x, y } = useMousePosition()

目前阶段可以从下面两种风格二选其一:

(1)如果在普通的JavaScript中声明基础变量类型与对象变量时一样区别使用refreacitve,也就是说如果声明响应式的基础类型使用ref,如果声明响应式对象变量使用reactive

(2)全部使用reactive,然后在组合函数返回对象时使用toRefs

目前(2020.08.01)官方对refreactive的最佳实践还没有建议,自己选择更适合自己的风格使用,我会选择风格1使用。

readonly

如果我们希望一个响应式对象在某些情况下被改变,例如我们提供了一个Provide的响应式对象,不希望它在被注入时被改变,这时就可以基于原始对象创建一个只读的Proxy对象

const original = reactive({ count: 0 });
const copy = readonly(original);

// 通过 original 修改 count,将会触发依赖 copy 的侦听器
original.count++;

// 通过 copy 修改 count,将导致失败并出现警告
copy.count++; // 警告: "Set operation on key 'count' failed: target is readonly."

readonly传入一个对象(普通或者响应式对象)或ref,返回原始对象的深层的制度代理,任何被访问的嵌套的Property也是只读的

computed

计算属性用来创建依赖于其他状态的状态,有两种方法,一种是接受Getter函数,并为Getter返回值返回一个不可变的响应式ref对象

const count = ref(1);
const plusOne = computed(() => count.value + 1);

console.log(plusOne.value); // 2

plusOne.value++; // error

也可以传入一个拥有getset函数的对象来创建一个可写的ref对象

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

watchEffect

与React的useEffect非常类似

watchEffect会根据响应式状态自动应用和重新应用副作用,它立即执行传入的一个函数,同时响应式追踪其依赖,并在依赖改变时重新运行改函数;

const count = ref(0);

watchEffect(() => console.log(count.value));
// -> logs 0

setTimeout(() => {
  count.value++;
  // -> logs 01
}, 100)

停止监听

watchEffectsetup中或生命周期钩子中被调用时,会被链接到组件的生命周期,在组件卸载时自动停止

watchEffect的返回值是一个函数,可以显示调用它来手动停止侦听

const stop = watchEffect(() => {
  /* ... */
})

// later
stop()

清除副作用

副作用函数会执行一些异步的副作用,需要在失效时清除(即完成之前状态已经被改变了),所以侦听器被传入的函数中,可以接受onInvalidate函数作为入参,用来注册清理失效时的回调,在下面的情况中发生时,onInvalidate中传入的回调函数会被触发:

  • 副作用即将重新执行(watchEffect再次执行)
  • 侦听器被停止(如果在setup()或生命周期中函数中调用watchEffect的话,就是组件卸载时)
watchEffect((onInvalidate) => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id 改变时 或 停止侦听时
    // 取消之前的异步操作
    token.cancel()
  })
})

执行时机

watchEffect会在组件初始运行时同步打印出来,在监听状态变化后,会在组件更新后执行副作用。

Vue会缓存副作用函数,并且异步的刷新它们,可以避免在同一个Tick中多个状态改变导致不必要的重复调用。在核心实现中,组件的update函数也是一个被侦听的副作用,当一个用户定义的副作用函数进入队列时,默认情况下,自定义的副作用函数会在所有组件的update前执行




在这个例子中:

  • count会在初始运行是同步打印出来
  • 更改count时,在组件更新前执行副作用

如果需要在组件更新后重新运行侦听器副作用(常见的就是在侦听器中获取更新后的DOM,在Vue 2.x中需要使用$nextTick实现),可以传递带有flush选项的options

  • post,在组件更新后执行
  • sync,同步运行,低效,很少需要
  • pre,默认,在组件更新前执行

这些选项也会更改副作用的首次运行实际

// 在组件更新后触发,这样你就可以访问更新的 DOM。
// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
watchEffect(
  () => {
    /* ... */
  },
  {
    flush: 'post'
  }
)

调试

在第二个参数中传入onTrackonTrigger来调试,建议在传入的回调中进行debugger

watchEffect(
  () => {
    /* 副作用的内容 */
  },
  {
    onTrigger(e) {
      debugger;
    },
  }
)

onTrack在依赖被追踪时被调用,onTrigger在依赖变更导致副作用被触发时调用,这两个回调都接受到一个包含所有依赖项信息的调试器事件

仅在开发模式下生效

watch

watch需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它是惰性的,即只有当被侦听的源发生变化时才会执行回调

// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

// 侦听多个数据源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

使用侦听器俩比较数组和对象的值,要求使用副本监听,来避免更新前的值与更新后的值指向同一份引用而无法体现更新前后值的区别:

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

watch(
  () => [...numbers],
  (numbers, prevNumbers) => {
    console.log(numbers, prevNumbers);
  })

numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]

尝试检查深度嵌套对象或者数组中某个成员的Property变化时,仍然需要设置deeptrue

const state = reactive({ 
  id: 1, 
  attributes: { 
    name: "",
  },
});

watch(
  () => state,
  (state, prevState) => {
    console.log(
      "deep ",
      state.attributes.name,
      prevState.attributes.name
    );
  },
  { deep: true }
);

state.attributes.name = "Alex"; // 日志: "deep " "Alex" "Alex"

上面的状态是有问题的,因为stateprevState都是更新后的值,因为返回的都是该对象的当前值的上一个状态值的引用,为了完全侦听深度嵌套的对象和数组,需要对值进行深拷贝(上面使用结构来创建副本的原理相同),可以使用loadsh.cloneDeep等工具来实现

import _ from 'lodash';

const state = reactive({
  id: 1,
  attributes: {
    name: "",
  },
});

watch(
  () => _.cloneDeep(state),
  (state, prevState) => {
    console.log(
      state.attributes.name, 
      prevState.attributes.name
    );
  }
);

state.attributes.name = "Alex"; // 日志: "Alex" ""

watch vs watchEffect

二者共享的行为包括:

  • 停止侦听
  • 清除副作用(watchonInvalidate是第三个参数,watchEffect是唯一参数)
  • 刷新时机
  • 侦听器调试

二者的不同点:

  • watch懒执行(不会立即执行),watchEffect会在初始化时立即执行
  • watch将依赖提取为第一个参数,更明确哪些状态的改变会重新运行副作用,而watchEffect自动收集所有依赖
  • watch可以访问侦听状态变化前后的值

响应式系统工具集

unref

用来快速返回ref的值,如果参数是ref,返回它的value,否则返回参数本身。它是val = isRef(val) ? ref.value : ref的语法糖

toRef

reactive对象的属性创建一个ref,这个ref可以被传递并且保持对其源Property的响应式连接

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

const fooRef = toRef(state, 'foo')

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

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

当需要将一个Prop中的属性作为ref传给组合逻辑函数时,可以使用toRef

export default {
  setup(props) {
    useSomeFeature(toRef(props, 'foo'))
  },
}

即使源Property不存在,toRef也会返回一个可用的ref,这使得它在可选Prop时特别有用,可选Prop并不会被toRefs处理

toRefs

把一个响应式对象转换为普通对象,该普通对象的每个属性都是一个ref,与原来的响应式对象一一对应

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

const stateAsRefs = toRefs(state)
/*
stateAsRefs 的类型如下:

{
  foo: Ref,
  bar: Ref
}
*/

// ref 对象 与 原属性的引用是 "链接" 上的
state.foo++
console.log(stateAsRefs.foo) // 2

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

当从一个组合逻辑中返回响应式对象时,用toRefs是很有效的,它可以让消费组件可以解构或者扩展返回的对象,而不失去响应性

function useFeatureX() {
  const state = reactive({
    foo: 1,
    bar: 2,
  })

  // 对 state 的逻辑操作

  // 返回时将属性都转为 ref
  return toRefs(state)
}

export default {
  setup() {
    // 可以解构,不会丢失响应性
    const { foo, bar } = useFeatureX()

    return {
      foo,
      bar,
    }
  },
}

isRef

判断值是否是ref对象

isProxy

判断一个对象是否是由reactive或者readonly创建的代理

isReactive

判断一个对象是否是由reactive创建的代理。

如果这个代理是由readonly创建的,但是又被reactive创建的另一个代理包裹了一层,那么同样也会返回true

isReadonly

判断一个对象是否是由readonly创建的代理。

高级响应式系统API

customRef

用来自定义ref,可以显示依赖追踪和触发响应,接受一个函数,函数的两个参数是用于追踪的track和触发响应式的trigger,返回一个带有getset属性的对象

可以使用自定义ref来实现带防抖功能的v-model

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'),
    }
  },
}

个人还不知道有什么好的应用场景,实际上上面的例子不通过custromRef来实现,可能灵活度还更大

toRaw

返回reactive或者readonly方法转换为响应式代理的普通对象。用于临时读取数据而无需承担代理访问/跟踪的开销,也可用于写入数据而避免触发更改,访问不会被代理、跟踪,写入时不会触发更改。

不建议一致持有原始对象的引用。

markRaw

显示标记一个对象永远不会转换为响应式代理,返回这个对象本身

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

// 如果被 markRaw 标记了,即使在响应式对象中作属性,也依然不是响应式的
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false

它和下方的shallowXXXAPI的作用都是用来让用户有选择地退出默认的深度响应式/只读转换模式,并将原始的、未被代理的对象嵌入视图中,主要用来提升性能:

  • 有些值不应该是响应式的,例如复杂的第三方类库的实例或者Vue组件对象
  • 一个元素数量庞大,但是数据不可变的大列表,跳过Proxy也可以提升性能

这种标识只停留在根级别,markRaw对象的属性如果被reactive处理,仍然会返回一个响应式对象,并且导致原始值与Proxy值不同

const foo = markRaw({
  nested: {},
})

const bar = reactive({
  // 尽管 `foo` 己经被标记为 raw 了, 但 foo.nested 并没有
  nested: foo.nested,
})

console.log(foo.nested === bar.nested) // false

shallowReactive

只为某个对象的私有(第一层)属性创建浅层次的响应式代理,不会对深层属性做深层次、递归地响应式代理

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

// 变更 state 的自有属性是响应式的
state.foo++

// ...但不会深层代理
isReactive(state.nested) // false
state.nested.bar++ // 非响应式

shallowReadonly

shallowReactive类似,只为对象的私有(第一层)属性创建浅层的只读响应代理

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

// 改变 state 本身的 property 将失败
state.foo++

// ...但适用于嵌套对象
isReadonly(state.nested) // false
state.nested.bar++ // 适用

shallowRef

创建一个ref,将会追踪它的.value更改操作,但是不会对变更后的.value做响应式代理转换

const foo = shallowRef({})

// 更改对操作会触发响应
foo.value = {}

// 但上面新赋的这个对象并不会变为响应式对象
isReactive(foo.value) // false

注意,如果每次都为foo.value重新赋值,那么仍然会触发响应式改动。上面说的“不会变为响应式对象”指的是更改value的某个属性不会触发响应式改动

你可能感兴趣的:(Vue)