Match-id-1059c254f7f934e23263fa9399c4df22bb4f49f0

This commit is contained in:
* 2021-12-23 10:07:46 +08:00 committed by *
commit 29e25bb232
19 changed files with 843 additions and 0 deletions

View File

@ -0,0 +1,71 @@
import type {VNode} from '../Types';
import type {Hook} from './HookType';
let processingVNode: VNode = null;
let activatedHook: Hook<any, any> | null = null;
// 当前hook函数对应的hook对象
let currentHook: Hook<any, any> | null = null;
export function getProcessingVNode() {
return processingVNode;
}
export function setProcessingVNode(vNode: VNode) {
processingVNode = vNode;
}
export function getActivatedHook() {
return activatedHook;
}
export function setActivatedHook(hook: Hook<any, any>) {
activatedHook = hook;
}
export function setCurrentHook(hook: Hook<any, any>) {
currentHook = hook;
}
export function throwNotInFuncError() {
throw Error(
'Hooks should be used inside function component.',
);
}
// 新建一个hook并放到vNode.hooks中
export function createHook(state: any = null): Hook<any, any> {
const newHook: Hook<any, any> = {
state: state,
hIndex: processingVNode.hooks.length,
};
currentHook = newHook;
processingVNode.hooks.push(newHook);
return currentHook;
}
export function getNextHook(hook: Hook<any, any>, vNode: VNode) {
return vNode.hooks[hook.hIndex + 1] || null;
}
// 获取当前hook函数对应的hook对象。
// processing中的hook和activated中的hook需要同时往前走
// 原因1.比对hook的数量有没有变化非必要2.从activated中的hook获取removeEffect
export function getCurrentHook(): Hook<any, any> {
currentHook = currentHook !== null ? getNextHook(currentHook, processingVNode) : (processingVNode.hooks[0] || null);
const activated = processingVNode.twins;
activatedHook = activatedHook !== null ? getNextHook(activatedHook, activated) : ((activated && activated.hooks[0]) || null);
if (currentHook === null) {
if (activatedHook === null) {
throw Error('Hooks are more than expected, please check whether the hook is written in the condition.');
}
createHook(activatedHook.state);
}
return currentHook;
}

View File

@ -0,0 +1,6 @@
export const EffectConstant = {
NoEffect: 0,
DepsChange: 1, // dependence发生了改变
LayoutEffect: 2, // 同步触发的effect
Effect: 4, // 异步触发的effect
};

View File

@ -0,0 +1,76 @@
import type {ContextType} from '../Types';
import hookMapping from './HookMapping';
import {useRefImpl} from './UseRefHook';
import {useEffectImpl, useLayoutEffectImpl} from './UseEffectHook';
import {useCallbackImpl} from './UseCallbackHook';
import {useMemoImpl} from './UseMemoHook';
import {useImperativeHandleImpl} from './UseImperativeHook';
const {
UseContextHookMapping,
UseReducerHookMapping,
UseStateHookMapping
} = hookMapping;
type BasicStateAction<S> = ((S) => S) | S;
type Dispatch<A> = (A) => void;
export function useContext<T>(
Context: ContextType<T>,
): T {
return UseContextHookMapping.val.useContext(Context);
}
export function useState<S>(initialState: (() => S) | S,): [S, Dispatch<BasicStateAction<S>>] {
return UseStateHookMapping.val.useState(initialState);
}
export function useReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: (I) => S,
): [S, Dispatch<A>] {
return UseReducerHookMapping.val.useReducer(reducer, initialArg, init);
}
export function useRef<T>(initialValue: T): {current: T} {
return useRefImpl(initialValue);
}
export function useEffect(
create: () => (() => void) | void,
deps?: Array<any> | null,
): void {
return useEffectImpl(create, deps);
}
export function useLayoutEffect(
create: () => (() => void) | void,
deps?: Array<any> | null,
): void {
return useLayoutEffectImpl(create, deps);
}
export function useCallback<T>(
callback: T,
deps?: Array<any> | null,
): T {
return useCallbackImpl(callback, deps);
}
export function useMemo<T>(
create: () => T,
deps?: Array<any> | null,
): T {
return useMemoImpl(create, deps);
}
export function useImperativeHandle<T>(
ref: {current: T | null} | ((inst: T | null) => any) | null | void,
create: () => T,
deps?: Array<any> | null,
): void {
return useImperativeHandleImpl(ref, create, deps);
}

