useState: 让函数组件具有维持状态的能力
在一个函数组件的多次渲染之间,这个state是共享的 下面看一个简单的代码: const [count , setCount] = useState(0)
useState的定义:
- useState(initialState)的参数initialState是创建state的初始值,它可以是任意类型,比如数字,对象,数组等等
- useState()返回值是一个有着两个元素的数组。第一个数组元素用来读取state的值,第二个则是用来设置这个state的值。第一数组元素state是一个只读的数,我们要设置它的值只能通过第二个元素setState来设置。
- 如果创建创建多个state,那么我们就需要多次调用useState
useState 和 类组件里面的setState类似。但是两者并不相同区别: 类组件中的state只能有一个,一般都是把一个对象作为一个state,然后通过不同的属性来表示不同的状态。而函数组件中用useState则可以很容易的创建多个state,所以更加语义化。
state中永远不要保存可以通过计算得到的值。例如:
- 从props传递过来的值。有时候props传递过来的值无法直接使用,而是通过一定的计算后再UI上展示,比如说排序。那么我们要做的就是每次用的时候,都重新排序一下,或者利用某些cache机制,而不是将结果直接放到state里。
- 从URL中读取的值。比如有时需要读取url中的参数,把它作为组件的一部分状态。那么我们可以在每次需要用的时候从URL中读取,而不是读出来直接放到state里。
- 从cookie localStorage中读取的值。通常来说,也是每次要用的时候直接读取,而不是读出来后放到state里。
state便于维护状态,但是也有缺点。一旦组件有自己状态,意味着组件如果重新创建,就需要有恢复状态的过程,这通常会让组件变得更复杂。
比如一个组件想在服务器段请求获取一个用户列表显示,如果把读取的数据放到本地的state里,那么每个用到这个组件的地方,就都需要重新获取一遍。
所以有时候我们需要一个状态管理框架去维护管理所有组件的state。
useEffect:执行副作用 副作用是指一段和当前执行结果无关的代码。比如说要修改函数外部的某些变量,要求发起一个请求,等等。在函数组件的执行过程中,useEffect 中的代码的执行是不影响渲染出来的ui的。 useEffect(callback,dependencies)
useEffect接受两个要执行的函数 callback,第二个是可选的依赖项数组dependencies。其中依赖项是可选的,如果不指定那么callback就会在每次函数组件执行完后都执行;如果指定了那么只有依赖项中的值发生变化的时候,他才会执行。
useEffect涵盖了class组件中ComponentDidmount, componentDidUpdate, componentWillUnmount 三个生命周期方法。useEffect 是每次组件render完后判断依赖并执行就可以了。 当useEffect 没有依赖项时,每次render后都会重新执行。 当空数组作为依赖项时,则只在首次执行时触发,对应class组件componentDidmount。
除此之外useEffect还允许返回一个函数,用于在组件销毁的时候做一些清理的操作。比如移除事件的监听。这个机制就几乎等价于类组件中的componentWillUnmount。
useEffect 的四种执行时机:
- 每次render后执行:不提供第二个依赖项参数。useEffect(()=>{})
- 仅第一次render后执行:提供一个空数组作为依赖项。useEffect(()=>{},[])
- 第一次及依赖项发生变化后执行:提供依赖项数组。useEffect(()=>{},[deps])
- 组件unmount后执行:返回一个回调函数。useEffect(()=>{return ()=>{}},[])
Hooks 本身作为纯粹的javaScript 函数,不是通过某个特殊的API创建的,而是直接定义一个函数。只能在函数组件的顶级作用域使用;只能在函数组件或其他Hooks中使用。
Hooks 只能在函数组件的顶级作用域使用 所谓顶层作用域,就是Hooks不能在循环,条件判断或者嵌套函数内执行,而必须在顶层。同时Hooks在组件的多次渲染之间,必须按顺序执行。因为在React组件内部,其实时维护了一个对应组件的固定Hooks执行列表的,以便在多次渲染之间保持Hooks的状态,并做对比。
Hooks规则总结:第一,所有Hook必须要被执行到。第二,必须按顺序执行。
useCallback: 缓存回调函数 在react函数组件中,每一次UI的变化,都是通过重新执行整个函数来完成的,这和传统的class组件有很大的区别:函数组件中并没有一个直接的方式在多次渲染之间维持一个状态。 每次组件状态发生变化的时候,函数组件实际上都会重新执行一遍。在每次执行的时候,实际上都会创建一个新的事件处理函数 例如handleIncrement(这个函数会让count+1)。这个事件处理函数包含了count这个变量的闭包,以确定每次能够得到正确的结果。 这也意味着,即使count没有发生变化,但是函数组件因为其它状态发生变化而重新渲染时,这种写法也会每次创建一个新的函数。创建一个新的事件处理函数,虽然不影响结果的正确性,但其实是没有必要的,因为这样做不仅增加了系统的开销,更重要的是:每次创建新函数的方式会让接收事件处理函数的组件,需要重新渲染。如果每次都是一个新的,那么这个react就会认为这个组件的props发生了变化,从而必须重新渲染。因此,我们需要做到的是:只有当count发生变化时,我们才需要重新定一个回调函数。 useCallback(fn,deps) 这里fn是定义的回调函数,deps是依赖的变量数组。只有当某个依赖变量发生变化时,才会重新声明fn这个回调
import React, { useState, useCallback } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleIncrement = useCallback(
() => setCount(count + 1),
[count], // 只有当 count 发生变化时,才会重新创建回调函数
);
// ...
return <button onClick={handleIncrement}>+</button>
}
我们把count这个state,作为一个依赖传递给useCallback。只要count发生变化才需要创建一个回调函数,保证了组件不会创建重复的回调函数,而接收这个回调函数作为属性的组件,也不会频繁地需要重新渲染。 useCallback 和 useMemo 的区别: useCallback缓存的是一个函数,而useMemo缓存的是计算结果。
useMemo: 缓存计算的结果。 useMemo(fn,deps) 使用场景:如果某个数据是通过其他数据计算得到的,那么只有当用到的数据,也就是依赖的数据发生变化的时候,才应该需要重新计算。 useMemo 可以避免在用到的数据没发生变化时进行的重复计算,并且能避免子组件的重复渲染,如果父组件的某个变量每次都需要重新计算来得到,对着这个父组件来说就会每次都需要刷新。如果能将此属性缓存,就和useCallback一样,可以避免很多不必要的组件刷新。 其实useCallback的功能其实是可以利用useMemo来实现的。比如:
const myEventHandler = useMemo(() => {
// 返回一个函数作为缓存结果
return () => {
// 在这里进行事件处理
}
}, [dep1, dep2]);
他们两者其实很类似:都是建立了一个绑定某个结果到依赖数据的关系。只有当依赖变了,这个结果才需要被重新得到。
useRef: 在多次渲染之间共享数据
在类组件中我们定义类的成员变量,以便能够在对象上通过成员属性去保存一些数据。但是在函数组件中,是没有这样一个空间去保存数据的。因此,React让useRef来提供这样的功能。 返回的 ref 对象在组件的整个生命周期内持续存在。 const myRefContainer = useRef(initialValue) 我们可以吧useRef看作是阿兹函数组件之外创建的一个容器空间。在这个容器上,我们可以通过唯一的current属性设置一个值,从而在函数组件的多次渲染之间共享这个值。 例子:
import React, { useState, useCallback, useRef } from "react";
export default function Timer() {
// 定义 time state 用于保存计时的累积时间
const [time, setTime] = useState(0);
// 定义 timer 这样一个容器用于在跨组件渲染之间保存一个变量
const timer = useRef(null);
// 开始计时的事件处理函数
const handleStart = useCallback(() => {
// 使用 current 属性设置 ref 的值
timer.current = window.setInterval(() => {
setTime((time) => time + 1);
}, 100);
}, []);
// 暂停计时的事件处理函数
const handlePause = useCallback(() => {
// 使用 clearInterval 来停止计时
window.clearInterval(timer.current);
timer.current = null;
}, []);
return (
<div>
{time / 10} seconds.
<br />
<button onClick={handleStart}>Start</button>
<button onClick={handlePause}>Pause</button>
</div>
);
}
我们使用useRef来创建一个保存window.setInterval返回句柄的空间,从而能够在用户点击暂停按钮时清楚定时器,达到暂停计时的目的。 使用useRef保存的数据一般和UI的渲染无关,当ref的值发生变化时,是不会触发组件的重新渲染的,这也是useRef区别于useState的地方。 除了存储跨渲染的数据之外,useRe还能保存某个DOM节点的引用。
function TextInputWithFocusButton() {
const inputEl = useRef(null);
const onButtonClick = () => {
// current 属性指向了真实的 input 这个 DOM 节点,从而可以调用 focus 方法
inputEl.current.focus();
};
return (
<>
<input ref={inputEl} type="text" />
<button onClick={onButtonClick}>Focus the input</button>
</>
);
}
ref 这个属性提供了获得 DOM 节点的能力,并利用 useRef 保存了这个节点的应用。这样的话,一旦 input 节点被渲染到界面上,那我们通过 inputEl.current 就能访问到真实的 DOM 节点的实例了。
useContext:定义全局状态
react提供了context这样一个机制,能够让所有在某个组件开始的组件树上创建一个context,这样这个组件树上的所有组件,就能访问和修改这个context。 const value = useContext(MyContext); 一个context是从某个组件为根组件的组件树上可用的,所以我们需要有API能够创建一个context,这就是React,createContext API 如下: const Mycontext = React.createContext(initialValue)
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee"
},
dark: {
foreground: "#ffffff",
background: "#222222"
}
};
// 创建一个 Theme 的 Context
const ThemeContext = React.createContext(themes.light);
function App() {
// 整个应用使用 ThemeContext.Provider 作为根组件
return (
// 使用 themes.dark 作为当前 Context
<ThemeContext.Provider value={themes.dark}>
<Toolbar />
</ThemeContext.Provider>
);
}
// 在 Toolbar 组件中使用一个会使用 Theme 的 Button
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
);
}
// 在 Theme Button 中使用 useContext 来获取当前的主题
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button style={{
background: theme.background,
color: theme.foreground
}}>
I am styled by theme context!
</button>
);
}
context看起来是一个全局变量,其实是有数据绑定的作用,当context值发生变化时会触发组件的自动刷新。 动态的切换context的值:
// ...
function App() {
// 使用 state 来保存 theme 从而可以动态修改
const [theme, setTheme] = useState("light");
// 切换 theme 的回调函数
const toggleTheme = useCallback(() => {
setTheme((theme) => (theme === "light" ? "dark" : "light"));
}, []);
return (
// 使用 theme state 作为当前 Context
<ThemeContext.Provider value={themes[theme]}>
<button onClick={toggleTheme}>Toggle Theme</button>
<Toolbar />
</ThemeContext.Provider>
);
}
context 相当于提供了一个定义React世界中全局变量的机制,而全局变量则意味着两点:
- 会让调试变得困难,因为你很难跟踪某个context的变化究竟是如何产生的。
- 让组件的复用变得困呐,因为一个组件如果使用某个context,他就必须确保被用到的地方一定有这个context的provider在其父组件的路径上。 context更多的是提供了一个强大的机制,让React应用具备定义全局的响应式数据的能力。
正确理解函数组件的生命周期
react 可能引起状态变化的原因:
- 用户操作产生的事件,比如点击了某个按钮。
- 副作用产生的事件,比如发起某个请求正确返回了。 这两种事件本身不会导致组件的重新渲染,一定是因为改变了某个状态,这个状态可能是State 或者 Context,从而导致了UI的重新渲染。 在函数组件中你要思考的方式永远是:当某个状态发生变化时,我要做什么,而不再是在class组件中的某个生命周期方法中我要做什么。 使用useRef来来实现一个初始化的实例的过程 useSingleton :
import { useRef } from 'react';
// 创建一个自定义 Hook 用于执行一次性代码
function useSingleton(callback) {
// 用一个 called ref 标记 callback 是否执行过
const called = useRef(false);
// 如果已经执行过,则直接返回
if (called.current) return;
// 第一次调用时直接执行
callBack();
// 设置标记为已执行过
called.current = true;
}
调用这个hooks:
import useSingleton from './useSingleton';
const MyComp = () => {
// 使用自定义 Hook
useSingleton(() => {
console.log('这段代码只执行一次');
});
return (
<div>My Component</div>
);
};
useSingleton 这个 Hook 的核心逻辑就是定义只执行一次的代码。而是否在所有代码之前执行,则取决于在哪里调用,可以说,它的功能其实是包含了构造函数的功能的。 useEffect的写法并没有完全等价于传统的这几个生命周期方法:
- useEffect(callback)这个hook接收的callback,只有在依赖项变化时才执行,而传统的componentDidUpdate则一定会执行,Hook的机制其实更具有语义话,因为过去在componentDidUpdate中,我们通常需要手动判断某个状态是否发生变化,然后在执行特定的逻辑。
- callback返回的函数在下一次依赖项发生变化以及组件销毁之前执行,而传统的componentWillUnmount只在组件销毁时才会执行。
创建一个自定义Hooks
自定义hooks在形式上非常简单,声明一个名字以use开头的函数,如果内部并没有用其他任何hooks,那么这个函数就不是一个hook,而只是一个普通的函数。但是如果用了其他Hooks,那么他就是一个hook。 例如:
import { useState, useCallback }from 'react';
function useCounter() {
// 定义 count 这个 state 用于保存当前数值
const [count, setCount] = useState(0);
// 实现加 1 的操作
const increment = useCallback(() => setCount(count + 1), [count]);
// 实现减 1 的操作
const decrement = useCallback(() => setCount(count - 1), [count]);
// 重置计数器
const reset = useCallback(() => setCount(0), []);
// 将业务逻辑的操作 export 出去供调用者使用
return { count, increment, decrement, reset };
}
// 使用
import React from 'react';
function Counter() {
// 调用自定义 Hook
const { count, increment, decrement, reset } = useCounter();
// 渲染 UI
return (
<div>
<button onClick={decrement}> - </button>
<p>{count}</p>
<button onClick={increment}> + </button>
<button onClick={reset}> reset </button>
</div>
);
}
在上述代码中,我们吧原来在函数组件中实现的逻辑提取出来,成为一个单独的Hook,一方面能让这个逻辑得到重用,另外一方面也能让代码更加语义化,并且易于理解和维护。 自定义Hooks的两个特点:
- 名字一定是以use开头的函数,这样react才能知道这个函数是一个Hook;
- 函数内部一定调用了其他的Hooks,可以是内置的Hooks,也可以是其他自定义Hooks。这样才能够让组件刷新,或者去产生副作用。
封装通用逻辑
import { useState } from 'react';
const useAsync = (asyncFunction) => {
// 设置三个异步逻辑相关的 state
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// 定义一个 callback 用于执行异步逻辑
const execute = useCallback(() => {
// 请求开始时,设置 loading 为 true,清除已有数据和 error 状态
setLoading(true);
setData(null);
setError(null);
return asyncFunction()
.then((response) => {
// 请求成功时,将数据写进 state,设置 loading 为 false
setData(response);
setLoading(false);
})
.catch((error) => {
// 请求失败时,设置 loading 为 false,并设置错误状态
setError(error);
setLoading(false);
});
}, [asyncFunction]);
return { execute, loading, data, error };
};
我们在组件中只需要关心业务逻辑相关的部分:
import React from "react";
import useAsync from './useAsync';
export default function UserList() {
// 通过 useAsync 这个函数,只需要提供异步逻辑的实现
const {
execute: fetchUsers,
data: users,
loading,
error,
} = useAsync(async () => {
const res = await fetch("https://reqres.in/api/users/");
const json = await res.json();
return json.data;
});
return (
// 根据状态渲染 UI...
);
}
利用Hooks能够管理React 组件状态的能力,将一个组件中的某一部分状态独立出来,从而实现了通用逻辑的重用。 这种类型的封装哦我们呢写一个工具类就可以了,为什么要通过Hooks进行封装呢? 因为在Hooks中,你可以管理当前组件的state,从而将更多的逻辑写在可重用的Hooks中。但是要知道,在普通的工具类中是无法直接修改组件state的,那么也就无法在数据改变的时候触发组件的重新渲染。
监听浏览器状态:useScroll
虽然react组件基本上不需要关心太多浏览器的API,但是有时候缺失必须的:
- 界面需要根据在窗口大小变化重新布局;
- 在页面滚动时,需要根据滚动条位置,来决定是否显示一个“返回顶部”的按钮 Hooks的优点就是可以让React的组件绑定在任何可能的数据源上。这样当数据源繁盛变化是,组件能够自动刷新。 例子:
import { useState, useEffect } from 'react';
// 获取横向,纵向滚动条位置
const getPosition = () => {
return {
x: document.body.scrollLeft,
y: document.body.scrollTop,
};
};
const useScroll = () => {
// 定一个 position 这个 state 保存滚动条位置
const [position, setPosition] = useState(getPosition());
useEffect(() => {
const handler = () => {
setPosition(getPosition(document));
};
// 监听 scroll 事件,更新滚动条位置
document.addEventListener("scroll", handler);
return () => {
// 组件销毁时,取消事件监听
document.removeEventListener("scroll", handler);
};
}, []);
return position;
};
监听滚动:
import React, { useCallback } from 'react';
import useScroll from './useScroll';
function ScrollTop() {
const { y } = useScroll();
const goTop = useCallback(() => {
document.body.scrollTop = 0;
}, []);
const style = {
position: "fixed",
right: "10px",
bottom: "10px",
};
// 当滚动条位置纵向超过 300 时,显示返回顶部按钮
if (y > 300) {
return (
<button onClick={goTop} style={style}>
Back to Top
</button>
);
}
// 否则不 render 任何 UI
return null;
}
自定义hooks的四个使用场景:
- 抽取业务逻辑
- 封装通用逻辑
- 监听浏览器状态
- 拆分复杂组件
redux的两个特点:
- Redux Store是全局唯一的。(即整个应用程序一般只有一个store)
- Redux Store是树状结构,可以更天然地映射到组件树的结构,虽然不是必须的
Redux: state Action 和 Reducer 其中state即store,一般就是一个纯JS Object Action 也是一个对象,用于描述动作。 Reducer则是一个函数,接受Action 和 State 并作为参数,通过计算得到新的store
state + Action = new state
action 并不是一个具体的概念,而可以把它看作是redux的一个使用模式。他通过组合使用同步action,在没有引入新概念的同时,用一致的方式提供了处理异步逻辑的方案。
Hooks通过useState的内置Hook 来完成组件的更新。自定义Hooks要实现的逻辑要么用到state,要么用到state,要么用到副作用,是一定会用到内置Hooks或者其他自定义Hooks的。 useEffect 一定是在函数render之后执行,而函数中的代码,是直接影响当次render的结果。
异步发送请求
import { useState, useEffect } from "react";
import apiClient from "./apiClient";
export default (id) => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
// 当 id 不存在,直接返回,不发送请求
if (!id) return;
setLoading(true);
setData(null);
setError(null);
apiClient
.get(`/users/${id}`)
.then((res) => {
setLoading(false);
setData(res.data);
})
.catch((err) => {
setLoading(false);
setError(err);
});
}, [id]);
return {
loading,
error,
data
};
};
import { useState } from "react";
import CommentList from "./CommentList";
import useArticle from "./useArticle";
import useUser from "./useUser";
import useComments from "./useComments";
const ArticleView = ({ id }) => {
const { data: article, loading, error } = useArticle(id);
const { data: comments } = useComments(id);
const { data: user } = useUser(article?.userId);
if (error) return "Failed.";
if (!article || loading) return "Loading...";
return (
<div className="exp-09-article-view">
<h1>
{id}. {article.title}
</h1>
{user && (
<div className="user-info">
<img src={user.avatar} height="40px" alt="user" />
<div>{user.name}</div>
<div>{article.createdAt}</div>
</div>
)}
<p>{article.content}</p>
<CommentList data={comments || []} />
</div>
);
};
Hooks 必须在顶层作用域调用,而不能放在条件判断,循环等语句,也不能放在return 语句之后。react需要在函数组件内部维护所用到的Hooks状态,所以无法在条件语句中使用Hooks。
render props 实现UI逻辑重用需求场景
import { Popover } from "antd";
function ListWithMore({ renderItem, data = [], max }) {
const elements = data.map((item, index) => renderItem(item, index, data));
const show = elements.slice(0, max);
const hide = elements.slice(max);
return (
<span className="exp-10-list-with-more">
{show}
{hide.length > 0 && (
<Popover content={<div style={{ maxWidth: 500 }}>{hide}</div>}>
<span className="more-items-wrapper">
and{" "}
<span className="more-items-trigger"> {hide.length} more...</span>
</span>
</Popover>
)}
</span>
);
}
// 这里用一个示例数据
import data from './data';
function ListWithMoreExample () => {
return (
<div className="exp-10-list-with-more">
<h1>User Names</h1>
<div className="user-names">
Liked by:{" "}
<ListWithMore
renderItem={(user) => {
return <span className="user-name">{user.name}</span>;
}}
data={data}
max={3}
/>
</div>
<br />
<br />
<h1>User List</h1>
<div className="user-list">
<div className="user-list-row user-list-row-head">
<span className="user-name-cell">Name</span>
<span>City</span>
<span>Job Title</span>
</div>
<ListWithMore
renderItem={(user) => {
return (
<div className="user-list-row">
<span className="user-name-cell">{user.name}</span>
<span>{user.city}</span>
<span>{user.job}</span>
</div>
);
}}
data={data}
max={5}
/>
</div>
</div>
);
};
在react 中 父子组件的交互式通过props这个机制其实是双向的,父组件通过props把值传递给子组件,而子组件则通过暴露一些事件,给父组件反馈一些状态或者数据。(组件之间通信的基础)
react17之前所有的事件都是绑定在document 上的 react 17之后所有的事件都绑定在整个APP上的根节点上,这主要是为了以后页面上可能存在多版本react考虑的。 原因: 第一虚拟DOM render的时候,dom很可能还没有真实地render 到页面上,所以无法绑定事件。 第二react可以屏蔽底层事件的细节,避免浏览器的兼容性问题 同时对于react native 这种不是通过浏览器render 的运行时,也能提供一致的API 浏览器的原生机制事件会从被触发的节点往父节点冒泡,直到根节点,所以根节点其实是可以收到所有的事件的(冒泡模型) 无论事件在哪个节点被触发,React都可以通过事件的srcElement 这个属性,知道它是从哪个节点开始出发的,这样react就可以收集管理所有的事件,然后再以一致的api暴露出来。 自定义事件和原生事件