React 基础问题解答

前言

  • 本文为了帮助使用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如果不变的话倒是不用改,否则需要返回一个清理函数