工作结束,又到了愉快的总结时间。
vue3 beta 版本发布之后,大家都说借鉴了 React Hooks,一直使用 vue 开发项目的我也来跟风学习下。
React 官方文档看起来实在费劲,自己总结了常见的几个 Hook用法,希望能帮助大家理解。
这篇文章主要整理一下 React 中的几个常见 Hook:
import React from "react"; import ReactDOM from "react-dom"; const rootElement = document.getElementById("root"); function App() { // 0是n的默认值,setN是操作n的函数 const [n, setN] = React.useState(0); return ( <div className="App"> <p>{n}</p> <p> <button onClick={() => setN(n + 1)}>+1</button> </p> </div> ); } ReactDOM.render(<App />, rootElement); 复制代码
如果 state 是一个对象,不能部分更新 setState,必须将对象所有属性一起传递。
import React, { useState, useReducer } from "react"; import ReactDOM from "react-dom"; // 1. 创建初始值 initialState const initial = { n: 0 }; // 2. 创建所有操作 reducer(state, action) 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() { // 3.传给 useReducer,得到读 state 和写 dispatch const [state, dispatch] = useReducer(reducer, initial); const { n } = state; const onClick = () => { // 4. 调用 写({type: '操作类型'}) dispatch({ type: "add", number: 1 }); }; const onClick2 = () => { dispatch({ type: "multi", number: 2 }); }; return ( <div className="App"> <h1>n: {n}</h1> <button onClick={onClick}>+1</button> <button onClick={onClick2}>x2</button> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); 复制代码
总的来说,useReducer相当于useState的复杂版。
好处是能够践行Flux/Redux的思想。
那么useState 和 useReducer 应该如何选择呢?
事不过三原则。如果同一个操作重复了3次以上,就使用useReducer。
import React from "react"; import ReactDOM from "react-dom"; const rootElement = document.getElementById("root"); const C = React.createContext(null); function App() { const [theme, setTheme] = React.useState("red"); return ( // C.Provider 表示 value 这个的作用域从这里开始 <C.Provider value={{theme, setTheme}}> <div className={`App ${theme}`}> <p>{theme}</p> <ChildA /> </div> </C.Provider> ); } function ChildA() { const { setTheme } = React.useContext(themeContext); return ( <div> <button onClick={() => setTheme("red")}>red</button> </div> ); } ReactDOM.render(<App />, rootElement); 复制代码
useContext 翻译为使用上下文,那什么是上下文呢?
上下文就是运行一段程序,需要的所有变量。
比如我们都知道的全局变量,就是全局的上下文。因为全局代码都需要这个变量。
上下文也是局部的全局变量,在一段代码内,都需要这个变量。
useContext 的更新机制不是响应式的,而是重新渲染,从根组件到子组件逐渐通知的过程。
你在一个模块将 C 里的值变化,另一个模块不会感知到这个变化。
当然,这个变化对于我们写代码基本没有影响。
effect 翻译为「副作用」,什么是副作用呢?
在函数式编程中,对环境的改变即为副作用。比如修改 document.title。
但我们不一定非要把副作用放在 useEffect 里。
实际上对于没有接触过函数式编程的前端工程,叫做 afterRender 更好,意思是每次 render 后运行。
可以代替三个钩子:
以上三种用途可同时存在。
如果同时存在多个 useEffect,会按照出现顺序依次执行。
import React, { useState, useEffect } from "react"; import ReactDOM from "react-dom"; function App() { const [n, setN] = useState(0); const onClick = () => { setN(i => i+1) } // 1. [] 里面的变量变化时执行 => 只执行一次,不会再次执行 useEffect(() => { console.log('第一次渲染后执行这句话') }, []); // 2. n 变化时执行 useEffect(() => { console.log('n 变化了') }, [n]); // 3. 销毁前渲染,通过 return useEffect(() => { const timer = setInterval(() => { console.log('hi') }, 1000) }, return () => { window.clearInterval(id); }); return ( <div className="App"> <h1>n: {n}</h1> <button onClick={onClick}>+1</button> </div> ); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); 复制代码
layout effect 翻译为「布局副作用」。
useEffect 在浏览器渲染完成后执行,useLayoutEffect 在浏览器渲染前执行。 useLayoutEffect 总是比 useEffect 先执行。
为了用户体验,优先使用 useEffect。
因为 useLayoutEffect 在渲染前执行,和 useEffect 相比有更长时间的白屏。
当 useEffect 不能满足你的需求时,再使用 useLayoutEffect。
useLayoutEffect 里的任务最好影响了 Layout(页面布局)。
要理解 React.useMemo,需要先理解 React.memo。
React 默认有多余的render,比如说 n 变了,但是依赖 m 的组件也刷新了。
代码中的组件,比如 Child 用 React.memo(Child) 代替。
就可以做到 props 不变,不会再执行一个函数组件。
import React from "react"; import ReactDOM from "react-dom"; import "./styles.css"; function App() { const [n, setN] = React.useState(0); const [m, setM] = React.useState(0); const onClick = () => { setN(n + 1); }; return ( <div className="App"> <div> <button onClick={onClick}>update n {n}</button> </div> <Child data={m}/> </div> ); } const Child = React.memo((props) => { console.log("child 执行了"); console.log('假设这里有大量代码') return <div>child: {props.data}</div>; }); const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); 复制代码
在 Child 添加了监听函数之后,监听函数每次都会执行。
function App() { const [n, setN] = React.useState(0); const [m, setM] = React.useState(0); const onClick = () => { setN(n + 1); }; const onClickChild = () => { console.log(m); }; return ( <div className="App"> <div> <button onClick={onClick}>update n {n}</button> </div> <!-- {/* Child2 居然又执行了 */}--> <Child data={m} onClick={onClickChild} /> </div> ); } const Child = React.memo((props) => { console.log("child 执行了"); console.log('假设这里有大量代码') return <div>child: {props.data}</div>; }); 复制代码
因为 App 运行时会生成新的函数,新旧函数虽然功能意义,但是地址不一样。
怎么办?用useMemo
第一个参数是 () => value
,第二个参数是依赖[m,n]
。
只有当依赖变化,才会计算新的value。 如果依赖不变,那么就重用之前的value。
(和 vue2 的 computed 功能相同哇)
如果你的value是个函数,就要写成 useMemo(() =>(x) => console.log(x))
。
这是一个很难返回的函数,于是就有了useCallback。
useCallback 只是一个语法糖。
useCallback(x => log(x), [m])
等价于 useMemo(() =>(x) => console.log(x), [m])
如果你需要一个值,在组件不断 render 时保持不变。使用 useRef。
但是页面不会自动更新,需要手动刷新。
function App() { // 初始化 const count = useRef(0); // {current: 0} const onClick = () => { // 读取:count.current // 为什么需要 current? // 为了保证两次 useRef 是同一个值。不改变 useRef 对象的地址,只改变 count.current 的值 count.current += 1; console.log(count.current) } return ( <div className="App"> // 页面不会自动更新,没有 re-render <h1>n: {count.current}</h1> <button onClick={onClick}>+1</button> </div> ); } 复制代码
vue3
import { ref, watchEffect } from 'vue' // 初始化:const count = ref(0) const count = ref(0) function increment() { // 读取:count.value count.value++ } const renderContext = { count, increment } // 不同点:当count.value 变化时,vue3 会自动 render, 页面变化 watchEffect(() => { renderTemplate( `<button @click="increment">{{ count }}</button>`, renderContext ) }) 复制代码
如果一个函数组件,想要接受其他组件传递的 ref 参数。 必须使用 React.forwardRef 包裹起来。
因为 props 不包含 ref
function App() { const count = useRef(0); // {current: 0} return ( <div className="App"> <Button3 ref={buttonRef}>按钮</Button3> </div> ); } const Button3 = React.forwardRef((props, ref) => { return <button ref={ref}></button> }) 复制代码
不管用的什么 hook,只需要将增删改查暴露出去。 使用时直接调用,不用关心内部逻辑。
一个例子
import React, { useRef, useState, useEffect } from "react"; import ReactDOM from "react-dom"; // 自定义 hooks useList const useList = () => { const [list, setList] = useState(null); useEffect(() => { ajax("/list").then(list => { setList(list); }); }, []); // [] 确保只在第一次运行 // 暴露出读写接口 return { list: list, setList: setList }; }; function App() { // 使用 hooks index.js const { list, setList } = useList(); return ( <div className="App"> <h1>List</h1> {list ? ( <ol> {list.map(item => ( <li key={item.id}>{item.name}</li> ))} </ol> ) : ( "加载中..." )} </div> ); } // mock function ajax() { return new Promise((resolve, reject) => { setTimeout(() => { resolve([ { id: 1, name: "Frank" }, { id: 2, name: "Jack" } ]); }, 2000); }); } const rootElement = document.getElementById("root"); ReactDOM.render(<App />, rootElement); 复制代码
译为过时的闭包。 在使用 useEffect 时,需要注意过时的闭包问题。 具体问题可以点击这里查看,我在这简单描述下。
function createIncrement(i) { // 一个函数用到了它外面的变量,就是闭包 let value = 0; function increment() { value += i; console.log(value); const message = `Current value is ${value}`; return function logValue() { console.log(message); }; } return increment; } const inc = createIncrement(1); const log = inc(); // logs 1 (第一次执行,得到message1) inc(); // logs 2 (第二次执行,得到message2) inc(); // logs 3 (第三次执行,得到message3) log(); // logs "Current value is 1" 因为这里保存了一个过时的 log 复制代码
实际上,第一次执行 inc(),得到 message1;第二次,得到 message... log 保存的是 message,这就是过时的闭包。
如果我们想得到的是 3,应该怎么做?
const inc = createIncrement(1); inc(); // logs 1 inc(); // logs 2 const latestLog = inc(); // logs 3 // Works! latestLog(); // logs "Current value is 3" 复制代码
让我们将行const message = ...;移到logValue()函数主体中:
function createIncrementFixed(i) { let value = 0; function increment() { value += i; console.log(value); return function logValue() { //****************** const message = `Current value is ${value}`; console.log(message); //****************** }; } return increment; } const inc = createIncrementFixed(1); const log = inc(); // logs 1 inc(); // logs 2 // Works! log(); // logs "Current value is 3" 复制代码
让我们研究一下使用 useEffect() 时过时关闭的情况。
钩子在组件内部 useEffect() 记录以下值count:
function WatchCount() { const [count, setCount] = useState(0); useEffect(function() { setInterval(function log() { console.log(`Count is: ${count}`); }, 2000); }, []); return ( <div> {count} <button onClick={() => setCount(count + 1) }> Increase </button> </div> ); } 复制代码
然后单击几次增加按钮。然后看一下控制台,每隔2秒钟就会出现一次Count is: 0。
为什么会发生?
第一次渲染时,闭包log()将count变量捕获为0。 后来,即使count增加,log()仍然从最初的渲染使用count:0。log()是一个过时的闭包。
解决方案是让useEffect()闭包log()依赖count:
useEffect(function() { setInterval(function log() { console.log(`Count is: ${count}`); }, 2000); // 正确设置依赖 项count 后,请useEffect()在count更改后立即更新闭包。 }, [count]); 复制代码