0%

用最简单的方法让 React 用上 @vue/reactivity

远离 hooks 的依赖数组,使 React 更对得起他的名字。

核心

20 行不到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { effect, stop } from '@vue/reactivity'

function untrack (context) {
if (context.$$reactiveRender) {
stop(context.$$reactiveRender)
context.$$reactiveRender = null
}
}

function track (context, renderFunction) {
untrack(context)
context.$$reactiveRender = effect(renderFunction, {
lazy: true,
scheduler: () => { context.forceUpdate() }
})
return context.$$reactiveRender()
}

其中响应式组件上下文 context 长这样:

1
2
3
4
5
6
7
import type { ReactiveEffect } from '@vue/reactivity'
import type { ReactNode } from 'react'

declare interface ReactiveComponentContext {
$$reactiveRender: ReactiveEffect<ReactNode> | null
forceUpdate (callback?: () => void): void
}

核心思想就是把渲染函数用 vue 的 effect 包一层,JSX 中访问到的响应式对象会被依赖收集,有变更时自动更新组件。

Hooks 写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import * as React from 'react'
import { ref, computed } from '@vue/reactivity'

const emptyDepList = []

function useForceUpdate () {
const setState = React.useState(null)[1]
return React.useCallback(() => { setState(Object.create(null)) }, emptyDepList)
}

function useMutable (factory) {
const ref = React.useRef()
if (ref.current == null) {
const maybeObject = factory()
if ((typeof maybeObject !== 'object' || maybeObject === null) && (typeof maybeObject !== 'function')) {
throw new TypeError('useMutable callback must return object')
}
ref.current = maybeObject
}
return ref.current
}

function useReactiveContext () {
const forceUpdate = useForceUpdate()
return useMutable(() => ({
$$reactiveRender: null,
forceUpdate
}))
}

function useRender (jsxFac) {
// 响应式组件上下文
const context = useReactiveContext()

// 组件销毁取消监听
React.useEffect(() => () => { untrack(context) }, emptyDepList)

// 每次渲染重新依赖收集
return track(context, jsxFac)
}

function Counter () {
const _this = useMutable(() => {
const localCount = ref(0)
const localDoubleCount = computed(() => localCount.value * 2)
const onClick = () => {
localCount.value++
}
return {
localCount,
localDoubleCount,
onClick
}
})

return useRender(() =>
<div>{_this.localCount.value} * 2 = {_this.localDoubleCount.value} <button onClick={_this.onClick}>Local +</button></div>
)
}

Class 写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import * as React from 'react'
import { ref, computed } from '@vue/reactivity'

class Counter extends React.Component {
constructor (props) {
super(props)

// 组件实例本身当成响应式组件上下文
this.$$reactiveRender = null

this.localCount = ref(0)
this.localDoubleCount = computed(() => this.localCount.value * 2)
this.onClick = () => {
this.localCount.value++
}
}

render () {
// 每次渲染重新依赖收集
return track(this, () =>
<div>{this.localCount.value} * 2 = {this.localDoubleCount.value} <button onClick={this.onClick}>Local +</button></div>
)
}

componentWillUnmount () {
// 组件销毁取消监听
untrack(this)
}
})

仓库

reactive-react