本文首发于个人博客
上次的一次搞定前端四大手写在知乎上收获了500多个赞,简直让我受宠若惊。今天就趁热打铁,写一下一次搞定七大 React Hooks,一方面是为了复习下 React,另一方面是跟小伙伴们分享一些我学习 React Hooks 时的心得体会。由于水平有限,目前只能从 React Hooks 的基本使用方法和使用要点上做些分享,关于 Hooks 的原理上的探究日后再做更新。
让我们先来看道字节面试题,题目是实现一个自定义的 Hook,实现点击切换状态。
function SomeComponent() {
const [state, toggleState] = useToggle(false);
return <div>
{state ? 'true' : 'false'}
<button onClick={toggleState}></button>
</div>
}
// 请实现 useToggle
function useToggle(initialValue) {
const [value, setValue] = useState(initialValue);
const toggle = () => {setValue(!value)};
return [value, toggle];
}
复制代码
七大 Hooks 都有哪些
useState
状态useEffect
钩子,还有它的兄弟useLayoutEffect
useContext
上下文useReducer
代替 ReduxuseMemo
缓存,还有它的小弟useCallback
useRef
引用自定义 Hook
混合
useState
基本语法:
const [X, setX] = React.useState(X的初始值)
简单示例:
function App() {
const [user,setUser] = useState({name:'Varian', age: 18})
const onClick = ()=>{
setUser({
name: 'Janye'
})
}
return (
<div className="App">
<h1>{user.name}</h1>
<h2>{user.age}</h2>
<button onClick={onClick}>Click</button>
</div>
);
}
复制代码
我们会发现,点击按钮之后,age 消失了,而我们明明只改了 name 呀,为什么呢?
简单来说就是前后是两个完全不相关的对象。
展开讲的话 React 在数据变化时会创建新的虚拟 DOM 对象,然后将这个虚拟 DOM 对象跟原虚拟 DOM 进行一个 DOM Diff,得到一个最小的变化过程 Patch,并把这个 Patch 渲染到页面上,Diff 的时候发现新对象没有 age 这个属性,于是就把它删除了。
于是在使用 useState 的时候我们需要注意两个地方:
- 想要原来的值,必须在 setX 里先进行复制,类似这样
setUser({...user, name: 'Janye'})
- setX(obj) 时,obj 的地址必须改变
useEffect
useEffect
的作用主要是用来解决函数组件如何像类组件一样使用生命周期钩子的问题。
它有三个使用场景:
- 作为 componentDidMount 使用,第二个参数为空数组
[]
- 作为 componentDidUpdate 使用,第二个参数为指定依赖
- 作为 componentWillUnmount 使用,通过 return
这里给一个最简单的例子:
const BlinkyRender = () => {
const [value, setValue] = useState(0);
useEffect(() => {
document.querySelector('#x').innerText = `value: 1000`
}, [value]);
return (
<div id="x" onClick={() => setValue(0)}>value: {value}</div>
);
};
ReactDOM.render(
<BlinkyRender />,
document.querySelector("#root")
);
复制代码
那么它跟它的兄弟 useLayoutEffect
有什么区别呢?
useEffect
在浏览器渲染完成后执行,useLayoutEffect
在浏览器渲染前执行,useLayoutEffect
总是比 useEffect
先执行。
那么为了用户体验(先渲染就能先看到),通常我们应该先用useEffect
。
useContext
如果我们想在组件之间共享状态的话,可以使用 useContext
。
它的使用可以分为三个步骤:
- 使用
C = createContext(initial)
创建上下文 - 使用
<C.provider>
圈定作用域 - 在作用域内使用
useContext(C)
来使用上下文
简单示例:
const C = createContext(null);
function App() {
console.log("App 执行了");
const [n, setN] = useState(0);
return (
<C.Provider value={{ n, setN }}>
<div className="App">
<Baba />
</div>
</C.Provider>
);
}
function Baba() {
const { n, setN } = useContext(C);
return (
<div>
我是爸爸 n: {n} <Child />
</div>
);
}
function Child() {
const { n, setN } = useContext(C);
const onClick = () => {
setN(i => i + 1);
};
return (
<div>
我是儿子 我得到的 n: {n}
<button onClick={onClick}>+1</button>
</div>
);
}
复制代码
useReducer
如果要一句话解释 useReducer
的话,它是用来代替 Redux 的,或者说,是一个加强版的 useState
。
使用上来说,一共有四步:
- 创建初始值 initialState
- 创建所有操作 reducer(state, action)
- 传给 useReducer,得到读和写 API
- 调用
写({type: '操作类型'})
这里给一个基本的示例:
const initial = {
n: 0
};
const reducer = (state, action) => {
if (action.type === "add") {
return { n: state.n + action.number };
} else if (action.type === "multi") {
return { n: state.n * 2 };
} else {
throw new Error("unknown type");
}
};
function App() {
const [state, dispatch] = useReducer(reducer, initial);
const { n } = state;
const onClick = () => {
dispatch({ type: "add", number: 1 });
};
const onClick2 = () => {
dispatch({ type: "add", number: 2 });
};
return (
<div className="App">
<h1>n: {n}</h1>
<button onClick={onClick}>+1</button>
<button onClick={onClick2}>+2</button>
</div>
);
}
复制代码
useMemo
基本语法:useMemo(回调函数, [依赖])
类似与 Vue 的计算属性 computed,useMemo 具有缓存,依赖改变才重新渲染的功能。
跟它的小弟 useCallback
的唯一区别是:useMemo
可以缓存所有对象,useCallback
只能缓存函数。
useCallback(x => log(x), [m])
等价于 useMemo(() => x => log(x), [m])
useRef
主要作用是创建一个数据的引用,并让这个数据在 render 过程中始终保持不变。
基本语法:
const count = useRef(0)
,读取用 count.current
用法这里给大家参考一下我封装 Echarts 时的例子:
export function ReactEcharts(props) {
const {option, loading} = props
const container = useRef(null)
const chart = useRef(null)
useEffect(() => {
const width = document.documentElement.clientWidth
const c = container.current
console.log(c)
c.style.width = `${width - 20}px`
c.style.height = `${(width - 20) * 1.2}px`
chart.current = echarts.init(c, 'dark')
}, []) // [] - mounted on first time
useEffect(() => {
chart.current.setOption(option)
}, [option]) // when option change 类似 vue 的 watch
useEffect(() => {
if (loading) chart.current.showLoading()
else chart.current.hideLoading()
}, [loading])
return (
<div ref={container}/>
)
}
复制代码
自定义 Hook
可以理解为我们可以把上面的 Hook 按照实际的需求混合起来,封装成一个函数,给一个简单示例:
const useList = () => {
const [list, setList] = useState(null);
useEffect(() => {
ajax("/list").then(list => {
setList(list);
});
}, []); // [] 确保只在第一次运行
return {
list: list,
setList: setList
};
};
export default useList;
复制代码
写在最后
结合最近参加面试的经历,有两个感想跟大家分享一下:
- 技术的学习和提高离不开持之以恒地练习,需要不断温故知新才能克服遗忘曲线;
- 利用好每次和面试官交流的机会,对于自己生疏的知识点进行及时反思复盘,进一步完善自己的知识体系。