View File

@ -0,0 +1,78 @@
import type {VNode} from '../Types';
import hookMapping from './HookMapping';
const {
UseStateHookMapping,
UseReducerHookMapping,
UseContextHookMapping,
} = hookMapping;
import {getNewContext} from '../components/context/Context';
import {
getActivatedHook,
getProcessingVNode,
setActivatedHook,
setProcessingVNode,
setCurrentHook, getNextHook
} from './BaseHook';
import {useStateImpl,} from './UseStateHook';
import {useReducerImpl,} from './UseReducerHook';
import {HookStage, setHookStage} from './HookStage';
// hook对外入口
export function exeFunctionHook(
funcComp: (props: Object, arg: Object) => any,
props: Object,
arg: Object,
activated: VNode | null,
processing: VNode,
): any {
// 重置全局变量
resetGlobalVariable();
// 初始化hook实现函数
initHookMapping();
setProcessingVNode(processing);
processing.hooks = [];
processing.effectList = [];
// 设置hook阶段
if (activated === null || !activated.hooks.length) {
setHookStage(HookStage.Init);
} else {
setHookStage(HookStage.Update);
}
let comp = funcComp(props, arg);
// 设置hook阶段为null用于判断hook是否在函数组件中调用
setHookStage(null);
// 判断hook是否写在了if条件中如果在if中会出现数量不对等的情况
const activatedHook = getActivatedHook();
if (activatedHook !== null) {
if (getNextHook(getActivatedHook(), activated) !== null) {
throw Error('Hooks are less than expected, please check whether the hook is written in the condition.');
}
}
// 重置全局变量
resetGlobalVariable();
return comp;
}
function resetGlobalVariable() {
setHookStage(null);
setProcessingVNode(null);
setActivatedHook(null);
setCurrentHook(null);
}
export function initHookMapping() {
UseContextHookMapping.val = {useContext: context => getNewContext(getProcessingVNode(), context, true)};
UseReducerHookMapping.val = {useReducer: useReducerImpl};
UseStateHookMapping.val = {useState: useStateImpl};
}

View File

@ -0,0 +1,21 @@
/**
*
*/
import type {
UseContextHookType,
UseReducerHookType,
UseStateHookType
} from '../Types';
const UseStateHookMapping: {val: (null | UseStateHookType)} = {val: null};
const UseReducerHookMapping: {val: (null | UseReducerHookType)} = {val: null};
const UseContextHookMapping: {val: (null | UseContextHookType)} = {val: null};
const hookMapping = {
UseStateHookMapping,
UseReducerHookMapping,
UseContextHookMapping,
}
export default hookMapping;

View File

@ -0,0 +1,15 @@
// hooks阶段
export enum HookStage {
Init = 1,
Update = 2,
}
let hookStage: HookStage = null;
export function getHookStage() {
return hookStage;
}
export function setHookStage(phase: HookStage) {
hookStage = phase;
}

View File

@ -0,0 +1,46 @@
import {EffectConstant} from './EffectConstant';
import {VNode} from '../Types';
export interface Hook<S, A> {
state: Reducer<S, A> | Effect | Memo<S> | CallBack<S> | Ref<S>;
hIndex: number;
}
export interface Reducer<S, A> {
stateValue: S | null;
trigger: Trigger<A> | null;
reducer: ((S, A) => S) | null;
updates: Array<Update<S, A>> | null;
isUseState: boolean;
}
export type Update<S, A> = {
action: A;
didCalculated: boolean;
state: S | null;
};
export type EffectList = Array<Effect> | null;
export type Effect = {
effect: () => (() => void) | void;
removeEffect: (() => void) | void;
dependencies: Array<any> | null;
effectConstant: typeof EffectConstant;
};
export type Memo<V> = {
result: V | null;
dependencies: Array<any> | null;
};
export type CallBack<F> = {
func: F | null;
dependencies: Array<any> | null;
};
export type Ref<V> = {
current: V | null;
};
export type Trigger<A> = (A) => void;

