API
observer 的实现原理是对传入的组件返回一个包裹组件(HOC),在组件内调用 useObserver 因此会额外产生一层组件。如果你正在封装组件,那么在组件内使用 useObserver 来实现响应式的同步是更直接的方式。 observer 本身是@formily/vue 为了兼容 Vue2 的实现所产生的,在 Vue3 的封装中应该优先使用 useObserver。
另外 useObserver 内部使用了 Vue3 未暴露的接口改写了内部响应式的更新方式。如果你不希望使用 hack 的方式解决响应式同步的问题,那么使用formilyComputed是更透明直接的选择,对于自己封装的组件基本都可以绕过useObserver的使用。
observer
描述
在 Vue 中,将组件渲染方法变成 Reaction,每次视图重新渲染就会收集依赖,依赖更新会自动重渲染。
签名
ts
interface IObserverOptions {
scheduler?: (updater: () => void) => void // 调度器,可以手动控制更新时机
name?: string // 包装后的组件的name
}
interface observer<T extends VueComponent> {
(component: T, options?: IObserverOptions): T
}用例
<script>
import { observable } from '@formily/reactive'
import { observer } from '@silver-formily/reactive-vue'
export default observer({
data() {
// 能与 vue 的响应系统共存
const obs = observable({
value: 'Hello world',
})
return {
obs,
}
},
})
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="(e) => {
obs.value = e.target.value
}"
>
</div>
<div>{{ obs.value }}</div>
</div>
</template>Hello world
<script>
import { observable } from '@formily/reactive'
import { observer } from '@silver-formily/reactive-vue'
export default observer({
data() {
// 能与 vue 的响应系统共存
const obs = observable({
value: 'Hello world',
})
return {
obs,
}
},
})
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="(e) => {
obs.value = e.target.value
}"
>
</div>
<div>{{ obs.value }}</div>
</div>
</template>
查看源码
useObserver 推荐
observer的内部实现,目前更推荐使用这种方式,可以减少不必要的组件包裹。
签名
ts
// 与observer一致
interface IObserverOptions {
scheduler?: (updater: () => void) => void
name?: string // 包装后的组件的name
}
interface useObserver {
(options?: IObserverOptions): void
}用例
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { useObserver } from '@silver-formily/reactive-vue'
useObserver()
const obs = observable({
value: 'Hello world',
})
function handleInput(event: Event) {
const target = event.target as HTMLInputElement | null
if (target) {
obs.value = target.value
}
}
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="handleInput"
>
</div>
<div>{{ obs.value }}</div>
</div>
</template>Hello world
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { useObserver } from '@silver-formily/reactive-vue'
useObserver()
const obs = observable({
value: 'Hello world',
})
function handleInput(event: Event) {
const target = event.target as HTMLInputElement | null
if (target) {
obs.value = target.value
}
}
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="handleInput"
>
</div>
<div>{{ obs.value }}</div>
</div>
</template>
查看源码
formilyComputed 1.0.0
将一个 @formily/reactive的响应式转为 Vue3 的响应式(ComputedRef)。在Vue3中可以完全替代 observable.computed
签名
ts
import type { IReactionOptions } from '@formily/reactive'
import type { ComputedRef } from 'vue'
// options 默认值为 { fireImmediately: true }
interface formilyComputed {
(tracker: () => T, options?: IReactionOptions): ComputedRef<T>
}用例
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { formilyComputed } from '@silver-formily/reactive-vue'
const obs = observable({
value: 'Hello formilyComputed',
})
const uppercaseValue = formilyComputed(() => obs.value.toUpperCase())
const charCount = formilyComputed(() => obs.value.length)
function handleInput(event: Event) {
const target = event.target as HTMLInputElement | null
if (target) {
obs.value = target.value
}
}
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="handleInput"
>
</div>
<div>原始:{{ obs.value }}</div>
<div>formilyComputed(大写):{{ uppercaseValue }}</div>
<div>formilyComputed(长度):{{ charCount }}</div>
</div>
</template>原始:Hello formilyComputed
formilyComputed(大写):HELLO FORMILYCOMPUTED
formilyComputed(长度):21
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { formilyComputed } from '@silver-formily/reactive-vue'
const obs = observable({
value: 'Hello formilyComputed',
})
const uppercaseValue = formilyComputed(() => obs.value.toUpperCase())
const charCount = formilyComputed(() => obs.value.length)
function handleInput(event: Event) {
const target = event.target as HTMLInputElement | null
if (target) {
obs.value = target.value
}
}
</script>
<template>
<div>
<div>
<input
:style="{
height: 28,
padding: '0 8px',
border: '2px solid #888',
borderRadius: 3,
}"
:value="obs.value"
@input="handleInput"
>
</div>
<div>原始:{{ obs.value }}</div>
<div>formilyComputed(大写):{{ uppercaseValue }}</div>
<div>formilyComputed(长度):{{ charCount }}</div>
</div>
</template>
查看源码
autorunEffect 1.1.0
将 autorun 封装成 Vue 组合式函数。它会在 setup() 或活动中的 effect scope 内立即建立订阅,并在对应作用域释放时自动调用 dispose,同时返回一个可提前手动停止的清理函数。
签名
ts
import type { Dispose, Reaction } from '@formily/reactive'
interface autorunEffect {
(tracker: Reaction, name?: string): Dispose
}用例
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { autorunEffect } from '@silver-formily/reactive-vue'
import { ref } from 'vue'
const state = observable({
count: 0,
})
const writtenCount = ref(0)
const trackedCount = ref(0)
const runCount = ref(0)
const active = ref(true)
const stop = autorunEffect(() => {
runCount.value += 1
trackedCount.value = state.count
})
function syncWrittenCount() {
writtenCount.value = state.count
}
function increment() {
state.count += 1
syncWrittenCount()
}
function addTen() {
state.count += 10
syncWrittenCount()
}
function dispose() {
if (!active.value)
return
stop()
active.value = false
}
</script>
<template>
<div>
<div class="demoToolbar">
<button class="demoButton" @click="increment">
count + 1
</button>
<button class="demoButton" @click="addTen">
count + 10
</button>
<button class="demoButton secondary" :disabled="!active" @click="dispose">
dispose
</button>
</div>
<div>手动写入值:{{ writtenCount }}</div>
<div>autorun 同步值:{{ trackedCount }}</div>
<div>autorun 执行次数:{{ runCount }}</div>
<div>订阅状态:{{ active ? 'active' : 'disposed' }}</div>
</div>
</template>
<style scoped src="./effectDemo.css"></style>手动写入值:0
autorun 同步值:0
autorun 执行次数:1
订阅状态:active
<script setup lang="ts">
import { observable } from '@formily/reactive'
import { autorunEffect } from '@silver-formily/reactive-vue'
import { ref } from 'vue'
const state = observable({
count: 0,
})
const writtenCount = ref(0)
const trackedCount = ref(0)
const runCount = ref(0)
const active = ref(true)
const stop = autorunEffect(() => {
runCount.value += 1
trackedCount.value = state.count
})
function syncWrittenCount() {
writtenCount.value = state.count
}
function increment() {
state.count += 1
syncWrittenCount()
}
function addTen() {
state.count += 10
syncWrittenCount()
}
function dispose() {
if (!active.value)
return
stop()
active.value = false
}
</script>
<template>
<div>
<div class="demoToolbar">
<button class="demoButton" @click="increment">
count + 1
</button>
<button class="demoButton" @click="addTen">
count + 10
</button>
<button class="demoButton secondary" :disabled="!active" @click="dispose">
dispose
</button>
</div>
<div>手动写入值:{{ writtenCount }}</div>
<div>autorun 同步值:{{ trackedCount }}</div>
<div>autorun 执行次数:{{ runCount }}</div>
<div>订阅状态:{{ active ? 'active' : 'disposed' }}</div>
</div>
</template>
<style scoped src="./effectDemo.css"></style>
查看源码
reactionWatch 1.1.0
将 reaction 封装成 Vue 组合式函数。和原始 API 一样支持 tracker、subscriber 与 options,差别在于它会自动注册作用域清理,避免组件卸载或作用域结束后遗留订阅。
签名
ts
import type { Dispose, IReactionOptions } from '@formily/reactive'
interface reactionWatch {
<T>(
tracker: () => T,
subscriber?: (value: T, oldValue: T) => void,
options?: IReactionOptions<T>,
): Dispose
}用例
<script setup lang="ts">
import { batch, observable } from '@formily/reactive'
import { reactionWatch } from '@silver-formily/reactive-vue'
import { ref } from 'vue'
const state = observable({
aa: 1,
bb: 2,
})
const rawAa = ref(state.aa)
const rawBb = ref(state.bb)
const trackerValue = ref(state.aa + state.bb)
const subscriberValue = ref<number | null>(null)
const subscriberRuns = ref(0)
const active = ref(true)
const stop = reactionWatch(
() => {
const next = state.aa + state.bb
trackerValue.value = next
return next
},
(next) => {
subscriberRuns.value += 1
subscriberValue.value = next
},
)
function syncRawState() {
rawAa.value = state.aa
rawBb.value = state.bb
}
function swapValues() {
batch(() => {
state.aa = 2
state.bb = 1
})
syncRawState()
}
function setAaToFour() {
state.aa = 4
syncRawState()
}
function dispose() {
if (!active.value)
return
stop()
active.value = false
}
</script>
<template>
<div>
<div class="demoToolbar">
<button class="demoButton" @click="swapValues">
batch swap
</button>
<button class="demoButton" @click="setAaToFour">
aa = 4
</button>
<button class="demoButton secondary" :disabled="!active" @click="dispose">
dispose
</button>
</div>
<div>aa / bb:{{ rawAa }} / {{ rawBb }}</div>
<div>tracker 结果:{{ trackerValue }}</div>
<div>subscriber 结果:{{ subscriberValue ?? '-' }}</div>
<div>subscriber 次数:{{ subscriberRuns }}</div>
<div>订阅状态:{{ active ? 'active' : 'disposed' }}</div>
</div>
</template>
<style scoped src="./effectDemo.css"></style>aa / bb:1 / 2
tracker 结果:3
subscriber 结果:-
subscriber 次数:0
订阅状态:active
<script setup lang="ts">
import { batch, observable } from '@formily/reactive'
import { reactionWatch } from '@silver-formily/reactive-vue'
import { ref } from 'vue'
const state = observable({
aa: 1,
bb: 2,
})
const rawAa = ref(state.aa)
const rawBb = ref(state.bb)
const trackerValue = ref(state.aa + state.bb)
const subscriberValue = ref<number | null>(null)
const subscriberRuns = ref(0)
const active = ref(true)
const stop = reactionWatch(
() => {
const next = state.aa + state.bb
trackerValue.value = next
return next
},
(next) => {
subscriberRuns.value += 1
subscriberValue.value = next
},
)
function syncRawState() {
rawAa.value = state.aa
rawBb.value = state.bb
}
function swapValues() {
batch(() => {
state.aa = 2
state.bb = 1
})
syncRawState()
}
function setAaToFour() {
state.aa = 4
syncRawState()
}
function dispose() {
if (!active.value)
return
stop()
active.value = false
}
</script>
<template>
<div>
<div class="demoToolbar">
<button class="demoButton" @click="swapValues">
batch swap
</button>
<button class="demoButton" @click="setAaToFour">
aa = 4
</button>
<button class="demoButton secondary" :disabled="!active" @click="dispose">
dispose
</button>
</div>
<div>aa / bb:{{ rawAa }} / {{ rawBb }}</div>
<div>tracker 结果:{{ trackerValue }}</div>
<div>subscriber 结果:{{ subscriberValue ?? '-' }}</div>
<div>subscriber 次数:{{ subscriberRuns }}</div>
<div>订阅状态:{{ active ? 'active' : 'disposed' }}</div>
</div>
</template>
<style scoped src="./effectDemo.css"></style>
查看源码