页面级的 hooks 数据流
虽然我们假定当你阅读本文档的时候,你已经了解过 react hooks 相关的知识了,但是本次的文档希望面临的受众更广一些,所以依旧会很详细的讲解几个常用的 hooks。如果你无法很轻易的掌握本文档的内容,那你可以通过搜索阅读其他的文档来强化这些概念。此处不给出推荐链接,推荐进行内部的初级开发认证。
本文档假定你是按照我们课程设计的顺序进行阅读的,即表示到达这里,你已经熟练掌握了 dva 的相关概念。因此我会直接使用 dva 的概念来类比介绍 react hooks 的概念。但事实上 react hooks 的概念是要比 dva 来的更容易掌握的。这么做的目的也是为了让你再次熟悉 dva。
为什么都不是最佳实践了,我还要一直提 dva
useState
我们经常在项目中看到如下的代码:
import { useState } from 'react'; const [state, setstate] = useState(initialState) 复制代码
这里我们以 dva 的概念类比理解:
initialState 与 init state
// initialState state: { name: 'learn umi', }, // 如果我们想像上面这样定义和初始化变量,我们可以使用如下 hooks const [name, setName] = useState('learn umi'); 复制代码
name 与 select state
在我们定义好变量之后,在后续的任何时候取值,都能取到最新的数据。
// hooks const [name, setName] = useState('learn umi'); const newName = name + 1; // dva 中 const { name } = yield select(_=>_.global); const newName = name + 1; 复制代码
setName 与 reducers save
// hooks const [name, setName] = useState('learn umi'); setName('Umi 入门教程') // dva 中 yield put({ type: 'save', payload: { name: 'umi 入门教程' }, }); 复制代码
注意:调用 setName 是要注意深浅拷贝的问题,你可以简单的记忆,当你 set 的数据是一个数组或者对象时,记得使用解构(...)返回一个新的对象。
async/await 与 effects
const delay = () => new Promise(resolve => { setTimeout(resolve, 1000); }) // hooks const [list, setList] = useState(['step1', 'step2', 'step3', 'step4']); const deleteItem = async item => { await delay() list.splice(list.findIndex(e => e === item), 1) setList([...list]) } // dva 中 { state: { name: 'learn umi', list: ['step1','step2','step3','step4'] }, effects: { *deleteItem({ payload }, { call, put,select }) { const { list } = yield select(_=>_.global); yield call(delay) list.splice(list.findIndex(e => e === payload), 1) yield put({ type: 'save', payload: { list }, }); }, }, } 复制代码
整理
新建文件 src/pages/useState.tsx
import React, { useState } from "react"; import { Link } from "umi"; export default function List() { const [list, setList] = useState(["step1", "step2", "step3", "step4"]); const [name, setName] = useState("learn umi"); const delay = () => new Promise((resolve) => { setTimeout(resolve, 1000); }); const deleteItem = async (item: any) => { await delay(); list.splice( list.findIndex((e) => e === item), 1 ); setList([...list]); }; return ( <div> <h1>{name}</h1> <button onClick={() => { setName("Umi 入门教程"); }} > Click Me! </button> <Link to="/">Go to index page</Link> <ul> {list.map((i) => ( <li key={i}> <button onClick={() => { deleteItem(i); }} > 删除{i}{" "} </button> </li> ))} </ul> </div> ); } 复制代码
访问 http://localhost:8888/useState,因为我们之前给 render
加了 3 秒延时渲染,所以你将需要等待 3 秒才能看到页面,点击 Click Me!
页面上的文字,从 learn umi
变为 Umi 入门教程
。点击下方的按钮,被点击的按钮,将会在延迟1秒之后,从页面中被移除。
到这里,我们就将 setState 的概念讲解完毕了,你很容易发现,在 react hooks 中就一行代码的逻辑,在 dva 中却需要跨越两个文件写比较多的模版代码。这也是我们现在不推荐在项目中重度使用 dva 的主要原因。
由于 react hooks 无法在 组件之外使用,因此我们依旧需要保留 dva 用作一些全局的数据管理和在一些组件之外操作数据的场景。
useEffect
在 dva 中也有一个同名的概念 effects,之所以没有拿它和 useEffect 类比,是因为他们不太像是一个东西,放在一起反而容易混乱。比起 effects,useEffect 更像 dva 中的 subscription。你会发现在前面的概念和实战中,我们都没有提到这个概念。因为在实际的项目中,我们发现使用 useEffect 会比 subscription 逻辑更加清晰。如果你对 subscription 感兴趣,你可以查看 dva 的文档了解更多。
useEffect 接收一个包含命令式、且可能有副作用代码的函数。
useEffect(didUpdate); 复制代码
当 useEffect 只接收一个函数时,表示函数在每一次页面渲染完成之后调用。
在项目中我们常用的方法是当达成某一个条件之后,再执行某个函数这样的逻辑。因此我们在第二参数传入一个数组,数组里面是我们期望这些值变化的时候,触发函数调用。
比如,当 name 值发生变化时调用:
import { useState, useEffect } from 'react'; const [name, setName] = useState('learn umi'); useEffect(() => { console.log('name value change!'); }, [name]) 复制代码
注意:name 值发生变化的时机包括 name 的初始化数据。即此时的 useEffect 的函数至少会被调用一次。
当你希望页面初始化的时候,调用 effect 时,你可以在第二参数传入一个空数组([])。这就告诉 React 你的 effect 不依赖于 props 或 state 中的任何值,所以它永远都不需要重复执行。
import { useState, useEffect } from 'react'; useEffect(() => { console.log('page init'); }, []) 复制代码
当你需要在组件中绑定监听或者定时器时,你也可以在此时机中执行。但是请一定要记得在组件卸载时需要清除 effect 创建的诸如订阅或计时器 ID 等资源。要实现这一点,只需要在 effect 函数中,返回清除函数即可。
比如,清除计时器 ID:
import { useState, useEffect } from 'react'; useEffect(() => { console.log('page init'); const timekey = setInterval(() => { console.log('每秒调用一次'); }, 1000); return () => { // 清除 clearInterval(timekey); }; }, []) 复制代码
比如,清除订阅:
import { useState, useEffect } from 'react'; useEffect(() => { const subscription = props.source.subscribe(); return () => { // 清除订阅 subscription.unsubscribe(); }; }); 复制代码
开发技巧
一般情况下 name 都是能取到最新的值的,但如果你是在一个比较复杂的环境中使用它,并且你无法保证它一定是最新值的时候,在使用 setName 的时候,可以使用函数式的设置方式,如 setName(name=>name+1) 该用法会先接收最新的值,可以让他脱离外层的引用,这在 useEffect 回调中频繁修改数值的时候,会非常好用。
比如,我们每秒修改一次数值:
import { useState, useEffect } from 'react'; const [count, setCount] = useState(0); useEffect(() => { console.log('page init'); const timekey = setInterval(() => { console.log('每秒调用一次'); setCount(count=>count+1); }, 1000); return () => { // 清除 clearInterval(timekey); }; }, []) 复制代码
整理
新建文件 src/pages/useEffect.tsx
import React, { useState, useEffect } from "react"; export default function List() { const [name, setName] = useState("learn umi"); const [effect, setEffect] = useState("no"); const [count, setCount] = useState(0); useEffect(() => { setEffect(name === "umi 入门教程" ? "yes" : "no"); }, [name]); useEffect(() => { console.log("page init"); const timekey = setInterval(() => { console.log("每秒调用一次"); setCount((count) => count + 1); }, 1000); return () => { // 清除 clearInterval(timekey); }; }, []); return ( <div> <h1>count:{count}</h1> <h1>{name}</h1> <h1>name change:{effect}</h1> <button onClick={() => { setName("umi 入门教程"); }} > Click Me! </button> </div> ); } 复制代码
访问 http://localhost:8888/useEffect,你将在页面上看到一个自动累加的计数器,点击 Click Me!
页面上的文字,从 learn umi
变为 Umi 入门教程
。name change:
也从 no
变成 yes
。
总结
在开发中我们最常用的 react 官方 hooks 就是 useState 和 useEffect。对于官方的其他 hooks,对我们的开发也有很大的帮助,当你熟练掌握 useState 和 useEffect 之后,你应该去官网全面的学习全部的 react hooks。
到目前位置,我们详细的讲解了我们在项目中会使用到的 dva 数据流和页面级的 hooks 数据流。熟练的掌握这些概念,会够大幅度的提升你的开发体验。因为不管再复杂的页面,都可以拆成这样一传一传的数据流。可以说掌握这些概念,就已经可以让你很好的编写前端逻辑了。