1. 简介
- 什么是UI(头/尾) => Hooks解决方案 => API&使用建议 => 最佳实践demo => 闭坑指南
- 什么是UI
- UI = f(data) 函数f将数据(data)映射到用户界面(UI)
- 状态是数据吗?
- 状态(state)是什么
- 状态有一个隐含的意思,就是存在改变状态的行为(behavior)
- 例如:点赞数隐含了一个点赞的行为,而点赞这个行为只有在某种上下文(context)中才会存在
2. Hooks基本概念
- 如何描述UI(User Interface)
- UI = 数据 => 视图 => 消息(输入&操作) => 重算 => 数据 => 视图 ...
- 消息和重算可以合并成一种行为
- UI = 数据 => 视图 => 行为 => 数据 => 视图 ...
- 行为有同步行为和异步行为,包含重算的逻辑
- 数据不对应行为,把状态对应行为
- 数据拆分:不变的是属性,变化的是状态
- 以不变的属性传入视图,用行为去驱动状态
- 再简化
- 视图只需要感知状态,而不需要感知行为
- 再抽象出作用的概念
- 与状态变化有关的行为
- 与上下文存在的状态,会改变视图的行为,类似于跳转等,根据与当前视图无关的上下文状态有关的行为
- 行为本身就是作用的一种
- 最后抽象
-
与视图的关联(状态 & 作用 & 上下文)都希望做到松散的耦合,放在外面,从而做到复用
-
关联的关系称之为Hooks
-
重新定义UI界面
- state hook 状态的hook
- effect hook 作用的hook
- context hook 上下文的hook
- 定义:函数V(视图) = f(props, state)
- UI = V usehook1() usehook2() ... 视图使用了第一个hook,第二个hook ...
3. Hooks Api
3.1 基本用法
- 三个基础hook:状态、作用、上下文
3.1.1 状态
- 在某个上下文中(用户界面)数据和改变数据的行为
const [count, setCount] = useState(0)
// [状态,行为] = hooks API
// React hooks会把数据和行为绑定
- 基本使用
import React, { useState } from "react";
export default () => {
const [count, setCount] = useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
};
- 简单的抽离hooks
- 封装一个addCount的行为
- 改变count是依靠addCount的行为,把这个hook挂在视图上,和UI解耦,触发行为即可,根据状态渲染
import React, { useState } from "react";
function useCount(initialValue) {
const [count, setCount] = useState(initialValue);
return [
count,
() => {
setCount(count + 1);
}
];
}
export default () => {
const [count, addCount] = useCount(0);
return (
<div>
<p>{count}</p>
<button onClick={() => addCount()}>+1</button>
</div>
);
};
- 对比state以及setState方式
//相同类型的事务要有相同类型的行为,不能组合成一团处理,要分散解耦
state = { count1: 0, count2: 1}
setState({count1..., count2... })
- 状态和背后的行为封装在一起,才是一个独立的行为模块
import React, { useState } from "react";
function useCount(initialValue) {
const [count, setCount] = useState(initialValue);
return [
count,
() => {
setCount(count + 1);
}
];
}
export default () => {
const [count1, addCount1] = useCount(0);
const [count2, addCount2] = useCount(0);
return (
<div>
<p>{count1}</p>
<p>{count2}</p>
<button onClick={() => addCount1()}>+1</button>
<button onClick={() => addCount2()}>+1</button>
</div>
);
};
3.1.2 作用(Effect)
-
UI不仅仅是一个将数据映射到视图的函数
-
客观世界存在输入输出以外的东西
- 改变URL
- console.log()
- set cookie 的过程...
-
基本用法
import React, { useState, useEffect } from "react";
export default () => {
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`you clicked count ${count} times`);
});
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
};
// useEffect每次重新render都会创建内部新函数
- 此种好处是每次重新render不用创建新函数
- useEffect中依赖(deps)的用法
import React, { useEffect, useState } from "react";
function log(count) {
console.log(`you clicked count ${count} times`);
}
export default () => {
const [count, setCount] = useState(0);
// 读作:依赖[]中状态变化的作用
// 不写[], 只要状态有变化就触发
// 依赖发生变化就会执行一个新的效果
useEffect(log.bind(null, count), [count]);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
};
- 跳转demo
import React, { useEffect, useState } from "react";
export default () => {
const [count, setCount] = useState(0);
useEffect(()=>{
if(count > 5) {
window.location.href="https://www.baidu.com"
}
}, [count]);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
};
- 副作用的销毁以及传值的小坑(箭头函数,闭包)
- 不要直接set,用函数的形式set
import React, { useEffect, useState } from "react";
function useInterval(callback, time) {
// 只执行一次定时器,依赖[]
useEffect(() => {
const I = setInterval(callback, time);
return () => {
clearInterval(I);
};
}, []);
}
export default () => {
const [count, setCount] = useState(0);
useInterval(() => {
console.log(count);
setCount((count) => count + 1);
}, 1000);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
};
3.2 拟人方法理解Context
- UI产生过程中,能够从context中获取信息(知识)
- UI更像一个人而不是机械的结构
UI => (data) => {
const { userType } = useContext(userTypeContext)
switch(userType) {
...不同的渲染逻辑
}
}
- 点击切换按钮颜色
import React, { useContext, useState } from "react";
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
// 创建context
const ThemeContext = React.createContext({
theme: themes.light,
toggle: () => {}
});
// 传递Context,React.createContext创建
// value中重写Context值
// set中函数传递,不要直接setTheme
// value 中传递知识,下面包含的组件都要理解
export default () => {
const [theme, setTheme] = useState(themes.light);
return (
<ThemeContext.Provider
value={{
theme,
toggle: () => {
setTheme((theme) => {
setTheme(theme === themes.light ? themes.dark : themes.light);
});
}
}}
>
<Toolbar />
</ThemeContext.Provider>
);
};
const Toolbar = () => {
return <ThemedButton />;
};
// 传递理解Context用useContext使用
const ThemedButton = () => {
const context = useContext(ThemeContext);
return (
<button
style={{
fontSize: "32px",
color: context.theme.foreground,
background: context.theme.background
}}
onClick={() => {
context.toggle();
}}
>
Click Me!
</button>
);
};
3.3 Reducer的作用和使用场景
- reducer 减压器,是一个设计模式,也是一个计算过程的抽象
- reducer是一个函数,把很多意图(action)映射成一个状态(state),state驱动视图渲染,dispatch是发送一个action的方法
- dispatch是派发,派发一个action
- useState没有抽象成一套
// 派发 action,改变state
import React, { useReducer } from "react";
const initialState = { count: 0 };
function reducer(state, action) {
console.log(state, action);
switch (action.type) {
case "add":
return { count: state.count + 1 };
case "sub":
return { count: state.count - 1 };
default:
throw new Error();
}
}
export default function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
{state.count}
<button
onClick={() => {
dispatch({ type: "add" });
}}
>
+
</button>
<button
onClick={() => {
dispatch({ type: "sub" });
}}
>
-
</button>
</div>
);
}
3.4 ref引用
-
引用行为 reference,引用一个对象
-
引用React管理以外的对象
- 需要在React之外做一些事情,例如,input的focus,要拿到输入框的实例,媒体操作等,例如canvas,video,React不管理这些东西,让用户用引用自己管理
- 引用通常搭配作用useEffect使用
-
附带作用:方便的保存值
-
点击focus,输入框聚焦
import React, { useRef } from "react";
export default function App() {
const refInput = useRef();
return (
<div>
<input ref={refInput} />
<button
onClick={() => {
refInput.current.focus();
//refInput.current 拿到当前值
}}
>
Foucs
</button>
</div>
);
}
- 暂存上一次的值
import React, { useRef, useState } from "react";
//每次重新渲染都会创建一个新的App
// 全局定义一个值,污染每次的App const prev = null;
export default function App() {
const [count, setCount] = useState(0);
const prev = useRef(null);
return (
<div>
<p>当前值:{count}</p>
<p>之前值:{prev.current}</p>
<button
onClick={() => {
prev.current = count;
setCount((x) => x + 1);
}}
>
Click Me to add
</button>
<button
onClick={() => {
prev.current = count;
setCount((x) => x - 1);
}}
>
Click Me to remove
</button>
</div>
);
}
3.5 缓存设计
-
为什么要缓存
- V视图 = f(state, props) useHooks 渲染视图并用了hooks
- 想在f中 new Object(),只能创建一次
- 一些复杂计算只有在状态改变后才做
-
缓存一个函数 useCallback
-
缓存一个值 useMemo
-
缓存值
import React, { useMemo, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const memorizedText = useMemo(() => {
console.log("run useMemo function");
return `this is a memorized text ${Date.now()}`;
}, [Math.floor(count / 10)]);
// 逢10 缓存刷新
// [count % 10 === 0] 10和11时候true false变换了两次
return (
<div>
{memorizedText}
<p>U clicked {count} times</p>
<button
onClick={() => {
setCount((x) => x + 1);
}}
>
Click Me
</button>
</div>
);
}
- 每次渲染都会创建新的add函数
import React, { useState } from "react";
const s = new Set();
export default function App() {
const [count, setCount] = useState(0);
function add() {
setCount((x) => x + 1);
}
s.add(add);
console.log(s.size); // 每次都在创建add函数,点击一次长度加一
return (
<div>
<p>U clicked {count} times</p>
<button onClick={add}>Click Me</button>
</div>
);
}
- 缓存函数,除非影响性能,否则意义不大,阅读成本
import React, { useCallback, useState } from "react";
const s = new Set();
export default function App() {
const [count, setCount] = useState(0);
const add = useCallback(() => {
setCount((x) => x + 1);
}, []);
s.add(add);
console.log(s.size); // 只创建一次
return (
<div>
<p>U clicked {count} times</p>
<button onClick={add}>Click Me</button>
</div>
);
}
3.6 避坑指南 & 使用建议
3.6.1. 使用memo减少重绘次数
6.6.2. hooks同步问题
import React, { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
console.log(count); // 一直输出0 显示count在变化
setCount((x) => x + 1);
}, 1000);
}, []);
return (
<div>
<p> {count} times</p>
</div>
);
}
import React, { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
function myEffect() {
const I = setInterval(() => {
console.log(count); // 更改依赖 count有变化
setCount((x) => x + 1);
}, 1000);
return () => clearInterval(I);
}
// 效果的依赖是空,效果只执行使用一次
// myEffect只有第一次生效,每次重新渲染,都会产生一个新的myEffect, 但是只有第一次的生效
// 第一个count是0,这个count在第一个函数的闭包中被创建,后面每次都是拿第一个count 0
// 要想更新console的count,必须更新依赖,但是每次都会创建新的setInterval,必须回收
// 如果每次依赖count重新创建myEffect产生新的效果,销毁旧的效果,setInterval就变成了setTimeout
useEffect(myEffect, [count]);
return (
<div>
<p> {count} times</p>
</div>
);
}
3.6.3. 可以构造自己的hooks封装行为
- 模拟获取列表
import React, { useEffect, useState } from "react";
function sleep(time) {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, time);
});
}
async function getPerson() {
await sleep(200);
return ["a", "b", "c"];
}
function usePerson() {
const [list, setList] = useState(null);
async function request() {
const list = await getPerson();
setList(list);
}
useEffect(request, []);
return list;
}
export default function App() {
const list = usePerson();
if (list === null) {
return <div>loading</div>;
}
return (
<div>
{list.map((name, i) => {
return <li key={i}>{name}</li>;
})}
</div>
);
}
3.6.4. 每种行为一个hook
- 每一个hook应该是一种行为
- 网络请求是一个行为
- 耦合示例,这应该是不同行为操作的
import React, { useEffect, useState } from "react";
export default function App() {
const [state, setState] = useState({
count: 0,
company: "Apple"
});
return (
<div>
{state.count}
{state.company}
<button
onClick={() => {
setState((prev) => {
return {
company: prev.company,
count: prev.count + 1
};
});
}}
>
+
</button>
</div>
);
}
3.6.5. 不要考虑生命周期
4. 一个拖拽列表
- 左边视图(data = state + props),右边行为(状态 + 事件)
import React, { useEffect, useState } from "react";
const list = [
{
src:
"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fwww.xkzzz.com%3A8080%2Fzb_users%2Fupload%2F2017%2F12%2F20171212134240_55395.png&refer=http%3A%2F%2Fwww.xkzzz.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1627636569&t=6c8a0114e80f2a528f9331b1cca8ab18",
title: "React"
},
{
title: "Angular",
src:
"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.mukewang.com%2F5801a11f000140fa03120325.png&refer=http%3A%2F%2Fimg.mukewang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1627636608&t=22b5288cbac4af4128f73299491e5232"
},
{
title: "Vue",
src:
"https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg-blog.csdnimg.cn%2F20190114135012858.png&refer=http%3A%2F%2Fimg-blog.csdnimg.cn&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1627636640&t=a7316f589673a0a93eea2b688ca40540"
}
];
export default function App() {
return (
<div className="App">
<Card {...list[0]} />
</div>
);
}
function DraggableList() {}
function Draggable() {}
// 占空间的部分
function Bar() {}
function Card({ title, src }) {
return (
<div className="card">
<img src={src} />
<span className="text">{title}</span>
</div>
);
}