React 性能优化完全指南,将自己这几年的心血总结成篇!

拓展知识

  1. 推荐阅读为什么 setState 是异步的?[19]
  1. 为什么面试官不会问“函数组件中的 setState 是同步的还是异步的?”?因为函数组件中生成的函数是通过闭包引用了 state,而不是通过 this.state 的方式引用 state,所以函数组件的处理函数中 state 一定是旧值,不可能是新值。可以说函数组件已经将这个问题屏蔽掉了,所以面试官也就不会问了。可参考线上示例[20]。
  1. 根据官方文档[21],在 React 并发模式中,将默认以批量更新方式执行 setState。到那时候,也可能就不需要这个优化了。

按优先级更新,及时响应用户


优先级更新是批量更新的逆向操作,其思想是:优先响应用户行为,再完成耗时操作。常见的场景是:页面弹出一个 Modal,当用户点击 Modal 中的确定按钮后,代码将执行两个操作。

a) 关闭 Modal。

b) 页面处理 Modal 传回的数据并展示给用户。

当 b) 操作需要执行 500ms 时,用户会明显感觉到从点击按钮到 Modal 被关闭之间的延迟。

例子参考:CodeSandbox 在线 Demo[22]。

在该例子中,用户添加一个整数后,页面要隐藏输入框,并将新添加的整数加入到整数列表,将列表排序后再展示。

以下为一般的实现方式,将 slowHandle 函数作为用户点击按钮的回调函数。

const slowHandle = () => {

setShowInput(false)

setNumbers([…numbers, +inputValue].sort((a, b) => a - b))

}

slowHandle() 执行过程耗时长,用户点击按钮后会明显感觉到页面卡顿。

如果让页面优先隐藏输入框,用户便能立刻感知到页面更新,不会有卡顿感。

实现优先级更新的要点是将耗时任务移动到下一个宏任务中执行,优先响应用户行为。

例如在该例中,将 setNumbers 移动到 setTimeout 的回调中,用户点击按钮后便能立即看到输入框被隐藏,不会感知到页面卡顿。优化后的代码如下。

const fastHandle = () => {

// 优先响应用户行为

setShowInput(false)

// 将耗时任务移动到下一个宏任务执行

setTimeout(() => {

setNumbers([…numbers, +inputValue].sort((a, b) => a - b))

})

}

发布者订阅者跳过中间组件 Render 过程


React 推荐将公共数据放在所有「需要该状态的组件」的公共祖先上,但将状态放在公共祖先上后,该状态就需要层层向下传递,直到传递给使用该状态的组件为止。

每次状态的更新都会涉及中间组件的 Render 过程,但中间组件并不关心该状态,它的 Render 过程只负责将该状态再传给子组件。在这种场景下可以将状态用发布者订阅者模式维护,只有关心该状态的组件才去订阅该状态,不再需要中间组件传递该状态。

当状态更新时,发布者发布数据更新消息,只有订阅者组件才会触发 Render 过程,中间组件不再执行 Render 过程。

只要是发布者订阅者模式的库,都可以进行该优化。比如:redux、use-global-state、React.createContext 等。例子参考:发布者订阅者模式跳过中间组件的渲染阶段[23],本示例使用 React.createContext 进行实现。

import { useState, useEffect, createContext, useContext } from ‘react’

const renderCntMap = {}

const renderOnce = name => {

return (renderCntMap[name] = (renderCntMap[name] || 0) + 1)

}

// 将需要公共访问的部分移动到 Context 中进行优化

// Context.Provider 就是发布者

// Context.Consumer 就是消费者

const ValueCtx = createContext()

const CtxContainer = ({ children }) => {

const [cnt, setCnt] = useState(0)

useEffect(

() => {

const timer = window.setInterval(() => {

setCnt(v => v + 1)

}, 1000)

return () => clearInterval(timer)

},

[setCnt]

)

return <ValueCtx.Provider value={cnt}>{children}</ValueCtx.Provider>

}

