React的 SuspenseReact的内置组件 中文意为 悬念 ，所以表现的是一种拿不准的状态，该组件的作用是，作为一个包裹组件，包裹我们需要加载的子级组件，在子级组件未加载完毕的时候展示替代组件（比如loading组件）<Suspense fallback={<Loading />}> <SomeComponent /> </Suspense>本文直接对目前Suspense的使用方法做一个直白讲解，Suspense的演变过程以及理念可以看文末的参考链接详细了解。常用用法Suspense 结合Data Fetch在我刚开始学习React的时候，Suspense还只能React.lazy结合使用用于加载异步组件。而现在Suspense已经可以对任何异步状态生效了,当然这需要遵守一定的规则。Suspense结合 Data Fetch的代码如下：import { fetchData } from './data'; export default function Albums() { const albums = use(fetchData(`/the-beatles/album`)); console.log('albums', albums); return ( {albums.map((album) => ( {album.title} ({album.year}) ))} ); } // This is a workaround for a bug to get the demo running. // TODO: replace with real implementation when the bug is fixed. function use(promise) { console.log('promise', promise); if (promise.status === 'fulfilled') { return promise.value; } else if (promise.status === 'rejected') { throw promise.reason; } else if (promise.status === 'pending') { throw promise; } else { promise.status = 'pending'; promise.then( (result) => { promise.status = 'fulfilled'; promise.value = result; }, (reason) => { promise.status = 'rejected'; promise.reason = reason; }, ); console.log('throw', promise); throw promise; } } 注意上述代码中的use方法就是将我们的异步请求方法转换为能够符合suspense用法的函数，即要求被Suspense包裹的组件是能够抛出一个Promise 异常。在这个 Promise 结束后再渲染组件，因此use函数需要在 Pending 状态时抛出一个 Promise，使其可以被 Suspense 捕获到。Suspense和lazy结合使用这个就是最开始大家都了解的使用方法了import React, { Suspense, lazy } from 'react'; const AsynComponent = lazy( async () => await lazyLoad(import('./lazyLoad')), ); export default function Index() { return ( <Suspense fallback={ loading.... }> <AsynComponent /> </Suspense> ); } // 模拟加载组件的延迟 async function lazyLoad(promise) { await new Promise((resolve) => { setTimeout(resolve, 2000); }); return promise; }动态加载外部组件最近工作中有做到在pc端动态加载H5组件资产的需求，用于展示发布的组件。这里我利用suspense和错误边界来进行一个封装，让其成为一个通用的组件。代码示例：错误边界组件这里按照需求自定义即可// ErrorBoundary.tsx import React, { ErrorInfo, ReactNode } from 'react'; interface Props { fallback: ReactNode; children: ReactNode; } interface State { hasError: boolean; } export default class ErrorBoundary extends React.Component<Props, State> { state: State = { hasError: false }; static getDerivedStateFromError(): State { return { hasError: true }; } componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error('ErrorBoundary caught an error:', error, errorInfo); } render() { if (this.state.hasError) { return this.props.fallback; } return this.props.children; } }动态加载组件import React, { Suspense, useRef } from 'react'; import ReactDOM from 'react-dom'; import ErrorBoundary from './ErrorBoundary'; // 手动定义全局变量 保证umd方式导入的组件加载正常 window.React = React; window.ReactDOM = ReactDOM; interface Props { src: string; // UMD 脚本地址 globalName: string; // UMD 暴露的全局变量名 fallback?: React.ReactNode; // 加载态 errorFallback?: React.ReactNode; // 错误态 componentProps?: Record<string, unknown>; // 组件的props } interface Thenable<T> extends PromiseLike<T> { then<TResult1 = T, TResult2 = never>( onfulfilled?: | ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: | ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null, ): Thenable<TResult1 | TResult2>; reason?: string; status?: 'rejected' | 'fulfilled' | 'pending'; value?: unknown; } const cache = new Map(); export function loadComponent( src: string, getData: (src: string) => Promise<unknown>, ) { if (!cache.has(src)) cache.set(src, getData(src)); return cache.get(src); } function use<T>(promise: Thenable<T>) { // console.log('promise', promise); if (promise.status === 'fulfilled') { return promise.value; } else if (promise.status === 'rejected') { throw promise.reason; } else if (promise.status === 'pending') { throw promise; } else { promise.status = 'pending'; promise.then( (result) => { promise.status = 'fulfilled'; promise.value = result; }, (reason) => { promise.status = 'rejected'; promise.reason = reason; }, ); // console.log('throw', promise); throw promise; } } export const UmdComponent: React.FC<Props> = ({ src, globalName, componentProps, }) => { const abortControllerRef = useRef<AbortController | null>(null); const scriptRef = useRef<HTMLScriptElement | null>(null); function loadScript(src: string): Promise<React.ElementType> { abortControllerRef.current?.abort(); abortControllerRef.current = new AbortController(); const { signal } = abortControllerRef.current; return new Promise((resolve, reject) => { // 1. 检查是否已加载 if (scriptRef.current) { // 清除脚本 document.head.removeChild(scriptRef.current); scriptRef.current = null; } // 2. 动态插入 const script = document.createElement('script'); script.id = 'globalName'; script.src = src; script.async = true; // 3. 加载回调 const onLoad = () => { if (signal.aborted) return; const ctor = (window as any)[globalName]; if (!ctor) { reject(new Error(`UMD global "${globalName}" not found.`)); return; } resolve(ctor); }; // 4. 错误回调 const onError = (event: Event | Error) => { console.log(event); reject(new Error('Script load failed')); }; script.addEventListener('load', onLoad); script.addEventListener('error', onError); document.head.appendChild(script); }); } const Component = use(loadComponent(src, loadScript)) as React.ElementType; return <Component {...componentProps} />; }; // 包装器：Suspense + ErrorBoundary export const UmdComponentLoader: React.FC<Props> = (props) => ( <ErrorBoundary fallback={props.errorFallback}> <Suspense fallback={props.fallback}> <UmdComponent {...props} /> </Suspense> </ErrorBoundary> );组件使用import React from 'react'; import { UmdComponentLoader } from './umd'; // 链接测试时请自行替换为真实umd方式的cdn export default function Index() { return ( <UmdComponentLoader src="https://xxx/umd-named/index.umd.min.js" globalName="xxx" componentProps={{ }} fallback={<>加载中。。。。</>} errorFallback={<>加载失败。。。。</>} /> ); }存在的问题fetch库支持情况：目前Suspense 的Data Fetch还是处于比较新的一个阶段，可以看到 一些 支持Suspense的框架都暂时不推荐在生产环境使用其Suspense模式，比如 swr和react-query，不过我们只要了解这个规则使用，自己简单的使用，比如 自定义类似与use（React19会提供这个方法）的方法 那也是没有问题的。当然还是要关注最新的变动。 swr React-query一闪而过的fallback：Suspense目前也存在问题就是 加载闪烁的问题，这个问题在以前结合lazy异步加载组件的时候经常遇到，如果第一次进入页面，组件加载的很快，那我们就会看到一闪而过的loading，这对用户来讲是十分糟糕的体验。我们比如可以考虑为Suspense设置一些新的属性，用于控制fallback 的最小出现时刻，或者控制fallback至少显示多长时间。目前Suspense还未支持这一特性，当然期待未来会支持这个特效。github上也有这个issue当然我们可以对fallback组件进行改造来支持这一特性<Suspense fallback={MyFallback} />; const MyFallback = () => { // 计时器，200 ms 以内 return null，200 ms 后 return <Spin />(也可以结合设置最短展示Spin多少ms) }附录代码// fetchData --data // Note: the way you would do data fetching depends on // the framework that you use together with Suspense. // Normally, the caching logic would be inside a framework. const cache = new Map(); // 注意这个缓存函数，如果不设置缓存的，use函数每次都会生成一个新的promise，导致永远无法更新promise状态，进而导致无限渲染 export function fetchData(url) { if (!cache.has(url)) { cache.set(url, getData(url)); } return cache.get(url); } async function getData(url) { if (url === '/the-beatles/albums') { return await getAlbums(); } else { throw Error('Not implemented'); } } async function getAlbums() { // Add a fake delay to make waiting noticeable. await new Promise((resolve) => { setTimeout(resolve, 2000); }); return [ { id: 13, title: 'Let It Be', year: 1970, }, { id: 12, title: 'Abbey Road', year: 1969, }, { id: 11, title: 'Yellow Submarine', year: 1969, }, { id: 10, title: 'The Beatles', year: 1968, }, { id: 9, title: 'Magical Mystery Tour', year: 1967, }, { id: 8, title: "Sgt. Pepper's Lonely Hearts Club Band", year: 1967, }, { id: 7, title: 'Revolver', year: 1966, }, { id: 6, title: 'Rubber Soul', year: 1965, }, { id: 5, title: 'Help!', year: 1965, }, { id: 4, title: 'Beatles For Sale', year: 1964, }, { id: 3, title: "A Hard Day's Night", year: 1964, }, { id: 2, title: 'With The Beatles', year: 1963, }, { id: 1, title: 'Please Please Me', year: 1963, }, ]; }参考文章https://react.dev/reference/react/Suspensehttps://juejin.cn/post/6844904093463363591#heading-0https://indepth.dev/posts/1044/why-react-suspense-will-be-a-game-changer
虚拟滚动虚拟滚动在前端是一个很常见的性能优化解决方案。当我们需要渲染大量的dom的时候（比如几十万个列表数据），由于浏览器的内存限制，几十万个dom节点全部渲染出来就会导致页面滚动卡顿。而虚拟滚动就是模拟浏览器的原生滚动方式，按需加载列表元素，避免大量渲染dom元素造成卡顿。基本思路根据容器高度和每个列表项的高度计算出我们需要展示多少条数据发生滚动的时候记录滚动高度根据滚动的高度截取应该展示数组中的那些数据对于每个列表项的高度，我们这里可以分为定高的列表项和不定高的列表项定高定高的思路比较简单，首先初始化的时候，记录容器的高度，列表项的高度，从而计算出，当前视口要展示的元素的起始索引，/** * list 带有高度的数组 * offset 滚动offset */ const findIndexOverHeight = (list: ICalculationList[], offset: number) => { let currentHeight = 0; for (let i = 0; i < list.length; i++) { currentHeight += list[i].domHeight; if (currentHeight > offset) { return i; } } return list.length - 1; } 我们的开始索引就是offset=0 所计算出来的位置，结束索引就是 offset+containerHeight 所计算出来的高度let offset = 0; let start = findIndexOverHeight(list, offset); let end = findIndexOverHeight(list, offset+containerHeight);所以我们根据上述的函数就可以计算出来视图展示所要的数据范围了，再使用slice(startIndex,endIndex)就可以截取需要展示的数据，然后我们再将这些数据渲染到视图中。但是上述渲染出来后可以发现，我们都是从头到位完整的渲染这些数据项，而滚动有时候并不会都将视图中的数据项都完整的展示出来，有些数据项可能只展示了一部分，这里就需要设置偏移量来使我们的滚动贴合原生滚动的效果。可以看到图中的renderOffset就是偏移量，计算方式就是 滚动偏移量 减去 视图起始位置元素距离顶部的高度 ，所以为了设置偏移量，还需要设置一个包裹元素来包裹所渲染的列表项，我们的偏移量就加在这个包裹元素上，我们一般可以使用translateY来设置。不定高相比定高，不定高就在渲染前设置一个预估高度，这个高度指的是每个数据项的最小高度，然后再渲染出来列表元素后获取每一个元素的实际高度，最后按照定高的逻辑进行重新渲染。总结对于虚拟列表，主要思路就这些，我参照其他优秀博主的方案实现了一个React版的虚拟滚动，主要功能和问题如下：自定义的滚动条支持无限加载，滚动到底部再添加新数据10万+的数据，定高和非定高都可以流畅滚动 百万级数据快速拖滚动条会卡顿，定高的不会，主要是非定高的需要重新计算高度，所以时间都消耗在计算上了。数据量超过10万的情况快速将滚动条从顶部拖动到底部可能会出现滚动不到最后一项的bug，这点还没解决。对于列表项中有图片的情况，这个场景比较少见，所以暂时没有实现，实现思路的话也是和非定高一样，只是需要动态监听每一个列表项的高度变化，然后再进行更新。源码代码示意参考本文只是简略说了下思路，详细的可以看如下文章新手也能看懂的虚拟滚动实现方法react-virtualized
路由拦截在 Vue 中有现成的 API，所以在 Vue 中实现起来并不复杂。但是 React-router 中并没有这个功能，作者的目的是为了更加的灵活，鼓励我们自己去实现这个功能。当然思路很简单，我的思路就是在每个路由组件外层包裹一层组件进行判断。本次路由使用的是v6版本，与之前的版本有很大的改动，所以请事先去了解这些变化。talk is cheap show me my code首先基本目录如下：在App.jsx中导入路由配置：import { Link } from "react-router-dom"; import { Route, Routes, useRoutes } from "react-router-dom"; import "./App.css"; import routes from "./router"; function App(props) { const elements = useRoutes(routes); return ( 跳转 router1 | router2 {elements} ); } export default App; router.jsx内容如下：/* * @Date: 2022-01-23 20:15:56 * @LastEditors: zhangheng * @LastEditTime: 2022-03-09 16:18:35 */ import React, { lazy, Suspense, ComponentType } from "react"; import type { RouteObject } from "react-router-dom"; import Authorized from "./authorized"; import NoAuthor from "@/pages/403"; const routes: RouteObject[] = [ { path: "/router1", element: () => import("@/pages/router1"), }, { path: "/router2", element: () => import("@/pages/router2"), }, { path: "/403", element: <NoAuthor />, }, ]; type lazyPropsType = () => Promise<{ default: ComponentType }>; interface propsType { importRoute: lazyPropsType; } //定义自动包裹Suspense函数 function LazyElement(props: propsType) { return function () { const { importRoute } = props; const LazyComponent = lazy(importRoute); return ( <Suspense fallback={ 路由懒加载... }> <LazyComponent /> </Suspense> ); }; } // 处理routes 如果element是懒加载，要包裹Suspense,这里还自动包了一层懒加载组件 function dealRoutes(routesArr: RouteObject[]) { if (routesArr && Array.isArray(routesArr) && routesArr.length > 0) { routesArr.forEach((route) => { if (route.element && typeof route.element == "function") { const importRoute = route.element as lazyPropsType; route.element = ( <Authorized component={LazyElement({ importRoute })} include={["/router1", "/router2"]} /> ); } if (route.children) { dealRoutes(route.children); } }); } } dealRoutes(routes); export default routes; authorized.tsx鉴权组件，很简洁的：/* * @Date: 2022-03-09 11:49:30 * @LastEditors: zhangheng * @LastEditTime: 2022-03-09 16:17:18 */ //这里是路由拦截组件判断进入的路由是否有权限 import { memo, PropsWithChildren, ComponentType } from "react"; import { Navigate, useLocation } from "react-router-dom"; interface RediectType { path?: string; [key: string]: any; } function Rediect(props: PropsWithChildren<RediectType>) { const { path = "/" } = props; return <Navigate to={path} />; } interface AuthorizedType { component: ComponentType; include?: string[]; //需要鉴权的路由 } export default memo(function Authorized( props: PropsWithChildren<AuthorizedType> ) { //props/state //判断逻辑 const isToken = false; const { component: RouteComponent, include } = props; function returnRouteComponent(include: string[] = []) { const location = useLocation(); if (include.includes(location.pathname)) { //当然判断登陆了之后还可以判断用户是否有权限，这里不再补充 return isToken ? <RouteComponent /> : <Rediect path="/403" />; } else { return <RouteComponent />; } } //redux hooks //other hooks //其他逻辑 return returnRouteComponent(include); }); 当然我只是简单的设置了几个配置项，所以根据项目复杂度自行进行丰富吧。
React使用动画我们在开发中可以使用一些动画来控制组件消失和隐藏的过渡动画，增加用户交互的体验。React社区提供了一个库react-transition-group来完成过渡动画。使用网址参考安装方式：//npm npm install react-transition-group -S //yarn yarn add react-transition-group -D这个库可以帮助我们实现组件的入场和离场动画，实际上就是为组件添加类名，根据不同的时刻，我们可以就此来设置过渡动画。该库体积很小，主要包含四个组件：Transition， CSSTransition， SwitchTransition ，TransitionGroupTransition该组件是一个与平台无关的组件，不一定要结合css，如果要结合css可以使用CSSTransition。这个组件常用于组件的挂载和卸载的过渡动画，该组件默认情况下Transition组件不会改变它呈现的组件的行为，它只跟踪组件的enter和exit状态。这取决于你赋予的状态效果。例如，当组件进入或退出时，我们可以向它添加style样式，以下是一个案例：import React, { PureComponent } from "react"; import { Transition } from "react-transition-group"; const duration = 300; const defaultStyle = { transition: `opacity ${duration}ms ease-in-out`, opacity: 0, }; const transitionStyles = { entering: { opacity: 1 }, entered: { opacity: 1 }, exiting: { opacity: 0 }, exited: { opacity: 0 }, }; export default class transitionDemo extends PureComponent { render() { return ( <Transition in={this.props.in} timeout={duration}> {(state) => ( I'm a fade Transition! )} </Transition> ); } }import React, { useState } from "react"; import CSSTransitionDemo from "./CSSTransitionDemo"; import SwitchTransitionDemo from "./SwitchTransitionDemo"; import TransitionGroupDemo from "./TransitionGroupDemo"; import TransitionDemo from "./transitionDemo"; function App() { const [show, setShow] = useState(false); return ( <TransitionDemo in={show} timeout={500} /> setShow(!show)}>点击隐藏 ); } export default App; 我们可以控制in的值来控制为组件赋予状态首先我们初始化in的值为false，那么组件内容默认不会显示，即处于exited状态，那么style设置的样式透明为0，就不会显示。当我们点击按钮，组件进入entering状态，当500ms（timeout）后组件处于entered状态。再次点击按钮组件处于exiting状态，延时之后组件就回归exited状态了。本质上就是在不同的状态添加不同的style样式。CSSTransition该组件在组件切换的不同时刻为组件添加类名，我们可以按照这些类名来给组件添加动画。该组件在执行的额过程中有三个状态，分别为：appear，enter，exit每个状态对应一组类名：appear状态： -appear，-enter，-exitenter状态：-appear-active，-enter-active，-exit-activeexit状态：-appear-done，-enter-done，-exit-done组件常用的属性in 触发进入或者退出状态。当为true的时候组件触发进入状态，会为组件添加class类名 -enter，-enter-acitve，动画结束后移除之前的两个class，并添加-enter-done类名。当为false的时候组件触发离开状态，会为组件添加-exit，-exit-active这两个类名，当动画结束时移除这两个类名，添加类名-exit-doneclassName动画class类名的前缀。决定了在编写css时，对应的class名称：比如card-enter，card-enter-active，card-enter-done。class类这个属性变化存在的时间是按照time来设置的，反正最好保证和css的动画时间一致timeout过渡动画的时间，决定了执行动画时对应类名的存在时间。appear该属性决定了是否在第一次进入时添加动画，需要一开始的in为true，即页面刷新和第一次加载进入时就会执行动画，也是添加对应的类。unmountOnExit退出动画结束后是否卸载组件，该值默认为false还有常见的钩子函数等。案例演示：import React, { PureComponent } from "react"; import { CSSTransition } from "react-transition-group"; import "./csstransition.css"; export default class CSSTransitionDemo extends PureComponent { constructor(props) { super(props); this.state = { isShow: true, }; } render() { return ( <CSSTransition appear unmountOnExit={false} in={this.state.isShow} classNames="card" timeout={300} onEnter={el=>{console.log('开始状态')}} onEntering={el=>{console.log('进入状态')}} > 哈哈啊 哈哈啊 哈哈啊 哈哈啊 </CSSTransition> { this.setState({ isShow: !this.state.isShow }); }} > 显示/隐藏 ); } }.card-enter, .card-appear { opacity: 0; } .card-enter-active,.card-appear-active { opacity: 1; transition: opacity 300ms; } /* 显示动画结束*/ /* .card-enter-done. card-appear-done { } */ .card-exit { opacity: 1; transform: scale(1); } .card-exit-active { opacity: 0; transform: scale(0.8); transition: opacity 300ms,transform 300ms; } .card-exit-done { opacity: 0; transform: scale(0.6); }import CSSTransitionDemo from "./CSSTransitionDemo"; function App() { const [show, setShow] = useState(false); return ( <CSSTransitionDemo /> ); } export default App;SwitchTransition该组件可以完成两个组件切换时的动画显示。常用属性mode两个值：in-out表示新组件先进来，旧组件再移除。out-in表示旧组件先移除，新组件再进入。注意： SwitchTransition组件里面要有CSSTransition或者Transition组件，不能直接包裹你想要切换的组件SwitchTransition里面的CSSTransition或Transition组件不再像以前那样接受in属性来判断元素是何种状态，取而代之的是key属性，保证不同状态的 key不同即可。案例展示：import React, { PureComponent } from "react"; import "./switchtransition.css"; import { SwitchTransition, CSSTransition } from "react-transition-group"; export default class SwitchTransitionDemo extends PureComponent { state = { isOn: true }; render() { /* 关于为什么要设置time属性，动画执行的时间是按照css设置的， 而class类这个属性变化存在的时间是按照time来设置的，反正最好保证这两个时间一致 */ return ( <SwitchTransition mode="out-in"> <CSSTransition key={this.state.isOn ? "on" : "off"} classNames="btn" timeout={300} > { this.setState({ isOn: !this.state.isOn }); }} > {this.state.isOn ? "on" : "off"} </CSSTransition> </SwitchTransition> ); } }.btn-enter { opacity: 0; transform: translateX(100%); } .btn-enter-active { opacity: 1; transform: translateX(0); transition: opacity 300ms, transform 300ms; } .btn-exit { transform: translateX(0); opacity: 1; } .btn-exit-active { opacity: 0; transform: translateX(-100%); transition: opacity 300ms,transform 300ms; }import SwitchTransitionDemo from "./SwitchTransitionDemo"; function App() { return ( <SwitchTransitionDemo /> ); } export default App;TransitionGroup当我们有一组动画要执行的时候可以使用该组件，比如一个列表增删时的过渡动画。案例展示：import React, { PureComponent } from "react"; import { TransitionGroup, CSSTransition } from "react-transition-group"; import "./transitiongroup.css"; export default class TransitionGroupDemo extends PureComponent { state = { arr: ["zhangsan", "lisi", "wan"], }; render() { return ( this.addArr()}>添加 <TransitionGroup> {this.state.arr.map((item, index) => { return ( <CSSTransition key={index} timeout={500} classNames="item"> {item} </CSSTransition> ); })} </TransitionGroup> ); } addArr() { this.setState({ arr: [...this.state.arr, "haha"], }); } }.item-enter { opacity: 0; } .item-enter-active { opacity: 1; transition: opacity 500ms; } /* 显示动画结束*/ .item-enter-done { } .item-exit { opacity: 1; } .item-exit-active { opacity: 0; transition: opacity 500ms; } .item-exit-done { opacity: 0; }import TransitionGroupDemo from "./TransitionGroupDemo"; function App() { return ( <TransitionGroupDemo /> ); } export default App; 目前到这里就结束了，这些动画只是基本使用，可以配合上一些动画第三库来食用更佳。
React中书写css代码React本身就是一个支持组件化的框架，但是React并没有对css处理好组件化。相较于Vue，Vue这个一块就处理的很好，css放置在style标签中，而且可以设置scope作用域，防止样式覆盖，也可以设置css预处理器。在组件化开发中我们需要书写局部的css，全局的css，动态的css(根据状态来设置css)。React中书写css就没有那么的方便，下面介绍一些在React中css的书写方式。内联样式官方推荐的一种写法。export default class componentName extends Component { render() { return ( 你好 世界 ); } }css中短横线链接的属性需要转为小驼峰的形式来书写，以对象的形式书写css样式，同时可以引入state中的属性来动态引入属性。当然缺点很明显，如果需要书写大量的代码，那这种方式一定不方便，没有代码提示等。另外一些样式也不能书写，伪元素等选择器是不能书写的。普通的css引入我们将css单独写在一个文件中，在引入到文件中。import React, { Component } from "react"; import './index.css' export default class componentName extends Component { render() { return ( 你好 世界 ); } }但这样引入的css没有组件之间的作用域，是作用在全局的，属于全局css，会相互影响。所以适合写一些基本的全局样式。css module 写法该方法不是React自带的方法，只要在类似webpack配置打包环境下都可以使用。如果在其他项目中使用我们需要自己配置，配置webpack.config.js中的属性modules:true等。React脚手架已经配置了css modules的配置。我们书写的.css/.less/.scss都需要修改为.module.css/.module.less/.module.scss。引入方式，需要一个变量来接受，className需要以{style.className}形式来书写import React, { Component } from "react"; import style from './index.module.css' export default class componentName extends Component { render() { return ( 你好 世界 ); } }这样就避免了作用域问题，渲染出来的className如下：当然缺陷也有，比如引入的类名不能是短横线连接，不方便修改属性。但这方法也是不错的css书写方式。CSS in JS通过js来增强React中css书写的一种方式。当然我们学些这个方式前先了解下ES6的一种语法，ES6标签模板字符串我们使用该方法需要引入第三方库，这里推荐以下几个库：styled-componentsemotionglamorous我们以styled-components这个库来演示，这个库在社区中是很流行的。安装方式：//npm方式 npm install styled-components -S //yarn方式 yarn add styled-components该库本质上是调用函数来创建一个组件，组件会添加不会重复的class。该方式支持css预处理器的嵌套写法，支持css的各类写法。基本用法：import styled from "styled-components";//创建一个组件 const TitleWrapper = styled.div` color: yellow; .title { background-color: tomato; &::after { content: "---aaa"; } } `;这个函数中根元素为div，其中的样式只作用于该组件中export default class CssInJS extends Component { state = {color:'pink'} render() { return ( <TitleWrapper> 我是谁 hahah </TitleWrapper> ); } } 渲染结果：props,attr属性props可以穿透，直接作用在元素上const InputWrapper = styled.input` background-color: red; `; export default class CssInJS extends Component { state = {color:'pink'} render() { return ( {/* 属性是可以穿透的，直接应用到标签上 */} <InputWrapper type="password"></InputWrapper> ); } }渲染结果：另外获取props需要通过${}形式获取，其中定义一个函数，props会作为函数的参数。const InputWrapper = styled.input` background-color: red; padding: ${(props) => props.padding}; `; export default class CssInJS extends Component { state = { color: "pink" }; render() { return ( <InputWrapper type="text" padding="20px"></InputWrapper> ); } }attr属性，为元素设置一些默认属性const Input2Wrapper = styled.input.attrs({ placeholder: "zhangsan", bColor: "red",//这种不存在的属性不能添加 'data-day':111 })` background-color: #f2f2f2; color: ${(props) => props.color}; `; export default class CssInJS extends Component { state = { color: "pink" }; render() { return ( <Input2Wrapper color={this.state.color} /> ); } }渲染效果：高级特性支持样式继承我们可以将另一个组件传入函数中来实现样式的继承const SuperButton = styled.button` padding: 8px 30px; border-radius: 5px; `; const MyButton = styled(SuperButton)` background-color: red; color: #bfa; `; export default class CssInJS extends Component { render() { return ( <MyButton>按钮</MyButton> ); } }渲染效果：主题设置import CIJ from "./cssInJS"; import { ThemeProvider } from "styled-components"; function App() { return ( 你好 <ThemeProvider theme={{color:'#bcf', fontSize:'30px'}}> <CIJ></CIJ> </ThemeProvider> ); } export default App;后代元素中获取theme中的属性const TitleWrapper = styled.div` color: ${(props) => props.theme.color}; font-size: ${(props) => props.theme.fontSize}; .title { background-color: tomato; &::after { content: "---aaa"; } } `;动态添加className在项目中我们需要为组件动态的添加一些css类名，这在react中只能自己实现。React没有为我们提供像Vue中的一些模板语法(比如一些指令v-bind等)，我们可以借助一个第三方库classnames来简化我们的工作量。//npm npm install classnames -S //yarn yarn add classnamesimport React, { Component } from 'react' import classNames from 'classnames'; export default class App extends Component { constructor(props){ super(props); this.state = { isActive:true, info:'info' } } render() { return ( 你好 世界 000 111 222 ) } }渲染结果：
Redux的使用纯函数函数式编程中有一个概念就是纯函数，JS中也有这个概念。在React中，纯函数的概念非常重要。定义：如果一个函数满足以下条件就可以称之为纯函数。此函数在相同的输入值时，需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关，也和由I/O设备产生的外部输出无关。就是输入值相同的时候，你无论调用该函数多少次输出的值都是一样的。该函数不依赖使用全局变量，常量是可以的。不会修改程序的状态，或者不能引起副作用。即不能影响输出值以外的内容。案例：//纯函数 function (a, b) { return a+b; } //不是纯函数,当然你可以把let改为const那就称之为纯函数了，因为foo成为常量了，是无法被修改的 let foo = 1; function (a) { return foo+a; }总之纯函数的出现是为了时函数不那么复杂，更容易调试和组合。React中之所以要求函数组件或class组件都要想纯函数一样是为了保护props不被修改。Redux概念目前使用JS开发的程序已经越来越复杂了，所以我们需要管理的状态也越来越多和复杂。我们在加入了组件化开发模式后需要对每个组件的状态进行合理的管理，有时还需涉及状态共享。所以为了更好的管理这些状态，我们可以使用redux这个状态管理库来管理我们的状态。Redux就是一个帮助我们管理State的容器：Redux是JavaScript的状态容器，提供了可预测的状态管理，使得状态更加可控和追踪。Redux并不依赖与React，还可以和Vue一同使用，本身体积很小（2kb左右）。首先看看React的工作原理：由上图可以看出Redux主要由三个核心部份组成action， store， reducer。核心概念之store该模块是把state和action和reducer联系起来，引入reducer文件然后向外暴露store对象。我们可以通过store获得state和派发action。核心概念之actionRedux要求所有修改数据的操作都要通过action来派发，action一般是一个JS对象，用来描述此次更新的type和content。核心概念之reducer该模块就是将state和action联系起来，reducer就是一个纯函数，用于接收action来生成新的state。简单案例展示，单独使用redux//导入redux const redux = require("redux"); //初始state const initialState = { counter: 0, }; //创建一个reducer function reducer(state = initialState, action) { switch (action.type) { case "INCREMENT": return { ...state, counter: state.counter + 1 }; case "DECREMENT": return { ...state, counter: state.counter - 1 }; case "ADD_NUM": return { ...state, counter: state.counter + action.num }; case "SUB_NUM": return { ...state, counter: state.counter - action.num }; default: return state; } } //store const store = redux.createStore(reducer); //action const action1 = { type: "INCREMENT" }; const action2 = { type: "DECREMENT" }; const action3 = { type: "ADD_NUM", num: 5 }; const action4 = { type: "SUB_NUM", num: 12 }; //订阅store修改 store.subscribe(() => { console.log('state发送了改变'); console.log(store.getState()); }) //派发action store.dispatch(action1); store.dispatch(action2); store.dispatch(action3); store.dispatch(action4);输出结果展示：state发送了改变 { counter: 1 } state发送了改变 { counter: 0 } state发送了改变 { counter: 5 } state发送了改变 { counter: -7 }Redux的三大原则单一数据源整个应用的state应该只存在一个store中尽量只创建一个store，便于管理，虽然可以创建多个。单一数据源对状态的追踪，修改，维护。state是只读的不能直接修改state的值，我们只能根据旧的state来返回新的state。唯一修改state的方法就是通过派发action。保证所有的修改都被集中化处理，并且按照严格的顺序来执行，所以不需要担心race condition（竞态）的问题。使用纯函数来修改通过reducer将 旧state和 action联系在一起，并且返回一个新的state。所有的reducer都应该是纯函数，不能产生任何的副作用。Redux结合React使用在实际开发中我们管理的状态是很多的，所以我们需要将React的模块分开管理，也可以按照状态的种类进行分类。首先我们定义一个store文件夹，这里就存放redux相关的模块文件。然后创建action.js index.js reducer.js constant.js这四个文件夹，当然可以根据自己喜好命名我们还是演示一个求和案例：action.jsimport { ADD_NUM, SUB_NUM } from "./constant.mjs"; export const action1 = (num) => ({ type: ADD_NUM, num, }); export const action2 = (num) => ({ type: SUB_NUM, num, });constant.js//对action的类别名进行常量化，方便管理 export const ADD_NUM = 'ADD_NUM'; export const SUB_NUM = 'SUB_NUM';reducer.jsimport { ADD_NUM, SUB_NUM } from "./constant.mjs"; const defaultState = { counter: 0, }; export const reducer = (state = defaultState, action) => { switch (action.type) { case ADD_NUM: return { ...state, counter: state.counter + action.num }; case SUB_NUM: return { ...state, counter: state.counter - action.num }; default: return state; } };index.jsimport redux from "redux"; import { reducer } from "./reducer.mjs"; const store = redux.createStore(reducer); export default store;这里我定义两个了两个组件：home.jsimport React, { PureComponent } from "react"; //引入store和action import store from "../redux-test/test分模块/index.mjs"; import { action1, action2 } from "../redux-test/test分模块/action.mjs"; export default class home extends PureComponent { state = { counter: store.getState().counter, }; render() { return ( Home 当前计数{this.state.counter} { this.addNum(); }} > + { this.subNum(); }} > - ); } componentDidMount() { //监听state改变 this.unSubscribe = store.subscribe(() => { this.setState({ counter: store.getState().counter, }); }); } //取消订阅 componentWillUnmount() { this.unSubscribe(); } //派发action addNum() { store.dispatch(action1(1)); } subNum() { store.dispatch(action2(2)); } }about.js//和home是一样的，主要展示state的共享 import React, { PureComponent } from "react"; import store from "../redux-test/test分模块/index.mjs"; import { action1, action2 } from "../redux-test/test分模块/action.mjs"; export default class About extends PureComponent { state = { counter: store.getState().counter, }; render() { return ( About 当前计数{this.state.counter} { this.addNum(); }} > + { this.subNum(); }} > - ); } componentDidMount() { this.unSubscribe = store.subscribe(() => { this.setState({ counter: store.getState().counter, }); }); } componentWillUnmount() { this.unSubscribe(); } addNum() { store.dispatch(action1(1)); } subNum() { store.dispatch(action2(2)); } }效果如下：点击一次按钮后：我们现在就简单的演示了rearedux结合react的使用，但是这样使用还有很多的缺点。比如，两个文件都需要引入store，同时都要编写监听state改变的订阅函数（当然解决方式之一是将订阅函数写在index.js中监听根app组件）。可以看到两个组件都编写了很多重复代码。connect函数为了解决上面出现的问题，我们现在编写一个函数来对代码进行抽取。//利用高阶函数来传递state中的数据和action,增强生命周期 import { PureComponent } from "react"; import store from "../redux-test/test分模块/index.js"; //自行封装一个connect函数 export default function connect(mapStateToProps, mapDispatchToProps) { return function enhanceHOC(WrappedComponent) { return class extends PureComponent { constructor(props) { super(props); this.state = { storeState: mapStateToProps(store.getState()), }; } //订阅更新组件 componentDidMount() { this.unsubscribe = store.subscribe(() => { this.setState({ storeState: mapStateToProps(store.getState()), }); }); } //取消订阅 componentWillUnmount() { this.unsubscribe(); } render() { return ( <WrappedComponent {...this.props} {...mapStateToProps(store.getState())} {...mapDispatchToProps(store.dispatch)} /> ); } }; }; }组件中使用：import React, { PureComponent } from "react"; import { action1, action2 } from "../redux-test/test分模块/action.js"; import connect from "../utils/connect"; function About(props) { return ( About 当前计数{props.counter} { props.action1(5); }} > + { props.action2(2); }} > - ); } //需要定义这两个 const mapStateToProps = (state) => { return { counter: state.counter, }; }; const mapDispatchToProps = (dispatch) => { return { action1: (num) => { dispatch(action1(num)); }, action2: (num) => { dispatch(action2(num)); }, }; }; export default connect(mapStateToProps, mapDispatchToProps)(About);相当于通过这个函数将获取state和派发action的方式放置到组件的props中，同时检测订阅state更新的函数以高阶函数方式集成进来。但是我们注意这个connect函数是依赖于store，如果我们想把它作为一个单独的库来封装的话是不够独立的。所以我们可以想办法将store以context方式传递给组件。另外创建一个文件context.jsimport React from 'react'; const StoreContext = React.createContext(); export { StoreContext }在index.js中导入import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import store from "./redux-test/test分模块/index.js"; import { StoreContext } from "./utils/context"; ReactDOM.render( <React.StrictMode> <StoreContext.Provider value={store}> <App /> </StoreContext.Provider> </React.StrictMode>, document.getElementById("root") );改进后的connect.jsimport { PureComponent } from "react"; //这里需要引入store，所以不够独立，不能作为一个库文件使用,所以我们要换一种方式来获取store //import store from "../redux-test/test分模块/index.mjs"; import { StoreContext } from "../utils/context"; //自行封装一个connect函数 export default function connect(mapStateToProps, mapDispatchToProps) { return function enhanceHOC(WrappedComponent) { return class extends PureComponent { constructor(props, context) { //在构造函数中要使用参数中context,不能直接使用this.context,现在还获取不到 super(props); this.state = { storeState: mapStateToProps(context.getState()), }; } //获取context static contextType = StoreContext; //订阅更新组件 componentDidMount() { this.unsubscribe = this.context.subscribe(() => { this.setState({ storeState: mapStateToProps(this.context.getState()), }); }); } //取消订阅 componentWillUnmount() { this.unsubscribe(); } render() { return ( <WrappedComponent {...this.props} {...mapStateToProps(this.context.getState())} {...mapDispatchToProps(this.context.dispatch)} /> ); } }; }; }React-redux通过上述操作我们简化了在组件中操作store中state的步骤，但实际上React为我们提供了一个库叫做react-redux我们实现的connect就是大致还原了这个库的基本用处。当然这个库肯定功能逻辑都比自己书写的更加全面和缜密。安装方式：//npm npm install react-redux //yarn yarn add react-redux使用方式：import React, { PureComponent } from "react"; import { action1, action2, action3, action4, } from "../redux-test/test分模块/action.js"; //引入官方的connect函数 import { connect } from "react-redux"; import axios from "axios"; class home extends PureComponent { render() { return ( Home 当前计数{this.props.counter} { this.props.action1(2); }} > + { this.props.action2(1); }} > - ); } componentDidMount() { axios.get("http://123.207.32.32:8000/home/multidata").then((res) => { const data = res.data.data; this.props.changeBanners(data.banner.list); this.props.changeRecommends(data.recommend.list); }); } } const mapStateToProps = (state) => ({ counter: state.counter, banners: state.banners, recommends: state.recommends, }); const mapDispatchToProps = (dispatch) => ({ action1: (num) => dispatch(action1(num)), action2: (num) => dispatch(action2(num)), changeBanners(banners) { dispatch(action3(banners)); }, changeRecommends(recommends) { dispatch(action4(recommends)); }, }); export default connect(mapStateToProps, mapDispatchToProps)(home);import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import store from "./redux-test/test分模块/index.js"; //提供了Provider组件来代替context方式 import { Provider } from "react-redux"; ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById("root") ); React中的异步操作我们通过store保存的state数据还可能通过网络请求获取，所以就会设计到异步操作，我们虽然可以将异步操作写在组件中，但是这会使得代码不容易管理当然也不够优雅，而且网络请求的数据也是redux的一部分，所以我们推荐将异步步骤也写在redux中进行管理。以下是设计好的加上异步任务的redux流程图实际上就是需要一个中间件来扩展action到renducer这之间的代码。React-thunk官方推荐的处理异步请求的中间件就是使用react-thunk安装方式：//npm npm install react-thunk //yarn yarn add react-thunk使用方式：在导入store的index.js文件中添加中间件import { createStore, applyMiddleware } from "redux"; import thunk from "redux-thunk"; import { reducer } from "./reducer.js"; const store = createStore(reducer, applyMiddleware(thunk)); export default store;这时候我们的action就可以返回一个函数了//redux-thunk中定义函数，同步的action返回对象，异步的返回一个函数 export const getHomeMultidataAction = () => { //返回的函数会接受两个参数一个为dispatch，另一个为state return (dispatch, getState) => { axios.get("http://xxx").then((res) => { const data = res.data.data; //getState获取state中数据 console.log(getState()); //调用dispatch保存数据 dispatch(action3(data.banner.list)); dispatch(action4(data.recommend.list)); }); }; };组件中使用：//组件中使用，这里省略了部分组件代码 const mapDispatchToProps = (dispatch) => ({ action1: (num) => dispatch(action1(num)), action2: (num) => dispatch(action2(num)), getHomeMultidata() { dispatch(getHomeMultidataAction()); }, });redux-saga//todoredux-devtools官网还提供了一个调试工具方便我们跟踪state的状态。通过这个工具我们就可以知道state更改前后的状态还有如何更改的，而且还有回放功能。使用方式一：结合中间件使用第一步，在浏览器中安装Redux DevTools，这个安装方式就自己找了第二步，在redux中集成中间件import { createStore, applyMiddleware, compose } from "redux"; import thunk from "redux-thunk"; import { reducer } from "./reducer.js"; //引入redux-dev-tool,要开启trace功能就需要配置 const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true, }) || compose; const store = createStore(reducer, composeEnhancers(applyMiddleware(thunk))); export default store;之后就可以愉快的 调试了。使用方式二：结合npm插件使用，当然前提还是要浏览器安装redux-devtool需要安装一个npm包//npm npm i redux-devtools-extension -D //yarn yarn add redux-devtools-extension在store中配置：import { createStore, applyMiddleware, compose } from "redux"; import thunk from "redux-thunk"; import { reducer } from "./reducer.js"; import {composeWithDevTools} from 'redux-devtools-extension' //没有中间件就直接使用composeWithDevTools()即可 const store = createStore(reducer, composeWithDevTools(applyMiddleware(thunk))); export default store; 当然详细用法链接关于中间件的一些知识补充我们现在有个需求，希望每次dispatch都能打印当前action，然后dispatch后打印最新的state以下的代码为例：//导入redux const redux = require("redux"); const initialState = { counter: 0, }; //创建一个reducer function reducer(state = initialState, action) { switch (action.type) { case "INCREMENT": return { ...state, counter: state.counter + 1 }; case "DECREMENT": return { ...state, counter: state.counter - 1 }; case "ADD_NUM": return { ...state, counter: state.counter + action.num }; case "SUB_NUM": return { ...state, counter: state.counter - action.num }; default: return state; } } const store = redux.createStore(reducer); const action1 = { type: "INCREMENT" }; const action2 = { type: "DECREMENT" }; const action3 = { type: "ADD_NUM", num: 5 }; const action4 = { type: "SUB_NUM", num: 12 }; //订阅store修改 store.subscribe(() => { console.log('state发送了改变'); console.log(store.getState()); }) //修改dispatch //打印日志 function patchLogging(store) { const next = store.dispatch; store.dispatch = function (action) { console.log("更改前action", action); next(action); console.log("更改后：", store.getState()); }; } //模拟thunk中间件 function thunk(store) { const next = store.dispatch; store.dispatch = function (action) { if (typeof action === "function") { action(store.dispatch, store.getState); } else { next(action); } }; } patchLogging(store); thunk(store); //派发action function bar(dispatch, state) { console.log(state()); dispatch(action2); } //派发action store.dispatch(action1); //模拟thunk中间件 store.dispatch(bar)结果：更改前action { type: 'INCREMENT' } state改变 { counter: 1 } 更改后： { counter: 1 } { counter: 1 } 更改前action { type: 'DECREMENT' } state改变 { counter: 0 } 更改后： { counter: 0 }上述代码处理大致展示了中间件的一种实现，以及演示了redux-thunk的简单实现。但是如果有大量的中间件，我们一个一个执行未必有点繁琐，所以我们需要合并中间件执行。//封装一个函数来实现所有中间件的合并执行 function middeleware(store, ...middelewares){ middelewares.forEach((midlewareItem) => { midlewareItem(store); }) } middeleware(store, patchLogging, thunk)以上只是简单了解中间件，可以看到实现过程是非常的灵活，更详细的实现可以参阅redux合并中间件的源码流程。reducer合并在项目管理状态比较庞大的时候我们可能需要将state分成多个模块进行管理，比如目录如下：redux可能会涉及多个模块，这时候就有多个reducer，我们需要将reducer合并处理。import { reducer as homeReducer } from "./home"; import { reducer as counterReducer } from "./counter"; export const reducer = (state = {}, action) => { return { counterInfo: counterReducer(state.counterInfo, action), homeInfo: homeReducer(state.homeInfo, action), }; };上述是自定义的一个合并reducer的函数。redux内部是提供了一个函数的combineReducers。官方实现：import { reducer as homeReducer } from "./home"; import { reducer as counterReducer } from "./counter"; import { combineReducers } from "redux"; export const reducer = combineReducers({ counterInfo: counterReducer, homeInfo: homeReducer, });该函数将我们传入的reducers合并到一个对象中，最终返回一个combination的函数（相当于我们之前的reducer函数了），在执行combination函数的过程中，它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state， 新的state会触发订阅者发生对应的刷新，而旧的state可以有效的组织订阅者发生刷新。redux中hook的使用在组件中我们要使用redux中保存的数据需要通过connect来进行联系，另外还需要编写mapStateToProps和mapDispatchToProps。如果我们有大量的代码需要引入redux，那写这些函数将感到繁琐，所以我们可以使用react-redux内置的一些hook来代替这些方法。使用方式：import { useDispatch, useSelector, shallowEqual } from 'react-redux';const { xxxstate } = useSelector( (state) => ({ xxxstate: state.recommend.topBanners }), shallowEqual ); const dispatch = useDispatch(); //执行dispatch dispatch(xxxACtion())上述两行代码就可以使用redux的代码和dispatch了，但是使用hook是有一定的问题。虽然我们使用redux hook可以简化代码而且也可以不使用connect，但是redux-hook有性能问题，connect内部是做了优化的，会对前后的state进行比较来确定依赖的组件是否需要更新。而useSelector只是对传入的函数进行执行，然后对返回的结果进行比较，而每次执行函数返回的都是新的一个对象。当然解决方案也很简单，导入react-redux中的一个函数shallowEqual，作为useSelector的第三个参数。
React的setState详解我们使用React开发的时候，需要修改state中的数据，而我们并不能通过直接修改state中的数据来 实现界面的更新。当然用过Vue的都知道，Vue有自己的响应式系统，能够知道哪些数据发生了改变并自动更新视图。setState的基本使用React需要通过setState这个API来进行视图的重新渲染。这个函数是挂载在Component的原型上供我们使用。另外我们还要遵循一个原则就是永远都不要直接修改state中的属性值，通过setState来修改也是。****如：//这样修改是不会触发组件的更新 this.state.counter += 1; //正确的方式如下 this.setState({ counter:this.state.counter+1 })setState的更新setState是异步更新的我们通过setState修改数据之后是不能立马获取改变后的值的。//counter初值为1 this.setState({ counter:this.state.counter+1 }) console.log(this.state.counter);//1关于为什么setState是异步更新的在官方的github上有解释 链接我们每次调用setState来更新时，视图同步更新，但是多次调用就可能使得视图更新频繁，影响性能，所以设计为异步更新方式是为了批处理更新状态，将多次的更新合并成一次更新。另外如果同步更新state而没有及时执行render，那么state中的数据就和props数据不同步。当然我们也可以获取异步更新后的记结果，setState第二个参数可以传入一个回调函数，在这个回调函数中就可以获取异步更新后的结果。this.setState({ counter:this.state.counter+1 }, () => { console.log(this.state.counter) })setState是同步更新的在某些情况下我们可以让setState同步更新。把函数放在计数器中，设置时间为0那就是同步执行， 另外放置在原生的事件执行的回调函数也是同步执行的。setTimeout(() => { //初始为1 this.setState({ counter:this.state.counter+1 }) console.log(this.state.counter) //2 }, 0)当然整体上执行还是异步执行。所以我们这里可以做个总结：当我们在组件的生命周期或者React的合成事件中执行setState，其是异步执行的。当我们在计数器中执行或者原生的回调函数执行setState，其是同步执行的。setState是如何执行的找到定义方法的地方：Component.prototype.setState = function(partialState, callback) { //下面主要是校验和警告 invariant( typeof partialState === 'object' || typeof partialState === 'function' || partialState == null, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.', ); //这里是关键，这个updater是每个类组件都拥有的方法 this.updater.enqueueSetState(this, partialState, callback, 'setState'); };点击进入updater：function Component(props, context, updater) { this.props = props; this.context = context; // If a component has string refs, we will assign a different object later. this.refs = emptyObject; // We initialize the default updater but the real one gets injected by the // renderer. this.updater = updater || ReactNoopUpdateQueue; }找到如下对象：****主要是调用了enqueueSetState这个方法，注意eventTime和lane这两个常量，点进后面的执行函数，这个函数主要是获取setState的执行上下文，来判断执行方式---同步或者异步。export function requestEventTime() { if ((executionContext & (RenderContext | CommitContext)) !== NoContext) { // We're inside React, so it's fine to read the actual time. return now(); } // We're not inside React, so we may be in the middle of a browser event. if (currentEventTime !== NoTimestamp) { // Use the same start time for all updates until we enter React again. return currentEventTime; } // This is the first update since React yielded. Compute a new start time. currentEventTime = now(); return currentEventTime; }export function requestUpdateLane(fiber: Fiber): Lane { // Special cases const mode = fiber.mode; //根据mode来判断同步还是批处理 if ((mode & ConcurrentMode) === NoMode) { return (SyncLane: Lane); } else if ( !deferRenderPhaseUpdateToNextBatch && (executionContext & RenderContext) !== NoContext && workInProgressRootRenderLanes !== NoLanes ) { // This is a render phase update. These are not officially supported. The // old behavior is to give this the same "thread" (lanes) as // whatever is currently rendering. So if you call `setState` on a component // that happens later in the same render, it will flush. Ideally, we want to // remove the special case and treat them as if they came from an // interleaved event. Regardless, this pattern is not officially supported. // This behavior is only a fallback. The flag only exists until we can roll // out the setState warning, since existing code might accidentally rely on // the current behavior. return pickArbitraryLane(workInProgressRootRenderLanes); } const isTransition = requestCurrentTransition() !== NoTransition; if (isTransition) { if ( __DEV__ && warnOnSubscriptionInsideStartTransition && ReactCurrentBatchConfig._updatedFibers ) { ReactCurrentBatchConfig._updatedFibers.add(fiber); } // The algorithm for assigning an update to a lane should be stable for all // updates at the same priority within the same event. To do this, the // inputs to the algorithm must be the same. // // The trick we use is to cache the first of each of these inputs within an // event. Then reset the cached values once we can be sure the event is // over. Our heuristic for that is whenever we enter a concurrent work loop. if (currentEventTransitionLane === NoLane) { // All transitions within the same event are assigned the same lane. currentEventTransitionLane = claimNextTransitionLane(); } return currentEventTransitionLane; } // Updates originating inside certain React methods, like flushSync, have // their priority set by tracking it with a context variable. // // The opaque type returned by the host config is internally a lane, so we can // use that directly. // TODO: Move this type conversion to the event priority module. const updateLane: Lane = (getCurrentUpdatePriority(): any); if (updateLane !== NoLane) { return updateLane; } // This update originated outside React. Ask the host environment for an // appropriate priority, based on the type of event. // // The opaque type returned by the host config is internally a lane, so we can // use that directly. // TODO: Move this type conversion to the event priority module. const eventLane: Lane = (getCurrentEventPriority(): any); return eventLane; }最后会调用enqueueUpdateexport function enqueueUpdate<State>( fiber: Fiber, //更新链表 update: Update<State>, lane: Lane, ) { const updateQueue = fiber.updateQueue; if (updateQueue === null) { // Only occurs if the fiber has been unmounted. return; } const sharedQueue: SharedQueue<State> = (updateQueue: any).shared; if (isInterleavedUpdate(fiber, lane)) { const interleaved = sharedQueue.interleaved; if (interleaved === null) { // This is the first update. Create a circular list. update.next = update; // At the end of the current render, this queue's interleaved updates will // be transferred to the pending queue. pushInterleavedQueue(sharedQueue); } else { update.next = interleaved.next; interleaved.next = update; } sharedQueue.interleaved = update; } else { const pending = sharedQueue.pending; if (pending === null) { // This is the first update. Create a circular list. update.next = update; } else { update.next = pending.next; pending.next = update; } sharedQueue.pending = update; } if (__DEV__) { if ( currentlyProcessingQueue === sharedQueue && !didWarnUpdateInsideUpdate ) { console.error( 'An update (setState, replaceState, or forceUpdate) was scheduled ' + 'from inside an update function. Update functions should be pure, ' + 'with zero side-effects. Consider using componentDidUpdate or a ' + 'callback.', ); didWarnUpdateInsideUpdate = true; } } }setState的合并更新数据的合并我们通过setState修改state中的一个数据时，并不会把其他的数据覆盖掉而是会合并。源码如下：function getStateFromUpdate<State>( workInProgress: Fiber, queue: UpdateQueue<State>, update: Update<State>, prevState: State, nextProps: any, instance: any, ): any { switch (update.tag) { case ReplaceState: { const payload = update.payload; if (typeof payload === 'function') { // Updater function if (__DEV__) { enterDisallowedContextReadInDEV(); } const nextState = payload.call(instance, prevState, nextProps); if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictLegacyMode ) { setIsStrictModeForDevtools(true); try { payload.call(instance, prevState, nextProps); } finally { setIsStrictModeForDevtools(false); } } exitDisallowedContextReadInDEV(); } return nextState; } // State object return payload; } case CaptureUpdate: { workInProgress.flags = (workInProgress.flags & ~ShouldCapture) | DidCapture; } // Intentional fallthrough case UpdateState: { const payload = update.payload; let partialState; //如果传入的参数为函数就不会合并执行 if (typeof payload === 'function') { // Updater function if (__DEV__) { enterDisallowedContextReadInDEV(); } partialState = payload.call(instance, prevState, nextProps); if (__DEV__) { if ( debugRenderPhaseSideEffectsForStrictMode && workInProgress.mode & StrictLegacyMode ) { setIsStrictModeForDevtools(true); try { payload.call(instance, prevState, nextProps); } finally { setIsStrictModeForDevtools(false); } } exitDisallowedContextReadInDEV(); } } else { // Partial state object partialState = payload; } if (partialState === null || partialState === undefined) { // Null and undefined are treated as no-ops. return prevState; } // Merge the partial state and the previous state. //合并部分状态和前一个状态。 return Object.assign({}, prevState, partialState); } case ForceUpdate: { hasForceUpdate = true; return prevState; } } return prevState; }可以看到内部是合并处理的。本身的合并如果我们多次调用setState修改state的值//初始值为1 this.setState({ counter:this.state.counter+1 }) this.setState({ counter:this.state.counter+1 }) this.setState({ counter:this.state.counter+1 })实际上内部会将上述的setState合并为一次调用。内部会判断执行队列中所有setState的前一次的值和更新后的值是否相等，如果相等就会合并执行(这里发现都是从1变为2)。并不会执行后在之前的基础再次更新。但是我们有时并不希望合并。setState第一个参数可以传入一个函数。同时接收参数 this.setState((preState, props) => { return { counter: preState.counter + 1, }; }); this.setState((preState, props) => { return { counter: preState.counter + 1, }; }); this.setState((preState, props) => { return { counter: preState.counter + 1, }; });上述操作就是可以累加。内部会判断setState传入的是对象还是函数，从而是否合并执行。
React Hooks 使用介绍Hook 是React16.8新增的特性，可以说是扩展了函数式组件的各项功能。有了这个功能，函数式组件和类式组件在实现功能上就没有什么差异了，而且使用hook后，能使代码更加的简洁，代码也更具复用性了。useState以一个计数器来介绍：import React, {useState} from 'react' function Counter() { const [count, setCount] = useState(0); return ( 当前计数{count} setCount(count + 1)}>+1 setCount(count - 1)}>-1 ); }上面的组件就实现了一个计数器，点击按钮就会加减，hook使的函数组件有了自己的状态。**useState就是一个hook。**参数：初始化值，不设置就为undefined，也可以可以传入数组对象，另外还可以传入一个带有返回值的函数。**返回值：数组，包含两个元素，一般我们简化书写会使用数组解构语法。第一个元素使当前状态的值（第一次调用为初始化的值）第二个元素为设置状态值的函数。比如，上面的例子count的初始值为0 ，可以通过setCount来重新设置count的值，而且也会触发组件的创新渲染。我们可以在组件中多次使用这个hook来创建多个变量。****另外我们多次调用setXxx函数来修改对应状态的时候，其实也和类组件中的setState基本一样： //会被合并成一次执行 setCount(count+10); setCount(count+10); setCount(count+10); setCount(count+10); //不会被合并 setCount((prev) => prev + 10); setCount((prev) => prev + 10); setCount((prev) => prev + 10); setCount((prev) => prev + 10); setCount((prev) => prev + 10); 可以看到使用hook是多么的清爽。比新的一年换上新内裤还爽~~。那么接下来介绍第二个hook。useEffect函数组件自身没有生命周期这类函数，我们要实现这个功能就可以使用这个hook现在演示一个需求，页面的title总是显示counter的数字。****首先使用类组件进行实现：class ChangeTitle extends React.Component { constructor(props) { super(props); this.state = { count: 0, }; } componentDidMount() { document.title = this.state.count; } componentDidUpdate() { document.title = this.state.count; } render() { return ( 类组件当前标题计数:{this.state.count} this.setState({ count: this.state.count + 1 })}> +1 ); } }可以看到更改标题的操作有重复的部分。****函数式组件：function HookChangetitle() { const [count, setCount] = useState(0); //模拟生命周期 useEffect(() => { document.title = count; }); return ( 函数当前计数{count} setCount(count + 1)}>+1 setCount(count - 1)}>-1 ); }useEffect这个hook可以告知React在渲染好组件后执行某些操作。该hook要求传入一个回调函数，在React执行完DOM更新操作后就会回调这个函数。默认情况下，无论是第一次渲染还是之后的每次更新都会执行一次这个回调函数。清除effect我们需要在组件卸载的时候清除componentWillUnmount中进行清除。而useEffect为我们可以这样做，在我们传入的回调函数中，我么们可以指定一个返回值，返回一个回调函数。这是useEffect可选清除机制，每个useEffect都可以返回一个清除函数。 useEffect(() => { xxx return () => { console.log('清除了'); } });React会在组件每次更新和卸载的时候执行清除操作。而且useEffect也可以多次调用，我们可以根据代码用途来分开调用，使得代码更加明朗。useEffect性能优化某些代码我们只希望在组件第一次挂载完毕后执行一次就行了，之后的更新就无需再执行。但是默认情况下useEffect在第一次挂载和之后的更新都会执行回调。这多次的执行一定程度上会导致一些性能问题。所以useEffect可以传入第二个参数，第二个参数是一个数组，传入该useEffect的依赖项。如果该useEffect不依赖任何内容的话可以传入一个空数组。当传入空数组的时候，useEffect中的回调函数就相当于componentDidMount，回调函数返回的回调函数就相当于componentWillUnmount。当然传入的依赖项如果改变了那就会重新执行回调函数和返回的回调函数。useContext没啥好说的，另类的获取context的方式。const ThemeContext = React.createContext(); function ContextHooksDemo() { const theme = useContext(ThemeContext); return 主题颜色{theme.color} ; }当最上层的Provider更新的时候，该hook会重新触发渲染并传递新的值。useReduceruseReducer仅仅是useState的一种替代方案。在某些情况下，state处理比较复杂的逻辑就可以使用这个hook来进行拆分。//处理逻辑 function counterReducer(state, action) { switch (action.type) { case "increment": return { ...state, counter: state.counter + 1 }; case "decrement": return { ...state, counter: state.counter - 1 }; default: return state; } } function ReducerHookDemo() { const [state, dispatch] = React.useReducer(counterReducer, { counter: 111 }); return ( {state.counter} dispatch({ type: "increment" })}>+1 dispatch({ type: "decrement" })}>-1 ); }另外这里通过useReducer创建的数据是不能共享的，所以不能代替Redux，只是用法有点类似。useCallback这个hook也是为了进行性能的优化。函数有两个参数，第一个传入一个回调函数，第二个传入数组（依赖项）。这个hook会返回一个函数的memoized。下面展示两个案例：案 例一：使用useCallback和不使用useCallback定义一个函数是否会带来性能的优化。function CallbackDemo01() { const [count, setCount] = useState(0); function increment() { console.log("执行"); setCount(count + 1); } //下面使用usecallback不能带来性能优化，和上面的差不多 const increment2 = React.useCallback( function () { console.log("执行"); setCount(count + 1); }, [count] ); return ( CallbackDemo01 {count} +1 useCallback优化+1 ); }单纯从上面这个案例看不出来什么性能优化，因为这两个函数在组件创建的时候都会重新定义，count更新也会重新定义，所以结合下面这个案例来进行理解。案例二：使用useCallback和不使用useCallback定义一个函数传递给子组件是否会带来性能的优化。const Mybutton = React.memo(function (props) { console.log("MyButton重新渲染:" + props.title); return +1 ; }); function CallbackDemo02() { console.log("callbackDemo02重新渲染"); const [count, setCount] = useState(0); const [show, setShow] = useState(false); const increment1 = () => { console.log("执行increment1"); setCount(count + 1); }; //当执行show切换时，父组件会重新渲染，子组件默认会处理。如果子组件使用memo进行包裹，那么子组件会对props进行浅层比较，相同就不需要重新渲染，但是这里两个函数在父组件重新渲染也会重新创建，所以传递给父组件的两次props是不同的，所以也会更新， //但实际上函数依赖的count并没有发生变动，组件更新是没有必要的，所以使用usecallback就避免了这个问题 const increment2 = React.useCallback(() => { console.log("执行increment2"); setCount(count + 1); }, [count]); return ( callbackdemo02{count} <Mybutton title="btn1" increment={increment1}></Mybutton> <Mybutton title="btn2" increment={increment2}></Mybutton> setShow(!show)}>切换 ); }总之通常使用useCallback的目的是不希望子组件进行多次渲染。另外要注意子组件一定要使用memo包裹，否则使用useCallback是无效的。useMemo这个hook也是为了性能优化。useMemo返回的也是一个memoized，在依赖不变的情况下，多次定义返回的都是同一个值。案例展示：****有时候我们定义一个函数，组件内其他的状态改变会引起这个组件的重新渲染，然后内部的函数就会重新在定义或者执行一次，如果函数的依赖状态没有改变，那这次的重新定义和执行是没有必要的。function calcNumber(count) { console.log("执行计算"); let total = 0; for (let i = 1; i <= count; i++) { total += i; } return total; } //useMemo 也是返回一个memoized const MyInfo = React.memo((props) => { console.log("My重新渲染"); return ( 名字：{props.info.name}年龄:{props.info.age} ); }); function MemoHookDemo01() { const [count, setCount] = useState(10); const [show, setShow] = useState(true); const info = React.useMemo(() => { return { name: "zhangsan", age: 18 }; }, []); //计算1到count的和，当count不变时该函数不会重新执行 const total = React.useMemo(() => { return calcNumber(count); }, [count]); return ( memoHook 计算的和为{total} setCount(count + 1)}>+1 <MyInfo info={info}></MyInfo> setShow(!show)}>切换 ); }这个有点类似vue中的computed，当然原理完全不一样。useRef该hook返回一个ref对象，返回的ref对象在整个生命周期保持不变。常见有两种用法：用法一：引入DOM元素，或者组件（需要为class组件）。用法二：保存一个数据整个对象在整个生命周期中可以保持不变。function RefHookDemo01() { //获取dom const titleRef = React.useRef(); const inputRef = React.useRef(); function changeDom() { titleRef.current.innerHTML = "hello refhook"; inputRef.current.focus(); } //可以保存上一次的state值，因为countRef更新不会引起dom的更新，所以当界面更新完时count和countRef.current值一样，但实际dom中countRef.current展示的为上一次的值 const [count, setCount] = useState(0); const countRef = React.useRef(count); useEffect(() => { countRef.current = count; console.log(countRef.current); }, [count]); return ( RefHookDemo01 changeDom()}>修改dom <MyInfo info={{ name: "nihao", age: 20 }}></MyInfo> 上一次count:{countRef.current} 这一次的count:{count} setCount(count + 10)}>+10 ); }useImperativeHandle这个hook不是那么好理解，这里举一个使用例子：我们通过ref要获取函数式组件中的dom元素时，不能直接获取，需要ref的转发，就是利用到forwardRef这个API对函数式组件进行包裹，然后子组件拿到父组件的ref绑定到某一个元素上，这样父组件就有这个元素的完全使用权了。但是我们只希望父组件能够操作这个元素某些属性，比如只能操作focus这个事件。那么useImperativeHandle这个hook就是用来限制传递给父组件操作的。该hook有三个参数，第一个为父组件传递过来的ref，第二个为回调函数，内部返回一个对象，对象中的方法就是暴露给父组件能操作的，第三个为依赖项`注意：React19已经废弃了forwardRef，直接将ref加入到prop中，不再需要forwardRef这个函数包裹了。https://react.dev/reference/react/forwardRefconst MyInput = React.forwardRef(function (props, ref) { //有时候我们并不像向父组件暴露过多的属性 const inputRef = React.useRef(); React.useImperativeHandle( ref, () => ({ focus: () => { inputRef.current.focus(); }, }), [inputRef] ); return ; }); function ImperativeHookDemo() { const inputRef = React.useRef(); return ( <MyInput ref={inputRef} /> inputRef.current.focus()}>获取焦点 ); }useLayoutEffect这个hook和useEffect很相似，只有一点点区别，useEffect会在组件内容更行到DOM后执行，不会阻塞DOM的更新。而useLayoutEffect则是再组件内容更新到DOM上之前执行，会阻塞DOM更新。所以视情况来选择hook。function LayoutEffectHookDemo() { const [count, setCount] = useState(10); //这里使用useEffect可能会出现先从o再变为随机数的过程在网页上可能会闪一下，使用useLayoutEffect就不会 React.useLayoutEffect(() => { if (count === 0) { setCount(Math.random()); } }, [count]); return ( layoutEffect:{count} setCount(0)}>改变 ); }以上的案例可以尝试体会下二者的区别。当然还有一些hook没有提到，大部分常用的hook都已经介绍了，剩下的可以查看官网。自定义hook本质上自定义hook只是一种函数代码逻辑的抽取，并不算React特性，只是为了代码和逻辑的复用。以下演示几个自定义hook的案例。注意自定义的hook函数的命名必须以use开头。函数创建和销毁时打印一些东西：//可以提到一个单独的文件中 function useLoggingLife(name) { React.useEffect(() => { console.log(`${name}组件创建出来`); return () => { console.log(`${name}销毁了`); }; }, [name]); } function CustomHookDemo01() { useLoggingLife("CustomHookDemo01"); return ( CustomHookDemo01 <Home></Home> <About></About> ); }context的共享：import React from "react"; import { UserContext, TokenContext } from "./App"; export default function useContext() { const user = React.useContext(UserContext); const token = React.useContext(TokenContext); return [user, token]; }import React from "react"; import useContextHook from "./useContextHook"; export default function CustomContextShareHook() { const [token, user] = useContextHook(); console.log(token, user); return ( 获取context ); }function App() { return ( <UserContext.Provider value={{ name: "zhangsan", age: 19 }}> <TokenContext.Provider value="abckdfhafh"> <CustomContextShareHook></CustomContextShareHook> </TokenContext.Provider> </UserContext.Provider> ); }本地缓存：import React from "react"; export default function useLocalStore(key) { //获取 const [name, setName] = React.useState(() => { const name = JSON.parse(window.localStorage.getItem(key)); return name; }); //设置 React.useEffect(() => { window.localStorage.setItem(key, JSON.stringify(name)); }, [name]); return [name, setName]; }import React from "react"; import useLocalStore from './useLocalStore' export default function LocalStore() { const [name, setName] = useLocalStore('saber') return ( 获取设置name:{name} setName("zhangsan")}>设置name ); }//todo useState源码//todo redux hooks
React 复习React的三个核心依赖库 使用React需要引入上述的三个库，crossorigin这个属性是可以获取到跨站脚本的错误信息。关于React为何要分为三个库主要是因为react-native因为手机端的react-dom会将React渲染出来的虚拟对应到原生的控件上，在web端就是生成真实的dom。为什么要引入babelbabel在前端，用途是非常的广泛。作为前端经常使用的代码转换器和编译器。可以对js做兼容性处理，比如es6转es5。默认React的开发可以不需要babel，但是我们要写一些html模板就需要使用React.createElement这个函数，但是要先一些嵌套的额标签的话，那是十分繁琐的。所以我们可以编写jsx代码（JavaScript XML），babel的作用就是将jsx语法转换为React.createElement写法。比如：左边我们可以按照html的写法创建标签，而经过babel编译后最终为React.createElement形式，可以看到该形式是较为复杂。JSX语法我们要编写jsx语法需要将代码写在如下标签中，表示我们写的为jsx代码。 jsx的书写规范首先jsx顶层只能有一个根元素，我们需要在外层包裹一个div，或者Fragment标签。如果我们要换行书写还需要在外层包裹一个（）。jsx支持单标签但是要以 /> 结尾。jsx内部要混入变量使用{}，其中也可以使用一些表达式。标签的属性如果与一些保留词冲突的话需要改写，比如样式的类名class需要使用className，另外还有label标签中的for属性，要使用htmlFor来代替。内联样式，要用style={{key:value}}的形式去写。{}中能展示的变量类型可以展示的类型：string， number，Array（会将每一项拼在一起成字符串）。不能展示的类型：undefined，null，boolean，一般对象不能不能作为jsx的子类即对象不能直接放置在{}中。在{}不能展示的变量类型，如果需要展示的话可以转换变量类型或者调用toString()等方法。条件渲染当逻辑判断很多的时候可以使用if else来判断，简单的情况可以使用三目运算符或者交集运算符。还可以根据性能要求使用display:none或者操作dom的方式来切换dom显示。 事件绑定和thisjsx中的dom事件绑定就是在指定的dom上以onXxxx形式绑定事件 点击 关于绑定事件函数中的this指向问题。上述函数中的this不做任何处理时默认指向undefined的。因为React在处理绑定的事件函数内部通过 btnClick.apply(undefined,[arg])来调用的，内部是无法获得该上下文的this的。解决方式1：显示绑定thisthis.btnClick.bind(this);解决方式2：使用箭头函数来定义事件函数btnClick = () => { console.log(this) }解决方式3：在{}是用箭头函数返回事件函数 { this.decrement("why") }}>-1 推荐使用第三种，因为传值的时候也不需要使用柯里化来定义事件函数了。jsx本质jsx实际上就是React.createElement的语法糖通过createElement创建出来的对象就是ReactElement对象，最终形成一个虚拟DOM​ react渲染流程： jsx -> createElement函数 -> ReactElement(对象树) -> ReactDOM.render -> 真实的DOM​ react native渲染流程：​ jsx -> createElement函数 -> ReactElement(对象树) -> ReactDOM.render -> 原生的控件（UIButton/Button）源码展示：export function createElement(type, config, children) { let propName; // Reserved names are extracted const props = {}; let key = null; let ref = null; let self = null; let source = null; if (config != null) { if (hasValidRef(config)) { ref = config.ref; if (__DEV__) { warnIfStringRefCannotBeAutoConverted(config); } } if (hasValidKey(config)) { key = '' + config.key; } self = config.__self === undefined ? null : config.__self; source = config.__source === undefined ? null : config.__source; // Remaining properties are added to a new props object for (propName in config) { if ( hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName) ) { props[propName] = config[propName]; } } } // Children can be more than one argument, and those are transferred onto // the newly allocated props object. //多余的参数都会放到一个数组中来处理 const childrenLength = arguments.length - 2; if (childrenLength === 1) { props.children = children; } else if (childrenLength > 1) { //开辟一个数组，使得插入更高效 const childArray = Array(childrenLength); //遍历添加 for (let i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } if (__DEV__) { if (Object.freeze) { Object.freeze(childArray); } } props.children = childArray; } // Resolve default props if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } if (__DEV__) { if (key || ref) { const displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type; if (key) { defineKeyPropWarningGetter(props, displayName); } if (ref) { defineRefPropWarningGetter(props, displayName); } } } return ReactElement( type, key, ref, self, source, ReactCurrentOwner.current, props, ); }React的组件化创建组件方式React有两种创建组件的方式：类式组件和函数式组件。另外根据组件的其他概念，比如状态，职责，UI等，可分为：有状态组件，无状态组件，展示型组件和容器型组件，异步组件，高阶组件等。类式组件首先无论是类组件还是函数式组件，创建时的名称首字母必须大写，不然会被识别为html的内置标签。类组件实现需要继承React.component或者React.pureComponent这个父类组件。类组件必须实现render函数，该函数也是唯一必须实现的。官网现在推荐使用ES6的类定义语法来定义类式组件。import React, { Component } from 'react' export default class componentName extends Component { render() { return ( ) } }函数式组件函数式组件就是使用function函数表达式来定义组件，需要返回的内容结构和类组件中render函数返回的结构相同。相比类式组件，函数式组件有以下特点：没有生命周期，类式组件有一套自己的生命周期函数。没有this实例，类式组件的this指向实例。没有内部的状态(state)，类式组件可以通过在this.state中定义状态变量。export default function ComponentName(){ return ( ) }生命周期函数React组件的生命周期函数表示了组件从开始创建到渲染成功，到最后销毁这一系列的过程。我们可以通过这些生命周期函数来在不同的组件阶段实现各种功能。函数式组件可以通过hooks来模拟生命周期函数，这里我们主要谈类式组件的生命周期函数。constructor谈之前我们先谈下类式组件的构造函数constructor。我们使用constructor一般用来初始化组件的state，或者给事件绑定this，或者父类的构造函数来保存props。constructor(props){ super(props); this.state = {message:'hello world'} }上述的构造函数可以省去不写，因为默认不写时，React在执行生成实例时就会调用上述实现方式的构造函数。如果不写构造函数，我们也可以将state直接写在类式作用域中，调用方式不变： class MyComponent extends React.Component { //初始化状态 state = {isHight:false} render() { const {isHight} = this.state; return 你{isHight ? '身体一定很好' : '是个LSP'} } }但是还有一个属性props，如果我们没有在构造函数中声明保存，这个属性我们也可以直接在render获取。export default class index extends Component { // constructor(props) { // super(props); // } render() { const { name, age } = this.props; return ( 我是home组件 {name}---{age} ); } }props是获取从父组件传递来的参数的属性，之所以render函数能够获取到props，是内部源码做了一层处理，将props保存了下来。所以我们可以直接使用props。生命周期函数在React的不同版本有所不同，本篇所讲的主要针对于React版本大于16.13.1。旧版生命周期函数图解：新版生命周期函数图解：旧版的生命周期函数已经不推荐使用了，当然仍然可以使用。如果要使用的话需要在函数名前加上UNSAFE_前缀。我们不加前缀的话React会内部转发调用带前缀的生命周期函数，同时会报警告。以下生命周期是过时的的：UNSAFE_componentWillMount() 组件即将挂载成功。UNSAFE_componentWillReceiveProps() 在已挂载的组件接收新的 props 之前被调用。UNSAFE_componentWillUpdate() 当组件收到新的 props 或 state 时。会在渲染之前调用该函数。新版生命周期相对要复杂些：componentDidMount当我们的组件第一次挂载完毕（插入DOM树）后，就会执行该函数。我们可以在这个生命周期函数中执行一些初始化的事，比如：一些需要依赖DOM的操作（获取一些DOM元素属性），网络请求，一些事件的订阅（对应在componentWillUnmount中取消订阅）。componentDidUpdate当组件更新完后会被立即执行，首次渲染不会执行该方法。一般我们是通过this.setState或者render函数调用来触发这个函数。该函数可以获取上一次的state，props和snapshot（快照值）。可以比较是否改变来执行相应的操作。注意我们可以在该函数中调用this.setState方法，但是要注意需要条件判断执行，否则会出现死循环。componentWillUnmount组件即将卸载的函数，可以在该函数中执行取消订阅，网络请求取消或者引用销毁等操作。我们可以手动调用ReactDOM.unmountComponentAtNode()来卸载指定的节点getDerivedStateFromProps这个函数并不常用，他是定义在类上的静态方法需要加上类名或者static。当state的值在任何时候都是取决于props就可以使用该函数。该方法返回一个对象来更新stategetSnapshotBeforeUpdate在组件即将更改前执行，可以获取一些DOM信息，然后返回。我们可以通过componentDidUpdate这个函数来接受返回值。shouldComponentUpdateReact组件会根据这个函数返回的值来确定是否更新，默认的行为是state每次发生变化组件都会更新。当 props 或 state 发生变化时，shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。这里注意如果调用forceUpdate强制更新，会跳过该函数执行。关于该函数的其他知悉可以了解：[React性能优化]()总结：初始化阶段，由ReactDOM.render()触发---初次渲染constructor()static getDerivedStateFromProps()render()componentDidMount()更新阶段，由组件内部this.setSate()或父组件重新render触发static getDerivedStateFromProps()shouldComponentUpdate()render()getSnapshotBeforeUpdatecomponentDidUpdate()卸载阶段：由内部ReactDOM.unmountComponentAtNode()触发componentWillUnmount组件的通信方式在组件中我们会经常遇到组件相互传值的方式，其中需要用到props这个重要的属性。父组件传递给子组件父组件通过在子组件标签上以 属性=值 方式传递数据给子组件。父组件：export default class SpuerCon extends Component { render(){ return ( <ChildrenCon name='zhangsan' age={18} /> ) } }类子组件接收：export default class ChildrenCon extends Component { render(){ return ( {this.props.name}---{this.props.age} ) } }函数组件接收：export function ChildrenCon(props){ return ( {props.name}---{props.age} ) }如果使用过Vue，我们给子组件传递值的时候，子组件可以对传递过来的props进行验证。React也可以对传递过来的props进行验证，但是React自身并不能验证，我们需要借助第三方库或者TS来进行验证。这里第三方库推荐prop-types。//npm安装方式 npm install prop-types -S //yarn 安装方式 yarn add prop-types使用方式：import React, { Component } from "react"; import PropTypes from 'prop-types'; export default class ChildrenCon extends Component { render(){ const {name, age} = this.props; return ( {name}---{age} ) } //类型验证 static propTypes = { name:PropTypes.string.isRequired } //默认值设置 static defaultProps = { age:100 } }子组件传递给父组件在Vue中子传递父是通过自定义事件实现，在React中就还是通过Props实现的，我们可以向子组件传递一个函数，让子组件执行来实现子传父。import React, { Component } from "react"; //子组件 class Btn extends Component { render() { return + ; } } //父组件 export default class CpnBtn extends Component { constructor(props) { super(props); this.state = { counter: 0, }; } render() { return ( {this.state.counter} <Btn increment={(e) => this.increment()} /> ); } increment() { this.setState({ counter: this.state.counter + 1, }); } }非父子组件数据的共享有时候我们需要跨多个层级进行数据共享，但是一直通过props逐层传递就会出现中间的冗余。我们可以使用React的Context方法。Context是为了共享那些对于一个组件树而言是"全局"的数据，例如当前认证的用户、主题或首选语言等。我们首先需要创建一个共享的Context对象，我们可以在createContext()函数中传入一个对象设置默认值。//创建context对象，类组件获取context的方式 const ProfileContext = React.createContext(); const ThemeContext = React.createContext();当组件所处的树中没有匹配到 Provider 时，其 defaultValue 参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意：将 undefined 传递给 Provider 的 value 时，消费组件的 defaultValue 不会生效。export default class Con extends Component { constructor(props) { super(props); this.state = { nickname: "zhangsan", level: 1991, }; } render() { return ( <ProfileContext.Provider value={this.state}> <ThemeContext.Provider value={{ color: "red" }}> <Profile /> </ThemeContext.Provider> </ProfileContext.Provider> ); } } function Profile(props) { return ( {/* <ProfileHeader /> */} <FProfileHeader /> 设置1 设置2 设置3 设置4 ); }后代元素接收数据，类和函数组件都可以通过Consumer方式获取。function FProfileHeader() { return ( <ProfileContext.Consumer> {(value) => { return ( <ThemeContext.Consumer> {(theme) => { return ( 用户等级：{value.nickname} 用户昵称：{value.level} ); }} </ThemeContext.Consumer> ); }} </ProfileContext.Consumer> ); }另一种获取数据方式，只有类组件可以使用：class ProfileHeader extends Component { //声明接收context后就可以使用this.context来获取了 static contextType = ProfileContext; render() { return ( 用户等级：{this.context.level} 用户昵称：{this.context.nickname} ); } }事件总线事件总线其实就是利用发布订阅方式进行跨组件传递，这里推荐两个库：events和pubsub-js，这两个库都可以用作传递数据，这里就以events来做案例。安装方式：//npm方式 npm install events -S //yarn方式 yarn add events使用方式：import React, { PureComponent } from "react"; //引入 import { EventEmitter } from "events"; //创建事件 const event = new EventEmitter(); export default class Home extends PureComponent { render() { return ( 我是home组件 <Profile /> ); } //一般在这个函数中执行事件监听 componentDidMount(){ event.on('test1', (arg)=>{ console.log(arg); }) } //取消监听 componentWillUnmount(){ event.removeListener('testt1'); } } class Profile extends PureComponent { render() { return this.send()}>我是profile组件 ; } send(){ event.emit('test1', {name:12, age:33}); } } React中实现vue的slotReact中没有像Vue中提供的slot插槽相关的API，但是灵活的JSX属性可以实现类似的功能。方式1：import Nav from "./nav"; function App() { return ( left center right ); } export default App;卸载组件中的标签会以props的形式传递给组件，组件中获取方式如下：import React, { Component } from "react"; import "./nav.css"; export default class componentName extends Component { render() { const { children } = this.props; return ( {children[0]} {children[1]} {children[2]} ); } }这种方式可以实现slot的效果，但是还是不够灵活，组件内必须按照固定的位置来渲染组件。方式2：import Nav2 from "./nav2"; function App() { /* 两种方式传递组件，类似slot */ return ( <Nav2 leftSlot={ left } centerSlot={ center } /* href如果只写#可能会触发eslint，要避免出发警告可以加上个 / */ rightSlot={right} /> ); } export default App;通过属性传值方式就可以在外部控制指定位置的插槽了，组件中接收方式如下：import React, { Component } from "react"; import "./nav.css"; export default class componentName extends Component { render() { const { leftSlot, centerSlot, rightSlot } = this.props; return ( {leftSlot} {rightSlot} {centerSlot} ); } } 这种方式在外部就可以控制插槽的位置，类似于vue中的具名插槽。PureComponent当我们在state中添加一个属性，但是我们并没有在jsx中使用这个属性，即视图并没有依赖这个数据，我们就不用更新视图，但是React只要监听到state的改变就会重新调用render。另外如果子元素也没有使用父元素的state那么也不用更新，但是只要父元素执行了render函数，其子元素的所有的render函数都会执行，这些更新是完全没有必要。我们可以通过shouldComponentUpdate来控制组件的更新。该函数默认返回true，即需要调用render函数，我们可以比较新旧props来判断是否需要更新，即返回false就不更新。当然React是想到了这个问题，所以提供了一个组件PureComponent。我们书写组件如果需要判断是否更新就可以继承这个组件export default class Con extends PureComponent { //... }该组件重写了shouldComponentUpdate这个方法，默认对新旧的state或者props进行浅比较，判断是否更新。所以我们不需要自己实现比较函数，当然React也不推荐我们自行实现深比较函数，因为深比较也会消耗性能，这是不必要的。React通过该比较函数判断是否需要更新function shallowEqual(objA: mixed, objB: mixed): boolean { if (is(objA, objB)) { return true; } if ( typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null ) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); if (keysA.length !== keysB.length) { return false; } // Test for A's keys different from B. for (let i = 0; i < keysA.length; i++) { if ( !hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]]) ) { return false; } } return true; }高阶组件memo我们之前处理组件更新问题对于类组件时可以使用PureCompoent，但是对于函数式组件我们就可以使用memo这个高阶组件来处理。我们可以使用这个高阶组件来包裹一个函数式组件返回一个组件。import React, { PureComponent, memo } from "react"; const Fcn = memo(function () { console.log("函数组件重新调用"); return 函数组件 ; });这样该函数就和继承了PureComponent的类式组件一样了。refReact的原生开发中也不建议直接操作DOM，但有时我们确实获取DOM可以通过ref方式。在React中使用ref有三种方式：字符串方式 你好 console.log(this.refs.dom);这种方式已经过时，官方不推荐使用了对象形式首先创建一个ref对象import React, { Component, createRef } from "react"; export default class Con extends Component { constructor(props) { super(props); this.dom = createRef(); } }render() { return ( 世界 ); }获取方式：console.log(this.dom1.current);函数形式传入一个回调函数，这个函数会传入一个元素对象，我们定义属性接收就可以。 (this.dom2 = c)}>哈哈 console.log(this.dom2)推荐方式二三都可以，但是要注意上述方式都只适用于类组件，函数组件需要通过React.forwardRef方式获取。ref的转发之前说的方式都只适用于类式组件，而函数时组件可以通过React中自带的一个高阶函数(forwardRef)处理来使用ref。import React, { PureComponent, createRef, forwardRef } from "react";const Profile = forwardRef(function (props, ref) { /* 通过该高阶函数增强，多了一个参数ref，此时将ref放在指定元素上就可以获取对应dom 了 */ return 我是函数组件 ; });export default class App extends PureComponent { constructor(props) { super(props); this.profileRef = createRef(); } render() { return ( {/* 要注意ref不是属性，和props不一样this，该属性是react内部管理 */} <Profile ref={this.profileRef} /> this.getRef()}>获取ref ); } getRef() { console.log(this.profileRef.current); } } 运行结果：受控组件和非受控组件在HTML中表单元素（input，textarea，select等）通常自己维护state，并且根据用户输入进行更新。而在React中可变状态通常保存在state中来进行更新维护。我们可以把两者结合起来，使 React 的 state 成为"唯一数据源"。渲染表单的 React 组件还控制着用户输入过程中表单发生的操作。被 React 以这种方式控制取值的表单输入元素就叫做"受控组件"。其实可以理解为vue中对于表单数据的双向绑定（v-model）。import React, { Component } from "react"; export default class Fcn extends Component { constructor(props) { super(props); this.state = { username: "zhangsan", fruist: "pear", }; } render() { return ( this.handleSubmit(e)}> 用户： {/* react中select加上value可设置默认值，原生的是在option标签上加selected */} 苹果 香蕉 梨子 ); } handleSubmit(e) { //取消默认行为 e.preventDefault(); console.log(this.state.username); console.log(this.state.fruist); } gethandleChange(valueType) { return (e) => { this.setState({ //计算属性名获取属性名称，将多种输入框的受控函数合并 [valueType]: e.target.value, }); }; } } 当然表单数据将交由 DOM 节点本身来处理就称为非受控组件。一般还是推荐使用受控组件。Portals的使用我们所写的DOM元素都是挂载在id为root的根元素上，但是有些DOM结构我们希望它独立的挂载到父元素或者根元素以外的地方，那么就可以使用Portals这个API。在父组件中我们希望将Modal这个组件中的子元素渲染到指定id的元素内。class Home extends Component { render() { return ( home <Modal> {/* 需求，将所有的Modal中的子组件渲染到屏幕中间中去 */} modal </Modal> ); } }我们可以在Modal组件中使用这个apiclass Modal extends Component { render() { /* 将子组件挂载到#modal， */ return ReactDOM.createPortal( this.props.children, document.getElementById("modal") ); } }public/index.html中设置 You need to enable JavaScript to run this app. 渲染结果：fragment的使用我们在书写jsx语法是，返回的DOM元素是必须要一个跟标签包裹，比如div，但是这个div除了满足语法外没有任何的作用，所以使用fragment就可以解决这个问题。这个标签使用React内部自带import React,{ Fragment } from "react"; function foo() { return ( <Fragment> </Fragment> ) }该标签并不会被渲染成任何元素，如果不加key属性可以简写为<></>，如果加上key就需要完整的写法。StrictMode这个组件不会被渲任何元素，主要作用为检查子元素的潜在问题。类似JS的严格模式。ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); 主要检测内部的子元素是否使用了不安全的生命周期，过时的refAPI和一些废弃的方法和API。还可以检测意外的副作用，比如组件的constructor就会执行两次来确定是否产生副作用。
React 中自定义防抖函数最近在做一个React的项目，中间做到搜索提示这个功能的时候遇到了请求次数过多的问题，当然对于这种情况肯定需要防抖来是请求延后发送，避免提示框随用户输入切换过于频繁，影响交互体验。首先实现一个经典的防抖函数，这有什么难的嘛：export const debounce = (fn, delay) => { let time = null; return function(...args) { if(time) { clearTimeout(time); } time = setTimeout(() => { fn.apply(this, args); }, delay) } }然后在函数组件中使用：useEffect(() => { getSearchRes(); }, [dispatch, keywords]); /* 其他逻辑 */ //防抖函数处理 const getSearchRes = debounce(() => //请求函数放置在redux中 dispatch(getSearchKeywordsResAction(keywords, { offset: 0, limit: 4 })) );最后保存测试，发现连续输入并不是在最后一次输入后延时发送请求，而是每次输入都会发送请求，只是每次发送请求延时了而已。这咋没用哇。之后查阅了其他网站后发现了问题，还是自己对React hooks不熟悉哇，当我们输入时改变keywords，都会导致函数组件重新刷新执行，所以里面所有的hooks都会重新执行，我定义的普通函数也会重新执行。防抖函数中的time每次都是定义一个新的，并没有保存到time的状态，毕竟防抖的关键就是利用闭包保存time变量。所以要保存time这个变量使其在整个组件声明周期不变，于是就想到了useRef这个hook。解决方式有了，我们可以考虑编写自定义hooks来实现这个功能:import { useRef } from 'react'; //防抖函数 export const useDebounce = (fn, delay = 200) => { //todo 关于react hooks 中的防抖实现 const { current } = useRef({}); return function (...args) { if (current.time) { clearTimeout(current.time); } current.time = setTimeout(() => { fn.apply(this, args); }, delay); }; };这样就使得防抖函数生效了。通过这个案例其实也是让我知道了React真的能反映一个人JS原生编写能力。哎好好补基础了。参考文章：一起围观由React Hooks防抖引发的面试翻车现场