这是何等的亵渎!在 React 里面使用 Vue!
reactivue
翻 antfu 大佬的 repo 时发现了 reactivue 这个库。我将其称为我最喜爱的库排行第一!将 Vue 用在 React 里面,我感觉我整个人都混乱邪恶了起来!
import React from 'react'
import { useSetup, ref, computed, onUnmounted } from 'reactivue'
interface Props {
value: number
}
function MyCounter(Props: Props) {
const state = useSetup(
(props: Props) => { // props is a reactive object in Vue
const counter = ref(props.value)
const doubled = computed(() => counter.value * 2)
const inc = () => counter.value += 1
onUnmounted(() => console.log('Goodbye World'))
return { counter, doubled, inc }
},
Props // pass React props to it
)
// state is a plain object just like React state
const { counter, doubled, inc } = state
return (
<div>
<div>{counter} x 2 = {doubled}</div>
<button onClick={inc}>Increase</button>
</div>
)
}
正好最近想要学习一下 React ,从这个库一窥 React 与 Vue 的差异也是相当不错。
React 和 Vue 的核心差异之一
得益于 Vue reactivity system ,在 React 里面结合 Vue 的响应式系统这一部分成为可能。
要在 React 中使用 Vue 响应式系统,首先需要理解 React 和 Vue 的核心差异之一:也就是同样作为组件,在 Vue 中不止有渲染函数,而在 React 中只有渲染函数。(仅讨论 React 的 functional component)。
为了方便说明,我们分别用 Vue 和 React 写一个 Counter 的例子:
Vue
import { defineComponent, ref } from 'vue'
const Button = defineComponent({
emits: ['click'],
setup(_, { emit }) {
return () => {
return <button onClick={ $event => emit('click', $event) }>+1</button>
}
},
})
export const Counter = defineComponent({
setup() {
const count = ref(0)
const onClick = () => {
count.value++
}
return () => {
return <div><p>{ count.value }</p><Button onClick={ onClick } /></div>
}
},
})
React(由于需要考虑在复用角度和Vue等价,这里加上了 memo 等 api)
import { memo, useCallback, useEffect, useRef, useState } from "react";
const Button = memo(function Button({ onClick }: { onClick: () => void }) {
return <button onClick={onClick}>+1</button>;
});
export function Counter(): JSX.Element {
const [count, setCount] = useState(0);
const countRef = useRef<number>(count); // 这个也可以不用
useEffect(() => {
countRef.current = count;
}, [count]);
const handleClick = useCallback(() => {
setCount(countRef.current + 1);
}, []);
return (
<div>
<p>{count}</p>
<Button onClick={handleClick} />
</div>
);
}
可以看出,同样作为组件,在 Vue 中不止有渲染函数,而在 React 中只有渲染函数。
在 Vue 中:
- 声明了 setup ,类似于 class 的 new 过程。会初始化所有 ref function 等。
- 然后返回了 render 函数,也就是返回了
() => VNodeChild
。而返回的是函数,就使得返回的 render 函数天然的就可以闭包地获取上面声明的变量。 - 通过 vue 自身的响应式系统,render 可以在第一次执行后收集到它所依赖的响应式数据。就可以在这些响应式数据变更时重新执行。
而在 React 中:
- 直接返回了
JSX.Element
也就是组件本身就是一个渲染函数,也只是渲染函数。 - 是否执行该渲染函数是由上层确认的。
- 函数本身是没有状态的,所以在每次执行此渲染函数的过程中,会利用 hook 去获取藏在外部闭包中的当前组件状态,从而进行渲染。
可以这么理解,Vue 中将组件所有的属性初始化/声明提前写出来,并统一放到了setup。就像是一个使用闭包和 ender 数组成的 class 。
而 React 仅仅是 render 函数,函数本身是无法拥有状态的,所以需要借助 hook ,也就是如 setState 等利用闭包去存储当前组件的状态。或许理解为第一次渲染时,执行函数的过程中完成了所有状态数据的初始化。而后在每次重新渲染时,去获取、比较、利用 hook 存储的状态,来完成组件的渲染。
如何在 React 中使用 @vue/reactivity
思路是在 vue reactivity 数据发生变更时(使用 watch 检测),触发当前组件重新渲染。
既然 React 仅仅是一个 render 函数,那我们就模仿 setState 等方法,让 React 组件拥有一个利用闭包存储状态在外部的响应式数据!而这个生存响应式数据的函数,就是 setup
函数。
function Counter() {
const state = useSetup(() => {
const counter = ref(props.value)
const doubled = computed(() => counter.value * 2)
const inc = () => counter.value += 1
return { counter, doubled, inc }
})
const { counter, doubled, inc } = state
return (
<div>
<div>{counter} x 2 = {doubled}</div>
<button onClick={inc}>Increase</button>
</div>
)
}
简化了一下 reactivue
中的示例。useSetup
让 Counter
第一次渲染时,闭包地保存了使用 @vue/reactivity
生成的 state
。每一次 state 发生变动,将会触发这个组件的重新渲染(和一个普通的 useState 的 setter 如何触发组件重新渲染的流程相同)。而让 state 发生变动和根据 state 变动触发重新渲染的部分,则交给了 vue 的响应式系统。
下面是简化过的 reactivue
useSetup
实现。去掉了其中 lifecycle
props
传入和__DEV__
等相关功能。
export function useSetup<State extends Record<any, any>, Props = {}>(
setupFunction: (props: Props) => State,
ReactProps?: Props,
): UnwrapRef<State> {
const id = useState(getNewInstanceId)[0]
const setTick = useState(0)[1]
const createState = () => {
const instance = createNewInstanceWithId(id)
useInstanceScope(id, () => {
const setupState = setupFunction() ?? {}
const data = ref(setupState)
instance.data = data
})
return instance.data.value
}
// run setup function
const [state, setState] = useState(createState)
// trigger React re-render on data changes
useEffect(() => {
useInstanceScope(id, (instance) => {
if (!instance)
return
const { data } = instance
watch(
data,
() => {
setTick(+new Date())
},
{ deep: true, flush: 'post' },
)
})
return () => {
unmountInstance(id)
}
}, [])
return state
}
第一步是通过 useState
的特性,给当前组件赋予一个自增的 id
,如果是同一个组件,此id不会发生变化:
const id = useState(getNewInstanceId)[0]
这个 id
会用在下面的 createNewInstanceWithId
和 useInstanceScope
中,目的是生成一个和该组件伴随的 instance 来存储如 state 和生命周期相关等各种事项。
useInstanceScope
的设计让 callback 能获取到当前 instance 从而获取到当前 instance 的状态,比如处于 lifecycle 哪个阶段。
然后利用 useState 生成一个 setTick 函数,每次发生变更时 setTick(+new Date())
去触发 React 组件的 re-render。
const setTick = useState(0)[1]
通过 useState
执行 setupFunction
进行相应式数据的初始化。
这里的
setState
其实是用不着的,触发更新使用的是 setTick 。不过可以处理 hmr 导致的一些问题。
const createState = () => {
const instance = createNewInstanceWithId(id)
useInstanceScope(id, () => {
const setupState = setupFunction() ?? {}
const data = ref(setupState)
instance.data = data
})
return instance.data.value
}
const [state, setState] = useState(createState)
接下来通过 useEffect(() => { ... }, [])
去构造了一个仅执行一次的 function 。在里面 watch 了 instance.data
也就是 setupFunction
的返回值。每当 instance.data
变动时会触发 setTick
从而触发 re-render。
给 useSetup
传入 props
分为两步:
- 初始化时将 props 转为 reactive 并放到 instance 上
- 使用 useEffect 在 props 变化时,将 instance 上的 reactive props 重新赋值
具体可以看 reactivue
源码。
构造 lifecycle
vue 中有这么几种生命周期 hook:
- onBeforeMount
- onMounted
- onBeforeUpdate
- onUpdated
- onBeforeUnmount
- onUnmounted
缺少了 lifecycle 是不行的,我们必须要在组件 unmount 的时候将 vue reactivity 生成的 effects 给 stop 掉。
- onBeforeMount 在 setup function 执行后,第一次 render 返回之前。
- onMounted 利用 useEffect 的执行时机,设置在
useEffect(() => { ... }, [])
内。 - onBeforeUpdate onUpdated 设置在 setTick 周围
- onBeforeUnmount onUnmounted 利用
useEffect(() => { ... }, [])
的 cleanup , return 一个 unmout callback 在其中进行这两个生命周期的触发,设置在 effects stop 的周围。
具体可以看 reactivue
源码。
总结
用这个库,作为 Vue 用户,有种“虽然已经属于别人(React)了,但是还是爱你(Vue)的”的感觉。
React 用户会不会有种 “虽然还没离婚(和 React),但是心已经属于别人(Vue)了”的感觉呢?
这真是太刺激糟糕啦!!!
转载自:https://juejin.cn/post/7224344504776867901