当vue老选手双修react并被其吊打这件事

2022.08.14
评论

本文以react18与其hooks举例,并不会过多叙述类写法

小坑篇

  1. 在使用React.StrictMode下,组件的默认渲染全部会执行两次,详情见 官方解释

暂不理解篇

组件篇

比较普通的变化

  1. 对于函数式组件来说,这个函数本身就是render函数,所以每次涉及到数据更新,都会导致函数重新创建,内部的变量也随之重新创建,这个需要自己通过hooks的使用进行优化
  2. jsx替换template,具体自行了解
  3. 组件标签上的class被className替代,这是jsx的原因,避免了class关键字冲突;组件的style属性需要手动定义并手动绑定你需要的节点
  4. 没有所谓的@与v-,只有jsx,具体自行了解
  5. 组件导入默认被注册,如果需异步注册使用需使用lazy
  6. 样式强制独立,抽离为单独文件,导入即可使用,也有css in js的各种写法,如emotion
  7. react-create-app脚手架内置了scss
  8. ref概念偏差。类组件会返回其实例,函数组件默认啥都不返回(因为他们没有实例,返回需使用forwardRef与useImperativeHandle配合),forwardRef还有转发节点的功能
  9. 提供了性能测试组件Profiler
  10. 提供了节点挂载接口Portals
  11. 空节点使用Fragment或代替

附加内容

React 顶层API

文档

名称类型作用
React.Componentclass类组件的基类
React.PureComponentclass和楼上的相似,但多了一个props 和 state浅比较更新的shouldComponentUpdate函数
React.memo组件缓存、高阶组件。props变化(浅比较)时才会更新,但组件内部使用 useState,useReducer 或 useContext 创建的内容会正常更新
React.Children接口对于子集(props.children)的一系列辅助函数,无需考虑子集是否为单一还是列表, 如 map count
React.Fragment组件空标签组件
React.createRef/React.forwardRef接口ref系操作接口
React.lazy函数组件动态引入加载函数
React.Suspense组件异步加载组件
React.startTransition函数非紧急更新内容

DOM 整改

  • className代替class
  • 新增dangerouslySetInnerHTML属性,相当于innerHTML,但对xss进行了处理
  • style 值只能是对象,不能是字符串;涉及到px单位的属性会自动增加px字符串,如 <div style={{ height: 10 }}>相当于 <div style={{ height: '10px' }}>
  • 整合了响应事件,如onCopy、onKeyDown等,具体查看 合成事件 – React (reactjs.org)

TS类型

react社区ts文档

路由篇v6

官方文档

路由嵌套/路由跳转/路由传参

App.jsx

import { Routes, Route, Navigate, useNavigate } from 'react-router-dom';
 
export default function App() {
  const navigate = useNavigate(); // 注意,此方法必须声明在根部
  
  // 函数式路由跳转
  const toMain = () => {
    navigate(`/home/main/${132}`, { // 携带param参数
      replace: true // 是否覆盖当前路由
    });
  };
  // 路由回退
  const back = () => navigate(-1)
    
  return (
    <div className="App">
      <Routes>
        {/* 默认空路由重定向 */}
      	<Route path="" element={<Navigate to="/study" />} /> 
        {/* 有嵌套的路由 */}
      	<Route path="/home/*" element={<Study />} /> 
        {/* 无嵌套的路由 */}
      	<Route path="/comic" element={<Comic />} />
       </Routes>
    </div>
  );
}

Home.jsx

export default function Home() {
  return (
    <div className="home">
      <Routes>
        {/* 带有param参数的子路由定义,path写法和vue类似 */}
      	<Route path="main/:id" element={<HomeMain />} />
      </Routes>
    </div>
  );
}

HomeMain.jsx

import { useParams } from 'react-router-dom';
 
export default function HomeMain() {
  const { id = -1 } = useParams(); // 路由param参数获取
  return (
    <div className="home-main">
      
    </div>
  );
}

路由传参

上面已经介绍了param传参,接下来介绍state与search传参(对应vue-router中的params和query);值得一提的是,这三种传参都不会因为页面刷新而消失

state/search

import { useNavigate, useLocation } from 'react-router-dom';
 
// ....
const navigate = useNavigate();
navigate('/A?name=lucy', {
    state: { // state参数设置
   		name: 'adic'
    }
});
 
// ....
const { state, search } = useLocation(); // 路由state参数获取
// 注意,state为一开始跳转时传入的对象,但search为原生字符串,如?name=lucy,需要自己拆分处理(这个我不确定有没有其他办法,v6文档有点emmm)

样式篇

未完待续

全局状态管理篇

redux与ts。实现我们需要@reduxjs/toolkit与react-redux两个依赖,请自行安装

Redux 中文官网 - JavaScript 应用的状态容器,提供可预测的状态管理。 | Redux 中文官网

基础配置

// store.ts 根仓库
 
import { configureStore } from '@reduxjs/toolkit';
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import user from './useUser.store'; // 实际仓库
 
// 从 store 本身推断出 `RootState` 和 `AppDispatch` 类型
export type RootState = ReturnType<typeof store.getState>;
// 推断出类型: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
 
export const store = configureStore({
  reducer: {
    user
  }
});
// 用于触发仓库的函数
export const useAppDispatch: () => AppDispatch = useDispatch;
// 用于获取仓库的值
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
// useUser.store.ts user仓库
 
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
 
