2023 的 Flag 还没有立下来,这一年已经过去了。给自己的 2024 先定一些小目标吧(试图让时间流逝的慢一些)。
-
整理一波收藏,把过去收藏的一些内容好好整理一下
-
写一到两篇关于 Turbo 和 Lerna 的文章
-
Github 好久没更新了,把已经过时的项目或者代码更新一下,如果有新的内容再提交一些
-
提交 48 个 Pr
-
继续之前的算法,今年冲到 300
-
写一个库
经常写博客的地方,会时常记录一些学习笔记、技术博客或者踩坑历程。
本文对 16.8 版本之后 React 发布的新特性 Hooks 进行了详细讲解,并对一些常用的 Hooks 进行代码演示,希望可以对需要的朋友提供些帮助。
本文已收录在 Github
: https://github.com/beichensky/Blog 中,欢迎 Star!
Hooks
是 React v16.7.0-alpha
中加入的新特性。它可以让你在 class
以外使用 state
和其他 React
特性。
本文就是演示各种 Hooks API
的使用方式,对于内部的原理这里就不做详细说明。
Foo.js
import React, { useState } from 'react';
function Foo() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Foo;
useState
就是一个 Hook
,可以在我们不使用 class
组件的情况下,拥有自身的 state
,并且可以通过修改 state
来控制 UI
的展示。
const [state, setState] = useState(initialState)
initialState
,可以是数字,字符串等,也可以是对象或者数组。state
变量,setState
修改 state
值的方法。setState
的异同点:setState
,数据只改变一次。setState
是合并,而函数组件中的 setState
是替换。之前想要使用组件内部的状态,必须使用 class
组件,例如:
Foo.js
import React, { Component } from 'react';
export default class Foo extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
而现在,我们使用函数式组件也可以实现一样的功能了。也就意味着函数式组件内部也可以使用 state
了。
Foo.js
import React, { useState } from 'react';
function Foo() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Foo;
创建初始状态是比较昂贵的,所以我们可以在使用 useState
API 时,传入一个函数,就可以避免重新创建忽略的初始状态。
普通的方式:
// 直接传入一个值,在每次 render 时都会执行 createRows 函数获取返回值
const [rows, setRows] = useState(createRows(props.count));
优化后的方式(推荐):
// createRows 只会被执行一次
const [rows, setRows] = useState(() => createRows(props.count));
之前很多具有副作用的操作,例如网络请求,修改 UI 等,一般都是在 class
组件的 componentDidMount
或者 componentDidUpdate
等生命周期中进行操作。而在函数组件中是没有这些生命周期的概念的,只能 return
想要渲染的元素。
但是现在,在函数组件中也有执行副作用操作的地方了,就是使用 useEffect
函数。
useEffect(() => { doSomething });
两个参数:
第一个是一个函数,是在第一次渲染以及之后更新渲染之后会进行的副作用。
第二个参数是可选的,是一个数组,数组中存放的是第一个函数中使用的某些副作用属性。用来优化 useEffect
effect
使用的任何值。 否则,您的代码将引用先前渲染中的旧值。effect
并仅将其清理一次(在装载和卸载时),则可以将空数组([])作为第二个参数传递。 这告诉 React
你的 effect
不依赖于来自 props
或 state
的任何值,所以它永远不需要重新运行。虽然传递 [] 更接近熟悉的
componentDidMount
和componentWillUnmount
执行规则,但我们建议不要将它作为一种习惯,因为它经常会导致错误。
假如此时我们有一个需求,让 document
的 title
与 Foo
组件中的 count
次数保持一致。
使用 类组件:
Foo.js
import React, { Component } from 'react';
export default class Foo extends Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${ this.state.count } times`;
}
componentDidUpdate() {
document.title = `You clicked ${ this.state.count } times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
而现在在函数组件中也可以进行副作用操作了。
Foo.js
import React, { useState, useEffect } from 'react';
function Foo() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
// 类似于 componentDidMount 和 componentDidUpdate:
useEffect(() => {
// 使用浏览器API更新文档标题
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Foo;
不仅如此,我们可以使用 useEffect 执行多个副作用(可以使用一个 useEffect 执行多个副作用,也可以分开执行)
useEffect(() => {
// 使用浏览器API更新文档标题
document.title = `You clicked ${count} times`;
});
const handleClick = () => {
console.log('鼠标点击');
}
useEffect(() => {
// 给 window 绑定点击事件
window.addEventListener('click', handleClick);
});
现在看来功能差不多了。但是在使用类组件时,我们一般会在
componentWillMount
生命周期中进行移除注册的事件等操作。那么在函数组件中又该如何操作呢?
useEffect(() => {
// 使用浏览器API更新文档标题
document.title = `You clicked ${count} times`;
});
const handleClick = () => {
console.log('鼠标点击');
}
useEffect(() => {
// 给 window 绑定点击事件
window.addEventListener('click', handleClick);
return () => {
// 给 window 移除点击事件
window.removeEventListener('click', handleClick);
}
});
可以看到,我们传入的第一个参数,可以 return
一个函数出去,在组件被销毁时,会自动执行这个函数。
上面我们一直使用的都是 useEffect
中的第一个参数,传入了一个函数。那么 useEffect
的第二个参数呢?
useEffect
的第二个参数是一个数组,里面放入在 useEffect
使用到的 state
值,可以用作优化,只有当数组中 state
值发生变化时,才会执行这个 useEffect
。
useEffect(() => {
// 使用浏览器API更新文档标题
document.title = `You clicked ${count} times`;
}, [ count ]);
Tip:如果想模拟 class 组件的行为,只在 componetDidMount 时执行副作用,在 componentDidUpdate 时不执行,那么
useEffect
的第二个参数传一个 [] 即可。(但是不建议这么做,可能会由于疏漏出现错误)
const value = useContext(MyContext);
接受上下文对象(从中 React.createContext
返回的值)并返回该上下文的当前上下文值。当前上下文值由树中调用组件上方 value
最近的 prop
确定 <MyContext.Provider>
。
useContext(MyContext)
则相当于 static contextType = MyContext
在类中,或者 <MyContext.Consumer>
。
在 App.js
文件中创建一个 context
,并将 context
传递给 Foo
子组件
App.js
import React, { createContext } from 'react';
import Foo from './Foo';
import './App.css';
export const ThemeContext = createContext(null);
export default () => {
return (
<ThemeContext.Provider value="light">
<Foo />
</ThemeContext.Provider>
)
}
在 Foo
组件中,使用 useContext
API 可以获取到传入的 context
值
Foo.js
import React, { useContext } from 'react';
import { ThemeContext } from './App';
export default () => {
const context = useContext(ThemeContext);
return (
<div>Foo 组件:当前 theme 是:{ context }</div>
)
}
useContext
必须是上下文对象本身的参数:
useContext(MyContext)
useContext(MyContext.Consumer)
useContext(MyContext.Provider)
useContext(MyContext) 只允许您阅读上下文并订阅其更改。您仍然需要 <MyContext.Provider> 在树中使用以上内容来为此上下文提供值。
const [state, dispatch] = useReducer(reducer, initialArg, init);
useState
的替代方案。 接受类型为 (state, action) => newState 的reducer
,并返回与 dispatch
方法配对的当前状态。
当你涉及多个子值的复杂
state
(状态) 逻辑时,useReducer
通常优于useState
。
Foo.js
import React, { useReducer } from 'react';
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
export default () => {
// 使用 useReducer 函数创建状态 state 以及更新状态的 dispatch 函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<br />
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
还可以惰性地创建初始状态。为此,你可以将init函数作为第三个参数传递。初始状态将设置为 init(initialArg)
。
它允许你提取用于计算 reducer
外部的初始状态的逻辑。这对于稍后重置状态以响应操作也很方便:
Foo.js
import React, { useReducer } from 'react';
function init(initialCount) {
return {count: initialCount};
}
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
case 'reset':
return init(action.payload);
default:
throw new Error();
}
}
export default ({initialCount = 0}) => {
const [state, dispatch] = useReducer(reducer, initialCount, init);
return (
<>
Count: {state.count}
<br />
<button
onClick={() => dispatch({type: 'reset', payload: initialCount})}>
Reset
</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
state
状态值结构比较复杂时,使用 useReducer
更有优势。useState
获取的 setState
方法更新数据时是异步的;而使用 useReducer
获取的 dispatch
方法更新数据是同步的。针对第二点区别,我们可以演示一下:
在上面 useState
用法的例子中,我们新增一个 button
:
useState
中的 Foo.js
import React, { useState } from 'react';
function Foo() {
// 声明一个名为“count”的新状态变量
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
<button onClick={() => {
setCount(count + 1);
setCount(count + 1);
}}>
测试能否连加两次
</button>
</div>
);
}
export default Foo;
点击 测试能否连加两次 按钮,会发现,点击一次,
count
还是只增加了 1,由此可见,useState
确实是 异步 更新数据;
在上面 useReducer
用法的例子中,我们新增一个 button
:
useReducer
中的 Foo.js
import React, { useReducer } from 'react';
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
export default () => {
// 使用 useReducer 函数创建状态 state 以及更新状态的 dispatch 函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<br />
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => {
dispatch({type: 'increment'});
dispatch({type: 'increment'});
}}>
测试能否连加两次
</button>
</>
);
}
点击 测试能否连加两次 按钮,会发现,点击一次, count 增加了 2,由此可见,每次 dispatch 一个 action 就会更新一次数据,useReducer 确实是 同步 更新数据;
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
返回值 memoizedCallback
是一个 memoized
回调。传递内联回调和一系列依赖项。useCallback
将返回一个回忆的memoized版本,该版本仅在其中一个依赖项发生更改时才会更改。
当将回调传递给依赖于引用相等性的优化子组件以防止不必要的渲染(例如 shouldComponentUpdate
)时,这非常有用。
配合子组件使用 PureComponent
、memo
时,可以减少子组件不必要的渲染次数
不使用 useCallback
的情况下给子组件传递函数
Foo.js
import React from 'react';
const Foo = ({ onClick }) => {
console.log('Foo:', 'render');
return <button onClick={onClick}>Foo 组件按钮</button>
}
export default Foo
Bar.js
import React from 'react';
const Bar = ({ onClick }) => {
console.log('Bar:', 'render');
return <button onClick={onClick}>Bar 组件按钮</button>;
};
export default Bar;
App.js
import React, { useState } from 'react';
import Foo from './Foo';
import Bar from './Bar';
function App() {
const [count, setCount] = useState(0);
const fooClick = () => {
console.log('点击了 Foo 组件的按钮');
};
const barClick = () => {
console.log('点击了 Bar 组件的按钮');
};
return (
<div style={{ padding: 50 }}>
<p>{count}</p>
<Foo onClick={fooClick} />
<br />
<br />
<Bar onClick={barClick} />
<br />
<br />
<button onClick={() => setCount(count + 1)}>count increment</button>
</div>
);
}
export default App;
此时我们点击上面任意 count increment 按钮,都会看到控制台打印了两条输出, Foo 和 Bar 组件都会被重新渲染。但其实在我们当前的逻辑中,Foo 和 Bar 组件根本不需要重新 render
现在我们使用 useCallback
进行优化
使用 useCallback
优化后的版本
Foo.js
import React from 'react';
const Foo = ({ onClick }) => {
console.log('Foo:', 'render');
return <button onClick={onClick}>Foo 组件按钮</button>;
};
export default React.memo(Foo);
Bar.js
import React from 'react';
const Bar = ({ onClick }) => {
console.log('Bar:', 'render');
return <button onClick={onClick}>Bar 组件按钮</button>;
};
export default React.memo(Bar);
App.js
import React, { useCallback, useState } from 'react';
import Foo from './Foo';
import Bar from './Bar';
function App() {
const [count, setCount] = useState(0);
const fooClick = useCallback(() => {
console.log('点击了 Foo 组件的按钮');
}, []);
const barClick = useCallback(() => {
console.log('点击了 Bar 组件的按钮');
}, []);
return (
<div style={{ padding: 50 }}>
<p>{count}</p>
<Foo onClick={fooClick} />
<br />
<br />
<Bar onClick={barClick} />
<br />
<br />
<button onClick={() => setCount(count + 1)}>count increment</button>
</div>
);
}
export default App;
此时点击 count increment 按钮,可以看到控制台没有任何输出。
如果将 useCallback
或者 React.memo
移除,可以看到对应的组件又会出现不必要的 render
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个memoized值。
传递“创建”函数和依赖项数组。useMemo
只会在其中一个依赖项发生更改时重新计算 memoized
值。此优化有助于避免在每个渲染上进行昂贵的计算。
useMemo在渲染过程中传递的函数会运行。不要做那些在渲染时通常不会做的事情。例如,副作用属于 useEffect,而不是 useMemo。
Vue
的 computed
,可以根据依赖变化自动重新计算props
发生变化时,App 组件状态会改变,重新渲染。此时 Foo 组件 和 Bar 组件 也都会重新渲染。其实这种情况是比较浪费资源的,现在我们就可以使用 useMemo
进行优化,Foo 组件用到的 props
变化时,只有 Foo 组件进行 render
,而 Bar 却不会重新渲染。例子:
Foo.js
import React from 'react';
export default ({ text }) => {
console.log('Foo:', 'render');
return <div>Foo 组件:{ text }</div>
}
Bar.js
import React from 'react';
export default ({ text }) => {
console.log('Bar:', 'render');
return <div>Bar 组件:{ text }</div>
}
App.js
import React, { useState } from 'react';
import Foo from './Foo';
import Bar from './Bar';
export default () => {
const [a, setA] = useState('foo');
const [b, setB] = useState('bar');
return (
<div>
<Foo text={ a } />
<Bar text={ b } />
<br />
<button onClick={ () => setA('修改后的 Foo') }>修改传给 Foo 的属性</button>
<button onClick={ () => setB('修改后的 Bar') }>修改传给 Bar 的属性</button>
</div>
)
}
此时我们点击上面任意一个按钮,都会看到控制台打印了两条输出, A 和 B 组件都会被重新渲染。
现在我们使用 useMemo
进行优化
App.js
import React, { useState, useMemo } from 'react';
import Foo from './Foo';
import Bar from './Bar';
import './App.css';
export default () => {
const [a, setA] = useState('foo');
const [b, setB] = useState('bar');
+ const foo = useMemo(() => <Foo text={ a } />, [a]);
+ const bar = useMemo(() => <Bar text={ b } />, [b]);
return (
<div>
+ {/* <Foo text={ a } />
+ <Bar text={ b } /> */}
+ { foo }
+ { bar }
<br />
<button onClick={ () => setA('修改后的 Foo') }>修改传给 Foo 的属性</button>
<button onClick={ () => setB('修改后的 Bar') }>修改传给 Bar 的属性</button>
</div>
)
}
此时我们点击不同的按钮,控制台都只会打印一条输出,改变 a 或者 b,A 和 B 组件都只有一个会重新渲染。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
const refContainer = useRef(initialValue);
useRef
返回一个可变的 ref
对象,其 .current
属性被初始化为传递的参数(initialValue)。返回的对象将存留在整个组件的生命周期中。
useRef
就像一个“盒子”,可以在其 .current
财产中保持一个可变的价值。useRef Hooks
不仅适用于 DOM 引用。 “ref” 对象是一个通用容器,其 current
属性是可变的,可以保存任何值(可以是元素、对象、基本类型、甚至函数),类似于类上的实例属性。useRef
具有闭包穿透的能力注意:useRef() 比 ref 属性更有用。与在类中使用 instance(实例) 字段的方式类似,它可以 方便地保留任何可变值。
注意,内容更改时useRef 不会通知您。变异.current属性不会导致重新渲染。如果要在 React 将引用附加或分离到DOM节点时运行某些代码,则可能需要使用回调引用。
下面这个例子中展示了可以在 useRef()
生成的 ref
的 current
中存入元素、字符串
Example.js
import React, { useRef, useState, useEffect } from 'react';
export default () => {
// 使用 useRef 创建 inputEl
const inputEl = useRef(null);
const [text, updateText] = useState('');
// 使用 useRef 创建 textRef
const textRef = useRef();
useEffect(() => {
// 将 text 值存入 textRef.current 中
textRef.current = text;
console.log('textRef.current:', textRef.current);
});
const onButtonClick = () => {
// `current` points to the mounted text input element
inputEl.current.value = "Hello, useRef";
};
return (
<>
{/* 保存 input 的 ref 到 inputEl */}
<input ref={ inputEl } type="text" />
<button onClick={ onButtonClick }>在 input 上展示文字</button>
<br />
<br />
<input value={text} onChange={e => updateText(e.target.value)} />
</>
);
}
点击 在 input 上展示文字 按钮,就可以看到第一个 input 上出现 Hello, useRef
;在第二个 input 中输入内容,可以看到控制台打印出对应的内容。
useLayoutEffect(() => { doSomething });
与 useEffect Hooks
类似,都是执行副作用操作。但是它是在所有 DOM 更新完成后触发。可以用来执行一些与布局相关的副作用,比如获取 DOM 元素宽高,窗口滚动距离等等。
进行副作用操作时尽量优先选择 useEffect,以免阻止视觉更新。与 DOM 无关的副作用操作请使用
useEffect
。
用法与 useEffect
类似。但会在 useEffect
之前执行
Foo.js
import React, { useRef, useState, useLayoutEffect } from 'react';
export default () => {
const divRef = useRef(null);
const [height, setHeight] = useState(100);
useLayoutEffect(() => {
// DOM 更新完成后打印出 div 的高度
console.log('useLayoutEffect: ', divRef.current.clientHeight);
})
return <>
<div ref={ divRef } style={{ background: 'red', height: height }}>Hello</div>
<button onClick={ () => setHeight(height + 50) }>改变 div 高度</button>
</>
}
在函数组件中,没有组件实例,所以无法像类组件中,通过绑定子组件的实例调用子组件中的状态或者方法。
那么在函数组件中,如何在父组件调用子组件的状态或者方法呢?答案就是使用 useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
第一个参数是 ref
值,可以通过属性传入,也可以配合 forwardRef
使用
第二个参数是一个函数,返回一个对象,对象中的属性都会被挂载到第一个参数 ref
的 current
属性上
第三个参数是依赖的元素集合,同 useEffect
、useCallback
、useMemo
,当依赖发生变化时,第二个参数会重新执行,重新挂载到第一个参数的 current
属性上
注意:
createHandle
重复执行hook
中,对于同一个 ref
,只能使用一次 useImperativeHandle
,多次的话,后面执行的 useImperativeHandle
的 createHandle
返回值会替换掉前面执行的 useImperativeHandle
的 createHandle
返回值Foo.js
import React, { useState, useImperativeHandle, useCallback } from 'react';
const Foo = ({ actionRef }) => {
const [value, setValue] = useState('');
/**
* 随机修改 value 值的函数
*/
const randomValue = useCallback(() => {
setValue(Math.round(Math.random() * 100) + '');
}, []);
/**
* 提交函数
*/
const submit = useCallback(() => {
if (value) {
alert(`提交成功,用户名为:${value}`);
} else {
alert('请输入用户名!');
}
}, [value]);
useImperativeHandle(
actionRef,
() => {
return {
randomValue,
submit,
};
},
[randomValue, submit]
);
/* !! 返回多个属性要按照上面这种写法,不能像下面这样使用多个 useImperativeHandle
useImperativeHandle(actionRef, () => {
return {
submit,
}
}, [submit])
useImperativeHandle(actionRef, () => {
return {
randomValue
}
}, [randomValue])
*/
return (
<div className="box">
<h2>函数组件</h2>
<section>
<label>用户名:</label>
<input
value={value}
placeholder="请输入用户名"
onChange={e => setValue(e.target.value)}
/>
</section>
<br />
</div>
);
};
export default Foo;
App.js
import React, { useRef } from 'react';
import Foo from './Foo'
const App = () => {
const childRef = useRef();
return (
<div>
<Foo actionRef={childRef} />
<button onClick={() => childRef.current.submit()}>调用子组件的提交函数</button>
<br />
<br />
<button onClick={() => childRef.current.randomValue()}>
随机修改子组件的 input 值
</button>
</div>
);
};
这里我们就仿照官方的 useReducer
做一个自定义的 Hooks
。
在 src
目录下新建一个 useReducer.js
文件:
useReducer.js
import React, { useState } from 'react';
function useReducer(reducer, initialState) {
const [state, setState] = useState(initialState);
function dispatch(action) {
const nextState = reducer(state, action);
setState(nextState);
}
return [state, dispatch];
}
Tip: Hooks 不仅可以在函数组件中使用,也可以在别的 Hooks 中进行使用。
好了,自定义 useReducer
编写完成了,下面我们看一下能不能正常使用呢?
改写 Foo
组件
Example.js
import React from 'react';
// 从自定义 useReducer 中引入
import useReducer from './useReducer';
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
export default () => {
// 使用 useReducer 函数创建状态 state 以及更新状态的 dispatch 函数
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<br />
<button onClick={() => dispatch({type: 'increment'})}>+</button>
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
</>
);
}
不要从常规 JavaScript
函数调用 Hooks
;
不要在循环,条件或嵌套函数中调用 Hooks
;
必须在组件的顶层调用 Hooks
;
可以从 React
功能组件调用 Hooks
;
可以从自定义 Hooks
中调用 Hooks
;
自定义 Hooks
必须使用 use
开头,这是一种约定;
根据上一段所写,在 React
中使用 Hooks
需要遵循一些特定规则。但是在代码的编写过程中,可能会忽略掉这些使用规则,从而导致出现一些不可控的错误。这种情况下,我们就可以使用 React 提供的 ESLint 插件:eslint-plugin-react-hooks。下面我们就看看如何使用吧。
$ npm install eslint-plugin-react-hooks --save
// Your ESLint configuration
// "react-hooks/rules-of-hooks": "error", // Checks rules of Hooks
// "react-hooks/exhaustive-deps": "warn" // Checks effect dependencies
{
"plugins": [
"react-hooks"
],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
本文已收录在 Github: https://github.com/beichensky/Blog 中,欢迎 Star,欢迎 Follow!
在 React 18 新特性(一):自动批量更新 一文中提到:在 React 新版本中,更新会有优先级的顺序。
那如果希望更新时进行低优先级的处理,应该如何做呢,就是今天讲到的主题:渐变更新。
如果还不知道如何搭建 React18 的体验环境,可以先查看这篇文章:使用 Vite 尝鲜 React 18
startTransition
接受一个回调函数,可以将放入其中的 setState
更新推迟
允许组件将速度较慢的更新延迟渲染,以便能够立即渲染更重要的更新
先来看一个例子,在使用谷歌或者百度搜索时,都会遇到如下的场景:
这里的展示分为两部分
一部分是输入框中的搜索内容
另一部分是展示的联想内容。
从用户的角度进行分析:
输入框中的内容是需要即时更新的
而联想出来的内容是需要进行请求或者加载的,甚至于最开始的时候联想的不准确,用不到。所以用户可以接受这部分内容有一定延迟。
那在这种情况下,用户的输入就是高优先级操作,而联想区域的变化就属于低优先级的操作。
我们写一段代码来实现一下这个搜索框。
App.jsx
import React, { useEffect, useState, startTransition } from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [value, setValue] = useState('');
const [keywords, setKeywords] = useState([]);
useEffect(() => {
const getList = () => {
const list = value
? Array.from({ length: 10000 }, (_, index) => ({
id: index,
keyword: `${value} -- ${index}`,
}))
: [];
return Promise.resolve(list);
};
getList().then(res => setKeywords(res));
}, [value]);
return (
<>
<input value={value} onChange={e => setValue(e.target.value)} />
<ul>
{keywords.map(({ id, keyword }) => (
<li key={id}>{keyword}</li>
))}
</ul>
</>
);
};
// 使用 react 18 新的并发模式写法进行 dom render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
// legacy 旧模式
// ReactDOM.render(<App />, document.getElementById('root')!)
然后我们先看一下现在的效果(这里暂时不讨论防抖或者节流):
可以看到,不仅联想区域的内容加载缓慢,甚至用户的交互内容也反应迟钝。
既然刚才说到了低优先级更新,那么此时,我们是否可以让联想区域的内容低优更新,以避免抢占用户操作的更新呢?
接下来主角登场,使用 startTransition
对代码进行改造。
App.jsx
import React, { useEffect, useState, startTransition } from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [value, setValue] = useState('');
const [keywords, setKeywords] = useState([]);
useEffect(() => {
const getList = () => {
const list = value
? Array.from({ length: 10000 }, (_, index) => ({
id: index,
keyword: `${value} -- ${index}`,
}))
: [];
return Promise.resolve(list);
};
- //getList().then(res => setKeywords(res));
// 仅仅只是将 setKeywords 用 startTransition 包裹一层,即可启用渐变更新
+ getList().then(res => startTransition(() => setKeywords(res)));
}, [value]);
return (
<>
<input value={value} onChange={e => setValue(e.target.value)} />
<ul>
{keywords.map(({ id, keyword }) => (
<li key={id}>{keyword}</li>
))}
</ul>
</>
);
};
// 使用 react 18 新的并发模式写法进行 dom render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
// legacy 旧模式
// ReactDOM.render(<App />, document.getElementById('root')!)
重新执行后,看看此时的效果:
可以看到,此时界面的响应速度比之前快了许多。
useDeferredValue
相当于是 startTransition(() => setState(xxx))
的语法糖,在内部会调用一次 setState
,但是此更新的优先级更低
那么我们用 useDeferredValue
改写一下上面的代码,看看是否有哪里不一样呢?
App.jsx
import React, { useEffect, useState, useDeferredValue } from 'react';
import ReactDOM from 'react-dom';
const App = () => {
const [value, setValue] = useState('');
const [keywords, setKeywords] = useState([]);
+ const text = useDeferredValue(value);
useEffect(() => {
const getList = () => {
const list = value
? Array.from({ length: 10000 }, (_, index) => ({
id: index,
keyword: `${value} -- ${index}`,
}))
: [];
return Promise.resolve(list);
};
getList().then(res => setKeywords(res));
// 只是将依赖的值由 value 更新为 text
+ }, [text]);
return (
<>
<input value={value} onChange={e => setValue(e.target.value)} />
<ul>
{keywords.map(({ id, keyword }) => (
<li key={id}>{keyword}</li>
))}
</ul>
</>
);
};
// 使用 react 18 新的并发模式写法进行 dom render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
// legacy 旧模式
// ReactDOM.render(<App />, document.getElementById('root')!)
看看此时界面的响应速度:
可以看到此时的响应速度和使用 startTransition
时相差无几。
还记得在 React 18 新特性(二):Suspense & SuspenseList 一文中使用过的 Suspense
组件以及 User
组件吗?我们在这两个组件的基础上,展示一下 useTransition
的用法和特性。
假设我们目前需要使用 Suspense
来包裹 User
组件,此时 User
组件内部会有网络请求等耗时操作。点击按钮,会触发 User
组件的更新,重新进行耗时操作获取数据
App.jsx
import React, { Suspense, useState } from 'react';
import ReactDOM from 'react-dom';
// 对 promise 进行一层封装
function wrapPromise(promise) {
let status = 'pending';
let result;
let suspender = promise.then(
r => {
status = 'success';
result = r;
},
e => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
},
};
}
// 网络请求,获取 user 数据
const requestUser = id =>
new Promise(resolve =>
setTimeout(() => resolve({ id, name: `用户${id}`, age: 10 + id }), id * 100)
);
// User 组件
const User = props => {
const user = props.resource.read();
return <div>当前用户是: {user.name}</div>;
};
// 通过 id 获取对应 resource
const getResource = id => wrapPromise(requestUser(id));
const App = () => {
const [resource, setResource] = useState(getResource(10));
return (
<>
<Suspense fallback={<div>Loading...</div>}>
<User resource={resource} />
</Suspense>
<button onClick={() => setResource(wrapPromise(requestUser(1)))}>切换用户</button>
</>
);
};
// 使用 react 18 新的并发模式写法进行 dom render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
// legacy 旧模式
// ReactDOM.render(<App />, document.getElementById('root')!)
OK,那我们看一下此时的效果哈:
可以看到,第一次加载时,会出现 loading
效果,这是正常的,但是在点击按钮,切换用户时,依然会有 loading
效果的出现,这本来没有问题,但是当请求速度很快时,就会出现闪一下的问题。此时应该不需要 loading
的出现。
这个时候,useTransition
就派上用场了。
useTransition
允许组件再切换到下一个界面之前等待内容加载,从而避免出现不必要的加载状态
允许组件将速度较慢的数据获取更新推迟到随后渲染(低优先级更新),以便能够立即渲染更重要的更新
useTransition
返回包含两个元素的数组:
isPending: Boolean
,通知我们是否正在等待过渡效果的完成startTransition: Function
,用它来包裹需要延迟更新的状态使用 useTransition
中返回的 startTransition
包裹需要更新的 setState,就会降低更新的优先级,并且会对界面进行缓冲,等待下一个界面准备就绪后直接进行更新。
App.jsx
import React, { Suspense, useState, useTransition } from 'react';
import ReactDOM from 'react-dom';
// 对 promise 进行一层封装
function wrapPromise(promise) {
let status = 'pending';
let result;
let suspender = promise.then(
r => {
status = 'success';
result = r;
},
e => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
},
};
}
// 网络请求,获取 user 数据
const requestUser = id =>
new Promise(resolve =>
setTimeout(() => resolve({ id, name: `用户${id}`, age: 10 + id }), id * 100)
);
// User 组件
const User = props => {
const user = props.resource.read();
return <div>当前用户是: {user.name}</div>;
};
// 通过 id 获取对应 resource
const getResource = id => wrapPromise(requestUser(id));
const App = () => {
const [resource, setResource] = useState(getResource(10));
+ const [isPending, startTransition] = useTransition();
return (
<>
<Suspense fallback={<div>Loading...</div>}>
<User resource={resource} />
</Suspense>
+ <button onClick={() => startTransition(() => setResource(wrapPromise(requestUser(1))))}>
切换用户
</button>
</>
);
};
// 使用 react 18 新的并发模式写法进行 dom render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
// legacy 旧模式
// ReactDOM.render(<App />, document.getElementById('root')!)
可以看到,加载状态的 loading
就不会出现了,闪一下的情况消失了:
那么问题来了,如果耗时操作确实会花费很久的时间,没有 loading
的话,对于用户来说就没有任何的反馈了呀。
别急,这个时候第一个元素 isPending
就可以用起来啦:
App.jsx
import React, { Suspense, useState, useTransition } from 'react';
import ReactDOM from 'react-dom';
// 对 promise 进行一层封装
function wrapPromise(promise) {
let status = 'pending';
let result;
let suspender = promise.then(
r => {
status = 'success';
result = r;
},
e => {
status = 'error';
result = e;
}
);
return {
read() {
if (status === 'pending') {
throw suspender;
} else if (status === 'error') {
throw result;
} else if (status === 'success') {
return result;
}
},
};
}
// 网络请求,获取 user 数据
const requestUser = id =>
new Promise(resolve =>
setTimeout(() => resolve({ id, name: `用户${id}`, age: 10 + id }), id * 100)
);
// User 组件
const User = props => {
const user = props.resource.read();
return <div>当前用户是: {user.name}</div>;
};
// 通过 id 获取对应 resource
const getResource = id => wrapPromise(requestUser(id));
const App = () => {
const [resource, setResource] = useState(getResource(10));
const [isPending, startTransition] = useTransition();
return (
<>
<Suspense fallback={<div>Loading...</div>}>
<User resource={resource} />
</Suspense>
+ {isPending ? <div>Loading</div> : null}
<button
onClick={() => startTransition(() => setResource(wrapPromise(requestUser(20))))}
>
切换用户
</button>
</>
);
};
// 使用 react 18 新的并发模式写法进行 dom render
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
// legacy 旧模式
// ReactDOM.render(<App />, document.getElementById('root')!)
此时点击按钮切换用户,会有 2s 左右的等待时间,就可以展示出 loading
状态,用来提示用户:
所以,在使用 useTransition
时,一定要注意场景:
在明确知道耗时操作速度极快的情况下,可以直接使用返回值中的 startTransition
如果不能保证响应速度,还是需要使用 isPending
进行过渡状态的判断和展示
如果对于更新的优先级有较高的要求,可以不使用 useTransition
好啦,关于 startTransition
、useDeferredValue
、useTransition
的用法和使用场景都已经介绍完了。
所有的代码均已在文中贴出。
文中有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star。
本文详细介绍了 Fiber 的**以及代码实现过程。对于理解和入门 Fiber 架构有一定帮助。
不过仅限于模拟实现,更多细节仍需参考官方源码进行学习。
代码实现已经进行整理:源码地址
本文已收录在 Github: https://github.com/beichensky/Blog 中,欢迎 Star!
fiber
渲染过程
之前的树形结构渲染过程:
diff
策略所以下图的 D 节点所在 DOM 树会被删除重新创建,F 节点也会被删除后,重新创建
下图的 D 节点所在 DOM 树会被删除后重新创建
开发者可以通过 key prop
来暗示哪些子元素在不同的渲染下能保持稳定
key
或使用 index
作为 key
时,下图会进行 F、B、C、D 节点的更新,并新增 E 节点key
值时,只会新增 F 节点添加到 B 节点之前,B、C、D、E 节点都不会发生更新操作fiber
**将树形结构转化成链表结构
Fiber
对象fiber
是一个链表元素对象,包含以下基本属性
fiber
DOM
fiber
fiber
有了这些属性就可以构成一个链表结构,可以通过当前 fiber
找到父 fiber
以及兄弟 fiber
requestIdleCallback
api: window.requestIdleCallback(callback)
作用
当浏览器处于空闲状态时,会调用传入的 callback 函数
callback 会接受一个参数 deadLine
可以通过 deadLine.timeRemaining 判断浏览器是否处于空闲状态
fiber
定义 perforUnitOfWork
函数用来对 fiber
进行操作和更新
perforUnitOfWork
有两个作用
fiber
任务fiber
任务function perforUnitOfWork(fiber) {
if (!fiber) {
return null;
}
// 1、执行 fiber 操作
const { type } = fiber;
if (typeof type === "function") {
type.prototype && type.prototype.isReactComponent
? updateClassComponent(fiber)
: updateFunctionComponent(fiber);
} else {
updateHostComponent(fiber);
}
/**
* 2、返回下一个 fiber
* 优先返回子节点 fiber
* 如果没有子 fiber,返回兄弟节点 fiber
* 没有兄弟节点 fiber,返回父节点的 fiber
*/
if (fiber.child) {
return fiber.child;
}
while (fiber) {
if (fiber.sibling) {
return fiber.sibling;
}
fiber = fiber.return;
}
return null;
}
updateHostComponent function
更新原始 DOM 标签
fiber
设置 node
属性function updateHostComponent(fiber) {
if (!fiber.node) {
fiber.node = createNode(fiber);
}
if (fiber.props && fiber.props.children) {
reconcileChildren(fiber, fiber.props.children);
}
}
updateClassComponent function
更新类组件
render
render
产生的虚拟 DOM 和类组件的 fiber
生成链表结构function updateClassComponent(fiber) {
const { type: Type, props } = fiber;
const vNode = new Type(props).render();
reconcileChildren(fiber, [vNode]);
}
updateFunctionComponent function
更新函数式组件
fiber
生成链表结构function updateFunctionComponent(fiber) {
const { type, props } = fiber;
const vNode = type(props);
reconcileChildren(fiber, [vNode]);
}
createNode
创建真实 DOM
function createNode(fiber) {
let node = null;
const { type, props } = fiber;
if (type === "TEXT") {
node = document.createTextNode("");
} else if (typeof type === "string") {
node = document.createElement(type);
} else {
node = document.createDocumentFragment();
}
updateNode(node, props);
return node;
}
updateNode
更新 DOM 属性
function updateNode(node, nextProps) {
if (!nextProps) {
return;
}
Object.keys(nextProps)
.filter(propName => propName !== "children")
.forEach(propName => {
// 设置事件监听
if (propName.startsWith("on")) {
const eventName = propName.slice(2).toLowerCase();
node.addEventListener(eventName, nextProps[propName]);
} else {
// 设置节点属性
node[propName] = nextProps[propName];
}
});
}
reconcileChildren
协调子元素
function reconcileChildren(returnFiber, children) {
let prevFiber = null;
// 循环子元素,生成链表结构
for (let i = 0; i < children.length; i++) {
const child = children[i];
const newFiber = {
type: child.type,
key: child.key,
props: child.props,
node: null,
base: null,
return: returnFiber,
effectTag: "PLACEMENT"
};
if (prevFiber === null) {
returnFiber.child = newFiber;
} else {
prevFiber.sibling = newFiber;
}
prevFiber = newFiber;
}
}
上面只是写了执行 fiber 任务的 perforUnitOfWork 函数,但是还没有调用过,下面我们看看 perforUnitOfWork 函数是什么时候被调用的
workLoop
作用:轮询判断是否需要执行 fiber
操作。
通过 requestIdleCallback
方法判断,在浏览器处于空闲状态时才会继续执行 perforUnitOfWork
函数
// 下一个将被执行的 fiber
let nextUnitWork = null;
// `root fiber`
let wipRoot = null;
function workLoop(deadLine) {
while (nextUnitWork && deadLine.timeRemaining() > 0) {
nextUnitWork = perforUnitOfWork(nextUnitWork);
}
if (nextUnitWork == null && wipRoot) {
commitRoot();
}
window.requestIdleCallback(workLoop);
}
window.requestIdleCallback(workLoop);
commitRoot
:提交 root fiber
任务
function commitRoot() {
commitWorker(wipRoot.child);
wipRoot = null;
}
commitWorker
:更新 fiber
节点
function commitWorker(fiber) {
if (!fiber) {
return;
}
if (fiber.node && fiber.effectTag === "PLACEMENT") {
let parentFiber = fiber.return;
let parentNode = null;
while (parentFiber) {
if (parentFiber.node && parentFiber.node.nodeType !== 11) {
parentNode = parentFiber.node;
break;
}
parentFiber = parentFiber.return;
}
parentNode.appendChild(fiber.node);
}
// 提交子 fiber 任务
commitWorker(fiber.child);
// 提交兄弟 fiber 任务
commitWorker(fiber.sibling);
}
render
渲染函数workLoop 函数中,执行 perforUnitOfWork 方法的条件是 nextUnitWork 存在。
因此我们需要给 nextUnitWork 一个初始值,我们在界面上调用 render 函数的时候可以给 nextUnitWork 和 wipRoot 赋上初始值
render
const render = (vNode, container) => {
wipRoot = {
props: {
children: [vNode]
},
node: container,
base: null,
return: null,
sibling: null
};
nextUnitWork = wipRoot;
};
useState
更新的话:
1、需要重新启动
fiber
任务,所以这里我们新增一个currentRootFiber
用来存储上一次的wipRoot
;2、需要知道当前执行的
fiber
是哪个,所以需要新增一个wipFunctionFible
变量在updateFunctionComponent
的时候赋值3、不同类型的元素进行切换时,需要删除之前的元素节点,因此需要新增一个
deletions
变量用来存储需要删除的fiber
let currentRootFiber = null;
let wipFunctionFible = null;
let deletions = [];
useState
初始化阶段:直接返回初始值和 setState 函数
更新阶段:从上一次的 fiber 中取出之前的 hook
值,为 state
赋上最新的值
setState
函数会给将新的 state
值填充到 hook
的 queue
属性中
函数组件每次更新会重新执行 useState
hook
的 queue
属性中取出最新的 state
值hookIndex
后移,执行下一个 hook
/**
* 函数组件更新 state 的 hook
* @param {*} init
*/
export const useState = init => {
/**
* 获取到上一次的 fiber ,如果存在,则取出上一次的 fiber 中存储的 hooks
* 根据当前执行 fiber 的 hookIndex 找到当前 useState 值的 oldHook
* 如果 oldHook 存在,则使用 oldHook,不存在则使用初始值
*
* 循环 hook 中队列 queue 存储的值,为 state 设置最新的值
*/
const oldFiber = wipFunctionFible.base && wipFunctionFible.base;
const oldHook = oldFiber && oldFiber.hooks[wipFunctionFible.hookIndex];
const hook = oldHook
? { state: oldHook.state, queue: oldHook.queue }
: { state: init, queue: [] };
hook.queue.forEach(i => (hook.state = i));
hook.queue = [];
/**
* 设置 state,将接收到的 aciton push 到队列 queue 中
*
* 为 wipRoot 赋值,并将 wipRoot 赋值给 nextUnitWork,启动 fiber 任务
*/
const setState = action => {
// 若新的 action 和 上一次的 state 相同,则无需更新
if (action === hook.state) {
return;
}
// 不同,则 push 到 queue 中
hook.queue.push(action);
wipRoot = {
props: currentRootFiber.props,
node: currentRootFiber.node,
base: currentRootFiber
};
nextUnitWork = wipRoot;
deletions = [];
};
/**
* 将 hook push 到 wipFunctionFible 的 hooks 属性中,用于下一次的更新操作。
* 并将 wipFunctionFible 的 hookIndex 后移,执行下一个 useState
*/
wipFunctionFible.hooks.push(hook);
wipFunctionFible.hookIndex++;
return [hook.state, setState];
};
updateFunctionComponent
函数在 updateFunctionComponent
中为 fiber 设置 hooks
和 hookIndex
属性,用来在后面操作 useState
时使用
hooks
属性:保存函数组件中的 hook
hookIndex
属性:当前函数组件中执行到某个 hook
的下标
function updateFunctionComponent(fiber) {
fiber.hooks = [];
fiber.hookIndex = 0;
wipFunctionFible = fiber;
const { type, props } = fiber;
const vNode = type(props);
reconcileChildren(fiber, [vNode]);
}
reconcileChildren
函数function reconcileChildren(returnFiber, children) {
let prevFiber = null;
let oldFiber = returnFiber.base && returnFiber.base.child;
// 循环子元素,生成链表结构
for (let i = 0; i < children.length; i++) {
const child = children[i];
let newFiber = null;
// 判断 oldFiber 和 child 是否可以复用
const sameType =
oldFiber &&
child &&
oldFiber.key === child.key &&
oldFiber.type === child.type;
// 1、如果可以复用,则将 oldFiber 设置为 newFiber 的 base 属性,并将 effectTag 设置为 UPDATE
if (sameType) {
newFiber = {
type: child.type,
key: child.key,
props: child.props,
node: oldFiber.node,
base: oldFiber,
return: returnFiber,
effectTag: "UPDATE"
};
}
// 2、如果不能复用,则将 newFiber 的 effectTag 属性设置为 PLACEMENT
if (!sameType && child) {
newFiber = {
type: child.type,
key: child.key,
props: child.props,
node: null,
base: null,
return: returnFiber,
effectTag: "PLACEMENT"
};
}
// 3、如果 oldFiber 存在但是不能复用,则将 oldFiber 的 effectTag 属性设置为 DELETION,
// 并添加到 deletions 数组中
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
// oldFiber 后移,找到兄弟节点
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (prevFiber === null) {
returnFiber.child = newFiber;
} else {
prevFiber.sibling = newFiber;
}
prevFiber = newFiber;
}
}
commitWorker
函数中新增更新和删除操作判断 effectTag
为 PLACEMENT
,执行新增操作
判断 effectTag
为 UPDATE
,则进行属性更新
判断 effectTag
为 DELETION
,则将上一次的 fiber
的 node
节点,从文档中删除
function commitWorker(fiber) {
if (!fiber) {
return;
}
// 执行新增插入操作
if (fiber.node && fiber.effectTag === "PLACEMENT") {
let parentFiber = fiber.return;
let parentNode = null;
// 有可能 parentFiber 是fragment 或者 Provider 等这些组件,是没有 node 属性的,所以要循环向上找到 parentNode
while (parentFiber) {
if (parentFiber.node && parentFiber.node.nodeType !== 11) {
parentNode = parentFiber.node;
break;
}
parentFiber = parentFiber.return;
}
parentNode.appendChild(fiber.node);
} else if (fiber.node && fiber.effectTag === "UPDATE") {
// 执行属性更新操作
updateNode(fiber.node, fiber.base.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
// 执行删除操作
commitDeletion(fiber);
// !!! 这里要 return 掉,不能执行 child 和 sibling,因为 child 和 sibling 的 effectTag 值并没有修改成 DELETION
return;
}
// 提交子 fiber 任务
commitWorker(fiber.child);
// 提交兄弟 fiber 任务
commitWorker(fiber.sibling);
}
commitDeletion
function commitDeletion(fiber) {
// 当前 fiber 上不存在 node,则删除子 fiber 的 node
if (fiber.node) {
fiber.node.remove();
} else {
commitDeletion(fiber.child);
}
}
对比新旧属性,删除旧属性,设置新属性。
function updateNode(node, prevProps, nextProps) {
if (!node || !nextProps || !prevProps) {
return;
}
/**
* 移除上一次的事件监听
* 若当前 nextProps 中不包含上一次的某个属性,则将该属性值置为空
*/
Object.keys(prevProps)
.filter(propName => propName !== "children")
.forEach(propName => {
if (propName.startsWith("on")) {
const eventName = propName.slice(2).toLowerCase();
node.removeEventListener(eventName, prevProps[propName], false);
} else if (!([propName] in nextProps)) {
node[propName] = "";
}
});
// 将新的属性设置到 node 上
Object.keys(nextProps)
.filter(propName => propName !== "children")
.forEach(propName => {
// 设置事件监听
if (propName.startsWith("on")) {
const eventName = propName.slice(2).toLowerCase();
node.addEventListener(eventName, nextProps[propName], false);
} else {
// 设置节点属性
node[propName] = nextProps[propName];
}
});
}
但是此时组件只能进行插入和更新,还不能进行复用,也不能计算出新的组件应该插入的位置。所以我们需要改写一下
reconcileChildren
函数
就是新旧组件的 diff
过程,遵循上面提到的 react diff
**。
function reconcileChildren(returnFiber, children) {
// 找到上一次的 fiber
let oldFiber = returnFiber.base && returnFiber.base.child;
let prevFiber = null;
// fiber 上一次的位置下标
let lastPlacedIndex = 0;
// 用来循环的下标
let newIdx = 0;
// 用来临时存放 oldFiber 的变量
let nextOldFiber = null;
// 判断初次渲染还是更新的 flag
const shouldTrackSideEffects = !!oldFiber;
}
placeChild
函数
function placeChild(newFiber, lastPlacedIndex, newIdx, shouldTrackSideEffects) {
// 将 newFiber 在当前层级的位置设置到 newFiber 的 index 属性上
newFiber.index = newIdx;
if (!shouldTrackSideEffects) {
return lastPlacedIndex;
}
const base = newFiber.base;
if (base !== null) {
if (base.index < lastPlacedIndex) {
return lastPlacedIndex;
} else {
return base.index;
}
} else {
newFiber.effectTag = PLACEMENT;
return lastPlacedIndex;
}
}
/ 1、组件更新时,走这个条件分支
for (; oldFiber && newIdx < children.length; newIdx++) {
const newChild = children[newIdx];
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
const sameType =
newChild &&
oldFiber &&
newChild.key === oldFiber.key &&
newChild.type === oldFiber.type;
if (!sameType) {
break;
}
const newFiber = {
type: newChild.type,
key: newChild.key,
props: newChild.props,
return: returnFiber,
node: oldFiber.node,
base: oldFiber,
effectTag: UPDATE,
};
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx, shouldTrackSideEffects);
if (prevFiber === null) {
returnFiber.child = newFiber;
} else {
prevFiber.sibling = newFiber;
}
prevFiber = newFiber;
oldFiber = nextOldFiber;
}
// 2、如果是循环结束,会走到这个条件分支,则将剩余的 oldFiber 删除
if (newIdx === children.length) {
while (oldFiber) {
deletions.push({
...oldFiber,
effectTag: DELETION
});
oldFiber = oldFiber.sibling;
}
}
// 3、如果 oldFiber 不存在,代表新增元素,可能是初始化,也可能是新插入的元素
if (!oldFiber) {
for (; newIdx < children.length; newIdx++) {
const newChild = children[newIdx];
if (!newChild) {
continue;
}
const newFiber = {
type: newChild.type,
key: newChild.key,
props: newChild.props,
return: returnFiber,
node: null,
base: null,
effectTag: PLACEMENT
};
lastPlacedIndex = placeChild(
newFiber,
lastPlacedIndex,
newIdx,
shouldTrackSideEffects
);
if (prevFiber === null) {
returnFiber.child = newFiber;
} else {
prevFiber.sibling = newFiber;
}
prevFiber = newFiber;
}
return;
}
index
不同但可以复用时// 将链表结构转化成 map 结构
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 4、当 oldFiber 存在并且 oldFiber 不能被复用的时候,会走到这个条件分支
for (; newIdx < children.length; newIdx++) {
const newChild = children[newIdx];
let newFiber = {
type: newChild.type,
key: newChild.key,
props: newChild.props,
return: returnFiber
};
/**
* 和第 1 个条件分支不同:
* 1 中是一一对比,看是否能够复用 oldFiber
* 这里是根据 key(没有 key 使用 index) 从剩余的 oldFiber 中查找出是否有对应的 oldFiber
*/
const matchedFiber = existingChildren.get(
newChild.key == null ? newIdx : newChild.key
);
// 如果匹配到对应 key / index 的 oldFiber,并且 type 也是相同的,则可以进行复用,更新即可
if (matchedFiber && matchedFiber.type === newChild.type) {
newFiber = {
...newFiber,
node: matchedFiber.node,
base: matchedFiber,
effectTag: UPDATE
};
// 匹配到 oldFiber 之后,则从 map 中移除对应的 fiber,避免重复匹配
existingChildren.delete(newChild.key == null ? newIdx : newChild.key);
} else {
// 没有匹配到的话,则新增 fiber
newFiber = {
...newFiber,
node: null,
base: null,
effectTag: PLACEMENT
};
}
lastPlacedIndex = placeChild(
newFiber,
lastPlacedIndex,
newIdx,
shouldTrackSideEffects
);
if (prevFiber === null) {
returnFiber.child = newFiber;
} else {
prevFiber.sibling = newFiber;
}
prevFiber = newFiber;
}
// 更新阶段,fiber 操作执行完毕,map 中仍有未被匹配的 oldFiber ,则进行删除
if (shouldTrackSideEffects) {
existingChildren.forEach(child => {
deletions.push({
...child,
effectTag: DELETION
});
});
}
将链表转化为 Map
结构的 mapRemainingChildren
函数
查询是否有可复用 oldFiber 时,从 Map 中查找比链表要更方便,所以可以提前将链表结构转为 Map
function mapRemainingChildren(returnFiber, currentChildFiber) {
const existingChildren = new Map();
while (currentChildFiber) {
if (currentChildFiber.key !== null) {
existingChildren.set(currentChildFiber.key, currentChildFiber);
} else {
existingChildren.set(currentChildFiber.index, currentChildFiber);
}
currentChildFiber = currentChildFiber.sibling;
}
return existingChildren;
}
DOM
新增的方式修改 commitWorker
函数
function commitWorker(fiber) {
if (!fiber) {
return;
}
// 执行新增插入操作
if (fiber.node && fiber.effectTag === PLACEMENT) {
let parentFiber = fiber.return;
let parentNode = null;
// 有可能 parentFiber 是 Fragment 或者 Provider 等这些组件,是没有 node 属性的,所以要循环向上找到 parentNode
while (parentFiber) {
if (parentFiber.node && parentFiber.node.nodeType !== FRAGMENT) {
parentNode = parentFiber.node;
break;
}
parentFiber = parentFiber.return;
}
if (fiber.type !== "TEXT") {
console.log("新增", fiber);
}
// parentNode.appendChild(fiber.node);
insertOrAppend(fiber, parentNode);
} else if (fiber.node && fiber.effectTag === UPDATE) {
// 执行属性更新操作
updateNode(fiber.node, fiber.base.props, fiber.props);
} else if (fiber.effectTag === DELETION) {
// 执行删除操作
commitDeletion(fiber);
// !!! 这里要 return 掉,不能执行 child 和 sibling,因为 child 和 sibling 的 effectTag 值并没有修改成 DELETION
return;
}
// 提交子 fiber 任务
commitWorker(fiber.child);
// 提交兄弟 fiber 任务
commitWorker(fiber.sibling);
}
insertOrAppend
函数
function getHostSibling(fiber) {
let sibling = fiber.return.child;
while (sibling) {
// !!! 这里判断 fiber.index 小于它的兄弟节点 index 即可,因为此时 index 可能不是连续的,不能直接使用 fiber.index + 1 === sibling.index 来判断
if (fiber.index < sibling.index && sibling.effectTag === "UPDATE") {
return sibling.node;
}
sibling = sibling.sibling;
}
return null;
}
function insertOrAppend(fiber, parentNode) {
let before = getHostSibling(fiber);
let node = fiber.node;
if (before) {
parentNode.insertBefore(node, before);
} else {
parentNode.appendChild(node);
}
}
至此,fiber
的初始化和更新操作已经基本完成。这里只是简单的对 fiber
**进行了一个实现,还有很多功能没有完善。比如重新排序的组件还不能按照正确的顺序执行。有兴趣的朋友可以继续向下拓展。
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
本文已收录在 Github: https://github.com/beichensky/Blog 中,欢迎 Star,欢迎 Follow!
在 react 18
版本之前,在面试中经常会出现这个问题,那么答案又是什么样的呢?
在 React
合成事件中是异步的
在 hooks
中是异步的
其他情况皆是同步的,例如:原生事件、setTimeout
、Promise
等
看看下面这段代码的执行结果,就知道所言非虚了
class App extends React.Component {
state = {
count: 0
}
componentDidMount() {
this.setState({count: this.state.count + 1})
console.log(this.state.count);
this.setState({count: this.state.count + 1})
console.log(this.state.count);
setTimeout(() => {
this.setState({count: this.state.count + 1})
console.log(this.state.count);
this.setState({count: this.state.count + 1})
console.log(this.state.count);
});
}
render() {
return <h1>Count: {this.state.count}</h1>
}
}
有经验的同学肯定都知道,最终的结果是: 0 0 2 3
。
原因就是因为 componentDidMount
中的 setState
是批量更新,在整体逻辑没走完之前,不会进行更新。所以前两次打印结果都是 0,并且将两次更新合并成了一次。
而在 setTimeout
中,脱离了 React
的掌控,变成了同步更新,因为下方的 log
可以实时打印出即时的状态。
此时 React
的内部的处理逻辑我们可以写一段代码简单模拟一下:
先声明三个变量,用来记录数据
isBatchUpdate
: 判断是否批量更新的标志
count
: 状态
queue
: 存储状态的数组
声明一个 handleClick
方法,来模拟 React
合成事件
声明一个 setState
方法,来模拟 React
的 setState
// 判断是否批量更新的标志
let isBatchUpdate = false;
// 状态
let count = 0;
// 存储最新状态的数组
let queue = [];
const setState = (state) => {
// 批量更新,则将状态暂存,否则直接更新
if (isBatchUpdate) {
queue.push(state);
} else {
count = state;
}
}
const handleClick = () => {
// 进入事件,先将 isBatchUpdate 设置为 true
isBatchUpdate = true
setState(count + 1)
console.log(count);
setState(count + 1)
console.log(count);
setTimeout(() => {
setState(count + 1)
console.log(count);
setState(count + 1)
console.log(count);
})
// 事件结束,将 isBatchUpdate 置为 false
isBatchUpdate = false;
}
handleClick();
count = queue.pop();
// 更新完成,重置状态数组 queue
queue = [];
可以看到,上面这段代码的打印结果也是 0 0 2 3
。
上面提到,在原生事件以及 setTimeout
等情况下,setState
是同步的,那如果我们仍然希望这种情况下可以同步更新,该怎么办呢?
React
也提供了一种解决方案:从 react-dom
包中暴露了一个 API
: unstable_batchedUpdates
那我们简单用一下看看效果:
class App extends React.Component {
state = {
count: 0
}
componentDidMount() {
this.setState({count: this.state.count + 1})
console.log(this.state.count);
this.setState({count: this.state.count + 1})
console.log(this.state.count)
setTimeout(() => {
ReactDOM.unstable_batchedUpdates(() => {
this.setState({count: this.state.count + 1})
console.log(this.state.count)
this.setState({count: this.state.count + 1})
console.log(this.state.count)
})
})
}
render() {
return <h1>Count: {this.state.count}</h1>
}
}
可以看到此时的打印结果为 0 0 1 1
。
Ok,React 18 之前
setState
的更新方式就说到这里,那 React 18 里做了什么改动呢?
上面提到了默认批量更新以及手动批量更新,那有些同学不满足了呀,觉得手动的还是不够智能,在很多情况下还得手动去调用 unstable_batchedUpdates
这个函数,用起来不爽。
别急,React 18 新版本就可以解决这些同学的痛点了!
Ok,直接上代码,看看 React 18 到底怎么用的
class App extends React.Component {
state = {
count: 0
}
componentDidMount() {
this.setState({count: this.state.count + 1})
console.log(this.state.count);
this.setState({count: this.state.count + 1})
console.log(this.state.count)
setTimeout(() => {
this.setState({count: this.state.count + 1})
console.log(this.state.count)
this.setState({count: this.state.count + 1})
console.log(this.state.count)
})
}
render() {
return <h1>Count: {this.state.count}</h1>
}
}
// 使用 react 18 新的并发模式写法进行 dom render
ReactDOM.createRoot(document.getElementById('#root')!).render(<App />)
组件代码保持和第一版的一致,没有使用 unstable_batchedUpdates
。
可以看到,此时的打印结果也是: 0 0 1 1
仅仅是使用了新的 API
: ReactDOM.createRoot(root).render(jsx)
。React 就能实现自动的批量更新了。感觉有点神奇。
我们依然写一段代码来模拟一下这个过程:
此时不需要 isBatchUpdate
来判断是否批量更新了,而是通过更新的优先级来进行判断
每次更新会进行优先级的判定,相同优先级的任务会被合并。
事件执行完毕,进行任务的执行和更新
// 状态
let count = 0;
// 存储状态的数组
let queue = [];
const setState = (state) => {
const newState = {payload: state, priority: 0 }
// 判断当前优先级的任务集合是否存在,不存在则初始化,存在则存到对应由县级的任务集合中
if (queue[newState.priority]) {
queue[newState.priority].push(newState.payload)
} else {
queue[newState.priority] = [newState.payload]
}
}
const handleClick = () => {
setState(count + 1)
console.log(count);
setState(count + 1)
console.log(count);
setTimeout(() => {
setState(count + 1);
console.log(count);
setState(count + 1)
console.log(count);
})
}
handleClick();
count = queue.pop().pop();
setTimeout(() => {
count = queue.pop().pop();
})
可以看到,上面这段代码的执行结果也是 0 0 1 1
上述模拟代码仅为了展示优先级批量更新,不代表任何 React 源码的逻辑和**
好了,自动批量更新的新特性就说到这里了。这里引入了三个问题:
Q: React 18 之后提供了 ReactDOM.createRoot
(root).render(jsx) 的 API,那之前 ReactDOM.render
的 API 还支持吗?
A: 支持的,并且行为和之前版本是一致的。只有使用了 ReactDOM.createRoot
这种方式,才会启用新的并发模式。
Q: React 全自动更新后,那如果我就是想拿到更新之后的数据怎么办呢?
A: 类组件中可以使用 setState(state, callback)
的方式,在 callback
中取到最新的值,函数组件可以使用 useEffect
,将 state
作为依赖。即可以拿到最新的值。
Q: 文章中说到的优先级的概念是怎么回事呢?
A: 这个涉及到 React 最新的调度以及更新的机制,优先级的概念以及其他优先级的任务如何创建,我们之后会一一展开来说。
目前的话,可以理解为 React 的更新机制进行了变化,不再依赖于批量更新的标志。而是根据任务优先级来进行更新:高优先级的任务先执行,低优先级的任务后执行。
代码量很少,主要是修改了 ReactDOM
的渲染方式,可以亲自尝试一下,有疑惑的地方可以说出来一起进行讨论。
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
本文详细介绍了 TypeScript 高级类型的使用场景,对日常 TypeScript 的使用可以提供一些帮助。
本文已收录在 Github
: https://github.com/beichensky/Blog 中,走过路过点个 Star 呗
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。
语法: T & U
其返回类型既要符合
T
类型也要符合U
类型
用法:假设有两个接口:一个是 Ant
蚂蚁接口,一个是 Fly
飞翔接口,现在有一只会飞的蚂蚁:
interface Ant {
name: string;
weight: number;
}
interface Fly {
flyHeight: number;
speed: number;
}
// 少了任何一个属性都会报错
const flyAnt: Ant & Fly = {
name: '蚂蚁呀嘿',
weight: 0.2,
flyHeight: 20,
speed: 1,
};
联合类型与交叉类型很有关联,但是使用上却完全不同。
语法: T | U
其返回类型为连接的多个类型中的任意一个
用法:假设声明一个数据,既可以是 string
类型,也可以是 number
类型
let stringOrNumber: string | number = 0
stringOrNumber = ''
再看下面这个例子,start
函数的参数类型既是 Bird | Fish
,那么在 start
函数中,想要直接调用的话,只能调用 Bird
和 Fish
都具备的方法,否则编译会报错
class Bird {
fly() {
console.log('Bird flying');
}
layEggs() {
console.log('Bird layEggs');
}
}
class Fish {
swim() {
console.log('Fish swimming');
}
layEggs() {
console.log('Fish layEggs');
}
}
const bird = new Bird();
const fish = new Fish();
function start(pet: Bird | Fish) {
// 调用 layEggs 没问题,因为 Bird 或者 Fish 都有 layEggs 方法
pet.layEggs();
// 会报错:Property 'fly' does not exist on type 'Bird | Fish'
// pet.fly();
// 会报错:Property 'swim' does not exist on type 'Bird | Fish'
// pet.swim();
}
start(bird);
start(fish);
语法:T extends K
这里的 extends 不是类、接口的继承,而是对于类型的判断和约束,意思是判断 T 能否赋值给 K
可以在泛型中对传入的类型进行约束
const copy = (value: string | number): string | number => value
// 只能传入 string 或者 number
copy(10)
// 会报错:Argument of type 'boolean' is not assignable to parameter of type 'string | number'
// copy(false)
也可以判断 T 是否可以赋值给 U,可以的话返回 T,否则返回 never
type Exclude<T, U> = T extends U ? T : never;
会遍历指定接口的 key 或者是遍历联合类型
interface Person {
name: string
age: number
gender: number
}
// 将 T 的所有属性转换为只读类型
type ReadOnlyType<T> = {
readonly [P in keyof T]: T[P]
}
// type ReadOnlyPerson = {
// readonly name: Person;
// readonly age: Person;
// readonly gender: Person;
// }
type ReadOnlyPerson = ReadOnlyType<Person>
语法:parameterName is Type
parameterName
必须是来自于当前函数签名里的一个参数名,判断 parameterName 是否是 Type 类型。
具体的应用场景可以跟着下面的代码思路进行使用:
看完联合类型的例子后,可能会考虑:如果想要在 start
函数中,根据情况去调用 Bird
的 fly
方法和 Fish
的 swim
方法,该如何操作呢?
首先想到的可能是直接检查成员是否存在,然后进行调用:
function start(pet: Bird | Fish) {
// 调用 layEggs 没问题,因为 Bird 或者 Fish 都有 layEggs 方法
pet.layEggs();
if ((pet as Bird).fly) {
(pet as Bird).fly();
} else if ((pet as Fish).swim) {
(pet as Fish).swim();
}
}
但是这样做,判断以及调用的时候都要进行类型转换,未免有些麻烦,可能会想到写个工具函数判断下:
function isBird(bird: Bird | Fish): boolean {
return !!(bird as Bird).fly;
}
function isFish(fish: Bird | Fish): boolean {
return !!(fish as Fish).swim;
}
function start(pet: Bird | Fish) {
// 调用 layEggs 没问题,因为 Bird 或者 Fish 都有 layEggs 方法
pet.layEggs();
if (isBird(pet)) {
(pet as Bird).fly();
} else if (isFish(pet)) {
(pet as Fish).swim();
}
}
看起来简洁了一点,但是调用方法的时候,还是要进行类型转换才可以,否则还是会报错,那有什么好的办法,能让我们判断完类型之后,就可以直接调用方法,不用再进行类型转换呢?
OK,肯定是有的,类型谓词 is
就派上用场了
function isBird(bird: Bird | Fish): bird is Bird {
return !!(bird as Bird).fly
}
function start(pet: Bird | Fish) {
// 调用 layEggs 没问题,因为 Bird 或者 Fish 都有 layEggs 方法
pet.layEggs();
if (isBird(pet)) {
pet.fly();
} else {
pet.swim();
}
};
每当使用一些变量调用 isFish
时,TypeScript
会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
TypeScript 不仅知道在 if 分支里 pet 是 Fish 类型; 它还清楚在 else 分支里,一定不是 Fish 类型,一定是 Bird 类型
可以用 infer P
来标记一个泛型,表示这个泛型是一个待推断的类型,并且可以直接使用
比如下面这个获取函数参数类型的例子:
type ParamType<T> = T extends (param: infer P) => any ? P : T;
type FunctionType = (value: number) => boolean
type Param = ParamType<FunctionType>; // type Param = number
type OtherParam = ParamType<symbol>; // type Param = symbol
判断 T 是否能赋值给 (param: infer P) => any
,并且将参数推断为泛型 P,如果可以赋值,则返回参数类型 P,否则返回传入的类型
再来一个获取函数返回类型的例子:
type ReturnValueType<T> = T extends (param: any) => infer U ? U : T;
type FunctionType = (value: number) => boolean
type Return = ReturnValueType<FunctionType>; // type Return = boolean
type OtherReturn = ReturnValueType<number>; // type OtherReturn = number
判断 T 是否能赋值给 (param: any) => infer U
,并且将返回值类型推断为泛型 U,如果可以赋值,则返回返回值类型 P,否则返回传入的类型
typeof v === "typename"
或 typeof v !== "typename"
用来判断数据的类型是否是某个原始类型(
number
、string
、boolean
、symbol
)并进行类型保护
"typename"必须是 "number", "string", "boolean"或 "symbol"。 但是 TypeScript 并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
看下面这个例子, print
函数会根据参数类型打印不同的结果,那如何判断参数是 string
还是 number
呢?
function print(value: number | string) {
// 如果是 string 类型
// console.log(value.split('').join(', '))
// 如果是 number 类型
// console.log(value.toFixed(2))
}
有两种常用的判断方式:
根据是否包含 split
属性判断是 string
类型,是否包含 toFixed
方法判断是 number
类型
弊端:不论是判断还是调用都要进行类型转换
使用类型谓词 is
弊端:每次都要去写一个工具函数,太麻烦了
typeof
一展身手的时候了function print(value: number | string) {
if (typeof value === 'string') {
console.log(value.split('').join(', '))
} else {
console.log(value.toFixed(2))
}
}
使用 typeof 进行类型判断后,TypeScript 会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
与 typeof
类似,不过作用方式不同,instanceof
类型保护是通过构造函数来细化类型的一种方式。
instanceof
的右侧要求是一个构造函数,TypeScript
将细化为:
prototype
属性的类型,如果它的类型不为 any
的话还是以 类型谓词 is 示例中的代码做演示:
最初代码:
function start(pet: Bird | Fish) {
// 调用 layEggs 没问题,因为 Bird 或者 Fish 都有 layEggs 方法
pet.layEggs();
if ((pet as Bird).fly) {
(pet as Bird).fly();
} else if ((pet as Fish).swim) {
(pet as Fish).swim();
}
}
使用 instanceof
后的代码:
function start(pet: Bird | Fish) {
// 调用 layEggs 没问题,因为 Bird 或者 Fish 都有 layEggs 方法
pet.layEggs();
if (pet instanceof Bird) {
pet.fly();
} else {
pet.swim();
}
}
可以达到相同的效果
keyof T
对于任何类型
T
,keyof T
的结果为T
上已知的 公共属性名 的 联合
interface Person {
name: string;
age: number;
}
type PersonProps = keyof Person; // 'name' | 'age'
这里,keyof Person
返回的类型和 'name' | 'age' 联合类型是一样,完全可以互相替换
keyof
只能返回类型上已知的 公共属性名class Animal {
type: string;
weight: number;
private speed: number;
}
type AnimalProps = keyof Animal; // "type" | "weight"
例如我们经常会获取对象的某个属性值,但是不确定是哪个属性,这个时候可以使用 extends
配合 typeof
对属性名进行限制,限制传入的参数只能是对象的属性名
const person = {
name: 'Jack',
age: 20
}
function getPersonValue<T extends keyof typeof person>(fieldName: keyof typeof person) {
return person[fieldName]
}
const nameValue = getPersonValue('name')
const ageValue = getPersonValue('age')
// 会报错:Argument of type '"gender"' is not assignable to parameter of type '"name" | "age"'
// getPersonValue('gender')
T[K]
类似于
js
中使用对象索引的方式,只不过js
中是返回对象属性的值,而在ts
中返回的是T
对应属性 P 的类型
interface Person {
name: string
age: number
weight: number | string
gender: 'man' | 'women'
}
type NameType = Person['name'] // string
type WeightType = Person['weight'] // string | number
type GenderType = Person['gender'] // "man" | "women"
Readonly<T>
)type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
用于将
T
类型的所有属性设置为只读状态。
interface Person {
name: string
age: number
}
const person: Readonly<Person> = {
name: 'Lucy',
age: 22
}
// 会报错:Cannot assign to 'name' because it is a read-only property
person.name = 'Lily'
readonly
只读, 被readonly
标记的属性只能在声明时或类的构造函数中赋值,之后将不可改(即只读属性)
ReadonlyArray<T>
)interface ReadonlyArray<T> {
/** Iterator of values in the array. */
[Symbol.iterator](): IterableIterator<T>;
/**
* Returns an iterable of key, value pairs for every entry in the array
*/
entries(): IterableIterator<[number, T]>;
/**
* Returns an iterable of keys in the array
*/
keys(): IterableIterator<number>;
/**
* Returns an iterable of values in the array
*/
values(): IterableIterator<T>;
}
只能在数组初始化时为变量赋值,之后数组无法修改
interface Person {
name: string
}
const personList: ReadonlyArray<Person> = [{ name: 'Jack' }, { name: 'Rose' }]
// 会报错:Property 'push' does not exist on type 'readonly Person[]'
// personList.push({ name: 'Lucy' })
// 但是内部元素如果是引用类型,元素自身是可以进行修改的
personList[0].name = 'Lily'
Partial<T>
)用于将 T
类型的所有属性设置为可选状态,首先通过 keyof T
,取出类型 T
的所有属性,
然后通过 in
操作符进行遍历,最后在属性后加上 ?
,将属性变为可选属性。
type Partial<T> = {
[P in keyof T]?: T[P];
}
interface Person {
name: string
age: number
}
// 会报错:Type '{}' is missing the following properties from type 'Person': name, age
// let person: Person = {}
// 使用 Partial 映射后返回的新类型,name 和 age 都变成了可选属性
let person: Partial<Person> = {}
person = { name: 'pengzu', age: 800 }
person = { name: 'z' }
person = { age: 18 }
Required<T>
)和 Partial
的作用相反
用于将 T
类型的所有属性设置为必选状态,首先通过 keyof T
,取出类型 T
的所有属性,
然后通过 in
操作符进行遍历,最后在属性后的 ?
前加上 -
,将属性变为必选属性。
type Required<T> = {
[P in keyof T]-?: T[P];
}
interface Person {
name?: string
age?: number
}
// 使用 Required 映射后返回的新类型,name 和 age 都变成了必选属性
// 会报错:Type '{}' is missing the following properties from type 'Required<Person>': name, age
let person: Required<Person> = {}
Pick<T>
)type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
从
T
类型中提取部分属性,作为新的返回类型。
Pick
来实现。interface Goods {
type: string
goodsName: string
price: number
}
// 作为网络请求参数,只需要 goodsName 和 price 就可以
type RequestGoodsParams = Pick<Goods, 'goodsName' | 'price'>
// 返回类型:
// type RequestGoodsParams = {
// goodsName: string;
// price: number;
// }
const params: RequestGoodsParams = {
goodsName: '',
price: 10
}
Omit<T>
)定义:type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
和
Pick
作用相反,用于从T
类型中,排除部分属性
用法:比如长方体有长宽高,而正方体长宽高相等,所以只需要长就可以,那么此时就可以用 Omit
来生成正方体的类型
interface Rectangular {
length: number
height: number
width: number
}
type Square = Omit<Rectangular, 'height' | 'width'>
// 返回类型:
// type Square = {
// length: number;
// }
const temp: Square = { length: 5 }
Extract<T, U>
)语法:Extract<T, U>
提取
T
中可以 赋值 给U
的类型
定义:type Extract<T, U> = T extends U ? T : never;
用法:
type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
type T02 = Extract<string | number | (() => void), Function>; // () => void
Exclude<T, U>
)语法:Exclude<T, U>
与
Extract
用法相反,从T
中剔除可以赋值给U
的类型
定义:type Exclude<T, U> = T extends U ? never : T
用法:
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d"
type T01 = Exclude<string | number | (() => void), Function>; // string | number
Record<K, T>
)type Record<K extends string | number | symbol, T> = {
[P in K]: T;
}
接收两个泛型,
K
必须可以是可以赋值给string | number | symbol
的类型,通过in
操作符对K
进行遍历,每一个属性的类型都必须是T
类型
Person
类型的数组转化成对象映射,可以使用 Record
来指定映射对象的类型interface Person {
name: string
age: number
}
const personList = [
{ name: 'Jack', age: 26 },
{ name: 'Lucy', age: 22 },
{ name: 'Rose', age: 18 },
]
const personMap: Record<string, Person> = {}
personList.map((person) => {
personMap[person.name] = person
})
比如在传递参数时,希望参数是一个对象,但是不确定具体的类型,就可以使用 Record
作为参数类型
function doSomething(obj: Record<string, any>) {
}
NonNullable<T>
)type NonNullable<T> = T extends null | undefined ? never : T
从 T 中剔除
null
、undefined
、never
类型,不会剔除void
、unknow
类型
type T01 = NonNullable<string | number | undefined>; // string | number
type T02 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[]
type T03 = NonNullable<{name?: string, age: number} | string[] | null | undefined>; // {name?: string, age: number} | string[]
ConstructorParameters<typeof T>
)返回 class 中构造函数参数类型组成的 元组类型
/**
* Obtain the parameters of a constructor function type in a tuple
*/
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never;
class Person {
name: string
age: number
weight: number
gender: 'man' | 'women'
constructor(name: string, age: number, gender: 'man' | 'women') {
this.name = name
this.age = age;
this.gender = gender
}
}
type ConstructorType = ConstructorParameters<typeof Person> // [name: string, age: number, gender: "man" | "women"]
const params: ConstructorType = ['Jack', 20, 'man']
InstanceType<T>
)获取 class 构造函数的返回类型
/**
* Obtain the return type of a constructor function type
*/
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any;
class Person {
name: string
age: number
weight: number
gender: 'man' | 'women'
constructor(name: string, age: number, gender: 'man' | 'women') {
this.name = name
this.age = age;
this.gender = gender
}
}
type Instance = InstanceType<typeof Person> // Person
const params: Instance = {
name: 'Jack',
age: 20,
weight: 120,
gender: 'man'
}
Parameters<T>
)获取函数的参数类型组成的 元组
/**
* Obtain the parameters of a function type in a tuple
*/
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;
type FunctionType = (name: string, age: number) => boolean
type FunctionParamsType = Parameters<FunctionType> // [name: string, age: number]
const params: FunctionParamsType = ['Jack', 20]
ReturnType<T>
)获取函数的返回值类型
/**
* Obtain the return type of a function type
*/
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
type FunctionType = (name: string, age: number) => boolean | string
type FunctionReturnType = ReturnType<FunctionType> // boolean | string
高级类型
用法 | 描述 |
---|---|
& | 交叉类型,将多个类型合并为一个类型,交集 |
| | 联合类型,将多个类型组合成一个类型,可以是多个类型的任意一个,并集 |
关键字
用法 | 描述 |
---|---|
T extends U | 类型约束,判断 T 是否可以赋值给 U |
P in T | 类型映射,遍历 T 的所有类型 |
parameterName is Type | 类型谓词,判断函数参数 parameterName 是否是 Type 类型 |
infer P | 待推断类型,使用 infer 标记类型 P,就可以使用待推断的类型 P |
typeof v === "typename" | 原始类型保护,判断数据的类型是否是某个原始类型(number 、string 、boolean 、symbol ) |
instanceof v | 类型保护,判断数据的类型是否是构造函数的 prototype 属性类型 |
keyof | 索引类型查询操作符,返回类型上已知的 公共属性名 |
T[K] | 索引访问操作符,返回 T 对应属性 P 的类型 |
映射类型
用法 | 描述 |
---|---|
Readonly | 将 T 中所有属性都变为只读 |
ReadonlyArray | 返回一个 T 类型的只读数组 |
ReadonlyMap<T, U> | 返回一个 T 和 U 类型组成的只读 Map |
Partial | 将 T 中所有的属性都变成可选类型 |
Required | 将 T 中所有的属性都变成必选类型 |
Pick<T, K extends keyof T> | 从 T 中摘取部分属性 |
Omit<T, K extends keyof T> | 从 T 中排除部分属性 |
Exclude<T, U> | 从 T 中剔除可以赋值给 U 的类型 |
Extract<T, U> | 提取 T 中可以赋值给 U 的类型 |
Record<K, T> | 返回属性名为 K,属性值为 T 的类型 |
NonNullable | 从 T 中剔除 null 和 undefined |
ConstructorParameters | 获取 T 的构造函数参数类型组成的元组 |
InstanceType | 获取 T 的实例类型 |
Parameters | 获取函数参数类型组成的元组 |
ReturnType | 获取函数返回值类型 |
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持
本文已收录在 Github
: https://github.com/beichensky/Blog 中,欢迎 Star!
本文共用 19 个例子,详细讲解了 React 基础入门知识,列举了相关 API 的使用方式,并且在每个 API 的说明中给出了详细的使用规则、建议以及注意事项。
对于不熟悉 React 的朋友,可以作为入门文档进行学习
对于已经掌握 React 的朋友,也可以作为 API 参考手册,用来查漏补缺
demo11:函数组件修改自身状态、发送网络请求、添加事件监听的方式,useState
、useReducer
以及 useEffect
的使用
demo12:类组件的错误边界处理:static getDerivedStateFromError
和 componentDidCatch
生命周期
demo
相关的代码都依赖以下模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>React JSX</title>
<style>
.blue {
color: blue;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="../libs/react.min.js"></script>
<script src="../libs/react-dom.min.js"></script>
<script src="../libs/babel.min.js"></script>
<script type="text/jsx">
// 真正编写 jsx 代码的地方
</script>
</body>
</html>
jsx
语法的基本使用
类似于 xml
的代码格式,但是可以书写 js
逻辑
// 利用 babel 可以直接在 javascript 环境下使用 jsx 语法
// 由于 class 是关键字,所以在 jsx 中给元素设置 class 需要使用 className
const jsx = (
<div>
<h1 className="blue">Hello React</h1>
<p>用于构建用户界面的 JavaScript 库</p>
</div>
);
ReactDOM.render(jsx, root);
如何在 jsx
语法中编写 js
代码
在标签内需要使用 js
语法的时候,使用 {}
将 js
表达式包裹起来即可
{}
中可以是 js
基础类型、引用类型(对象,数组等),也可以是 js
表达式
无论是标签内部还是标签属性,都需要在 {}
内才能使用 js
语法
使用 {/* */}
可以在 jsx
语法中书写注释
下面演示在 jsx
中使用 js
的循环、条件判断语法
const todoList = ['吃饭', '睡觉', '敲代码'];
function handleAlert() {
alert('Hello React!')
}
const a = 1;
const b = 2;
const showModal = true;
const loadingStatus = 'refreshing';
const jsx = (
<div>
{/* style 可以使用对象的形式来写,style 的属性必须使用驼峰法则 */}
<h1 style={{ fontSize: 24, color: 'blue' }}>Hello React</h1>
{/* 逻辑运算符 */}
{a === b && <section>等于</section>}
{/* 三目运算符 */}
{showModal ? <section>弹窗组件</section> : null}
{/* 列表循环生成新数组,数组内元素会被直接渲染到界面,
每个节点可以给一个 key 值,方便 react 在更新时的 diff 对比 */}
<ul>
{todoList.map(todo => <li key={todo}>{todo}</li>)}
</ul>
<p>
{
{
'loading': '加载中。。。。',
'refreshing': '点击刷新重试!',
'no-more': '没有更多了'
}[loadingStatus] /** loadingStatus 是 `loading`、`refreshing`、`no-more` 其中一种状态 **/
}
</p>
{/* 添加事件 */}
<button onClick={handleAlert}>弹出提示</button>
</div>
);
ReactDOM.render(jsx, root);
类组件和函数组件的声明和使用
下面的代码演示 React 组件的声明和使用
// 1、类组件,需要继承 React.Component,render 函数的执行结果会被作为界面展示内容
class ClassComponent extends React.Component {
render() {
return (
<div>
<h1>Hello, Class Component</h1>
</div>
);
}
}
// 2、函数组件,本身就是一个函数,函数的执行结果会被作为界面展示内容
const FunctionComponent = () => {
return (
<div>
<h1>Hello, Function Component</h1>
</div>
);
}
const App = () => (
<div>
<ClassComponent/>
<FunctionComponent/>
</div>
)
ReactDOM.render(<App/>, root);
如何为函数组件和类组件中的元素添加事件绑定?
在类组件中,为元素绑定事件时,事件函数内可能会用到组件的一些属性或者方法,那么此时 this
指向会出现问题。目前可以使用以下三种解决办法:
使用箭头函数代替普通函数
使用 bind
函数绑定 this
指向
使用匿名函数的方式调用组件的属性或者方法
而函数组件中不存在这个问题
class ClassComponent extends React.Component {
// 箭头函数
arrowFunction = () => {
console.log('使用箭头函数,this 指向:', this);
}
// bind 绑定 this
bindFunction() {
console.log('使用 bind 改变 this 指向:', this);
}
render() {
return (
<React.Fragment>
<h3>类组件</h3>
<div>
<button onClick={ this.arrowFunction }>箭头函数打印 this</button>
<br /><br />
<button onClick={ this.bindFunction.bind(this) }>bind 函数打印 this</button>
<br /><br />
<button onClick={() => console.log('匿名函数调用,this 指向:', this)}>匿名函数打印 this</button>
</div>
</React.Fragment>
);
}
}
/**
* 在函数组件中,是不存在组件的 this 实例的,因此绑定事件时,不需要有类组件中的顾虑
*/
const FunctionComponent = () => {
// 箭头函数
const arrowFunction = () => {
console.log('使用箭头函数');
}
// bind 绑定函数
const bindFunction = function() {
console.log('使用 bind 调用函数');
}
// 普通函数
const normalFunction = function() {
console.log('调用普通函数');
}
return (
<React.Fragment>
<h3>函数组件</h3>
<div>
<button onClick={ arrowFunction }>普通函数</button>
<br /><br />
<button onClick={ arrowFunction }>箭头函数</button>
<br /><br />
<button onClick={ bindFunction.bind(this) }>bind 函数</button>
<br /><br />
<button onClick={() => console.log('匿名函数调用')}>匿名函数</button>
</div>
</React.Fragment>
);
}
const App = () => (
<div>
<ClassComponent />
<FunctionComponent />
</div>
)
ReactDOM.render(<App />, root);
React
组件中父子组件传值的方式:props
props
属性,包含父组件传递过来的所有参数props
参数,包含父组件传递过来的所有参数props
中会包含一个 children
属性,标签内的所有内容都会被存放到 children
中。可以是标签、组件或者文本/**
* 1、类组件的实例上会挂载 props 属性,包含父组件传递过来的所有参数
*
* props 中会包含一个 children 属性,标签内的所有内容都会被存放到 children 中。
* 可以是标签、组件或者文本
*/
class ClassComponent extends React.Component {
render() {
return (
<div className="box">
<h1>Class Component</h1>
<p>Receive Message: { this.props.msg }</p>
{ this.props.children }
</div>
);
}
}
/**
* 2、函数组件会接受一个 props 参数,包含父组件传递过来的所有参数
*
* props 中会包含一个 children 属性,标签内的所有内容都会被存放到 children 中。
* 可以是标签、组件或者文本
*/
const FunctionComponent = (props) => {
return (
<div className="box">
<h1>Function Component</h1>
<p>Receive Message: { props.msg }</p>
{ props.children }
</div>
);
}
const App = () => (
<div>
<h1>App</h1>
<ClassComponent msg="App 传递过来的 msg 信息">
App 传递过来的 children 是文本
</ClassComponent>
<FunctionComponent msg="App 传递过来的 msg 信息">
App 传递过来的 children 是 ClassComponent 组件:
<ClassComponent msg="Function Component 传递过来的 msg 信息"/>
</FunctionComponent>
</div>
);
ReactDOM.render(<App />, root);
React 类组件如何控制自身状态变化并触发界面更新?
类组件中通过 state
属性控制自身状态变化,导致组件重新进行渲染(render
)
类组件在 state
属性中对数据进行初始化,state
是一个对象
通过实例的 setState
函数可以更新 state
内容,重新渲染界面
setState
接受两个参数,第一个参数是对象或者是函数,第二个参数回调函数 callback
state
进行合并,设置为新的 state
值例如 state 初始值为 {a: 1, b: 2}, setState({ b, 3 }),那么新的 state 为 { a: 1, b: 3 }
state
值,返回值就是希望更新的对象,更新规则同上例如 state 初始值为 {a: 1, b: 2},setState((prevState) => ({ b: prevState.b - 1 })),那么新的 state 为 { a: 1, b: 1 }
class App extends React.Component {
state = {
count: 0
}
increment = () => {
this.setState({
count: this.state.count + 1
}, () => {
console.log(`最新的 state 值:${this.state.count}`)
});
}
decrement = () => {
this.setState((prevState) => ({
count: prevState.count - 1
}));
}
render() {
return (
<div>
<p>count: { this.state.count }</p>
<button onClick={ this.increment }>increment</button>
<br />
<br />
<button onClick={ this.decrement }>decrement</button>
</div>
);
}
}
ReactDOM.render(<App />, root);
类组件中 setState
用法详解
React
事件中,多次 setState
会被合并setState
不会被合并,会按顺序执行setState
,还是一个定时器中执行 setState
)执行 setState
,不会被合并,会按照代码顺序执行class App extends React.Component {
state = {
count: 0
}
handleClick = () => {
// 1、同一 React 事件内的多次 setState 会被合并,最终的结果 count 只会 + 1
this.setState({
count: this.state.count + 2
});
this.setState({
count: this.state.count + 1
});
}
handleCallbackClick = () => {
/*
* 2、setState 第二个参数是一个回调函数 callback,当上一次 setState 完成时,会触发这个回调函数
* 在 callback 内部可以获取到最新的 state 值
*/
// 3、这种写法 setState 也不会被合并,两次操作都会按顺序执行
this.setState({
count: this.state.count + 2
}, () => {
this.setState({
count: this.state.count + 1
})
});
}
handleSetTimeoutClick = () => {
// 3、使用 setTimeout 的方式使用 setState 不会被合并,两次操作都会按顺序执行
setTimeout(() => this.setState({ count: this.state.count + 2 }));
setTimeout(() => this.setState({ count: this.state.count + 1 }));
}
handleOriginClick = () => {
// 4、在绑定的原生事件中多次调用 setState 不会被合并,两次操作都会执行
this.setState({
count: this.state.count + 2
});
this.setState({
count: this.state.count + 1
});
}
componentDidMount() {
const originBtn = document.querySelector('#originBtn');
originBtn.addEventListener('click', this.handleOriginClick, false);
}
componentWillUnmount() {
const originBtn = document.querySelector('#originBtn');
originBtn.removeEventListener('click', this.handleOriginClick, false);
}
render() {
return (
<div>
<p>count: { this.state.count }</p>
<button onClick={ this.handleClick }>increment</button>
<br />
<br />
<button onClick={ this.handleCallbackClick }>callback increment</button>
<br />
<br />
<button onClick={ this.handleSetTimeoutClick }>setTimeout increment</button>
<br />
<br />
<button id="originBtn">origin event increment</button>
</div>
);
}
}
ReactDOM.render(<App />, root);
在类组件中进行 异步操作 或者 为元素绑定原生事件 的时机:componentDidMount
function fetchData() {
return new Promise(rseolve => {
setTimeout(() => {
const todoList = [
{ id: 1, name: '吃饭'},
{ id: 2, name: '睡觉'},
{ id: 3, name: '敲代码'},
];
rseolve(todoList);
}, 1000)
});
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
todos:[],
toggle: true,
loading: true,
}
}
handleWindowClick = () => {
this.setState({ toggle: !this.state.toggle });
}
componentDidMount() {
// 1、网络请求
fetchData()
.then(result => {
this.setState({ todos: result });
})
.finally(() => {
this.setState({ loading: false });
});
// 2、添加事件监听
window.addEventListener('click', this.handleWindowClick, false);
}
componentWillUnmount() {
// 移除事件监听
window.removeEventListener('click', this.handleWindowClick, false);
}
render() {
const { todos, toggle, loading } = this.state;
return (
<React.Fragment>
<span style={{ color: 'gray', fontSize: 14 }}>随便点点试试</span>
<h1 className="ani" style={{ height: toggle ? 50 : 200 }}>Hello React</h1>
{
loading ?
<p>Loading ...</p> :
<ul>
{ todos.map(todo => <li key={ todo.id }>{ todo.name }</li>) }
</ul>
}
</React.Fragment>
);
}
}
ReactDOM.render(<App />, root);
React
类组件各生命周期触发时机
更多内容可参考这里:React 类组件生命周期详解
/**
* 生命周期执行过程
*
* 初始化:constructor -> static getDerivedStateFromProps -> render -> componentDidMount
* 更新:static getDerivedStateFromProps -> shouldComponentUpdate -> render -> getSnapshotBeforeUpdate -> componentDidUpdate
* 销毁:componentWillUnmount
*/
class LifeCycleComponent extends React.Component {
/**
* 组件初次渲染或者更新之前触发
*
* 返回值会作为新的 state 值与组件中之前的 state 进行合并
*/
static getDerivedStateFromProps(nextProps, prevState) {
console.log('LifeCycleComponent >>>', 'getDerivedStateFromProps ----', 'init or update');
return null;
}
/**
* 组件创建时调用
* 可以在这里做一些初始化操作
*/
constructor(props) {
super(props);
console.log('LifeCycleComponent >>>', 'constructor ----', 'init');
this.state = {
count: 0
}
}
/**
* 组件初次挂载完成时触发
* 可以在这里处理一些异步操作,比如:事件监听,网络请求等
*/
componentDidMount() {
console.log('LifeCycleComponent >>>', 'componentDidMount ----', 'mounted');
}
/**
* 组件触发更新时调用,决定组件是否需要更新
* 返回 true,则组件会被更新,返回 false,则组件停止更新
*/
shouldComponentUpdate(nextProps, nextState) {
console.log('LifeCycleComponent >>>', 'shouldComponentUpdate ----', 'need update ? ');
return true;
}
/**
* 组将 render 之后,提交更新之前触发,返回值会作为 componentDidUpdate 的第三个参数传入
*/
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('LifeCycleComponent >>>', 'getSnapshotBeforeUpdate ----', 'before update');
return null;
}
/**
* 组件更新结束后触发
*/
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('LifeCycleComponent >>>', 'componentDidUpdate ----', 'updated');
}
/**
* 组将即将被卸载时触发
*/
componentWillUnmount() {
console.log('LifeCycleComponent >>>', 'componentWillUnmount ----', 'will unmount');
}
increment = () => {
const { count } = this.state;
this.setState({
count: count + 1
});
}
/**
* 渲染函数 render
*/
render() {
console.log('LifeCycleComponent >>>', 'render');
const { msg } = this.props;
const { count } = this.state;
return (
<div>
<h1>LifeCycleComponent</h1>
<p>Receive Message: { msg }</p>
<p>count: { count }</p>
<button onClick={ this.increment }>increment</button>
</div>
);
}
}
class App extends React.Component {
state = {
message: 'Hello World',
show: true
}
render() {
const { message, show } = this.state;
return (
<div>
<button onClick={ () => this.setState({ message: 'Hello React' }) }>修改 message </button> | {' '}
<button onClick={ () => this.setState({ show: !show }) }>
{ show ? '销毁 LifeCycleComponent' : '创建 LifeCycleComponent' }
</button>
{ show && <LifeCycleComponent msg={ message } /> }
</div>
);
}
}
ReactDOM.render(<App />, root);
React 中受控组件的定义和使用
类似于 vue
中的 v-model
定义:组件的值受 React
中状态的控制,组件的变化会导致 React
中状态的更新
/**
* 受控组件:
* 组件的属性值会受到 React state 的控制,
* 并且在组件的属性值发生变化时,React 的 state 值会做相应的修改。
*/
class App extends React.Component {
state = {
username: ''
}
handleNameChange = (e) => {
this.setState({ username: e.target.value });
}
handleSubmit = () => {
const { username } = this.state;
if (username) {
alert(`提交成功,username = ${ username }`);
} else {
alert('请填写用户名!');
}
}
render() {
const { username } = this.state
return (
<div>
<p>username: { username }</p>
<section>
<label>用户名:</label>
<input
placeholader="请输入用户名"
value={ username }
onChange={ this.handleNameChange }
/>
</section>
<br />
<button onClick={ this.handleSubmit }>submit</button>
</div>
);
}
}
ReactDOM.render(<App />, root);
如何在 React 函数组件中控制自身状态变化以及副作用处理?
函数组件中控制自身状态的相关 hooks
:useState
和 useReducer
函数组件中处理副作用相关的 hooks
: useLayoutEffect
和 useEffect
useState: [state, setState] = useState(initState)
initState
可以是函数,也可以是值,函数的话,仅会在组件创建时执行一次,返回值作为 state
的初始值
组件中可以使用多次 useState
,创建出不同的状态
与 类组件中的 setState
不同的地方在于,同一 React
事件中,多次 setState
会被最后一次的替换,其他逻辑相似
useReducer: [state, dispatch] = useState(reducer)
reducer
是一个函数,第一个参数是上一次的 state 值;第二个参数是传入的 action
,不传的话则没有。返回值会作为新的 state
进行使用useEffect: useEffect(() => doSomething, deps)
useEffect
是副作用执行 hook
,第一次组件渲染完毕或依赖的 deps
发生变化时,doSomething
逻辑都会被执行
deps
是一个数组,发生变化的判断标准是将 deps
中的依赖进行前后两次的浅比较
useLayoutEffect: useLayoutEffect(() => doSomething, deps)
useLayoutEffect
也是副作用执行 hook
,同 useEffect
,第一次组件渲染完毕或依赖的 deps
发生变化时,doSomething
逻辑都会被执行useEffect
不同的地方在于,组件渲染完毕,会同步执行 useLayoutEffect
,而异步执行 useEffect
useLayoutEffect
同一帧的 useLayoutEffect
会在 useEffect
前执行
/**
* 为了能够在函数组件中也能使用状态、执行副作用等操作,引入了 hooks 的概念
*
* useState: 函数组件也可以拥有自身状态
*
* useReducer: useState 的升级版,可以根据不同操作返回不同的状态值
*
* useEffect 用法:
* 1、第一个参数是副作用函数,第二个参数是依赖项集合
* 2、副作用函数的返回值可以是一个函数,会在当前 useEffect 被销毁时执行,可以在这里做一些状态回收,事件解除等操作
* 3、依赖项发生变化时,副作用操作会重新执行
* 4、希望 useEffect 只执行一次,则可以给依赖项一个空数组
* 5、希望组件的每次更新都执行 useEffect,可以不写依赖项
*/
const { useState, useEffect, useReducer, useLayoutEffect } = React;
const App = () => {
//
const [count, setCount] = useState(0);
const [num, dispatch] = useReducer((state, action) => {
switch(action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}, 0)
useEffect(() => {
console.log('useEffect')
// 执行异步操作获取数据
setCount(10);
}, [])
useLayoutEffect(() => {
console.log('useLayoutEffect')
// 绑定事件
const handleClick = () => {
alert(count);
}
const box = document.querySelector('#box');
box.addEventListener('click', handleClick, false);
return () => {
box.removeEventListener('click', handleClick, false);
}
}, [count])
return (
<div>
<p>count: { count }</p>
<button onClick={() => setCount(count + 1)}>count increment</button>
<p>num: { num }</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>num increment</button> | {' '}
<button onClick={() => dispatch({ type: 'DECREMENT' })}>num decrement</button>
<br />
<br />
<button id="box">alert count</button>
</div>
);
}
ReactDOM.render(<App />, root)
React
中错误边界处理
组件出现异常,会触发 static getDerivedStateFromError
和 componentDidCatch
生命周期
static getDerivedStateFromError
的返回值会合并到组件的 state
中作为最新的 state
值
/**
* 错误边界处理:组件出现异常,会触发 static getDerivedStateFromError 和 componentDidCatch 生命周期
*
* static getDerivedStateFromError 的返回值会合并到组件的 state 中作为最新的 state 值
*/
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
errorMsg: ''
};
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
this.setState({errorMsg: error.message})
console.log('异常信息:', error, ' , ', errorInfo )
}
render() {
const { hasError, errorMsg } = this.state;
if (hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong, Error Message: {errorMsg}</h1>;
}
return this.props.children;
}
}
const App = () => {
const [count, setCount] = React.useState(0);
if (count > 0) {
throw TypeError('数据异常');
}
return (
<div>
<h2>App 组件</h2>
<p>count: { count }</p>
<button onClick={() => setCount(count + 1)}>increment</button>
</div>
);
}
ReactDOM.render(<ErrorBoundary><App /></ErrorBoundary>, root);
React 高阶组件(HOC)的两种创建方式
属性代理(Props Proxy
):类组件和函数组件都可以使用
反向继承(Inheritance Inversion
,缩写 II
):只用类组件可以使用
/**
* 高阶组件的两种创建方式
* 1、属性代理(Props Proxy):类组件和函数组件都可以使用
* 2、反向继承(Inheritance Inversion, 缩写II):只用类组件可以使用
*/
/**
* 通过属性代理的方式向组件中注入 permission 属性
*/
function ComposeHOC(OriginComponent) {
const permission = 'edit permission from ComposeHOC'
return (props) => <OriginComponent {...props} permission={permission} />
}
/**
* 通过反向继承的方式向组件中注入 DOM 节点
*/
function iiHOC(OriginComponent) {
return class WrapperComponent extends OriginComponent {
render() {
return <div>
<h1>Title from iiHOC</h1>
{ super.render() }
</div>;
}
}
}
const ComponentA = (props) => <h2 className="box">ComponentA props permission: { props.permission }</h2>
class ComponentB extends React.Component {
render() {
return <h2 className="box">ComponentB</h2>;
}
}
// 使用高阶组件包裹 A、B 组件
const WrapperComponentA = ComposeHOC(ComponentA);
const WrapperComponentB = iiHOC(ComponentB);
const App = () => (<div><WrapperComponentA /><WrapperComponentB /></div>);
ReactDOM.render(<App />, root);
Context
在类组件和函数组件中的使用
Context
: const { Provider, Consumer } = React.createContext()
Provider
标签包裹住父组件,则在任意深度的子孙组件中,都可以在 Consumer
标签中通过 renderProps
的方式获取到对应 Context
的值useContext()
获取到对应 Context
的值更多 Context
用法可参考这里:React 中 Context 用法详解
const { createContext, Component, useContext, useState } = React;
/**
* createContext: 创建 context 上下文
* Context.Provider: 需要对子组件进行包裹,在能在子组件中获取到 context 中的 value
* Context.Consumer: 在类组件中使用 Render Props 的方式 context 上下文
* useContext: 在函数组件中使用 context 上下文
*/
const UserContext = React.createContext();
const { Provider, Consumer } = UserContext;
class ClassComponent extends React.Component {
render() {
return (
<div className="box">
<h2>类组件</h2>
<Consumer>
{user => (<div>name: { user.name }</div>)}
</Consumer>
</div>
);
}
}
const FunctionComponent = () => {
const user = useContext(UserContext);
return (
<div className="box">
<h2>函数组件</h2>
<div>name: { user.name }</div>
</div>
);
}
const App = () => {
const [user, setUser] = useState({ name: '孙悟空' });
return (
<Provider value={user}>
<h1>App</h1>
<label>Change name:</label>
<input
placeholder="请输入用户名称"
value={user.name}
onChange={(e) => setUser({ name: e.target.value })}
/>
<ClassComponent />
<FunctionComponent />
</Provider>
);
}
ReactDOM.render(<App />, root);
类组件和函数组件中
ref
的使用
作用
DOM
节点或者 React
组件的引用创建 ref
:
ref = React.createRef()
ref = useRef()
使用:
ref
直接作为元素的 ref
属性使用,给子组件设置 ref
时,需要配合 forwardRef
包裹子组件ref
可以作为元素或者组件的 ref
,也可以只作为一个变量使用,将变量随函数组件的创建而创建,销毁而销毁const { createRef, useRef } = React;
/**
* createRef:在类组件中为元素设置 ref
* useRef: 在函数组件中为元素设置 ref
*
* 之前使用受控组件的方式进行表单提交。其实也可以使用 ref 的方式操作非受控组件
*/
class ClassComponent extends React.Component{
inputRef = createRef();
submit = () => {
const { value } = this.inputRef.current;
if (value) {
alert(`提交成功,用户名为:${ value }`);
} else {
alert('请输入用户名!');
}
}
render() {
return (
<div className="box">
<h2>类组件</h2>
<section>
<label>用户名:</label>
<input ref={ this.inputRef } placeholder="请输入用户名" />
</section>
<br />
<button onClick={ this.submit }>提交</button>
</div>
);
}
}
const FunctionComponent = () => {
const inputRef = useRef();
const submit = () => {
const { value } = inputRef.current;
if (value) {
alert(`提交成功,用户名为:${ value }`);
} else {
alert('请输入用户名!');
}
}
return (
<div className="box">
<h2>函数组件</h2>
<section>
<label>用户名:</label>
<input ref={ inputRef } placeholder="请输入用户名" />
</section>
<br />
<button onClick={ submit }>提交</button>
</div>
);
}
const App = () => (
<div>
<ClassComponent />
<FunctionComponent />
</div>
)
ReactDOM.render(<App />, root);
在函数组件中,父组件如何调用子组件中的状态或者函数:使用
useImperativeHandle
用法:useImperativeHandle(ref, createHandle, [deps])
第一个参数是 ref
值,可以通过属性传入,也可以配合 forwardRef
使用
第二个参数是一个函数,返回一个对象,对象中的属性都会被挂载到第一个参数 ref
的 current
属性上
第三个参数是依赖的元素集合,同 useEffect
、useCallback
、useMemo
,当依赖发生变化时,第二个参数会重新执行,重新挂载到第一个参数的 current
属性上
注意事项
第三个参数,依赖必须按照要求填写,少了会导致返回的对象属性异常,多了会导致 createHandle
重复执行
一个组件或者 hook
中,对于同一个 ref
,只能使用一次 useImperativeHandle
,多次的话,后面执行的 useImperativeHandle
的 createHandle
返回值会替换掉前面执行的 useImperativeHandle
的 createHandle
返回值
const { useState, useRef, useImperativeHandle, useCallback } = React;
const ChildComponent = ({ actionRef }) => {
const [value, setValue] = useState('')
/**
* 随机修改 value 值的函数
*/
const randomValue = useCallback(() => {
setValue(Math.round(Math.random() * 100) + '');
}, []);
/**
* 提交函数
*/
const submit = useCallback(() => {
if (value) {
alert(`提交成功,用户名为:${value}`);
} else {
alert('请输入用户名!');
}
}, [value]);
useImperativeHandle(actionRef, () => {
return {
randomValue,
submit,
}
}, [randomValue, submit])
/* !! 返回多个属性要按照上面这种写法,不能像下面这样使用多个 useImperativeHandle
useImperativeHandle(actionRef, () => {
return {
submit,
}
}, [submit])
useImperativeHandle(actionRef, () => {
return {
randomValue
}
}, [randomValue])
*/
return (
<div className="box">
<h2>函数组件</h2>
<section>
<label>用户名:</label>
<input value={value} placeholder="请输入用户名" onChange={e => setValue(e.target.value)}/>
</section>
<br/>
</div>
);
}
const App = () => {
const childRef = useRef();
return (
<div>
<ChildComponent actionRef={childRef}/>
<button onClick={() => childRef.current.submit()}>调用子组件的提交函数</button>
<br/>
<br/>
<button onClick={() => childRef.current.randomValue()}>随机修改子组件的 input 值</button>
</div>
)
}
ReactDOM.render(<App/>, root);
React
中传送门 Portals
的使用:可以将指定 React
元素挂载到任意的 DOM
节点上去, 虽然在层级关系上,看起来实在父组件下,但在界面上是挂载到了指定的 DOM
节点上
官网解释:
Portal
提供了一种将子节点渲染到存在于父组件以外的DOM
节点的优秀的方案。
用法: ReactDOM.createPortal(child, container)
React
元素DOM
元素Portals
的典型用例是当父组件有 overflow: hidden
或 z-index
样式时, 但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框。
注意:尽管 portal 可以被放置在 DOM 树中的任何地方,但在任何其他方面,其行为和普通的 React 子节点行为一致。
由于 portal 仍存在于 React 树, 且与 DOM 树 中的位置无关,那么无论其子节点是否是 portal,像 context 这样的功能特性都是不变的。包含事件冒泡。
/**
* 通过 createPortal API,将 Modal 组件的真实节点挂载到新建的 div 元素上去
* 虽然在 React 树中,Modal 组件仍然在 App 组件中,但是在界面上,Modal 节点其实是挂载在了新的 div 节点上
*/
const { useEffect, useState } = React;
const { createPortal } = ReactDOM;
const modalRoot = document.createElement('div');
/**
* Modal: 弹窗组件
*/
function Modal({ children, onCancel }) {
useEffect(() => {
document.body.appendChild(modalRoot);
return () => {
document.body.removeChild(modalRoot);
}
})
return createPortal(
<div className="modal">
<div className="modal-inner">
<div className="mask"/>
<section className="modal-content-wrapper">
<div className="modal-content">
<header>
<h1>提示弹窗</h1>
</header>
<hr/>
<content>{children}</content>
<footer>
<button onClick={onCancel}>关闭</button>
</footer>
</div>
</section>
</div>
</div>,
modalRoot
);
}
const App = () => {
const [visible, setVisible] = useState(false);
return (
<div>
<h1>App</h1>
<br/>
<button onClick={() => setVisible(true)}>展示弹窗</button>
{visible && <Modal onCancel={() => setVisible(false)}>
自定义内容
</Modal>}
</div>
);
}
ReactDOM.render(<App/>, root);
优化 React
组件的几种方式
Fragment
: 可以作为标签包裹子元素,并不会在 DOM
中生成真实节点PureComponent
: 会对类组件的 props
和 state
做一次浅比较,只有当数据发生变化式,组件才会重新渲染
PureComponent
的更多内容可以参考这里:PureComponent 使用注意事项以及源码解析memo
: 作用和 PureComponent
类似,只不过是作为高阶组件,作用在函数组件上const { Fragment, PureComponent, memo, useState, Component } = React;
class ClassComponent extends PureComponent {
render() {
console.log('PureComponent render');
return (
<div className="box">
<h1>PureComponent 组件</h1>
<p>count: {this.props.count}</p>
</div>
);
}
}
const FunctionComponent = memo((props) => {
console.log('memo function render');
return (
<div className="box">
<h1>memo 函数组件</h1>
<p>num: {props.num}</p>
</div>
);
})
const App = () => {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
return (
<Fragment>
<p>打开控制台查看 render 日志</p>
<div>
<button onClick={ () => setCount(count + 1) }>increment count</button> | {' '}
<button onClick={ () => setNum(num + 1) }>increment num</button>
<ClassComponent count={ count }/>
<FunctionComponent num={ num }/>
</div>
</Fragment>
);
}
ReactDOM.render(<App />, root);
提升函数组件性能常用的两个 hooks
: useCallback
、useMemo
useCallback(fn, deps)
: 会对函数进行缓存,当第二个参数依赖项发生变化时,才会重新生成新的函数
fn
: 返回的函数deps
: 依赖项集合,是个数组useMemo(fn, deps)
: 会对函数的返回值进行缓存,当第二个参数依赖项发生变化时,才会重新执行,返回新的数据
fn
: 需要执行的函数deps
: 依赖项集合,是个数组React
组件useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
const { Fragment, useCallback, useMemo, useState } = React;
const App = () => {
const [count, setCount] = useState(0);
const [num, setNum] = useState(0);
const doubleCount = useMemo(() => {
return count * 2;
}, [count])
const alertNum = useCallback(() => {
alert(`num 值:${num}`);
}, [num])
console.log('render');
return (
<Fragment>
<p>count: {count}</p>
<p>doubleCount: {doubleCount}</p>
<p>num: {num}</p>
<button onClick={() => setCount(count + 1)}>increment count</button>
| {' '}
<button onClick={() => setNum(num + 1)}>increment num</button>
| {' '}
<button onClick={alertNum}>alert num</button>
</Fragment>
);
}
ReactDOM.render(<App/>, root);
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
在 十分钟学会 react-redux 一文中详细讲解了 react-redux
的使用。
在 从零到一实现 Redux 中,实现了关于 redux 的核心代码。
下面我们按照上一篇的节奏,继续实现一下 react-redux
的核心代码。
本文已收录在
Github
: https://github.com/beichensky/Blog 中,欢迎 Star!
包含以下核心 API
Provider
: 上下文组件connect
: 带参的高阶函数useSelector
: 获取需要的 state
数据useDispatch
: 获取 dispatch
全局只有一个 store 对象,需要在多层级组件中传递 store
,
并且 store 中的 state 发生变化,组件需要相应的做出更新。
所以这里我们使用 Context
进行数据传递
import React, { useContext, useEffect, useReducer } from "react";
import { bindActionCreators } from "./redux";
const StoreContext = React.createContext();
/**
* Provider 组件,用来传递 Context 中数据,进行跨层级组件通信
*/
const Provider = ({ store, children }) => (
// 将 store 作为 value 传递下去
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
export { Provider, connect, useDispatch, useSelector };
订阅监听事件,state
发生变化,强制更新组件
将 stateProps
、dispatchProps
、mergeProps
合并到组件的 props
中
API
:connect([mapStateToProps],[mapDispatchToProps],[mergeProps],[options])
connect
的用法相对复杂一些,接受四个参数(我们这里暂时不管第四个参数),返回的是一个高阶组件。用来连接当前组件和 Redux store
。
mapStateToProps
:函数类型,接受两个参数: state
和 ownProps
(当前组件的 props
,不建议使用,会导致重渲染,损耗性能),必须返回一个纯对象,这个对象会与组件的 props
合并
(state[, ownProps]) => ({ count: state.count, todoList: state.todos })
mapDispatchToProps
:object | 函数
不传递这个参数时,dispatch
会默认挂载到组件的的 props
中
传递 object
类型时,会把 object
中的属性值使用 dispatch
包装后,与组件的 props
合并
对象的属性值都必须是 ActionCreator
dispatch
不会再挂载到组件的 props
中
传递函数类型时,接收两个参数:dispatch
和 ownProps
(当前组件的 props
,不建议使用,会导致重渲染,损耗性能),必须返回一个纯对象,这个对象会和组件的 props
合并
mergeProps
:(很少使用) 函数类型。如果指定了这个参数,mapStateToProps()
与 mapDispatchToProps()
的执行结果和组件自身的 props
将传入到这个回调函数中。该回调函数返回的对象将作为 props
传递到被包装的组件中。你也许可以用这个回调函数,根据组件的 props
来筛选部分的 state
数据,或者把 props
中的某个特定变量与 ActionCreator
绑定在一起。如果你省略这个参数,默认情况下组件的 props
返回 Object.assign({}, ownProps, stateProps, dispatchProps)
的结果
mergeProps(stateProps, dispatchProps, ownProps): props
connect
/**
* connect 函数,源码中包含四个参数,我们这里只用到这些,所以就暂时只实现了前三个参数
*
* @param {*} mapStateToProps 将 state 合并到组件的 props 中的函数
* @param {*} mapDispatchToProps 将 actionCreator 合并到组件的 props 中的函数
* @param {*} mergeProps 自定义属性合并到组件的 props
*/
const connect = (
mapStateToProps,
mapDispatchToProps,
mergeProps
) => WrapperComponent => {
return props => {
const { getState, dispatch, subscribe } = useContext(StoreContext);
const [, forceUpdate] = useReducer(x => x + 1, []);
// 执行 mapStateToProps,获取用户需要的 state 数据
const stateProps = mapStateToProps(getState());
// 默认将 dispatch 挂载到 props 上
let dispatchProps = { dispatch };
// 判断 mapDispatchToProps 是函数还是对象,函数的话,执行获取返回的对象
if (typeof mapDispatchToProps === "function") {
dispatchProps = mapDispatchToProps(dispatch);
} else if (mapDispatchToProps === "object") {
// 对象的话,直接将对象中的 actionCreator 使用 dispatch 进行包装
dispatchProps = bindActionCreators(mapDispatchToProps, dispatch);
}
mergeProps = mergeProps(stateProps, dispatchProps, props);
useEffect(() => {
// 添加事件订阅,state 发生变化时会触发,更新组件
const unsubscribe = subscribe(() => forceUpdate());
return () => {
unsubscribe();
};
}, [subscribe]);
return (
<WrapperComponent
{...props}
{...stateProps}
{...dispatchProps}
{...mergeProps}
/>
);
};
};
useDispatch
/**
* 获取 store 对象
*/
const useStore = () => {
const store = useContext(StoreContext);
return store;
};
/**
* 获取 store 中 dispatch
*/
const useDispatch = () => {
const store = useStore();
return store.dispatch;
};
订阅事件监听,state
发生变化,强制更新组件
接受一个函数作为参数,函数的返回值作为 useSelector
的返回值传递出去
/**
* useSelector 从 store 中获取当前组件所需要的 state
*
* @param {(state) => props} selector 用户传入的函数,接收 store 当前的 state,返回一个组织好的数据对象
*/
const useSelector = selector => {
const [, forceUpdate] = useReducer(x => x + 1, []);
const { subscribe, getState } = useStore();
useEffect(() => {
// 添加事件订阅,state 发生变化时会触发,更新组件
const unsubscribe = subscribe(() => forceUpdate());
return () => {
unsubscribe();
};
}, [subscribe]);
return selector(getState());
};
至此, react-redux
的核心功能基本已经实现,不过有很多细节和参数的兼容都没有进行处理,有兴趣的朋友可以参照源码完善。
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
React 18
目前已经开放 alpha
版本可以供我们体验了,那为了更方便快捷的体验 React 18
新特性,我们今天使用 Vite
搭建一个简易版的 React
开发环境,帮助我们快速尝鲜。
本文已收录在 Github: https://github.com/beichensky/Blog 中,欢迎 Star,欢迎 Follow!
新建一个 react-react18-demo
文件作为我们的项目
mkdir vite-react18-demo
cd vite-react-react18-demo
npm init -y
使用 Vite
以及 React 18 进行开发,那自然需要先安装
注意:node 版本需要大于 12,否则的话,即便
Vite
安装成功,启动时也会报错:Cannot find module worker_threads
npm install react@alpha react-dom@alpha
npm install vite -D
使用 Webpack
时需要启动 devServer
,但使用 Vite
时不需要再去额外配置
想要使用 Vite
启动项目,直接在 package.json
文件中添加命令:
启动:"start": "vite"`
打包:"build": "vite build"
那我们之前用 Webpack
时可以热更新,那 Vite
可以吗?当然是可以的了。
npm install @vitejs/plugin-react-refresh -D
安装完之后,和 Webpack
一样,新建 vite.config.js
配置文件,在 plugins
属性中添加热更新插件即可
import { defineConfig } from 'vite'
import refreshReactPlugin from '@vitejs/plugin-react-refresh'
export default defineConfig({
plugins: [refreshReactPlugin()]
})
ok,那我们现在许多项目使用了 typescript
,使用 vite
开发时可以集成吗?当然可以了。步骤如下:
安装依赖
npm install typescript @types/react @types/react-dom -D
根目录下添加 tsconfig.json
配置文件
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react",
"types": ["react/next", "react-dom/next"]
},
"include": ["./src"]
}
添加完之后,由于使用了 typescript
,那我们修改一下打包命令:"build": "tsc && vite build"
src
目录,在 src
下创建一个 index.tsx
文件index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
const App = () => {
return <h1>Hello, React 18</h1>
}
// 使用 react 18 新的并发模式写法进行 dom render
ReactDOM.createRoot(document.getElementById('root')!).render(<App />)
现在入口文件有了,但是还没有承载的页面,所以我们可以在根目录下创建 index.html
作为页面
创建 script
标签,src
指向刚才创建的入口文件 index.tsx
设置 script
标签 type
为 module
:可以导入 ES6
模块,可以启用 ESM
模块机制
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
执行 npm run start
命令后,会启动 3000 端口(被占用的话会向后顺延)。打开浏览器,输入 http://localhost:3000
,就可以看到:Hello, React 18
这几个大字了。
执行 npm run build
命令后,会进行项目打包,生成 dist 文件夹
。我们使用 live-server
插件(需要提前进行全局安装哦,npm i live-server -g
),看看有没有打包成功
cd dist
liver-server
打开浏览器,输入 http://localhost:8080/
,同样可以看到:Hello, React 18
这几个大字。说明打包成功了。
Ok,Vite
搭配 React 18 的环境搭建到这里就成功了。后面我们会详细介绍一下 React 18 更新的新特性。
注意:node 版本需要大于 12,否则的话,即便 Vite 安装成功,启动时也会报错:
Cannot find module worker_threads
代码都在文中了,有想要快速搭建 React 调试环境的,可以速度冲了。
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
本文已收录在 Github
: https://github.com/beichensky/Blog 中,欢迎 Star!
相信看完本文,你可以得到需要的答案。
先来看一下函数组件的运作方式:
Counter.js
function Counter() {
const [count, setCount] = useState(0);
return <p onClick={() => setCount(count + 1)}>count: {count}</p>;
}
每次点击 p
标签,count
都会 + 1,setCount
会触发函数组件的渲染。函数组件的重新渲染其实是当前函数的重新执行。
在函数组件的每一次渲染中,内部的 state
、函数以及传入的 props
都是独立的。
比如:
// 第一次渲染
function Counter() {
// 第一次渲染,count = 0
const [count, setCount] = useState(0);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
// 点击 p 标签触发第二次渲染
function Counter() {
// 第二次渲染,count = 1
const [count, setCount] = useState(0);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
// 点击 p 标签触发第三次渲染
function Counter() {
// 第三次渲染,count = 2
const [count, setCount] = useState(0);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
// ...
在函数组件中声明的方法也是类似。因此,在函数组件渲染的每一帧对应这自己独立的
state
、function
、props
。
useState
VS setState
useState
只能作用在函数组件,setState
只能作用在类组件
useState
可以在函数组件中声明多个,而类组件中的状态值都必须声明在 this
的 state
对象中
一般的情况下,state
改变时:
useState
修改 state
时,同一个 useState
声明的值会被 覆盖处理,多个 useState
声明的值会触发 多次渲染
setState
修改 state
时,多次 setState
的对象会被 合并处理
useState
修改 state
时,设置相同的值,函数组件不会重新渲染,而继承 Component
的类组件,即便 setState
相同的值,也会触发渲染
useState
VS useReducer
useState
设置初始值时,如果初始值是个值,可以直接设置,如果是个函数返回值,建议使用回调函数的方式设置const initCount = c => {
console.log('initCount 执行');
return c * 2;
};
function Counter() {
const [count, setCount] = useState(initCount(0));
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
会发现即便 Counter
组件重新渲染时没有再给 count
重新赋初始值,但是 initCount
函数却会重复执行
修改成回调函数的方式:
const initCount = c => {
console.log('initCount 执行');
return c * 2;
};
function Counter() {
const [count, setCount] = useState(() => initCount(0));
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
这个时候,initCount
函数只会在 Counter
组件初始化的时候执行,之后无论组件如何渲染,initCount
函数都不会再执行
useReducer
设置初始值时,初始值只能是个值,不能使用回调函数的方式
useState
修改状态时,同一个 useState
声明的状态会被覆盖处理function Counter() {
const [count, setCount] = useState(0);
return (
<p
onClick={() => {
setCount(count + 1);
setCount(count + 2);
}}
>
clicked {count} times
</p>
);
}
当前界面中
count
的step
是 2
useReducer
修改状态时,多次 dispatch
会按顺序执行,依次对组件进行渲染function Counter() {
const [count, dispatch] = useReducer((x, payload) => x + payload, 0);
return (
<p
onClick={() => {
dispatch(1);
dispatch(2);
}}
>
clicked {count} times
</p>
);
}
当前界面中
count
的step
是 3
useReducer
的初始值,为什么还原不了比如下面这个例子:
const initPerson = { name: '小明' };
const reducer = function (state, action) {
switch (action.type) {
case 'CHANGE':
state.name = action.payload;
return { ...state };
case 'RESET':
return initPerson;
default:
return state;
}
};
function Counter() {
const [person, dispatch] = useReducer(reducer, initPerson);
const [value, setValue] = useState('小红');
const handleChange = useCallback(e => setValue(e.target.value), []);
const handleChangeClick = useCallback(() => dispatch({ type: 'CHANGE', payload: value }), [value]);
const handleResetClick = useCallback(() => dispatch({ type: 'RESET' }), []);
return (
<>
<p>name: {person.name}</p>
<input type="text" value={value} onChange={handleChange} />
<br />
<br />
<button onClick={handleChangeClick}>修改</button> |{' '}
<button onClick={handleResetClick}>重置</button>
</>
);
}
点击修改按钮,将对象的 name
改为 小红,点击重置按钮,还原为原始对象。但是我们看看效果:
可以看到 name
修改小红后,无论如何点击重置按钮,都无法还原。
这是因为在 initPerson
的时候,我们改变了 state
的属性,导致初始值 initPerson
发生了变化,所以之后 RESET
,即使返回了 initPerson``,但是name
值依然是小红。
所以我们在修改数据时,要注意,不要在原有数据上进行属性操作,重新创建新的对象进行操作即可。比如进行如下的修改:
// ...
const reducer = function (state, action) {
switch (action.type) {
case 'CHANGE':
// !修改后的代码
const newState = { ...state, name: action.payload }
return newState;
case 'RESET':
return initPerson;
default:
return state;
}
};
// ...
看看修改后的效果,可以正常的进行重置了:
useEffect
基本用法:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count: ', count);
});
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
每次点击 p
标签,Counter
组件都会重新渲染,都可以在控制台看到有 log
打印。
useEffect
模拟 componentDidMount
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count: ', count);
// 设置依赖为一个空数组
}, []);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
将 useEffect
的依赖设置为空数组,可以看到,只有在组件初次渲染时,控制台会打印输出。之后无论 count
如何更新,都不会再打印。
useEffect
模拟 componentDidUpdate
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
if (count !== 0) {
console.log('count: ', count);
}
}, [count]);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
但是这样处理有个弊端,当有多个依赖项时,需要多次比较,因此可以选择使用下面这种方式。
useRef
设置一个初始值,进行比较function Counter() {
const [count, setCount] = useState(0);
const firstRender = useRef(true);
useEffect(() => {
if (firstRender.current) {
firstRender.current = false;
} else {
console.log('count: ', count);
}
}, [count]);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
useEffect
模拟 componentWillUnmount
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count: ', count);
return () => {
console.log('component will unmount')
}
}, []);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
useEffect
中包裹函数中返回的函数,会在函数组件重新渲染时,清理上一帧数据时触发执行。因此这个函数可以做一些清理的工作。
如果 useEffect
给定的依赖项是一个空数组,那么返回函数被执行时,代表着组件真正被卸载了。
给
useEffect
设置 依赖项为空数组,并且 返回一个函数,那么这个返回的函数就相当于是componentWillUnmount
请注意,必须要设置依赖项为空数组。如果不是空数组,那么这个函数并不是在组件被卸载时触发,而是会在组件重新渲染,清理上一帧的数据时触发。
useEffect
正确的为 DOM
设置事件监听function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = function() {
console.log('count: ', count);
}
window.addEventListener('click', handleClick, false)
return () => {
window.removeEventListener('click', handleClick, false)
};
}, [count]);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
在 useEffect
中设置事件监听,在 return
的函数中对副作用进行清理,取消监听事件
useEffect、useCallback、useMemo
中获取到的 state、props
为什么是旧值正如我们刚才所说,函数组件的每一帧会有自己独立的 state、function、props
。而 useEffect、useCallback、useMemo
具有缓存功能。
因此,我们取的是当前对应函数作用域下的变量。如果没有正确的设置依赖项,那么 useEffect、useCallback、useMemo
就不会重新执行,其中使用的变量还是之前的值。
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = function() {
console.log('count: ', count);
}
window.addEventListener('click', handleClick, false)
return () => {
window.removeEventListener('click', handleClick, false)
};
}, []);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
还是上一个例子,如果此时给
useEffect
设置空数组为依赖项,那么无论count
改变了多少次,点击window
,打印出来的count
依然是 0
useEffect
中为什么会出现无限执行的情况useEffect
设置依赖项,并且在 useEffect
中更新 state
,会导致界面无限重复渲染function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
这种情况会导致界面无限重复渲染,因为没有设置依赖项,如果我们想在界面初次渲染时,给 count
设置新值,给依赖项设置空数组即可。
修改后:只会在初始化时设置 count
值
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
}, []);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
上面这个例子是依赖项缺失的时候,会出现问题,那么在依赖项正常设置的情况下,也会出现问题。
count
增加的时候,我们需要进行翻页(page
+ 1),看看如何写:由于此时我们依赖 count
,依赖项中要包含 count
,而修改 page
时又需要依赖 page
,所以依赖项中也要包含 page
function Counter() {
const [count, setCount] = useState(0);
const [page, setPage] = useState(0);
useEffect(() => {
setPage(page + 1);
}, [count, page]);
return (
<>
<p onClick={() => setCount(count + 1)}>clicked {count} times</p>
<p>page: {page}</p>
</>
);
}
此时也会导致界面无限重复渲染的情况,那么此时修改 page
时改成函数的方式,并从依赖性中移除 page
即可
修改后:既能实现效果,又避免了重复渲染
function Counter() {
const [count, setCount] = useState(0);
const [page, setPage] = useState(0);
useEffect(() => {
setPage(p => p + 1);
}, [count]);
return (
<>
<p onClick={() => setCount(count + 1)}>clicked {count} times</p>
<p>page: {page}</p>
</>
);
}
执行更早但返回更晚的情况会错误的对状态值进行覆盖
在 useEffect
中,可能会有进行网络请求的场景,我们会根据父组件传入的 id
,去发起网络请求,id
变化时,会重新进行请求。
function App() {
const [id, setId] = useState(0);
useEffect(() => {
setId(10);
}, []);
// 传递 id 属性
return <Counter id={id} />;
}
// 模拟网络请求
const fetchData = id =>
new Promise(resolve => {
setTimeout(() => {
const result = `id 为${id} 的请求结果`;
resolve(result);
}, Math.random() * 1000 + 1000);
});
function Counter({ id }) {
const [data, setData] = useState('请求中。。。');
useEffect(() => {
// 发送网络请求,修改界面展示信息
const getData = async () => {
const result = await fetchData(id);
setData(result);
};
getData();
}, [id]);
return <p>result: {data}</p>;
}
展示结果:
上面的实例,多次刷新页面,可以看到最终结果有时展示的是 id 为 0 的请求结果
,有时是 id 为 10 的结果
。
正确的结果应该是 ‘id 为 10 的请求结果’。这个就是竞态带来的问题。
解决办法:
// 存储网络请求的 Map
const fetchMap = new Map();
// 模拟网络请求
const fetchData = id =>
new Promise(resolve => {
const timer = setTimeout(() => {
const result = `id 为${id} 的请求结果`;
// 请求结束移除对应的 id
fetchMap.delete(id);
resolve(result);
}, Math.random() * 1000 + 1000);
// 设置 id 到 fetchMap
fetchMap.set(id, timer);
});
// 取消 id 对应网络请求
const removeFetch = (id) => {
clearTimeout(fetchMap.get(id));
}
function Counter({ id }) {
const [data, setData] = useState('请求中。。。');
useEffect(() => {
const getData = async () => {
const result = await fetchData(id);
setData(result);
};
getData();
return () => {
// 取消对应网络请求
removeFetch(id)
}
}, [id]);
return <p>result: {data}</p>;
}
展示结果:
此时无论如何刷新页面,都只展示 id 为 10 的请求结果
。
// 模拟网络请求
const fetchData = id =>
new Promise(resolve => {
setTimeout(() => {
const result = `id 为${id} 的请求结果`;
resolve(result);
}, Math.random() * 1000 + 1000);
});
function Counter({ id }) {
const [data, setData] = useState('请求中。。。');
useEffect(() => {
let didCancel = false;
const getData = async () => {
const result = await fetchData(id);
if (!didCancel) {
setData(result);
}
};
getData();
return () => {
didCancel = true;
};
}, [id]);
return <p>result: {data}</p>;
}
可以发现,此时无论如何刷新页面,也都只展示 id 为 10 的请求结果
。
state
、props
的值函数组件是没有 this
指向的,所以为了可以保存住组件实例的属性,可以使用 useRef
来进行操作
函数组件的 ref
具有可以 穿透闭包 的能力。通过将普通类型的值转换为一个带有 current
属性的对象引用,来保证每次访问到的属性值是最新的。
state
值是相同的useRef
的情况下,每一帧里的 state
值是如何打印的function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = function() {
console.log('count: ', count);
}
window.addEventListener('click', handleClick, false)
});
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
先点击 p
标签 5 次,之后点击 window
对象,可以看到打印结果:
useRef
之后,每一帧里的 ref
值是如何打印的function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
// 将最新 state 设置给 countRef.current
countRef.current = count;
const handleClick = function () {
console.log('count: ', countRef.current);
};
window.addEventListener('click', handleClick, false);
});
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
和之前一样的操作,先点击 p
标签 5 次,之后点击 window
界面,可以看到打印结果
使用
useRef
即可以保证函数组件的每一帧里访问到的state
值是相同的。
函数组件是没有实例的,因此属性也无法挂载到 this
上。那如果我们想创建一个非 state
、props
变量,能够跟随函数组件进行创建销毁,该如何操作呢?
同样的,还是可以通过 useRef
,useRef
不仅可以作用在 DOM
上,还可以将普通变量转化成带有 current
属性的对象
比如,我们希望设置一个 Model
的实例,在组件创建时,生成 model
实例,组件销毁后,重新创建,会自动生成新的 model
实例
class Model {
constructor() {
console.log('创建 Model');
this.data = [];
}
}
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(new Model());
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
按照这种写法,可以实现在函数组件创建时,生成 Model
的实例,挂载到 countRef
的 current
属性上。重新渲染时,不会再给 countRef
重新赋值。
也就意味着在组件卸载之前使用的都是同一个 Model
实例,在卸载之后,当前 model
实例也会随之销毁。
仔细观察控制台的输出,会发现虽然
countRef
没有被重新赋值,但是在组件在重新渲染时,Model
的构造函数却依然会多次执行
所以此时我们可以借用 useState
的特性,改写一下。
class Model {
constructor() {
console.log('创建 Model');
this.data = [];
}
}
function Counter() {
const [count, setCount] = useState(0);
const [model] = useState(() => new Model());
const countRef = useRef(model);
return <p onClick={() => setCount(count + 1)}>clicked {count} times</p>;
}
这样使用,可以在不修改 state
的情况下,使用 model
实例中的一些属性,可以使 flag
,可以是数据源,甚至可以作为 Mobx
的 store
进行使用。
如题,当依赖频繁变更时,如何避免 useCallback
频繁执行呢?
function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return <p onClick={handleClick}>clicked {count} times</p>;
}
这里,我们把 click
事件提取出来,使用 useCallback
包裹,但其实并没有起到很好的效果。
因为 Counter
组件重新渲染目前只依赖 count
的变化,所以这里的 useCallback
用与不用没什么区别。
useReducer
替代 useState
可以使用 useReducer
进行替代。
function Counter() {
const [count, dispatch] = useReducer(x => x + 1, 0);
const handleClick = useCallback(() => {
dispatch();
}, []);
return <p onClick={handleClick}>clicked {count} times</p>;
}
useReducer
返回的 dispatch
函数是自带了 memoize
的,不会在多次渲染时改变。因此在 useCallback
中不需要将 dispatch
作为依赖项。
setState
中传递函数function Counter() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []);
return <p onClick={handleClick}>clicked {count} times</p>;
}
在 setCount
中使用函数作为参数时,接收到的值是最新的 state
值,因此可以通过这个值执行操作。
useRef
进行闭包穿透function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
const handleClick = useCallback(() => {
setCount(countRef.current + 1);
}, []);
return <p onClick={handleClick}>clicked {count} times</p>;
}
这种方式也可以实现同样的效果。但是不推荐使用,不仅要编写更多的代码,而且可能会产生出乎预料的问题。
上面讲述了 useCallback
的一些问题和解决办法。下面看一看 useMemo
。
useMemo
和 React.memo
不同:
useMemo
是对组件内部的一些数据进行优化和缓存,惰性处理。React.memo
是对函数组件进行包裹,对组件内部的 state
、 props
进行浅比较,判断是否需要进行渲染。useMemo
和 useCallback
的区别
useMemo
的返回值是一个值,可以是属性,可以是函数(包括组件)useCallback
的返回值只能是函数因此,useMemo
一定程度上可以替代 useCallback
,等价条件:useCallback(fn, deps) => useMemo(() => fn, deps)
所以,上述关于 useCallback
一些优化点同样适用于 useMemo
。
这里先说一下我的浅见:不建议频繁使用
各位大佬先别开喷,容我说一说自己的观点
原因:
原因解释了一波,那 useCallback 和 useMemo 是不是就没有意义呢,当然不是,一点作用没有的话,React 何必提供出来呢。
用还是要用的,不过我们需要根据情况进行判断,什么时候去使用。
下面介绍一些 useCallback 和 useMemo 适用的场景
场景一:需要对子组件进行性能优化
这个例子中,App 会向子组件 Foo 传递一个函数属性 onClick
使用 useCallback 进行优化前的代码
App.js
import React, { useState } from 'react';
import Foo from './Foo';
function App() {
const [count, setCount] = useState(0);
const fooClick = () => {
console.log('点击了 Foo 组件的按钮');
};
return (
<div style={{ padding: 50 }}>
<Foo onClick={fooClick} />
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>count increment</button>
</div>
);
}
export default App;
Foo.js
import React from 'react';
const Foo = ({ onClick }) => {
console.log('Foo 组件: render');
return <button onClick={onClick}>Foo 组件中的 button</button>;
};
export default Foo;
点击 App 中的 count increment 按钮,可以看到子组件 Foo 每次都会重新 render,但其实在 count 变化时,父组件重新 render,而子组件却不需要重新 render,当前情况自然没有什么问题。
但是如果 Foo 组件是一个非常复杂庞大的组件,那么此时就有必要对 Foo 组件进行优化,useCallback 就能派上用场了。
使用 useCallback 进行优化后的代码
App.js
中将传递给子组件的函数属性用 useCallback 包裹起来
import React, { useCallback, useState } from 'react';
import Foo from './Foo';
function App() {
const [count, setCount] = useState(0);
const fooClick = useCallback(() => {
console.log('点击了 Foo 组件的按钮');
}, []);
return (
<div style={{ padding: 50 }}>
<Foo onClick={fooClick} />
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>count increment</button>
</div>
);
}
export default App;
Foo.js
中使用 React.memo 对组件进行包裹(类组件的话继承 PureComponent 是同样的效果)
import React from 'react';
const Foo = ({ onClick }) => {
console.log('Foo 组件: render');
return <button onClick={onClick}>Foo 组件中的 button</button>;
};
export default React.memo(Foo);
此时再点击 count increment
按钮,可以看到,父组件更新,但是子组件不会重新 render
场景二:需要作为其他 hooks
的依赖,这里仅使用 useEffect
进行演示
这个例子中,会根据状态 page
的变化去重新请求网络数据,当 page
发生变化,我们希望能触发 useEffect
调用网络请求,而 useEffect
中调用了 getDetail
函数,为了用到最新的 page
,所以在 useEffect
中需要依赖 getDetail
函数,用以调用最新的 getDetail
使用 useCallback
处理前的代码
App.js
import React, { useEffect, useState } from 'react';
const request = (p) =>
new Promise(resolve => setTimeout(() => resolve({ content: `第 ${p} 页数据` }), 300));
function App() {
const [page, setPage] = useState(1);
const [detail, setDetail] = useState('');
const getDetail = () => {
request(page).then(res => setDetail(res));
};
useEffect(() => {
getDetail();
}, [getDetail]);
console.log('App 组件:render');
return (
<div style={{ padding: 50 }}>
<p>Detail: {detail.content}</p>
<p>Current page: {page}</p>
<button onClick={() => setPage(page + 1)}>page increment</button>
</div>
);
}
export default App;
但是按照上面的写法,会导致 App
组件无限循环进行 render
,此时就需要用到 useCallback
进行处理
使用 useCallback
处理后的代码
App.js
import React, { useEffect, useState, useCallback } from 'react';
const request = (p) =>
new Promise(resolve => setTimeout(() => resolve({ content: `第 ${p} 页数据` }), 300));
function App() {
const [page, setPage] = useState(1);
const [detail, setDetail] = useState('');
const getDetail = useCallback(() => {
request(page).then(res => setDetail(res));
}, [page]);
useEffect(() => {
getDetail();
}, [getDetail]);
console.log('App 组件:render');
return (
<div style={{ padding: 50 }}>
<p>Detail: {detail.content}</p>
<p>Current page: {page}</p>
<button onClick={() => setPage(page + 1)}>page increment</button>
</div>
);
}
export default App;
此时可以看到,App
组件可以正常的进行 render
了。这里仅使用 useEffect
进行演示,作为其他 hooks
的依赖项时,也需要照此进行优化
useCallback
使用场景总结:
向子组件传递函数属性,并且子组件需要进行优化时,需要对函数属性进行 useCallback
包裹
函数作为其他 hooks
的依赖项时,需要对函数进行 useCallback
包裹
同 useCallback
场景一:需要对子组件进行性能优化时,用法也基本一致
同 useCallback
场景二:需要作为其他 hooks
的依赖时,用法也基本一致
需要进行大量或者复杂运算时,为了提高性能,可以使用 useMemo
进行数据缓存
这里也是用到了 useMemo 的数据缓存功能,在依赖项发生变化之前,useMemo 中包裹的函数不会重新执行
看下面这个例子,App
组件中两个状态:count
和 Number
数组 dataSource
,点击 increment
按钮,count
会增加,点击 fresh
按钮,会重新获取 dataSource
,但是界面上并不需要展示 dataSource
,而是需要展示 dataSource
中所有元素的和,所以我们需要一个新的变量 sum
来承载,展示到页面上。
下面看代码
使用 useMemo
优化前的代码
App.js
import React, { useState } from 'react';
const request = () =>
new Promise(resolve =>
setTimeout(
() => resolve(Array.from({ length: 100 }, () => Math.floor(100 * Math.random()))),
300
)
);
function App() {
const [count, setCount] = useState(1);
const [dataSource, setDataSource] = useState([]);
const reduceDataSource = () => {
console.log('reduce');
return dataSource.reduce((reducer, item) => {
return reducer + item;
}, 0);
};
const sum = reduceDataSource();
const refreshClick = () => {
request().then(res => setDataSource(res));
};
return (
<div style={{ padding: 50 }}>
<p>DataSource 元素之和: {sum}</p>
<button onClick={refreshClick}>Refresh</button>
<p>Current count: {count}</p>
<button onClick={() => setCount(count + 1)}>increment</button>
</div>
);
}
export default App;
打开控制台,可以看到,此时无论点击 increment
或者 Refresh
按钮,reduceDataSource
函数都会执行一次,但是 dataSource
中有 100 个元素,所以我们肯定是希望在 dataSource
变化时才重新计算 sum
值,这时候 useMemo
就排上用场了。
使用 useMemo
优化后的代码
App.js
import React, { useMemo, useState } from 'react';
const request = () =>
new Promise(resolve =>
setTimeout(
() => resolve(Array.from({ length: 100 }, () => Math.floor(100 * Math.random()))),
300
)
);
function App() {
const [count, setCount] = useState(1);
const [dataSource, setDataSource] = useState([]);
const sum = useMemo(() => {
console.log('reduce');
return dataSource.reduce((reducer, item) => {
return reducer + item;
}, 0);
}, [dataSource]);
const refreshClick = () => {
request().then(res => setDataSource(res));
};
return (
<div style={{ padding: 50 }}>
<p>DataSource 元素之和: {sum}</p>
<button onClick={refreshClick}>Refresh</button>
<p>Current count: {count}</p>
<button onClick={() => setCount(count + 1)}>increment</button>
</div>
);
}
export default App;
此时可以看到,只有点击 Refresh
按钮 时,useMemo
中的函数才会重新执行。点击 increment
按钮时,sum 还是之前的缓存结果,不会重新计算。
useMemo
使用场景总结:
向子组件传递 引用类型 属性,并且子组件需要进行优化时,需要对属性进行 useMemo
包裹
引用类型值,作为其他 hooks
的依赖项时,需要使用 useMemo
包裹,返回属性值
需要进行大量或者复杂运算时,为了提高性能,可以使用 useMemo
进行数据缓存,节约计算成本
所以,在 useCallback 和 useMemo 使用过程中,如非必要,无需使用,频繁使用反而可能会增加依赖对比的成本,降低性能。
在函数组件中,没有组件实例,所以无法像类组件中,通过绑定子组件的实例调用子组件中的状态或者方法。
那么在函数组件中,如何在父组件调用子组件的状态或者方法呢?答案就是使用 useImperativeHandle
useImperativeHandle(ref, createHandle, [deps])
第一个参数是 ref 值,可以通过属性传入,也可以配合 forwardRef 使用
第二个参数是一个函数,返回一个对象,对象中的属性都会被挂载到第一个参数 ref 的 current 属性上
第三个参数是依赖的元素集合,同 useEffect、useCallback、useMemo,当依赖发生变化时,第二个参数会重新执行,重新挂载到第一个参数的 current 属性上
注意:
createHandle
重复执行hook
中,对于同一个 ref
,只能使用一次 useImperativeHandle
,多次的话,后面执行的 useImperativeHandle
的 createHandle
返回值会替换掉前面执行的 useImperativeHandle
的 createHandle
返回值Foo.js
import React, { useState, useImperativeHandle, useCallback } from 'react';
const Foo = ({ actionRef }) => {
const [value, setValue] = useState('');
/**
* 随机修改 value 值的函数
*/
const randomValue = useCallback(() => {
setValue(Math.round(Math.random() * 100) + '');
}, []);
/**
* 提交函数
*/
const submit = useCallback(() => {
if (value) {
alert(`提交成功,用户名为:${value}`);
} else {
alert('请输入用户名!');
}
}, [value]);
useImperativeHandle(
actionRef,
() => {
return {
randomValue,
submit,
};
},
[randomValue, submit]
);
/* !! 返回多个属性要按照上面这种写法,不能像下面这样使用多个 useImperativeHandle
useImperativeHandle(actionRef, () => {
return {
submit,
}
}, [submit])
useImperativeHandle(actionRef, () => {
return {
randomValue
}
}, [randomValue])
*/
return (
<div className="box">
<h2>函数组件</h2>
<section>
<label>用户名:</label>
<input
value={value}
placeholder="请输入用户名"
onChange={e => setValue(e.target.value)}
/>
</section>
<br />
</div>
);
};
export default Foo;
App.js
import React, { useRef } from 'react';
import Foo from './Foo'
const App = () => {
const childRef = useRef();
return (
<div>
<Foo actionRef={childRef} />
<button onClick={() => childRef.current.submit()}>调用子组件的提交函数</button>
<br />
<br />
<button onClick={() => childRef.current.randomValue()}>
随机修改子组件的 input 值
</button>
</div>
);
};
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
本文详细介绍了 React 生命周期的用法以及各个阶段的生命周期进行,并且用实例代码做了详细演示。
本文已收录在 Github
: https://github.com/beichensky/Blog 中,欢迎 Star!
上图是基于 React 16.4 之后的生命周期图解。如感觉不对,请先查看 React 版本
在 React 组件挂载之前被调用,实现 React.Component
的子类的构造函数时,要在第一行加上 super(props)。
React 构造函数通常只用于两个目的:
this.state
来初始化本地 state
如果没有初始化状态(state
),并且没有绑定方法,通常不需要为 React
组件实现一个构造函数。
不需要在构造函数中调用
setState()
,只需将初始状态设置给this.state
即可 。
getDerivedStateFromProps
在每次调用 render 方法之前调用。包括初始化和后续更新时。
包含两个参数:第一个参数为即将更新的
props
值,第二个参数为之前的state
值
返回值:返回为
null
时,不做任何副作用处理。倘若想更新某些state
状态值,则返回一个对象,就会对state
进行修改
该生命周期是静态函数,属于类的方法,其作用域内是找不到
this
的
render()
方法是类组件中唯一必须的方法,其余生命周期不是必须要写。
组件渲染时会走到该生命周期,展示的组件都是由 render() 生命周期的返回值来决定。
注意:
如果 shouldComponentUpdate() 方法返回 false ,render() 不会被调用。
在 React 组件装载(mounting)(插入树)后被立即调用。
componentDidMount 生命周期是进行发送网络请求、启用事件监听的好时机
如果有必要,可以在此生命周期中立刻调用 setState()
在组件准备更新之前调用,可以控制组件是否进行更新,返回 true 时组件更新,返回 false 组件不更新。
包含两个参数,第一个是即将更新的 props 值,第二个是即将跟新后的 state 值,可以根据更新前后的 props 或 state 进行判断,决定是否更新,进行性能优化
不要
shouldComponentUpdate
中调用 setState(),否则会导致无限循环调用更新、渲染,直至浏览器内存崩溃
getSnapshotBeforeUpdate()
在最近一次的渲染输出被提交之前调用。也就是说,在 render 之后,即将对组件进行挂载时调用。
它可以使组件在 DOM 真正更新之前捕获一些信息(例如滚动位置),此生命周期返回的任何值都会作为参数传递给
componentDidUpdate()
。如不需要传递任何值,那么请返回 null
componentDidUpdate()
在更新发生之后立即被调用。这个生命周期在组件第一次渲染时不会触发。
可以在此生命周期中调用 setState(),但是必须包含在条件语句中,否则会造成无限循环,最终导致浏览器内存崩溃
componentWillUnmount()
在组件即将被卸载或销毁时进行调用。
此生命周期是取消网络请求、移除监听事件、清理 DOM 元素、清理定时器等操作的好时机
注意:
componentWillMount()、componentWillUpdate()、componentWillReceiveProps() 即将被废弃,请不要再在组件中进行使用。因此本文不做讲解,避免混淆。
constructor()
static getDerivedStateFromProps()
render()
componentDidMount()
static getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentWillUnmount()
下面根据一个父子组件的props 改变、state 改变以及子组件的挂载/卸载等事件,对各生命周期执行顺序进行理解,有兴趣的同学可以一起看一下,也可以下载代码自己进行测试。
import React, { Component } from 'react';
import Child from './Child.js';
const parentStyle = {
padding: 40,
margin: 20,
border: '1px solid pink'
}
const TAG = "Parent 组件:"
export default class Parent extends Component {
constructor(props) {
super(props);
console.log(TAG, 'constructor');
this.state = {
num: 0,
mountChild: true
}
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log(TAG, 'getDerivedStateFromProps');
return null;
}
componentDidMount() {
console.log(TAG, 'componentDidMount');
}
shouldComponentUpdate(nextProps, nextState) {
console.log(TAG, 'shouldComponentUpdate');
return true;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log(TAG, 'getSnapshotBeforeUpdate');
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(TAG, 'componentDidUpdate');
}
componentWillUnmount() {
console.log(TAG, 'componentWillUnmount');
}
/**
* 修改传给子组件属性 num 的方法
*/
changeNum = () => {
let { num } = this.state;
this.setState({
num: ++ num
});
}
/**
* 切换子组件挂载和卸载的方法
*/
toggleMountChild = () => {
let { mountChild } = this.state;
this.setState({
mountChild: !mountChild
});
}
render() {
console.log(TAG, 'render');
const { num, mountChild } = this.state;
return (
<div style={ parentStyle }>
<div>
<p>父组件</p>
<button onClick={ this.changeNum }>改变传给子组件的属性 num</button>
<br />
<br />
<button onClick={ this.toggleMountChild }>卸载 / 挂载子组件</button>
</div>
{
mountChild ? <Child num={ num } /> : null
}
</div>
)
}
}
import React, { Component } from 'react'
const childStyle = {
padding: 20,
margin: 20,
border: '1px solid black'
}
const TAG = 'Child 组件:'
export default class Child extends Component {
constructor(props) {
super(props);
console.log(TAG, 'constructor');
this.state = {
counter: 0
};
}
static getDerivedStateFromProps(nextProps, prevState) {
console.log(TAG, 'getDerivedStateFromProps');
return null;
}
componentDidMount() {
console.log(TAG, 'componentDidMount');
}
shouldComponentUpdate(nextProps, nextState) {
console.log(TAG, 'shouldComponentUpdate');
return true;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log(TAG, 'getSnapshotBeforeUpdate');
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log(TAG, 'componentDidUpdate');
}
componentWillUnmount() {
console.log(TAG, 'componentWillUnmount');
}
changeCounter = () => {
let { counter }= this.state;
this.setState({
counter: ++ counter
});
}
render() {
console.log(TAG, 'render');
const { num } = this.props;
const { counter } = this.state;
return (
<div style={ childStyle }>
<p>子组件</p>
<p>父组件传过来的属性 num : { num }</p>
<p>自身状态 counter : { counter }</p>
<button onClick={ this.changeCounter }>改变自身状态 counter</button>
</div>
)
}
}
控制台中的 log 打印顺序为:
点击子组件中的 改变自身状态 按钮,则界面上 自身状态 counter: 的值会 + 1,控制台中的 log 打印顺序为:
点击父组件中的 改变传给子组件的属性 num 按钮,则界面上 父组件传过来的属性 num: 的值会 + 1,控制台中的 log 打印顺序为:
点击父组件中的 卸载 / 挂载子组件 按钮,则界面上子组件会消失,控制台中的 log 打印顺序为:
再次点击父组件中的 卸载 / 挂载子组件 按钮,则界面上子组件会重新渲染出来,控制台中的 log 打印顺序为:
当子组件自身状态改变时,不会对父组件产生副作用的情况下,父组件不会进行更新,即不会触发父组件的生命周期
当父组件中状态发生变化(包括子组件的挂载以及)时,会触发自身对应的生命周期以及子组件的更新
render 以及 render 之前的生命周期,则 父组件 先执行
render 以及 render 之后的声明周期,则子组件先执行,并且是与父组件交替执行
当子组件进行卸载时,只会执行自身的 componentWillUnmount 生命周期,不会再触发别的生命周期
可能总结的不好,不是很完整。只是根据一般情况进行的总结。有不妥之处,希望各位朋友能够多多指正。
源码地址(欢迎 Star,谢谢!)
还没看够?移步至:React Component 官网
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
本文已收录在 Github
: https://github.com/beichensky/Blog 中,欢迎 Star!
本文代码通过 create-react-app
脚手架进行搭建,所有的代码均可直接复制运行。
代码位置:react-redux-demo
本文主要讲解了 Redux 和 React-Redux 的使用,详细的概念以及设计**请看 Redux 中文官网
create-react-app
创建 React
项目# npx
npx create-react-app my-app
# npm
npm init react-app my-app
# yarn
yarn create react-app my-app
redux
cd my-app
npm install redux
# or
yarn add redux
是把数据从组件传到 store
的载体,是修改 store
数据的唯一来源。
是一个普通的 javascript
对象,必须包含一个 type
属性,用来通知 reducer
这个 action
需要做的操作类型。
比如:
{
type: 'ADD',
payload: 1
}
通过 store.dispatch(action)
将 action
传给 store
描述 store
数据如何更新的纯函数,接受两个参数
state
:store
中的 state
值,可以给 state
设置初始值
action
:通过 store.dispatch(action)
传递的 action
对象
通过 action
的 type
类型来判断如何更新 state
数据
比如:
function reducer(state = 0, { type, payload }) {
switch (type) {
case "ADD":
return state + payload;
case "DELETE":
return state - payload;
default:
return state;
}
}
将 action
和 reducer
联系到一起的对象,具有以下职责
state
getState()
方法获取 state
dispatch(action)
方法更新 state
subscribe(listener)
注册监听器;subscribe(listener)
返回的函数注销监听器store
的创建方式
const store = createStore(reducer[, prevState, ehancer]);
store.dispatch(action)
通知数据更新store
获取 state
数据reducer
实现数据更新store/index.js
import { createStore } from "redux";
// 创建 reducer 函数 ,更新 state 数据
const reducer = function(state = 0, { type }) {
switch (type) {
case "INCREMENT":
return ++state;
case "DECREMENT":
return --state;
default:
return state;
}
};
// 创建 store
const store = createStore(reducer);
export default store;
App.js
import React, { useEffect, useReducer, useCallback } from "react";
import store from "./store";
function App() {
// 模拟 forceUpdate 方法
const [, forceUpdate] = useReducer(x => x + 1, 0);
useEffect(() => {
// 订阅 store 监听事件
const unsubscribe = store.subscribe(() => {
forceUpdate();
});
return () => {
// 组件销毁时移除事件订阅
unsubscribe();
};
}, []);
const increment = useCallback(
// 分发 action
() => store.dispatch({ type: "INCREMENT" }),
[]
);
const decrement = useCallback(
// 分发 action
() => store.dispatch({ type: "DECREMENT" }),
[]
);
return (
<div className="App">
<h1>Hello Redux</h1>
{/* 获取当前 state 值 */}
<p>count: {store.getState()}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
</div>
);
}
export default App;
这个时候,计数器已经实现了,点击 increment
或者 decrement
按钮,会更新界面上的数据
那假如说,此时我们可能会处理多个业务场景,比如一个是计数器,一个是
TodoList
,会有两个reducer
,这个时候该如何创建呢?请看下一个API
是一个高阶函数,作用是将多个 reducer
函数按照合并生成一个 reducer
函数。
接受一个对象,返回一个 reducer
函数。对象的键可以设置任意属性名,对象的值是对应的 reducer
函数。
在使用
store
中的state
值时,state
中的对应的属性名就是之前传给combineReducers
方法的对象的属性名。
比如:
const reducer = combineReducers({
count: counterReducer,
todos: todoReducer
});
获取 state
时:
const state = store.getState();
// state: { count: xxx, todos: xxx }
我们在上面的例子中再加一个展示 TodoList
的功能
store/index.js
import { createStore, combineReducers } from "redux";
// 创建 counterReducer 函数 ,更新 state 数据
const counterReducer = function(state = 0, { type }) {
switch (type) {
case "INCREMENT":
return ++state;
case "DECREMENT":
return --state;
default:
return state;
}
};
// 创建 todoReducer 函数,更新 state 数据
const todoReducer = function(state = [], { type, payload }) {
switch (type) {
case "INIT":
return payload;
case "ADD":
state.push(payload);
return [...state];
default:
return state;
}
};
// 合并 reducer
const reducer = combineReducers({
count: counterReducer,
todos: todoReducer
});
// 创建 store
const store = createStore(reducer);
export default store;
App.js
import React, { useEffect, useReducer, useCallback, useState } from "react";
import store from "./store";
function App() {
// 模拟 forceUpdate 方法
const [, forceUpdate] = useReducer(x => x + 1, 0);
const [value, setValue] = useState("");
useEffect(() => {
// 订阅 store 监听事件
const unsubscribe = store.subscribe(() => {
forceUpdate();
});
return () => {
// 组件销毁时移除事件订阅
unsubscribe();
};
}, []);
const increment = useCallback(
// 分发 action
() => store.dispatch({ type: "INCREMENT" }),
[]
);
const decrement = useCallback(
// 分发 action
() => store.dispatch({ type: "DECREMENT" }),
[]
);
const add = useCallback(() => {
if (value) {
// 分发 action
store.dispatch({ type: "ADD", payload: value });
setValue("");
}
}, [value]);
// 解构 state
const { count, todos } = store.getState();
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
export default App;
至此,计数器和 TodoList
的功能都已经实现了
我们现在一直用的都是
redux
自己的功能,如果我想使用一些插件该怎么做呢,比如我想使用logger
插件打印一些日志,请看下一个API
使用 applyMiddleware
可以应用插件,扩展 redux
功能。
applyMiddleware
是一个函数,接受多个参数值,返回一个高阶函数供 createStore
使用。
const ehancer = applyMiddleware(middleware1[, middleware2, middleware3, ...]);
下面我们以 redux-logger
插件为例,使用 applyMiddleware
:
安装 redux-logger
npm install redux-logger -D
# or
yarn add redux-logger -D
store/index.js
// ...
// 合并 reducer
const reducer = combineReducers({
count: counterReducer,
todos: todoReducer
});
// 应用插件
const ehancer = applyMiddleware(logger);
// 创建 store
const store = createStore(reducer, ehancer);
export default store;
上面我们修改数据的时候一直都是在同步状态下进行,那如果现在有一个副作用操作,需要异步执行完成才能进行
state
更新,又该怎么做呢?就要用到redux-thunk
插件了
这个插件把 store
的 dispatch
方法做了一层封装,可以接受一个函数作为 action
。
当判断当前 action
是一个函数的时候,会自动执行,并将 dispatch
作为参数传给我们。
安装 redux-thunk
npm install redux-thunk -D
# or
yarn add redux-thunk -D
下面我们看看 redux-thunk
的用法以及使用场景
还是在刚才的例子上,我们想要在组件加载完成之后对 TodoList
添加一些初始值,这个过程是一个异步过程
将 redux-thunk
插件应用到 store
中去
store/index.js
import thunk from "redux-thunk";
// ...
// 应用插件
const ehancer = applyMiddleware(thunk, logger);
// 创建 store
const store = createStore(reducer, ehancer);
export default store;
App.js
// ...
useEffect(() => {
// 派发一个异步 action,是一个函数
store.dispatch(dispatch => {
setTimeout(() => {
dispatch({ type: "INIT", payload: ["吃饭", "睡觉", "敲代码"] });
}, 1000);
});
}, []);
// ...
我们现在分发
action
的时候,都是直接dispatch
一个对象,代码少的情况下还好,多的话可能就比较复杂,还要和reducer
中的type
对应,所以写起来比较麻烦,下面我们介绍一个概念:ActionCreator
这不是一个 API
或者方法,只是一种**和实现。就是通过调用一个函数生成一个对应的 action
,在需要的时候我们直接调用这个函数,进行 dispatch
就可以了
比如:
const addTodo = todo => ({ type: "ADD", payload: todo });
这种写法我们 dispatch
的时候不用考虑 type
,也不用写键值对,只要传入正确的参数,就可以了。
下面我们就把刚才我们写的例子修改一下,使用这种 ActionCreator
的**去编写 action
store/index.js
中添加 actionCreator
并导出
// ...
const increment = () => ({ type: "INCREMENT" });
const decrement = () => ({ type: "DECREMENT" });
const initTodos = todos => ({ type: "INIT", payload: todos });
const addTodo = todo => ({ type: "ADD", payload: todo });
// 异步 action,执行完成之后调用同步 action
const getAsyncTodos = () => dispatch =>
setTimeout(() => dispatch(initTodos(["吃饭", "睡觉", "写代码"])), 1000);
export const actionCreators = { increment, decrement, getAsyncTodos, addTodo };
App.js
中 dispatch
中调用 actionCreator
import React, { useEffect, useReducer, useCallback, useState } from "react";
import store, { actionCreators } from "./store";
const {
increment as inc,
decrement as dec,
getAsyncTodos,
addTodo
} = actionCreators;
function App() {
// 模拟 forceUpdate 方法
const [, forceUpdate] = useReducer(x => x + 1, 0);
const [value, setValue] = useState("");
useEffect(() => {
// 订阅 store 监听事件
const unsubscribe = store.subscribe(() => {
forceUpdate();
});
return () => {
// 组件销毁时移除事件订阅
unsubscribe();
};
}, []);
useEffect(() => {
// 派发一个异步 action,是一个函数
store.dispatch(getAsyncTodos());
}, []);
const increment = useCallback(
// 分发 action
() => store.dispatch(inc()),
[]
);
const decrement = useCallback(
// 分发 action
() => store.dispatch(dec()),
[]
);
const add = useCallback(() => {
if (value) {
// 分发 action
store.dispatch(addTodo(value));
setValue("");
}
}, [value]);
// 解构 state
const { count, todos } = store.getState();
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
export default App;
但是这个时候,感觉还是有点麻烦,每次触发
store
更新都要使用dispatch(actionCreator(args))
才行,能不能直接调用方法,就能触发 store 更新呢。
当然也可以了,redux
为我们提供了一个bindActionCreators
函数
这个函数是将 dispatch
绑定到了 actionCreator
方法上,之后只要我们执行 actionCreator
就会触发 store
更新了,不用每次都 dispacth
了。
接受两个参数:
actionCreators
:是一个对象,对象的属性名可以任意命名,属性值是对应的 actionCreator
方法dispatch
:store
中的 dipatch
属性返回一个新的对象,对象的属性名是刚才传入的 actionCreators
中的属性名,属性值时包装后的方法,执行即可触发 store
更新
比如:
const finalActions = bindActionCreators({
increment: () => ({ type: 'INCREMENT }),
decrement: () => ({ type: 'DECREMENT })
}, dispatch)
// finalActions: { increment, decrement }
下面我们将 App.js
中的代码进行一波优化,看看最后的效果
import React, { useEffect, useReducer, useCallback, useState } from "react";
import { bindActionCreators } from "redux";
import store, { actionCreators } from "./store";
// 生成包装后的 actionCreator,执行之后就会触发 store 数据的更新
const { increment, decrement, getAsyncTodos, addTodo } = bindActionCreators(
actionCreators,
store.dispatch
);
function App() {
// 模拟 forceUpdate 方法
const [, forceUpdate] = useReducer(x => x + 1, 0);
const [value, setValue] = useState("");
useEffect(() => {
// 订阅 store 监听事件
const unsubscribe = store.subscribe(() => {
forceUpdate();
});
return () => {
// 组件销毁时移除事件订阅
unsubscribe();
};
}, []);
// 初始化 TodoList
useEffect(() => {
getAsyncTodos();
}, []);
const add = useCallback(() => {
if (value) {
// 分发 action
addTodo(value);
setValue("");
}
}, [value]);
// 解构 state
const { count, todos } = store.getState();
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
export default App;
有没有发现,到目前为止,数据变化时,我们更新 React 组件还是通过自己去添加 subscribe 订阅,一个组件还好,那在项目开发的过程中,每个组件都这么写,岂不是太麻烦了。
可以看我的下一篇博客:十分钟学会 react-redux,详细介绍了react-redux
的用法。
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
本文已收录在 Github
: https://github.com/beichensky/Blog 中,欢迎 Star!
Context
的创建API: const MyContext = React.createContext(initialValue)
initialValue
:context
初始值
返回值
MyContext.Provider
: 提供者,是一个 React
组件,使用 Provider
标签包裹后的组件,自身以及后代组件都可以访问到 MyContext
的值
MyContext.Consumer
: 消费者,是 React
组件,使用 Consumer
包裹后,可以使用 render props
的方式渲染内容,获取到 MyContext
的值
import React from "react";
const defaultTheme = { color: "black" };
const ThemeContext = React.createContext(defaultTheme);
Context.Provider
的使用每个 Context
对象都会返回一个 Provider React
组件,它允许消费组件订阅 context
的变化。
使用一个 Provider
来将当前的 context
传递给以下的组件树,无论多深,任何组件都能读取这个值。
接受一个属性 value
,子组件中获取到的 context
值就是 value
值
Provider
使用function App() {
return (
<ThemeContext.Provider value={{ color: "blue" }}>
<p>Hello World</p>
</ThemeContext.Provider>
);
}
Provider
使用function App() {
const [count, setCount] = useState(0);
return (
<ThemeContext.Provider value={{ color: "blue" }}>
<CounterContext.Provider value={{ count, setCount }}>
<p>Hello World</p>
</CounterContext.Provider>
</ThemeContext.Provider>
);
}
Context
Context.Provider
不仅可以设置 value
,也可以动态的修改 value
当 value
值发生变化的时候,所有依赖改 Context
的子组件都会进行渲染
创建动态 CounterContext
const defaultTheme = { color: "black" };
const defaultCounter = {
count: 1,
setCount: () => {},
};
const ThemeContext = React.createContext(defaultTheme);
const CounterContext = React.createContext(defaultCounter);
将修改 value
的方法作为 value
的属性传递下去
function App() {
const [count, setCount] = useState(0);
return (
<ThemeContext.Provider value={{ color: "blue" }}>
<CounterContext.Provider value={{ count, setCount }}>
<p>App 页面 count: {count}</p>
<ContextType />
<HookContext />
<ConsumerContext />
</CounterContext.Provider>
</ThemeContext.Provider>
);
}
在子组件中调用修改 value
的方法
export default function HookContext() {
const { color } = useContext(ThemeContext);
const { count, setCount } = useContext(CounterContext);
return (
<>
<h2 style={{ color }}>useContext 使用</h2>
<p>
<div>HookContext 页面 count: {count}</div>
<button onClick={() => setCount(count + 1)}>increment</button>
</p>
</>
);
}
Context
值static contextType
Context.Consumer
useContext
Class.contextType
在类组件中设置静态属性 contextType
为某个 Context
在使用的时候通过 this.context
获取到 Context
的值
export default class ContextType extends Component {
static contextType = ThemeContext;
render() {
const { color } = this.context;
return <h2 style={{ color }}>ContextType 使用</h2>;
}
}
Context.Consumer
Context.Consumer
是一个 React 组件可以订阅 context
的变更,既可以在函数组件中使用也可以在类组件中使用
这种方法需要一个函数作为子元素(function as a child)
。这个函数接收当前的 context
值,并返回一个 React
节点。
传递给函数的 value
值等等价于组件树上方离这个 context
最近的 Provider
提供的 value
值。如果没有对应的 Provider
,value
参数等同于传递给 createContext()
的 defaultValue
Context
export default function ConsumerContext() {
return (
<ThemeContext.Consumer>
{(theme) => {
const { color } = theme;
return <h2 style={{ color }}>Context.Consumer</h2>;
}}
</ThemeContext.Consumer>
);
}
Context
export default function ConsumerContext() {
return (
<ThemeContext.Consumer>
{(theme) => (
<CounterContext.Consumer>
{(context) => {
const { color } = theme;
const { count } = context;
return (
<>
<h2 style={{ color }}>Context.Consumer</h2>
<p>ConsumerContext 页面 count: {count}</p>
</>
);
}}
</CounterContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
useContext
通过 useContext
可以获取到 value
值,参数是对应的 Context
可以再一个组件中使用多次 useContext
获取多个 Context
对应的 value
值
export default function HookContext() {
const { color } = useContext(ThemeContext);
const { count, setCount } = useContext(CounterContext);
return (
<>
<h2 style={{ color }}>useContext 使用</h2>
<p>
<div>HookContext 页面 count: {count}</div>
<button onClick={() => setCount(count + 1)}>increment</button>
</p>
</>
);
}
displayName
context
对象接受一个名为 displayName
的 property
,类型为字符串。React DevTools
使用该字符串来确定 context
要显示的内容。
示例,下述组件在 DevTools
中将显示为 MyDisplayName
:
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
创建 Context
: const MyContext = React.createContext(defaultValue)
使用 Context.Provider
组件将子组件进行包裹,则无论子组件层级多深,都可以获取到对应的 value
值
使用 Class.contextType
的方式获取 Context
,可以在组件中通过 this.context
的方式获取到对应的 value
值
Context
使用 Context.Consumer
组件消费 Context
Context
的 value
值,返回一个组件Context
使用 useContext
消费 Context
useContext
消费多个 Context
Context.Provider
中的 value
发生变化,则依赖当前 Context
的子组件都会发生进行更新
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
本文简要介绍了 React 中 PureComponent 与 Component 的区别以及使用时需要注意的问题,并在后面附上了源码解析,希望对有疑惑的朋友提供一些帮助。
先介绍一下 PureComponent,平时我们创建 React 组件一般是继承于 Component,而 PureComponent 相当于是一个更纯净的 Component,对更新前后的数据进行了一次浅比较。只有在数据真正发生改变时,才会对组件重新进行 render。因此可以大大提高组件的性能。
本文已收录在 Github
: https://github.com/beichensky/Blog 中,欢迎 Star!
App.js
里面的 state
有两个属性,text
属性是基本数据类型,todo
属性是引用类型。针对这两种数据类型分别进行对比:
import React, { Component, PureComponent } from 'react';
import './App.css';
class App extends Component {
constructor(props) {
super(props)
this.state = {
text: 'Hello',
todo: {
id: 1,
message: '学习 React'
}
}
}
/**
* 修改 state 中 text 属性的函数
*/
changeText = () => {
this.setState({
text: 'World'
});
}
/**
* 修改 state 中 todo 对象的函数
*/
changeTodo = () => {
this.setState({
id: 1,
message: '学习 Vue'
});
}
render() {
// 打印 log,查看渲染情况
console.log('tag', 'render');
const { text, todo } = this.state;
return (
<div className="App">
<div>
<span>文字:{ text }</span>
<button onClick={ this.changeText }>更改文字</button>
</div>
<br />
<div>
<span>计划:{ todo.message }</span>
<button onClick={ this.changeTodo }>更改计划</button>
</div>
</div>
);
}
}
export default App;
运行项目,打开控制台,此时看到只有一个 log
:tag render
点击 5 次 ·更改文字· 按钮,可以看到控制台再次多打印了 5 次 log
,浏览器中的 Hello
文字变成了 World
点击 5 次 ·更改计划· 按钮,控制台一样多打印 5 次 log
,浏览器中的 学习 React
计划变成了 学习 Vue
分析一下,其实 5 次点击中只有一次是有效的,后来的数据其实并没有真正改变,但是由于依然使用了 setState()
,所以还是会重新 render
。所以这种模式是比较消耗性能的。
其实 PureComponent
用法也是和 Component
一样,只不过是将继承 Component
换成了 PureComponent
。
...
// 上面的代码和之前一致
class App extends PureComponent {
// 下面的代码也和之前一样
...
}
export default App;
和上面 Component 的测试方式一样
点击 5 次 ·更改文字· 按钮,可以看到控制台只多打印了 1 次 log
,浏览器中的 Hello
文字变成了 World
点击 5 次 ·更改计划· 按钮,控制台只多打印了 1 次 log
,浏览器中的 学习 React
计划变成了 学习 Vue
由此可以看出,使用 PureComponent 还是比较节省性能的,即便是使用了 setState(),也会在数据真正改变时才会重新渲染组件
下面我们将代码中 changeText
和 changeTodo
方法修改一下
/**
* 修改 state 中 text 属性的函数
*/
changeText = () => {
let { text } = this.state;
text = 'World';
this.setState({
text
});
}
/**
* 修改 state 中 todo 对象的函数
*/
changeTodo = () => {
let { todo } = this.state;
todo.message = "学习 Vue";
this.setState({
todo
});
}
此时我们再重新测试一下:
点击 ·更改文字· 按钮,控制台多打印一次 log
,浏览器中的 Hello
文字变成了 World
**注意:**点击 ·更改计划· 按钮,控制台没有 log
打印,浏览器中的计划也没有发生改变
为什么代码修改之后,明明 todo 里的 message 属性也已经发生变化了,调用 setState(),却不进行渲染了呢?这是因为 PureComponent 在调用 shouldComponent 生命周期的时候,对数据进行了一次浅比较,判断数据是否发生改变,没发生改变,返回 false,改变了,就返回 true。那这个浅比较的机制是怎么做的呢?我们一起看下面源码解析,来分析一下。
ReactBaseClasses.js
(Github 代码位置)function ComponentDummy() {}
ComponentDummy.prototype = Component.prototype;
/**
* Convenience component with default shallow equality check for sCU.
*/
function PureComponent(props, context, updater) {
this.props = props;
this.context = context;
// If a component has string refs, we will assign a different object later.
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);
pureComponentPrototype.isPureReactComponent = true;
可以看到 PureComponent
的使用和 Component
一致,只时最后为其添加了一个 isPureReactComponent
属性。ComponentDummy
就是通过原型模拟继承的方式将 Component
原型中的方法和属性传递给了 PureComponent
。同时为了避免原型链拉长导致属性查找的性能消耗,通过 Object.assign
把属性从 Component
拷贝了过来。
但是这里只是 PureComponent
的声明创建,没有显示如何进行比较更新的,那我们继续看下面的代码。
ReactFiberClassComponent.js
(Github 代码位置)function checkShouldComponentUpdate(
workInProgress,
ctor,
oldProps,
newProps,
oldState,
newState,
nextContext,
) {
...
// 这里根据上面 PureComponent 设置的属性 isPureReactComponent 判断一下,如果是 PureComponent,就会走里面的代码,将比较的值返回出去
if (ctor.prototype && ctor.prototype.isPureReactComponent) {
return (
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
);
}
}
shallowEqual
是在 share
包中一个工具方法,看一下其中的内部实现吧。
shallowEqual.js
(Github 代码位置)import is from './objectIs';
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* Performs equality by iterating through keys on an object and returning false
* when any key has values which are not strictly equal between the arguments.
* Returns true when the values of all keys are strictly equal.
*/
function shallowEqual(objA: mixed, objB: mixed): boolean {
if (is(objA, objB)) {
return true;
}
if (
typeof objA !== 'object' ||
objA === null ||
typeof objB !== 'object' ||
objB === null
) {
return false;
}
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) {
return false;
}
// Test for A's keys different from B.
for (let i = 0; i < keysA.length; i++) {
if (
!hasOwnProperty.call(objB, keysA[i]) ||
!is(objA[keysA[i]], objB[keysA[i]])
) {
return false;
}
}
return true;
}
export default shallowEqual;
这里面还调用了 is
函数,这个函数也是 share
包中的一个工具方法。
objectIs.js
(Github 代码位置)/**
* inlined Object.is polyfill to avoid requiring consumers ship their own
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
*/
function is(x: any, y: any) {
return (
(x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare
);
}
export default is;
PureComponent
源码分析总结由上面的源码可以发现,其实 PureComponent
和 Component
中的方法和属性基本一致,只不过 PureComponent
多了一个 isPureReactComponent
为 true
的属性。在 checkShouldComponentUpdate
的时候,会根据这个属性判断是否是 PureComponent
,如果是的话,就会根据 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
这个判断语句的返回值作为更新依据。所以,查看了 shallowEqual
和 objectIs
的文件源码,我们可以得出 PureComponent
的浅比较结论:
先通过 is
函数判断两个参数是否相同,相同则直接返回 ture,也就是不更新组件。
objectIs.js
代码可知,基本属性类型判断值是否相同(包括 NaN
),引用数据类型判断是否是一个引用若 is
函数判断为 false
,则判断两个参数是否都为 对象 且 都不为 null
,若任意一个 不是对象 或 任意一个为 null
,直接返回 false
,也就是更新组件
若前两个判断都通过,则可断定两个参数皆为对象,此时判断它们 keys
的长度是否相同,若不同,则直接返回 false
,即更新组件
若 keys
长度不同,则对两个对象中的第一层属性进行比较,若都相同,则返回 true
,有任一属性不同,则返回 false
阅读源码之后,可以发现之前我们修改了 changeTodo
方法的逻辑之后,为什么数据改变,组件却依然不更新的原因了。是因为修改的是同一个对象,所以 PureComponent
默认引用相同,不进行组件更新,所以才会出现这个陷阱,在使用的过程中希望大家注意一下这个问题。
对比 PureComponent
和 Component
,可以发现,PureComponent 性能更高,一般有几次有效修改,就会进行几次有效更新
为了避免出现上面所说的陷阱问题,建议将 React
和 Immutable.js
配合使用,因为 Immutable.js
中的数据类型都是不可变,每个变量都不会相同。但是由于 Immutable
学习成本较高,可以在项目中使用 immutability-helper
插件,也能实现类似的功能。关于 immutability-helper
的使用,可以查看我的另一篇博客:immutability-helper 插件的基本使用
虽然 PureComponent
提高了性能,但是也只是对数据进行了一次浅比较,最能优化性能的方式还是自己在 shouldComponent
生命周期中实现响应逻辑
关于 PureComponent
浅比较的总结可以查看上面的 PureComponent 源码分析总结
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
在使用 Vue3
提供的 watch API
时
有时会遇到监听的数据变了,但是不触发 watch
的情况;
有时修改数据会触发 watch
,重新赋值无法触发;
有时重新赋值能触发 watch
,但是修改内部数据又不触发;
再或者监听外部传入的数据时,是否和直接监听组件内部数据时的行为一致?
面临这些问题,决心通过下面的应用场景一探究竟!避免重复踩坑,对应不同的问题,找到合适的解决方案。
本文已收录在 Github: github.com/beichensky/Blog 中,欢迎 Star,欢迎 Follow!
使用 reactive
声明的响应式数据,类型是 Proxy
使用 ref
声明的响应式数据,类型是 RefImpl
使用 computed
得到的响应式数据,类型也属于 RefImpl
使用 ref
声明时,如果是引用类型,内部会将数据使用 reactive
包裹成 Proxy
watch(source, callback, options)
source
: 需要监听的响应式数据或者函数
callback
:监听的数据发生变化时,会触发 callback
newValue
:数据的新值
oldValue
:数据的旧值
onCleanup
:函数类型,接受一个回调函数。每次更新时,会调用上一次注册的 onCleanup
函数
options
:额外的配置项
immediate
:Boolean
类型,是否在第一次就触发 watch
deep
:Boolean
类型,是否开启深度监听
flush
:pre
| post
| sync
pre
:在组件更新前执行副作用
post
:在组件更新后运行副作用
sync
:每个更改都强制触发 watch
onTrack
:函数,具备 event
参数,调试用。将在响应式 property
或 ref
作为依赖项被追踪时被调用
onTrigger
:函数,具备 event
参数,调试用。将在依赖项变更导致副作用被触发时被调用。
当监听的 reactive
声明的响应式数据时,修改响应式数据的任何属性,都会触发 watch
const state = reactive({
name: '张三',
address: {
city: {
cityName: '上海',
},
},
});
watch(
state,
(newValue, oldValue) => {
console.log(newValue, oldValue);
},
{
deep: false,
}
);
setTimeout(() => {
state.name = '李四';
}, 1000);
setTimeout(() => {
state.address.city.cityName = '北京';
}, 2000);
可以发现,name
和 cityName
发生变化时,都会触发 watch
。但是,这里会发现两个问题:
无论是修改 name
或者 cityName
时,oldValue
和 newValue
的值是一样的;
尽管我们将 deep
属性设置成了 false
,但是 cityName
的变化依然会触发 watch
。
当监听的响应式数据是 Proxy
类型时,newValue
和 oldValue
由于是同一个引用,所以属性值是一样的;
当监听的响应式数据是 Proxy
类型时,deep
属性无效,无论设置成 true
还是 false
,都会进行深度监听。
由于在业务开发中,定义的数据中可能属性比较多,我们指向监听其中某一个属性,那我们看看该如何操作
如果只想监听 name
属性时,由于 name
是个基本类型,所以 source
参数需要用回调函数的方式进行监听:
watch(
() => state.name,
(newValue, oldValue) => {
console.log(newValue, oldValue);
}
);
setTimeout(() => {
state.name = '李四';
}, 1000);
setTimeout(() => {
state.address.city.cityName = '北京';
}, 2000);
这是可以看到,
newValue
为 张三,oldValue
为 李四,并且在修改cityName
时,不会再触发watch
。
监听 address
属性时,我们也可以使用回调函数的方式进行监听
watch(
() => state.address,
(newValue, oldValue) => {
console.log(newValue, oldValue);
}
);
setTimeout(() => {
state.name = '李四';
}, 1000);
setTimeout(() => {
state.address.city.cityName = '北京';
}, 2000);
豁。。。发现控制台现在一次日志 都不打印了,按道理说,修改 name
时,不触发 watch
是正常的,但是修改 cityName
时,是想要触发的啊。
先看一下现在这种情况,如何触发 watch
:
watch(
() => state.address,
(newValue, oldValue) => {
console.log(newValue, oldValue);
}
);
setTimeout(() => {
state.name = '李四';
}, 1000);
setTimeout(() => {
state.address = {
city: {
cityName: '北京',
},
};
}, 2000);
这个时候,发现 1秒 和 2秒 之后,控制台出现打印结果了。那我们知道了,需要修改 address
属性,才能触发监听,修改更深层的属性,触发不了,这个时候明白了,应该是没有深度监听,Ok,那我们把 deep
属性设置为 true
试试:
watch(
() => state.address,
(newValue, oldValue) => {
console.log(newValue, oldValue);
},
{
deep: true,
}
);
setTimeout(() => {
state.name = '李四';
}, 1000);
setTimeout(() => {
state.address.city.cityName = '杭州';
}, 3000);
setTimeout(() => {
state.address = {
city: {
cityName: '北京',
},
};
}, 2000);
果不其然,控制台中,正常的打印了两次日志,说明,无论直接修改 address
还是修改 address
内部的深层属性,都可以正常的触发 watch
。
好的,到这里,可能有些同学说了:那我直接监听 state.address
不就可以了吗?这样 deep
属性也不用加。
那我们演示一下看看会不会存在问题:
watch(state.address, (newValue, oldValue) => {
console.log(newValue, oldValue);
});
setTimeout(() => {
state.address.city.cityName = '杭州';
}, 3000);
setTimeout(() => {
state.address = {
city: {
cityName: '北京',
},
};
}, 2000);
在控制台,只打印了第一次修改 cityName
时的日志,第二次修改 address
时,无法触发 watch
好,现在把上面两次修改调换一下位置:
watch(state.address, (newValue, oldValue) => {
console.log(newValue, oldValue);
});
setTimeout(() => {
state.address = {
city: {
cityName: '北京',
},
};
}, 2000);
setTimeout(() => {
state.address.city.cityName = '杭州';
}, 3000);
控制台里,一次日志都没有了,也就意味着,修改 address
时,无法触发监听,并且之后,由于 address
的引用发生变化,导致后续 address
内部的任何修改也都触发不了 watch
了。这是一个致命问题。
当指向监听响应式数据的某一个属性时,需要使用函数的方式设置 source
参数:
如果属性类型是基本类型,可以正常监听,并且 newValue
和 oldValue
,可以正常返回;
如果属性类型是引用类型,需要将 deep
设置为 true
才能进行深度监听。
如果属性类型时引用类型,并且没有用函数的方式注册 watch
,那么在使用时,一旦重新对该属性赋值,会导致监听失效。
ref
声明的数据为基本类型时,直接使用 watch
监听即可
const state = ref('张三');
watch(state, (newValue, oldValue) => {
console.log(newValue, oldValue);
});
setTimeout(() => {
state.value = '李四';
}, 1000);
1秒 后,在控制台可以看到,打印出了 李四 和 张三。
众所周知,ref
声明的数据,都会自带 value
属性。所以下面这种写法效果同上:
const state = ref('张三');
watch(() => state.value, (newValue, oldValue) => {
console.log(newValue, oldValue);
});
setTimeout(() => {
state.value = '李四';
}, 1000);
ref
声明的数据为引用类型时,内部会接入 reactive
将数据转化为 Proxy
类型。所以该数据的 value
对应的是 Proxy
类型。
const state = ref({
name: '张三',
address: {
city: {
cityName: '上海',
},
},
});
watch(state, (newValue, oldValue) => {
console.log(newValue, oldValue);
});
setTimeout(() => {
state.value = {
name: '李四',
address: {
city: {
cityName: '上海',
},
},
};
}, 1000);
setTimeout(() => {
state.value.address.city.cityName = '北京';
}, 2000);
1秒后,控制台打印出了日志,但是 2秒后,却没有日志再出现了,这又是什么原因呢,我们把上面的代码转个形。在 ref
声明的数据为基本类型时,这段里说过,监听 state
和 () => state.value
,效果是一样的,那我们看一下转换后的代码:
watch(() => state.value, (newValue, oldValue) => {
console.log(newValue, oldValue);
});
上面说了,当 ref
声明的数据是引用类型时,内部会借助 reactive
转化为 Proxy
类型。那这段代码是不是感觉似曾相识?哈哈,不就是将 deep
属性设置为 true
就可以了么。
const state = ref({
name: '张三',
address: {
city: {
cityName: '上海',
},
},
});
watch(
state,
(newValue, oldValue) => {
console.log(newValue, oldValue);
},
{
deep: true,
}
);
setTimeout(() => {
state.value = {
name: '李四',
address: {
city: {
cityName: '上海',
},
},
};
}, 1000);
setTimeout(() => {
state.value.address.city.cityName = '北京';
}, 2000);
加上 deep
之后,可以看到,在 1秒及 2秒后,都会在控制台打印出日志。说明此时,无论是修改 state
的 value
,还是修改深层属性,都会触发 watch
。
有些同学可能说了,我直接函数返回 state
行不行:
const state = ref({
name: '张三',
address: {
city: {
cityName: '上海',
},
},
});
watch(
() => state,
(newValue, oldValue) => {
console.log(newValue, oldValue);
}
);
setTimeout(() => {
state.value = {
name: '李四',
address: {
city: {
cityName: '上海',
},
},
};
}, 1000);
setTimeout(() => {
state.value.address.city.cityName = '北京';
}, 2000);
好的,这里我帮大家试过了,跟上面的效果有些区别:
当 deep
为 false
时,修改 value
或者深层属性,都不会触发 watch
而设置deep
为 true
时,修改 vaue 或者深层属性,都会触发 watch
既然是 Proxy
类型的数据,那么我们直接按照之前演示的方式,直接使用不就好了么:
App 组件
<script setup>
import { reactive } from 'vue';
import Child from './Child.vue';
const state = reactive({
name: '张三',
address: {
city: {
cityName: '上海',
},
},
});
setTimeout(() => {
state.name = '李四';
}, 1000);
setTimeout(() => {
state.address.city.cityName = '北京';
}, 2000);
</script>
<template>
<Child :data="state" />
</template>
Child 组件
<script setup>
import { watch } from 'vue';
const props = defineProps(['data']);
watch(props.data, (newValue, oldValue) => {
console.log(newValue, oldValue);
});
</script>
好的,在 1秒 和 2秒之后,可以看到控制台打印出的有两次日志。Ok,乍一看感觉没有问题哈,那我们修改一下 App
组件里的数据传递:
App 组件
<script setup>
import { reactive, ref } from 'vue';
import Child from './Child.vue';
const state = reactive({
name: '张三',
address: {
city: {
cityName: '上海',
},
},
});
const otherState = reactive({
name: '李四',
});
const flag = ref(true);
setTimeout(() => {
flag.value = false;
}, 500);
setTimeout(() => {
state.name = '李四';
}, 1000);
setTimeout(() => {
state.address.city.cityName = '北京';
}, 2000);
</script>
<template>
<Child :data="flag ? otherState : state" />
</template>
有些同学可能就问,
flag ? otherState : state
这里用computed
包装一下不行吗?当然可以,但是这里不是为了演示问题嘛,一切写法皆有可能对吧。
修改完 App
组件之后,按道理应该会打印三次日志,但是惊讶的发现:无论多久,控制台里都不会有日志打印,也就是说,data 属性的变化根本没有触发 watch
。这是为啥呢?又该怎么处理呢?
因为在 App
组件中,我们切换了要传递给 Child
组件的数据,所以 watch
监听的 prop
不是同一个了
所以需要使用函数的方式监听 prop
Child 组件
<script setup>
import { watch } from 'vue';
const props = defineProps(['data']);
watch(() => props.data, (newValue, oldValue) => {
console.log(newValue, oldValue);
});
</script>
确实哈,修改完之后,控制台里打印了一次日志,而且新旧值不同,说明切换数据的时候监听到了,但还是不对,还少了两次。
到了这里,还记得我们上面讨论过的,使用函数作为 source
监听时,想监听深层的属性,那就需要添加 deep
属性为 true
才可以。
Child 组件
<script setup>
import { watch } from 'vue';
const props = defineProps(['data']);
watch(
() => props.data,
(newValue, oldValue) => {
console.log(newValue, oldValue);
},
{
deep: true,
}
);
</script>
好的,添加了 deep: true
之后,控制台中分别在 500ms、1秒、2秒后打印出了日志。此时达到了想要的效果。很棒!
当 ref 定义的数据作为
prop
进行传递时,会进行脱ref
的操作,也就是说,基本类型会直接将数据作为prop
传递,引用类型会作为Proxy
传入
直接使用函数作为 source
参数,进行监听即可:
App 组件
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const state = ref('张三');
setTimeout(() => {
state.value = '李四';
}, 1000);
</script>
<template>
<Child :data="state" />
</template>
Child 组件
,此时由于 ref
定义的是基本数据类型,所以也不存在是否需要深度监听的问题
<script setup>
import { watch } from 'vue';
const props = defineProps(['data']);
watch(
() => props.data,
(newValue, oldValue) => {
console.log(newValue, oldValue);
}
);
</script>
上面说过,
ref
作为prop
传递时,会脱ref
,也就意味着,传给子组件的就是Proxy
类型的数据,用法及可能遇到的问题,请参照 proxy 作为 prop 传递时 里的代码和示例。
同proxy 作为 prop 传递时,请参照 proxy 作为 prop 传递时 里的代码和示例。
provide API
提供的数据为ref
时,不会进行脱ref
操作,同 四、watch 监听 ref 声明的响应式数据,请参照 四、watch 监听 ref 声明的响应式数据 里的代码和示例
可以在 watch 中使用函数的方式进行监听,前提是需要将
deep
设置true
哦,这样对象内部如果包含了响应式的数据,也是可以触发监听的。
在实际开发过程中,可能会需要同时监听多个值,我们看一下多个值的情况,watch
是如何处理以及响应的:
import { reactive, ref, watch } from 'vue';
const state = reactive({
name: '张三',
address: {
city: {
cityName: '上海',
},
},
});
consotherState = reactive({
name: '李四',
});
const flag = ref(true);
watch([state, () => otherState.name, flag], (newValue, oldValue) => {
console.log(newValue, oldValue);
});
setTimeout(() => {
flag.value = false;
}, 500);
setTimeout(() => {
otherState.name = '李四';
}, 1000);
setTimeout(() => {
state.address.city.cityName = '北京';
}, 2000);
可以再控制台看到,三次变化都会输出日志,并且 newValue
和 oldValue
都是一个数组,里面值的顺序对应着 source
里数组的顺序。
在业务开发的过程中,时常面临这样的需求:监听某个数据的变化,当数据发生变化时,重新进行网络请求。下面写一段代码,来模拟这个需求:
<script setup>
import { reactive, ref, watch } from 'vue';
let count = 2;
const loadData = (data) =>
new Promise((resolve) => {
count--;
setTimeout(() => {
resolve(`返回的数据为${data}`);
}, count * 1000);
});
const state = reactive({
name: '张三',
});
const data = ref('');
watch(
() => state.name,
(newValue) => {
loadData(newValue).then((res) => {
data.value = res;
});
}
);
setTimeout(() => {
state.name = '李四';
}, 100);
setTimeout(() => {
state.name = '王五';
}, 200);
</script>
<template>
<div>{{ data }}</div>
</template>
可以看到界面上展示的结果是:返回的数据为李四,显然这不是我们想要的结果。最后一次是将 name
修改为了王五,所以肯定是希望返回的结果为王五。那出现这个异常的原因是什么呢?
数据每次变化,都会发送网络请求,但是时间长短不确定,所以就有可能导致,后发的请求先回来了,所以会被先发的请求返回结果给覆盖掉。
那么该如何解决呢?上面提到过,watch
的 callback
中具备第三个参数 onCleanup
,我们来尝试着用一下:
watch(
() => state.name,
(newValue, oldValue, onCleanup) => {
let isCurrent = true;
onCleanup(() => {
isCurrent = false;
});
loadData(newValue).then((res) => {
if (isCurrent) {
data.value = res;
}
});
}
);
此时,在浏览器上,只会出现:返回的数据为王五。
onCleanup
接受一个回调函数,这个回调函数,在触发下一次watch
之前会执行,因此,可以在这里,取消上一次的网络请求,亦或做一些内存清理及数据变更等任何操作。
上面说了很多 watch 的应用场景和常见问题。在需要监听多个数据时,可以使用数组作为 source。但是多个数据,如果是很多个呢,可能比较负责的逻辑,其中使用了较多的响应式数据,这个时候,使用 watch 去监听,显然不太适合。
这里可以使用新的 API:watchEffect
watchEffect(effect, options)
effect
: 函数。内部依赖的响应式数据发生变化时,会触发 effect
重新执行
onCleanup
:形参,函数类型,接受一个回调函数。每次更新时,会调用上一次注册的 onCleanup
函数。作用同 watch 中的 onCleanup 参数。options
:
flush
:pre
| post
| sync
pre
:在组件更新前执行副作用;
post
:在组件更新后运行副作用,可以使用 watchPostEffect
替代;
sync
:每个更改都强制触发 watch
,可以使用 watchSyncEffect
替代。
onTrack
:函数,具备 event
参数,调试用。将在响应式 property
或 ref
作为依赖项被追踪时被调用
onTrigger
:函数,具备 event
参数,调试用。将在依赖项变更导致副作用被触发时被调用。
import { reactive, ref, watchEffect } from 'vue';
const state = reactive({
name: '张三',
});
const visible = ref(false);
watchEffect(() => {
console.log(state.name, visible.value);
});
setTimeout(() => {
state.name = '李四';
}, 1000);
setTimeout(() => {
visible.value = true;
}, 2000);
2秒之后,查看控制台,发现打印了三次日志:
第一次是初始值
第二次是修改 name 触发的监听
第三次是修改 visible 触发的监听
而且每次打印的都是当前最新值
由此可以看出:
watchEffect 默认监听,也就是默认第一次就会执行;
不需要设置监听的数据,在 effect 函数中,用到了哪个数据,会自动进行依赖,因此不用担心类似 watch 中出现深层属性监听不到的问题;
只能获取到新值,由于没有提前指定监听的是哪个数据,所以不会提供旧值。
在上面的用法中,感觉 watchEffect
使用起来还是很方便的,会自动依赖,而且还不用考虑各种深度依赖的问题。那 watchEffect
会不会有什么陷阱需要注意呢?
import { reactive, ref, watchEffect } from 'vue';
const state = reactive({
name: '张三',
});
const visible = ref(false);
watchEffect(() => {
setTimeout(() => {
console.log(state.name, visible.value);
})
});
setTimeout(() => {
state.name = '李四';
}, 1000);
setTimeout(() => {
visible.value = true;
}, 2000);
这次在 effect
函数中添加了异步任务,在 setTimeout
中使用响应式数据,会发现,控制台一直都只展示一个日志:第一次进入时打印的。
也就是说,在异步任务(无论是宏任务还是微任务)中进行的响应式操作,watchEffect
无法正确的进行依赖收集。所以后面无论数据如何变更,都不会触发 effect
函数。
如果真的需要用到异步的操作,可以在外面先取值,再放到异步中去使用
watchEffect(() => {
const name = state.name;
const value = visible.value;
setTimeout(() => {
console.log(name, value);
});
});
修改之后,在控制台中可以正常的看到三次日志。
当 wacth
的 source
为 Proxy
类型时:
deep
属性失效,强制进行深度监听;
新旧值指向同一个引用,导致内容是一样的。
当 watch
的 source
是 RefImpl
类型时:
直接监听 state
和 监听 () => state.value
是等效的;
如果 ref
定义的是引用类型,并且想要进行深度监听,需要将 deep
设置为 true
。
当 watch
的 source
是函数时,可以监听到函数返回值的变更。如果想监听到函数返回值深层属性的变化,需要将 deep
设置为 true
;
如果想监听多个值的变化,可以将 source
设置为数组,内部可以是 Proxy
对象,可以是 RefImpl
对象,也可以是具有返回值的函数;
在监听组件 props
时,建议使用函数的方式进行 watch
,并且希望该 prop
深层任何属性的变化都能触发,可以将 deep
属性设置为 true
;
使用 watchEffect
时,注意在异步任务中使用响应式数据的情况,可能会导致无法正确进行依赖收集。如果确实需要异步操作,可以在异步任务外先获取响应式数据,再将值放到异步任务里进行操作。
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
上一篇文章 轻松掌握 Redux 核心用法 详细讲解了 redux
的使用。
这篇文章我们按照上一篇的节奏,实现一下 redux
的核心代码。
本文已收录在
Github
: https://github.com/beichensky/Blog 中,欢迎 Star!
createStore
: 创建 store
combineReducers
: 合并 reducer
applyMiddleware
: 应用插件
bindActionCreators
: 将一个 actionCreator
组成的对象转换成 dispatcher
组成的对象
bindActionCreator
: 将 bindActionCreator
使用 dispatch
包装后转换成 dispatcher
createStore
:创建 store
对象接受两个参数
reducer
: 数据更新函数ehancer
: 应用中间件之后的增强型函数返回一个 store 对象,包含以下属性
getState
: 获取当前 state
数据dispatch
: 派发 action
对象subscribe
: 订阅 state
监听事件createStore
/**
* 创建 store 对象
* @param {function} reducer reducer 更新函数
* @param {*} prevState 进行服务端渲染时传入的已有的 state 数据(这里我们没有用到,暂时没做处理)
* @param {function} ehancer 插件应用结果
*/
export function createStore(reducer, prevState, ehancer) {
// 做一次兼容,可能第二个参数传递的就是 插件应用结果
if (typeof prevState === "function") {
ehancer = prevState;
}
// 如果 ehancer 确实传入的是一个函数,则在 ehancer 函数中执行 store 的创建
if (typeof ehancer === "function") {
return ehancer(createStore)(reducer);
}
// 当前 state 数据
let currentState;
// 存放订阅监听对象的数组
let listeners = [];
// 获取当前 state 数据
function getState() {
return currentState;
}
/**
* 订阅 state 数据变化
* @param {function} listener 订阅监听对象
*/
function subscribe(listener) {
const index = listeners.length;
listeners.push(listener);
// unsubscribe
return () => listeners.splice(index, 1);
}
/**
* 派发 action,触发 reducer 更新 state
* @param {plain object} action
*/
function dispatch(action) {
currentState = reducer(currentState, action);
listeners.forEach(listener => listener());
}
// 率先执行一次 dispatch,用来为 state 设置初始值
dispatch({ type: Math.random() });
return {
getState,
subscribe,
dispatch
};
}
combineReducers
:合并多个 reducer
函数接受一个对象参数,对象的属性值是 reducer
函数
返回一个新的 reducer
函数
调用新的 reducer
函数的时候,会依次调用原始的 reducer
函数,并将各个属性对应的 state
合并到一个 state
对象上
combineReducers
/**
* combineReducers 将多个 reducer 合成一个新的 reducer 函数
* @param {object} reducerTarget 包含多个 reducer 的对象
*/
export function combineReducers(reducerTarget) {
const finalReducer = {};
// 将 reducerTarget 中值不是 function 的属性过滤掉
Object.entries(reducerTarget).forEach(([key, reducer]) => {
if (typeof reducer === "function") {
finalReducer[key] = reducer;
}
});
// 返回一个新的 reducer 函数
return (state = {}, action) => {
let hasChange = false;
// 将各个 reducer 对应的 state 值合并到同一个对象中
let nextState = {};
// 遍历所有的 reducer
for (const [key, reducer] of Object.entries(finalReducer)) {
const prevStateForKey = state[key];
// 执行 reducer 函数,设置最新的 state 值
const nextStateForKey = reducer(prevStateForKey, action);
nextState[key] = nextStateForKey;
hasChange = hasChange || nextStateForKey !== prevStateForKey;
}
return hasChange ? nextState : state;
};
}
applyMiddleware
: 应用插件接受多个插件作为参数
返回一个增强型的函数 ,用来创建 store
,也就是在 createStore
方法中调用的 ehancer
createStore
还需要接受一个 reducer
,因此增强型函数 ehancer
执行后仍然返回一个函数,接收 reducer
,用来真正的创建 store
ehancer
函数中需要做的操作:
创建 store
应用中间件,执行所有中间件
dispatch
,所以 dispatch
必须作为参数传递state
值,所以 getState
也要作为参数传递使用 compose 函数,依次使用中间件对 dispatch
进行包装,获取到包装后的 dispatch
返回出去
compose
/**
* 将多个函数按顺序合成,返回一个新的函数
* @param {...any} funcs 多个函数
*/
export function compose(...funcs) {
if (!funcs) {
return;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((fn1, fn2) => (...args) => fn1(fn2(...args)));
}
applyMiddleware
/**
* 应用插件
* @param {...function} middlewares 多个插件
*/
export function applyMiddleware(...middlewares) {
// 在 createStore 中走 ehancer 时会来到这里
return createStore => reducer => {
const store = createStore(reducer);
let dispatch = store.dispatch;
/**
* 执行插件函数,将 getState 和 被插件包装后的 dispatch 传给插件
* 这里为了避免多个插件使用同一个 dispatch 互相影响,所有使用箭头函数包裹了一层
*
* 插件执行完毕后,将执行结果合并成一个执行链,是一个函数的数组
*
* 将执行链中的函数通过 compose 合成,生成一个新的函数,传入 dispatch 并执行,获取到一个被插件包装后的 dispatch
*/
const middleParams = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
};
const middlewareChains = middlewares.map(middleware =>
middleware(middleParams)
);
dispatch = compose(...middlewareChains)(dispatch);
// 将 store 返回出去
return {
...store,
dispatch
};
};
}
bindActionCreators
actionCreator
: 是一个函数,返回值是 action
对象dispatcher
: 是一个函数,执行后会直接派发 action
,触发 reducer
更新,不需要再通过 dispatch
转一次bindActionCreator
方法
接受两个参数
actionCreator
: Function
,返回 action
对象dispatch
: Dispatch
返回一个函数
dispatch
: 直接执行即可派发 action
,触发 reducer
更新数据/**
* 将 actionCreator 使用 dispatch 进行包裹,生成一个可以直接触发更新的 dispatcher
* @param {function} func actionCreator 函数
* @param {function} dispatch
*/
export function bindActionCreator(func, dispatch) {
return (...args) => dispatch(func(...args));
}
bindActionCreators
接收两个参数
actionCreators
: Object
,对象属性值都是 actionCreator
dispatch
: Dispatch
返回一个对象
dispatcher
组成的对象/**
* 将 actionCreators 中的 actionCreator 使用 dispatch 进行包裹,返回包含 多个 dispatcher 的对象
* @param {object} actionCreators 包含多个 actionCreator 的对象
* @param {function} dispatch
*/
export function bindActionCreators(actionCreators, dispatch) {
const dispatchers = {};
Object.entries(actionCreators).forEach(([key, actionCreator]) => {
dispatchers[key] = bindActionCreator(actionCreator, dispatch);
});
return dispatchers;
}
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
本文已收录在 Github: https://github.com/beichensky/Blog 中,欢迎 Star,欢迎 Follow!
本文介绍了 React 18 版本中 Suspense
组件和新增 SuspenseList
组件的使用以及相关属性的用法。并且和 18 之前的版本做了对比,介绍了新特性的一些优势。
早在 React 16 版本,就可以使用 React.lazy
配合 Suspense
来进行代码拆分,我们来回顾一下之前的用法。
在编写 User
组件,在 User
组件中进行网络请求,获取数据
User.jsx
import React, { useState, useEffect } from 'react';
// 网络请求,获取 user 数据
const requestUser = id =>
new Promise(resolve =>
setTimeout(() => resolve({ id, name: `用户${id}`, age: 10 + id }), id * 1000)
);
const User = props => {
const [user, setUser] = useState({});
useEffect(() => {
requestUser(props.id).then(res => setUser(res));
}, [props.id]);
return <div>当前用户是: {user.name}</div>;
};
export default User;
在 App 组件中通过 React.lazy
的方式加载 User
组件(使用时需要用 Suspense
组件包裹起来哦)
App.jsx
import React from "react";
import ReactDOM from "react-dom";
const User = React.lazy(() => import("./User"));
const App = () => {
return (
<>
<React.Suspense fallback={<div>Loading...</div>}>
<User id={1} />
</React.Suspense>
</>
);
};
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
效果图:
此时,可以看到 User 组件在加载出来之前会 loading
一下,虽然进行了代码拆分,但还是有两个美中不足的地方
需要在 User
组件中进行一些列的操作:定义 state
,effect
中发请求,然后修改 state
,触发 render
虽然看到 loading
展示了出来,但是仅仅只是组件加载完成,内部的请求以及用户想要看到的真实数据还没有处理完成
Ok, 带着这两个问题,我们继续向下探索。
Suspense
让子组件在渲染之前进行等待,并在等待时显示 fallback 的内容
Suspense
内的组件子树比组件树的其他部分拥有更低的优先级
执行流程
在 render
函数中可以使用异步请求数据
react
会从我们的缓存中读取
如果缓存命中,直接进行 render
如果没有缓存,会抛出一个 promise
异常
当 promise
完成后,react
会重新进行 render
,把数据展示出来
完全同步写法,没有任何异步 callback
子组件没有加载完成时,会抛出一个 promise
异常
监听 promise
,状态变更后,更新 state
,触发组件更新,重新渲染子组件
展示子组件内容
import React from "react";
class Suspense extends React.Component {
state = {
loading: false,
};
componentDidCatch(error) {
if (error && typeof error.then === "function") {
error.then(() => {
this.setState({ loading: true });
});
this.setState({ loading: false });
}
}
render() {
const { fallback, children } = this.props;
const { loading } = this.state;
return loading ? fallback : children;
}
}
export default Suspense;
针对上面我们说的两个问题,来修改一下我们的 User
组件
const User = async (props) => {
const user = await requestUser(props.id);
return <div>当前用户是: {user.name}</div>;
};
多希望 User
组件能这样写,省去了很多冗余的代码,并且能够在请求完成之前统一展示 fallback
但是我们又不能直接使用 async
、await
去编写组件。这时候怎么办呢?
结合上面我们讲述的 Suspense
实现原理,那我们可以封装一层 promise
,请求中,我们将 promise
作为异常抛出,请求完成展示结果。
wrapPromise
函数的含义:
接受一个 promise
作为参数
定义了 promise
状态和结果
返回一个包含 read
方法的对象
调用 read
方法时,会根据 promise
当前的状态去判断抛出异常还是返回结果。
function wrapPromise(promise) {
let status = "pending";
let result;
let suspender = promise.then(
(r) => {
status = "success";
result = r;
},
(e) => {
status = "error";
result = e;
}
);
return {
read() {
if (status === "pending") {
throw suspender;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result;
}
},
};
}
使用 wrapPromise
重新改写一下 User
组件
// 网络请求,获取 user 数据
const requestUser = (id) =>
new Promise((resolve) =>
setTimeout(
() => resolve({ id, name: `用户${id}`, age: 10 + id }),
id * 1000
)
);
const resourceMap = {
1: wrapPromise(requestUser(1)),
};
const User = (props) => {
const resource = resourceMap[props.id];
const user = resource.read();
return <div>当前用户是: {user.name}</div>;
};
这时候可以看到界面首先展示 loading
,请求结束后,直接将数据展示出来。不需要编写副作用代码,也不需要在组件内进行 loading
的判断。
上面我们讲述了 Suspense
的用法,那如果有多个 Suspense
同时存在时,我们想控制他们的展示顺序以及展示方式,应该怎么做呢?
React 中也提供了一个新的组件:SuspenseList
SuspenseList
组件接受三个属性
revealOrder
: 子 Suspense
的加载顺序
forwards: 从前向后展示,无论请求的速度快慢都会等前面的先展示
Backwards: 从后向前展示,无论请求的速度快慢都会等后面的先展示
together: 所有的 Suspense 都准备好之后同时显示
tail: 指定如何显示 SuspenseList
中未准备好的 Suspense
不设置:默认加载所有 Suspense 对应的 fallback
collapsed:仅展示列表中下一个 Suspense 的 fallback
hidden: 未准备好的项目不限时任何信息
children: 子元素
子元素可以是任意 React 元素
当子元素中包含非 Suspense
组件时,且未设置 tail
属性,那么此时所有的 Suspense
元素必定是同时加载,设置 revealOrder
属性也无效。当设置 tail
属性后,无论是 collapsed
还是 hidden
,revealOrder
属性即可生效
子元素中多个 Suspense
不会相互阻塞
User
组件
import React from "react";
function wrapPromise(promise) {
let status = "pending";
let result;
let suspender = promise.then(
(r) => {
status = "success";
result = r;
},
(e) => {
status = "error";
result = e;
}
);
return {
read() {
if (status === "pending") {
throw suspender;
} else if (status === "error") {
throw result;
} else if (status === "success") {
return result;
}
},
};
}
// 网络请求,获取 user 数据
const requestUser = (id) =>
new Promise((resolve) =>
setTimeout(
() => resolve({ id, name: `用户${id}`, age: 10 + id }),
id * 1000
)
);
const resourceMap = {
1: wrapPromise(requestUser(1)),
3: wrapPromise(requestUser(3)),
5: wrapPromise(requestUser(5)),
};
const User = (props) => {
const resource = resourceMap[props.id];
const user = resource.read();
return <div>当前用户是: {user.name}</div>;
};
export default User;
App
组件
import React from "react";
import ReactDOM from "react-dom";
const User = React.lazy(() => import("./User"));
// 此处亦可以不使用 React.lazy(),直接使用以下 import 方式引入也可以
// import User from "./User"
const App = () => {
return (
<React.SuspenseList revealOrder="forwards" tail="collapsed">
<React.Suspense fallback={<div>Loading...</div>}>
<User id={1} />
</React.Suspense>
<React.Suspense fallback={<div>Loading...</div>}>
<User id={3} />
</React.Suspense>
<React.Suspense fallback={<div>Loading...</div>}>
<User id={5} />
</React.Suspense>
</React.SuspenseList>
);
};
ReactDOM.createRoot(document.getElementById("root")).render(<App />);
wrapPromise
方法取自 Dan Abramov 的 frosty-hermann-bztrp好了,关于 React 中 Suspense 以及 SuspenseList 组件的用法,就已经介绍完了,在 SuspenseList 使用章节,所有的代码均已贴出来了。有疑惑的地方可以说出来一起进行讨论。
文中有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star。
本文主要介绍了 react-redux 的用法,以及在各种场景下不同 API 的使用方式和区别。
本文代码是在上一篇博客的基础上进行的,有疑惑的地方可以先查看上一篇博客:轻松掌握 Redux 核心用法
本文已收录在
Github
: https://github.com/beichensky/Blog 中,欢迎 Star!
npm install react-redux
# or
yarn add react-redux
Provider
: 使用 Provider
标签包裹根组件,将 store
作为属性传入,后续的子组件才能获取到 store 中的 state
和 dispatch
connect
:返回一个高阶组件,用来连接 React
组件与 Redux store
,返回一个新的已与 Redux store
连接的组件类。
useDispatch
:返回一个 dispatch
对象
useSelector
:接受一个函数,将函数的返回值返回出来
根目录下 index.js
文件
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./App";
import store from "./store";
import "./index.css";
ReactDOM.render(
<React.StrictMode>
{/* 使用 Provider 标签包裹住根组件,并将 store 作为参数传入 */}
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById("root")
);
API
:connect([mapStateToProps],[mapDispatchToProps],[mergeProps],[options])
connect
的用法相对复杂一些,接受四个参数,返回的是一个高阶组件。用来连接当前组件和 Redux store
。
mapStateToProps
:函数类型,接受两个参数: state
和 ownProps
(当前组件的 props
,不建议使用,会导致重渲染,损耗性能),必须返回一个纯对象,这个对象会与组件的 props
合并
(state[, ownProps]) => ({ count: state.count, todoList: state.todos })
mapDispatchToProps
:object | 函数
dispatch
会默认挂载到组件的的 props
中object
类型时,会把 object
中的属性值使用 dispatch
包装后,与组件的 props
合并{
increment: () => ({ type: "INCREMENT" }),
decrement: () => ({ type: "DECREMENT" }),
}
对象的属性值都必须是 ActionCreator
dispatch
不会再挂载到组件的 props
中
传递函数类型时,接收两个参数:dispatch
和 ownProps
(当前组件的 props
,不建议使用,会导致重渲染,损耗性能),必须返回一个纯对象,这个对象会和组件的 props
合并
(state[, ownProps]) => ({
dispatch,
increment: dispatch({ type: "INCREMENT" }),
decrement: dispatch({ type: "DECREMENT" })
})
mergeProps
:(很少使用) 函数类型。如果指定了这个参数,mapStateToProps()
与 mapDispatchToProps()
的执行结果和组件自身的 props
将传入到这个回调函数中。该回调函数返回的对象将作为 props
传递到被包装的组件中。你也许可以用这个回调函数,根据组件的 props
来筛选部分的 state
数据,或者把 props
中的某个特定变量与 ActionCreator
绑定在一起。如果你省略这个参数,默认情况下组件的 props
返回 Object.assign({}, ownProps, stateProps, dispatchProps)
的结果
mergeProps(stateProps, dispatchProps, ownProps): props
context?: Object
pure?: boolean
areStatesEqual?: Function
areOwnPropsEqual?: Function
areStatePropsEqual?: Function
areMergedPropsEqual?: Function
forwardRef?: boolean
下面 改写一下 App.js
中 redux
的用法
mapDispatchToProps
参数不传时import React, { useEffect, useCallback, useState, useMemo } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import { actionCreators } from "./store";
function App({ count, todos, dispatch }) {
const [value, setValue] = useState("");
// 生成包装后的 actionCreator,执行之后就会触发 store 数据的更新
const { increment, decrement, getAsyncTodos, addTodo } = useMemo(
() => bindActionCreators(actionCreators, dispatch),
[dispatch]
);
// 初始化 TodoList
useEffect(() => {
getAsyncTodos();
}, [getAsyncTodos]);
const add = useCallback(() => {
if (value) {
// 分发 action
addTodo(value);
setValue("");
}
}, [value, addTodo]);
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
// count、todos 也会被挂载到组件的 props 中
const mapStateToProps = ({ count, todos }) => ({ count, todos });
// 第二个参数没有传递,dispatch 默认会挂载到组件的 props 中
export default connect(mapStateToProps)(App);
mapDispatchToProps
参数为对象时import React, { useEffect, useCallback, useState } from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";
function App({ count, todos, increment, decrement, getAsyncTodos, addTodo }) {
const [value, setValue] = useState("");
// 初始化 TodoList
useEffect(() => {
getAsyncTodos();
}, [getAsyncTodos]);
const add = useCallback(() => {
if (value) {
// 分发 action
addTodo(value);
setValue("");
}
}, [value, addTodo]);
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
// count、todos 会被挂载到组件的 props 中
const mapStateToProps = ({ count, todos }) => ({ count, todos });
// actionCreators 中的 actionCreator 会被 dispatch 进行包装,之后合并到组建的 props 中去
const mapDispatchToProps = { ...actionCreators };
export default connect(mapStateToProps, mapDispatchToProps)(App);
mapDispatchToProps
参数为函数时import React, { useEffect, useCallback, useState } from "react";
import { connect } from "react-redux";
import { actionCreators } from "./store";
import { bindActionCreators } from "redux";
function App({
count,
todos,
increment,
decrement,
getAsyncTodos,
addTodo,
dispatch
}) {
const [value, setValue] = useState("");
// 初始化 TodoList
useEffect(() => {
getAsyncTodos();
}, [getAsyncTodos]);
const add = useCallback(() => {
if (value) {
// 分发 action
addTodo(value);
setValue("");
}
}, [value, addTodo]);
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
// count、todos 会被挂载到组件的 props 中
const mapStateToProps = ({ count, todos }) => ({ count, todos });
// mapDispatchToProps 为函数时,actionCreators 中的 actionCreator 需要自己处理,返回的对象会被合并到组件的 props 中去
const mapDispatchToProps = dispatch => ({
dispatch,
...bindActionCreators(actionCreators, dispatch)
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
虽然上面使用
connect
是在class
组件,但是在函数组件中依然适用。
上面我们在组件中使用的是 connect
,但是在现在这个 hooks
盛行的时代,怎么能只有高阶组件呢,所以下面我们来探究一下 useDispatch
和 useSelector
的用法。
改写 App.js
文件
import React, { useEffect, useCallback, useState, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { bindActionCreators } from "redux";
import { actionCreators } from "./store";
function App() {
const [value, setValue] = useState("");
// 从 useDispatch 中获取 dispatch
const dispatch = useDispatch();
// 生成包装后的 actionCreator,执行之后就会触发 store 数据的更新
const { increment, decrement, getAsyncTodos, addTodo } = useMemo(
() => bindActionCreators(actionCreators, dispatch),
[dispatch]
);
// 通过 useSelector 获取需要用到 state 值
const { count, todos } = useSelector(({ count, todos }) => ({
count,
todos
}));
// 初始化 TodoList
useEffect(() => {
getAsyncTodos();
}, [getAsyncTodos]);
const add = useCallback(() => {
if (value) {
// 分发 action
addTodo(value);
setValue("");
}
}, [value, addTodo]);
return (
<div className="App">
<h1>Hello Redux</h1>
<p>count: {count}</p>
<button onClick={increment}>increment</button>
<button onClick={decrement}>decrement</button>
<br />
<br />
<input
placeholder="请输入待办事项"
value={value}
onChange={e => setValue(e.target.value)}
/>
<button onClick={add}>add</button>
<ul>
{todos.map(todo => (
<li key={todo}>{todo}</li>
))}
</ul>
</div>
);
}
export default App;
使用 hooks
方式改写之后,感觉简洁了不少,数据来源也很清晰。至于用 connect
还是 hook
的方式,可以根据情况自己选择。
Redux
由 Action、Reducer、Store
组成
Reducer
创建 Store
store.dispatch(action)
触发更新函数 reducer
reducer
更新数据subscribe
通过 combineReducers
可以合并多个 reducer
通过 applyMiddleware
可以使用插件
通过 bindActionCreators
可以将 ActionCreator
转化成 dispatch
包装后的 ActionCreator
是不是还没有看过瘾呢?没有的话请看我的下一篇博客,详细讲解了
Redux
以及React-Redux
的实现原理。
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.