diff --git a/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js b/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js index 0b5d18a8..ef17a0aa 100644 --- a/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js +++ b/packages/inula/scripts/__tests__/ComponentTest/FunctionComponent.test.js @@ -14,6 +14,7 @@ */ import * as Inula from '../../../src/index'; +const { useState } = Inula; describe('FunctionComponent Test', () => { it('渲染无状态组件', () => { const App = props => { @@ -89,4 +90,39 @@ describe('FunctionComponent Test', () => { Inula.render(, 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 ( + <> +

{count}

+ + {trend &&

The count is {trend}

} + + ); + } + + let count = 0; + function Child({ trend }) { + count++; + return
{trend}
; + } + + let update; + function App() { + const [count, setCount] = useState(0); + update = setCount; + return ; + } + + Inula.render(, container); + update(1); + expect(count).toBe(2); + }) }); diff --git a/packages/inula/src/renderer/hooks/HookMain.ts b/packages/inula/src/renderer/hooks/HookMain.ts index 48f39f42..c2943f49 100644 --- a/packages/inula/src/renderer/hooks/HookMain.ts +++ b/packages/inula/src/renderer/hooks/HookMain.ts @@ -18,12 +18,19 @@ 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, Arg>( funcComp: (props: Props, arg: Arg) => any, @@ -45,8 +52,14 @@ export function runFunctionWithHooks, Arg>( setHookStage(HookStage.Update); } - const comp = funcComp(props, arg); + let comp = funcComp(props, arg); + if (hasUpdatedInRender) { + resetGlobalVariable(); + processing.oldHooks = processing.hooks; + setHookStage(HookStage.Update); + comp = runFunctionAgain(funcComp, props, arg); + } // 设置hook阶段为null,用于判断hook是否在函数组件中调用 setHookStage(null); @@ -63,3 +76,22 @@ export function runFunctionWithHooks, Arg>( return comp; } + +function runFunctionAgain, 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; +} diff --git a/packages/inula/src/renderer/hooks/UseReducerHook.ts b/packages/inula/src/renderer/hooks/UseReducerHook.ts index 1278e65e..fe56820b 100644 --- a/packages/inula/src/renderer/hooks/UseReducerHook.ts +++ b/packages/inula/src/renderer/hooks/UseReducerHook.ts @@ -21,6 +21,7 @@ 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(action: A, hook: Hook): Update { @@ -64,8 +65,13 @@ export function TriggerAction(vNode: VNode, hook: Hook, isUseState: } } - // 执行vNode节点渲染 - launchUpdateFromVNode(vNode); + if (vNode === getProcessingVNode()) { + // 绑定的VNode就是当前渲染的VNode时,就是在函数组件体内触发setState + markUpdatedInRender(); + } else { + // 执行vNode节点渲染 + launchUpdateFromVNode(vNode); + } } export function useReducerForInit(reducer, initArg, init, isUseState?: boolean): [S, Trigger] {