View File

@ -0,0 +1,32 @@
import {
createHook,
getCurrentHook,
throwNotInFuncError
} from './BaseHook';
import {getHookStage, HookStage} from './HookStage';
import {isArrayEqual} from '../utils/compare';
export function useCallbackImpl<F>(func: F, dependencies?: Array<any> | null): F {
const stage = getHookStage();
if (stage === null) {
throwNotInFuncError();
}
let hook;
const deps = dependencies !== undefined ? dependencies : null;
if (stage === HookStage.Init) {
hook = createHook();
hook.state = {func, dependencies: deps};
} else if (stage === HookStage.Update) {
hook = getCurrentHook();
const lastState = hook.state;
// 判断dependencies是否相同不同就不更新state
if (lastState !== null && deps !== null && isArrayEqual(deps, lastState.dependencies)) {
return lastState.func;
}
hook.state = {func, dependencies: deps};
}
return func;
}

View File

@ -0,0 +1,83 @@
import {
createHook,
getCurrentHook,
getActivatedHook,
getProcessingVNode, throwNotInFuncError
} from './BaseHook';
import {FlagUtils} from '../vnode/VNodeFlags';
import {EffectConstant} from './EffectConstant';
import type {Effect, EffectList} from './HookType';
import {getHookStage, HookStage} from './HookStage';
import {isArrayEqual} from '../utils/compare';
export function useEffectImpl(effectFunc: () => (() => void) | void, deps?: Array<any> | null,): void {
// 异步触发的effect
useEffect(effectFunc, deps, EffectConstant.Effect);
}
export function useLayoutEffectImpl(effectFunc: () => (() => void) | void, deps?: Array<any> | null): void {
// 同步触发的effect
useEffect(effectFunc, deps, EffectConstant.LayoutEffect);
}
function useEffect(
effectFunc: () => (() => void) | void,
deps: Array<any> | void | null,
effectType: number
): void {
const stage = getHookStage();
if (stage === null) {
throwNotInFuncError();
}
if (stage === HookStage.Init) {
return useEffectForInit(effectFunc, deps, effectType);
} else if (stage === HookStage.Update) {
return useEffectForUpdate(effectFunc, deps, effectType);
}
}
export function useEffectForInit(effectFunc, deps, effectType): void {
const hook = createHook();
const nextDeps = deps !== undefined ? deps : null;
FlagUtils.markUpdate(getProcessingVNode());
// 初始阶段设置DepsChange标记位; 构造EffectList数组并赋值给state
hook.state = createEffect(effectFunc, undefined, nextDeps, EffectConstant.DepsChange | effectType);
}
export function useEffectForUpdate(effectFunc, deps, effectType): void {
const hook = getCurrentHook();
const nextDeps = deps !== undefined ? deps : null;
let removeFunc;
if (getActivatedHook() !== null) {
const effect = getActivatedHook().state as Effect;
// removeEffect是通过执行effect返回的所以需要在activated中获取
removeFunc = effect.removeEffect;
const lastDeps = effect.dependencies;
// 判断dependencies是否相同不同就不设置DepsChange标记位
if (nextDeps !== null && isArrayEqual(nextDeps, lastDeps)) {
hook.state = createEffect(effectFunc, removeFunc, nextDeps, effectType);
return;
}
}
FlagUtils.markUpdate(getProcessingVNode());
// 设置DepsChange标记位构造Effect并赋值给state
hook.state = createEffect(effectFunc, removeFunc, nextDeps, EffectConstant.DepsChange | effectType);
}
function createEffect(effectFunc, removeFunc, deps, effectConstant): Effect {
const effect: Effect = {
effect: effectFunc,
removeEffect: removeFunc,
dependencies: deps,
effectConstant: effectConstant,
};
getProcessingVNode().effectList.push(effect);
return effect;
}

