feat(react): use api of @vue/runtime-core in tiny-react (#710)

* feat(react): use api of @vue/runtime-core in tiny-react

* feat(react): renderless 只执行一次的情况下,props 转化为响应式
This commit is contained in:
Mr.栋 2023-11-03 18:17:03 +08:00 committed by GitHub
parent da7748a564
commit 32cca5ede1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 261 additions and 257 deletions

View File

@ -1,14 +1,16 @@
import { Alert } from '@opentiny/react'
import { Button, Alert, Switch, Badge } from '@opentiny/react'
// 在这里导入组件,进行 api 调试
function App() {
return (
<div
className='app'
>
<Alert
description='吃饭了吗'
></Alert>
<Button></Button>
<Alert description='默认提示组件'/>
<Switch/>
<Badge value={10}></Badge>
</div>
)
}

View File

@ -4,5 +4,5 @@ import App from './App.tsx'
import './main.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<App />
<App />
)

View File

@ -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"
}
}

49
packages/react-common/src/hooks.js vendored Normal file
View File

@ -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')
})
}
}, [])
}

View File

@ -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'

View File

@ -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'