useReducer
什么是useReducer
useReducer
:和useState
的作用类似,用来管理相对复杂的状态数据。
操作方法
步骤:
- 定义一个
reducer
函数(根据不同的action
返回不同的新状态)。 - 在组件中调用
useReducer
,并传入reducer
函数和状态的初始值。 - 事件发生时,通过
dispatch
函数分派一个action
对象(通知reducer
要返回哪个新状态并渲染UI)。
示例代码:
1 | import { useReducer } from 'react' |
更新过程
分派action传参
方法:分派action时如果想要传递参数,需要在action对象中添加一个payload参数,放置状态参数。
示例代码:
1 | import { useReducer } from 'react' |
useMemo
作用
渲染性能优化,在每次重新渲染的时候能够缓存计算的结果。
解决的问题
假设存在这么一个场景,在一个组件中有两个状态count1
和count2
,基于count1
计算fn
,count1
变化会自动触发fn
重新计算。这一步没有问题,但是count2
变化了,也会触发fn
重新计算。这就是问题所在。
示例代码:
1 | import { useState } from 'react' |
我们点击change count2
,通过浏览器控制台,会看到fn
重新计算了。
操作
使用useMemo
做缓存之后可以保证只有count1依赖项
发生变化时才会重新计算。
1 | useMemo(() => { |
示例代码:
1 | import { useMemo, useState } from 'react' |
memo
作用
允许组件在props没有改变的情况下跳过重新渲染
组件默认的渲染机制
顶层组件发生重新渲染,这个组件树的子级组件都会被重新渲染。
如果子组件不需要做渲染更新,就存在浪费。
示例代码:
1 | import { useState } from 'react' |
操作方法
经过memo
函数包裹生成的缓存组件只有在props
发生变化的时候才会重新渲染。
1 | const MemoComponent = React.memo(function SomeComponent (props){ |
1 | import React, { useState } from 'react' |
props变化重新渲染
示例代码:
1 | import React, { useState } from 'react' |
props的比较机制
对于props的比较,进行的是"浅比较",底层使用Object.is
进行比较,针对于对象数据类型,只会对比俩次的引用是否相等,如果不相等就会重新渲染,React并不关心对象中的具体属性。
例如:
props
是简单类型,Object.is(3, 3) => true
,没有变化props
是引用类型(对象/数组),Object.is([], []) => false
,有变化,React只关心引用是否变化。
示例代码:
1 | import React, { useState } from 'react' |
解释说明:虽然两次的list
状态都是[1,2,3]
,但是因为组件App两次渲染生成了不同的对象引用list
,所以传给MemoSon
组件的props
视为不同,子组件就会发生重新渲染。
自定义比较函数
1 | import React, { useState } from 'react' |
useCallback
作用
在组件多次重新渲染的时候缓存函数。
解决的问题
在下文的代码中,尽管Input
组件被memo
高阶组件包裹,用来防止不必要的重新渲染,但是memo
的作用是基于比较props
的变化来决定是否需要重新渲染。具体来说,memo
会浅比较当前和上一次的props
,如果它们是相等的(即没有变化),那么组件就不会被重新渲染;反之,如果props
有变化,组件就会被重新渲染。
在下文的代码中,App
组件中的changeHandler
函数是在每次父组件渲染时创建的新函数。即使这个函数的逻辑没有改变,由于它是一个新的函数引用,所以传递给Input
组件的onChange prop
在每次父组件重新渲染时都会被视为不同的值。因此,即使Input
组件使用了memo
,它仍然会在父组件状态(如count
)改变时被重新渲染,因为它的onChange prop
被认为是变化了。
示例代码:
1 | import { memo, useState } from "react" |
useCallback缓存函数
useCallback缓存之后的函数可以在组件渲染时保持引用稳定,也就是返回同一个引用。
示例代码:
1 | import { memo, useCallback, useState } from "react" |
重点关注:const changeHandler = useCallback((value) => console.log(value), [])
。
forwardRef
作用
允许自定义组件使用ref将一个DOM节点暴露给父组件。
解决的问题
假设存在一个自定义组件Input
,我们给该组件绑定ref
。
示例代码:
1 | import { useRef } from 'react' |
运行结果:
1 | hook.js:608 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? |
操作方法
1 | import { forwardRef, useRef } from 'react' |
useImperativeHandle
作用
不暴露子组件中的DOM元素,而暴露子组件内部的方法。
操作方法
示例代码:
1 | import { forwardRef, useImperativeHandle, useRef } from "react" |
zustand
什么是zustand
zustand
,状态管理工具,相比Redux
更简洁。
- 官网:https://zustand-demo.pmnd.rs/
- Github地址:https://github.com/pmndrs/zustand
- 官方文档:https://zustand.docs.pmnd.rs/getting-started/introduction
- 安装命令:
npm i zustand
快速开始
创建store,store/useCounterStore.js
:
1 | import { create } from 'zustand' |
App.js
:
1 | import useStore from './store/useCounterStore.js' |
异步支持
优点
对于异步操作的支持不需要特殊的操作,直接在函数中编写异步逻辑,最后把接口的数据放到set函数中返回即可。
案例
store/channelStore.js
:
1 | import { create } from 'zustand' |
App.js
:
1 | import { useEffect } from 'react' |
切片模式
场景
当我们单个store比较大的时候,可以采用一种切片模式
进行模块拆分再组合。
拆分并组合切片
store/useStore.js
:
1 | import { create } from 'zustand' |
组件使用
App.js
:
1 | import useStore from "./store/useStore" |
Taro
什么是Taro
Taro,一个开放式跨端跨框架解决方案,支持使用"React/Vue/Nerv"等框架来开发"微信/京东/百度/支付宝/字节跳动/QQ/飞书"小程序和"H5/RN"等应用。
- 官网:https://taro.zone
- 官网文档:https://taro-docs.jd.com/docs
- GitHub地址:https://github.com/nervjs/taro
- 安装命令:
npm install -g @tarojs/cli
案例(微信小程序)
开始
初始化项目
安装指定版本的@tarojs/cli
:
1 | npm install -g @tarojs/cli@3.6.35 |
使用Taro初始化项目:
taro init myApp
,然后会有一些选项,在本文,我们选择如下:- 请输入项目介绍
- 请选择框架 React
- 是否需要使用 TypeScript ? No
- 请选择 CSS 预处理器(Sass/Less/Stylus) Sass
- 请选择编译工具 Webpack5
- 请选择包管理工具 npm
- 请选择模板源 Github(最新)
- 请选择模板 taro-ui(使用 taro-ui 的模板)
cd myApp
,进入新创建的项目根目录。npm install
,安装依赖。
然后我们执行npm run build:weapp
,构建微信小程序页面。
再使用微信开发者工具,导入项目。
导入项目后,我们点击编译,即可看到效果。
函数组件
点开src/app.js
,内容如下:
1 | import { Component } from 'react' |
注意class App extends Component
,这是类组件(Class Components)。实际上,React官方已经不推荐这种方式了。
我们将其修改为React官方推荐的"函数组件(Functional Components)。
示例代码:
1 | import './app.scss'; |
- 去除了
componentDidMount () {}
、componentDidShow () {}
和componentDidHide () {}
,没有实际业务逻辑的代码。
对于pages/index/index.jsx
,同样,改成函数组件。
1 | import { View, Text } from '@tarojs/components'; |
目录结构
assets
:用于存放使用到的素材。components
:用于存放使用的通用子组件。data
:用于存放使用到的数据。util
:用于存放工具类代码。
首页
基本结构
在app.js
新增一行import 'taro-ui/dist/style/index.scss'
,引入全局样式。
如需分别引入样式文件,可以参考TaroUI的官网。https://taro-ui.jd.com
app.js
:
1 | import './app.scss'; |
pages/index/index.jsx
:
1 | import { View, Image } from '@tarojs/components' |
pages/index/index.scss
:
1 | .indexPage{ |
对于底部的GlobalFooter
,我们将其封装成一个独立的组件。
components/GlobalFooter/index.jsx
:
1 | import { View } from '@tarojs/components' |
components/GlobalFooter/index.scss
:
1 | .footPage{ |
交互逻辑
点击按钮后,进行调整。
关于跳转方法,可以参考Taro官方文档的路由跳转。
https://taro-docs.jd.com/docs/router#路由跳转
pages/index/index.jsx
:
1 | import Taro from '@tarojs/taro' |
问答页
基本结构
pages/doQuestion/index.jsx
:
1 | import { View } from '@tarojs/components' |
pages/doQuestion/index.scss
:
1 | .questionPage{ |
定义标题为"问答",pages/doQuestion/index.config.js
:
1 | export default definePageConfig({ |
在app.config.js
添加'pages/doQuestion/index'
:
1 | export default defineAppConfig({ |
交互逻辑
关键点:
- 当前的题目号、题目内容。
- 按钮的出现逻辑:
- 上一题按钮,在第一题的时候,不出现。
- 查看结果按钮,只有当题目数为10时出现。
- 题目数为10时,下一题按钮不出现。
- 只有当选择后,下一题按钮才能有效,否则无效。
- 单选框初初始不显示勾。
- 将心理测试的问题导入并显示。
- 答案存储。
- 跳转到结果页。
- 回到前一题时,应该显示之前的选项。
1 | import { View } from '@tarojs/components'; |
结果页
pages/result/index.jsx
:
1 | import {Image, View} from '@tarojs/components' |
pages/result/index.scss
:
1 | .resultPage{ |
pages/result/index.config.js
:
1 | export default definePageConfig({ |
src/util/bzUtil.js
:
1 | const answerList = ["B","B","B","A"]; |
app.config.js
:
1 | export default defineAppConfig({ |
完整版本myApp.zip
提供下载链接。