From 32cca5ede1dfa77fcd99f3caaebdbc2b5cd21be6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mr=2E=E6=A0=8B?= <103579791+pe-3@users.noreply.github.com>
Date: Fri, 3 Nov 2023 18:17:03 +0800
Subject: [PATCH] feat(react): use api of @vue/runtime-core in tiny-react
(#710)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat(react): use api of @vue/runtime-core in tiny-react
* feat(react): renderless 只执行一次的情况下,props 转化为响应式
---
examples/react-docs/src/App.tsx | 10 +-
examples/react-docs/src/main.tsx | 2 +-
packages/react-common/package.json | 3 +-
packages/react-common/src/hooks.js | 49 ++++
packages/react-common/src/index.ts | 142 ++++++-----
packages/react-common/src/vue-hooks.js | 312 ++++++++++---------------
6 files changed, 261 insertions(+), 257 deletions(-)
create mode 100644 packages/react-common/src/hooks.js
diff --git a/examples/react-docs/src/App.tsx b/examples/react-docs/src/App.tsx
index 101e93316..49c4de61c 100644
--- a/examples/react-docs/src/App.tsx
+++ b/examples/react-docs/src/App.tsx
@@ -1,14 +1,16 @@
-import { Alert } from '@opentiny/react'
+import { Button, Alert, Switch, Badge } from '@opentiny/react'
// 在这里导入组件,进行 api 调试
function App() {
+
return (
)
}
diff --git a/examples/react-docs/src/main.tsx b/examples/react-docs/src/main.tsx
index dce122516..92b474417 100644
--- a/examples/react-docs/src/main.tsx
+++ b/examples/react-docs/src/main.tsx
@@ -4,5 +4,5 @@ import App from './App.tsx'
import './main.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
-
+
)
diff --git a/packages/react-common/package.json b/packages/react-common/package.json
index c3bf8374b..4dcf6479c 100644
--- a/packages/react-common/package.json
+++ b/packages/react-common/package.json
@@ -14,6 +14,7 @@
"@opentiny/vue-theme": "workspace:~",
"classnames": "^2.3.2",
"react": "18.2.0",
- "tailwind-merge": "^1.8.0"
+ "tailwind-merge": "^1.8.0",
+ "@vue/runtime-core": "^3.3.7"
}
}
diff --git a/packages/react-common/src/hooks.js b/packages/react-common/src/hooks.js
new file mode 100644
index 000000000..fdfb20560
--- /dev/null
+++ b/packages/react-common/src/hooks.js
@@ -0,0 +1,49 @@
+import { useState, useRef, useEffect } from "react"
+import { nextTick } from '@vue/runtime-core'
+
+export function useExcuteOnce(cb, ...args) {
+ const isExcuted = useRef(false)
+ const result = useRef()
+ if (!isExcuted.current) {
+ isExcuted.current = true
+ result.current = cb(...args)
+ }
+ return result.current
+}
+
+export function useReload() {
+ const [_, reload] = useState(0)
+ return () => reload(pre => pre + 1)
+}
+
+export function useOnceResult(func, ...args) {
+ const result = useRef()
+ if (!result.current) {
+ result.current = func(...args)
+ }
+ return result.current
+}
+
+// 在这里出发生命周期钩子
+export function useVueLifeHooks($bus) {
+ $bus.emit('hook:onBeforeUpdate')
+ nextTick(() => {
+ $bus.emit('hook:onUpdated')
+ })
+
+ useExcuteOnce(() => {
+ $bus.emit('hook:onBeforeMount')
+ })
+
+ useEffect(() => {
+ $bus.emit('hook:onMounted')
+
+ return () => {
+ // 卸载
+ $bus.emit('hook:onBeforeUnmount')
+ nextTick(() => {
+ $bus.emit('hook:onUnmounted')
+ })
+ }
+ }, [])
+}
diff --git a/packages/react-common/src/index.ts b/packages/react-common/src/index.ts
index 4c1b3345b..72a2f9f16 100644
--- a/packages/react-common/src/index.ts
+++ b/packages/react-common/src/index.ts
@@ -1,29 +1,19 @@
-import * as hooks from 'react'
+import { useEffect } from 'react'
import { Svg } from './svg-render.jsx'
-
-import { nextTick, ref, computed, readonly, watch, onBeforeUnmount, inject, provide } from './vue-hooks.js'
+import { generateVueHooks } from './vue-hooks.js'
import { emit, on, off, once, emitEvent } from './event.js'
import { If, Component, Slot, For, Transition } from './virtual-comp.jsx'
-import { filterAttrs, vc, getElementCssClass } from './utils.js'
+import { filterAttrs, vc, getElementCssClass, eventBus } from './utils.js'
import { useFiber } from './fiber.js'
import { useVm } from './vm.js'
-import { useReactive } from './reactive.js'
import { twMerge } from 'tailwind-merge'
import { stringifyCssClass } from './csscls.js'
+import { useExcuteOnce, useReload, useOnceResult, useVueLifeHooks } from './hooks.js'
+// 导入 vue 响应式系统
+import { effectScope, nextTick, reactive } from '@vue/runtime-core'
import '@opentiny/vue-theme/base/index.less'
-const vue_hooks = {
- nextTick,
- ref,
- computed,
- readonly,
- watch,
- onBeforeUnmount,
- inject,
- provide
-}
-
// emitEvent, dispath, broadcast
export const $prefix = 'Tiny'
@@ -41,73 +31,99 @@ export const useSetup = ({
vm,
parent
}) => {
- const render = typeof props.tiny_renderless === 'function' ? props.tiny_renderless : renderless
- const { dispath, broadcast } = emitEvent(vm)
+ const $bus = useOnceResult(() => eventBus())
- const utils = {
- vm,
- parent,
- emit: emit(props),
- constants,
- nextTick,
- dispath,
- broadcast,
- t() { },
- mergeClass,
- mode: props.tiny_mode
- }
- const sdk = render(
- props,
- {
- ...hooks,
- useReactive,
- ...vue_hooks,
- reactive: useReactive
- },
- utils,
- extendOptions
- )
- const attrs = {
- a: filterAttrs,
- m: mergeClass,
- vm: utils.vm,
- gcls: (key) => getElementCssClass(classes, key),
- }
+ // 刷新逻辑
+ const reload = useReload()
+ useExcuteOnce(() => {
+ // 1. 响应式触发 $bus 的事件
+ // 2. 事件响应触发组件更新
+ $bus.on('event:reload', reload)
+ })
- if (Array.isArray(api)) {
- api.forEach((name) => {
- const value = sdk[name]
+ // 收集副作用,组件卸载自动清除副作用
+ const scope = useOnceResult(() => effectScope())
+ useEffect(() => {
+ return () => scope.stop()
+ }, [])
- if (typeof value !== 'undefined') {
- attrs[name] = value
- }
+ // 创建响应式 props,每次刷新更新响应式 props
+ const reactiveProps = useOnceResult(() => reactive(props))
+ Object.assign(reactiveProps, props)
+
+ // 执行一次 renderless
+ // renderless 作为 setup 的结果,最后要将结果挂在 vm 上
+ let setupResult = useExcuteOnce(() => {
+ const render = typeof reactiveProps.tiny_renderless === 'function' ? reactiveProps.tiny_renderless : renderless
+ const { dispath, broadcast } = emitEvent(vm)
+
+ const utils = {
+ vm,
+ parent,
+ emit: emit(reactiveProps),
+ constants,
+ nextTick,
+ dispath,
+ broadcast,
+ t() { },
+ mergeClass,
+ mode: reactiveProps.tiny_mode
+ }
+
+ let sdk
+ scope.run(() => {
+ sdk = render(
+ reactiveProps,
+ {
+ ...generateVueHooks({
+ $bus
+ })
+ },
+ utils,
+ extendOptions
+ )
})
- }
- return attrs
+ const attrs = {
+ a: filterAttrs,
+ m: mergeClass,
+ vm: utils.vm,
+ gcls: (key) => getElementCssClass(classes, key),
+ }
+
+ if (Array.isArray(api)) {
+ api.forEach((name) => {
+ const value = sdk[name]
+
+ if (typeof value !== 'undefined') {
+ attrs[name] = value
+ }
+ })
+ }
+
+ return attrs
+ })
+
+ useVueLifeHooks($bus)
+
+ return setupResult
}
export {
Svg,
- vc,
If,
Component,
Slot,
For,
Transition,
+ vc,
emit,
on,
off,
once,
emitEvent,
useVm,
- nextTick,
useFiber,
- ref,
- computed,
- readonly,
- useReactive,
- watch
}
-export const reactive = useReactive
+export * from './vue-hooks.js'
\ No newline at end of file
diff --git a/packages/react-common/src/vue-hooks.js b/packages/react-common/src/vue-hooks.js
index ec4bbb255..ec8b1c395 100644
--- a/packages/react-common/src/vue-hooks.js
+++ b/packages/react-common/src/vue-hooks.js
@@ -1,199 +1,135 @@
-import { useRef, useEffect } from 'react'
-import { useReload } from './reactive'
+import {
+ // 响应式:核心
+ ref,
+ computed,
+ reactive,
+ readonly,
+ watch,
+ watchEffect,
+ watchPostEffect,
+ watchSyncEffect,
+ // 响应式:工具
+ isRef,
+ unref,
+ toRef,
+ toValue,
+ toRefs,
+ isProxy,
+ isReactive,
+ isReadonly,
+ // 响应式:进阶
+ shallowRef,
+ triggerRef,
+ customRef,
+ shallowReactive,
+ shallowReadonly,
+ toRaw,
+ markRaw,
+ effectScope,
+ getCurrentScope,
+ onScopeDispose,
+ // 通用
+ nextTick
+} from '@vue/runtime-core'
-function Create(target) {
- Object.keys(target).forEach(key => {
- this[key] = target[key]
- })
-}
-function Readonly(target) {
- Create.call(this, target)
-}
+// 通用
+const inject = () => { }
+const provide = () => { }
-const useDepChange = (dependencies, immediate = false) => {
- let isDepChange = false
- const pre_dep = useRef()
- if (!pre_dep.current) {
- isDepChange = true && immediate
- }
- else {
- for (let i in dependencies) {
- if (pre_dep.current[i] !== dependencies[i]) {
- isDepChange = true
- break
- }
- }
- }
- pre_dep.current = dependencies
- return isDepChange
-}
+export function generateVueHooks({
+ $bus
+}) {
+ const reload = () => $bus.emit('event:reload')
-export const nextTick = (callback) => {
- queueMicrotask(callback)
-}
-
-export const ref = (value) => {
- const reload = useReload()
- const proxy = useRef()
- if (!proxy.current) {
- proxy.current = new Proxy({
- value
- }, {
- get(target, property) {
- if (property !== 'value') return
- return target[property]
- },
- set(target, property, newVal) {
- if (property !== 'value') return true
- target[property] = newVal
- reload()
- return true
- }
- })
- }
- return proxy.current
-}
-
-export function computed(getter) {
- const thisObj = {}
- Object.setPrototypeOf(thisObj, computed.prototype)
- if (typeof getter === 'function') {
- thisObj.get = getter
- }
- else if (typeof getter === 'object') {
- if (typeof getter.get === 'function') {
- thisObj.get = getter.get
- }
- if (typeof getter.set === 'function') {
- thisObj.set = getter.set
- }
- }
- return new Proxy({
- value: ''
- }, {
- get(
- target,
- property,
- receiver
- ) {
- if (property === 'v-hooks-type') {
- return computed
- }
- else if (property === 'value') {
- return thisObj.get()
- }
- },
- set(
- target,
- property,
- value,
- receiver
- ) {
- if (property === 'value') {
- if (typeof thisObj.set === 'function') {
- thisObj.set(value)
- }
- return true
- }
- return true
- }
- })
-}
-
-export const readonly = (target) => {
- const proxy = useRef()
- if (!proxy.current) {
- proxy.current = new Proxy(new Readonly(target), {
- get: (target, property) => target[property],
- set: () => true
- })
- }
- return proxy.current
-}
-
-export const watchEffect = (effect, dependencies, options) => {
- const cache = useRef()
- const isDepChange = useDepChange(dependencies)
- if (!cache.current) cache.current = { effect: true }
- const { flush } = options || { flust: 'pre' }
- const onCleanUp = (callback) => cache.current.clean = callback
- if (cache.current.effect && isDepChange) {
- const clean = cache.current.clean
- typeof clean === 'function' && clean()
- if (flush === 'pre') {
- effect(onCleanUp)
- }
- else if (flush === 'sync') {
- effect(onCleanUp)
- }
- else {
+ function toPageLoad(reactiveHook, reload) {
+ return function (...args) {
+ const result = reactiveHook(...args)
nextTick(() => {
- effect(onCleanUp)
+ watch(
+ result,
+ () => {
+ typeof reload === 'function' && reload()
+ },
+ {
+ flush: "sync"
+ }
+ );
})
- }
- }
- return () => cache.current.effect = false
-}
-
-export const watchPostEffect = (effect, dependencies) => watchEffect(effect, dependencies, { flush: 'post' })
-
-export const watch = (source, callback, options = {}) => {
- const cache = useRef()
- let source_value
- if (Array.isArray(source)) {
- source_value = source.map((item) => typeof item === 'function' && item())
- }
- else {
- source_value = [(typeof source === 'function' && source())]
- }
- const isDepChange = useDepChange(source_value, options.immediate)
- if (!cache.current) cache.current = { clear: false }
- if (isDepChange && !cache.current.clear) {
- callback(
- source_value.length === 1 ? source_value[0] : source_value,
- cache.current.pre
- )
- }
- cache.current.pre = source_value
- return () => cache.current.clear = true
-}
-
-const provideMap = new WeakMap()
-export const provide = (vm) => (key, value) => {
- if (!provideMap.has(vm)) {
- provideMap.set(vm, {})
- }
- const provideObj = provideMap.get(vm)
- provideObj[key] = value
-}
-
-export const inject = (_parent) => (key, defaultValue, treatDefaultAsFactory) => {
- let parent = _parent
- let context = null
-
- while (parent) {
- parent = parent.$parent
- if (provideMap.has(parent)) {
- context = provideMap.get(parent)
- break
+ return result
}
}
- let val = context && context[key]
- if (!val) {
- val = treatDefaultAsFactory ? defaultValue() : defaultValue
+ return {
+ // 响应式:核心
+ ref: toPageLoad(ref, reload),
+ computed: toPageLoad(computed, reload),
+ reactive: toPageLoad(reactive, reload),
+ readonly,
+ watchEffect,
+ watchPostEffect,
+ watchSyncEffect,
+ watch,
+ // 响应式:工具
+ isRef,
+ unref,
+ toRef: toPageLoad(toRef, reload),
+ toValue,
+ toRefs,
+ isProxy,
+ isReactive,
+ isReadonly,
+ // 响应式:进阶
+ shallowRef: toPageLoad(shallowRef, reload),
+ triggerRef,
+ customRef: toPageLoad(customRef, reload),
+ shallowReactive: toPageLoad(shallowReactive, reload),
+ shallowReadonly,
+ toRaw,
+ markRaw,
+ effectScope,
+ getCurrentScope,
+ onScopeDispose,
+ // 依赖注入
+ inject,
+ provide,
+ // 生命周期函数
+ onBeforeUnmount() {
+ $bus.on('hook:onBeforeUnmount')
+ },
+ onMounted() {
+ $bus.on('hook:onMounted')
+ },
+ onUpdated() {
+ $bus.on('hook:onUpdated')
+ },
+ onUnmounted() {
+ $bus.on('hook:onUnmounted')
+ },
+ onBeforeMount() {
+ $bus.on('hook:onBeforeMount')
+ },
+ onBeforeUpdate() {
+ $bus.on('hook:onBeforeUpdate')
+ },
+ onErrorCaptured() {
+ $bus.on('hook:onErrorCaptured')
+ },
+ onRenderTracked() {
+ $bus.on('hook:onRenderTracked')
+ },
+ onRenderTriggered() {
+ $bus.on('hook:onRenderTriggered')
+ },
+ onActivated() {
+ $bus.on('hook:onActivated')
+ },
+ onDeactivated() {
+ $bus.on('hook:onDeactivated')
+ },
+ onServerPrefetch() {
+ $bus.on('hook:onServerPrefetch')
+ }
}
- return val
}
-export const onBeforeUnmount = (callback) => {
- useEffect(() => {
- return callback
- }, [])
-}
-
-export const onMounted = (callback) => {
- useEffect(() => {
- callback()
- }, [])
-}
+export * from '@vue/runtime-core'
\ No newline at end of file