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