const slice = createSlice({ // 仓库实例
  name: 'user', // 仓库名称
  initialState: { // 仓库值
    islogged: false,
    token: ''
  },
  reducers: { // 仓库函数,内部仅支持同步操作
    setToken: (state, { payload }: PayloadAction<string>) => {
      if (!payload) return;
      state.islogged = true;
      state.token = payload;
    }
  }
});
 
export const userStoreActions = slice.actions; // 供使用的函数(reducers中定义的)集合
 
export default slice.reducer;  // 根仓库需要的依赖
// index.tsx 项目入口组件
 
import { store } from './store'; 
import { Provider } from 'react-redux';
 
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
    <Provider store={store}> // 仓库注入
    	<App />
    </Provider>
);
reportWebVitals();
// xxx.tsx 某个使用到仓库的组件
 
import { useAppSelector, useAppDispatch } from './store';
import { userStoreActions } from './useUser.store';
 
export default function AppLogin() {
    const dispatch = useAppDispatch();
	
    // 此时isLogin是响应式的状态
    const isLogin = useAppSelector((state) => state.user.islogged);
    const login = () => {
        // 使用仓库函数时 需要使用dispatch包裹一次此函数
    	dispatch(userStoreActions.setToken(data.jwt)); 
    }
	return <></>
}
// xxx.ts 某个使用到仓库的ts文件
import { userStoreActions } from './useUser.store';
import { store } from './store';
 
function xx() {
    // 此操作为Thunk理念,详情请看一下文档
    // https://cn.redux.js.org/tutorials/essentials/part-5-async-logic#thunks-%E4%B8%8E%E5%BC%82%E6%AD%A5%E9%80%BB%E8%BE%91
    store.dispatch((dispatch, getState) => {
        const token = getState().user.islogged
    	dispatch(userStoreActions.setToken(''));
    });
}

hooks篇

通过hooks创建的内容都会被特殊处理(收集),无需担心组件重新渲染时对于变量的改变

useState

创建响应式变量;注意,在修改值时并不会立刻修改值,是一种异步的修改概念和this.setState相同;建议配合纯函数使用

const [a, setA] = useState(0) // 确定的初始值
const [b] = useState(()=> Math.random()) // 不确定但会在初始化生成的初始值
const [c] = useState(Math.random()) // 不确定但会在每次视图更新是生成的初始值
 
setA(1) // 普通的赋值
setA(a => a + 1) // 基于当前值的赋值

useReducer

复杂变量管理

  • 配合useContext可实现redux功能
const [list, listDispatch] = useReducer<
  React.Reducer<
    // list的数据类型
    number[],
    // listDispatch函数的参数类型。请使用联合类型,这样更有利于type对应的payload类型验证
    | {
      type: 'add';
      payload: number;
    }
    | {
      type: 'clear';
    }
  >,
  // ->a3回调函数的参数类型
  number[]
>(
  // a1 第一个参数为list当前值 第二个参数为listDispatch调用时的传入参数
  (state, action) => {
    // 下面根据不同的类型做不同的处理
    switch (action.type) {
      case 'add':
        return [...state, action.item];
      case 'clear':
        return [];
      default:
        throw new Error();
    }
  },
  // a2 list初始化的值
  [],
  // a3 可选 对->a2值的重写操作,常用于类型重定义
  (state) => state
);

useRef\useImperativeHandle

实例获取(默认情况下获取的是没有如何值的)\实例暴露值

  • useRef不会去监听绑定内容的变化,如果需要监听,请使用useCallback
  • useRef同样可以用于声明变量,但变量改变时并不会想useState那样触发视图更新,所以更适合节点或计时器id的记录

useEffect\useLayoutEffect

相当于监听属性\视图更新后触发的监听属性。

  • 此hooks生成的结果会被自动收集,无需进行useCallback等hooks的手动收集
  • 如果 Reducer Hook 的返回值与当前 state 相同,React 将跳过子组件的渲染及副作用的执行

useContext

组件多级通讯

useCallback

用以避免render函数执行时多次创建同一个函数,需要手动传入依赖值来确定是否重新创建;也可以用来监测dom

监测dom

export function useDom<T extends Element>(cb: (e: T | null) => void) {
  // eslint-disable-next-line
  return useCallback(cb, []);
}

useMemo

类似于计算属性,但需要手动传入其依赖值;也可以用于避免创建相同的内容

useDeferredValue

类似于防抖节流的值副本。可以配合memo来进行优化渲染

  • 注意,值副本是在正常变化的,减缓的是渲染
export default function Add() {
  const [a, c] = useState('');
  const v = useDeferredValue(a);
 
  return (
    <div className="add">
      <input type="text" onInput={(e: any) => c(e.target.value)} />
      {useMemo(
        () =>
          Array(10000)
            .fill(0)
            .map((_, i) => <p key={i}>{v}</p>),
        [v] // 如果将 v 替换为默认的 a,则会导致input持续输入时页面极其卡顿
      )}
    </div>
  );
}

useTransition

类似于防抖节流的函数执行。他会将传入内容认为是 非紧急渲染(默认其他都是紧急渲染)

  • 注意,值副本是在正常变化的,减缓的是渲染
export default function Add() {
  const [a, c] = useState('');
  const [isPending, startTransition] = useTransition();
  const sets = (e: string) => {
    startTransition(() => {
      c(e);
    });
  };
  return (
    <>
      <input type="text" onInput={(e: any) => sets(e.target.value)} />
      {
        // 在不断输入时,会发现h1标签在闪烁,因为延迟执行的原因
        isPending && <h1>{a}</h1>
      }
    </>
  );
}