function CompA({}) {

const cnt = useContext(ValueCtx)

// 组件内使用 cnt

return (

组件 CompA Render 次数:

{renderOnce(‘CompA’)}

)

}

function CompB({}) {

const cnt = useContext(ValueCtx)

// 组件内使用 cnt

return (

组件 CompB Render 次数:

{renderOnce(‘CompB’)}

)

}

function CompC({}) {

return (

组件 CompC Render 次数:

{renderOnce(‘CompC’)}

)

}

export const PubSubCommunicate = () => {

return (

优化后场景

将状态提升至最低公共祖先的上层,用 CtxContainer 将其内容包裹。

每次 Render 时,只有组件A和组件B会重新 Render 。

父组件 Render 次数:

{renderOnce(‘parent’)}

)

}

export default PubSubCommunicate

运行后效果:TODO: 放图。从图中可看出,优化后只有使用了公共状态的组件 CompA 和 CompB 发生了更新,减少了父组件和 CompC 组件的 Render 次数。

useMemo 返回虚拟 DOM 可跳过该组件 Render 过程


利用 useMemo 可以缓存计算结果的特点,如果 useMemo 返回的是组件的虚拟 DOM,则将在 useMemo 依赖不变时,跳过组件的 Render 阶段。

该方式与 React.memo 类似,但与 React.memo 相比有以下优势:

  1. 更方便。React.memo 需要对组件进行一次包装,生成新的组件。而 useMemo 只需在存在性能瓶颈的地方使用,不用修改组件。

  2. 更灵活。useMemo 不用考虑组件的所有 Props,而只需考虑当前场景中用到的值,也可使用 useDeepCompareMemo[24] 对用到的值进行深比较。

例子参考:useMemo 跳过组件 Render 过程[25]。

该例子中,父组件状态更新后,不使用 useMemo 的子组件会执行 Render 过程,而使用 useMemo 的子组件不会执行。

import { useEffect, useMemo, useState } from ‘react’

import ‘./styles.css’

const renderCntMap = {}

function Comp({ name }) {

renderCntMap[name] = (renderCntMap[name] || 0) + 1

return (

组件「

{name}」 Render 次数:

{renderCntMap[name]}

)

}

export default function App() {

const setCnt = useState(0)[1]

useEffect(

() => {

const timer = window.setInterval(() => {

setCnt(v => v + 1)

}, 1000)

return () => clearInterval(timer)

},

[setCnt]

)

const comp = useMemo(() => {

return 

}, [])

return (

{comp}

)

}

debounce、throttle 优化频繁触发的回调


在搜索组件中,当 input 中内容修改时就触发搜索回调。当组件能很快处理搜索结果时,用户不会感觉到输入延迟。

但实际场景中,中后台应用的列表页非常复杂,组件对搜索结果的 Render 会造成页面卡顿,明显影响到用户的输入体验。

在搜索场景中一般使用 useDebounce[26] + useEffect 的方式获取数据。

例子参考:debounce-search[27]。

import { useState, useEffect } from ‘react’

import { useDebounce } from ‘use-debounce’

export default function App() {

const [text, setText] = useState(‘Hello’)

const [debouncedValue] = useDebounce(text, 300)

useEffect(

() => {

// 根据 debouncedValue 进行搜索

},

[debouncedValue]

)

return (

<input

defaultValue={‘Hello’}

onChange={e => {

setText(e.target.value)

}}

/>

Actual value: {text}

Debounce value: {debouncedValue}

)

}

为什么搜索场景中是使用 debounce,而不是 throttle 呢?throttle 是 debounce 的特殊场景,throttle 给 debounce 传了 maxWait 参数,可参考 useThrottleCallback[28]。

在搜索场景中,只需响应用户最后一次输入,无需响应用户的中间输入值,debounce 更适合使用在该场景中。

而 throttle 更适合需要实时响应用户的场景中更适合,如通过拖拽调整尺寸或通过拖拽进行放大缩小(如:window 的 resize 事件)。