View File

@ -0,0 +1,42 @@
import {useLayoutEffectImpl} from './UseEffectHook';
import {getHookStage} from './HookStage';
import {throwNotInFuncError} from './BaseHook';
import type {Ref} from './HookType';
export function useImperativeHandleImpl<R>(
ref: { current: R | null } | ((any) => any) | null | void,
func: () => R,
dependencies?: Array<any> | null,
): void {
const stage = getHookStage();
if (stage === null) {
throwNotInFuncError();
}
const params = isNotNull(dependencies) ? dependencies.concat([ref]) : null;
useLayoutEffectImpl(effectFunc.bind(null, func, ref), params);
}
function isNotNull(object: any): boolean {
return object !== null && object !== undefined;
}
function effectFunc<R>(
func: () => R,
ref: Ref<R> | ((any) => any) | null,
): (() => void) | void {
if (typeof ref === 'function') {
const value = func();
ref(value);
return () => {
ref(null);
};
}
if (isNotNull(ref)) {
ref.current = func();
return () => {
ref.current = null;
};
}
}

View File

@ -0,0 +1,35 @@
import {
createHook,
getCurrentHook,
throwNotInFuncError
} from './BaseHook';
import {getHookStage, HookStage} from './HookStage';
import {isArrayEqual} from '../utils/compare';
export function useMemoImpl<V>(fun: () => V, deps?: Array<any> | null,): V {
const stage = getHookStage();
if (stage === null) {
throwNotInFuncError();
}
let hook;
let result;
const nextDeps = deps === undefined ? null : deps;
if (stage === HookStage.Init) {
hook = createHook();
result = fun();
} else if (stage === HookStage.Update) {
hook = getCurrentHook();
const lastState = hook.state;
// dependencies相同不更新state
if (lastState !== null && nextDeps !== null && isArrayEqual(nextDeps, lastState.dependencies)) {
return lastState.result;
}
result = fun();
}
hook.state = {result, dependencies: nextDeps};
return hook.state.result;
}

View File

