React 组件与状态

2024-07-15  本文已影响0人  李霖弢
import { Fragment } from 'react';

const listItems = people.map(person =>
  <Fragment key={person.id}>
    <h1>{person.name}</h1>
    <p>{person.bio}</p>
  </Fragment>
);

组件渲染流程

  1. 组件初次渲染,或组件或其祖先的状态发生了改变,触发组件渲染
  2. 组件渲染:执行组件函数(不包括Hook),并在虚拟DOM树中进行diff运算
    注意,组件函数必须都是没有副作用的纯函数。
  3. 浏览器绘制(DOM渲染):此时DOM才真正更新
  4. useEffect回调触发

组件渲染的逻辑

每次组件渲染会重新执行组件函数,但不包括其中的 Hook 内容

纯函数

组件应当都是纯函数,只负责自己的任务,不改变外部变量,且相同的输入总得到相同的输入。以此防止组件每次渲染时造成副作用。

Hook 内容不会在组件再次渲染时重复执行

父组件传入的 props 如通过useState镜像,则父组件稍后传递不同的props时,该state不会更新:

function Message({ messageColor }) {
  const [color, setColor] = useState(messageColor);
}

props

function Card({children}) {
  return <div>hello {children}</div>
}

export default function Profile() {
  return <Card><b>world</b></Card>
}
//最后会渲染成<div>hello <b>world</b></div>
function MyApp() {
    function speak(name) {
        console.log("name", name);
    }
    return (
        <>
            <Child name="VV" speak={(name) => speak(name)} />
        </>
    );
}


function Child({ name, speak }) {
    return (
        <>
            <div>我的名字是{name}</div>
            <button onClick={() => speak(name)}>说话</button>
        </>
    );
}
export default function Father(props) {
  return (
    <Child
      {...props}
    />
  );
}
function Child({ savedContact, onSave }) {
  ...
}

事件


Hook 与状态管理

以 use 开头的函数被称为 Hook,用于管理状态。

useState
const [state, setState] = useState(初始值)

注意,state在每次渲染中是固定的(快照),setState不会改变当前渲染中的state,只会改变下一次渲染的值!
如想要在当前渲染中就改变,可使用普通变量或useRef
一个事件处理函数会全部执行完再进行渲染,称为批处理

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(10);
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(n=>n + 1);
        setNumber(n=>n + 1);
        console.log(number);//0
      }}>点击后变成3</button>
    </>
  )
}
使用flushSync函数同步更新DOM

传入flushSync执行的setState操作会提前触发DOM渲染(但快照中的state保持不变),而不会等待批处理完成:

const [number, setNumber] = useState(0);
return (
   <>
       <h1 id="h1">{number}</h1>
       <button onClick={() => {
           flushSync(() => {
               setNumber(1);
               console.log(number);//0
               console.log(document.getElementById("h1").innerHTML);//0
           });
           console.log(number);//0
           console.log(document.getElementById("h1").innerHTML);//1
       }}>点我</button>
   </>
);
state 不保存在 JSX 标签里

state 与树中放置该 JSX 的位置相关联。因此在渲染树中相同位置的相同组件,其内部状态会得到保留:

//以下<Counter/>组件内部的state,在 isFancy 切换时会得到保留
{isFancy ? (
  <Counter isFancy={true} /> 
) : (
  <Counter isFancy={false} /> 
)}

//以下<Counter/>组件内部的state,在 isPlayerA 切换时会重置(因位置不同)
{isPlayerA &&
  <Counter person="Taylor" />
}
{!isPlayerA &&
  <Counter person="Sarah" />
}

重置其内部状态的几个方式:

突变 mutation

通过 setState 以外的方式造成的 state 数据变化称为突变(例如通过原生JS直接修改对象、数组的某个成员)。突变的结果会在下次渲染中显示,但突变本身不会触发渲染。
应当避免突变的发生


useReducer

当需要在多处分别对同一state做出不同修改时,可以通过useReducer代替useState进行统一管理。

import { useReducer } from 'react';
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
function tasksReducer(tasks, action) {
  switch (action.type) {
    case 1:
      return{
        ...tasks,
        age: action.age
      }
    default: {
      throw Error('未知 action: ' + action.type);
    }
  }
}

useRef(脱围)

类似useState,返回一个ref实例,用于在组件渲染间保留信息。
不要在渲染期间(即组件主体)读取或写入ref,应该移到事件处理程序或者 Effect 中。

import { useRef } from 'react';
const ref = useRef(0);
ref.current = ref.current + 1;//1
import { useRef } from 'react';
export default function Form() {
    const inputRef = useRef(null);
    const divRefs = useRef(new Map());
       return (
        <>
            <input ref={inputRef} />
            <button onClick={() => inputRef.current.focus()}>
                聚焦输入框
            </button>

            {
                [0, 1, 2, 3, 4, 5].map(i => (
                    <div key={i} ref={node => {
                        if(node){
                            divRefs.current.set(i, node)
                        }else{
                            divRefs.current.delete(i)
                        }
                    }}
                        onClick={() => {
                            divRefs.current.get(i).style.color = "red";
                        }}>
                        我是第{i}个,点我会变红
                    </div>
                ))
            }
        </>
    );
}