实时响应用户操作场景中,如果回调耗时小,甚至可以用  requestAnimationFrame 代替 throttle。

懒加载


在 SPA 中,懒加载优化一般用于从一个路由跳转到另一个路由。

还可用于用户操作后才展示的复杂组件,比如点击按钮后展示的弹窗模块(有时候弹窗就是一个复杂页面 ???)。

在这些场景下,结合 Code Split 收益较高。懒加载的实现是通过 Webpack 的动态导入和 React.lazy 方法,参考例子 lazy-loading[29]。

实现懒加载优化时,不仅要考虑加载态,还需要对加载失败进行容错处理。

import { lazy, Suspense, Component } from ‘react’

import ‘./styles.css’

// 对加载失败进行容错处理

class ErrorBoundary extends Component {

constructor(props) {

super(props)

this.state = { hasError: false }

}

static getDerivedStateFromError(error) {

return { hasError: true }

}

render() {

if (this.state.hasError) {

return 

这里处理出错场景

}

return this.props.children

}

}

const Comp = lazy(() => {

return new Promise((resolve, reject) => {

setTimeout(() => {

if (Math.random() > 0.5) {

reject(new Error(‘模拟网络出错’))

} else {

resolve(import(‘./Component’))

}

}, 2000)

})

})

export default function App() {

return (

实现懒加载优化时,不仅要考虑加载态,还需要对加载失败进行容错处理。

)

}

懒渲染


懒渲染指当组件进入或即将进入可视区域时才渲染组件。常见的组件 Modal/Drawer 等,当 visible 属性为 true 时才渲染组件内容,也可以认为是懒渲染的一种实现。懒渲染的使用场景有:

  1. 页面中出现多次的组件,且组件渲染费时、或者组件中含有接口请求。如果渲染多个带有请求的组件,由于浏览器限制了同域名下并发请求的数量,就可能会阻塞可见区域内的其他组件中的请求,导致可见区域的内容被延迟展示。

  2. 需用户操作后才展示的组件。这点和懒加载一样,但懒渲染不用动态加载模块,不用考虑加载态和加载失败的兜底处理,实现上更简单。

懒渲染的实现中判断组件是否出现在可视区域内是通过 react-visibility-observer[30] 进行监听。例子参考:懒渲染[31]

import { useState, useEffect } from ‘react’

import VisibilityObserver, {

useVisibilityObserver

} from ‘react-visibility-observer’

const VisibilityObserverChildren = ({ callback, children }) => {

const { isVisible } = useVisibilityObserver()

useEffect(

() => {

callback(isVisible)

},

[callback, isVisible]

)

return <>{children}</>

}

export const LazyRender = () => {

const [isRendered, setIsRendered] = useState(false)

if (!isRendered) {

return (

<VisibilityObserver rootMargin={‘0px 0px 0px 0px’}>

<VisibilityObserverChildren

callback={isVisible => {

if (isVisible) {

setIsRendered(true)

}

}}

)

}

console.log(‘滚动到可视区域才渲染’)

return 

我是 LazyRender 组件

}

export default LazyRender

虚拟列表


虚拟列表是懒渲染的一种特殊场景。虚拟列表的组件有  react-window[32] 和 react-virtualized,它们都是同一个作者开发的。

react-window 是 react-virtualized 的轻量版本,其 API 和文档更加友好。

所以新项目中推荐使用 react-window,而不是使用 Star 更多的 react-virtualized。

使用 react-window 很简单,只需要计算每项的高度即可。下面代码中每一项的高度是 35px。

例子参考:官方示例[33]

import { FixedSizeList as List } from ‘react-window’

const Row = ({ index, style }) => 

Row {index}

const Example = () => (

<List

height={150}

itemCount={1000}

itemSize={35} // 每项的高度为 35

width={300}

{Row}

)

如果每项的高度是变化的,可给 itemSize 参数传一个函数。对于这个优化点,笔者遇到一个真实案例。

