0%

react18.2代码打印顺序和流程分析

问题1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 2 3 4 5 7 6 1(当前调度未超出时间切片)
// 2 3 4 5 6 1 7(当前调度超出时间切片, 在后面加sleep也可以看出来,effect的调度放在了之后的宏任务中执行)
import { useEffect } from "react";
import { createRoot } from "react-dom/client";

export const App = ({ name }) => {
console.log("log:", 5);

Promise.resolve().then(() => {
console.log("log:", 6);
});

useEffect(() => {
console.log("log:", 7);
});

return (
<div>
<h1>Hello {name}!</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
};

const root = createRoot(document.getElementById("root"));

console.log("log:", 2);
root.render(<App name="StackBlitz" />);
console.log("log:", 3);

Promise.resolve().then(() => {
console.log("log:", 4);
});

setTimeout(() => {
console.log("log:", 1);
}, 0);

问题2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 把setTimeout放在render之前的执行顺序
// 2 3 4 1 5 7 6
// 2 3 4 1 5 6 7
import { useEffect } from "react";
import { createRoot } from "react-dom/client";

export const App = ({ name }) => {
console.log("log:", 5);

Promise.resolve().then(() => {
console.log("log:", 6);
});

useEffect(() => {
console.log("log:", 7);
});

return (
<div>
<h1>Hello {name}!</h1>
<p>Start editing to see some magic happen :)</p>
</div>
);
};

const root = createRoot(document.getElementById("root"));

console.log("log:", 2);
setTimeout(() => {
console.log("log:", 1);
}, 0);

root.render(<App name="StackBlitz" />);
console.log("log:", 3);

Promise.resolve().then(() => {
console.log("log:", 4);
});

问题3

从源码来看,effectcreate 函数是在一个被调度的 callback 里面去执行的,为什么它还是会比 Promise.then()callback 函数先执行?

useEffect 是在 commit 阶段被 scheduler 加入到 taskQueue 中的,一个时间切片(5ms)内,如果时间允许,可能会执行很多个 task

taskQueue 中的任务又是在宏任务中被执行的,先于微任务。

总之,当前时间切片时间允许就是 effectPromise.then()前,如果时间不够 effectPromise.then() 后。

问题4

如果中间加了个sleep呢?如果不加呢?

useEffect 是在 commit 阶段被 scheduler 加入到 taskQueue 中的。

  • 加sleep,导致超出时间切片5ms,taskQueue中的调度就要等到下一个宏任务再去调度
  • 不加sleep,可能在5ms之内,useEffect的调度就被执行了,也有可能有各种高优先级的任务在 useEffect 前导致以后得调度再执行 useEffect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

// 加:2 3 4 5 6 1 7
// 不加:2 3 4 5 7 6 1 或者 2 3 4 5 6 1 7

import { useEffect } from "react";
import { createRoot } from "react-dom/client";

function sleep() {
const start = performance.now();
while (performance.now() - start < 10) {
// block
}
}

const Child = () => {
useEffect(() => {
console.log("log:", 7);
});

return null;
};

export const App = ({ name }) => {
console.log("log:", 5);

Promise.resolve().then(() => {
console.log("log:", 6);
});

// sleep();

return (
<div>
<h1>Hello {name}!</h1>
<p>Start editing to see some magic happen :)</p>
<Child />
</div>
);
};

const root = createRoot(document.getElementById("root"));

console.log("log:", 2);
root.render(<App name="StackBlitz" />);
console.log("log:", 3);

Promise.resolve().then(() => {
console.log("log:", 4);
});

setTimeout(() => {
console.log("log:", 1);
}, 0);

问题5

5.1 刷新页面,console.log打印了几次?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState, useEffect } from 'react'

export default function UseABC() {
const [count, setCount] = useState(-1)
const [count1, setCount1] = useState(-100)

useEffect(() => {
setCount1(0)
})

console.log('x') //sy-log

return (
<>
<h3>UseEffectPage {count1}</h3>
</>
)
}

答案:3次。

  • 第一次:函数组件初次渲染,执行log函数,并且此次由于useEffect函数执行,在fiber节点上记录了包含create函数的effect对象;

  • 第二次:源于组件初次渲染完成之后,延迟useEffect的create函数,此时create函数中执行的是setState事件。setState导致函数组件更新,那么再次执行函数组件这个函数,log再次打印。并且此次又记录了create函数的effect对象。

  • 第三次:基本同第二次,不同的地方在于第三次的时候,前后两次状态值相同,函数组件检测到没有更新发生,bailout了。

  • 详细过程:

    • 初始化执行UseABC组件函数,flush -> workloop -> performConcurrentWorkOnRoot,进到render阶段,在当前fiber的updateQueue上记录了要更新的effect,打印一次log
    • 初始化到了commit阶段,通过 scheduleCallback 向 taskQueue 加入一个优先级为 NormalPriority 的 flushPassiveEffects 任务
      • /* commit的最后还是会ensureRootIsScheduled调度一次微任务scheduleImmediateTask(processRootScheduleInMicrotask);或者performConcurrentWorkOnRoot的最后调度scheduleTaskForRootDuringMicrotask是否要加入高优先级任务,可能涉及到其它高优先级任务插队执行的问题,在flushWork结束的finally里判断如果有任务再次发起宏任务调度 */
    • 不考虑高优先级任务插队或者其他各种情况的话,正常执行完毕后回到 workloop 中,会取出task 或者在下次切片中取出task执行,所以 flushPassiveEffects 任务得以执行,在从root开始遍历,执行各个fiber上的updateQueue的effect
    • 执行到setCount1时候,等于执行dispatchSetState.bind(null,currentlyRenderingFiber,queue),相当于增加了一个调度更新scheduleUpdateOnFiber,cheduleUpdateOnFiber执行时会标记root上的更新也会把对应的update添加到对应fiber上,最后通过 ensureRootIsScheduled 发起微任务调度,但是commit已经发起过了,就不重复发起了,最终是通过微任务把setCount1加入到了taskQueue中
    • 不管中间发生了什么,当再次执行与setCount1任务相同优先级的调度时候,从root开始遍历,再次执行UseABC组件函数,count1变成了0,且再打印一次log(执行UseABC组件函数的过程中又遇到了useEffect执行setCount1,又添加了调度更新)
    • 当再次执行与此次任务相同优先级的调度时候,那就再次同上执行UseABC组件函数,再打印一次log,但是不同的是,现在count1的值没变化,bailout了

5.2 如果useEffect依赖为[],console.log打印了几次?

答案:2次

  • 第一次:函数组件初次渲染,执行log函数,并且此次由于useEffect函数执行,在fiber节点上记录了包含create函数的effect对象;
  • 第二次:源于组件初次渲染完成之后,延迟useEffect的create函数,此时create函数中执行的是setState事件。setState导致函数组件更新,那么再次执行函数组件这个函数,log再次打印。但是此次useEffect没有变化,故不再后续执行。