高阶组件

一 前言 React高阶组件(HOC),对于很多react开发者来说并不陌生,它是灵活使用react组件的一种技巧,高阶组件本身不是组件,它是一个参数为组件,返回值也是一个组件的函数。 高阶作用用于强化组件,复用逻辑,提升渲染性能等作用。高阶组件也并不是很难理解,其实接触过后还是蛮简单的,接下来将按照,高阶组件理解?,高阶组件具体怎么使用?应用场景, 高阶组件实践(源码级别) 为突破口,带大家详细了解一下高阶组件。 我们带着问题去开始今天的讨论: 1 什么是高阶组件,它解决了什么问题? 2 有几种高阶组件,它们优缺点是什么? 3 如何写一个优秀高阶组件? 4 hoc怎么处理静态属性,跨层级ref等问题? 5 高阶组件怎么控制渲染,隔离渲染? 6 高阶组件怎么监控原始组件的状态? … 高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。HOC 自身不是 React API 的一部分,它是一种基于 React 的组合特性而形成的设计模式。 二 全方位看高阶组件 1 几种包装强化组件的方式 ① mixin模式 原型图 老版本的react-mixins 在react初期提供一种组合方法。通过React.createClass,加入mixins属性,具体用法和vue 中mixins相似。具体实现如下。 const customMixin = { componentDidMount(){ console.log( '------componentDidMount------' ) }, say(){ console.log(this.state.name) } } const APP = React.createClass({ mixins: [ customMixin ], getInitialState(){ return { name:'alien' } }, render(){ const { name } = this.state return <div> hello ,world , my name is { name } </div> } }) 这种mixins只能存在createClass中,后来React.createClass连同mixins这种模式被废弃了。mixins会带来一些负面的影响。 ...

December 17, 2024

Fragment 和 Portals

