React运行机制
React主要包含两个库React和ReactDom。
React元素
React通过JSX让我们可以不操作DOM元素,通过声明式用js对象来描述我们想让React构建的元素。我们写的JSX标签,会在编译阶段变成React.createElement()
来创建React元素。
React.createElement("h1",{id:"recipe-0"},"innertext...")
- 创建元素类型
- 属性
- 子元素
一个React元素结构如下:
- type属性:告知React要创建的元素类型
- props属性:表示构建一个DOM元素需要的数据和子元素
- children属性:表示嵌套显示在文本里
ReactDOM
创建React元素后,我们还需要渲染。使用ReactDOM.render(element,document.getElementById()"root")
在DOM中渲染
为什么将渲染和创建React元素分开:
- 性能优化:减少频繁操作DOM元素的开销
- 方便渲染到不同的设备上,例如浏览器DOM,canvas或者原生App上。
- 便于SSR渲染
子元素
props.children
为标签的子元素。
其他React元素也可成为子元素。
JSX
用React.createElement()
来创建React元素显然太复杂了,我们需要使用JSX。
JSX是JS与XML的结合,是对JavaScript的扩展,使用基于标签的句法直接定义React元素。
Babel
JSX可读性高,但浏览器无法解释。所以需要Babel把JSX都转换成React.createElement()
调用。
Babel负责编译:如果我们使用了JSX或者新的JS特性,浏览器都无法解释,让Babel把我们写的js转换成浏览器可以解释的版本。
JavaScript 引擎只能实现标准中的一部分,所以当我们需要使用新特性时可以使用:
- 转译器:例如Babel,重写旧的语法结构
- 垫片 Polyfills:更新/添加新的内建函数,没有语法更改所以转译器会失效。
React状态管理
如果不借助其他工具,想要管理组件树中的状态,可以在组件树根部存储状态。防止状态数据分散在多个组件中不便于追踪和修改。
- 沿着组件树向下发送状态。尽量写纯组件(即不含状态的函数组件),通过属性获取状态。
- 沿组件树向上发送交互:向下传递操作函数,将属性传递给亲组件
使用Ref
使用useRef,在JSX标签上添加ref属性,就可直接获取到DOM元素进行操作。
自定义钩子
对于重复的操作可以提取出来定义为函数。
更多钩子
useEffect
对于副作用,如果直接写入函数组件会阻塞渲染。副作用就是函数组件除了返回UI以外所做的其他事情。
useEffect(()⇒fn(),[])
通过依赖数组控制执行副作用的时机。
- 监听数组内变量,发生变化就调用
- 空数组:首次渲染后调用
- useEffect返回的函数将在把组件从组件树上移除时调用,做清理工作
渲染→useEffect
useMemo、useCallback
对于非原始值的数据作为依赖,每次渲染不管是否变化都是新的值。可以在函数组件外定义。对于必须在组件作用域内定义的值,每次渲染都都会重新创建实例,如果useEffect要依赖该值,那么不管该值有没有变化,每次渲染都会执行。
这个时候需要用useMemo定义。useMemo(()⇒fn(),[])
调用一个函数,保存得到的结果;如果使用相同的输入调用函数,返回缓存的值。
对于函数,使用useCallback备忘。
useLayoutEffect
执行时间:渲染→调用useLayoutEffect→浏览器绘制,即把组件元素加入DOM中→调用useEffect
使用useLayoutEffect对浏览器绘制操作。
钩子使用规则
-
钩子只在组件的作用域外运行
-
钩子只在顶层代码中调用
调用钩子后,React会在数组中保存钩子的值,所以不能在条件语句中使用钩子,否则索引会变。
-
在钩子内部进行条件判断,异步操作
useReducer
const [ ①state , ②dispatch ] = useReducer(③reducer,initState)
reducer是一个默认接收当前状态的纯函数,还可接收通过dispatch传递的参数操作当前状态,返回一个新状态。dispatch用于调用reducer更新状态。
当对数据更新需要复杂的操作时或者对复杂的状态进行更新时,可以使用useReducer代替useState。
提升组件性能
提升组件性能:
- 避免不必要的渲染
- 减少渲染传播的时间:避免非必要渲染:memo、useMemo、useCallback
对于纯组件,可以使用memo()包装组件,创建只在属性有变化时渲染的组件。
import React, { useState, memo } from "react";
const Cat = ({ name }) => {
console.log(`rendering ${name}`);
return <p>{name}</p>;
};
const PureCat = memo(Cat);
但如果增加了函数属性,memo就会失效,需要定义更具体的规则指明何时重新渲染组件:
const RenderCatOnce = memo(Cat, () => true);
const AlwaysRenderCat = memo(Cat, () => false);
传给memo的第二个参数是一个断言(只返回布尔值)。返回false则重新渲染,true则不重新渲染。
也可通过useCallback备忘函数属性,useMemo备忘对象属性。
处理数据
发起http请求
发起http请求需要处理三种状态:data、error、loading。修改data,渲染出错error详情以及在请求处理待定状态时,显示”loading“信息。
function GitHubUser({ login }) {
const [data, setData] = useState();
const [error, setError] = useState();
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!login) return;
setLoading(true);
fetch(`https://api.github.com/users/${login}`)
.then(data => data.json())
.then(setData)
.then(() => setLoading(false))
.catch(setError);
}, [login]);
if (loading) return <h1>loading...</h1>;
if (error)
return <pre>{JSON.stringify(error, null, 2)}</pre>;
if (!data) return null;
return (
<div className="githubUser">
<img
src={data.avatar_url}
alt={data.login}
style={{ width: 200 }}
/>
<div>
<h1>{data.login}</h1>
{data.name && <p>{data.name}</p>}
{data.location && <p>{data.location}</p>}
</div>
</div>
);
}
自定义useFetch钩子
export function useFetch(uri) {
const [data, setData] = useState();
const [error, setError] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!uri) return;
fetch(uri)
.then(data => data.json())
.then(setData)
.then(() => setLoading(false))
.catch(setError);
}, [uri]);
return {
loading,
data,
error
};
}
创建Fetch组件
由于渲染部分的逻辑是一样的,可以将渲染的部分也作为参数创建一个可以复用的组件。
function Fetch({
uri,
renderSuccess,
loadingFallback = <p>loading...</p>,
renderError = error => (
<pre>{JSON.stringify(error, null, 2)}</pre>
)
}) {
const { loading, data, error } = useFetch(uri);
if (loading) return loadingFallback;
if (error) return renderError(error);
if (data) return renderSuccess({ data });
}
渲染属性
- 作为属性传入的组件,在满足条件时被渲染
- 返回组件的函数属性
可以提高组件的可重用性,把复杂机制和单调的样板代码抽离出来。
function List({ data = [], renderItem, renderEmpty }) {
return !data.length ? (
renderEmpty
) : (
<ul>
{data.map((item, i) => (
<li key={i}>{renderItem(item)}</li>
))}
</ul>
);
}
传入如何渲染的函数和data为empty时代替渲染的组件。这个List组件就可重用。
虚拟化
对于大型数据,我们不应该全部渲染,应该只在窗口和窗口上下渲染部分数据。随着用户滚动屏幕,消除用户已经看到的结果。
不用造轮子,可以使用虚拟化列表组件react-window等实现。
import React from "react";
import { FixedSizeList } from "react-window";
import faker from "faker";
const bigList = [...Array(5000)].map(() => ({
name: faker.name.findName(),
email: faker.internet.email(),
avatar: faker.internet.avatar()
}));
export default function App() {
const renderRow = ({ index, style }) => (
<div style={{ ...style, ...{ display: "flex" } }}>
<img
src={bigList[index].avatar}
alt={bigList[index].name}
width={50}
/>
<p>
{bigList[index].name} - {bigList[index].email}
</p>
</div>
);
return (
<FixedSizeList
height={window.innerHeight}
width={window.innerWidth - 20}
itemCount={bigList.length}
itemSize={50}
>
{renderRow}
</FixedSizeList>
);
}