在公司的招聘项目中,通过下拉菜单可查看某个候选人的所有投递记录。平常这个列表也就几十条,但后来用户反馈『下拉菜单点击后要很久才能展示出投递列表』。

该问题的原因就是这个候选人在我们系统中有上千条投递,一次性展示上千条投递导致页面卡住了。

所以在开发过程中,遇到接口返回的是所有数据时,需提前预防这类 bug,使用虚拟列表优化。

跳过回调函数改变触发的 Render 过程


React 组件的 Props 可以分为两类。

a) 一类是在对组件 Render 有影响的属性,如:页面数据、getPopupContainer[34] 和 renderProps 函数。

b) 另一类是组件 Render 后的回调函数,如:onClick、onVisibleChange[35]。

b) 类属性并不参与到组件的 Render 过程,因为可以对 b) 类属性进行优化。

当 b)类属性发生改变时,不触发组件的重新 Render ,而是在回调触发时调用最新的回调函数。

Dan Abramov 在 A Complete Guide to useEffect[36] 文章中认为,每次 Render 都有自己的事件回调是一件很酷的特性。

但该特性要求每次回调函数改变就触发组件的重新 Render ,这在性能优化过程中是可以取舍的。

例子参考:跳过回调函数改变触发的 Render 过程[37]。

以下代码比较难以理解,可通过调试该例子,帮助理解消化。

import { Children, cloneElement, memo, useEffect, useRef } from ‘react’

import { useDeepCompareMemo } from ‘use-deep-compare’

import omit from ‘lodash.omit’

let renderCnt = 0

export function SkipNotRenderProps({ children, skips }) {

if (!skips) {

// 默认跳过所有回调函数

skips = prop => prop.startsWith(‘on’)

}

const child = Children.only(children)

const childProps = child.props

const propsRef = useRef({})

const nextSkippedPropsRef = useRef({})

Object.keys(childProps)

.filter(it => skips(it))

.forEach(key => {

// 代理函数只会生成一次,其值始终不变

nextSkippedPropsRef.current[key] =

nextSkippedPropsRef.current[key] ||

function skipNonRenderPropsProxy(…args) {

propsRef.current[key].apply(this, args)

}

})

useEffect(() => {

propsRef.current = childProps

})

// 这里使用 useMemo 优化技巧

// 除去回调函数,其他属性改变生成新的 React.Element

return useDeepCompareMemo(

() => {

return cloneElement(child, {

…child.props,

…nextSkippedPropsRef.current

})

},

[omit(childProps, Object.keys(nextSkippedPropsRef.current))]

)

}

// SkipNotRenderPropsComp 组件内容和 Normal 内容一样

export function SkipNotRenderPropsComp({ onClick }) {

return (

跳过『与 Render 无关的 Props』改变触发的重新 Render

Render 次数为:

{++renderCnt}

<button onClick={onClick} style={{ color: ‘blue’ }}>

点我回调,回调弹出值为 1000(优化成功)

)

}

export default SkipNotRenderPropsComp

动画库直接修改 DOM 属性,跳过组件 Render 阶段


这个优化在业务中应该用不上,但还是非常值得学习的,将来可以应用到组件库中。

参考 react-spring[38]  的动画实现,当一个动画启动后,每次动画属性改变不会引起组件重新 Render ,而是直接修改了 dom 上相关属性值。

例子演示:CodeSandbox 在线 Demo[39]

import React, { useState } from ‘react’

import { useSpring, animated as a } from ‘react-spring’

import ‘./styles.css’

let renderCnt = 0

export function Card() {

const [flipped, set] = useState(false)

const { transform, opacity } = useSpring({

opacity: flipped ? 1 : 0,

transform: perspective(600px) rotateX(${flipped ? 180 : 0}deg),

config: { mass: 5, tension: 500, friction: 80 }

})

// 尽管 opacity 和 transform 的值在动画期间一直变化

// 但是并没有组件的重新 Render

return (

Render 次数:

{++renderCnt}

<a.div

class=“c back”

style={{ opacity: opacity.interpolate(o => 1 - o), transform }}

/>

<a.div

class=“c front”

style={{

opacity,

transform: transform.interpolate(t => ${t} rotateX(180deg))

}}

/>

)

}

