!98 inula,inulax,inula-router 累计问题修复
Merge pull request !98 from xuan/main
This commit is contained in:
commit
7076320eaa
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
### 核心能力
|
### 核心能力
|
||||||
|
|
||||||
#### 响应式API
|
#### 响应式API(实验性功能,可在reactive分支查看代码或使用npm仓中experiment版本体验)
|
||||||
|
|
||||||
* openInula 通过最小化重新渲染的范围,从而进行高效的UI渲染。这种方式避免了虚拟 DOM 的开销,使得 openInula 在性能方面表现出色。
|
* openInula 通过最小化重新渲染的范围,从而进行高效的UI渲染。这种方式避免了虚拟 DOM 的开销,使得 openInula 在性能方面表现出色。
|
||||||
* openInula 通过比较变化前后的 JavaScript 对象以细粒度的依赖追踪机制来实现响应式更新,无需用户过度关注性能优化。
|
* openInula 通过比较变化前后的 JavaScript 对象以细粒度的依赖追踪机制来实现响应式更新,无需用户过度关注性能优化。
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
import Inula from 'openinula';
|
import Inula from 'openinula';
|
||||||
import { useLayoutEffect, useRef, reduxAdapter, InulaNode } from 'openinula';
|
import { useLayoutEffect, useRef, reduxAdapter, InulaNode } from 'openinula';
|
||||||
import { connect, ReactReduxContext } from 'react-redux';
|
import { connect, ReactReduxContext } from 'react-redux';
|
||||||
import { Store } from 'redux';
|
import type { Store } from 'redux';
|
||||||
import { History, Location, Router } from '../router';
|
import { History, Location, Router } from '../router';
|
||||||
import { Action, DefaultStateType, Navigation } from '../history/types';
|
import { Action, DefaultStateType, Navigation } from '../history/types';
|
||||||
import { ActionMessage, onLocationChanged } from './actions';
|
import { ActionMessage, onLocationChanged } from './actions';
|
||||||
|
@ -130,9 +130,14 @@ function getConnectedRouter(type: StoreType) {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ConnectedHRouterWithContext = (props: any) => {
|
||||||
|
const { store, ...rest } = props;
|
||||||
|
return <ConnectedRouter store={store} storeType={type} {...rest} />;
|
||||||
|
};
|
||||||
|
|
||||||
// 针对不同的Store类型,使用对应的connect函数
|
// 针对不同的Store类型,使用对应的connect函数
|
||||||
if (type === 'InulaXCompat') {
|
if (type === 'InulaXCompat') {
|
||||||
return hConnect(null as any, mapDispatchToProps)(ConnectedRouterWithContext as any);
|
return hConnect(null as any, mapDispatchToProps)(ConnectedHRouterWithContext as any);
|
||||||
}
|
}
|
||||||
if (type === 'Redux') {
|
if (type === 'Redux') {
|
||||||
return connect(null, mapDispatchToProps)(ConnectedRouterWithContext);
|
return connect(null, mapDispatchToProps)(ConnectedRouterWithContext);
|
||||||
|
|
|
@ -107,7 +107,7 @@ export function createHashHistory<S = DefaultStateType>(option: HashHistoryOptio
|
||||||
warning(state !== undefined, 'Hash history does not support state, it will be ignored');
|
warning(state !== undefined, 'Hash history does not support state, it will be ignored');
|
||||||
|
|
||||||
const action = Action.push;
|
const action = Action.push;
|
||||||
const location = createLocation<S>(history.location, to, undefined, '');
|
const location = createLocation<S>(history.location, to, state, '');
|
||||||
|
|
||||||
transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => {
|
transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => {
|
||||||
if (!isJump) {
|
if (!isJump) {
|
||||||
|
@ -132,7 +132,7 @@ export function createHashHistory<S = DefaultStateType>(option: HashHistoryOptio
|
||||||
function replace(to: To, state?: S) {
|
function replace(to: To, state?: S) {
|
||||||
warning(state !== undefined, 'Hash history does not support state, it will be ignored');
|
warning(state !== undefined, 'Hash history does not support state, it will be ignored');
|
||||||
const action = Action.replace;
|
const action = Action.replace;
|
||||||
const location = createLocation<S>(history.location, to, undefined, '');
|
const location = createLocation<S>(history.location, to, state, '');
|
||||||
|
|
||||||
transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => {
|
transitionManager.confirmJumpTo(location, action, getUserConfirmation, isJump => {
|
||||||
if (!isJump) {
|
if (!isJump) {
|
||||||
|
|
|
@ -28,8 +28,8 @@ export function createPath(path: Partial<Path>): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parsePath(url: string): Partial<Path> {
|
export function parsePath(url: string): Partial<Path> {
|
||||||
|
let pathname = url || '/';
|
||||||
const parsedPath: Partial<Path> = {
|
const parsedPath: Partial<Path> = {
|
||||||
pathname: url || '/',
|
|
||||||
search: '',
|
search: '',
|
||||||
hash: '',
|
hash: '',
|
||||||
};
|
};
|
||||||
|
@ -38,16 +38,16 @@ export function parsePath(url: string): Partial<Path> {
|
||||||
if (hashIdx > -1) {
|
if (hashIdx > -1) {
|
||||||
const hash = url.substring(hashIdx);
|
const hash = url.substring(hashIdx);
|
||||||
parsedPath.hash = hash === '#' ? '' : hash;
|
parsedPath.hash = hash === '#' ? '' : hash;
|
||||||
url = url.substring(0, hashIdx);
|
pathname = pathname.substring(0, hashIdx);
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchIdx = url.indexOf('?');
|
const searchIdx = url.indexOf('?');
|
||||||
if (searchIdx > -1) {
|
if (searchIdx > -1) {
|
||||||
const search = url.substring(searchIdx);
|
const search = url.substring(searchIdx);
|
||||||
parsedPath.search = search === '?' ? '' : search;
|
parsedPath.search = search === '?' ? '' : search;
|
||||||
url = url.substring(0, searchIdx);
|
pathname = pathname.substring(0, searchIdx);
|
||||||
}
|
}
|
||||||
parsedPath.pathname = url;
|
parsedPath.pathname = pathname;
|
||||||
return parsedPath;
|
return parsedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,12 +116,12 @@ export function stripBasename(path: string, prefix: string): string {
|
||||||
export function createMemoryRecord<T, S>(initVal: S, fn: (arg: S) => T) {
|
export function createMemoryRecord<T, S>(initVal: S, fn: (arg: S) => T) {
|
||||||
let visitedRecord: T[] = [fn(initVal)];
|
let visitedRecord: T[] = [fn(initVal)];
|
||||||
|
|
||||||
function getDelta(to: S, form: S): number {
|
function getDelta(toKey: S, formKey: S): number {
|
||||||
let toIdx = visitedRecord.lastIndexOf(fn(to));
|
let toIdx = visitedRecord.lastIndexOf(fn(toKey));
|
||||||
if (toIdx === -1) {
|
if (toIdx === -1) {
|
||||||
toIdx = 0;
|
toIdx = 0;
|
||||||
}
|
}
|
||||||
let fromIdx = visitedRecord.lastIndexOf(fn(form));
|
let fromIdx = visitedRecord.lastIndexOf(fn(formKey));
|
||||||
if (fromIdx === -1) {
|
if (fromIdx === -1) {
|
||||||
fromIdx = 0;
|
fromIdx = 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,27 +24,42 @@ import { parsePath } from '../history/utils';
|
||||||
|
|
||||||
type NavLinkProps = {
|
type NavLinkProps = {
|
||||||
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
|
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
|
||||||
isActive?: (match: Matched | null, location: Location) => boolean;
|
isActive?<P extends { [K in keyof P]?: string }>(match: Matched<P> | null, location: Location): boolean;
|
||||||
|
exact?: boolean;
|
||||||
|
strict?: boolean;
|
||||||
|
sensitive?: boolean;
|
||||||
|
className?: string | ((isActive: boolean) => string);
|
||||||
|
activeClassName?: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
} & LinkProps;
|
} & Omit<LinkProps, 'className'>;
|
||||||
|
|
||||||
type Page = 'page';
|
type Page = 'page';
|
||||||
|
|
||||||
function NavLink<P extends NavLinkProps>(props: P) {
|
function NavLink<P extends NavLinkProps>(props: P) {
|
||||||
const { to, isActive, ...rest } = props;
|
const { to, isActive, exact, strict, sensitive, className, activeClassName, ...rest } = props;
|
||||||
const context = useContext(Context);
|
const context = useContext(Context);
|
||||||
|
|
||||||
const toLocation = typeof to === 'function' ? to(context.location) : to;
|
const toLocation = typeof to === 'function' ? to(context.location) : to;
|
||||||
|
|
||||||
const { pathname } = typeof toLocation === 'string' ? parsePath(toLocation) : toLocation;
|
const { pathname } = typeof toLocation === 'string' ? parsePath(toLocation) : toLocation;
|
||||||
|
|
||||||
const match = pathname ? matchPath(context.location.pathname, pathname) : null;
|
const match = pathname ? matchPath(context.location.pathname, pathname, {
|
||||||
|
exact: exact,
|
||||||
|
strictMode: strict,
|
||||||
|
caseSensitive: sensitive,
|
||||||
|
}) : null;
|
||||||
|
|
||||||
const isLinkActive = match && isActive ? isActive(match, context.location) : false;
|
const isLinkActive = !!(isActive ? isActive(match, context.location) : match);
|
||||||
|
|
||||||
|
let classNames = typeof className === 'function' ? className(isLinkActive) : className;
|
||||||
|
if (isLinkActive) {
|
||||||
|
classNames = [activeClassName, classNames].filter(Boolean).join('');
|
||||||
|
}
|
||||||
|
|
||||||
const page: Page = 'page';
|
const page: Page = 'page';
|
||||||
const otherProps = {
|
const otherProps = {
|
||||||
'aria-current': isLinkActive ? page : false,
|
className: classNames,
|
||||||
|
'aria-current': isLinkActive ? page : undefined,
|
||||||
...rest,
|
...rest,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -43,15 +43,19 @@ export type RouteProps<P extends Record<string, any> = {}, Path extends string =
|
||||||
function Route<Path extends string, P extends Record<string, any> = GetURLParams<Path>>(props: RouteProps<P, Path>) {
|
function Route<Path extends string, P extends Record<string, any> = GetURLParams<Path>>(props: RouteProps<P, Path>) {
|
||||||
const context = useContext(RouterContext);
|
const context = useContext(RouterContext);
|
||||||
|
|
||||||
const { computed, location, path } = props;
|
const { computed, location, path, component, render, strict, sensitive, exact } = props;
|
||||||
let { children, component, render } = props;
|
let { children } = props;
|
||||||
let match: Matched<P> | null;
|
let match: Matched<P> | null;
|
||||||
|
|
||||||
const routeLocation = location || context.location;
|
const routeLocation = location || context.location;
|
||||||
if (computed) {
|
if (computed) {
|
||||||
match = computed;
|
match = computed;
|
||||||
} else if (path) {
|
} else if (path) {
|
||||||
match = matchPath<P>(routeLocation.pathname, path);
|
match = matchPath<P>(routeLocation.pathname, path, {
|
||||||
|
strictMode: strict,
|
||||||
|
caseSensitive: sensitive,
|
||||||
|
exact: exact,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
match = context.match;
|
match = context.match;
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,22 +29,27 @@ function Router<P extends RouterProps>(props: P) {
|
||||||
const { history, children = null } = props;
|
const { history, children = null } = props;
|
||||||
const [location, setLocation] = useState(props.history.location);
|
const [location, setLocation] = useState(props.history.location);
|
||||||
const pendingLocation = useRef<Location | null>(null);
|
const pendingLocation = useRef<Location | null>(null);
|
||||||
|
const unListen = useRef<null | (() => void)>(null);
|
||||||
|
const isMount = useRef<boolean>(false);
|
||||||
|
|
||||||
// 在Router加载时就监听history地址变化,以保证在始渲染时重定向能正确触发
|
// 在Router加载时就监听history地址变化,以保证在始渲染时重定向能正确触发
|
||||||
const unListen = useRef<null | (() => void)>(
|
if (unListen.current === null) {
|
||||||
history.listen(arg => {
|
unListen.current = history.listen(arg => {
|
||||||
pendingLocation.current = arg.location;
|
pendingLocation.current = arg.location;
|
||||||
}),
|
});
|
||||||
);
|
}
|
||||||
|
|
||||||
// 模拟componentDidMount和componentWillUnmount
|
// 模拟componentDidMount和componentWillUnmount
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
isMount.current = true;
|
||||||
if (unListen.current) {
|
if (unListen.current) {
|
||||||
unListen.current();
|
unListen.current();
|
||||||
}
|
}
|
||||||
// 监听history中的位置变化
|
// 监听history中的位置变化
|
||||||
unListen.current = history.listen(arg => {
|
unListen.current = history.listen(arg => {
|
||||||
setLocation(arg.location);
|
if (isMount.current) {
|
||||||
|
setLocation(arg.location);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (pendingLocation.current) {
|
if (pendingLocation.current) {
|
||||||
|
@ -53,6 +58,7 @@ function Router<P extends RouterProps>(props: P) {
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (unListen.current) {
|
if (unListen.current) {
|
||||||
|
isMount.current = false;
|
||||||
unListen.current();
|
unListen.current();
|
||||||
unListen.current = null;
|
unListen.current = null;
|
||||||
pendingLocation.current = null;
|
pendingLocation.current = null;
|
||||||
|
|
|
@ -40,7 +40,7 @@ export type Matched<P = any> = {
|
||||||
|
|
||||||
const defaultOption: Required<ParserOption> = {
|
const defaultOption: Required<ParserOption> = {
|
||||||
// url匹配时是否大小写敏感
|
// url匹配时是否大小写敏感
|
||||||
caseSensitive: true,
|
caseSensitive: false,
|
||||||
// 是否严格匹配url结尾的/
|
// 是否严格匹配url结尾的/
|
||||||
strictMode: false,
|
strictMode: false,
|
||||||
// 是否完全精确匹配
|
// 是否完全精确匹配
|
||||||
|
|
|
@ -20,10 +20,11 @@ import RouterContext from './context';
|
||||||
function withRouter<C extends ComponentType>(Component: C) {
|
function withRouter<C extends ComponentType>(Component: C) {
|
||||||
|
|
||||||
function ComponentWithRouterProp(props: any) {
|
function ComponentWithRouterProp(props: any) {
|
||||||
|
const { wrappedComponentRef, ...rest } = props;
|
||||||
const { history, location, match } = useContext(RouterContext);
|
const { history, location, match } = useContext(RouterContext);
|
||||||
const routeProps = { history: history, location: location, match: match };
|
const routeProps = { history: history, location: location, match: match };
|
||||||
|
|
||||||
return <Component {...props} {...routeProps} />;
|
return <Component {...routeProps} {...rest} ref={wrappedComponentRef} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ComponentWithRouterProp;
|
return ComponentWithRouterProp;
|
||||||
|
|
|
@ -24,5 +24,13 @@
|
||||||
"watch-test": "yarn test --watch --dev"
|
"watch-test": "yarn test --watch --dev"
|
||||||
},
|
},
|
||||||
"files": ["build/**/*", "README.md"],
|
"files": ["build/**/*", "README.md"],
|
||||||
"types": "./build/@types/index.d.ts"
|
"types": "./build/@types/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"default": "./index.js"
|
||||||
|
},
|
||||||
|
"./package.json":"./package.json",
|
||||||
|
"./jsx-runtime": "./jsx-runtime.js",
|
||||||
|
"./jsx-dev-runtime": "./jsx-dev-runtime.js"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -473,4 +473,61 @@ describe('Fragment', () => {
|
||||||
expect(LogUtils.getNotClear()).toEqual([]);
|
expect(LogUtils.getNotClear()).toEqual([]);
|
||||||
expect(container.textContent).toBe('1');
|
expect(container.textContent).toBe('1');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Fragment 设置相同的key不会重新渲染组件', () => {
|
||||||
|
const { useState, useRef, act, Fragment } = Inula;
|
||||||
|
let setFn;
|
||||||
|
const didMount = jest.fn();
|
||||||
|
const didUpdate = jest.fn();
|
||||||
|
|
||||||
|
const App = () => {
|
||||||
|
const [list, setList] = useState([
|
||||||
|
{ text: 'Apple', id: 1 },
|
||||||
|
{ text: 'Banana', id: 2 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
setFn = setList;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{list.map(item => {
|
||||||
|
return (
|
||||||
|
<Fragment key={item.id}>
|
||||||
|
<Child val={item.text} />
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Child = ({ val }) => {
|
||||||
|
const mount = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mount.current) {
|
||||||
|
didMount();
|
||||||
|
mount.current = true;
|
||||||
|
} else {
|
||||||
|
didUpdate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div>{val}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
act(() => Inula.render(<App />, container));
|
||||||
|
|
||||||
|
expect(didMount).toHaveBeenCalledTimes(2);
|
||||||
|
act(() => {
|
||||||
|
setFn([
|
||||||
|
{ text: 'Apple', id: 1 },
|
||||||
|
{ text: 'Banana', id: 2 },
|
||||||
|
{ text: 'Grape', id: 3 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 数组前两项Key不变子组件更新,第三个子组件会挂载
|
||||||
|
expect(didMount).toHaveBeenCalledTimes(3);
|
||||||
|
expect(didUpdate).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Inula from '../../../src/index';
|
import * as Inula from '../../../src/index';
|
||||||
const { useState } = Inula;
|
|
||||||
describe('FunctionComponent Test', () => {
|
describe('FunctionComponent Test', () => {
|
||||||
it('渲染无状态组件', () => {
|
it('渲染无状态组件', () => {
|
||||||
const App = props => {
|
const App = props => {
|
||||||
|
@ -90,39 +89,4 @@ describe('FunctionComponent Test', () => {
|
||||||
Inula.render(<App />, container);
|
Inula.render(<App />, container);
|
||||||
expect(container.querySelector('div').style['_values']['--max-segment-num']).toBe(10);
|
expect(container.querySelector('div').style['_values']['--max-segment-num']).toBe(10);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('函数组件渲染中重新发生setState不触发整个应用重新更新', () => {
|
|
||||||
function CountLabel({ count }) {
|
|
||||||
const [prevCount, setPrevCount] = useState(count);
|
|
||||||
const [trend, setTrend] = useState(null);
|
|
||||||
if (prevCount !== count) {
|
|
||||||
setPrevCount(count);
|
|
||||||
setTrend(count > prevCount ? 'increasing' : 'decreasing');
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>{count}</h1>
|
|
||||||
<Child trend={trend} />
|
|
||||||
{trend && <p>The count is {trend}</p>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let count = 0;
|
|
||||||
function Child({ trend }) {
|
|
||||||
count++;
|
|
||||||
return <div>{trend}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let update;
|
|
||||||
function App() {
|
|
||||||
const [count, setCount] = useState(0);
|
|
||||||
update = setCount;
|
|
||||||
return <CountLabel count={count} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
Inula.render(<App />, container);
|
|
||||||
update(1);
|
|
||||||
expect(count).toBe(2);
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||||
|
*
|
||||||
|
* openInula is licensed under Mulan PSL v2.
|
||||||
|
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||||
|
* You may obtain a copy of Mulan PSL v2 at:
|
||||||
|
*
|
||||||
|
* http://license.coscl.org.cn/MulanPSL2
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||||
|
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||||
|
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||||
|
* See the Mulan PSL v2 for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Inula from '../../../src/index';
|
||||||
|
import { getLogUtils } from '../jest/testUtils';
|
||||||
|
|
||||||
|
describe('StrictMode Component test', () => {
|
||||||
|
const LogUtils = getLogUtils();
|
||||||
|
const { useState, useEffect, useRef, render, act } = Inula;
|
||||||
|
it('StrictMode is same to Fragment', () => {
|
||||||
|
const Parent = () => {
|
||||||
|
const [, setS] = useState('1');
|
||||||
|
return (
|
||||||
|
<Inula.StrictMode>
|
||||||
|
<button id="btn" onClick={() => setS(prevState => prevState + '!')}>
|
||||||
|
Click
|
||||||
|
</button>
|
||||||
|
<Child />
|
||||||
|
</Inula.StrictMode>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Child = () => {
|
||||||
|
const isMount = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMount.current) {
|
||||||
|
LogUtils.log('didMount');
|
||||||
|
isMount.current = true;
|
||||||
|
} else {
|
||||||
|
LogUtils.log('didUpdate');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
act(() => render(<Parent />, container));
|
||||||
|
// 子组件初始化,会挂载一次
|
||||||
|
expect(LogUtils.getAndClear()).toStrictEqual(['didMount']);
|
||||||
|
const button = container.querySelector('#btn');
|
||||||
|
// 父组件State更新,子组件也会更新一次
|
||||||
|
act(() => button.click());
|
||||||
|
expect(LogUtils.getAndClear()).toStrictEqual(['didUpdate']);
|
||||||
|
});
|
||||||
|
});
|
|
@ -21,6 +21,7 @@ import { findDOMByClassInst } from '../renderer/vnode/VNodeUtils';
|
||||||
import { listenSimulatedDelegatedEvents } from '../event/EventBinding';
|
import { listenSimulatedDelegatedEvents } from '../event/EventBinding';
|
||||||
import { Callback } from '../renderer/Types';
|
import { Callback } from '../renderer/Types';
|
||||||
import { InulaNode } from '../types';
|
import { InulaNode } from '../types';
|
||||||
|
import { EVENT_KEY } from './DOMInternalKeys';
|
||||||
|
|
||||||
function createRoot(children: any, container: Container, callback?: Callback) {
|
function createRoot(children: any, container: Container, callback?: Callback) {
|
||||||
// 清空容器
|
// 清空容器
|
||||||
|
@ -89,7 +90,7 @@ function findDOMNode(domOrEle?: Element): null | Element | Text {
|
||||||
|
|
||||||
// 情况根节点监听器
|
// 情况根节点监听器
|
||||||
function removeRootEventLister(container: Container) {
|
function removeRootEventLister(container: Container) {
|
||||||
const events = (container as any).$EV;
|
const events = (container as any)[EVENT_KEY];
|
||||||
if (events) {
|
if (events) {
|
||||||
Object.keys(events).forEach(event => {
|
Object.keys(events).forEach(event => {
|
||||||
const listener = events[event];
|
const listener = events[event];
|
||||||
|
|
|
@ -22,9 +22,12 @@ import type { Container, Props } from './DOMOperator';
|
||||||
|
|
||||||
import { DomComponent, DomText, TreeRoot } from '../renderer/vnode/VNodeTags';
|
import { DomComponent, DomText, TreeRoot } from '../renderer/vnode/VNodeTags';
|
||||||
|
|
||||||
const INTERNAL_VNODE = '_inula_VNode';
|
const randomKey = Math.random().toString(16).slice(2);
|
||||||
const INTERNAL_PROPS = '_inula_Props';
|
const INTERNAL_VNODE = `_inula_VNode_${randomKey}`;
|
||||||
const INTERNAL_NONDELEGATEEVENTS = '_inula_NonDelegatedEvents';
|
const INTERNAL_PROPS = `_inula_Props_${randomKey}`;
|
||||||
|
const INTERNAL_NONDELEGATEEVENTS = `_inula_nonDelegatedEvents_${randomKey}`;
|
||||||
|
export const HANDLER_KEY = `_inula_valueChangeHandler_${randomKey}`;
|
||||||
|
export const EVENT_KEY = `_inula_ev_${randomKey}`;
|
||||||
|
|
||||||
// 通过 VNode 实例获取 DOM 节点
|
// 通过 VNode 实例获取 DOM 节点
|
||||||
export function getDom(vNode: VNode): Element | Text | null {
|
export function getDom(vNode: VNode): Element | Text | null {
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
* 只有值发生变化时才会触发change事件。
|
* 只有值发生变化时才会触发change事件。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const HANDLER_KEY = '_valueChangeHandler';
|
import { HANDLER_KEY } from '../DOMInternalKeys';
|
||||||
|
|
||||||
// 判断是否是 check 类型
|
// 判断是否是 check 类型
|
||||||
function isCheckType(dom: HTMLInputElement): boolean {
|
function isCheckType(dom: HTMLInputElement): boolean {
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
*/
|
*/
|
||||||
import { allDelegatedInulaEvents, portalDefaultDelegatedEvents, simulatedDelegatedEvents } from './EventHub';
|
import { allDelegatedInulaEvents, portalDefaultDelegatedEvents, simulatedDelegatedEvents } from './EventHub';
|
||||||
import { isDocument } from '../dom/utils/Common';
|
import { isDocument } from '../dom/utils/Common';
|
||||||
import { getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys';
|
import { EVENT_KEY, getNearestVNode, getNonDelegatedListenerMap } from '../dom/DOMInternalKeys';
|
||||||
import { asyncUpdates, runDiscreteUpdates } from '../renderer/TreeBuilder';
|
import { asyncUpdates, runDiscreteUpdates } from '../renderer/TreeBuilder';
|
||||||
import { handleEventMain } from './InulaEventMain';
|
import { handleEventMain } from './InulaEventMain';
|
||||||
import { decorateNativeEvent } from './EventWrapper';
|
import { decorateNativeEvent } from './EventWrapper';
|
||||||
|
@ -73,8 +73,8 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) {
|
||||||
const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent;
|
const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent;
|
||||||
|
|
||||||
// 事件存储在DOM节点属性,避免多个VNode(root和portal)对应同一个DOM, 造成事件重复监听
|
// 事件存储在DOM节点属性,避免多个VNode(root和portal)对应同一个DOM, 造成事件重复监听
|
||||||
currentRoot.realNode.$EV = currentRoot.realNode.$EV ?? {};
|
currentRoot.realNode[EVENT_KEY] = currentRoot.realNode[EVENT_KEY] ?? {};
|
||||||
const events = currentRoot.realNode.$EV;
|
const events = currentRoot.realNode[EVENT_KEY];
|
||||||
|
|
||||||
if (!events[nativeFullName]) {
|
if (!events[nativeFullName]) {
|
||||||
events[nativeFullName] = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture);
|
events[nativeFullName] = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture);
|
||||||
|
|
|
@ -142,7 +142,13 @@ function triggerInulaEvents(
|
||||||
const target = nativeEvent.target || nativeEvent.srcElement!;
|
const target = nativeEvent.target || nativeEvent.srcElement!;
|
||||||
|
|
||||||
// 触发普通委托事件
|
// 触发普通委托事件
|
||||||
const listenerList: ListenerUnitList = getCommonListeners(nativeEvtName, vNode, nativeEvent, target, isCapture);
|
const listenerList: ListenerUnitList = getCommonListeners(
|
||||||
|
nativeEvtName,
|
||||||
|
vNode,
|
||||||
|
nativeEvent as MouseEvent,
|
||||||
|
target,
|
||||||
|
isCapture
|
||||||
|
);
|
||||||
|
|
||||||
let mouseEnterListeners: ListenerUnitList = [];
|
let mouseEnterListeners: ListenerUnitList = [];
|
||||||
if (inulaEventToNativeMap.get('onMouseEnter')!.includes(nativeEvtName)) {
|
if (inulaEventToNativeMap.get('onMouseEnter')!.includes(nativeEvtName)) {
|
||||||
|
|
|
@ -91,6 +91,10 @@ function mergeData(state, data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: StoreEnhancer): ReduxStoreHandler {
|
export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: StoreEnhancer): ReduxStoreHandler {
|
||||||
|
if (typeof preloadedState === 'function' && typeof enhancers === 'undefined') {
|
||||||
|
enhancers = preloadedState;
|
||||||
|
preloadedState = undefined;
|
||||||
|
}
|
||||||
const store = createStoreX({
|
const store = createStoreX({
|
||||||
id: 'defaultStore',
|
id: 'defaultStore',
|
||||||
state: { stateWrapper: preloadedState },
|
state: { stateWrapper: preloadedState },
|
||||||
|
@ -107,6 +111,7 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?:
|
||||||
return;
|
return;
|
||||||
} // NOTE: reducer should never return undefined, in this case, do not change state
|
} // NOTE: reducer should never return undefined, in this case, do not change state
|
||||||
state.stateWrapper = result;
|
state.stateWrapper = result;
|
||||||
|
return action;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
import type { VNode } from '../Types';
|
import type { VNode } from '../Types';
|
||||||
import { FlagUtils } from '../vnode/VNodeFlags';
|
import { FlagUtils } from '../vnode/VNodeFlags';
|
||||||
import { TYPE_COMMON_ELEMENT, TYPE_FRAGMENT, TYPE_PORTAL } from '../../external/JSXElementType';
|
import { TYPE_COMMON_ELEMENT, TYPE_FRAGMENT, TYPE_PORTAL, TYPE_STRICT_MODE } from '../../external/JSXElementType';
|
||||||
import { DomText, DomPortal, Fragment, DomComponent } from '../vnode/VNodeTags';
|
import { DomText, DomPortal, Fragment, DomComponent } from '../vnode/VNodeTags';
|
||||||
import {
|
import {
|
||||||
updateVNode,
|
updateVNode,
|
||||||
|
@ -35,9 +35,9 @@ enum DiffCategory {
|
||||||
ARR_NODE = 'ARR_NODE',
|
ARR_NODE = 'ARR_NODE',
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是不是被 FRAGMENT 包裹
|
// 检查是不是被 FRAGMENT 或 StrictMode 包裹
|
||||||
function isNoKeyFragment(child: any) {
|
function isNoKeyFragmentOrStrictMode(child: any) {
|
||||||
return child != null && child.type === TYPE_FRAGMENT && child.key === null;
|
return child != null && (child.type === TYPE_FRAGMENT || child.type === TYPE_STRICT_MODE) && child.key === null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清除单个节点
|
// 清除单个节点
|
||||||
|
@ -159,7 +159,7 @@ function getNewNode(parentNode: VNode, newChild: any, oldNode: VNode | null) {
|
||||||
const key = oldNode !== null ? oldNode.key : newChild.key;
|
const key = oldNode !== null ? oldNode.key : newChild.key;
|
||||||
resultNode = createFragmentVNode(key, newChild.props.children);
|
resultNode = createFragmentVNode(key, newChild.props.children);
|
||||||
} else {
|
} else {
|
||||||
resultNode = updateVNode(oldNode, newChild);
|
resultNode = updateVNode(oldNode, newChild.props.children);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -631,7 +631,7 @@ export function createChildrenByDiff(
|
||||||
newChild: any,
|
newChild: any,
|
||||||
isComparing: boolean
|
isComparing: boolean
|
||||||
): VNode | null {
|
): VNode | null {
|
||||||
const isFragment = isNoKeyFragment(newChild);
|
const isFragment = isNoKeyFragmentOrStrictMode(newChild);
|
||||||
newChild = isFragment ? newChild.props.children : newChild;
|
newChild = isFragment ? newChild.props.children : newChild;
|
||||||
|
|
||||||
// 1. 没有新节点,直接把vNode标记为删除
|
// 1. 没有新节点,直接把vNode标记为删除
|
||||||
|
|
|
@ -18,19 +18,12 @@ import type { VNode } from '../Types';
|
||||||
import { getLastTimeHook, setLastTimeHook, setCurrentHook, getNextHook } from './BaseHook';
|
import { getLastTimeHook, setLastTimeHook, setCurrentHook, getNextHook } from './BaseHook';
|
||||||
import { HookStage, setHookStage } from './HookStage';
|
import { HookStage, setHookStage } from './HookStage';
|
||||||
|
|
||||||
const NESTED_UPDATE_LIMIT = 50;
|
|
||||||
// state updated in render phrase
|
|
||||||
let hasUpdatedInRender = false;
|
|
||||||
function resetGlobalVariable() {
|
function resetGlobalVariable() {
|
||||||
setHookStage(null);
|
setHookStage(null);
|
||||||
setLastTimeHook(null);
|
setLastTimeHook(null);
|
||||||
setCurrentHook(null);
|
setCurrentHook(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markUpdatedInRender() {
|
|
||||||
hasUpdatedInRender = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// hook对外入口
|
// hook对外入口
|
||||||
export function runFunctionWithHooks<Props extends Record<string, any>, Arg>(
|
export function runFunctionWithHooks<Props extends Record<string, any>, Arg>(
|
||||||
funcComp: (props: Props, arg: Arg) => any,
|
funcComp: (props: Props, arg: Arg) => any,
|
||||||
|
@ -52,14 +45,8 @@ export function runFunctionWithHooks<Props extends Record<string, any>, Arg>(
|
||||||
setHookStage(HookStage.Update);
|
setHookStage(HookStage.Update);
|
||||||
}
|
}
|
||||||
|
|
||||||
let comp = funcComp(props, arg);
|
const comp = funcComp(props, arg);
|
||||||
|
|
||||||
if (hasUpdatedInRender) {
|
|
||||||
resetGlobalVariable();
|
|
||||||
processing.oldHooks = processing.hooks;
|
|
||||||
setHookStage(HookStage.Update);
|
|
||||||
comp = runFunctionAgain(funcComp, props, arg);
|
|
||||||
}
|
|
||||||
// 设置hook阶段为null,用于判断hook是否在函数组件中调用
|
// 设置hook阶段为null,用于判断hook是否在函数组件中调用
|
||||||
setHookStage(null);
|
setHookStage(null);
|
||||||
|
|
||||||
|
@ -76,22 +63,3 @@ export function runFunctionWithHooks<Props extends Record<string, any>, Arg>(
|
||||||
|
|
||||||
return comp;
|
return comp;
|
||||||
}
|
}
|
||||||
|
|
||||||
function runFunctionAgain<Props extends Record<string, any>, Arg>(
|
|
||||||
funcComp: (props: Props, arg: Arg) => any,
|
|
||||||
props: Props,
|
|
||||||
arg: Arg
|
|
||||||
) {
|
|
||||||
let reRenderTimes = 0;
|
|
||||||
let childElements;
|
|
||||||
while (hasUpdatedInRender) {
|
|
||||||
reRenderTimes++;
|
|
||||||
if (reRenderTimes > NESTED_UPDATE_LIMIT) {
|
|
||||||
throw new Error('Too many setState called in function component');
|
|
||||||
}
|
|
||||||
hasUpdatedInRender = false;
|
|
||||||
childElements = funcComp(props, arg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return childElements;
|
|
||||||
}
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ import { setStateChange } from '../render/FunctionComponent';
|
||||||
import { getHookStage, HookStage } from './HookStage';
|
import { getHookStage, HookStage } from './HookStage';
|
||||||
import type { VNode } from '../Types';
|
import type { VNode } from '../Types';
|
||||||
import { getProcessingVNode } from '../GlobalVar';
|
import { getProcessingVNode } from '../GlobalVar';
|
||||||
import { markUpdatedInRender } from './HookMain';
|
|
||||||
|
|
||||||
// 构造新的Update数组
|
// 构造新的Update数组
|
||||||
function insertUpdate<S, A>(action: A, hook: Hook<S, A>): Update<S, A> {
|
function insertUpdate<S, A>(action: A, hook: Hook<S, A>): Update<S, A> {
|
||||||
|
|
Loading…
Reference in New Issue