本身在掘金上看了也看了不少关于hooks的文章,感受都讲得不是很详细。并且也有不少的水文。最近本身打算重学react,系统性的再把hooks给学习一遍。html
类组件的缺点:(来自官网动机)react
在组件之间复用状态逻辑很难es6
复杂组件变得难以理解编程
难以理解的 classapi
你必须去理解 JavaScript 中 this
的工做方式,这与其余语言存在巨大差别。还不能忘记绑定事件处理器。没有稳定的语法提案,代码很是冗余。数组
hooks的出现,解决了上面的问题。另外,还有一些其余的优势:缓存
1. 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook安全
确保老是在你的 React 函数的最顶层调用他们。遵照这条规则,你就能确保 Hook 在每一次渲染中都按照一样的顺序被调用。这让 React 可以在屡次的 useState
和 useEffect
调用之间保持 hook 状态的正确。性能优化
2. 只在 React 函数中调用 Hookbabel
不要在普通的 JavaScript 函数中调用 Hook,你能够:
至于为何会有这些规则,若是你感兴趣,请参考Hook 规则
const [state, setState] = useState(initialState)
import React,{useState} from "react";
function Example() {
const [count, setCount] = useState(0);
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div>
);
}
export default Example;
复制代码
useState
Hook。它让咱们在函数组件中存储内部 state。Example
组件内部,咱们经过调用 useState
Hook 声明了一个新的 state 变量。它返回一对值给到咱们命名的变量上。咱们把变量命名为 count
,由于它存储的是点击次数。咱们经过传 0
做为 useState
惟一的参数来将其初始化为 0
。第二个返回的值自己就是一个函数。它让咱们能够更新 count
的值,因此咱们叫它 setCount
。setCount
。React 会从新渲染 Example
组件,并把最新的 count
传给它。// 声明多个 state 变量
const [age, setAge] = useState(42);
const [fruit, setFruit] = useState('banana');
const [todos, setTodos] = useState([{ text: '学习 Hook' }]);
复制代码
你没必要使用多个 state 变量。State 变量能够很好地存储对象和数组,所以,你仍然能够将相关数据分为一组。
import React,{useState} from "react";
function Example() {
const [count, setCount] = useState(0);
const [person, setPerson] = useState({name:'jimmy',age:22});
return (
<div> <p>name {person.name} </p> // 若是新的 state 须要经过使用先前的 state 计算得出,那么能够将回调函数当作参数传递给 setState。 // 该回调函数将接收先前的 state,并返回一个更新后的值。 <button onClick={() => setCount(count=>count+1)}>Click me</button> <button onClick={() => setPerson({name:'chimmy'})}>Click me</button> </div>
);
}
export default Example;
复制代码
setPerson更新person时,不像 class 中的 this.setState
,更新 state 变量老是替换它而不是合并它。上例中的person为{name:'chimmy'} 而不是{name:'chimmy',age:22}
Effect Hook 可让你在函数组件中执行反作用(数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于反作用)操做
useEffect(fn, array)
useEffect在初次完成渲染以后都会执行一次, 配合第二个参数能够模拟类的一些生命周期。
若是你熟悉 React class 的生命周期函数,你能够把 useEffect
Hook 看作 componentDidMount``componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
若是第二个参数为空数组,useEffect至关于类组件里面componentDidMount。
import React, { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("我只会在组件初次挂载完成后执行");
}, []);
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div>
);
}
export default Example;
复制代码
页面渲染完成后,会执行一次useEffect。打印“我只会在组件初次挂载完成后执行”,当点击按钮改变了state,页面从新渲染后,useEffect不会执行。
若是不传第二个参数,useEffect 会在初次渲染和每次更新时,都会执行。
import React, { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log("我会在初次组件挂载完成后以及从新渲染时执行");
});
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click me</button> </div>
);
}
export default Example;
复制代码
初次渲染时,会执行一次useEffect,打印出“我会在初次组件挂载完成后以及从新渲染时执行”。 当点击按钮时,改变了state,页面从新渲染,useEffect都会执行,打印出“我会在初次组件挂载完成后以及从新渲染时执行”。
effect 返回一个函数,React 将会在执行清除操做时调用它。
useEffect(() => {
console.log("订阅一些事件");
return () => {
console.log("执行清除操做")
}
},[]);
复制代码
注意:这里不仅是组件销毁时才会打印“执行清除操做”,每次从新渲染时也都会执行。至于缘由,我以为官网解释的很清楚,请参考 解释: 为何每次更新的时候都要运行 Effect
import React, { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
const [number, setNumber] = useState(1);
useEffect(() => {
console.log("我只会在cout变化时执行");
}, [count]);
return (
<div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}>Click cout</button> <button onClick={() => setNumber(number + 1)}>Click number</button> </div>
);
}
export default Example;
复制代码
上面的例子,在点击 click cout按钮时,才会打印“我只会在cout变化时执行”。 由于useEffect 的第二个参数的数组里面的依赖是cout,因此,只有cout发生改变时,useEffect 才会执行。若是数组中有多个元素,即便只有一个元素发生变化,React 也会执行 effect。
使用 Hook 其中一个目的就是要解决 class 中生命周期函数常常包含不相关的逻辑,但又把相关逻辑分离到了几个不一样方法中的问题。
import React, { useState, useEffect } from "react";
function Example() {
useEffect(() => {
// 逻辑一
});
useEffect(() => {
// 逻辑二
});
useEffect(() => {
// 逻辑三
});
return (
<div> useEffect的使用 </div>
);
}
export default Example;
复制代码
Hook 容许咱们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每个 effect。
useEffect是不能直接用 async await 语法糖的
/* 错误用法 ,effect不支持直接 async await*/
useEffect(async ()=>{
/* 请求数据 */
const res = await getData()
},[])
复制代码
useEffect
的回调参数返回的是一个清除反作用的 clean-up
函数。所以没法返回 Promise
,更没法使用 async/await
那咱们应该如何让useEffect
支持async/await
呢?
const App = () => {
useEffect(() => {
(async function getDatas() {
await getData();
})();
}, []);
return <div></div>;
};
复制代码
useEffect(() => {
const getDatas = async () => {
const data = await getData();
setData(data);
};
getDatas();
}, []);
复制代码
经过使用这个 Hook,你能够告诉 React 组件须要在渲染后
执行某些操做。React 会保存你传递的函数(咱们将它称之为 “effect”),而且在执行 DOM 更新以后调用它。
useEffect
?将 useEffect
放在组件内部让咱们能够在 effect 中直接访问 count
state 变量(或其余 props)。咱们不须要特殊的 API 来读取它 —— 它已经保存在函数做用域中。Hook 使用了 JavaScript 的闭包机制,而不用在 JavaScript 已经提供了解决方案的状况下,还引入特定的 React API。
const value = useContext(MyContext);
接收一个 context 对象(React.createContext
的返回值)并返回该 context 的当前值。当组件上层最近的 <MyContext.Provider>
更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext
provider 的 context value
值。即便祖先使用 React.memo
或 shouldComponentUpdate
,也会在组件自己使用 useContext
时从新渲染。
别忘记 useContext
的参数必须是 context 对象自己:
useContext(MyContext)
useContext(MyContext.Consumer)
useContext(MyContext.Provider)
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
// 建立两个context
export const UserContext = React.createContext();
export const TokenContext = React.createContext();
ReactDOM.render(
<UserContext.Provider value={{ id: 1, name: "chimmy", age: "20" }}> <TokenContext.Provider value="我是token"> <App /> </TokenContext.Provider> </UserContext.Provider>,
document.getElementById("root")
);
复制代码
app.js
import React, { useContext } from "react";
import { UserContext, TokenContext } from "./index";
function Example() {
let user = useContext(UserContext);
let token = useContext(TokenContext);
console.log("UserContext", user);
console.log("TokenContext", token);
return (
<div> name:{user?.name},age:{user?.age} </div>
);
}
export default Example;
复制代码
打印的值以下
若是你在接触 Hook 前已经对 context API 比较熟悉,那应该能够理解,useContext(MyContext)
至关于 class 组件中的 static contextType = MyContext
或者 <MyContext.Consumer>
。
useContext(MyContext)
只是让你可以读取 context 的值以及订阅 context 的变化。你仍然须要在上层组件树中使用 <MyContext.Provider>
来为下层组件提供 context。
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState
的替代方案。它接收一个形如 (state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的 dispatch
方法。(若是你熟悉 Redux 的话,就已经知道它如何工做了。)
在某些场景下,useReducer
会比 useState
更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于以前的 state 等。而且,使用 useReducer
还能给那些会触发深更新的组件作性能优化,由于你能够向子组件传递 dispatch
而不是回调函数
React 会确保
dispatch
函数的标识是稳定的,而且不会在组件从新渲染时改变。这就是为何能够安全地从useEffect
或useCallback
的依赖列表中省略dispatch
。
import React, { useReducer } from "react";
export default function Home() {
function reducer(state, action) {
switch (action.type) {
case "increment":
return { ...state, counter: state.counter + 1 };
case "decrement":
return { ...state, counter: state.counter - 1 };
default:
return state;
}
}
const [state, dispatch] = useReducer(reducer, { counter: 0 });
return (
<div> <h2>Home当前计数: {state.counter}</h2> <button onClick={(e) => dispatch({ type: "increment" })}>+1</button> <button onClick={(e) => dispatch({ type: "decrement" })}>-1</button> </div>
);
}
复制代码
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
复制代码
返回一个 [memoized]回调函数。
把内联回调函数及依赖项数组做为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给通过优化的并使用引用相等性去避免非必要渲染(例如 shouldComponentUpdate
)的子组件时,它将很是有用。
import React, { useState } from "react";
// 子组件
function Childs(props) {
console.log("子组件渲染了");
return (
<> <button onClick={props.onClick}>改标题</button> <h1>{props.name}</h1> </>
);
}
const Child = React.memo(Childs);
function App() {
const [title, setTitle] = useState("这是一个 title");
const [subtitle, setSubtitle] = useState("我是一个副标题");
const callback = () => {
setTitle("标题改变了");
};
return (
<div className="App"> <h1>{title}</h1> <h2>{subtitle}</h2> <button onClick={() => setSubtitle("副标题改变了")}>改副标题</button> <Child onClick={callback} name="桃桃" /> </div>
);
}
复制代码
执行结果以下图
当我点击改副标题这个 button 以后,副标题会变为「副标题改变了」,而且控制台会再次打印出子组件渲染了
,这就证实了子组件从新渲染了,可是子组件没有任何变化,那么此次 Child 组件的从新渲染就是多余的,那么如何避免掉这个多余的渲染呢?
咱们在解决问题的以前,首先要知道这个问题是什么缘由致使的?
我们来分析,一个组件从新从新渲染,通常三种状况:
接下来用排除法查出是什么缘由致使的:
第一种很明显就排除了,当点击改副标题 的时候并无去改变 Child 组件的状态;
第二种状况,咱们这个时候用 React.memo
来解决了这个问题,因此这种状况也排除。
那么就是第三种状况了,当父组件从新渲染的时候,传递给子组件的 props 发生了改变,再看传递给 Child 组件的就两个属性,一个是 name
,一个是 onClick
,name
是传递的常量,不会变,变的就是 onClick
了,为何传递给 onClick 的 callback 函数会发生改变呢?其实在函数式组件里每次从新渲染,函数组件都会重头开始从新执行,那么这两次建立的 callback 函数确定发生了改变,因此致使了子组件从新渲染。
const callback = () => {
doSomething(a, b);
}
const memoizedCallback = useCallback(callback, [a, b])
复制代码
把函数以及依赖项做为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,这个 memoizedCallback 只有在依赖项有变化的时候才会更新。
那么只需这样将传给Child组件callback函数的改造一下就OK了
const callback = () => { setTitle("标题改变了"); };
// 经过 useCallback 进行记忆 callback,并将记忆的 callback 传递给 Child
<Child onClick={useCallback(callback, [])} name="桃桃" />
复制代码
这样咱们就能够看到只会在首次渲染的时候打印出子组件渲染了,当点击改副标题和改标题的时候是不会打印子组件渲染了的。
const cacheSomething = useMemo(create,deps)
create
:第一个参数为一个函数,函数的返回值做为缓存值。deps
: 第二个参数为一个数组,存放当前 useMemo 的依赖项,在函数组件下一次执行的时候,会对比 deps 依赖项里面的状态,是否有改变,若是有改变从新执行 create ,获得新的缓存值。cacheSomething
:返回值,执行 create 的返回值。若是 deps 中有依赖项改变,返回的从新执行 create 产生的值,不然取上一次缓存值。useMemo 会记录上一次执行 create 的返回值,并把它绑定在函数组件对应的 fiber 对象上,只要组件不销毁,缓存值就一直存在,可是 deps 中若是有一项改变,就会从新执行 create ,返回值做为新的值记录到 fiber 对象上。
function Child(){
console.log("子组件渲染了")
return <div>Child</div>
}
const Child = memo(Child)
function APP(){
const [count, setCount] = useState(0);
const userInfo = {
age: count,
name: 'jimmy'
}
return <Child userInfo={userInfo}> } 复制代码
当函数组件从新render时,userInfo每次都将是一个新的对象,不管 count
发生改变没,都会致使 Child组件的从新渲染。
而下面的则会在 count
改变后才会返回新的对象。
function Child(){
console.log("子组件渲染了")
return <div>Child</div>
}
function APP(){
const [count, setCount] = useState(0);
const userInfo = useMemo(() => {
return {
name: "jimmy",
age: count
};
}, [count]);
return <Child userInfo={userInfo}> } 复制代码
实际上 useMemo 的做用不止于此,根据官方文档内介绍:以把一些昂贵的计算逻辑放到 useMemo 中,只有当依赖值发生改变的时候才去更新。
import React, {useState, useMemo} from 'react';
// 计算和的函数,开销较大
function calcNumber(count) {
console.log("calcNumber从新计算");
let total = 0;
for (let i = 1; i <= count; i++) {
total += i;
}
return total;
}
export default function MemoHookDemo01() {
const [count, setCount] = useState(100000);
const [show, setShow] = useState(true);
const total = useMemo(() => {
return calcNumber(count);
}, [count]);
return (
<div> <h2>计算数字的和: {total}</h2> <button onClick={e => setCount(count + 1)}>+1</button> <button onClick={e => setShow(!show)}>show切换</button> </div>
)
}
复制代码
当咱们去点击 show切换按钮时,calcNumber这个计算和的函数并不会出现渲染了.只有count 发生改变时,才会出现计算.
简单理解呢 useCallback 与 useMemo 一个缓存的是函数,一个缓存的是函数的返回的结果。useCallback 是来优化子组件的,防止子组件的重复渲染。useMemo 能够优化当前组件也能够优化子组件,优化当前组件主要是经过 memoize 来将一些复杂的计算逻辑进行缓存。固然若是只是进行一些简单的计算也不必使用 useMemo。
咱们能够将 useMemo 的返回值定义为返回一个函数这样就能够变通的实现了 useCallback。useCallback(fn, deps)
至关于 useMemo(() => fn, deps)
。
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref 对象,其 .current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变
useRef,它有一个参数能够做为缓存数据的初始值,返回值能够被dom元素ref标记,能够获取被标记的元素节点.
import React, { useRef } from "react";
function Example() {
const divRef = useRef();
function changeDOM() {
// 获取整个div
console.log("整个div", divRef.current);
// 获取div的class
console.log("div的class", divRef.current.className);
// 获取div自定义属性
console.log("div自定义属性", divRef.current.getAttribute("data-clj"));
}
return (
<div> <div className="div-class" data-clj="我是div的自定义属性" ref={divRef}> 我是div </div> <button onClick={(e) => changeDOM()}>获取DOM</button> </div>
);
}
export default Example;
复制代码
useRef还有一个很重要的做用就是缓存数据,咱们知道usestate ,useReducer 是能够保存当前的数据源的,可是若是它们更新数据源的函数执行一定会带来整个组件重新执行到渲染,若是在函数组件内部声明变量,则下一次更新也会重置,若是咱们想要悄悄的保存数据,而又不想触发函数的更新,那么useRef是一个很棒的选择。
下面举一个,每次换成state 上一次值的例子
import React, { useRef, useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
const numRef = useRef(count);
useEffect(() => {
numRef.current = count;
}, [count]);
return (
<div> <h2>count上一次的值: {numRef.current}</h2> <h2>count这一次的值: {count}</h2> <button onClick={(e) => setCount(count + 10)}>+10</button> </div>
);
}
export default Example;
复制代码
当 ref 对象内容发生变化时,useRef
并不会通知你。变动 .current
属性不会引起组件从新渲染。因此,上面的例子中虽然numRef.current的值,已经改变了,可是页面上仍是显示的上一次的值,从新更新时,才会显示上一次更新的值。
若是文章中有什么错误,欢迎指出。