export default Card

避免在 didMount、didUpdate 中更新组件 State


这个技巧不仅仅适用于 didMount、didUpdate,还包括 willUnmount、useLayoutEffect 和特殊场景下的 useEffect(当父组件的 cDU/cDM 触发时,子组件的 useEffect 会同步调用),本文为叙述方便将他们统称为「提交阶段钩子」。

React 工作流[40]提交阶段的第二步就是执行提交阶段钩子,它们的执行会阻塞浏览器更新页面。

如果在提交阶段钩子函数中更新组件 State,会再次触发组件的更新流程,造成两倍耗时。一般在提交阶段的钩子中更新组件状态的场景有:

  1. 计算并更新组件的派生状态(Derived State)。在该场景中,类组件应使用 getDerivedStateFromProps[41] 钩子方法代替,函数组件应使用函数调用时执行 setState[42]的方式代替。使用上面两种方式后,React 会将新状态和派生状态在一次更新内完成。

  2. 根据 DOM 信息,修改组件状态。在该场景中,除非想办法不依赖 DOM 信息,否则两次更新过程是少不了的,就只能用其他优化技巧了。

use-swr 的源码[43]就使用了该优化技巧。当某个接口存在缓存数据时,use-swr 会先使用该接口的缓存数据,并在 requestIdleCallback 时再重新发起请求,获取最新数据。

如果 use-swr 不做该优化的话,就会在 useLayoutEffect 中触发重新验证并设置 isValidating 状态为 true[44],引起组件的更新流程,造成性能损失。

React Profiler 定位 Render 过程瓶颈

=============================

React Profiler 是 React 官方提供的性能审查工具,本文只介绍笔者的使用心得,详细的使用手册请移步官网文档[45]。

Profiler 只记录了 Render 过程耗时


通过 React Profiler,开发者可以查看组件 Render 过程耗时,但无法知晓提交阶段的耗时。

尽管 Profiler 面板中有 Committed at 字段,但这个字段是相对于录制开始时间,根本没有意义。

所以提醒读者不要通过 Profiler 定位非 Render 过程的性能瓶颈问题。

通过在 React v16 版本上进行实验,同时开启 Chrome 的 Performance 和 React Profiler 统计。

如下图,在 Performance 面板中,调和阶段和提交阶段耗时分别为 642ms 和 300ms,而 Profiler 面板中只显示了 642ms。

拓展知识

  1. React 在 v17 版本后已移除 User Timing 统计功能,具体原因可参考 PR#18417[46]。
  1. 在 v17 版本上,笔者也通过测试代码[47]验证了 Profiler 中的统计信息并不包含提交阶段,有兴趣的读者可以看看。

开启「记录组件更新原因」


点击面板上的齿轮,然后勾选「Record why each component rendered while profiling.」,如下图。

然后点击面板中的虚拟 DOM 节点,右侧便会展示该组件重新 Render 的原因。

定位产生本次 Render 过程原因


由于 React 的批量更新(Batch Update)机制,产生一次 Render 过程可能涉及到很多个组件的状态更新。那么如何定位是哪些组件状态更新导致的呢?

在 Profiler 面板左侧的虚拟 DOM 树结构中,从上到下审查每个发生了渲染的(不会灰色的)组件。

如果组件是由于 State 或 Hook 改变触发了 Render 过程,那它就是我们要找的组件,如下图。

结语

==

笔者是从年前开始写这篇文章,到发布时已经写了一个月了,期间断断续续将自己这几年对 React 的理解加入到文章中,然后调整措辞和丰富示例,最后终于在周四前完成(周四是我定的 deadline ???)。既然自己付出了那么多努力,那就希望它能成为一份优秀的 React 优化手册吧。

