前言
- 本文为了帮助使用
Angular
但是没有使用过react
的人进行一个基础问题解答,方便读者更好的使用@cyia/ngx-bridge
- 本文不会去从 0 开始告诉读者怎么搭建,怎么安装,只会把一些基础的理解有些难度的地方进行讲解
- 本文以 18.2 官方文档为准,最好还是先看下文档,有个感性认识
写法
function MyButton() {
return <button>I'm a button</button>;
}
概念
函数组件/组件
React.FunctionComponent<{}>
let fn1 = () => {
return createElement('div');
};
let fn2 = () => {
return <div></div>;
};
节点/元素
React.ReactNode
createElement('div')
<div></div>
疑难
函数组件并不是纯函数
- 第一眼看到的时候,觉得他好像是一个纯函数,并且整个文档及 api 使用,都有一种暗示,就是当成纯函数开发
- 然而其实函数组件是带上下文的,只不过上下是隐藏的,如果这么写,你们可能就明白了
//示意
interface Context{
next:Context
children:Context[]
}
function enterContext(){
currentContext={...}
}
function leaveContext(){
currentContext={...}
}
let rootContext={...}
let currentContext={}
function useXXX(){
currentContext[xxx]
}
function fn(){
useXXX()
}
enterContext()
fn()
leaveContext()
每一个组件函数带了一个上下文,所以才能做到看起来比较魔幻持久化及更新值
(!)所有 hook(useXXX)必须是顺序固定的
- 既然从上下文中取值,而我们又没有定义 key,那么当然是按顺序取值
- 所以所有 hook 在调用的时候,要保证他们之间不存在
if(xxx){return}
等提前返回的行为 - 保证在任何时刻它们的顺序要一致,数量要一致
速记
- 运行在函数顶层
- 流程控制不变(如果某个条件是 false/true,里面还有 hook,那么在这个组件销毁前,永远要是一致)
新手就认为函数组件内的hook语句之间不能有任何流程控制语句把,等理解了再进行发挥
- hook 可以在函数中,但是要保证在销毁前一定执行/不执行
同样,新手如果使用函数,就保证一定要执行
- 下面的例子不算在函数中
useEffect(() => {
useState();
});
虽然确实定义在函数里,但是你不知道
useEffect
中()=>{useState()}
执行时机(当然,官网源码也确实不能) 这个例子的意思就是除非你知道回调的执行时机一定(不)执行
且同步执行
,否则不要使用
(!)hook 返回的值可以运行/出现在上下文外
- 虽然 react 文档中没有明确提出这一点,但是确实可以
- 毕竟上下文的意义其实就是缓存一些值,而这些返回的值/函数是用来显示/修改的,如果不能运行在上下文外,就意味着他们也有读取的功能需要限制顺序及上下文
- hook不可返回在其他地方调用,因为hook本身就是依赖上下文的
当然如果你是高手只要遵循顺序固定原则就行了…新手就不要瞎搞了,出bug也没法解决
函数组件的创建是动态的,上下文是静态的
- 虽然上下文需要严格限制写法,但是可以通过创建函数组件的方式生成新的上下文
- 所以如果 hook 是动态的,可以通过创建函数组件的方式实现,每个函数组件创建时都带着一个上下文
function Fn1() {
return <Fn2></Fn2>;
}
function Fn2() {
return <div></div>;
}
看起来
Fn1
什么都没干,但是实际上创建了一个上下文
(!)children 是节点,不是函数组件
- 这意味着传入的 children 如果是在
ts
下,要使用createElement(xxx)
包裹一下,然后再传入
function Fn1({ children }) {
return <Fn2>{children}</Fn2>;
}
function Fn1({ children }) {
return createElement('Fn2', null, children);
}
createElement(Fn1, null, createElement(Fn2));
(!)不要在一个函数组件中调用另一个函数组件
- 如果直接
Fn2(xxx)
,返回了节点,但是这时一个上下文中除了本身的 hook,还多了这个Fn2
的 hook - 这就可能违反了
顺序固定原则
如果说前面没有流程控制
return
之类的语法,可能问题不大,只是不优雅.但仍不建议这么使用 - 遇到这种情况,一定是要
createElement('Fn2',null,children)
生成一个元素而不是调用如果经常写 tsx,可能不存在这个问题…因为两者区分比较大,但是写 ts 因为返回的类型都是节点,很容易混淆
hook 只执行一次的话,那么就是[]
- 有些钩子比如
useEffect
如果想让他只执行一次,那么就是空数组
函数组件内的任何变更,都要使用 hook 返回的方法
- react 没有那么神通广大,必须通知,才能知道谁变更,而返回一般是带通知的,如果不带,那么就是自动通知并更新
function fn() {
let [v, setV] = useState(0);
// n
v = 1;
// y
setV(1);
}
useSyncExternalStore
算是一个例外?因为它是使用提供的回调来进行更新的
函数组件是一个快照
- 熟悉
Angular
开发的人,可能知道 ng 的视图逻辑和业务逻辑其实是分离的,所以视图逻辑更新时,不会去执行业务逻辑你的
ngOnInit
永远会执行一次 - 但是
React
两者处于强耦合状态,每一个更新都需要重新执行一遍函数组件,进行视图逻辑对比,但同时所有业务逻辑也被执行了一次
function fn() {
let [v, setV] = useState(get());
}
虽然
get()
只在第一次生效,但是他在每次都进行了执行…(即使没用),所以如果计算比较耗时,就需要用useMemo
封装
(!)函数组件内部存在持久化监听时,变量一定要用 useRef 这种获取
function fn() {
let [v, setV] = useState(get());
useEffect(() => {
xxx.addEventListener('click', () => {
console.log(v);
});
}, []);
}
useEffect
内的函数只会执行一次,之后内部的监听会持久化运行,但是v
由于前面说的快照原因,你拿的数据仅仅是第一次渲染时v
的值,之后可能会出现了 n 次的更新,所以需要改useRef
xxx
如果不变的话倒是不用改,否则需要返回一个清理函数