上述方法默认只能用于浏览器原生元素,当用于JSX中的自定义组件时,该组件需要由forwardRef方法创建(在此方法中还可以通过useImperativeHandle限制暴露的内容):

import { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
  return <input {...props} ref={ref} />;
});

export default function Form() {
  const inputRef = useRef(null);

  return (
    <>
      <MyInput ref={inputRef} />
    </>
  );
}


useEffect(脱围,由渲染引起的副作用,与React之外的系统同步)
//此处返回值内的ignore可以让同一次调用中的fetch回调失效,但不影响下一次的Effect
//可用于防止竞态条件,保证是最后一次触发的生效
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);
import { useEffect } from 'react';
useEffect(() => {
  // 这里的代码会在每次渲染后执行
});

useEffect(() => {
  // 这里的代码只会在组件挂载后执行
}, []);

useEffect(() => {
  //这里的代码只会在每次渲染后,并且 a 或 b 的值与上次渲染不一致时执行
}, [a, b]);

注意:组件内声明的对象、数组、函数等(包括父组件传递下来的props中的以上内容),因为每次渲染都不全等,如果传递给useEffect作为依赖则会导致每次渲染都触发useEffect。此时需要使用useMemouseCallback,或将该声明移动到组件外或Effect内。
而当某个响应式变量既需要参与useEffect内容,又不想因其变化而导致触发Effect,则可以使用useEffectEvent

useMemo 和 useCallback

useMemo 返回值,useCallback 返回函数。两者均用于避免组件每次渲染时的重复计算。

import { useMemo, useState, useCallback } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ 除非 todos 或 filter 发生变化,否则不会重新执行 getFilteredTodos()
  const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
  // ...
}
useEffectEvent

用于提取Effect中部分逻辑,以实现如下功能:存在某个响应式变量,既要在Effect中使用,又不想充当Effect的依赖项。

相比于useCallback的区别:

  1. 不用显式声明依赖
  2. 即使依赖变了,fn的引用也不变(与原来全等)
  3. 只能在useEffect内部使用,且不需要加入useEffect依赖
//在这里,onVisit 内的 url 对应 最新的 url(可能已经变化了),但是 visitedUrl 对应的是最开始引起这个 Effect(并且是本次 onVisit 调用)运行的 url 。
import { experimental_useEffectEvent as useEffectEvent } from 'react';

const onVisit = useEffectEvent(visitedUrl => {
  logVisit(visitedUrl, numberOfItems);
});

useEffect(() => {
  setTimeout(() => {
    onVisit(url);
  }, 5000); // 延迟记录访问
}, [url]);

使用 Immer 库简化 useState 和 useReducer

Immer 库可代替useStateuseReducer,以避免突变,并简化对象、数组的更新操作。

npm install use-immer
useImmer

提供的修改方法,其参数同样可以是一个更新函数或一个固定值。

import { useImmer } from 'use-immer';

const [person, updatePerson] = useImmer({
  name: 'Niki de Saint Phalle',
});

updatePerson(draft => {
  draft.name = e.target.value;
});
useImmerReducer

可以返回,也可以直接修改 draft 对象属性。
注意switch中如果不使用return,要使用break分隔case

import { useImmerReducer } from 'use-immer';

const [tasks, dispatch] = useImmerReducer(tasksReducer, [{id:1},{id:2},{id:3}]);
function tasksReducer(draft, action) {
  switch (action.type) {
    case 'changed': {
      const index = draft.findIndex((t) => t.id === action.task.id);
      draft[index] = action.task;
      break;
    }
    case 'deleted': {
      return draft.filter((t) => t.id !== action.id);
    }
    default: {
      throw Error('未知 action:' + action.type);
    }
  }
}

function handleChangeTask(task) {
  dispatch({
    type: 'changed',
    task: task,
  });
}

使用 Context 深层传递参数

用于祖先元素向其深层后代传递信息,以代替 props 逐级下传

// 单独的文件 MyContext.js
import { createContext } from 'react';
export const MyContext = createContext("默认值")

// 父组件提供Context,Context.Provider的后代元素都可以获得距离最近的值
import { MyContext } from './Context.js';
export default function App() {
  return (
    <>
      <MyContext.Provider value={100}>
      <List/>
      </MyContext.Provider>
    </>
  )
}

// 子组件获取Context
import { useContext } from 'react';
import { MyContext } from './MyContext.js';
function List() {
  const data = useContext(MyContext)
  return <div>{data}</div>
}
上一篇 下一篇

猜你喜欢

热点阅读