forked from opentiny/tiny-vue
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:
parent
da7748a564
commit
32cca5ede1
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -4,5 +4,5 @@ import App from './App.tsx'
|
|||
import './main.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<App />
|
||||
<App />
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
}
|
||||
}, [])
|
||||
}
|
|
@ -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'
|
|
@ -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'
|
Loading…
Reference in New Issue