一、Portals 某些情况下,我们希望渲染的内容独立于父组件,甚至独立于当前挂在的DOM元素(默认都是挂载到id为root的DOM元素上的) Portal提供了一种将子节点渲染到存在于父组件以外的DOM节点的优秀的方案: ReactDOM.createPortal(child, container) 第一个参数:是任何可渲染的React子元素,例如一个元素、字符串或fragment 第二个参数:是一个DOM元素 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点: render() { // React 挂载了一个新的 div,并且把子元素渲染其中 return ( <div> {this.props.children} </div> ); } 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的: render() { // React 并没有创建一个新的 div。它只是把子元素渲染到 `domNode` 中。 // `domNode` 是一个可以在任何位置的有效 DOM 节点。 return ReactDOM.createPortal( this.props.children, domNode ); } 一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框 案例 比如将h2挂在到id为zs的节点下 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <div id="zs"></div> </body> </html> App.jsx ...

December 17, 2024

性能优化

本文篇幅较长,将从 编译阶段 -> 路由阶段 -> 渲染阶段 -> 细节优化 -> 状态管理 -> 海量数据源,长列表渲染 方向分别加以探讨。 一 不能输在起跑线上,优化babel配置,webpack配置为项 1 真实项目中痛点 当我们用create-react-app或者webpack构建react工程的时候,有没有想过一个问题,我们的配置能否让我们的项目更快的构建速度,更小的项目体积,更简洁清晰的项目结构。 随着我们的项目越做越大,项目依赖越来越多,项目结构越来越来复杂,项目体积就会越来越大,构建时间越来越长,久而久之就会成了一个又大又重的项目,所以说我们要学会适当的为项目‘减负’,让项目不能输在起跑线上。 2 一个老项目 拿我们之前接触过的一个react老项目为例。我们没有用dva,umi快速搭建react,而是用react老版本脚手架构建的,这对这种老的react项目,上述的问题都会存在,下面让我们一起来看看。 我们首先看一下项目结构。 再看看构建时间。 为了方便大家看构建时间,我简单写了一个webpack,plugin ConsolePlugin ,记录了webpack在一次compilation所用的时间。 const chalk = require('chalk') /* console 颜色 */ var slog = require('single-line-log'); /* 单行打印 console */ class ConsolePlugin { constructor(options){ this.options = options } apply(compiler){ /** * Monitor file change 记录当前改动文件 */ compiler.hooks.watchRun.tap('ConsolePlugin', (watching) => { const changeFiles = watching.watchFileSystem.watcher.mtimes for(let file in changeFiles){ console.log(chalk.green('当前改动文件:'+ file)) } }) /** * before a new compilation is created. 开始 compilation 编译 。 */ compiler.hooks.compile.tap('ConsolePlugin',()=>{ this.beginCompile() }) /** * Executed when the compilation has completed. 一次 compilation 完成。 */ compiler.hooks.done.tap('ConsolePlugin',()=>{ this.timer && clearInterval( this.timer ) const endTime = new Date().getTime() const time = (endTime - this.starTime) / 1000 console.log( chalk.yellow(' 编译完成') ) console.log( chalk.yellow('编译用时:' + time + '秒' ) ) }) } beginCompile(){ const lineSlog = slog.stdout let text = '开始编译:' /* 记录开始时间 */ this.starTime = new Date().getTime() this.timer = setInterval(()=>{ text += '█' lineSlog( chalk.green(text)) },50) } } 构建时间如下: ...

December 17, 2024

代码分割

什么是代码分割 代码分割是一种将代码分割成多个小块的方式,然后按需加载或并行加载所需的块的技术。代码分割可以用于减少应用程序的初始加载时间或将代码切割成可按需加载的块,从而减少应用程序所需的总体代码量。 为什么要使用代码分割 在大型应用程序中,将所有代码放在一个文件中会导致应用程序加载时间过长,影响用户体验。为了解决这个问题,我们需要将代码分割成小块,然后按需加载或并行加载所需的块。 代码分割的方式 1. 动态导入 import import()是 JavaScript 中的一个动态导入语法,它允许在运行时异步加载模块。它返回一个 Promise,该 Promise 在模块加载完成后被解析为一个包含导出内容的模块对象。 使用 import()的语法如下: import(modulePath) .then((module) => { // 使用导入的模块 }) .catch((error) => { // 处理错误 }); 在这个语法中,modulePath 是一个字符串,用于指定要加载的模块路径。它可以是相对路径或绝对路径,也可以是一个包名。 当调用 import(modulePath)时,它会返回一个 Promise。这个 Promise 会在模块加载完成后被解析为一个包含导出内容的模块对象。你可以使用.then()方法来处理解析后的模块对象,并在其中使用导入的模块。 如果模块加载失败,Promise 会被拒绝,并触发.catch()方法中的错误处理逻辑。 注意事项 import()只能在模块的顶层作用域中使用,不能在函数内部或条件语句中使用。这是因为 import()是静态解析的,它在代码加载时就会执行,而不是在运行时。 另外,import()可以与其他语法结合使用,例如动态模块路径和对象解构。 动态模块路径: const modulePath = "./myModule"; import(modulePath) .then((module) => { // 使用导入的模块 }) .catch((error) => { // 处理错误 }); 在这个示例中,modulePath 是一个变量,它的值在运行时确定。这样可以根据需要动态加载不同的模块。 对象解构: import("./myModule") .then(({ myFunction, myVariable }) => { // 使用导入的函数和变量 myFunction(); console.log(myVariable); }) .catch((error) => { // 处理错误 }); 2. React.lazy React.lazy 是 React 16.6 版本引入的一个特性,它可以让你以动态的方式进行代码拆分(code splitting)。通过 React.lazy,你可以延迟加载(lazy load)一个组件,只有在需要时才会加载该组件,从而提高应用程序的性能。 React.lazy 的用法如下: ...

December 17, 2024

react 状态提升

在开始介绍状态提升之前,我想先介绍一下 Prop Drilling(属性钻取)的概念。 什么是Prop Drilling ? 那么,什么是状态提升呢?我们都知道 React 中的prop drilling(属性钻取)。prop drilling意味着从父组件向子组件传递一些 props。通过这种方式,我们可以在不同的组件之间共享数据或函数。可以使用 prop 传递任何类型的数据。 在上面的示例代码中,你可以看到,Home 组件具有一个名为 name 的状态,并且她与子组件共用 name 变量。在 React 中,只能将数据从父组件传递到子组件。 什么是 State Lifting 一般来说,状态提升允许你将数据从子组件移动到父组件,与Props Drilling不同,你可以在父组件使用子组件的状态值。这是不是很疯狂? 在 React 生态中,我们知道数据只能从父组件流向子组件,但现在将学习如何让数据反过来流动。 但是,为什么要这么做? 我们可以在父组件中定义状态并将其以 prop 的方式传递给子组件,但是这么做有可能会导致性能损失和代码复杂度提升。在我之前的博客中有提到:保持状态本地化很重要。因为当状态更新时,组件以及该组件的子组件都会重新渲染。 此外对于代码复杂性,对于如 Modal 这样的组件,选择在子组件中声明状态而不是在父组件中声明状态,将能够使代码更易于阅读。使用这种方法,你不必在每个父组件中声明状态。 通常对应用程序中的大多数组件来说,这是不必要的。但是,对于某些类型的组件,特别是可重用组件,会有这种需求。下面描述一下最通用的场景。 多说无用,上代码: 例如,我们有一个常见的 Modal 组件,我们想要在 Modal 中保留可见性状态。当一些组件想要使用这个 Modal 时,通常会定义自己的可见状态,但是通过 State Lifting,这个步骤将不再必要。 在 上面的 Modal 组件中,做了如下操作: 首先,使用forwardRef执行 ref 转发,ref 转发是一种自动将 ref 通过组件传递给子组件的技术。 使用 useState 声明状态以控制Modal组件的展示或隐藏。 使用 React 内置的 useImperativeHandle hook 将属性或者方法暴露出去,这里我们添加了一些数据和函数供父组件使用。现在父组件可以直接调用子组件的show、hide、isVisble这些方法和属性。 ...

December 17, 2024

forwardRef 该怎么用?

React.forwardRef 对于ref转发,官网是这样描述的 Ref 转发是一项将 ref 自动地通过组件传递到其一子组件的技巧。对于大多数应用中的组件来说,这通常不是必需的。但其对某些组件,尤其是可重用的组件库是很有用的。 想详细了解的可以去看官方文档 Refs 转发 – React (docschina.org) 现在让我们直奔主题吧! React.forwardRef(render)的返回值是react组件,接收的参数是一个 render函数,函数签名为render(props, ref),第二个参数将其接受的 ref 属性转发到render返回的组件中。 这项技术并不常见,但在以下两种场景中特别有用: 转发 ref 到组件内部的DOM 节点上 在高阶组件中转发ref 转发 ref到组件内部的DOM节点 比如我们想要将一个组件内部的某个元素暴露出去, 就可以这么做 // App.js import React from 'react'; import Foo from './component/Foo'; export default class App extends React.Component { constructor(props) { super(props); this.state = {}; this.input = React.createRef(); // 1 // ↑5 } handleClick = (e) => { const input = this.input.current; // 6 console.log(input); console.log(input.value); input.focus(); } render() { return ( <> <button onClick={this.handleClick}>click to get value</button> {/*2*/} <Foo ref={this.input}/> </> ) } } // Foo.jsx import React from 'react'; // 3 const Foo = React.forwardRef((props, myRef) => { return ( <div> <p>....一些其他节点</p> {/*4*/} <input type="text" defaultValue='ref 成功转发到 Foo 组件内部的 input节点上' ref={myRef}/> <p>....一些其他节点</p> <p>....一些其他节点</p> </div> ); }); export default Foo; 仔细看代码中标记的数字,这是ref转发的流程: ...

December 17, 2024

错误边界

人无完人,代码总会出错,出错并不可怕,关键是怎么处理。 我就想问问大家react的应用的错误怎么捕捉呢? 这个时候: 小白+++:怎么处理? 小白++: ErrorBoundary 小白+: ErrorBoundary, try catch 小黑#: ErrorBoundary, try catch, window.onerror 小黑##: 这个是个严肃的问题,我知道N种处理方式,你有什么更好的方案? ErrorBoundary EerrorBoundary是16版本出来的,有人问那我的15版本呢,我不听我不听,反正我用16,当然15有unstable_handleError。 关于ErrorBoundary官网介绍比较详细,这个不是重点,重点是他能捕捉哪些异常。 子组件的渲染 生命周期函数 构造函数 class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } componentDidCatch(error, info) { // Display fallback UI this.setState({ hasError: true }); // You can also log the error to an error reporting service logErrorToMyService(error, info); } render() { if (this.state.hasError) { // You can render any custom fallback UI return <h1>Something went wrong.</h1>; } return this.props.children; } } <ErrorBoundary> <MyWidget /> </ErrorBoundary> 开源世界就是好,早有大神封装了react-error-boundary 这种优秀的库。 你只需要关心出现错误后需要关心什么,还以来个 Reset, 完美。 ...

December 17, 2024

react-router

写在前面:为什么要学习react-router底层源码? 为什么要弄明白整个路由流程? 学习react-router,有助于我们学习单页面应用(spa)路由跳转原理,让我们理解从history.push,到组件页面切换的全套流程,使我们在面试的时候不再为路由相关的问题发怵,废话不说,让我们开启深入react-router源码之旅吧。 一 正确理解react-router 1 理解单页面应用 什么是单页面应用? 个人理解,单页面应用是使用一个html下,一次性加载js, css等资源,所有页面都在一个容器页面下,页面切换实质是组件的切换。 2 react-router初探,揭露路由原理面纱 ①react-router-dom和react-router和history库三者什么关系 **history** 可以理解为react-router的核心,也是整个路由原理的核心,里面集成了popState,history.pushState等底层路由实现的原理方法,接下来我们会一一解释。 **react-router**可以理解为是**react-router-dom**的核心,里面封装了Router,Route,Switch等核心组件,实现了从路由的改变到组件的更新的核心功能,在我们的项目中只要一次性引入react-router-dom就可以了。 **react-router-dom**,在react-router的核心基础上,添加了用于跳转的Link组件,和histoy模式下的BrowserRouter和hash模式下的HashRouter组件等。所谓**BrowserRouter和HashRouter,也只不过用了history库中createBrowserHistory和createHashHistory方法** react-router-dom 我们不多说了,这里我们重点看一下react-router。 ②来个小demo尝尝鲜? import { BrowserRouter as Router, Switch, Route, Redirect,Link } from 'react-router-dom' import Detail from '../src/page/detail' import List from '../src/page/list' import Index from '../src/page/home/index' const menusList = [ { name: '首页', path: '/index' }, { name: '列表', path: '/list' }, { name: '详情', path: '/detail' }, ] const index = () => { return <div > <div > <Router > <div>{ /* link 路由跳转 */ menusList.map(router=><Link key={router.path} to={ router.path } > <span className="routerLink" >{router.name}</span> </Link>) }</div> <Switch> <Route path={'/index'} component={Index} ></Route> <Route path={'/list'} component={List} ></Route> <Route path={'/detail'} component={Detail} ></Route> {/* 路由不匹配,重定向到/index */} <Redirect from='/*' to='/index' /> </Switch> </Router> </div> </div> } 效果如下 ...

December 17, 2024

react 渲染机制

前言 文本分为两大部分讲解,一部分是首次挂载渲染原理,另一部分是更新和卸载原理,很多地方非常抽象,希望大家仔细阅读,不然容易脱节。废话不多话,开车!! 正文 在开始之前,需要一些前置知识才能帮助我们更好的理解整个渲染过程。首先就是生命周期(16版本之后),为什么要讲一下生命周期?跟渲染原理有关系吗?当然有,如果你不理解渲染原理的话,更新一个嵌套很深的组件你甚至连父与子生命周期执行的先后顺序都不知道。本文直接对照16版本之后的新生命周期进行讲解,就不讲解老版本了。 初探-生命周期 顾名思义,跟人生一样,生命周期就是一个组件从诞生到销毁的过程。React在组件的生命周期中注册了一系列的钩子函数,支持开发者在其中注入代码,并在适当的时机运行。这里指的生命周期仅针对于类组件中的钩子函数。因为生命周期不是本文的重点,所以Hooks中的新增的钩子函数在本文中均不涉及,可以以后出个Hooks原理篇。 从图中可以看到,我把生命周期分为了挂载阶段、更新阶段、卸载阶段三个阶段。同时,在挂载阶段和更新阶段都会运行getDerivedStateFromProps和render,卸载阶段很好理解,只有一个componentWillUnMount,在卸载组件之前做一些事情,通常用来清除定时器等副作用操作。那么挂载阶段和更新阶段中的生命周期我们来逐一看下每个运行点及作用。 1. constructor 在同一个类组件对象只会运行一次。所以经常来做一些初始化的操作。同一个组件对象被多次创建,它们的construcotr互不干扰。 注意:在construcotr中要尽量避免(最好禁止)使用setState。 我们都知道使用setState会造成页面的重新渲染,但是在初始化阶段,页面都还没有将真实DOM挂载到页面上,那么重新渲染的又有什么意义呢。除异步的情况,比如setInterval中使用setState是没问题的,因为在执行的时候页面早已渲染完成。但也最好不要,容易一些引起奇怪的问题。 constructor(props) { super(props); this.state = { num: 1 }; //不可以,直接Warning this.setState({ num: this.state.num + 1 }); //可以使用,但不建议 setInterval(()=>{ this.setState({ num: this.state.num + 1 }); }, 1000); } 2. 静态属性 static getDerivedStateFromProps 该方法是一个静态属性,在16版本之前不存在,在新版生命周期中主要用来取代componentWillMount和componentWillReceiveProps,因为这两个老生命周期方法在一些开发者不规范的使用下极容易产生一些反模式的bug。因为是静态方法,所以你在其中根本拿不到this,更不可能调用setState。 该方法在挂载阶段和更新阶段都会运行。它有两个参数props和state当前的属性值和状态。它的返回值会合并掉当前的状态(state)。 如果返回了非Object的值,那么它啥都不会做,如果返回的是Object,那么它将会跟当前的状态合并,可以理解为Object.assign。通常情况下,几乎不怎么使用该方法。 /** * 静态方法,首次挂载和更新渲染都会运行该方法 * @param {*} props 当前属性 * @param {*} state 当前状态 */ static getDerivedStateFromProps(props, state){ // return 1; //没用 return { num: 999, //合并到当前state对象 }; } 3. render 最重要的生命周期,没有之一。用来生成虚拟节点(vDom)树。该方法只要遇到需要重新渲染都会运行。同样的,在render中也严禁使用setState,因为会导致无限递归重新渲染导致爆栈。 ...

December 17, 2024

React函数组件更新

React的函数组件(Function Components)自Hooks推出以来已成为React开发的主流方式。理解函数组件的更新原理对于编写高效、可维护的React应用至关重要。本文将深入探讨React函数组件的更新机制。 一、函数组件与类组件的本质区别 在深入更新原理前,我们需要明确函数组件与类组件的根本区别: 类组件:是类的实例,拥有实例属性和生命周期方法 函数组件:是纯函数,接收props作为参数,返回JSX 这种本质区别决定了它们的更新行为完全不同。 二、函数组件的执行机制 当React需要渲染一个函数组件时,它只是简单地调用这个函数: function MyComponent(props) { return <div>{props.message}</div>; } // React内部大致这样处理 const element = MyComponent({ message: 'Hello' }); 每次渲染都是全新的函数调用,所有局部变量和函数都会重新创建。这与类组件形成鲜明对比,类组件在更新时会复用同一个实例。 三、触发更新的场景 函数组件的更新主要由以下情况触发: 父组件重新渲染:即使props未变化,父组件渲染也会导致子组件重新渲染 props发生变化:当传入组件的props值改变时 状态更新:通过useState、useReducer等Hook更新状态 context变化:组件订阅的context值发生变化 Hooks依赖变化:useEffect、useMemo等Hook的依赖项发生变化 四、React的渲染流程 函数组件的更新遵循React的渲染流程: 触发更新:通过setState、父组件渲染等途径触发更新 协调阶段(Reconciliation): React调用函数组件获取新的JSX 与上一次渲染的JSX进行对比(Virtual DOM Diff) 提交阶段(Commit): 将差异应用到真实DOM 执行useLayoutEffect的回调 浏览器绘制:浏览器重绘屏幕 副作用执行:执行useEffect的回调 五、Hooks与组件更新 Hooks是函数组件能够拥有状态和生命周期的关键。理解Hooks的工作机制对理解组件更新至关重要。 useState的工作原理 const [count, setCount] = useState(0); React会在组件首次渲染时为每个useState调用分配一个"状态单元" 后续更新时,React会按照Hooks的调用顺序来提供对应的状态值 setCount调用会触发组件的重新渲染 useEffect与更新 useEffect(() => { // 副作用代码 return () => { // 清理函数 }; }, [dependencies]); 组件每次渲染后,React会比较依赖项数组 如果依赖项变化或没有提供依赖项数组,副作用会重新执行 清理函数会在副作用重新执行前或组件卸载时执行 六、优化更新性能 由于函数组件每次更新都会完整执行函数体,我们需要一些优化手段: ...

December 17, 2024