Match-id-eafc4a5a97eef04126b53bf2f85993eec94109ef

This commit is contained in:
* 2024-03-14 09:45:09 +08:00
parent aa5ece9589
commit ee39fc8320
16 changed files with 1196 additions and 0 deletions

View File

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

View File

@ -0,0 +1,65 @@
interface CssClassObject {
[k: string]: any
}
type CssClassArray = Array<string | CssClassObject>
export type CssClass = string | CssClassObject | CssClassArray
/**
* tailwind
*
* @param cssClassObject tailwind
* @returns string
*/
const stringifyCssClassObject = (cssClassObject: CssClassObject): string => {
const allCssClass: Array<string> = []
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<string> = []
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<CssClass>): string => {
if (!cssClasses || (Array.isArray(cssClasses) && !cssClasses.length)) return ''
const allCssClass: Array<string> = []
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}')
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 (
<If v-if={v_if}>
<Icon {...defaultProps} className={className} />
</If>
)
}
})
return funcObj[name]
}

View File

@ -0,0 +1,132 @@
/**
* filterAttrs
* @param {object} attrs props
* @param {Array} filters
* @param {boolean} include falsefilters
* @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]]
}

View File

@ -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 <If v-if={defaultVIfAsTrue(props)}>
<Is className={props.className} />
</If>
}
export function Slot(props) {
const {
name = 'default',
slots = {},
parent_children
} = props
const EmptySlot = () => '';
const S = slots[name] || EmptySlot
return (<If v-if={defaultVIfAsTrue(props)}>
<If v-if={name === 'default'}>
{parent_children || props.children}
</If>
<If v-if={name !== 'default'}>
<If v-if={S !== EmptySlot}>
<S {...props} />
</If>
<If v-if={S === EmptySlot}>
{props.children}
</If>
</If>
</If>)
}
export function For(props) {
const {
item: Item,
list = []
} = props
const listItems = list.map((item, index, list) => {
return (<Item item={item} key={index} index={index} list={list} />)
})
return (<If v-if={defaultVIfAsTrue(props)}>{listItems}</If>)
}
export function Transition(props) {
const {
name
} = props
// todo: improve tarnsiton comp
return <If v-if={defaultVIfAsTrue(props)}>{props.children}</If>
}
export const compWhiteList = [
'If',
'Component',
'Slot',
'For',
'Transition'
]

View File

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

View File

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

View File

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

View File

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