react 组件执行 render 的时机
useState 组件的内部的状态数据只要变化(setState)就会执行函数组件 FuncComp,不论状态数据是否会渲染在界面上 通过 props 传递进来的数据是一样的道理
function FuncComp() {
console.log("组件 rending");
useEffect(() => {
console.log("组件 render 结束");
});
const [i, setI] = useState(0);
const [j, setJ] = useState(0);
return (
<div>
<div>组件 FuncComp 的内部数据i:{i}</div>
<button
onClick={() => {
setI((x) => x + 1);
}}
>
f1_btn
</button>
<button
onClick={() => {
setJ((x) => x + 1);
}}
>
f2_btn
</button>
</div>
);
}
组件渲染和浏览器重绘性
浏览器渲染表达的含义是调用 dom 对象把虚拟 dom 渲染到真实 dom 上,这个事情是浏览器的渲染线程完成的 react 及业务代码是在 js 线程和浏览器渲染线程是互斥的同时只能一个线程执行 组件渲染指 react 组件执行 render 函数 更新虚拟 dom 浏览器重绘是指 因为组件的 render 函数 执行 diff 算法导致的页面更新
react 浏览器插件可以设置组件在 render 时候突出显示,设置方法参考图片
设置可以在浏览器重绘的时候背景色加深突出显示
-
父组件 render 子组件跟随 render
父子组件无论有没有通过 props 传递数据,只要父组件进行了 render,子组件无论状态数据有无变化都
会跟着进 render,react 的 render 执行很快,但是如果子组件多层嵌套,JS 是单线程会对性能有影响,
就需要考虑通过 useMemo 缓存子组件进行性能优化,需要提供缓存依赖项, useMemo 通过缓存依赖项判
断是否进行 render - 子组件 render 父组件不会跟随 render
react 内置钩子
useState 数据状态钩子
useState 第一次执行,在组件加载的时候,给数据赋默认值
useState 保存的是状态数据,这些状态数据需要跨多次 render 使用,不要保存经过计算或外部查询得到的数据在 useState
里,因为这些数据的状态不是组件能够完全自己维护的.比如通过 props 传递的数据 cookie 及 localStorage 等读取的数据,
这些数据随用随读.要注意 react 的 render 是异步渲染的的, 所以在 useState 更新状态数据后不能立即获取到数据
有俩种方式
-
字面量值
const [arr, setArr] = useState([0]);
-
传入一个无参函数, 对于默认值需要计算得出比较适合,如果把计算逻辑放在函数体内,每次执行组件 render 都会执行一遍此计算逻辑,
而此计算逻辑只需要在第一次加载时执行,是一种浪费const [data, setData] = useState(() => { return 0; });
利用组件在加载时赋默认值可以传入一个无参函数可以做类似构造函数的功能 如下代码自会在组件第一次装载的时候执行
const [,] = useState(() => { console.log("执行类似构造函数的功能"); });
useState 更新数据的时候是进行替换,不会做合并
class 组件更新数据会做合并
type objI = {
id?: number;
name?: string;
};
function App() {
const [obj, setObj] = useState({ id: 0, name: "" } as objI);
return (
<div>
{/* 执行后 obj={id:10} 不会合并 name 字段,会把 name(根据定义的数据类型) 字段赋值 null */}
<button
onClick={() => {
setObj({ id: (obj.id ?? 0) + 1 });
}}
>
btn-{obj.id}
</button>
{/* 执行后 obj={name:"bar"} 不会合并 id 字段,会把 id(根据定义的数据类型) 字段赋值 null*/}
<button
onClick={() => {
setObj({ name: "bar" });
}}
>
btn-{obj.name}
</button>
</div>
);
}
export default App;
useState 对引用类型数据是弱引用
useState 对引用类型数据是弱引用,如果要改变引用类型的数据并引发组件 render ,要传递新的引用,而不能只改变数据
function App() {
//值类型
const [i, setI] = useState(0);
//引用数组
const [arr, setArr] = useState(Array<number>(0));
//引用对象
const [obj, setObj] = useState({ id: 0 });
return (
<div>
<div>
{" "}
i:{i} <br />
arr1:{arr.join("-")} <br />
obj2:{JSON.stringify(obj)}
</div>
{/* 改变值类型值 */}
<button onClick={() => setI((x) => x + 1)}>改变值类型</button>
{/*
改变数组类型值 因为React hook 对传递的数据是浅比较,
对引用类型改变原始数据没有效果,需要传递新的引用
*/}
<button onClick={() => setArr((x) => [...x, i])}>改变值类型_数组</button>
{/* 虽然数据是改变了,但是因为是浅比较,不能引发render */}
<button
onClick={() =>
setArr((x) => {
x.push(i);
return x;
})
}
>
改变值类型_数组
</button>
<button
onClick={() =>
setObj((x) => {
return { ...x, id: i };
})
}
>
改变值类型_对象
</button>
</div>
);
}
export default App;
useEffect 副作用钩子
useEffect 执行时机是在组件 render 之后,是异步执行,性能没有影响,可以多次使用此钩子进行业务分离,
与本次界面绘制不相关的都是副作用,相关的业务写在一个 useEffect,达到逻辑复用.往往此钩子函数执行完成,
有可能有会触发下一次的组件 render.
不同的业务写在不同的 useEffect 达到关注点分离的作用,常见的副作用业务:数据获取,设置订阅以及手动更改
react 组件中的 dom,手动获取 dom 都属于副作用
可以通过依赖项决定在更新阶段是否执行 useEffect 函数
-
没有依赖项
只要组件进行 render 后,useEffect 函数就会执行
useEffect(() => { console.log("App 组件 Render 后执行"); });
-
依赖项是空数组
只在组件第一次加载的时候执行
useEffect(() => { console.log("App 组件 Render 后执行"); }, []);
-
有依赖项 件第一次加载的时候会执行,之后组件 render 后,依赖项的数据发生的变化才会执行 ,对依赖项是进行浅比较,
引用类型比较的是引用,引用内部的数据变化不比较,需要拷贝旧值,赋值给新值才会有效果,值对象比较的是值.依赖项的设置要准确
如下的 useEffect 没有设置依赖项,在组件加载的时候执行一次,触发了定时器,定时器闭包捕获的变量是 0 结果是无论 click me 按钮如何点击 定时器总会把 count 改为 1, 正确的做法是传递 count 做为依赖项
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return (
<div>
<div> you click {count} times</div>
<button onClick={() => setCount(count + 1)}>click me</button>
</div>
);
}
虽然解决了问题但是一个更好的解决方法是使用 setCount(x=>x+1) setCount(x=>x+1) 意味着我们只要告诉 react 我们分更新方法而不管现在是什么值
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount((x) => x + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return (
<div>
<div> you click {count} times</div>
<button onClick={() => setCount(count + 1)}>click me</button>
</div>
);
}
useEffect 返回回调
在本次 render 后返回一个函数在下次 render 前或组件卸载的时候被调用 useEffect 回调 利用此回调可以做清理或取消订阅,取消事件的效果.
如下代码在组件中,我们需要监听窗口的大小变化,以便做一些布局上的调整: 在页面第一次加载的时候绑定事件,组件卸载的时候取消绑定
// 设置一个 size 的 state 用于保存当前窗口尺寸
const [size, setSize] = useState({width:0 height:0});
useEffect(() => {
// 窗口大小变化事件处理函数
const handler = () => {
setSize({width:window.innerWidth,height:window.innerHeight});
};
// 监听 resize 事件
window.addEventListener('resize', handler);
// 返回一个 callback 在组件销毁时调用
return () => {
// 移除 resize 事件
window.removeEventListener('resize', handler);
};
}, []);
在每次按钮点击的时候,控制台都会打印,因为按钮点击意味着组件不是在卸载就是在重新 render
function Counter() {
useEffect(
() => () => {
console.log("卸载或下次render 前执行");
},
[]
);
return <div>Counter</div>;
}
function App() {
const [state, { toggle }] = useToggle();
return (
<div>
<div> {state && <Counter></Counter>}</div>
<button onClick={toggle}>{`${state}`}</button>
</div>
);
}
export default App;
useLayoutEffect dom 更新同步钩子
执行的时机是 react diff 执行后,render 执行前,同步执行的,可以使用它来读取 DOM 布局并同步触发重新渲染.在浏览器
执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新,会阻塞后面的流程,函数签和 useEffect 一样.
大部分场景都可以使用 useEffect 代替,在使用 useEffect 出现闪屏(useEffect 里更新 dom 的动画 )时可以考虑用, useLayoutEffect 代替
在 develop 环境测试没有发现更改 dom 样式,useLayoutEffect 和 useEffect 区别,基于 react 18.
跨组件或跨渲染保存可变数据 useRef 钩子
组件每次运行我们都称他为每一次 render,每一次渲染函数内部都拥有自己独立的 props 和 state,当在 jsx 中调用代码中的 state 进行渲染时,每一次渲染都会获得各自渲染作用域内的 props 和 state ,在每次 render 内 props 和 state 就是一些字面量常量
- 每次渲染就好像一次快照,快照之间是互相隔离的,通过 ref 可以把每次快照的数据都保存下来,这些 ref 的数据可以跨快照进行穿梭
- 对 ref 保存的数据进行更改并不会引发重新 render
- useRef 和 useEffect 配合参与业务逻辑
import React, { useEffect, useRef, useState } from "react";
import "./App.css";
function App() {
const [num, setNum] = useState(0);
const [str, setStr] = useState("hello");
console.log("App 组件函数Rending");
useEffect(() => {
console.log(`App 组件上次render的值 num: ${ref.current.num},str:${ref.current.str}`);
ref.current.num = num;
ref.current.str = str;
console.log("App 组件 Render 后执行");
}, [num, str]);
const ref = useRef({ num, str });
return (
<div>
<button
onClick={() => {
setNum((x) => x + 1);
}}
>
btn-{num}
</button>
<button
onClick={() => {
setStr((x) => x + "add-");
}}
>
btn-{str}
</button>
</div>
);
}
export default App;
ref 本身在组件的多次 render 中是同一个引用对象
function App() {
const ref = useRef({ num: 0 });
const [data, setData] = useState(0);
// 依赖项 ref 一直保持引用同一个对象
useEffect(() => {
ref.current.num = data;
}, [ref]);
return (
<div>
{/* 无论组件 render 多少次 ref.current.num 一直为 0*/}
{ref.current.num}
<br />
<button
onClick={() => {
setData((x) => x + 1);
}}
>
btn
</button>
</div>
);
}
useCallBack
把内联回调函数及依赖项数组作为参数传入 useCallback,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新 不要没有原则的去缓存一个内联函数,创建一个内联函数的开销并没有使用 useCallBack 开销大 使用场景:
和 useMemory 配合进行子组件的性能优化
子组件内部嵌套了组件或子组件是个大列表才有必要进行优化
// FComp 是一个嵌套很深的子组件,为了简化嵌套部分没有表示
// 因为不是嵌套很深的组件,渲染开销并不大,没有必要进行组件的缓存
function DeeplyNestedCompon(props: { func: () => void }) {
return (
<div>
<p>deeplyNestedCompon</p>
<button onClick={props.func}>btn</button>
</div>
);
}
function App() {
const [data, setData] = useState(0);
const callBack = useCallback(() => {
setData((x) => x + 1);
}, []);
//对嵌套很深的组件进行缓存
//在父组件进行render 时, useMemo 函数会根据依赖项判断是否生成一个新的对象
//依赖项是有父组件缓存的一个函数,如果此函数不用 useCallback 缓存,那么 useMemo
//就没有效果
const deeplyNestedCompony = useMemo(
() => <DeeplyNestedCompon func={callBack}></DeeplyNestedCompon>,
[callBack]
);
return (
<div>
<p>{data}</p>
<br />
<br />
{/* 因为有缓存,在父组件进行 render 的时候,子组件就不会进行无意义的 render */}
{deeplyNestedCompony}
</div>
);
}
export default App;
和 useMemory 配合进行子组件的性能优化,失效的情况
//子组件
function DeeplyNestedCompon(props: { func: () => void }) {
return (
<div>
<p>deeplyNestedCompon</p>
<button onClick={props.func}>btn</button>
</div>
);
}
function App() {
const [data, setData] = useState(0);
const [cal, setCal] = useState(0);
//回调函数的缓存项是会变化的状态数据 cal,父组件的 cal 只要变化,父组件进行 render,子组件也会进行
// render,子组件缓存失效
//如果缓存依赖项是空数组 [],闭包的原因,捕获到 cal为 0 ---> const callBack = useCallback(()
//=> { setData(cal + 1) }, [])
const callBack = useCallback(() => {
setData(cal + 1);
}, [cal]);
const deeplyNestedCompon = useMemo(() => {
console.log("hi");
return <DeeplyNestedCompon func={callBack}></DeeplyNestedCompon>;
}, [callBack]);
return (
<div>
<div>
{cal}--{data}
</div>
<br />
<button
onClick={() => {
setCal((x) => x + 1);
}}
>
+
</button>
<br /> <br />
{deeplyNestedCompon}
<br /> <br />
</div>
);
}
export default App;
和 useMemory 配合进行子组件的性能优化,失效的改善通过 useRef
//子组件
function DeeplyNestedCompon(props: { func: () => void }) {
return (
<div>
<p>deeplyNestedCompon</p>
<button onClick={props.func}>btn</button>
</div>
);
}
function App() {
const [data, setData] = useState(0);
const [cal, setCal] = useState(0);
//通过 ref 来引用 cal 值 ,而 ref 本身是不变的
const ref = useRef(0);
useEffect(() => {
ref.current = cal;
}, [cal]);
//缓存回调函数时通过 ref 获取 cal,而缓存函数不会失效
const callBack = useCallback(() => {
setData(ref.current + 1);
}, [ref]);
const deeplyNestedCompon = useMemo(() => {
console.log("hi");
return <DeeplyNestedCompon func={callBack}></DeeplyNestedCompon>;
}, [callBack]);
return (
<div>
<div>
{cal} -- {data}
</div>
<br />
<button
onClick={() => {
setCal((x) => x + 1);
}}
>
+
</button>
<br /> <br />
{deeplyNestedCompon}
<br /> <br />
</div>
);
}
export default App;
useMemory
useMemo 效果和 useCallback 一样 useCallback 缓存的是一个函数 useMemo 可以缓存任意结果
自定义钩子 进行业务分离方便维护
自定义 Hooks 在形式上其实非常简单,就是声明一个名字以 use 开头的函数,比如 useCounter. 这个函数在形式上和普通的 JavaScript 函数没有任何区别,你可以传递任意参数给这个 Hook, 也可以返回任何值。但是要注意,Hooks 和普通函数在语义上是有区别的,就在于函数中有没有用到 其它 Hooks。
将相关的逻辑做成独立的 Hooks,然后在函数组中使用这些 Hooks,通过参数传递和返回值让 Hooks 之间完成交互拆分逻辑的目的不一定是为了重用,而可以是仅仅为了业务逻辑的隔离。所以在这个场景下, 我们不一定要把 Hooks 放到独立的文件中,而是可以和函数组件写在一个文件中。这么做的原因就在于, 这些 Hooks 是和当前函数组件紧密相关的,所以写到一起,反而更容易阅读和理解
复用的业务 hook 相当于把原来写在一起的代码剥离初来
import { useState } from "react";
function useCount() {
console.log("执行useCount 钩子");
const [count, setCount] = useState(0);
const increaseCount = () => {
setCount((x) => x + 1);
console.log("执行+");
};
const decreaseCount = () => {
setCount((x) => x - 1);
console.log("执行-");
};
const reset = () => {
setCount(0);
console.log("执行reset");
};
return { count, increaseCount, decreaseCount, reset };
}
export default useCount;
使用 hook
import React from "react";
import useCount from "./useCount";
function App() {
//使用自定义钩子
const { count, increaseCount, decreaseCount, reset } = useCount();
return (
<div>
<div>{count}</div>
<button onClick={increaseCount}>add+1</button>
<button onClick={decreaseCount}>sub-1</button>
<button onClick={reset}>reset</button>
</div>
);
}
export default App;
react child
react child 类似于 vue 的 slot
组件的 child 是一个函数
// 组件定义 children 是一个函数
function funComp({ numTimes = 0, children = (index: number): any => {} }) {
let items = [];
for (let i = 0; i < numTimes; i++) {
items.push(children(i));
}
return <div>{items}</div>;
}
function App() {
return (
<div>
<FunComp numTimes={10}>
{(index) => <div key={index}>This is item {index} in the list</div>}
</FunComp>
</div>
);
}
export default App;
<!--
This is item 0 in the list
This is item 1 in the list
This is item 2 in the list
This is item 3 in the list
This is item 4 in the list
This is item 5 in the list
This is item 6 in the list
This is item 7 in the list
This is item 8 in the list
This is item 9 in the list
-->
不会渲染的子元素
false, null, undefined,true 是合法的子元素。但它们并不会被渲染
透过一个例子了解函数组件的不同之处
函数式组件本身是无状态,通过 hook 增加了状态的行为. 这种行为是每次透过 hook 改变数据状态,组件重新 render 都是重新生成了一个新的函数组件,新生成的函数组件都会执行一遍函数自身, 并把 props ,state 数据替换成常量,组件内的函数也是重新定义一次,组件内的函数(同步或异步)获取的 props ,state 都是替换后的常量。
每一次函数式组件 render 都是互相独立的 ,只是在组件重新 render 的时候获取上一个 render 的状态数据替换自己的默认值而已 函数式组件内的状态数据和函数都是和本次的 render 相关的,不能跨 render 直接获取到数据,要跨 render 可以通过 ref.
每一个组件内的函数(包括事件处理函数,effects,定时器或者 API 调用等等)会捕获某次渲染中定义的 props 和 state 在单次渲染的范围内,props 和 state 始终保持不变
在点击 click me 按钮后 count 无论如何更新,useEffect 回调内 count 获取的始终是 0
function Counter() {
const [count, setCount] = useState(0);
// useEffect 内延时获取的的 count 是一个闭包,要判断闭包的 count 所处于环境也就是 useEffect
//回调执行的
// 时候 count 的值,为了不频繁的创建及销毁定时器,给 useEffect 的依赖是一个空数组
// 正常是在 useEffect 回调内会进行业务逻辑的处理
useEffect(() => {
const id = setInterval(() => {
document.getElementById("p1")!.innerText = `you delay get count: ${count}`;
}, 2000);
return () => {
clearInterval(id);
};
}, []);
return (
<div>
<div> you click {count} times</div>
<div id="p1"></div>
<button onClick={() => setCount(count + 1)}>click me</button>
</div>
);
}
常用的 api
一些技巧
条件渲染
&& 代替 if
// ❌ 这种不是表达式,不支持在 jsx 里
{
if (show) {
return <div>true</div>;
} else {
return <div>false</div>;
}
}
&& 代替 if
// show 为 true ,则执行后面的表达式,代替 if
<div>{show && <div>显示</div>}</div>
?: 三目运算 代替 if-else 嵌套的三目运算也可以表达 多个 if-elseif- -else 但是可读性不高
{
show ? <div>true</div> : <div>false</div>;
}
立即执行的函数
{
(() => {
if (true) {
return <div>true</div>;
} else {
return <div>false</div>;
}
})();
}