组件
[文献链接](https://ijpqg5zm8j.feishu.cn/docs/doccnf8zVMcqwwjw6TWz5SnoUmf#hX0qpF
函数组件
定义组件最简单的方式就是编写JavaScript函数,返回一个React元素。
所有React组件必须要像纯函数一样保护props不被更改。
无状态组件
样式渲染、点击行为、文案都是通过 props 参数传递进来的,我们称这种自身没有任何状态的组件叫无状态组件 (Stateless Function Component)。
JSX 语法定义⾮闭合标签中的内容会作为 props.children 传递。
有状态的组件
在组件中要维护一个状态时,主要涉及两个 API 来保存状态,useState
、useRef
。
useState
useState
入参是初始状态。
如果传入的是一个 function,则将此 function 的返回作为初始状态。
// 下⾯两⾏代码是等价的
const [count, setCount] = useState(0);
const [count, setCount] = useState(() => 0);
count
初始值为 0
。function 传参只会在组件首次渲染时执行,也就是整个组件生命周期,function 传参只会执行一次。
调用 setCount
时,React 会重绘当前组件,更新 html 文档,触发浏览器重绘。
可以看出,每次使用 setCount
时,都会重新执行 Component 的函数,所以,绝对不能在 Component 函数中同步调用 setCount
,这样会导致无限重绘,页面假死。
实际上,页面的 UI 变更,总是有原因的:
- 用户触发的交互,如:键盘输入、鼠标点击、屏幕滑动等
- 定时器的触发,如:
setTimeout
、requestAnimationFrame
、setInterval
- IO 事件回调触发,如:AJAX 请求返回的回调
总结就是,setCount
操作必须在某个回调中调用,不应该出现在 Component 函数的同步调用栈中执行。
以下情况
useState
需使⽤ function 作为⼊参:
- 当初始状态需要复杂计算时
- 当初始状态是复杂对象时
关于setCount传入function的问题: 传函数的好处是,当依赖 state ⾃⾝最新状态来更新状态时,不需要访问外部变量。
useRef
当需要存放⼀个数据,需要⽆论在哪⾥都取到最新状态时,需要使⽤ useRef。
ref 是⼀种可变数据。
⾸先我们来通过⼀个例⼦来解释⼀下函数组件中常⻅的闭包问题:
function SomeComponent() {
const [count, setCount] = useState(0);
// true
// const countRef = useRef(count);
// countRef.current = count;
// useEffect 表示在第一次渲染完成后,执行回调函数,具体 useEffect 用法下面讲
useEffect(() => {
const id = setInterval(() => {
console.log(count)
// true
// console.log(countRef.current);
setCount(currentCount => currentCount + 1);
});
return () => { clearInterval(id); }
}, []);
return <h1>See what's printed in console.</h1>
}
观察 console 打印的值是什么:
- 上面的代码,console 永远打印 0。因为函数声明时(第⼀次运⾏时),count 是 0,之后⽆论这个函数调⽤多少次,都会是 0。
- 这时候,如果我们想要拿到 count 的最新值,就可以使⽤ useRef 声明⼀个可变数据对象,来存储 count。由于对象引⽤是不变的,当我们更新对象某个字段时,闭包函数就能访问到最新的值了。
避免基于可变对象的 ref 更新
对于 useRef 的值的更新,需要注意如果是在 Component 函数中同步赋值的情况,不要做基于其他任何 可变数据 的增量更新,比如:
// bad
countRef.current = countRef.current + 1;
// good
countRef.current = immutableState.count + 1;
因为在 StrictMode 下,React 每次渲染会执行两次 Component 函数,来检查函数组件的幂等性。这时基于可变数据的更新,会导致两次执行结果不一致,这是不允许的(会带来意想不到的更新结果,React 没有提供很好的 Warning 信息,很难排查)。
不要使用 useRef 获取子组件 instance
React 社区有个组件约定,对于要拿到组件实例情况下,一般通过 ref 传参去取得某组件的实例。比如,对于 DOM Element,使用 ref 可以拿到 dom 实例。但这种方式并不推荐。
useState、useRef 如何决策用哪种来维护状态
useRef 生成的可变对象,因为使用起来就跟普通对象一样,赋值时候 React 是无法感知到值变更的,所以也不会触发组件重绘。利用其与 useState 的区别,我们一般这样区分使用:
- 维护与 UI 相关的状态,使用 useState
- 确保更改时刷新 UI
- 值更新不需要触发重绘时,使用 useRef
- 不需要变更的数据、函数,使用 useState
比如,需要声明一个不可变的值时,可以这样:
const [immutable] = useState(someState);
不返回变更入口函数。useRef 虽然可以借助 TypeScript 达到语法检测上的 immutable,但实际还是 mutable 的。
组件通信
React 使用单向数据流进行 UI 绘制,只有父组件能控制子组件的状态,子组件不能修改父组件的状态。
单向数据流的优势在于 不存在数据绑定、数据作用域 等概念,这样使得首屏速度比双向绑定快。
其次,排查问题更简单。但不足之处是交互组件的编写相对于双向绑定,比较啰嗦。
父子组件通信
父组件通过向子组件传递 props 通信。子组件通过对父组件暴露注册函数的接口来通知父组件更新自身状态
兄弟组件通信
通过将兄弟组件的状态放到父组件上来进行通信
爷孙组件通信
爷孙组件通信主要有 3 种方式:
- 将孙子组件的 props 封装在一个固定字段中
- 将Son需要的所有props维护在一个字段中
- Daddy 和 Son 耦合。
- 通过 children 透传(类似vue中的slot)
- 通过 context 传递(类似vue中的provide、inject)
三种方案的决策
- 第一种方案一般用于固定结构和跨组件有互相依赖的场景,多见于 UI 框架中的复合组件与原子组件的设计中
- 第二种常用在嵌套层级不深的业务代码中,比如表单场景。优点是顶层 Grandpa 的业务收敛度很高,一眼能看清 UI 结构及状态绑定关系,相当于拍平了 React 组件树
- 第三种比较通用,适合复杂嵌套透传场景。缺点是范式代码较多,且会造成 react dev tools 层级过多;Context 无法在父组件看出依赖关系,必须到子组件文件中才能知道数据来源