@ -0,0 +1,140 @@
import type {Hook, Reducer, Trigger, Update} from './HookType';
import {
createHook,
getCurrentHook,
getProcessingVNode,
throwNotInFuncError
} from './BaseHook';
import {
launchUpdateFromVNode
} from '../TreeBuilder';
import {isSame} from '../utils/compare';
import {setStateChange} from '../render/FunctionComponent';
import {getHookStage, HookStage} from './HookStage';
import type {VNode} from '../Types';
export function useReducerImpl<S, P, A>(reducer: (S, A) => S, initArg: P, init?: (P) => S, isUseState?: boolean): [S, Trigger<A>] {
const stage = getHookStage();
if (stage === null) {
throwNotInFuncError();
}
if (stage === HookStage.Init) {
return useReducerForInit(reducer, initArg, init, isUseState);
} else if (stage === HookStage.Update) {
// 获取当前的hook
const currentHook = getCurrentHook();
// 获取currentHook的更新数组
const currentHookUpdates = (currentHook.state as Reducer<S, A>).updates;
return updateReducerHookState(currentHookUpdates, currentHook, reducer);
}
}
// 构造新的Update数组
function insertUpdate<S, A>(action: A, hook: Hook<S, A>): Update<S, A> {
const newUpdate: Update<S, A> = {
action,
state: null,
didCalculated: false,
};
let updates = (hook.state as Reducer<S, A>).updates;
// 更新updates数组newUpdate添加至数组尾部
if (updates === null) {
updates = [newUpdate];
(hook.state as Reducer<S, A>).updates = updates;
} else {
updates.push(newUpdate);
}
return newUpdate;
}
// setState, setReducer触发函数
export function TriggerAction<S, A, T>(vNode: VNode, hook: Hook<S, A>, action: A) {
const newUpdate = insertUpdate(action, hook);
const twins = vNode.twins;
// 判断是否需要刷新
if (!vNode.shouldUpdate && (twins === null || !twins.shouldUpdate)) {
const reducerObj = hook.state as Reducer<S, A>;
const { stateValue, reducer } = reducerObj;
// 在进入render阶段前reducer没有变化可以复用state值提升性能
newUpdate.state = reducer(stateValue, action);
// 标记为已经计算过,不需要重新计算了
newUpdate.didCalculated = true;
if (isSame(newUpdate.state, stateValue)) {
return;
}
}
// 执行vNode节点渲染
launchUpdateFromVNode(vNode);
}
export function useReducerForInit<S, A>(reducer, initArg, init, isUseState?: boolean): [S, Trigger<A>] {
// 计算初始stateValue
let stateValue;
if (typeof initArg === 'function') {
stateValue = initArg();
} else if (typeof init === 'function') {
stateValue = init(initArg);
} else {
stateValue = initArg;
}
const hook = createHook();
// 为hook.state赋值{状态值, 触发函数, reducer, updates更新数组, 是否是useState}
hook.state = {
stateValue: stateValue,
trigger: TriggerAction.bind(null, getProcessingVNode(), hook),
reducer,
updates: null,
isUseState
} as Reducer<S, A>;
return [hook.state.stateValue, hook.state.trigger];
}
// 更新hook.state
function updateReducerHookState<S, A>(currentHookUpdates, currentHook, reducer): [S, Trigger<A>] {
if (currentHookUpdates !== null) {
// 循环遍历更新数组,计算新的状态值
const newState = calculateNewState(currentHookUpdates, currentHook, reducer);
if (!isSame(newState, currentHook.state.stateValue)) {
setStateChange(true);
}
// 更新hook对象状态值
currentHook.state.stateValue = newState;
// 重置更新数组为null
currentHook.state.updates = null;
}
currentHook.state.reducer = reducer;
return [currentHook.state.stateValue, currentHook.state.trigger];
}
// 计算stateValue值
function calculateNewState<S, A>(currentHookUpdates: Array<Update<S, A>>, currentHook, reducer: (S, A) => S) {
let reducerObj = currentHook.state;
let state = reducerObj.stateValue;
// 循环遍历更新数组,计算新的状态值
currentHookUpdates.forEach(update => {
// 1. didCalculated = true 说明state已经计算过; 2. 如果来自 isUseState
if (update.didCalculated && reducerObj.isUseState) {
state = update.state;
} else {
const action = update.action;
state = reducer(state, action);
}
});
return state;
}

View File

@ -0,0 +1,20 @@
import {createHook, getCurrentHook, throwNotInFuncError} from './BaseHook';
import {getHookStage, HookStage} from './HookStage';
import type {Ref} from './HookType';
export function useRefImpl<V>(value: V): Ref<V> {
const stage = getHookStage();
if (stage === null) {
throwNotInFuncError();
}
let hook;
if (stage === HookStage.Init) {
hook = createHook();
hook.state = {current: value};
} else if (stage === HookStage.Update) {
hook = getCurrentHook();
}
return hook.state;
}

View File

@ -0,0 +1,11 @@
import type {Trigger} from './HookType';
import {useReducerImpl} from './UseReducerHook';
function defaultReducer<S>(state: S, action: ((S) => S) | S): S {
// @ts-ignore
return typeof action === 'function' ? action(state) : action;
}
export function useStateImpl<S>(initArg: (() => S) | S): [S, Trigger<((S) => S) | S>] {
return useReducerImpl(defaultReducer, initArg, undefined, true);
}

34
scripts/gen3rdLib.js Normal file
View File

