React那些事儿
Hooks是什么
- hooks是一个消息通知机制(如git hooks, webpack hooks、操作系统的hooks),在react运行的某个时机时通知做一些操作。
- 拥抱函数式编程,为了让用户以最小的代价实现"关注点分离原则",达到最初的设计理念,即"component = f(data)"。
- 几乎重新定义了react的写法,用户可以不用再去关心程序运行的生命周期,只需要关注状态的改变即可。
避免竞争条件(Race Condition)导致的未知错误
相信大家在使用react
的useState
更新状态时,应该最长使用如下写法吧:
tsx
import { useState } from "react";
const App = () => {
const [age, setAge] = useState<number>(0);
return (
<div>
{age}
<button onClick={() => setAge(age + 1)}>+</button>
</div>
);
};
export default App;
上面的代码再大多数情况下是没有什么问题的,点击+
按钮后都能够让age
不断累加,但是,在某些特殊场景下,就可能出现一些异常情况,如:
tsx
import { useState } from "react";
const App = () => {
const [age, setAge] = useState<number>(0);
const handleClick = () => {
setTimeout(() => {
setAge(age + 1);// 1
}, 100);
setTimeout(() => {
setAge(age + 1);// 1
}, 50);
};
return (
<div>
{age}{/** 1 */}
<button onClick={handleClick}>+</button>
</div>
);
};
export default App;
如上述情况,我们点击+号之后,虽然在回调函数中执行了两次setAage
操作,但实际上页面展示的依然是1
,这是为什么呢?这是由于我们的age
变量是从环境中来的,而由于setTimeout
形成了闭包,导致我们age的值始终都是0
。这样就会出现明明我调用了两次setAage
,但却只加到了1
的奇怪现象。
上述这种现象,其实就是所谓的竞争条件(Race Condition)
,由于闭包内的变量优先级高于闭包外变量,导致没有正常完成更新。
那么,我们要如何解决上述问题呢?其实也很简单:
tsx
import { useState } from "react";
const App = () => {
const [age, setAge] = useState<number>(0);
const handleClick = () => {
setTimeout(() => {
setAge(age => age + 1);// 2
}, 100);
setTimeout(() => {
setAge(age => age + 1);// 1
}, 50);
};
return (
<div>
{age}{/** 2 */}
<button onClick={handleClick}>+</button>
</div>
);
};
export default App;
上面只是更换了更新age的方式,从直接传入新值改成传入一个函数,这个函数会接收当前上下文中age
的上一个状态的值,并返回下一个状态,这样就能够始终保证,我们用来累加的age
一定是当前的最新状态了。
Hooks不能在分支语句中使用的分析
首先,我们得先搞清楚,Hooks
的实现原理,才能够弄明白为什么Hooks
不能在分支语句中使用。
我们可以简单的理解为,当我们使用useEffect|useState|useRef
等Hooks
时,react
内部实际上开辟了一个存储空间,并为每一次的Hooks
调用打上一个编号,如果一个编号上没有存储任何东西,则会初始化一个引用并存储其中,如果下一次相同编号的的Hooks
则无需重新初始化,直接使用该引用即可。如果我们将Hooks
放在一个分支语句或回调函数当中,就可能出现刚开始存在这个编号的Hooks
,下一次渲染又不存在的情况。这会导致他们的编号不一致,从而导致记录的Hooks
出现混乱。
总结一下:实际上,我们的Hooks可以理解成是程序的一个声明,而流程控制,如if-else
不是声明的一部分,如果我们把Hooks放在流程控制当中,会导致程序声明和逻辑的混乱。React内部通过Hooks的词法顺序来区分不同的Hooks
tsx
import { useState, useEffect, useRef } from "react";
const App = () => {
const [age, setAge] = useState<number>(0);
const button = useRef(null);
useEffect(() => {
console.log('init')
}, []);
const handleClick = () => {
setTimeout(() => {
setAge(age => age + 1);// 2
}, 100);
setTimeout(() => {
setAge(age => age + 1);// 1
}, 50);
};
return (
<div>
{age}{/** 2 */}
<button ref={button} onClick={handleClick}>+</button>
</div>
);
};
export default App;
// 上述代码中的Hooks,在react内部存储可以看成
[useState, useRef, useEffect]
// 当每次重新渲染时,读取到useState时,react会到存储空间中的第一位查找引用,读取到useRef时,将会去存储空间的第二位查找引用...,假如说,某个hooks存在分支语句中,如:
import { useState, useEffect, useRef } from "react";
const App = () => {
const [age, setAge] = useState<number>(0);
let button
if(age === 0) {
button = useRef(null);
}
useEffect(() => {
console.log('init')
}, []);
const handleClick = () => {
setTimeout(() => {
setAge(age => age + 1);// 2
}, 100);
setTimeout(() => {
setAge(age => age + 1);// 1
}, 50);
};
return (
<div>
{age}{/** 2 */}
<button ref={button} onClick={handleClick}>+</button>
</div>
);
};
export default App;
// 那么,当age不为0时,无法进入分支语句,因此没有了useRef定义,但是原本的存储空间依然还是:
[useState, useRef, useEffect]
// 这样就会导致原本useEffect在存储空间中的编号是3,但是现在编号变成了2,读取到了原本应该是useRef的引用,导致系统异常。
子组件传递信息给父组件
在react
中,父组件传参给子组件很简单,通过属性的方式就可以轻松实现,如果要子组件传递参数给父组件,我们通常会用以下两种方式:
ref
tsx
fucntion Child(props) {
return (
<>
<input ref={props.inputRef} />
</>
)
}
function Parent() {
cosnt inputRef = useRef(null);
useEffect(() => {
console.log(inputRef.current.value);
}, []);
return (
<Child inputRef={inputRef} />
);
}
callback
tsx
fucntion Child(props) {
return (
<>
<input onChange={e => props.onChange(e.target.value)} />
</>
)
}
function Parent() {
const handleChange = (value) => {
console.log(`value: ${value}`);
}
return (
<Child onChange={handleChange} />
);
}
容器组件传参
父子组件之前相互传参我们都清楚,那么,假如说我们要通过一个容器组件,把参数传递给他的children
要怎么传呢?
tsx
fucntion Child(props) {
return (
<>
{props.x}
</>
)
}
function Parent({children}) {
return (
<div className="container">
{children}
</div>
);
}
function App() {
// 如果我要将x传递给他的所有子组件,要怎么实现呢?
return <Parent x={1}>
<Child />
</Parent>
}
我们可以借助cloneElement
完成
tsx
fucntion Child(props: {x?: number}) {
return (
<>
{props.x}
</>
)
}
function Parent({children: JSX.Element, x: number}) {
return (
<div className="container">
{/* 克隆子节点,并将属性x赋值给所有子节点 */}
{React.cloneElement(children, {x})}
</div>
);
}
function App() {
return <Parent x={1}>
<Child />
</Parent>
}
避免爆栈(StackOverflow)
tsx
function APP() {
const [count, setCount] = useState(0);
useEffect(() => {
// 由于这个useEffect有count依赖,一旦count更新就会被重新触发,如果我们在这里又触发了setCount操作,
// 就会导致程序陷入死循环而导致爆栈,这一点大家需要注意
setCount(x => setCount(x));
}, [count]);
}
同时兼容受控与非受控的组件
受控组件简单的说就是他的状态交由外部改变,通过外部传入的value改变组件内部的状态,常见与表单组件。虽然受控组件更加灵活,但也一定程度上违反了"最小知识原则",用户在使用一个组件时的学习成本变高。那么,有没有办法让一个组件即能够兼容受控组件的灵活,又能能兼容非受控组件的简单易用呢?也就是说,能否实现一个既可以受控,又可以非受控的组件。
tsx
import { useEffect, useState } from "react";
export type MyInputPorps = {
value?: string;
defaultValue?: string;
onChange?: (value: string) => void;
};
function MyInput({ value, defaultValue, onChange }: MyInputPorps) {
// 受控组件和非受控组件的标志是有没有传value属性,如果传了就是受控组件,没传就是非受控组件
const controlled = typeof value !== "undefined";
const [_value, setValue] = useState(
(controlled ? value : defaultValue) || ""
);
useEffect(() => {
if (_value !== defaultValue) {
onChange?.(_value);
}
}, [_value]);
return (
<input
value={controlled ? value : undefined}
defaultValue={defaultValue}
onChange={(e) => {
if (controlled) {
onChange?.(e.target.value);
return;
}
setValue(e.target.value);
}}
/>
);
}
const App = () => {
const [val, setVal] = useState("name");
return (
<div>
<fieldset>
<legend>非受控组件</legend>
<MyInput defaultValue="123" />
</fieldset>
<fieldset>
<legend>受控组件</legend>
<MyInput
value={val}
onChange={(value) => {
console.log(value);
setVal(value);
}}
/>
</fieldset>
</div>
);
};
export default App;
Hooks
优化版
tsx
import { useEffect, useState } from "react";
export type MyInputPorps = {
value?: string;
defaultValue?: string;
onChange?: (value: string | undefined) => void;
};
export type UseValueOptions = {
value?: string;
defaultValue?: string;
onChange?: (value: string | undefined) => void;
};
function useValue({
value,
defaultValue,
onChange,
}: UseValueOptions): [string | undefined, (value: string | undefined) => void] {
// 受控组件和非受控组件的标志是有没有传value属性,如果传了就是受控组件,没传就是非受控组件
const controlled = typeof value !== "undefined";
const [_value, setValue] = useState<string | undefined>(
controlled ? value : defaultValue
);
useEffect(() => {
if (controlled && _value !== value) {
setValue(value);
}
}, [value]);
useEffect(() => {
if (!controlled && _value !== defaultValue) {
onChange?.(_value);
}
}, [_value]);
const setHandler = (value: string | undefined) => {
if (!controlled) {
setValue(value);
} else {
onChange?.(value);
}
};
return [_value, setHandler];
}
function MyInput({ value, defaultValue, onChange }: MyInputPorps) {
// 受控组件和非受控组件的标志是有没有传value属性,如果传了就是受控组件,没传就是非受控组件
const controlled = typeof value !== "undefined";
const [_value, setValue] = useState(
(controlled ? value : defaultValue) || ""
);
useEffect(() => {
if (_value !== defaultValue) {
onChange?.(_value);
}
}, [_value]);
return (
<input
type="text"
value={controlled ? value : undefined}
defaultValue={defaultValue}
onChange={(e) => {
if (controlled) {
onChange?.(e.target.value);
return;
}
setValue(e.target.value);
}}
/>
);
}
function MyInput2({ value, defaultValue, onChange }: MyInputPorps) {
// 受控组件和非受控组件的标志是有没有传value属性,如果传了就是受控组件,没传就是非受控组件
const [_value, setValue] = useValue({
value,
defaultValue,
onChange,
});
return (
<input
type="text"
value={value?_value:undefined}
defaultValue={defaultValue}
onChange={(e) => {
setValue(e.target.value);
}}
/>
);
}
const App = () => {
const [val, setVal] = useState("name");
const [val2, setVal2] = useState("age");
return (
<div>
<fieldset>
<legend>非受控组件</legend>
<MyInput defaultValue="123" />
</fieldset>
<fieldset>
<legend>受控组件</legend>
<MyInput
value={val}
onChange={(value) => {
console.log(value);
setVal(value as string);
}}
/>
</fieldset>
<fieldset>
<legend>非受控组件(Hooks)</legend>
<MyInput2 defaultValue="333" />
</fieldset>
<fieldset>
<legend>受控组件(Hooks)</legend>
<MyInput2
value={val2}
onChange={(value) => {
console.log(value);
setVal2(value as string);
}}
/>
</fieldset>
</div>
);
};
export default App;
强制触发重绘
我们经常会遇到一些场景,数据更新之后,由于这个数据并不是直接放在状态当中,不会自动触发重绘,此时,我们可能需要手动强制触发一下重绘使得识图更新,这边推荐使用版本号重绘法
tsx
function App() {
const [, setVersion] = useState(0);
const buz = useBuz();
useEffect(() => {
buz.on('refresh', () => {
// 此时,我们监听到业务hooks通知的期望页面被刷新的消息时,我们通过更新version状态,依次来触发页面的强制刷新重绘
setVersion(v => v + 1);
});
return () => {
buz.off('refresh');
};
}, []);
return (
<>
...
</>
)
}
Fiber是什么
Fiber
是React Element
的一个数据镜像Fiber
是一个Diff
工作Fiber
模拟了函数调用的关系(函数的递归调用与回溯的流程)
Fiber
在处理更新时,实际并不是直接操作当前的Fiber
节点本身,而是会将他copy
一份出来,然后在这个克隆版本上进行修改,当所有的操作都完成之后,再将这个克隆的Fiber
节点跟原本的节点进行Diff
。最后根据Diff
的结果将当前节点替换掉,完成页面更新。这个过程类似于我们使用Git
进行版本管理时,修改的文件并不会直接加入到版本管理,而是克隆一个副本,把更改放到这个副本上,当执行Commit
操作时才合并修改。本质上,这是一种处理并发的技巧,叫做:Copy on Write
Fiber更新的两个阶段
- 计算阶段(可中断)
- 计算
Work In Progress Fiber
,即上面说的当前Fiber
的副本。需要更新的属性、节点之类的信息都会在这里计算 - 进行
Dom Diff
,计算Dom
的更新
- 计算
- 提交阶段(不可中断)
- 提交所有的更新
Fiber的执行
驱动Fiber
的执行有很多种情况,主要的3种是:
- 根节点的渲染函数调用,即首次渲染(
ReactDom.render(<App />, document.getElementById('root'))
) - setState
- 属性的改变
当上述情况发生时,render
库会驱动Fiber
执行。
- 如果是新节点,则创建
Fiber树
- **计算阶段:**如果是变更操作,那么计算
Work In Progress Fiber
- 计算阶段:
render
库执行,利用Diff
算法计算更新 - **提交阶段:**应用更新
Fiber的并行
并行就是指任务可以交替同时进行,看起来好像一起执行的,如果要做到这点,那么Fiber
就必须得保证每个任务都是独立的。
理论上,在计算阶段,一切都是虚拟的,父Fiber
节点和子Fiber
节点可以是两个work。
之前说了,Fiber
的执行类似深度优先搜索,在不断地递归与回溯的过程中完成工作,那么,这样就形成了依赖关系,是不是就没办法并行了呢?
实际上是可以的。
因为Fiber
是先计算出所有的Work In Progress Fiber
后再进行Diff
操作的。
虽然在计算Work In Progress Fiber
时不能并行,但只要计算出来后,后面的Diff
操作和部分的更新操作都是可以并行执行的。
Fiber的价值
Fiber
之所以花那么大的功夫让其支持并行,目的是为了让我们Diff
和Update
等工作可以暂停
和恢复
,这样可以更加灵活的调度资源用于计算与渲染,尽可能的降低因过于复杂和密集的计算导致浏览器的渲染卡顿。