From ee39fc832018de2b08346987a235fce3bf552f15 Mon Sep 17 00:00:00 2001 From: * <*> Date: Thu, 14 Mar 2024 09:45:09 +0800 Subject: [PATCH] Match-id-eafc4a5a97eef04126b53bf2f85993eec94109ef --- packages/others/react-common/package.json | 20 +++ packages/others/react-common/src/csscls.ts | 65 +++++++ packages/others/react-common/src/event.ts | 90 ++++++++++ packages/others/react-common/src/fiber.ts | 87 ++++++++++ packages/others/react-common/src/hooks.ts | 24 +++ packages/others/react-common/src/index.ts | 149 ++++++++++++++++ packages/others/react-common/src/reactive.ts | 94 +++++++++++ .../others/react-common/src/render-stack.ts | 15 ++ .../others/react-common/src/resolve-props.ts | 32 ++++ .../others/react-common/src/svg-render.jsx | 23 +++ packages/others/react-common/src/utils.ts | 132 +++++++++++++++ .../others/react-common/src/virtual-comp.jsx | 80 +++++++++ packages/others/react-common/src/vm.ts | 106 ++++++++++++ packages/others/react-common/src/vue-hooks.ts | 159 ++++++++++++++++++ .../others/react-common/src/vue-instance.ts | 85 ++++++++++ packages/others/react-common/src/vue-props.ts | 35 ++++ 16 files changed, 1196 insertions(+) create mode 100644 packages/others/react-common/package.json create mode 100644 packages/others/react-common/src/csscls.ts create mode 100644 packages/others/react-common/src/event.ts create mode 100644 packages/others/react-common/src/fiber.ts create mode 100644 packages/others/react-common/src/hooks.ts create mode 100644 packages/others/react-common/src/index.ts create mode 100644 packages/others/react-common/src/reactive.ts create mode 100644 packages/others/react-common/src/render-stack.ts create mode 100644 packages/others/react-common/src/resolve-props.ts create mode 100644 packages/others/react-common/src/svg-render.jsx create mode 100644 packages/others/react-common/src/utils.ts create mode 100644 packages/others/react-common/src/virtual-comp.jsx create mode 100644 packages/others/react-common/src/vm.ts create mode 100644 packages/others/react-common/src/vue-hooks.ts create mode 100644 packages/others/react-common/src/vue-instance.ts create mode 100644 packages/others/react-common/src/vue-props.ts diff --git a/packages/others/react-common/package.json b/packages/others/react-common/package.json new file mode 100644 index 00000000..4dcf6479 --- /dev/null +++ b/packages/others/react-common/package.json @@ -0,0 +1,20 @@ +{ + "name": "@opentiny/react-common", + "version": "1.0.0", + "description": "", + "main": "src/index.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@opentiny/vue-renderless": "workspace:~", + "@opentiny/vue-theme": "workspace:~", + "classnames": "^2.3.2", + "react": "18.2.0", + "tailwind-merge": "^1.8.0", + "@vue/runtime-core": "^3.3.7" + } +} diff --git a/packages/others/react-common/src/csscls.ts b/packages/others/react-common/src/csscls.ts new file mode 100644 index 00000000..27f274a6 --- /dev/null +++ b/packages/others/react-common/src/csscls.ts @@ -0,0 +1,65 @@ +interface CssClassObject { + [k: string]: any +} +type CssClassArray = Array +export type CssClass = string | CssClassObject | CssClassArray + +/** + * 简单合并 tailwind 类对象为字符串值 + * + * @param cssClassObject tailwind 类对象 + * @returns string + */ +const stringifyCssClassObject = (cssClassObject: CssClassObject): string => { + const allCssClass: Array = [] + + Object.keys(cssClassObject).forEach((cssClass) => cssClassObject[cssClass] && allCssClass.push(cssClass)) + + return allCssClass.join('\u{20}') +} + +/** + * 简单合并 tailwind 类数组为字符串值 + * + * @param cssClassArray tailwind 类数组 + * @returns string + */ +const stringifyCssClassArray = (cssClassArray: CssClassArray): string => { + const allCssClass: Array = [] + + cssClassArray.forEach((cssClass) => { + if (typeof cssClass === 'string') { + allCssClass.push(cssClass) + } else if (typeof cssClass === 'object') { + allCssClass.push(stringifyCssClassObject(cssClass)) + } + }) + + return allCssClass.join('\u{20}') +} + +/** + * 简单合并 tailwind 类对象为字符串值,去重处理留给 tailwind-merge 处理 + * + * @param {*} cssClasses tailwind 类集合 + * @returns string + */ +export const stringifyCssClass = (cssClasses: Array): string => { + if (!cssClasses || (Array.isArray(cssClasses) && !cssClasses.length)) return '' + + const allCssClass: Array = [] + + cssClasses.forEach((cssClass) => { + if (cssClass) { + if (typeof cssClass === 'string') { + allCssClass.push(cssClass) + } else if (Array.isArray(cssClass)) { + allCssClass.push(stringifyCssClassArray(cssClass)) + } else if (typeof cssClass === 'object') { + allCssClass.push(stringifyCssClassObject(cssClass)) + } + } + }) + + return allCssClass.join('\u{20}') +} diff --git a/packages/others/react-common/src/event.ts b/packages/others/react-common/src/event.ts new file mode 100644 index 00000000..f573d47e --- /dev/null +++ b/packages/others/react-common/src/event.ts @@ -0,0 +1,90 @@ +import { eventBus } from './utils' + +const $busMap = new Map() + +export const emit = + (props) => + (evName, ...args) => { + const reactEvName = 'on' + evName.substr(0, 1).toUpperCase() + evName.substr(1) + + if (props[reactEvName] && typeof props[reactEvName] === 'function') { + props[reactEvName](...args) + } else { + const $bus = $busMap.get(props) + if ($bus) { + $bus.emit(evName, ...args) + } + } + } +export const on = (props) => (evName, callback) => { + if ($busMap.get(props)) { + const $bus = $busMap.get(props) + $bus.on(evName, callback) + } else { + const $bus = eventBus() + $bus.on(evName, callback) + $busMap.set(props, $bus) + } +} +export const off = (props) => (evName, callback) => { + const $bus = $busMap.get(props) + if (!$bus) return + $bus.off(evName, callback) +} +export const once = (props) => (evName, callback) => { + let $bus = null + const onceCallback = (...args) => { + callback(...args) + $bus && $bus.off(evName, onceCallback) + } + + if ($busMap.get(props)) { + $bus = $busMap.get(props) + $bus.on(evName, onceCallback) + } else { + $bus = eventBus() + $bus.on(evName, onceCallback) + $busMap.set(props, $bus) + } +} +export const emitEvent = (vm) => { + const broadcast = (vm, componentName, eventName, ...args) => { + const children = vm.$children + + Array.isArray(children) && + children.forEach((child) => { + const name = child.$options && child.$options.componentName + const component = child + + if (name === componentName) { + component.emit(eventName, ...args) + // todo: 调研 component.$emitter + // component.$emitter && component.$emitter.emit(eventName, params) + } else { + broadcast(child, componentName, eventName, ...args) + } + }) + } + + return { + dispatch(componentName, eventName, ...args) { + let parent = vm.$parent + if (parent.type === null) return + let name = parent.$options && parent.$options.componentName + while (parent && parent.type && (!name || name !== componentName)) { + parent = parent.$parent + + if (parent) name = parent.$options && parent.$options.componentName + } + + if (parent) { + parent.emit(eventName, ...args) + // fix: VUE3下事件参数为数组,VUE2下事件参数不是数组,这里修改为和VUE2兼容 + // parent.$emitter && parent.$emitter.emit(...[eventName].concat(params)) + } + }, + broadcast(componentName, eventName, ...args) { + broadcast(vm, componentName, eventName, ...args) + } + } +} diff --git a/packages/others/react-common/src/fiber.ts b/packages/others/react-common/src/fiber.ts new file mode 100644 index 00000000..c0c97ce0 --- /dev/null +++ b/packages/others/react-common/src/fiber.ts @@ -0,0 +1,87 @@ +import { useRef, useEffect, useState } from 'react' +import { compWhiteList } from './virtual-comp' + +export function getFiberByDom(dom) { + const key = Object.keys(dom).find((key) => { + return ( + key.startsWith('__reactFiber$') || // react 17+ + key.startsWith('__reactInternalInstance$') + ) // react <17 + }) + + return dom[key] +} + +function defaultBreaker({ type }) { + if (type && typeof type !== 'string') { + return !compWhiteList.includes(type.name) + } +} + +export function traverseFiber(fiber, handler, breaker = defaultBreaker) { + if (!fiber) return + typeof handler === 'function' && handler(fiber) + Array.isArray(handler) && + handler.forEach((task) => { + typeof task === 'function' && task(fiber) + }) + traverseFiber(fiber.sibling, handler, breaker) + breaker(fiber) || traverseFiber(fiber.child, handler, breaker) +} + +const parentMap = new WeakMap() +export function getParentFiber(fiber, isFirst = true, child = fiber) { + if (!fiber || !fiber.return) return null + if (parentMap.has(child)) return parentMap.get(child) + if (fiber.type && typeof fiber.type !== 'string' && !isFirst) { + parentMap.set(child, fiber) + return fiber + } + return getParentFiber(fiber.return, false, fiber) +} + +export function creatFiberCombine(fiber) { + if (!fiber) return + const refs = {} + const children = [] + + traverseFiber(fiber.child, [ + (fiber) => { + if (typeof fiber.type === 'string' && fiber.stateNode.getAttribute('v_ref')) { + refs[fiber.stateNode.getAttribute('v_ref')] = fiber.stateNode + } else if (fiber.memoizedProps.v_ref) { + refs[fiber.memoizedProps.v_ref] = fiber + } + }, + (fiber) => { + if (fiber.type && typeof fiber.type !== 'string') { + children.push(fiber) + } + } + ]) + + return { + fiber, + refs, + children + } +} + +export function useFiber() { + const ref = useRef() + const [parent, setParent] = useState() + const [current, setCurrent] = useState() + useEffect(() => { + if (ref.current) { + const current_fiber = getFiberByDom(ref.current) + setParent(getParentFiber(current_fiber.return)) + setCurrent(current_fiber.return) + } + }, []) + + return { + ref, + parent: creatFiberCombine(parent), + current: creatFiberCombine(current) + } +} diff --git a/packages/others/react-common/src/hooks.ts b/packages/others/react-common/src/hooks.ts new file mode 100644 index 00000000..f116d9bc --- /dev/null +++ b/packages/others/react-common/src/hooks.ts @@ -0,0 +1,24 @@ +import { useState, useRef } from 'react' + +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 +} diff --git a/packages/others/react-common/src/index.ts b/packages/others/react-common/src/index.ts new file mode 100644 index 00000000..d598ac23 --- /dev/null +++ b/packages/others/react-common/src/index.ts @@ -0,0 +1,149 @@ +import { Svg } from './svg-render' +import { generateVueHooks, useVueLifeHooks } from './vue-hooks.js' +import { emitEvent } from './event.js' +import { If, Component, Slot, For, Transition } from './virtual-comp' +import { filterAttrs, vc, getElementCssClass, eventBus } from './utils.js' +import { useFiber } from './fiber.js' +import { useVm } from './vm.js' +import { twMerge } from 'tailwind-merge' +import { stringifyCssClass } from './csscls.js' +import { useExcuteOnce, useReload, useOnceResult } from './hooks.js' + +// 导入 vue 响应式系统 +import { effectScope, nextTick, reactive } from '@vue/runtime-core' +import { useCreateVueInstance } from './vue-instance' + +import '@opentiny/vue-theme/base/index.less' + +// emitEvent, dispath, broadcast +export const $prefix = 'Tiny' + +export const $props = { + 'tiny_mode': String, + 'tiny_mode_root': Boolean, + 'tiny_template': [Function, Object], + 'tiny_renderless': Function, + 'tiny_theme': String, + 'tiny_chart_theme': Object +} + +export const mergeClass = (...cssClasses) => twMerge(stringifyCssClass(cssClasses)) + +const setup = ({ props, renderless, api, extendOptions = {}, classes = {}, constants, vm, parent, $bus }) => { + const render = typeof props.tiny_renderless === 'function' ? props.tiny_renderless : renderless + const { dispatch, broadcast } = emitEvent(vm) + + const utils = { + vm, + parent, + emit: vm.$emit, + constants, + nextTick, + dispatch, + broadcast, + t() {}, + mergeClass, + mode: props.tiny_mode + } + + const sdk = render( + props, + { + ...generateVueHooks({ + $bus + }) + }, + utils, + extendOptions + ) + + 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 +} + +export const useSetup = ({ props, renderless, api, extendOptions = {}, classes = {}, constants }) => { + const $bus = useOnceResult(() => eventBus()) + + // 刷新逻辑 + const reload = useReload() + useExcuteOnce(() => { + // 1. 响应式触发 $bus 的事件 + // 2. 事件响应触发组件更新 + $bus.on('event:reload', reload) + }) + + // 收集副作用,组件卸载自动清除副作用 + const scope = useOnceResult(() => effectScope()) + useExcuteOnce(() => { + $bus.on('hook:onBeforeUnmount', () => scope.stop()) + }) + + // 创建响应式 props,每次刷新更新响应式 props + const reactiveProps = useOnceResult(() => reactive(props)) + Object.assign(reactiveProps, props) + + const { ref, vm } = useCreateVueInstance({ + $bus, + props + }) + + // 执行一次 renderless + // renderless 作为 setup 的结果,最后要将结果挂在 vm 上 + let setupResult = useExcuteOnce(() => { + let result + // 在 effectScope 里运行 renderless + scope.run(() => { + result = setup({ + props: reactiveProps, + renderless, + api, + constants, + extendOptions, + classes, + vm, + parent, + $bus + }) + }) + return result + }) + + // 触发生命周期 + useVueLifeHooks($bus) + + Object.keys(setupResult).forEach((key) => { + vm[key] = setupResult[key] + }) + + return { + ...setupResult, + _: { + ref, + vm + } + } +} + +export { Svg, If, Component, Slot, For, Transition, vc, emitEvent, useVm, useFiber } + +export * from './vue-hooks.js' +export * from './vue-props.js' +export * from './render-stack.js' +export * from './vue-instance.js' +export * from './hooks' diff --git a/packages/others/react-common/src/reactive.ts b/packages/others/react-common/src/reactive.ts new file mode 100644 index 00000000..206d02d1 --- /dev/null +++ b/packages/others/react-common/src/reactive.ts @@ -0,0 +1,94 @@ +import { useState, useRef } from 'react' +import { computed } from './vue-hooks' + +// 响应式核心 +const reactiveMap = new WeakMap() +const reactive = (staticObject, handler = {}, path = [], rootStaticObject = staticObject) => { + reactiveMap.has(staticObject) || + reactiveMap.set( + staticObject, + new Proxy(staticObject, { + get(target, property, receiver) { + const targetVal = target[property] + if (targetVal && targetVal['v-hooks-type'] === computed) { + return targetVal.value + } + + const _path = [...path, property] + const res = typeof targetVal === 'object' ? reactive(targetVal, handler, _path, rootStaticObject) : targetVal + + // 监听访问 + handler.get && + handler.get({ + result: res, + root: rootStaticObject, + path: _path, + target, + property, + receiver + }) + + return res + }, + set(target, property, value, receiver) { + const targetVal = target[property] + if (targetVal && targetVal['v-hooks-type'] === computed) { + targetVal.value = value + return true + } + + const _path = [...path, property] + + // 监听修改 + handler.set && + handler.set({ + target, + property, + receiver, + root: rootStaticObject, + path: _path, + newVal: value, + oldVal: target[property] + }) + + target[property] = value + return true + } + }) + ) + + return reactiveMap.get(staticObject) +} + +export const useReload = () => { + const setReload = useState(0)[1] + return () => setReload((pre) => pre + 1) +} + +const isObject = (val) => val !== null && typeof val === 'object' + +// 用于从 hooks 链表中查找 reactive 生成的 state +export function Reactive(obj) { + Object.keys(obj).forEach((key) => { + this[key] = obj[key] + }) +} + +export const useReactive = (initalObject) => { + if (!isObject(initalObject)) { + return initalObject + } + const memoried = useRef() + const proxy = useRef() + const reload = useReload() + + if (!memoried.current && !proxy.current) { + memoried.current = new Reactive(initalObject) + proxy.current = reactive(memoried.current, { + set() { + reload() + } + }) + } + return proxy.current +} diff --git a/packages/others/react-common/src/render-stack.ts b/packages/others/react-common/src/render-stack.ts new file mode 100644 index 00000000..90545446 --- /dev/null +++ b/packages/others/react-common/src/render-stack.ts @@ -0,0 +1,15 @@ +const renderStack = [] + +export const getParent = () => renderStack[renderStack.length - 1] || {} + +export const getRoot = () => renderStack[0] || {} + +export const EnterStack = (props) => { + renderStack.push(props) + return '' +} + +export const LeaveStack = () => { + renderStack.pop() + return '' +} diff --git a/packages/others/react-common/src/resolve-props.ts b/packages/others/react-common/src/resolve-props.ts new file mode 100644 index 00000000..ac7149a0 --- /dev/null +++ b/packages/others/react-common/src/resolve-props.ts @@ -0,0 +1,32 @@ +// todo: 一个方法去拿到 props 身上的事件,以 on 为前缀 +const reactEventPrefix = /^on[A-Z]/ +export function getEventByReactProps(props) { + const $listeners = {} + Object.keys(props) + .filter((propName) => { + return reactEventPrefix.test(propName) && typeof props[propName] === 'function' + }) + .map((reactEvName) => { + return { + reactEvName, + vueEvName: reactEvName.substr(2).toLowerCase() + } + }) + .forEach(({ reactEvName, vueEvName }) => { + Object.assign($listeners, { + [vueEvName]: props[reactEvName] + }) + }) + return $listeners +} +export function getAttrsByReactProps(props) { + const $attrs = {} + Object.keys(props) + .filter((propName) => { + return !reactEventPrefix.test(propName) && !['children'].includes(propName) + }) + .forEach((attr) => { + $attrs[attr] = props[attr] + }) + return $attrs +} diff --git a/packages/others/react-common/src/svg-render.jsx b/packages/others/react-common/src/svg-render.jsx new file mode 100644 index 00000000..f7f4c431 --- /dev/null +++ b/packages/others/react-common/src/svg-render.jsx @@ -0,0 +1,23 @@ +import classNames from 'classnames' +import { If } from './virtual-comp' + +export const Svg = ({ name = 'Icon', component: Icon }) => { + const funcObj = ({ + [name](props) { + const className = classNames( + 'icon', + 'tiny-svg', + props.className + ) + const v_if = typeof props['v-if'] === 'boolean' ? props['v-if'] : true + const defaultProps = { ...props } + delete defaultProps['v-if'] + return ( + + + + ) + } + }) + return funcObj[name] +} diff --git a/packages/others/react-common/src/utils.ts b/packages/others/react-common/src/utils.ts new file mode 100644 index 00000000..6ec0aa33 --- /dev/null +++ b/packages/others/react-common/src/utils.ts @@ -0,0 +1,132 @@ +/** + * filterAttrs 属性过滤函数 + * @param {object} attrs 由父组件传入,且没有被子组件声明为 props 的一些属性 + * @param {Array} filters 过滤数组,元素可以为字符串,也可以为正则表达式 + * @param {boolean} include 是否返回为被过滤的属性集合,如果为 false,filters 是过滤不要的属性 + * @returns {object} 过滤后的属性对象 + */ +export const filterAttrs = (attrs, filters, include) => { + const props = {} + + for (let name in attrs) { + const find = filters.some((r) => new RegExp(r).test(name)) + + if ((include && find) || (!include && !find)) { + props[name] = attrs[name] + } + } + + return props +} + +/** + * event bus + * $bus.on + * $bus.off + * $bus.emit + */ + +export const eventBus = () => { + const $bus = {} + + const on = (eventName, callback) => { + if (!$bus[eventName]) { + $bus[eventName] = [] + } + + $bus[eventName].push(callback) + } + + const off = (eventName, callback) => { + if (!$bus[eventName]) { + return + } + + $bus[eventName] = $bus[eventName].filter((subscriber) => subscriber !== callback) + } + + const emit = (eventName, ...args) => { + if (!$bus[eventName]) { + return + } + + $bus[eventName].forEach((subscriber) => subscriber(...args)) + } + + const once = (eventName, callback) => { + const onceCallBack = (...args) => { + callback(...args) + off(eventName, onceCallBack) + } + on(eventName, onceCallBack) + } + + return { + on, + emit, + off, + once + } +} + +/** + * 实现 vue 中 :class 的用法 + */ + +export function VueClassName(className) { + if (typeof className === 'string') { + return className + } else if (Array.isArray(className)) { + return className.reduce((pre, cur, index) => { + if (typeof cur === 'string') { + return `${pre}${index === 0 ? '' : ' '}${cur}` + } else { + return `${pre}${index === 0 ? '' : ' '}${VueClassName(cur)}` + } + }, '') + } else if (typeof className === 'object') { + return Object.keys(className).reduce((pre, key, index) => { + if (className[key]) { + return `${pre}${index === 0 ? '' : ' '}${key}` + } else { + return pre + } + }, '') + } +} + +export const vc = VueClassName + +export const getElementCssClass = (classes = {}, key) => { + if (typeof key === 'object') { + const keys = Object.keys(key) + let cls = '' + keys.forEach((k) => { + if (key[k] && classes[k]) cls += `${classes[k]} ` + }) + return cls + } else { + return classes[key] || '' + } +} + +export function getPropByPath(obj, path) { + let tempObj = obj + // 将a[b].c转换为a.b.c + path = path.replace(/\[(\w+)\]/g, '.$1') + // 将.a.b转换为a.b + path = path.replace(/^\./, '') + + let keyArr = path.split('.') + let len = keyArr.length + + for (let i = 0; i < len - 1; i++) { + let key = keyArr[i] + if (key in tempObj) { + tempObj = tempObj[key] + } else { + return + } + } + return tempObj[keyArr[keyArr.length - 1]] +} diff --git a/packages/others/react-common/src/virtual-comp.jsx b/packages/others/react-common/src/virtual-comp.jsx new file mode 100644 index 00000000..d03ac15e --- /dev/null +++ b/packages/others/react-common/src/virtual-comp.jsx @@ -0,0 +1,80 @@ +export function If(props) { + if (props['v-if']) { + return (props.children) + } + else { + return '' + } +} + +function defaultVIfAsTrue(props) { + if (typeof props === 'object' && props.hasOwnProperty('v-if')) { + return props['v-if']; + } + else { + return true + } +} + +export function Component(props) { + const Is = props.is || (() => '') + return + + +} + +export function Slot(props) { + const { + name = 'default', + slots = {}, + parent_children + } = props + + const EmptySlot = () => ''; + + const S = slots[name] || EmptySlot + + return ( + + {parent_children || props.children} + + + + + + + {props.children} + + + ) +} + +export function For(props) { + const { + item: Item, + list = [] + } = props + + const listItems = list.map((item, index, list) => { + return () + }) + + return ({listItems}) +} + +export function Transition(props) { + const { + name + } = props + + // todo: improve tarnsiton comp + return {props.children} +} + +export const compWhiteList = [ + 'If', + 'Component', + 'Slot', + 'For', + 'Transition' +] diff --git a/packages/others/react-common/src/vm.ts b/packages/others/react-common/src/vm.ts new file mode 100644 index 00000000..c9b7a980 --- /dev/null +++ b/packages/others/react-common/src/vm.ts @@ -0,0 +1,106 @@ +import { useFiber, getParentFiber, creatFiberCombine } from './fiber.js' +import { getEventByReactProps, getAttrsByReactProps } from './resolve-props.js' +import { Reactive } from './reactive.js' +import { emit, on, off, once } from './event.js' + +const vmProxy = { + $parent: ({ fiber }) => { + const parentFiber = getParentFiber(fiber) + if (!parentFiber) return null + return createVmProxy(creatFiberCombine(parentFiber)) + }, + $el: ({ fiber }) => fiber.child?.stateNode, + $refs: ({ refs, fiber }) => createRefsProxy(refs, fiber.constructor), + $children: ({ children }) => children.map((fiber) => createVmProxy(creatFiberCombine(getParentFiber(fiber)))), + $listeners: ({ fiber }) => getEventByReactProps(fiber.memoizedProps), + $attrs: ({ fiber }) => getAttrsByReactProps(fiber.memoizedProps), + $slots: ({ fiber }) => fiber.memoizedProps.slots, + $scopedSlots: ({ fiber }) => fiber.memoizedProps.slots, + $options: ({ fiber }) => ({ componentName: fiber.type.name }), + $constants: ({ fiber }) => fiber.memoizedProps._constants, + $template: ({ fiber }) => fiber.memoizedProps.tiny_template, + $renderless: ({ fiber }) => fiber.memoizedProps.tiny_renderless, + $mode: () => 'pc', + state: ({ fiber }) => findStateInHooks(fiber.memoizedState), + $type: ({ fiber }) => fiber.type, + $service: (_, vm) => vm.state?.$service, + $emit: ({ fiber }) => emit(fiber.memoizedProps), + $on: ({ fiber }) => on(fiber.memoizedProps), + $once: ({ fiber }) => once(fiber.memoizedProps), + $off: ({ fiber }) => off(fiber.memoizedProps), + $set: () => (target, propName, value) => (target[propName] = value) +} + +const vmProxyMap = new WeakMap() +function createVmProxy(fiberCombine) { + if (!vmProxyMap.has(fiberCombine)) { + vmProxyMap.set( + fiberCombine, + new Proxy(fiberCombine, { + get(target, property, receiver) { + if (!vmProxy[property]) { + return target.fiber.memoizedProps[property] + } + return vmProxy[property](target, receiver) + }, + set(target, property, value) { + return true + } + }) + ) + } + return vmProxyMap.get(fiberCombine) +} + +function createEmptyProxy() { + return new Proxy( + {}, + { + get() { + return undefined + }, + set() { + return true + } + } + ) +} + +function createRefsProxy(refs, FiberNode) { + return new Proxy(refs, { + get(target, property) { + if (target[property] instanceof FiberNode) { + return createVmProxy(creatFiberCombine(target[property])) + } else { + return target[property] + } + } + }) +} + +function findStateInHooks(hookStart) { + let curHook = hookStart + // find state from hooks chain by Constructor Reactive + while (curHook) { + const refCurrent = curHook.memoizedState && curHook.memoizedState.current + if (refCurrent instanceof Reactive) break + curHook = curHook.next + } + return curHook && curHook.memoizedState && curHook.memoizedState.current +} + +export function useVm() { + const { ref, current, parent } = useFiber() + if (!ref.current) { + return { + ref, + current: createEmptyProxy(), + parent: createEmptyProxy() + } + } + return { + ref, + current: current.fiber && createVmProxy(current), + parent: parent.fiber && createVmProxy(parent) + } +} diff --git a/packages/others/react-common/src/vue-hooks.ts b/packages/others/react-common/src/vue-hooks.ts new file mode 100644 index 00000000..1892a4ad --- /dev/null +++ b/packages/others/react-common/src/vue-hooks.ts @@ -0,0 +1,159 @@ +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' +import { useExcuteOnce } from './hooks' +import { useEffect } from 'react' + +// 通用 +const inject = () => {} +const provide = () => {} + +export function generateVueHooks({ $bus }) { + const reload = () => $bus.emit('event:reload') + + function toPageLoad(reactiveHook, reload) { + return function (...args) { + const result = reactiveHook(...args) + nextTick(() => { + watch( + result, + () => { + typeof reload === 'function' && reload() + }, + { + flush: 'sync' + } + ) + }) + return result + } + } + + 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') + } + } +} + +// 在这里出发生命周期钩子 +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') + }) + } + }, []) +} + +export * from '@vue/runtime-core' diff --git a/packages/others/react-common/src/vue-instance.ts b/packages/others/react-common/src/vue-instance.ts new file mode 100644 index 00000000..01ecaad9 --- /dev/null +++ b/packages/others/react-common/src/vue-instance.ts @@ -0,0 +1,85 @@ +import { useRef } from 'react' +import { useExcuteOnce, useOnceResult } from './hooks' +import { reactive, nextTick, watch, computed } from '@vue/runtime-core' +import { getPropByPath } from './utils' +import { getParent, getRoot } from './render-stack' +import { getFiberByDom, traverseFiber } from './fiber' + +const collectRefs = (rootEl, $children) => { + const refs = {} + if (!rootEl) return refs + const rootFiber = getFiberByDom(rootEl) + // 收集普通元素 ref + traverseFiber(rootFiber, (fiber) => { + if (typeof fiber.type === 'string' && fiber.stateNode.getAttribute('v-ref')) { + refs[fiber.stateNode.getAttribute('v-ref')] = fiber.stateNode + } + }) + // 收集组件元素 ref + $children.forEach((child) => { + if (child.$props['v-ref']) { + refs[child.$props['v-ref']] = child + } + }) + return refs +} + +export function useCreateVueInstance({ $bus, props }) { + const ref = useRef() + const vm = useOnceResult(() => + reactive({ + $el: undefined, + $options: props.$options || {}, + $props: props, + $parent: getParent().vm || {}, + $root: getRoot().vm || {}, + $slots: props.slots, + $scopedSlots: props.slots, + $listeners: props.$listeners, + $attrs: props.$attrs, + // 通过 fiber 计算 + $children: [], + $refs: computed(() => collectRefs(vm.$el, vm.$children)), + // 方法 + $set: (target, property, value) => (target[property] = value), + $delete: (target, property) => delete target[property], + $watch: (expression, callback, options) => { + if (typeof expression === 'string') { + watch(() => getPropByPath(vm, expression), callback, options) + } else if (typeof expression === 'function') { + watch(expression, callback, options) + } + }, + $on: (event, callback) => $bus.on(event, callback), + $once: (event, callback) => $bus.once(event, callback), + $off: (event, callback) => $bus.off(event, callback), + $emit: (event, ...args) => $bus.emit(event, ...args), + $forceUpdate: () => $bus.emit('event:reload'), + $nextTick: nextTick, + $destroy: () => {}, + $mount: () => {} + }) + ) + + useExcuteOnce(() => { + const { $listeners } = props + Object.keys($listeners).forEach((eventName) => { + $bus.on(eventName, $listeners[eventName]) + }) + + // 给父的 $children 里 push 当前的 vm + const parent = vm.$parent + if (Array.isArray(parent.$children)) { + parent.$children.push(vm) + } + + nextTick(() => { + vm.$el = ref.current + }) + }) + + return { + ref, + vm + } +} diff --git a/packages/others/react-common/src/vue-props.ts b/packages/others/react-common/src/vue-props.ts new file mode 100644 index 00000000..c458e23c --- /dev/null +++ b/packages/others/react-common/src/vue-props.ts @@ -0,0 +1,35 @@ +export function defineVueProps(propsOptions, props) { + const $props = {} + const $attrs = {} + const $listeners = {} + const reactEventPrefix = /^on[A-Z]/ + + const propsArray = Array.isArray(propsOptions) ? propsOptions : Object.keys(propsOptions) + Object.keys(props).forEach((key) => { + if (propsArray.includes(key)) { + $props[key] = props[key] + } else { + if (reactEventPrefix.test(key)) { + $listeners[key.substr(2).toLowerCase()] = props[key] + } else { + $attrs[key] = props[key] + } + } + }) + + if (typeof propsOptions === 'object') { + Object.keys(propsOptions) + .filter((key) => !$props[key]) + .forEach((key) => { + const options = propsOptions[key] + const defaultValue = typeof options.default === 'function' ? options.default() : options.default + defaultValue !== undefined && ($props[key] = defaultValue) + }) + } + + return { + $props, + $attrs, + $listeners + } +}