@ -0,0 +1,34 @@
'use strict'
const ejs = require('ejs');
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');
const console = require('console');
const rimRaf = require('rimRaf');
const argv = require('minimist')(process.argv.slice(2));
const libPathPrefix = '../build';
const suffix = argv.dev ? 'development.js' : 'production.js';
const readLib = (lib) => {
const libName = lib.split('.')[0];
const libPath = path.resolve(__dirname, `${libPathPrefix}/${libName}/umd/${lib}`);
if (fs.existsSync(libPath)) {
return fs.readFileSync(libPath,'utf-8');
} else {
console.log(chalk.red(`Error: "${libPath}" 文件不存在\n先运行 npm run build`))
}
};
ejs.renderFile(path.resolve(__dirname, './template.ejs'), {
Horizon: readLib(`horizon.${suffix}`),
}, null, function(err, result) {
const common3rdLibPath = path.resolve(__dirname, `${libPathPrefix}/common3rdlib.min.js`)
rimRaf(common3rdLibPath, e => {
if (e) {
console.log(e)
}
fs.writeFileSync(common3rdLibPath, result);
console.log(chalk.green(`成功生成: ${common3rdLibPath}`))
})
});

View File

@ -0,0 +1,27 @@
'use strict';
const path = require('path');
const libPath = path.join(__dirname, '../../libs');
const baseConfig = {
entry: path.resolve(libPath, 'index.ts'),
module: {
rules: [
{
test: /\.(js)|ts$/,
exclude: /node_modules/,
use: [
{ loader: 'babel-loader' }
]
},
]
},
resolve: {
extensions: ['.js', '.ts'],
alias: {
'horizon-external': path.join(libPath, './horizon-external'),
'horizon': path.join(libPath, './horizon'),
}
},
};
module.exports = baseConfig;

View File

@ -0,0 +1,4 @@
const dev = require('./webpack.dev');
const pro = require('./webpack.pro');
module.exports = [...dev, ...pro];

View File

@ -0,0 +1,43 @@
const webpack = require('webpack');
const ESLintPlugin = require('eslint-webpack-plugin');
const baseConfig = require('./webpack.base');
const path = require('path');
const mode = 'development';
const devtool = 'inline-source-map';
const filename = 'horizon.development.js';
const plugins = [
new ESLintPlugin({fix: true}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"development"',
__DEV__: 'true',
}),
];
const umd = {
...baseConfig,
mode,
devtool,
output: {
path: path.resolve(__dirname, '../../build/horizon/umd'),
filename,
libraryTarget: 'umd',
library: 'Horizon',
},
plugins,
};
const cjs = {
...baseConfig,
mode,
devtool,
output: {
path: path.resolve(__dirname, '../../build/horizon/cjs'),
filename,
libraryTarget: 'commonjs',
},
plugins,
};
module.exports = [umd, cjs];

View File

@ -0,0 +1,59 @@
const webpack = require('webpack');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const baseConfig = require('./webpack.base');
const path = require('path');
const mode = 'production';
const devtool = 'none';
const filename = 'horizon.production.js';
const plugins = [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
__DEV__: 'false',
}),
];
const proBaseConfig = {
...baseConfig,
mode,
devtool,
plugins,
optimization: {
minimize: true
},
};
const umd = {
...proBaseConfig,
output: {
path: path.resolve(__dirname, '../../build/horizon/umd'),
filename,
libraryTarget: 'umd',
library: 'Horizon',
},
};
const cjs = {
...proBaseConfig,
output: {
path: path.resolve(__dirname, '../../build/horizon/cjs'),
filename,
libraryTarget: 'commonjs',
},
plugins: [
...plugins,
new CopyWebpackPlugin([
{
from: path.join(__dirname, '../../libs/index.js'),
to: path.join(__dirname, '../../build/horizon/index.js'),
},
{
from: path.join(__dirname, '../../libs/package.json'),
to: path.join(__dirname, '../../build/horizon/package.json'),
}
])
]
};
module.exports = [umd, cjs];