React Hooks
在了解
React Hooks
之前, 我们先看一下Class
和函数式
的一般写法
Class组件 一般写法
import React from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";
class App extends React.Component {
// 默认参数
static defaultProps = {
message: "Hello World",
firstName: 'Yang',
lastName: 'yu'
};
// 传参检查
static propTypes = {
message: PropTypes.string
};
/**
* 方便调试
*/
static displayName = "App";
// 状态
state = {
count: 1
};
/**
* 解决绑定 this 的几种方法
* 1. onClick={this.increment.bind(this)}
* 2. 在 constructor中绑定 tihs ==> this.increment = this.increment.bind(this)
* 3 箭头函数: 浪费内存
*/
increment = () => {
this.setState({
count: 2
});
};
// 计算属性
get name() {
return this.props.firstName + this.props.lastName
}
// 生命周期
componentDidMount() {}
render() {
return (
<div>
<div>{this.props.message}</div>
<div>{this.state.count}</div>
<button onClick={this.increment}>点击</button>
<div>{this.name}</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
函数组件一般写法
import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
interface Props {
message?: string;
}
const App: React.FunctionComponent<Props> = props => {
/**
* 如何使用 state, React版本 > 16.8
* useState 返回一个数组, 解构
*/
const [count, setCount] = useState(0);
const [count1, setCount1] = useState(0);
const increment = () => {
setCount(count + 1);
};
const increment1 = () => {
setCount1(count1 + 1);
};
/**
* 生命周期 代替 componetDidMount componentDidUpdate
*/
useEffect(() => {
console.log('componentDidMount() 或者 componentDidUpdate()')
})
// 第二个参数是 []
useEffect(() => {
console.log('只在第一次执行')
// axios.get()
}, [])
// 当某个 参数更新, 才触发
useEffect(() => {
console.log('count 更新了之后执行, 点击 count1 不会更新')
}, [count])
useEffect(() => {
return () => {
console.log('我死了')
}
})
return (
<div>
<div>{props.message}</div>
<div>{count}</div>
<div>{count1}</div>
<button onClick={increment}>按钮</button>
<button onClick={increment1}>按钮1</button>
</div>
);
};
App.defaultProps = {
message: 'Hello World'
}
App.displayName = 'yym'
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
下面我们来看看一些
Hooks API
useState
-
使用
const [n, setN] = React.useState(0) const [user, setUser] = React.useState({name: 'yym'})
-
不可局部更新
-
因为
useState
不会帮我们合并属性setUser({ ...user, name: 'yym1' })
-
-
地址会变
-
setState(obj)
如果obj
地址不变, 那么React
认为数据没有变化
-
-
接收函数
const [user, setUser] = useState(() => ({name: 'yym2', age: 12})) // 减少多余计算过程 setN(i => i + 1) setN(i => i + 1) // 对 state 多次操作,
简单实现 useState
下面代码可以放到
codesandbox
里面跑一下
看一个简单的例子
const App = () => {
const [n, setN] = useState(0);
return (
<div>
<div>{n}</div>
<button onClick={() => setN(n + 1)}>incrementN</button>
</div>
);
};
ReactDOM.render(<App />, document.querySelector("#root"))
1. 进入页面, render App, 调用 App() , n = 0
2. 用户点击 button, 调用 setN(n + 1), 再次 render App, 得到虚拟 DOM, DOM diff 更新真 DOM
3. 每次调用 App, 都会运行 useState(0), n 的值都会改变
QA:
1. 执行 setN 的时候会发生什么? n 会变吗? App() 会重新执行吗?
重新渲染; setN要把n改变, n 不变; App 会重新执行;
2. 如果 App() 会重新执行, 那么 useState(0) 的时候, n 每次值会不同
n 的值会不同
上面的例子我们可以分析:
-
setN
-
setN
一定会修改数据X
, 将n + 1
存入X
,(X 泛指)
-
setN
一定会触发<App />
, 重新渲染
-
-
useState
-
useState
会从X
读取 n 的最新值
-
-
X
- 每个组件有自己的数据
X
, 我们将其命名为state
- 每个组件有自己的数据
根据上面的结论, 我们尝试写一下 useState
import React from "react";
import ReactDOM from "react-dom";
let _state: any;
const myUseState = (initialValue: any) => {
_state = _state === undefined ? initialValue : _state;
const setState = (newState: any) => {
_state = newState;
// 渲染页面
render();
};
return [_state, setState];
};
// 不管这个实现
const render = () => ReactDOM.render(<App />, document.querySelector("#root"));
const App = () => {
console.log("App 渲染了");
// 使用自己写的 myUseState
const [n, setN] = myUseState(0);
return (
<div>
<div>{n}</div>
<button onClick={() => setN(n + 1)}>incrementN</button>
</div>
);
};
export default App;
上面的代码完全可以正常运行, 但当我们的 myUseState
有两个的时候, 就是下面的代码
import React from "react";
import ReactDOM from "react-dom";
let _state: any;
const myUseState = (initialValue: any) => {
_state = _state === undefined ? initialValue : _state;
const setState = (newState: any) => {
_state = newState;
// 渲染页面
render();
};
return [_state, setState];
};
// 不管这个实现
const render = () => ReactDOM.render(<App />, document.querySelector("#root" ));
const App = () => {
console.log("App 渲染了");
const [n, setN] = myUseState(0);
const [m, setM] = myUseState(1);
return (
<div>
<div>
{n}-{m}
</div>
<button onClick={() => setN(n + 1)}>incrementN</button>
<button onClick={() => setM(m + 1)}>incrementM</button>
</div>
);
};
export default App;
我们运行上面的代码, 发现更新 n
, m
也会改变, 更新m
, n
也会改变, 因为所有的数据都放在 _state
会冲突, 该怎么解决?
-
把
_state
做成对象_state = { m: 0, n: 0}, // 感觉不行, useState(0) 并不知道变量叫 m 还是 n,
-
把
_state
做成数组, 尝试一下_state = [0, 0]
// 把 _state 初始为数组 let _state: any[] = []; // 下标 let index = 0; const myUseState = (initialValue: any) => { // 设置唯一的值, 根据 myUseState 的顺序 const currentIndex = index; _state[currentIndex] = _state[currentIndex] === undefined ? initialValue : _state[currentIndex]; const setState = (newState: any) => { _state[currentIndex] = newState; // 渲染页面 render(); }; index += 1; return [_state[currentIndex], setState]; }; const render = () => { // 每次重新渲染 index 重置 index = 0; ReactDOM.render(<App />, document.querySelector("#root")); };
-
上面数组的方法会有一定的缺点
-
useState
的调用顺序- 若第一次顺序是 n m K
- 第二次必须保证顺序一致,
-
useState
不能写在if
中
const [n, setN] = React.useState(0); let m, setM; if (n % 2 === 1) { [m, setM] = React.useState(0); }
// 会报错 顺序不能变 React Hook "React.useState" is called conditionally. React Hooks must be called in the exact same order in every component render. (react-hooks/rules-of-hooks)eslint
-
代码在这里, 我们想一下 mpUseState
还有什么问题呢?
-
App
组件用了_state
和index
, 那其它组件用什么?- 给每个组件创建一个
_state
和index
- 给每个组件创建一个
- 放在全局作用域重名了怎么办?
- 放在组件对应的虚拟节点对象上
总结 :
每个函数组件对应一个
React
节点,每个节点保存着
_state
和idnex
useState
会读取state[index]
index
由useState
出现顺序决定setState
会修改state
, 并触发更新
上面的属于简化
useState
的实现, 我们看一个useState
出现问题的 代码
// 先点击 log , 再点击 incremwntN, 3s 后打印的是 n: 0, 而不是 n: 1
// 先点击 incrementN, 再点击 log, 3s 后 打印 n: 1
// ? 为啥是旧数据
1. setN 不会改变 n, 生成一个 n 的分身, 改变的不是n, 所以 n: 0,
import React, { useState } from "react";
const App = () => {
const [n, setN] = useState(0);
const log = () =>
setTimeout(() => {
console.log(`n: ${n}`);
}, 3000);
return (
<div>
<div>{n}</div>
<button onClick={() => setN(n + 1)}>incrementN</button>
<button onClick={log}>log</button>
</div>
);
};
export default App;
如何让状态始终只是一个?
-
全局变量
- 用
window.xxx
即可
- 用
-
用
useRef
const App = () => { const nRef = React.useRef(0); // { current: 0} const log = () => setTimeout(() => { console.log(`n: ${nRef.current}`); }, 3000); return ( <div> <div>{nRef.current} 不是实时刷新</div> <button onClick={() => (nRef.current += 1)}>incrementN</button> <button onClick={log}>log</button> </div> ); };
// 手动触发 App 更新, 强制, 更类似于 Vue3 const App = () => { const nRef = React.useRef(0); // { current: 0} const log = () => setTimeout(() => { console.log(`n: ${nRef.current}`); }, 3000); return ( <div> <div>{nRef.current}</div> <button onClick={() => (nRef.current += 1)}>incrementN</button> <button onClick={log}>log</button> </div> ); };
-
useContext
-
useContext
不仅能贯穿始终, 还能贯穿不同组件// 上下文 const themeContext = createContext(null); const App = () => { const [theme, setTheme] = useState("blue"); return ( // Provider 类似于作用域, 里面的可以使用 theme, settheme <themeContext.Provider value={{ theme, setTheme }}> <div style={{ backgroundColor: theme === "red" ? "red" : "blue" }}> <p>{theme}</p> <div> <ChildA /> </div> <div> <ChildB /> </div> </div> </themeContext.Provider> ); }; const ChildA = () => { const { setTheme } = useContext(themeContext); return ( <div> <button onClick={() => setTheme("red")}>red</button> </div> ); }; const ChildB = () => { const { setTheme } = useContext(themeContext); return ( <div> <button onClick={() => setTheme("blue")}>blue</button> </div> ); };
-
useReducer
使用方法:
-
useReducer
是复杂点的useState
import React, { useReducer } from "react";
// 1. 创建初始值
const initial = {
n: 0
};
// 2. 创建所有操作 reducer(state, action)
const reducer = (state: any, action: any) => {
if (action.type === "add") {
return { n: state.n + action.number };
} else if (action.type === "multiple") {
return { n: state.n * 2 };
} else {
throw new Error("unknown word");
}
};
const App = () => {
// 3. 传给 useReducer 得到读和写 API
const [state, dispatch] = useReducer(reducer, initial);
const onClick = () => {
// 4. 写
dispatch({ type: "add", number: 1 });
};
const onClick1 = () => {
dispatch({ type: "add", number: 2 });
};
return (
<div>
{/* 4. 读取值 */}
<h1>n: {state.n}</h1>
<button onClick={onClick}>add+1</button>
<button onClick={onClick1}>add+2</button>
</div>
);
};
export default App;
但如何代替 Redux
我们先弄一个初始的页面, 在里面一步步实现 Redux
功能, 示例Demo
// 这是我们初始页面, 有三个组件 User Books Movies
import React, { useReducer } from "react";
const App = () => {
return (
<div>
<User />
<hr />
<Books />
<Movies />
</div>
);
};
const User = () => {
return (
<div>
<h1>个人信息</h1>
</div>
);
};
export default App;
-
先把数据集中到
store
中// 代码和初始一样, 只是往里面加代码 import React, { useReducer } from "react"; const store = { user: null, books: null, movies: null, }
-
创建
reducer
, 使用usereducer
一样的const reducer = (state, action) => { switch (action.type) { case "setUser": return { ...state, user: action.user }; case "setBooks": return { ...state, books: action.books }; case "setMovies": return { ...state, movies: action.movies }; default: throw new Error(); } };
-
我们使用
createContext
, 创建一个上下文// 创建一个 context const Context = createContext(null);
-
使用
useReducer
并把useReducer
的读写 API, 放进Context Value
中const App = () => { // 使用 useReducer const [state, dispatch] = useReducer(reducer, store); return ( // 把 useReducer 的读写 API 放到 Context value里 <Context.Provider value={{ state, dispatch }}> <User /> <hr /> <Books /> <Movies /> </Context.Provider> ); };
-
经过前四步, 我们就可以在在
User, Books, Movie
的组件中使用Context.Provide
的value
const User = () => { // 使用 useContext const { state, dispatch } = useContext(Context); // 异步请求值 useEffect(() => { axios.get("/user").then((user: any) => { dispatch({ type: "setUser", user }); }); }, []); return ( <div> <h1>个人信息</h1> <div>name: {state.user ? state.user.name : ""}</div> </div> ); };
-
所以可以带到一个简单的 Redux 使用,
// 所有代码 import React, { useReducer, createContext, useContext, useEffect } from "react"; // store const store = { user: null, books: null, movies: null }; // reducer const reducer = (state, action) => { switch (action.type) { case "setUser": return { ...state, user: action.user }; case "setBooks": return { ...state, books: action.books }; case "setMovies": return { ...state, movies: action.movies }; default: throw new Error(); } }; // 创建一个 context const Context = createContext(null); const App = () => { // 使用 useReducer const [state, dispatch] = useReducer(reducer, store); return ( // 把 useReducer 的读写 API 放到 Context value里 <Context.Provider value={{ state, dispatch }}> <User /> </Context.Provider> ); }; // user 可以使用 store 的值 const User = () => { const { state, dispatch } = useContext(Context); useEffect(() => { axios.get("/user").then((user: any) => { dispatch({ type: "setUser", user }); }); }, []); return ( <div> <h1>个人信息</h1> <div>name: {state.user ? state.user.name : ""}</div> </div> ); }; export default App;
当一个项目比较大, 用到的组件比较多, 使用到的 reducer
比较多, 我们可以模块化
把
user, books, movies
单独建一个文件-
把对应的东西
export (default)
出去// 例: 重构 reducer // reducer const reducer = (state, action) => { switch (action.type) { case "setUser": return { ...state, user: action.user }; case "setBooks": return { ...state, books: action.books }; case "setMovies": return { ...state, movies: action.movies }; default: throw new Error(); } }; // ==> const obj = { 'setUser': (state, action) => { return { ...state, user: action.user }; }, // ... } const reducer_copy = (state, action) => { const fn = obj[action.type] if(fn) { return fn(state, action) } else { throw new Error('type 错误') } }
// user_reducer.js export default { 'setUser': (state, action) => { return { ...state, user: action.user }; }, } // books_reducer.js // ... // idnex.js import userReducer from './reducers/user_reducer' const obj = { ...userReducer, ...otherReducer }
useContext
useContext
改变一个数的时候, 是通过自顶向下, 逐级更新数据做到的
上下文
-
全局变量
是全局的上下文
-
上下文
是局部的全局变量
在上面的 useReducer
中我们也是用了 useContext
,
import React, { createContext, useState, useContext } from "react";
// 1. 创建一个 context 上下文
const Context = createContext(null);
function App() {
console.log("App 执行了");
const [n, setN] = useState(0);
return (
// 2. 使用 Context.Provide 弄一个 作用域 value 值
<Context.Provider value={{ n, setN }}>
<div className="App">
<Parent />
</div>
</Context.Provider>
);
}
function Parent() {
// 3. 作用域内使用 useContext
const { n, setN } = useContext(Context);
return (
<div>
我是爸爸 n: {n} <Child />
</div>
);
}
function Child() {
const { n, setN } = useContext(Context);
const onClick = () => {
setN(i => i + 1);
};
return (
<div>
我是儿子 我得到的 n: {n}
<button onClick={onClick}>+1</button>
</div>
);
}
useEffect & useLayoutEffect
useEffect
- 副作用: 对环境的改变就是副作用
- 其实也可以当做
afterRender: 每次render后运行
-
用途
- 作为
componentDidMount
使用,[]
作为第二个参数 - 作为
componentDidUpdate
使用, 可制定依赖 - 作为
componentWillUnMount
使用, 通过return
- 以上三种可以同时存在
- 作为
-
特点
- 如果同时存在多个
useEffect
, 会按照出现次序执行
- 如果同时存在多个
-
Demo
import React, { useState, useEffect } from "react"; const App = () => { const [n, setN] = useState(0); const [m, setM] = useState(0); const onClick = () => { setN(n + 1); }; const onClcik1 = () => { setM(m + 1); }; // componentDidMount useEffect(() => { console.log("第一次渲染, n或m变化我也不打印了"); document.title = "Hello"; }, []); // componentDidUpdate useEffect(() => { console.log("第一二..N次渲染, 任何state变化我就渲染"); }); // componentDidUpdate useEffect(() => { console.log("第一次渲染, 并且只有m变化我再渲染"); }, [m]); // componentDidMount & componentWillUnMount useEffect(() => { const timerId: any = setInterval(() => { console.log("定时器"); }, 1000); return () => { window.clearInterval(timerId); }; }); return ( <div> n: {n} <button onClick={onClick}>+n</button> <hr /> m: {m} <button onClick={onClcik1}>+m</button> </div> ); }; export default App;
useLayoutEffect
- 其函数签名与
useEffect
相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect
内部的更新计划将被同步刷新。 -
useEffect
在浏览器渲染完成后执行 -
useLayout
在浏览器渲染前执行
需要截图 老是的白板
- 特点
-
useLayoutEffect
总是比useEffect
先执行 (上面说useEffect
按次序执行, 如果有useLayoutEffect
, 先执行) -
useLayoutEffect
里的任务最好影响了Layout
- 尽可能使用标准的
useEffect
以避免阻塞视觉更新 - 优先使用
useEffect
-
useMemo & useCallback
-
在了解
React.useMemo
之前, 我们先来了解一下React.memo
, 直接看代码- 我们平常写
React
, 经常会有多余的render
-
React.memo
帮助我们控制何时重新渲染组件, 可以和函数式组件一起使用
// 这是一个普通的父子组件, 子组件使用了父组件的 state const App = () => { const [n, setN] = useState(0); const [m, setM] = useState(0); const onClick = () => { setN(n + 1) } return ( <div> <button onClick={onClick}>update: {n}</button> <Child data={m} /> </div> }; const Child = (props: any) => { console.log('child 执行了'); // TODO: 大量代码 return <div>child: {props.data}</div> }
- 上面的代码我每次更新
n
, 在控制台看到会更新Child
组件, 但是我们的Child
组件, 只依赖m
, 但m
没有变化, 我们不希望Child
执行 -
props
不变, 没必要执行函数组件 - 但我们怎么优化呢?
React.memo
// 上面的代码改变一下 // 使用 React.memo 包裹一下, 只有当我们改变 m 的时候, Child 才会执行 const Child = React.memo((props: any) => { console.log("child 执行了"); // TODO: 大量代码 return <div>child: {props.data}</div>; });
-
React.memo
使得一个组件只有在它的props
变化在执行一遍, 再次执行
- 我们平常写
-
但是
React.memo
有一个问题: 添加监听函数, 还是会执行Child
, 看代码const App = () => { const [n, setN] = useState(0); // 每次执行 m = 0, 值是一样的 const [m, setM] = useState(0); const onClick = () => { // n 变化不会执行 Child setN(n + 1); }; // 每次App 执行, 都会重新执行 // 之前是一个空函数, 现在是另一个空函数, 不是同一个空函数, 引用类型, 地址不同 const onClickChild = () => {}; return ( <div> <button onClick={onClick}>update: {n}</button> {/* 传一个函数 */} <Child data={m} onClick={onClickChild} /> </div> ); }; const Child = React.memo((props: any) => { console.log("child 执行了"); // TODO: 大量代码 // 在这里添加了 onClick事件 return <div onClick={props.onClick}>child: {props.data}</div>; });
- 怎么解决呢?
react.useMemo
- 怎么解决呢?
-
React.useMemo
-
如何使用?
// render 时: 先根据[name]里面的 name判断, 因为 useMemo 作为一个有着暂存能力, 暂存了上一次的结果 // 对比上一次 name, name值不变, data就不重新赋值成新的对象, 没有新的对象, 没有新的内存地址, 就不会 // 重新渲染 const data = useMemo(()=>{ return {} },[m, n]) // 1. 第一个参数 () => value // 2. 第二个参数依赖 [m, n] // 3. 当依赖变化, 重新计算新的 value // 4. 以来不变, 使用之前的 value //函数 const data = useMemo(() => { return () => {} })
-
上面使用
React.memo
改造成React.useMemo
const App = () => { const [n, setN] = useState(0); const [m, setM] = useState(0); const onClick = () => { setN(n + 1); }; // useMemo const onClickChild = useMemo(() => { return () => {}; }, [m]); return ( <div> <button onClick={onClick}>update: {n}</button> {/* dlkfs */} <Child data={m} onClick={onClickChild} /> </div> ); }; const Child = React.memo((props: any) => { console.log("child 执行了"); // TODO: 大量代码 return <div onClick={props.onClick}>child: {props.data}</div>; });
-
-
有点难用, 于是有了
useCallback
, 作用和useMemo
一样, 是useMemo
的语法糖-
用法
useCallback( () => { callback }, [input],) // 等价于 useMemo(() => () => { callback }, [input]) const onClickChild = useMemo(() => { return () => {}; }, [m]); const onClickChild = useCallback(() => {}, [m]);
-
useRef
-
使用
目的: 1. 如果需要一个值, 在组建不断 render 时保持不变 使用: 1. 初始化 => const count = useRef(0) 2. 读取: count.current
const App = () => { const [n, setN] = useState(0); // 初始化一个值 const count = useRef(0); const onClick = () => { setN(n + 1); }; useEffect(() => { // 通过 .current 来读取, 更新一次 +1 count.current += 1; console.log(count.current); }); return ( <div> <button onClick={onClick}>update: {n}</button> </div> ); };
-
为什么需要
current
?- 为了保证两次
useRef
是同一个值 (只有引用能做到)
- 为了保证两次
-
做几个
Hook
对比-
useState/useReducer
每次都变 -
useMemo/useCallback
依赖变化才变 -
useRef
永远不变
-
-
对比
Vue3 的 ref
// vue <template> <div @click="increment"> ref会更新: {count} </div> </template> <script> import { ref } from 'vue' export default { setup() { // 直接使用 ref const count = ref(0) const increment = () => { count.value++ } return { count, increment} } } </script>
// react 的 ref 不会自动更新ui const App = () => { const [n, setN] = useState(0); const count = useRef(0); const onClick = () => { setN(n + 1); }; const onClick2 = () => { count.current += 1; // 会改变值 +1 console.log(count.current, "count..."); }; return ( <div> <button onClick={onClick}>update: {n}</button> {/* 页面没有更新 */} <button onClick={onClick2}>update count: {count.current}</button> </div> ); }; // 在 实现 useState 中我们有如何手动刷新页面的方法, 使用 useState
forwardRef & useImperativeHandle
-
简单看个例子
const App = () => { const buttonRef = useRef(null); return ( <div> {/* 我们想要获取子组件的 DOM 引用 */} <ButtonComponent ref={buttonRef}>按钮</ButtonComponent> </div> ); }; const ButtonComponent = (props: any) => { console.log(props, 'props....') // {children: "按钮"} 没有得到 ref return <button className="red" {...props} />; }; // warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()? // 上面的报错, 函数组件不支持 ref, 可以使用 React.forwardRef()
-
使用
forwardRef
// 函数组件希望接收别的传来的 ref, 需要 forwardRef 包起来 // forwardRef 只是多加了一个 ref 参数 // 改造上面的 const App = () => { const buttonRef = useRef(null); return ( <div> <Button2 ref={buttonRef}>按钮</Button2> </div> ); }; // forwardRed 包裹 const Button2 = forwardRef((props, ref) => { return <button className="red" ref={ref} {...props} />; });
-
总结:
useRef: 1. 可以用来引用 DOM 对象 2. 也可以用来引用普通对象 forwardRef 1. 函数式组件 由于 props 不包含 ref, 所以需要 forwardRef
-
和
forwardRef
相关的useImperativeHandle
, 减少暴露给父组件的属性// 官方例子: function FancyInput(props, ref) { const inputRef = useRef(); // 可以自定义暴露出去的实例值 // 其实可以叫做 setRef useImperativeHandle(ref, () => ({ focus: () => { inputRef.current.focus(); } })); return <input ref={inputRef} ... />; } FancyInput = forwardRef(FancyInput);
自定义 Hook
- 自定义 Hook 是一个函数,其名称以 “
use
” 开头,函数内部可以调用其他的 Hook