这是我参与「第五届青训营 」伴学笔记创作活动的第 12 天
React 常用 Hooks笔记
前言
官方明确推荐函数式组件 react.docschina.org/docs/hooks-…
Hook 这个单词的意思是"钩子"。 React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。 你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用 use 前缀命名,便于识别。你要使用 xxx 功能,钩子就命名为 useXxx。hook是v16.8.0后,才新增的特性
生命周期在class组件中非常重要。但是太多的太多的生命周期难记,有些也不知道具体的场景麻烦。还有就是this指向的问题比如我们要写大量的bind函数来改变this的指向,当然也可以通过装饰器等其他方法改变,但是本质上来说还是要跟this打交道。
Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据) ,而并非强制按照生命周期划分。你还可以使用 reducer 来管理组件的内部状态,使其更加可预测。
Hook 使你在无需修改组件结构的情况下复用状态逻辑。 这使得在组件间或社区内共享 Hook 变得更便捷。
内置钩子:
- useState【维护状态】
- useEffect【完成副作用操作】
- useContext【使用共享状态】
- useReducer【类似redux】
- useCallback【缓存函数】
- useMemo【缓存值】
- useRef【访问DOM】
- useImperativeHandle【使用子组件暴露的值/方法】
- useLayoutEffect【完成副作用操作,会阻塞浏览器绘制】
useState
概念
返回一个 state,以及更新 state 的函数。
在初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。
setState 函数用于更新 state。它接收一个新的 state 值并将组件的依次重新渲染加入队列。
可以简单理解为,如果要改变数据联动视图就要使用useState
注意:如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
简单使用
import { useState } from "react";
export default function UseState() {
// 这两个参数我们可以自定义名字,用的是数组解构的方法
const [num, setNum] = useState(1);
function add() {
setNum(num + 1, () => console.log(num));
}
return (
<div>
<h2>Counter:{num}</h2>
<button onClick={() => add()}>++</button>
</div>
);
}
进阶写法
初始值比较比较复杂,则可以直接传入一个函数进行计算,该函数只会在初始化的时候被调用一次,后续在使用setState更新的时候,不会在改变值,也就是说useState是惰性的,可以对比一个函数中变量的使用。
import { useState } from "react";
export default function UseState() {
const initNum = (i) => {
return i + 1;
};
const [num, setNum] = useState(() => {
console.log("只会在初始化的时候触发了");
const initialState = initNum(1);
return initialState;
});
const add = () => {
// 改变数据不会再触发initNum
setNum(num + 1);
};
return (
<div>
<button onClick={() => add()}>+1</button>
<div>你好,react hook{num}</div>
</div>
);
}
useEffect
useEffect 可以让你在函数组件中执行副作用操作,接收两个参数,第一个参数是要执行的函数 callback,第二个参数是可选的依赖项数组 dependencies。其中依赖项是可选的,如果不指定,那么 callback 就会在每次函数组件执行完后都执行;如果指定了,那么只有依赖项中的值发生变化的时候,它才会执行。 简单来说就是当我们的依赖项发生发生变化的时候,可以异步的执行里面的回调。
注意:
useEffect是在render之后执行
import { useEffect, useState } from "react";
export default function UseEffect() {
/**
* 第一个参数是回调函数
* 第二个参数是依赖项
* 每次num变化时都会变化
*
* 注意初始化的时候,也会调用一次
*/
const [num, setNum] = useState(1);
useEffect(() => {
console.log("每次num,改变我才会触发");
return () => {
/**
* 这是卸载的回调可以执行某些操作
* 如果不想执行,则可以什么都不写
*/
console.log("卸载当前监听");
};
}, [num]);
useEffect(() => {
console.log("每次render页面我就会触发");
return () => {
console.log("卸载当前监听");
};
});
return (
<div>
<button onClick={() => setNum(num + 1)}>+1</button>
<div>你好,react hook{num}</div>
</div>
);
}
Immutable Data 不可变数据
概念
Immutable 意为「不可变的」。在编程领域,Immutable Data 是指一种一旦创建就不能更改的数据结构。它的理念是:在赋值时,产生一个与原对象完全一样的新对象,指向不同的内存地址,互不影响
作用
避免副作用
当我们需要对一个对象进行修改时,直接在原对象上进行变更很方便,也很节省内存。但是在 js 中,对象都是引用类型,在按引用传递数据的场景中,会存在多个变量指向同一个内存地址的情况,这样会引发不可控的副作用。
板子
import produce from "immer";
const newGoods = produce(goods, (draft) => {
draft[id].select = !draft[id].select;
});
setGoods(newGoods);
const a = { x: 1 };
const b = a;
b.x = 6;
a.x // 6
import React, { useState } from "react";
export default function App() {
const [list, setList] = useState([1, 2, 3]);
const addMutable = () => {
list.push("新数据");
setList(list); // 并不能添加 并未改变引用类型数据的地址
};
const addImmutable = () => {
setList([...list, "新数据"]); // 可以添加
};
return (
<div className="App">
<button onClick={addMutable}>已可变的方式添加</button>
<button onClick={addImmutable}>已不可变的方式添加</button>
{list.map((item, index) => (<li key={index}>{item}</li>))}
</div>
);
}
import produce from "immer";
const [goods, setGoods] = useState([
{ name: "hxs", price: 12, num: 1 },
{ name: "hsa", price: 29, num: 2 },
{ name: "ass", price: 32, num: 3 },
{ name: "ads", price: 12, num: 3 },
]);
const [selectAll, setSelectAll] = useState(false);
const update = (id, flag) => {
if (goods[id].num || flag) {
const newGoods = produce(goods, (draft) => { // 不可变数据更新视图才会重新渲染
draft[id].num += flag;
});
setGoods(newGoods);
}
};
路由的一些hooks www.yuque.com/zhuba_ahhh/…
react hooks组件传值
父子组件之间的传值
react hook 父子组件之间通过props进行传值
父传子
父组件:在子组件标签上定义属性
子组件:函数组件接收一个props是一个对象,父组件传的属性名就是props对象的key,属性的值就是对应的value
const Child = (props) => {
// 父组件穿过来一个name
return (
<div>
<div>{props.name}</div>
</div>
)
}
const Parent = () => {
// 组件标签上传递属性
return (
<Child name='张三'></Child>
)
}
子传父
概括:在props上定义一个方法,调用方法的时候传入参数,达到传值的效果
父组件:在子组件标签上定义一个属性,属性值为一个方法
import { useState } from "react";
const Child = (props) => {
const toParent = () => {
// 调用props上面的getChildData方法
props.getChildData && props.getChildData("传给父组件");
};
return (
<div>
<button onClick={toParent}>往父组件传值</button>
</div>
);
};
const Parent = () => {
// 点击子组件 就会触发这个函数
const getChild = (msg) => {
console.log(msg)
}
// 组件标签上传递属性,属性的值是一个函数
return <Child getChildData={getChild}></Child>;
};
React.memon
概念
高阶组件是参数为组件,返回值为新组件的函数。
React.memo 为高阶组件。 如果你的组件在相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。可以做渲染劫持 自定义逻辑做劫持。
import React, { useState, useEffect, useContext } from 'react';
// 如果num不改变当前组件不会重新渲染
const MyComponent = React.memo((props) => {
/* 使用 props 渲染 */
return (
<div>{props.num}</div>
)
})
export default function hook() {
const [num, setNum] = useState(1)
return (
<div>
<button onClick={() => setNum(num + 1)}>+1</button>
<MyComponent num={num}></MyComponent>
</div>
)
}
特点
React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState,useReducer 或 useContext 的 Hook,当 state 或 context 发生变化时,它仍会重新渲染。
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现,第二个参数是一个函数,返回true不渲染,false渲染,可以做渲染劫持 自定义逻辑做劫持
import React, { useState, useEffect, useContext } from 'react';
const MyComponent = React.memo((props) => {
/* 使用 props 渲染 */
return (
<div>{props.num}</div>)
/**
* prevProps 上次的值
* nextProps 最新的值
*
* 如果传来的值是偶数的话则不更新组件
* 可以做渲染劫持
* 自定义逻辑做劫持
*/
}, (prevProps, nextProps) => {
console.log(nextProps, nextProps.num % 2)
return nextProps.num % 2 === 0
})
export default function hook() {
const [num, setNum] = useState(1)
useEffect(() => {
/**
* 当它是一个空数组时,回调只会被触发一次,类似于 componentDidMount
*/
console.log("componentDidmount")
}, [])
return (
<div>
<button onClick={() => setNum(num + 1)}>+1</button>
<MyComponent num={num}></MyComponent>
</div>
)
}
劫持渲染
import { memo } from "react";
export default memo(
({ lists }) => {
return lists.map((list, index) => (
<div key={index}>
{list.name}---{index + 1}
</div>
));
},
// 劫持渲染 两个参数分别是前后的props
(pre, cur) => {
console.log(cur);
return pre.lists.length >= 4 && pre.lists.length <= 8;
}
);
useCallback
接收两个参数,第一个参数是个函数,第二个是依赖项。返回一个 memoized函数,依赖项不变的情况下,memoizedCallback的引用不变。即:useCallback会被缓存,从而达到渲染性能优化的目的。
使用场景:
- 函数定义时需要进行大量运算, 这种场景极少
- 需要比较引用的场景,如上文提到的useEffect,又或者是配合React.Memo使用:
import React, { useState, useCallback } from "react";
// react.memo会做一层浅比较
/**
* 因为我们每次触发render 都会重新执行一遍当前函数
* 所以说,我们的方法会重新赋值,react.memo时进行浅比较
* 重新赋值的方法和之前的方法,引用不一样,所以react.memo
* 会认为是一个新的对象,所以会重新渲染
*/
const ChildComponent = React.memo((props) => {
console.log("每次render都会触发吗?", props);
return (
<div>
<div>你好我是子组件</div>
</div>
);
});
export default function LearnUseCallBack() {
const [num, setNum] = useState(1);
const [count, setCount] = useState(1);
/**
* useCallback 第一个参数是一个函数
* 第二个参数是依赖项
* 依赖项不变的情况下,函数的引用不变
* 依赖项传空数组,那么函数会一直不变
* 如果什么都不穿,那么会失效
*
* 引用地址变了后,函数不会调用,他只负责引用地址
*/
const add = useCallback(() => {
console.log("你好");
setNum(num + 1);
}, [count]);
return (
<div>
<div>缓存函数</div>
<button onClick={add}>num + 1</button>
<button onClick={() => setCount(count + 1)}>count + 1</button>
num ==> {num}
count ==> {count}
<ChildComponent add={add}></ChildComponent>
</div>
);
}
import React, { useState, useCallback } from "react";
// react.memo会做一层浅比较
/**
* 因为我们每次触发render 都会重新执行一遍当前函数
* 所以说,我们的方法会重新赋值,react.memo时进行浅比较
* 重新赋值的方法和之前的方法,引用不一样,所以react.memo
* 会认为是一个新的对象,所以会重新渲染
*/
const ChildComponent = React.memo((props) => {
console.log("每次render都会触发", props);
return (
<div>
<div>你好我是子组件</div>
</div>
);
});
export default function LearnUseCallBack() {
const [num, setNum] = useState(1);
const [count, setCount] = useState(1);
/**
* useCallback 第一个参数是一个函数
* 第二个参数是依赖项
* 依赖项不变的情况下,函数的引用不变
* 依赖项传空数组,那么函数会一直不变
* 如果什么都不穿,那么会失效
*
* 引用地址变了后,函数不会调用,他只负责引用地址
*/
const add = useCallback(() => {
console.log("你好");
setNum(num + 1);
}, [count]);
return (
<div>
<div>缓存函数</div>
<button onClick={add}>num + 1</button>
<button onClick={() => setCount(count + 1)}>count + 1</button>
num ==> {num}
count ==> {count}
<ChildComponent add={add}></ChildComponent>
</div>
);
}
模仿生命周期函数
componentDidMount-useEffect
useEffect(() => {
/**
* 当它是一个空数组时,回调只会被触发一次,类似于 componentDidMount
*/
console.log("componentDidmount")
}, [])
shouldComponentUpdate-React.memo
React.memo可以模仿shouldComponentUpdate的部分功能,拦截
import React, { useState, useEffect, useContext } from 'react';
const MyComponent = React.memo((props) => {
/* 使用 props 渲染 */
return (
<div>{props.num}</div>)
/**
* prevProps 上次的值
* nextProps 最新的值
*
* 如果传来的值是偶数的话则不更新组件
*/
}, (prevProps, nextProps) => {
console.log(nextProps, nextProps.num % 2)
return nextProps.num % 2 === 0
})
export default function hook() {
const [num, setNum] = useState(1)
useEffect(() => {
/**
* 当它是一个空数组时,回调只会被触发一次,类似于 componentDidMount
*/
console.log("componentDidmount")
}, [])
return (
<div>
<button onClick={() => setNum(num + 1)}>+1</button>
<MyComponent num={num}></MyComponent>
</div>)
}
componentWillUnmount
useEffect(() => {
return () => { // return是结束时可以调用
console.log('componentWillUnmount')
}
}, [])
封装一个自定义hook useDidUpdate
// 引入我们封装的自定义hooks
import useDidUpdate from './hooks/use-didupdate'
export default () => {
useDidupdate(() => {
// 每次初始化的时候不触发,每次render都会触发
console.log("模仿componentDidUpdate")
})
}