!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 通过比较变化前后的 JavaScript 对象以细粒度的依赖追踪机制来实现响应式更新,无需用户过度关注性能优化。
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import Inula from 'openinula';
|
||||
import { useLayoutEffect, useRef, reduxAdapter, InulaNode } from 'openinula';
|
||||
import { connect, ReactReduxContext } from 'react-redux';
|
||||
import { Store } from 'redux';
|
||||
import type { Store } from 'redux';
|
||||
import { History, Location, Router } from '../router';
|
||||
import { Action, DefaultStateType, Navigation } from '../history/types';
|
||||
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函数
|
||||
if (type === 'InulaXCompat') {
|
||||
return hConnect(null as any, mapDispatchToProps)(ConnectedRouterWithContext as any);
|
||||
return hConnect(null as any, mapDispatchToProps)(ConnectedHRouterWithContext as any);
|
||||
}
|
||||
if (type === 'Redux') {
|
||||
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');
|
||||
|
||||
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 => {
|
||||
if (!isJump) {
|
||||
|
@ -132,7 +132,7 @@ export function createHashHistory<S = DefaultStateType>(option: HashHistoryOptio
|
|||
function replace(to: To, state?: S) {
|
||||
warning(state !== undefined, 'Hash history does not support state, it will be ignored');
|
||||
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 => {
|
||||
if (!isJump) {
|
||||
|
|
|
@ -28,8 +28,8 @@ export function createPath(path: Partial<Path>): string {
|
|||
}
|
||||
|
||||
export function parsePath(url: string): Partial<Path> {
|
||||
let pathname = url || '/';
|
||||
const parsedPath: Partial<Path> = {
|
||||
pathname: url || '/',
|
||||
search: '',
|
||||
hash: '',
|
||||
};
|
||||
|
@ -38,16 +38,16 @@ export function parsePath(url: string): Partial<Path> {
|
|||
if (hashIdx > -1) {
|
||||
const hash = url.substring(hashIdx);
|
||||
parsedPath.hash = hash === '#' ? '' : hash;
|
||||
url = url.substring(0, hashIdx);
|
||||
pathname = pathname.substring(0, hashIdx);
|
||||
}
|
||||
|
||||
const searchIdx = url.indexOf('?');
|
||||
if (searchIdx > -1) {
|
||||
const search = url.substring(searchIdx);
|
||||
parsedPath.search = search === '?' ? '' : search;
|
||||
url = url.substring(0, searchIdx);
|
||||
pathname = pathname.substring(0, searchIdx);
|
||||
}
|
||||
parsedPath.pathname = url;
|
||||
parsedPath.pathname = pathname;
|
||||
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) {
|
||||
let visitedRecord: T[] = [fn(initVal)];
|
||||
|
||||
function getDelta(to: S, form: S): number {
|
||||
let toIdx = visitedRecord.lastIndexOf(fn(to));
|
||||
function getDelta(toKey: S, formKey: S): number {
|
||||
let toIdx = visitedRecord.lastIndexOf(fn(toKey));
|
||||
if (toIdx === -1) {
|
||||
toIdx = 0;
|
||||
}
|
||||
let fromIdx = visitedRecord.lastIndexOf(fn(form));
|
||||
let fromIdx = visitedRecord.lastIndexOf(fn(formKey));
|
||||
if (fromIdx === -1) {
|
||||
fromIdx = 0;
|
||||
}
|
||||
|
|
|
@ -24,27 +24,42 @@ import { parsePath } from '../history/utils';
|
|||
|
||||
type NavLinkProps = {
|
||||
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;
|
||||
} & LinkProps;
|
||||
} & Omit<LinkProps, 'className'>;
|
||||
|
||||
type Page = 'page';
|
||||
|
||||
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 toLocation = typeof to === 'function' ? to(context.location) : to;
|
||||
|
||||
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 otherProps = {
|
||||
'aria-current': isLinkActive ? page : false,
|
||||
className: classNames,
|
||||
'aria-current': isLinkActive ? page : undefined,
|
||||
...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>) {
|
||||
const context = useContext(RouterContext);
|
||||
|
||||
const { computed, location, path } = props;
|
||||
let { children, component, render } = props;
|
||||
const { computed, location, path, component, render, strict, sensitive, exact } = props;
|
||||
let { children } = props;
|
||||
let match: Matched<P> | null;
|
||||
|
||||
const routeLocation = location || context.location;
|
||||
if (computed) {
|
||||
match = computed;
|
||||
} else if (path) {
|
||||
match = matchPath<P>(routeLocation.pathname, path);
|
||||
match = matchPath<P>(routeLocation.pathname, path, {
|
||||
strictMode: strict,
|
||||
caseSensitive: sensitive,
|
||||
exact: exact,
|
||||
});
|
||||
} else {
|
||||
match = context.match;
|
||||
}
|
||||
|
|
|
@ -29,22 +29,27 @@ function Router<P extends RouterProps>(props: P) {
|
|||
const { history, children = null } = props;
|
||||
const [location, setLocation] = useState(props.history.location);
|
||||
const pendingLocation = useRef<Location | null>(null);
|
||||
const unListen = useRef<null | (() => void)>(null);
|
||||
const isMount = useRef<boolean>(false);
|
||||
|
||||
// 在Router加载时就监听history地址变化,以保证在始渲染时重定向能正确触发
|
||||
const unListen = useRef<null | (() => void)>(
|
||||
history.listen(arg => {
|
||||
if (unListen.current === null) {
|
||||
unListen.current = history.listen(arg => {
|
||||
pendingLocation.current = arg.location;
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// 模拟componentDidMount和componentWillUnmount
|
||||
useLayoutEffect(() => {
|
||||
isMount.current = true;
|
||||
if (unListen.current) {
|
||||
unListen.current();
|
||||
}
|
||||
// 监听history中的位置变化
|
||||
unListen.current = history.listen(arg => {
|
||||
setLocation(arg.location);
|
||||
if (isMount.current) {
|
||||
setLocation(arg.location);
|
||||
}
|
||||
});
|
||||
|
||||
if (pendingLocation.current) {
|
||||
|
@ -53,6 +58,7 @@ function Router<P extends RouterProps>(props: P) {
|
|||
|
||||
return () => {
|
||||
if (unListen.current) {
|
||||
isMount.current = false;
|
||||
unListen.current();
|
||||
unListen.current = null;
|
||||
pendingLocation.current = null;
|
||||
|
|
|
@ -40,7 +40,7 @@ export type Matched<P = any> = {
|
|||
|
||||
const defaultOption: Required<ParserOption> = {
|
||||
// url匹配时是否大小写敏感
|
||||
caseSensitive: true,
|
||||
caseSensitive: false,
|
||||
// 是否严格匹配url结尾的/
|
||||
strictMode: false,
|
||||
// 是否完全精确匹配
|
||||
|
|
|
@ -20,10 +20,11 @@ import RouterContext from './context';
|
|||
function withRouter<C extends ComponentType>(Component: C) {
|
||||
|
||||
function ComponentWithRouterProp(props: any) {
|
||||
const { wrappedComponentRef, ...rest } = props;
|
||||
const { history, location, match } = useContext(RouterContext);
|
||||
const routeProps = { history: history, location: location, match: match };
|
||||
|
||||
return <Component {...props} {...routeProps} />;
|
||||
return <Component {...routeProps} {...rest} ref={wrappedComponentRef} />;
|
||||
}
|
||||
|
||||
return ComponentWithRouterProp;
|
||||
|
|
|
@ -24,5 +24,13 @@
|
|||
"watch-test": "yarn test --watch --dev"
|
||||
},
|
||||
"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(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';
|
||||
const { useState } = Inula;
|
||||
describe('FunctionComponent Test', () => {
|
||||
it('渲染无状态组件', () => {
|
||||
const App = props => {
|
||||
|
@ -90,39 +89,4 @@ describe('FunctionComponent Test', () => {
|
|||
Inula.render(<App />, container);
|
||||
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 { Callback } from '../renderer/Types';
|
||||
import { InulaNode } from '../types';
|
||||
import { EVENT_KEY } from './DOMInternalKeys';
|
||||
|
||||
function createRoot(children: any, container: Container, callback?: Callback) {
|
||||
// 清空容器
|
||||
|
@ -89,7 +90,7 @@ function findDOMNode(domOrEle?: Element): null | Element | Text {
|
|||
|
||||
// 情况根节点监听器
|
||||
function removeRootEventLister(container: Container) {
|
||||
const events = (container as any).$EV;
|
||||
const events = (container as any)[EVENT_KEY];
|
||||
if (events) {
|
||||
Object.keys(events).forEach(event => {
|
||||
const listener = events[event];
|
||||
|
|
|
@ -22,9 +22,12 @@ import type { Container, Props } from './DOMOperator';
|
|||
|
||||
import { DomComponent, DomText, TreeRoot } from '../renderer/vnode/VNodeTags';
|
||||
|
||||
const INTERNAL_VNODE = '_inula_VNode';
|
||||
const INTERNAL_PROPS = '_inula_Props';
|
||||
const INTERNAL_NONDELEGATEEVENTS = '_inula_NonDelegatedEvents';
|
||||
const randomKey = Math.random().toString(16).slice(2);
|
||||
const INTERNAL_VNODE = `_inula_VNode_${randomKey}`;
|
||||
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 节点
|
||||
export function getDom(vNode: VNode): Element | Text | null {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
* 只有值发生变化时才会触发change事件。
|
||||
*/
|
||||
|
||||
const HANDLER_KEY = '_valueChangeHandler';
|
||||
import { HANDLER_KEY } from '../DOMInternalKeys';
|
||||
|
||||
// 判断是否是 check 类型
|
||||
function isCheckType(dom: HTMLInputElement): boolean {
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
import { allDelegatedInulaEvents, portalDefaultDelegatedEvents, simulatedDelegatedEvents } from './EventHub';
|
||||
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 { handleEventMain } from './InulaEventMain';
|
||||
import { decorateNativeEvent } from './EventWrapper';
|
||||
|
@ -73,8 +73,8 @@ export function lazyDelegateOnRoot(currentRoot: VNode, eventName: string) {
|
|||
const nativeFullName = isCapture ? nativeEvent + 'capture' : nativeEvent;
|
||||
|
||||
// 事件存储在DOM节点属性,避免多个VNode(root和portal)对应同一个DOM, 造成事件重复监听
|
||||
currentRoot.realNode.$EV = currentRoot.realNode.$EV ?? {};
|
||||
const events = currentRoot.realNode.$EV;
|
||||
currentRoot.realNode[EVENT_KEY] = currentRoot.realNode[EVENT_KEY] ?? {};
|
||||
const events = currentRoot.realNode[EVENT_KEY];
|
||||
|
||||
if (!events[nativeFullName]) {
|
||||
events[nativeFullName] = listenToNativeEvent(nativeEvent, currentRoot.realNode, isCapture);
|
||||
|
|
|
@ -142,7 +142,13 @@ function triggerInulaEvents(
|
|||
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 = [];
|
||||
if (inulaEventToNativeMap.get('onMouseEnter')!.includes(nativeEvtName)) {
|
||||
|
|
|
@ -91,6 +91,10 @@ function mergeData(state, data) {
|
|||
}
|
||||
|
||||
export function createStore(reducer: Reducer, preloadedState?: any, enhancers?: StoreEnhancer): ReduxStoreHandler {
|
||||
if (typeof preloadedState === 'function' && typeof enhancers === 'undefined') {
|
||||
enhancers = preloadedState;
|
||||
preloadedState = undefined;
|
||||
}
|
||||
const store = createStoreX({
|
||||
id: 'defaultStore',
|
||||
state: { stateWrapper: preloadedState },
|
||||
|
@ -107,6 +111,7 @@ export function createStore(reducer: Reducer, preloadedState?: any, enhancers?:
|
|||
return;
|
||||
} // NOTE: reducer should never return undefined, in this case, do not change state
|
||||
state.stateWrapper = result;
|
||||
return action;
|
||||
},
|
||||
},
|
||||
options: {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
import type { VNode } from '../Types';
|
||||
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 {
|
||||
updateVNode,
|
||||
|
@ -35,9 +35,9 @@ enum DiffCategory {
|
|||
ARR_NODE = 'ARR_NODE',
|
||||
}
|
||||
|
||||
// 检查是不是被 FRAGMENT 包裹
|
||||
function isNoKeyFragment(child: any) {
|
||||
return child != null && child.type === TYPE_FRAGMENT && child.key === null;
|
||||
// 检查是不是被 FRAGMENT 或 StrictMode 包裹
|
||||
function isNoKeyFragmentOrStrictMode(child: any) {
|
||||
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;
|
||||
resultNode = createFragmentVNode(key, newChild.props.children);
|
||||
} else {
|
||||
resultNode = updateVNode(oldNode, newChild);
|
||||
resultNode = updateVNode(oldNode, newChild.props.children);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -631,7 +631,7 @@ export function createChildrenByDiff(
|
|||
newChild: any,
|
||||
isComparing: boolean
|
||||
): VNode | null {
|
||||
const isFragment = isNoKeyFragment(newChild);
|
||||
const isFragment = isNoKeyFragmentOrStrictMode(newChild);
|
||||
newChild = isFragment ? newChild.props.children : newChild;
|
||||
|
||||
// 1. 没有新节点,直接把vNode标记为删除
|
||||
|
|
|
@ -18,19 +18,12 @@ import type { VNode } from '../Types';
|
|||
import { getLastTimeHook, setLastTimeHook, setCurrentHook, getNextHook } from './BaseHook';
|
||||
import { HookStage, setHookStage } from './HookStage';
|
||||
|
||||
const NESTED_UPDATE_LIMIT = 50;
|
||||
// state updated in render phrase
|
||||
let hasUpdatedInRender = false;
|
||||
function resetGlobalVariable() {
|
||||
setHookStage(null);
|
||||
setLastTimeHook(null);
|
||||
setCurrentHook(null);
|
||||
}
|
||||
|
||||
export function markUpdatedInRender() {
|
||||
hasUpdatedInRender = true;
|
||||
}
|
||||
|
||||
// hook对外入口
|
||||
export function runFunctionWithHooks<Props extends Record<string, any>, Arg>(
|
||||
funcComp: (props: Props, arg: Arg) => any,
|
||||
|
@ -52,14 +45,8 @@ export function runFunctionWithHooks<Props extends Record<string, any>, Arg>(
|
|||
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是否在函数组件中调用
|
||||
setHookStage(null);
|
||||
|
||||
|
@ -76,22 +63,3 @@ export function runFunctionWithHooks<Props extends Record<string, any>, Arg>(
|
|||
|
||||
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 type { VNode } from '../Types';
|
||||
import { getProcessingVNode } from '../GlobalVar';
|
||||
import { markUpdatedInRender } from './HookMain';
|
||||
|
||||
// 构造新的Update数组
|
||||
function insertUpdate<S, A>(action: A, hook: Hook<S, A>): Update<S, A> {
|
||||
|
|
Loading…
Reference in New Issue