avatar


9.React [4/4]

useReducer

什么是useReducer

useReducer:和useState的作用类似,用来管理相对复杂的状态数据。

操作方法

步骤:

  1. 定义一个reducer函数(根据不同的action返回不同的新状态)。
  2. 在组件中调用useReducer,并传入reducer函数和状态的初始值。
  3. 事件发生时,通过dispatch函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI)。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { useReducer } from 'react'

// 1. 定义reducer函数,根据不同的action返回不同的新状态
function reducer(state, action) {
switch (action.type) {
case 'INC':
return state + 1
case 'DEC':
return state - 1
default:
return state
}
}

function App() {
// 2. 使用useReducer分派action
const [state, dispatch] = useReducer(reducer, 0)
return (
<>
{/* 3. 调用dispatch函数传入action对象 触发reducer函数,分派action操作,使用新状态更新视图 */}
<button onClick={() => dispatch({ type: 'DEC' })}>-</button>
{state}
<button onClick={() => dispatch({ type: 'INC' })}>+</button>
</>
)
}

export default App;

更新过程

更新过程

分派action传参

方法:分派action时如果想要传递参数,需要在action对象中添加一个payload参数,放置状态参数。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { useReducer } from 'react'

// 1. 根据不同的action返回不同的新状态
function reducer(state, action) {
console.log('reducer执行了')
switch (action.type) {
case 'INC':
return state + 1
case 'DEC':
return state - 1
case 'SET':
return action.payload
default:
return state
}
}

function App() {
// 2. 使用useReducer分派action
const [state, dispatch] = useReducer(reducer, 0)
return (
<>
{/* 3. 调用dispatch函数传入action对象 触发reducer函数,分派action操作,使用新状态更新视图 */}
<button onClick={() => dispatch({ type: 'DEC' })}>-</button>
{state}
<button onClick={() => dispatch({ type: 'INC' })}>+</button>
<button onClick={() => dispatch({ type: 'SET', payload: 100 })}>
set to 100
</button>
</>
)
}

export default App

useMemo

作用

渲染性能优化,在每次重新渲染的时候能够缓存计算的结果。

解决的问题

解决的问题

假设存在这么一个场景,在一个组件中有两个状态count1count2,基于count1计算fncount1变化会自动触发fn重新计算。这一步没有问题,但是count2变化了,也会触发fn重新计算。这就是问题所在。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { useState } from 'react'

function fib(n) {
console.log('斐波那契函数执行了')
if (n < 3) return 1
return fib(n - 2) + fib(n - 1)
}

function App() {

const [count1, setCount1] = useState(0)
const result = fib(count1)
const [count2, setCount2] = useState(0)
console.log('组件重新渲染了')
return (
<div>
<button onClick={() => setCount1(count1 + 1)}>change count1: {count1} </button>
<button onClick={() => setCount2(count2 + 1)}>change count2: {count2} </button>
{result}
</div>
)
}

export default App

我们点击change count2,通过浏览器控制台,会看到fn重新计算了。

操作

使用useMemo做缓存之后可以保证只有count1依赖项发生变化时才会重新计算。

1
2
3
useMemo(() => {
// 根据count1计算结果
}, [count1])

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useMemo, useState } from 'react'

function fib(n) {
console.log('斐波那契函数执行了')
if (n < 3) return 1
return fib(n - 2) + fib(n - 1)
}

function App() {

const [count1, setCount1] = useState(0)

const result = useMemo(() => {
// 返回计算得到的结果
return fib(count1)
},[count1])

const [count2, setCount2] = useState(0)
console.log('组件重新渲染了')
return (
<div>
<button onClick={() => setCount1(count1 + 1)}>change count1: {count1} </button>
<button onClick={() => setCount2(count2 + 1)}>change count2: {count2} </button>
{result}
</div>
)
}

export default App

memo

作用

允许组件在props没有改变的情况下跳过重新渲染