参考资料

[1]

线上代码: https://codesandbox.io/s/cdm-yu-commit-jieduanzhixingshunxu-fzu1w?file=/src/App.js

[2]

列表项使用 key 属性: #heading-7

[3]

避免在 didMount、didUpdate 中更新组件 State: #heading-18

[4]

React 组件的生命周期图: https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/

[5]

介绍事件循环的视频: https://www.youtube.com/watch?v=u1kqx6AenYw&t=853s

[6]

展开语法: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax

[7]

…state, item]`。这点可参考 Dan Abramov 在[演讲 Redux 时: https://www.youtube.com/watch?v=xsSnOQynTHs&t=690s

[8]

发布者订阅者跳过中间组件 Render 过程: #heading-10

[9]

跳过回调函数改变触发的 Render 过程: #heading-16

[10]

React Server Hooks 代码: https://github.com/facebook/react/blob/ee432635724d5a50301448016caa137ac3c0a7a2/packages/react-dom/src/server/ReactPartialRendererHooks.js#L452

[11]

useCallback: https://github.com/facebook/react/blob/ee432635724d5a50301448016caa137ac3c0a7a2/packages/react-reconciler/src/ReactFiberHooks.new.js#L1590

[12]

useMemo: https://github.com/facebook/react/blob/ee432635724d5a50301448016caa137ac3c0a7a2/packages/react-reconciler/src/ReactFiberHooks.new.js#L1613

[13]

How to memoize calculations: https://reactjs.org/docs/hooks-faq.html#how-to-memoize-calculations

[14]

memoizee: https://www.npmjs.com/package/memoizee

[15]

React 官方推荐: https://reactjs.org/docs/lists-and-keys.html#keys

[16]

没有添加、删除、排序功能的分页列表: https://codesandbox.io/s/meiyoutianjiashanchupaixugongnengdefenyeliebiao-d6zqr?file=/src/App.js

[17]

setState 同步还是异步: https://codesandbox.io/s/setstate-tongbuhuanshiyibu-1bo16

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

你要问前端开发难不难,我就得说计算机领域里常说的一句话,这句话就是『难的不会,会的不难』,对于不熟悉某领域技术的人来说,因为不了解所以产生神秘感,神秘感就会让人感觉很难,也就是『难的不会』;当学会这项技术之后,知道什么什么技术能做到什么做不到,只是做起来花多少时间的问题而已,没啥难的,所以就是『会的不难』。

我特地针对初学者整理一套前端学习资料,免费分享给大家,戳这里即可免费领取

前端路线图

calculations: https://reactjs.org/docs/hooks-faq.html#how-to-memoize-calculations

[14]

memoizee: https://www.npmjs.com/package/memoizee

[15]

React 官方推荐: https://reactjs.org/docs/lists-and-keys.html#keys

[16]

没有添加、删除、排序功能的分页列表: https://codesandbox.io/s/meiyoutianjiashanchupaixugongnengdefenyeliebiao-d6zqr?file=/src/App.js

[17]

setState 同步还是异步: https://codesandbox.io/s/setstate-tongbuhuanshiyibu-1bo16

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-vLuu4F3G-1712200454133)]

[外链图片转存中…(img-byE00qbG-1712200454133)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-f11xMerG-1712200454134)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

你要问前端开发难不难,我就得说计算机领域里常说的一句话,这句话就是『难的不会,会的不难』,对于不熟悉某领域技术的人来说,因为不了解所以产生神秘感,神秘感就会让人感觉很难,也就是『难的不会』;当学会这项技术之后,知道什么什么技术能做到什么做不到,只是做起来花多少时间的问题而已,没啥难的,所以就是『会的不难』。

我特地针对初学者整理一套前端学习资料,免费分享给大家,戳这里即可免费领取

[外链图片转存中…(img-jiViK9nQ-1712200454134)]

vue.js的36个技巧

  • 25
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值