组件默认的渲染机制

顶层组件发生重新渲染,这个组件树的子级组件都会被重新渲染。
如果子组件不需要做渲染更新,就存在浪费。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useState } from 'react'

function Son() {
console.log('子组件被重新渲染了')
return <div>this is son</div>
}

function App() {
const [, forceUpdate] = useState()
console.log('父组件重新渲染了')
return (
<>
<Son />
<button onClick={() => forceUpdate(Math.random())}>update</button>
</>
)
}

export default App

操作方法

经过memo函数包裹生成的缓存组件只有在props发生变化的时候才会重新渲染。

1
2
3
const MemoComponent = React.memo(function SomeComponent (props){
//
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { useState } from 'react'

const MemoSon = React.memo(function Son() {
console.log('子组件被重新渲染了')
return <div>this is span</div>
})

function App() {
const [, forceUpdate] = useState()
console.log('父组件重新渲染了')
return (
<>
<MemoSon />
<button onClick={() => forceUpdate(Math.random())}>update</button>
</>
)
}

export default App

props变化重新渲染

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useState } from 'react'

const MemoSon = React.memo(function Son() {
console.log('子组件被重新渲染了')
return <div>this is span</div>
})

function App() {
console.log('父组件重新渲染了')

const [count, setCount] = useState(0)
return (
<>
<MemoSon count={count} />
<button onClick={() => setCount(count + 1)}>+{count}</button>
</>
)
}

export default App

props的比较机制

对于props的比较,进行的是"浅比较",底层使用Object.is进行比较,针对于对象数据类型,只会对比俩次的引用是否相等,如果不相等就会重新渲染,React并不关心对象中的具体属性。

例如:

  • props是简单类型,Object.is(3, 3) => true,没有变化
  • props是引用类型(对象/数组),Object.is([], []) => false,有变化,React只关心引用是否变化。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React, { useState } from 'react'

const MemoSon = React.memo(function Son() {
console.log('子组件被重新渲染了')
return <div>this is span</div>
})

function App() {
// const [count, setCount] = useState(0)
const [list, setList] = useState([1, 2, 3])
return (
<>
<MemoSon list={list} />
<button onClick={() => setList([1, 2, 3])}>
{JSON.stringify(list)}
</button>
</>
)
}

export default App

解释说明:虽然两次的list状态都是[1,2,3],但是因为组件App两次渲染生成了不同的对象引用list,所以传给MemoSon组件的props视为不同,子组件就会发生重新渲染。

自定义比较函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React, { useState } from 'react'

// 自定义比较函数
function arePropsEqual(oldProps, newProps) {
console.log(oldProps, newProps)
return (
oldProps.list.length === newProps.list.length &&
oldProps.list.every((oldItem, index) => {
const newItem = newProps.list[index]
console.log(newItem, oldItem)
return oldItem === newItem
})
)
}

const MemoSon = React.memo(function Son() {
console.log('子组件被重新渲染了')
return <div>this is span</div>
}, arePropsEqual)

function App() {
console.log('父组件重新渲染了')
const [list, setList] = useState([1, 2, 3])
return (
<>
<MemoSon list={list} />
<button onClick={() => setList([1, 2, 3])}>
内容一样{JSON.stringify(list)}
</button>
<button onClick={() => setList([4, 5, 6])}>
内容不一样{JSON.stringify(list)}
</button>
</>
)
}

export default App

useCallback

作用

在组件多次重新渲染的时候缓存函数。

解决的问题

在下文的代码中,尽管Input组件被memo高阶组件包裹,用来防止不必要的重新渲染,但是memo的作用是基于比较props的变化来决定是否需要重新渲染。具体来说,memo会浅比较当前和上一次的props,如果它们是相等的(即没有变化),那么组件就不会被重新渲染;反之,如果props有变化,组件就会被重新渲染。

在下文的代码中,App组件中的changeHandler函数是在每次父组件渲染时创建的新函数。即使这个函数的逻辑没有改变,由于它是一个新的函数引用,所以传递给Input组件的onChange prop在每次父组件重新渲染时都会被视为不同的值。因此,即使Input组件使用了memo,它仍然会在父组件状态(如count)改变时被重新渲染,因为它的onChange prop被认为是变化了。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { memo, useState } from "react"

const Input = memo(function Input ({ onChange }) {
console.log('子组件重新渲染了')
return <input type="text" onChange={(e) => onChange(e.target.value)} />
})

function App () {
// 传给子组件的函数
const changeHandler = (value) => console.log(value)
// 触发父组件重新渲染的函数
const [count, setCount] = useState(0)
return (
<div className="App">
{/* 把函数作为prop传给子组件 */}
<Input onChange={changeHandler} />
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
)
}

export default App

useCallback缓存函数

useCallback缓存之后的函数可以在组件渲染时保持引用稳定,也就是返回同一个引用。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { memo, useCallback, useState } from "react"


const Input = memo(function Input ({ onChange }) {
console.log('子组件重新渲染了')
return <input type="text" onChange={(e) => onChange(e.target.value)} />
})

function App () {
// 传给子组件的函数
const changeHandler = useCallback((value) => console.log(value), [])
// 触发父组件重新渲染的函数
const [count, setCount] = useState(0)
return (
<div className="App">
{/* 把函数作为prop传给子组件 */}
<Input onChange={changeHandler} />
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
)
}

export default App

重点关注:const changeHandler = useCallback((value) => console.log(value), [])

forwardRef

作用

允许自定义组件使用ref将一个DOM节点暴露给父组件。

解决的问题

假设存在一个自定义组件Input,我们给该组件绑定ref

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { useRef } from 'react'

// 自定义组件
const Input = () => {
return <input />;
};

function App() {
const ref = useRef(null)

const focusHandle = () => {
console.log(ref)
}

return (
<div>
<Input ref={ref} />
<button onClick={focusHandle}>focus</button>
</div>
)
}

export default App

运行结果:

1
2
3
4
5
6
hook.js:608 Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?

Check the render method of `App`. Error Component Stack
at Input (<anonymous>)
at div (<anonymous>)
at App (App.js:9:1)

操作方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { forwardRef, useRef } from 'react'

// 自定义组件
const Input = forwardRef((props, ref) => {
return <input ref={ref}/>;
});

function App() {
const ref = useRef(null)

const focusHandle = () => {
ref.current.focus()
}

return (
<div>
<Input ref={ref} />
<button onClick={focusHandle}>focus</button>
</div>
)
}

export default App

useImperativeHandle

作用

不暴露子组件中的DOM元素,而暴露子组件内部的方法。

操作方法

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import { forwardRef, useImperativeHandle, useRef } from "react"

// 子组件

const Son = forwardRef((props, ref) => {
// 实现聚焦逻辑
const inputRef = useRef(null)
const focusHandler = () => {
inputRef.current.focus()
}

// 把聚焦方法暴露出去
useImperativeHandle(ref, () => {
return {
// 暴露的方法
focusHandler
}
})
return <input type="text" ref={inputRef} />
})


// 父组件
function App () {
const sonRef = useRef(null)
const focusHandler = () => {
console.log(sonRef.current)
sonRef.current.focusHandler()
}
return (
<>
<Son ref={sonRef} />
<button onClick={focusHandler}>focus</button>
</>
)
}

export default App

zustand

什么是zustand

zustand,状态管理工具,相比Redux更简洁。

快速开始

创建store,store/useCounterStore.js

1
2
3
4
5
6
7
8
9
10
11
12
import { create } from 'zustand'

const useStore = create((set) => {
return {
count: 0,
inc: () => {
set(state => ({ count: state.count + 1 }))
}
}
})

export default useStore

App.js

1
2
3
4
5
6
7
8
import useStore from './store/useCounterStore.js'

function App() {
const { count, inc } = useStore()
return <button onClick={inc}>{count}</button>
}

export default App

异步支持

优点

对于异步操作的支持不需要特殊的操作,直接在函数中编写异步逻辑,最后把接口的数据放到set函数中返回即可。

案例

store/channelStore.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { create } from 'zustand'

const URL = 'http://geek.itheima.net/v1_0/channels'

const useStore = create((set) => {
return {
count: 0,
ins: () => {
return set(state => ({ count: state.count + 1 }))
},
channelList: [],
fetchChannelList: async () => {
const res = await fetch(URL)
const jsonData = await res.json()
set({channelList: jsonData.data.channels})
}
}
})

export default useStore

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { useEffect } from 'react'
import useChannelStore from './store/channelStore'

function App() {
const { channelList, fetchChannelList } = useChannelStore()

useEffect(() => {
fetchChannelList()
}, [fetchChannelList])

return (
<ul>
{channelList.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}

export default App

切片模式

场景

当我们单个store比较大的时候,可以采用一种切片模式进行模块拆分再组合。

拆分并组合切片

store/useStore.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import { create } from 'zustand'

const URL = 'http://geek.itheima.net/v1_0/channels'


// 创建counter相关切片
const createCounterStore = (set) => {
return {
// 状态数据
count: 0,
// 修改状态数据的方法
inc: () => {
set((state) => ({ count: state.count + 1 }))
},
}
}

// 创建channel相关切片
const createChannelStore = (set) => {
return {
channelList: [],
fetchChannelList: async () => {
const res = await fetch(URL)
const jsonRes = await res.json()
console.log(jsonRes)
set({
channelList: jsonRes.data.channels
})
}
}
}

// 组合切片
const useStore = create((...a) => ({
...createCounterStore(...a),
...createChannelStore(...a)
}))

export default useStore

组件使用

App.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import useStore from "./store/useStore"
import { useEffect } from "react"

function App() {
const {count, inc, channelList, fetchChannelList } = useStore()
useEffect(() => {
fetchChannelList()
}, [fetchChannelList])
return (
<>
<button onClick={inc}>{count}</button>
<ul>
{channelList.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</>
)
}

export default App

Taro

什么是Taro

Taro,一个开放式跨端跨框架解决方案,支持使用"React/Vue/Nerv"等框架来开发"微信/京东/百度/支付宝/字节跳动/QQ/飞书"小程序和"H5/RN"等应用。

案例(微信小程序)

开始

初始化项目

安装指定版本的@tarojs/cli

1
npm install -g @tarojs/cli@3.6.35

使用Taro初始化项目:

  1. taro init myApp,然后会有一些选项,在本文,我们选择如下:
    • 请输入项目介绍
    • 请选择框架 React
    • 是否需要使用 TypeScript ? No
    • 请选择 CSS 预处理器(Sass/Less/Stylus) Sass
    • 请选择编译工具 Webpack5
    • 请选择包管理工具 npm
    • 请选择模板源 Github(最新)
    • 请选择模板 taro-ui(使用 taro-ui 的模板)
  2. cd myApp,进入新创建的项目根目录。
  3. npm install,安装依赖。

然后我们执行npm run build:weapp,构建微信小程序页面。

再使用微信开发者工具,导入项目。

导入项目

导入项目后,我们点击编译,即可看到效果。

编译

函数组件

点开src/app.js,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Component } from 'react'
import './app.scss'

class App extends Component {

componentDidMount () {}

componentDidShow () {}

componentDidHide () {}

// this.props.children 是将要会渲染的页面
render () {
return this.props.children
}
}

export default App

注意class App extends Component,这是类组件(Class Components)。实际上,React官方已经不推荐这种方式了。
我们将其修改为React官方推荐的"函数组件(Functional Components)。

示例代码:

1
2
3
4
5
6
7
8
import './app.scss';

const App = ({ children }) => {
// children 是将要渲染的页面
return <>{children}</>;
};

export default App
  • 去除了componentDidMount () {}componentDidShow () {}componentDidHide () {},没有实际业务逻辑的代码。

对于pages/index/index.jsx,同样,改成函数组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { View, Text } from '@tarojs/components';
import { AtButton } from 'taro-ui';

import "taro-ui/dist/style/components/button.scss"; // 按需引入
import './index.scss';

const Index = () => {
return (
<View className='index'>
<Text>Hello world!</Text>
<AtButton type='primary'>I need Taro UI</AtButton>
<Text>Taro UI 支持 Vue 了吗?</Text>
<AtButton type='primary' circle={true}>支持</AtButton>
<Text>共建?</Text>
<AtButton type='secondary' circle={true}>来</AtButton>
</View>
);
};

export default Index;

目录结构

  • assets:用于存放使用到的素材。
  • components:用于存放使用的通用子组件。
  • data:用于存放使用到的数据。
  • util:用于存放工具类代码。

首页

基本结构

app.js新增一行import 'taro-ui/dist/style/index.scss',引入全局样式。
如需分别引入样式文件,可以参考TaroUI的官网。https://taro-ui.jd.com

app.js

1
2
3
4
5
6
7
8
9
import './app.scss';
import 'taro-ui/dist/style/index.scss'

const App = ({ children }) => {
// children 是将要渲染的页面
return <>{children}</>;
};

export default App

pages/index/index.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { View, Image } from '@tarojs/components'
import { AtButton } from 'taro-ui'
import headerBg from '../../assets/headerBg.jpg'
import GlobalFooter from '../../components/GlobalFooter'

import './index.scss'

export default function Index() {
return (
<View className='indexPage'>
<View className='at-article__h1 title'>
MBTI心理测试
</View>
<View className='at-article__h3 subtitle'>这是一个心理测试,可以测出你的性格类型。</View>
<AtButton type='primary' circle >开始测试</AtButton>
<Image className='headerBg' src={headerBg} />
<GlobalFooter />
</View>
)
}

pages/index/index.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.indexPage{
background: #D0E9EE;
height: 80vh;

.title{
color:white;
padding-top:40px;
text-align:center;
}

.subtitle{
color:white;
margin-top:30px;
margin-bottom:48px;
text-align:center;
}

.headerBg{
width:100%;
}

}

对于底部的GlobalFooter,我们将其封装成一个独立的组件。
components/GlobalFooter/index.jsx

1
2
3
4
5
6
7
8
9
import { View } from '@tarojs/components'

import './index.scss'

export default function GlobalFooter() {
return (
<View className='at-article__h4 footPage'>KakaWanYifan</View>
)
}

components/GlobalFooter/index.scss

1
2
3
4
5
6
7
.footPage{
bottom: 15px;
position: fixed;
left:0;
right:0;
text-align: center;
}

交互逻辑

点击按钮后,进行调整。

关于跳转方法,可以参考Taro官方文档的路由跳转。
https://taro-docs.jd.com/docs/router#路由跳转

pages/index/index.jsx

1
2
3
4
5
6
7
8
import Taro from '@tarojs/taro'


<AtButton type='primary' className='enterBtn' circle onClick={()=>{
Taro.navigateTo({
url: '/pages/doQuestion/index'
})
}}>开始测试</AtButton>

问答页

基本结构

pages/doQuestion/index.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { View } from '@tarojs/components'
import { AtRadio, AtButton } from 'taro-ui'
import GlobalFooter from '../../components/GlobalFooter'

import './index.scss'

export default function Index() {
return (
<View className='questionPage'>

<View className='at-article__h1 title'>
题目
</View>

<AtRadio
options={[
{ label: '单选项一', value: 'A' },
{ label: '单选项二', value: 'B' }
]}
/>

<AtButton type='primary' className='enterBtn' circle >下一题</AtButton>
<AtButton size='normal' className='enterBtn' circle >上一题</AtButton>
<AtButton type='primary' className='enterBtn' circle >查看结果</AtButton>

<GlobalFooter />
</View>
)
}

pages/doQuestion/index.scss

1
2
3
4
5
6
7
8
9
10
11
12
.questionPage{
.title {
margin-bottom: 30px;
}

.enterBtn{
margin-bottom: 30px;
margin-top:20px;
width:65vw;
}

}

定义标题为"问答",pages/doQuestion/index.config.js

1
2
3
export default definePageConfig({
navigationBarTitleText: '问答'
})

app.config.js添加'pages/doQuestion/index'

1
2
3
4
5
6
7
8
9
10
11
12
export default defineAppConfig({
pages: [
'pages/index/index',
'pages/doQuestion/index',
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: 'black'
}
})

交互逻辑

关键点:

  • 当前的题目号、题目内容。
  • 按钮的出现逻辑:
    • 上一题按钮,在第一题的时候,不出现。
    • 查看结果按钮,只有当题目数为10时出现。
    • 题目数为10时,下一题按钮不出现。
  • 只有当选择后,下一题按钮才能有效,否则无效。
  • 单选框初初始不显示勾。
  • 将心理测试的问题导入并显示。
  • 答案存储。
  • 跳转到结果页。
  • 回到前一题时,应该显示之前的选项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import { View } from '@tarojs/components';
import { AtRadio, AtButton } from 'taro-ui';
import { useState, useEffect } from 'react';
import Taro from "@tarojs/taro";
import GlobalFooter from '../../components/GlobalFooter';
import questions from '../../data/questions.json';

import './index.scss';

export default function Index() {

const [current, setCurrent] = useState(1);
const [currentAnswer, setCurrentAnswer] = useState();

const [answerList] = useState([]);

const [currentQuestion, setCurrentQuestion] = useState(questions[0]);

const questionOptions = currentQuestion.options.map((option) => {
return {
label: `${option.key}. ${option.value}`,
value: option.key
};
});

useEffect(() => {
setCurrentAnswer(undefined);
}, [current]);

useEffect(() => {
setCurrentQuestion(questions[current - 1]);
}, [current]);

return (
<View className='questionPage'>
{/* {JSON.stringify(answerList)} */}
<View className='at-article__h1 title'>
{current}、{currentQuestion.title}
</View>

<AtRadio
options={questionOptions}
value={answerList[current - 1]}
onClick={(value) => {
setCurrentAnswer(value);
answerList[current - 1] = value;
}}
/>

{ current < questions.length && (
<AtButton type='primary' size='normal' className='enterBtn' disabled={!answerList[current - 1]} circle onClick={() => {
setCurrent(current + 1);
}}>下一题</AtButton>)
}

{ current > 1 && (
<AtButton size='normal' className='enterBtn' circle onClick={() => {
setCurrent(current - 1);
}}>上一题</AtButton>
)}

{ current === questions.length && (
<AtButton type='primary' size='normal' className='enterBtn' circle disabled={!answerList[current - 1]}
onClick={() => {
try {
Taro.setStorageSync('answerList', answerList);
} catch (e) {}

Taro.navigateTo({
url: '/pages/result/index'
});
}}
>查看结果</AtButton>
)}

<GlobalFooter />
</View>
);
}

结果页

pages/result/index.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import {Image, View} from '@tarojs/components'
import Taro from "@tarojs/taro";
import {AtButton} from "taro-ui";
import GlobalFooter from '../../components/GlobalFooter'

import './index.scss'
import headerBg from "../../assets/headerBg.jpg";
import {calculateHighestScore} from "../../util/bzUtil";
import questions from "../../data/questions.json";
import questionResults from "../../data/question_results.json";


export default function Index() {

const answerList = Taro.getStorageSync("answerList");

if (!answerList || answerList.length === 0) {
Taro.showToast({ title: "答案为空", icon: "error" , duration: 2000 });
}

const questionResult = calculateHighestScore(answerList, questions, questionResults);

return (
<View className='resultPage'>
{/*{JSON.stringify(answerList)}*/}
<View className='at-article__h1 title'>
{questionResult.resultName}
</View>
<View className='at-article__h3 subtitle'>{questionResult.resultDesc}</View>
<AtButton type='primary' className='enterBtn' circle onClick={()=>{
Taro.reLaunch({ url: '/pages/index/index' })
}}>返回首页</AtButton>

<Image className='headerBg' src={headerBg} />
<GlobalFooter />
</View>
)
}

pages/result/index.scss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.resultPage{
background: #D0E9EE;
height: 80vh;

.title{
color:white;
padding-top:40px;
text-align:center;
}

.subtitle{
color:white;
margin-top:30px;
margin-bottom:48px;
text-align:center;
}

.enterBtn{
width:65vw;
}

.headerBg{
width:100%;
}

}

pages/result/index.config.js

1
2
3
export default definePageConfig({
navigationBarTitleText: '结果'
})

src/util/bzUtil.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
const answerList = ["B","B","B","A"];
const questions = [
{
title: "你通常更喜欢",
options: [
{
result: "I",
value: "独自工作",
key: "A",
},
{
result: "E",
value: "与他人合作",
key: "B",
},
],
},
{
options: [
{
result: "S",
value: "喜欢有结构和常规",
key: "A",
},
{
result: "N",
value: "喜欢自由和灵活性",
key: "B",
},
],
title: "对于日常安排",
},
{
options: [
{
result: "P",
value: "首先考虑可能性",
key: "A",
},
{
result: "J",
value: "首先考虑后果",
key: "B",
},
],
title: "当遇到问题时",
},
{
options: [
{
result: "T",
value: "时间是一种宝贵的资源",
key: "A",
},
{
result: "F",
value: "时间是相对灵活的概念",
key: "B",
},
],
title: "你如何看待时间",
},
];
const questionResults = [
{
resultProp: ["I", "S", "T", "J"],
resultDesc: "忠诚可靠,被公认为务实,注重细节。",
resultPicture: "icon_url_istj",
resultName: "ISTJ(物流师)",
},
{
resultProp: ["I", "S", "F", "J"],
resultDesc: "善良贴心,以同情心和责任为特点。",
resultPicture: "icon_url_isfj",
resultName: "ISFJ(守护者)",
},
];

// 计算得分最高的题目评分结果
export function calculateHighestScore(answerList, questions, questionResults) {
// 初始化每个评分结果的分数
const scores = questionResults.map(result => ({
resultProp: result.resultProp,
resultDesc: result.resultDesc,
resultPicture: result.resultPicture,
resultName: result.resultName,
score: 0
}));

// 遍历用户答案和题目列表,计算分数
answerList.forEach((answer, index) => {
const question = questions[index];

const selectedOption = question.options.find(option => option.key === answer);
if (selectedOption) {
const result = selectedOption.result;
scores.forEach(score => {
if (score.resultProp.includes(result)) {
score.score++;
}
});
}
});

//默认给它一个值;
let highestScore=questionResults[0];
// 找到分数最高的评分结果
highestScore = scores.reduce((max, current) => (current.score > max.score ? current : max), { score: 0 });
return highestScore;
}

// 调用函数并输出结果
const highestScoreResult = calculateHighestScore(answerList, questions, questionResults);
console.log(highestScoreResult);

app.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
export default defineAppConfig({
pages: [
'pages/index/index',
'pages/doQuestion/index',
'pages/result/index',
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: 'WeChat',
navigationBarTextStyle: 'black'
}
})

完整版本myApp.zip提供下载链接

文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/12009
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板