react高阶
约 7547 字大约 25 分钟
2025-05-08
一.Immutable
问题:
var obj = { /* 一个复杂结构的对象 */ };
doSomething(obj);
// 上面的函数执行完后,此时的 obj 还是最初的那个 obj 吗?
// deepCopy?
核心概念: Immutable 数据一旦创建,就不能被修改。任何修改都会返回一个新的 Immutable 对象。
1. Immutable.js 介绍
- https://github.com/immutable-js/immutable-js
- 每次修改 Immutable 对象时都会创建一个新的不可变对象,原对象数据不受影响。
- 实现方式: Immutable.js 使用了 Persistent Data Structure(持久化数据结构)和 Structural Sharing(结构共享)来优化性能,避免深拷贝带来的开销。
- 持久化数据结构: 旧数据创建新数据时,保证旧数据仍然可用且不变。
- 结构共享: 只修改对象树中变化的节点和受影响的父节点,其他节点共享。
2. 深拷贝与浅拷贝的关系
(1) 浅拷贝示例:
var arr = { }; arr2 = arr; // arr2 指向 arr 的内存地址,修改 arr2 会影响 arr
(2)
Object.assign()
: 只复制一级属性,比浅拷贝多拷贝一层。(3)
JSON.parse(JSON.stringify(obj))
: 一种深拷贝方法,数组和对象都适用。- 缺点: 无法处理
undefined
值。
- 缺点: 无法处理
3. Immutable 优化性能的方式
**Persistent Data Structure(持久化数据结构):**使用旧数据创建新数据时,要保证旧数据同时可用且不变。
**Structural Sharing(结构共享):**如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
4. Immutable 中常用类型(Map,List)
(1) Map
const { Map } = require('immutable'); const map1 = Map({ a: 1, b: 2, c: 3 }); const map2 = map1.set('b', 50); map1.get('b') + " vs. " + map2.get('b'); // 2 vs. 50
(2) List
const { List } = require('immutable'); const list1 = List([ 1, 2 ]); const list2 = list1.push(3, 4, 5); const list3 = list2.unshift(0); const list4 = list1.concat(list2, list3); assert.equal(list1.size, 2); assert.equal(list2.size, 5); assert.equal(list3.size, 6); assert.equal(list4.size, 13); assert.equal(list4.get(0), 1); // push, set, unshift or splice 都可以直接用,返回一个新的 immutable 对象
(3) merge, concat
const { Map, List } = require('immutable'); const map1 = Map({ a: 1, b: 2, c: 3, d: 4 }); const map2 = Map({ c: 10, a: 20, t: 30 }); const obj = { d: 100, o: 200, g: 300 }; const map3 = map1.merge(map2, obj); // Map { a: 20, b: 2, c: 10, d: 100, t: 30, o: 200, g: 300 } const list1 = List([ 1, 2, 3 ]); const list2 = List([ 4, 5, 6 ]); const array = [ 7, 8, 9 ]; const list3 = list1.concat(list2, array); // List [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
(4) toJS
const { Map, List } = require('immutable'); const deep = Map({ a: 1, b: 2, c: List([ 3, 4, 5 ]) }); console.log(deep.toObject()); // { a: 1, b: 2, c: List [ 3, 4, 5 ] } console.log(deep.toArray()); // [ 1, 2, List [ 3, 4, 5 ] ] console.log(deep.toJS()); // { a: 1, b: 2, c: [ 3, 4, 5 ] } JSON.stringify(deep); // '{"a":1,"b":2,"c":[3,4,5]}'
(5) fromJS
const { fromJS } = require('immutable'); const nested = fromJS({ a: { b: { c: [ 3, 4, 5 ] } } }); // Map { a: Map { b: Map { c: List [ 3, 4, 5 ] } } } const nested2 = nested.mergeDeep({ a: { b: { d: 6 } } }); // Map { a: Map { b: Map { c: List [ 3, 4, 5 ], d: 6 } } } console.log(nested2.getIn([ 'a', 'b', 'd' ])); // 6 // 如果取一级属性 直接通过 get 方法,如果取多级属性 getIn(["a","b","c"]) // setIn 设置新的值 const nested3 = nested2.setIn([ 'a', 'b', 'd' ], "kerwin"); // Map { a: Map { b: Map { c: List [ 3, 4, 5 ], d: "kerwin" } } } // updateIn 回调函数更新 const nested3 = nested2.updateIn([ 'a', 'b', 'd' ], value => value + 1); console.log(nested3); // Map { a: Map { b: Map { c: List [ 3, 4, 5 ], d: 7 } } } const nested4 = nested3.updateIn([ 'a', 'b', 'c' ], list => list.push(6)); // Map { a: Map { b: Map { c: List [ 3, 4, 5, 6 ], d: 7 } } }
5. Immutable + Redux 的开发方式
// reducer.js
const initialState = fromJS({
category: "",
material: ""
});
const reducer = (prevstate = initialState, action = {}) => {
let { type, payload } = action;
switch (type) {
case GET_HOME:
var newstate = prevstate.set("category", fromJS(payload.category));
var newstate2 = newstate.set("material", fromJS(payload.material));
return newstate2;
default:
return prevstate;
}
};
// home.js
const mapStateToProps = (state) => {
return {
category: state.homeReducer.getIn(["category"]) || Map({}),
material: state.homeReducer.getIn(["material"]) || Map({})
};
};
this.props.category.get("相关属性");
this.props.category.toJS(); // 或者转成普通对象
6. 缺点
- 容易跟原生 JavaScript 混淆。
- 文档与调试不方便。
二.Mobx
1. Mobx 介绍
- Mobx 是一个功能强大,上手容易的状态管理工具。
- Mobx 背后的哲学:任何源自应用状态的东西都应该自动地获得。
- Mobx 利用 getter 和 setter 收集组件的数据依赖关系,在数据发生变化时精确知道哪些组件需要重绘,在界面规模变大的时候,往往会有很多细粒度更新。(vue类似)
2. Mobx 与 Redux 的区别
- 优点:
- Mobx 写法上更偏向于 OOP(面向对象编程)。
- 对一份数据直接进行修改操作,不需要始终返回一个新的数据。
- 并非单一 store, 可以多 store。
- Redux 默认以 JavaScript 原生对象形式存储数据,而 Mobx 使用可观察对象。
- 学习成本小。
- 面向对象编程, 而且对 TS 友好。
- 缺点:
- 过于自由:Mobx 提供的约定及模版代码很少,代码编写很自由,如果不做一些约定,比较容易导致团队代码风格不统一。
- 相关的中间件很少,逻辑层业务整合是个问题。
3. Mobx 的使用
(1) observable 和 autorun
import { observable, autorun } from 'mobx'; const value = observable.box(0); const number = observable.box(100); autorun(() => { console.log(value.get()); }); value.set(1); value.set(2); number.set(101); // 0, 1, 2。 // autorun 使用到才能被执行 // 只能是同步,异步需要处理 // 观察对象,通过 map const map = observable.map({ key: "value"}); // map.set("key", "new value"); // map.get("key") // 观察对象,不通过 map const map = observable({ key: "value"}); // map.key map.key="xiaoming" // 观察数组 const list = observable([1, 2, 4]); list[2] = 3;
(2) action,runInAction 和严格模式
import {observable, action, configure, runInAction} from 'mobx'; configure({enforceActions:'always'}) // 严格模式, 必须写 action, // 如果是 never,可以不写 action, // 最好设置 always, 防止任意地方修改值, 降低不确定性。 class Store { @observable number = 0; @observable name = "kerwin"; @action add = () => { this.number++; } // action 只能影响正在运行的函数,而无法影响当前函数调用的异步操作 @action load = async () => { const data = await getData(); runInAction(() => { this.name = data.name; }); } // runInAction 解决异步问题 } const newStore = new Store(); newStore.add(); // 如果在组件监听 componentDidMount() { autorun(()=>{ console.log(newStore.number); }) }
4. mobx-react 的使用
(1) react 组件里使用 @observer
observer
函数/装饰器可以用来将 React 组件转变成响应式组件。
(2) 可观察的局部组件状态
@observable
装饰器在 React 组件上引入可观察属性。而不需要通过 React 的冗长和强制性的setState
机制来管理。
import {observer} from "mobx-react" import {observable} from "mobx" @observer class Timer extends React.Component { @observable secondsPassed = 0 componentWillMount() { setInterval(() => { this.secondsPassed++ }, 1000) } // 如果是严格模式需要加上 @action 和 runInAction //一个新的生命周期钩子函数 componentWillReact //当组件因为它观察的数据发生了改变,它会安排重新渲染, //这个时候 componentWillReact 会被触发 componentWillReact() { console.log("I will re-render, since the todo has changed!"); } render() { return (<span>Seconds passed: { this.secondsPassed } </span> ) } } ReactDOM.render(<Timer />, document.body)
(3) Provider 组件
- 它使用了 React 的上下文(context)机制,可以用来向下传递 stores。 要连接到这些 stores,需要传递一个 stores 名称的列表给 inject,这使得 stores 可以作为组件的 props 使用。
this.props
class Store { @observable number = 0; @action add = () => { this.number++; } } export default new Store() // 导出 Store 实例 @inject("kerwinstore") @observer // 需要转换为响应式组件 class Child extends Component{ render(){ return <div> Child --{this.props.kerwinstore.number} </div> } } @inject("kerwinstore") class Middle extends Component{ render(){ return <div> Middle-<button onClick={()=>{ this.props.kerwinstore.add(); }}>test</button> <Child/> </div> } } // 通过 provider 传 store 进去 <Provider kerwinstore={store}> <Middle/> </Provider>
- 它使用了 React 的上下文(context)机制,可以用来向下传递 stores。 要连接到这些 stores,需要传递一个 stores 名称的列表给 inject,这使得 stores 可以作为组件的 props 使用。
5. 支持装饰器
创建
.babelrc
{ "presets": [ "@babel/preset-env" ], "plugins": [ [ "@babel/plugin-proposal-decorators", { "legacy": true } ] ] }
创建
config-overrides.js
const path = require('path') const { override, addDecoratorsLegacy } = require('customize-cra') function resolve(dir) { return path.join(__dirname, dir) } const customize = () => (config, env) => { config.resolve.alias['@'] = resolve('src') if (env === 'production') { config.externals = { 'react': 'React', 'react-dom': 'ReactDOM' } } return config }; module.exports = override(addDecoratorsLegacy(), customize())
安装依赖
npm i @babel/core @babel/plugin-proposal-decorators @babel/preset-env npm i customize-cra react-app-rewired
修改
package.json
... "scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-app-rewired eject" }, ...
三.TS (TypeScript)
1. TypeScript 介绍
- 定位: 静态类型语言,在写代码阶段就能检查错误,而非运行阶段。
- 优点:
- 类型系统是最好的文档,增加了代码的可读性和可维护性。
- 缺点:
- 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)等。
- 本质: TS 最后被编译成 JS。
2. 安装
create-react-app my-app --template typescript
3. 声明2
方式一: 在当前文件加上
declare const $: any;
方式二: 安装类型定义文件
npm i @types/jquery @types 是 npm 的一个分支,用来存放 *.d.ts 文件 npm i --save react-router-dom npm i --save @types/react-router-dom // 编译器需要通过这个声明文件,进行类型检查工作
4. 变量声明
// String(原生的构造函数) vs string (ts 中的类型)
var myname: string = "字符";
var mybool: boolean = false;
var mynumber: number = 100;
var mylist: Array<string> = ["111","222","3333"];
var myname2: string | number | boolean = 100; // 联合类型
var myname3: string | number = "kerwin";
var mylist2: Array<string| number> = [1,2,"kerwin"];
var mylist3:(string| number)[] = [1,2,"kerwin"];
5. 定义普通函数
// 接口描述形状
interface SearchFunc {
(source: string, subString: string): boolean;
}
// 对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
// 传参
function Test(list: String[], text?: String, ...args: String[]): void {
console.log(list, text, args)
}
Test(["1111","2222"])
// list:["1111","2222"] text: undefined args: []
Test(["0","1"],"a","b","c")
// list:["0","1"] text: "a" args: ["b","c"]
// 类型断言 as
function Test( mytext: string | number ) {
}
console.log((mytext as string).length) // 对
console.log((mytext as any).length) // 对
console.log((mytext as string[]).length) // 错,原声明没有这个类型,无法断言
6. 定义普通类
interface MyInter {
name: String, // 必选属性
readonly country: String, // 只读属性
getName(): void // 定义方法
}
class MyObj implements MyInter {
name = "kerwin"
country = "China"
private age = 100 // 私有属性, 不能在接口定义
getName() {
// ...
}
private getAge() {
// ...
} // 私有方法, 不能在接口定义
}
7. 定义类组件
interface PropInter {
name: string | number;
firstName?: string; // 可选属性
lastName?: string; // 可选属性
// [propName: string]: any 任意属性
}
interface StateInter {
count: number
}
// 根组件 ,第一个参数可以传 any
class HelloClass extends React.Component<PropInter, StateInter> {
state: StateInter = {
count: 0,
}; // setState 时候也才会检查
static defaultProps = { // 属性默认值
name: "default name"
firstName: "",
lastName: "",
};
}
8. 定义函数式组件
// 根组件
const App: React.FC = (props) => {
console.log(props)
const [name, setname] = useState<string>("kerwin")
return <div>
app
</div>
}
// 子组件接受属性 - 1
interface iprops {
count: number
}
const Child: React.FC<iprops> = (props) => {
return <div>
child-{props.count}
</div>
}
// 子组件接受属性 - 2
const Child = (props: iprops) => {
return <div>
child-{props.count}
</div>
}
// useRef
const mytext = useRef<HTMLInputElement>(null)
<input type="text" ref={mytext}/>
useEffect(() => {
console.log(mytext.current && mytext.current.value)
}, [])
// useContext
interface IContext {
call: string
}
const GlobalContext = React.createContext<IContext>({
call: "" // 定义初始值, 按照接口规则
})
<GlobalContext.Provider value={{
call: "电话"
}}>
....
</GlobalContext.Provider>
const {call} = useContext(GlobalContext)
// useReducer
interface IPrevState {
count: number
}
interface IAction {
type: string,
payload: any
}
function reducer (prevState: IPrevState, action: IAction) {
.....
return prevState
}
const [state, dispatch] = useReducer(reducer, {
count: 1
})
dispatch({
type: "Action1",
payload: []
})
9. 父子通信
// 父组件调用
<Child key={index} item={item} index={index} cb={(index) => {
var newlist = [...list]
newlist.splice(index, 1)
setList(newlist)
}}/>
// 子组件
interface ItemType {
item: string,
index: number, // 定义接口
cb: (param: number) => void // 定义接口
}
const Child = (props: ItemType) => {
let {index, item, cb} = props
return <div >{item}
<button onClick={() => cb(index)}>del-{index}</button>
</div>
}
10. 路由
编程式导航
// 使用编程式导航,需要引入接口配置 import { RouteComponentProps } from "react-router-dom"; interface IProps { 自己定义的接口 } type HomeProps = IProps & RouteComponentProps; // 两个接口属性都支持 interface IState {} class Home extends React.Component<HomeProps, IState> { private handleSubmit = async () => { // code for API calls this.props.history.push("/home"); }; } public render(): any { return <div>Hello</div>; }
动态路由
interface IParams { id: string } // RouteComponentProps 是一个泛型接口 class Detail extends Component< RouteComponentProps<IParams> > { componentDidMount() { console.log(this.props.match.params.id) } render() { return <div> detail </div> } }
11. Redux
import {createStore} from 'redux'
interface ActionInter {
type: String,
payload: any
}
const reducer = (prevState = {}, action: ActionInter) => {
return action.payload
}
const store = createStore(reducer, // enhancer)
export default store
四.styled-components
- 通过 JavaScript 改变 CSS 编写方式的解决方案之一,从根本上解决常规 CSS 编写的一些弊端。
- 通过 JavaScript 来为 CSS 赋能,我们能达到常规 CSS 所不好处理的逻辑复杂、函数方法、复用、避免干扰。样式书写将直接依附在 JSX 上面,HTML、CSS、JS 三者再次内聚。
- all in js 的思想
1.基本用法
const StyleApp = styled.div`
background: yellow;
border: 1px solid black;
ul {
li {
color: red;
}
}
&:hover {
background: pink
} // pc 测试
`
/*
<StyleApp>
<ul>
<li>1111</li>
<li>22222</li>
</ul>
</StyleApp>
*/
2.透传 props
const StyledInput = styled.input`
color: red;
background: yellow;
border: none;
border-radius: 3px;
`
<StyledInput type="text" placeholder="okok"/>
3.基于 props 做样式判断
const StyledButton = styled.button`
background: ${props => props.bg || 'blue'}
`
/*<StyledButton>click</StyledButton>
<StyledButton bg="red">click</StyledButton>*/
4.样式化任意组件(一定要写 className )
const Child = (props) => <div className={props.className}>child</div>
const StyledChild = styled(Child)`
background: red;
`
<StyledChild/>
5.扩展样式
const MyButton = styled.button`
background: yellow;
`
const BigButton = styled(MyButton)`
height: 100px;
width: 100px;
`
6.加动画
import styled, {keyframes} from 'styled-components'
const rotate360 = keyframes`
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
`;
const Rotate = styled.div`
width: 100px;
height: 100px;
background: yellow;
animation: ${rotate360} 1s linear infinite;
`
五.单元测试
挂载组件
import Enzyme, {mount} from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17'
// 在使用 Enzyme 前需要先适配 React 对应的版本
Enzyme.configure({ adapter: new Adapter() })
it('挂载拿到状态', () => {
const app = mount(<App />);
expect(app.state().name).toEqual('kerwin');
expect(app.state().age).toEqual(100);
})
/*
.text():返回当前组件的文本内容
.html():返回当前组件的HTML代码形式
.props():返回根组件的所有属性
.prop(key):返回根组件的指定属性
.state([key]):返回根组件的状态
.setState(nextState):设置根组件的状态
*/
.setProps(nextProps):设置根组件的属性
测试组件渲染出来的 HTML
it('组件渲染出来的 HTML', () => {
const app = mount(<App />);
expect(app.find('#myid').text()).toEqual('kerwin');
})
模拟用户交互
it('模拟用户交互', () => {
const app = mount(<App />);
app.find('#mybtn').simulate('click')
expect(app.state().name).toEqual('xiaoming');
})
六、redux-saga
- 在 saga 中,全局监听器和接收器使用 Generator 函数和 saga 自身的一些辅助函数实现对整个流程的管控
代码实现
// index.js
import {createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga';
import {reducer} from './reducer'
import mySagas from './saga'
const sagaMiddleware = createSagaMiddleware();// 创建中间件
const store = createStore(reducer, {list: []}, applyMiddleware(sagaMiddleware))
// 注意运行的时机是在 store 创建好了之后
sagaMiddleware.run(mySagas);
export default store
// saga.js
import {takeEvery, put} from 'redux-saga/effects'
import {changeList} from './action'
function *mySagas() {
// 监听 GET_LIST
// 在每个 `监听 GET_LIST` action 被 dispatch 时调用 getList
yield takeEvery("GET_LIST", getList);
// yield takeEvery("DELETE_LIST", deleteList);
}
function *getList() {
// 异步处理
let res = yield new Promise(resolve => {
setTimeout(() => {
resolve(["1111","2222","3333"])
}, 2000)
})
yield put(changeList(res)) // 发出新的 action
}
export default mySagas
// action.js
export const changeList = (value) => {
return {
type: "CHANGE_LIST",
payload: value
}
}
export const getSaAction = () => {
// GET_LIST 被 saga 监听
return {
type: "GET_LIST"
}
}
// reducer.js
export const reducer = (prevState, action) => {
let {type, payload} = action;
switch(type) {
case "CHANGE_LIST":
let newstate = {...prevState}
newstate.list = [...newstate.list, ...payload]
return newstate
default:
return prevState
}
}
// App.js
class App extends Component {
componentDidMount() {
store.subscribe(() => {
console.log(store.getState())
})
}
handleClick = () => {
store.dispatch(getSaAction())
}
render() {
return <div >
<button onClick={this.handleClick}>获取异步</button>
</div>
}
}
好的,我帮你整理和更新了你提供的 React 笔记,并进行了一些适当的修改,使其更清晰易懂。
七. React 补充
1. Portal
概念: Portal 提供了一种将子节点渲染到父组件 DOM 节点之外的 DOM 节点的能力。
1.1 用法
普通组件: 子组件的元素会挂载到父组件的 DOM 节点中。
render() { return ( <div> {this.props.children} </div> ); }
Portal 组件: React 不再创建 div 节点,而是将子元素渲染到指定的 DOM 节点上。
domNode
是一个有效的、任意位置的 DOM 节点。render() { return ReactDOM.createPortal( this.props.children, domNode ); }
典型应用场景: 当父组件的 DOM 元素有
overflow: hidden
或z-index
样式,而子元素需要超出父元素的盒子显示时,可以使用 Portal。 例如,对话框、悬浮框和小提示。
1.2 Portal 中的事件冒泡
虽然通过 Portal 渲染的元素在父组件的盒子之外,但渲染的 DOM 节点仍在 React 的元素树上。 因此,在该 DOM 元素上的点击事件仍然可以在 DOM 树中被监听到。
1.3 示例代码
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
const getDiv = () => {
const div = document.createElement('div');
document.body.appendChild(div);
return div;
};
const withPortal = (WrappedComponent) => {
class AddPortal extends Component {
constructor(props) {
super(props);
this.el = getDiv();
}
componentWillUnmount() {
document.body.removeChild(this.el);
}
render(props) {
return ReactDOM.createPortal(<WrappedComponent {...props} />, this.el);
}
}
return AddPortal;
};
class Modal extends Component {
render() {
return (
<div>
<div>a modal content</div>
<button type="button">Click</button>
</div>
);
}
}
const PortalModal = withPortal(Modal);
class Page extends Component {
constructor(props) {
super(props);
this.state = { clicks: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(state => ({
clicks: state.clicks + 1
}));
}
render() {
return (
<div onClick={this.handleClick}>
<h3>ppppppppp</h3>
<h3>num: {this.state.clicks}</h3>
<PortalModal />
</div>
);
}
}
export default Page;
2. Lazy 和 Suspense
2.1 React.lazy 定义
React.lazy
函数允许你像渲染常规组件一样处理动态引入(的组件),实现懒加载。
2.2 为什么代码要分割(Code Splitting)
当程序越来越大,代码量越来越多时,一个页面上可能会堆积很多功能,其中有些功能可能用不到,但仍然会被下载和加载。 代码分割可以优化加载性能,类似于图片懒加载的理论。
2.3 实现原理
当 Webpack 解析到 import()
语法时,它会自动地开始进行代码分割,将代码分割成单独的文件。 当使用到这些文件的时候,才会异步加载。
2.4 解决方案
React.lazy
和常用的第三方库 react-loadable
都使用了代码分割的原理,并配合 Webpack 进行代码打包拆分,从而达到异步加载的目的,大大提高首屏渲染速度。
注意: React.lazy
不支持服务端渲染,此时 react-loadable
是一个不错的选择。
2.5 如何使用 React.lazy
以下示例代码使用 create-react-app
脚手架搭建:
OtherComponent.js 文件内容
import React from 'react'; const OtherComponent = () => { return ( <div> 我已加载 </div> ); }; export default OtherComponent;
App.js 文件内容
import React, { Suspense, Component } from 'react'; import './App.css'; // 使用 React.lazy 导入 OtherComponent 组件 const OtherComponent = React.lazy(() => import('./OtherComponent')); export default class App extends Component { state = { visible: false }; render() { return ( <div className="App"> <button onClick={() => { this.setState({ visible: true }); }}> 加载 OtherComponent 组件 </button> <Suspense fallback={<div>Loading...</div>}> { this.state.visible ? <OtherComponent /> : null } </Suspense> </div> ); } }
解释:
- 使用
React.lazy
导入OtherComponent
组件。 - 使用
Suspense
组件包裹OtherComponent
,并指定fallback
属性,用于在组件加载时显示 loading 状态。 - 当点击按钮时,
OtherComponent
组件才会异步加载。
- 使用
2.6 Suspense
如果在 App 渲染完成后,包含 OtherComponent
的模块还没有被加载完成,可以使用加载指示器为此组件做优雅降级。 使用 Suspense
组件来解决这个问题。
注意: 使用 Suspense
的时候,fallback
属性必须存在且有内容,否则会报错。
3. forwardRef
概念: 引用传递(Ref forwarding)是一种通过组件向子组件自动传递引用 ref
的技术。
使用场景: 对于应用者的大多数组件来说没有什么作用。但是对于有些重复使用的组件,可能有用。 例如某些 input
组件,需要控制其 focus
,本来是可以使用 ref
来控制,但是因为该 input
已被包裹在组件中,这时就需要使用 Ref forward
来透过组件获得该 input
的引用。 可以透传多层。
3.1 未使用 forwardRef
子组件
class Child extends Component { componentDidMount() { this.props.callback(this.refs.myinput); } render() { return <div> <input type="text" ref="myinput" /> </div>; } }
父组件
class App extends Component { render() { return ( <div> <Child callback={(el) => { el.focus(); }} /> </div> ); } }
3.2 使用 forwardRef
子组件
import React, { forwardRef } from 'react'; const Child = forwardRef((props, ref) => { return <div> <input type="text" ref={ref} /> </div>; });
父组件
import React, { Component, createRef } from 'react'; class App extends Component { myref = createRef(); componentDidMount() { this.myref.current.focus(); } render() { return ( <div> <Child ref={this.myref} /> </div> ); } }
4. Functional Component 缓存 (React.memo)
4.1 为什么起 memo 这个名字?
在计算机领域,记忆化(Memoization)是一种主要用来提升计算机程序速度的优化技术方案。 它将开销较大的函数调用的返回结果存储起来,当同样的输入再次发生时,则返回缓存好的数据,以此提升运算效率。
4.2 作用
组件仅在它的 props
发生改变的时候进行重新渲染。 通常来说,在组件树中 React 组件,只要有变化就会走一遍渲染流程。 但是使用 React.memo()
,我们可以仅仅让某些组件进行渲染。
4.3 与 PureComponent 区别
PureComponent
只能用于class
组件。memo
用于functional
组件。
4.4 用法
import React, { memo } from 'react';
const Child = memo(() => {
return <div>
<input type="text" />
</div>;
});
或者
import React, { memo } from 'react';
const Child = () => {
return <div>
<input type="text" />
</div>;
};
const MemoChild = memo(Child);
好的,我来对你提供的 React 扩展内容进行完善和补充,使其更全面和易于理解。
八. React 扩展
1. GraphQL
1.1 介绍与 Hello World
GraphQL 是一种为 API 而生的查询语言,也是一种 API 数据查询规范。它允许客户端精确地请求所需的数据,避免了传统 REST API 中常见的过度获取(over-fetching)和获取不足(under-fetching)问题。
优点:
- 精确数据请求: 客户端可以指定需要哪些字段,服务器只返回这些字段。
- 强大的类型系统: GraphQL 使用类型系统来描述数据,有助于客户端和服务端之间的沟通,并提供更好的错误检查。
- 自省能力: 客户端可以查询 GraphQL 服务的 schema,了解可用数据类型和查询方式。
- 版本控制: GraphQL 允许在同一个 endpoint 上演化 API,而无需创建新的版本。
Hello World 示例(服务端 - Node.js + Express + graphql):
const express = require('express'); const { graphqlHTTP } = require('express-graphql'); const { buildSchema } = require('graphql'); // 1. 定义 Schema const schema = buildSchema(` type Query { hello: String } `); // 2. 定义 Resolver (处理函数) const root = { hello: () => 'Hello world!' }; // 3. 创建 Express 应用 const app = express(); // 4. 添加 GraphQL 中间件 app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: root, graphiql: true, // 启用 GraphiQL 图形界面 })); // 5. 启动服务器 app.listen(4000, () => { console.log('Running a GraphQL API server at http://localhost:4000/graphql'); });
客户端查询:
query { hello }
服务端响应:
{ "data": { "hello": "Hello world!" } }
解释:
- Schema: 定义了 API 的数据结构和可用查询。
type Query
定义了根查询类型,hello: String
定义了一个名为hello
的字段,返回字符串类型。 - Resolver: 为 Schema 中的每个字段提供一个函数,用于获取数据。
hello: () => 'Hello world!'
定义了hello
字段的 resolver,简单地返回 "Hello world!" 字符串。 - GraphiQL: 一个交互式的 GraphQL IDE,用于探索 API、编写和执行查询。
1.2 参数类型与传递
GraphQL 支持多种参数类型,包括:
- Int: 整数
- Float: 浮点数
- String: 字符串
- Boolean: 布尔值
- ID: 唯一标识符(通常是字符串)
- Lists: 数组
- Non-Null: 表示字段不能为空
示例:
type Query { user(id: ID!): User users(limit: Int, offset: Int): [User] } type User { id: ID! name: String! email: String }
客户端查询示例:
query { user(id: "123") { id name email } users(limit: 10, offset: 20) { id name } }
服务端 Resolver 示例:
const root = { user: (args) => { // args.id 包含客户端传递的 ID // 在数据库中查找用户并返回 return { id: args.id, name: 'John Doe', email: 'john.doe@example.com' }; }, users: (args) => { // args.limit 和 args.offset 包含客户端传递的 limit 和 offset // 从数据库中获取用户列表并返回 return [ { id: '1', name: 'Alice' }, { id: '2', name: 'Bob' } ]; } };
1.3 Mutation
Mutation 用于修改服务端数据。 它类似于 REST API 中的 POST、PUT、PATCH 和 DELETE 请求。
示例:
type Mutation { createUser(name: String!, email: String!): User updateUser(id: ID!, name: String, email: String): User deleteUser(id: ID!): Boolean }
客户端 Mutation 示例:
mutation { createUser(name: "Jane Doe", email: "jane.doe@example.com") { id name email } } mutation { updateUser(id: "123", name: "Jane Updated") { id name email } } mutation { deleteUser(id: "123") }
服务端 Resolver 示例:
const root = { createUser: (args) => { // args.name 和 args.email 包含客户端传递的数据 // 创建新用户并返回 return { id: 'new-user-id', name: args.name, email: args.email }; }, updateUser: (args) => { // args.id, args.name 和 args.email 包含客户端传递的数据 // 更新用户并返回 return { id: args.id, name: args.name, email: args.email }; }, deleteUser: (args) => { // args.id 包含客户端传递的 ID // 删除用户并返回 true/false return true; } };
1.4 结合数据库
GraphQL 可以与各种数据库集成,例如 MongoDB、MySQL、PostgreSQL 等。
示例(MongoDB + Mongoose):
const mongoose = require('mongoose'); const { buildSchema } = require('graphql'); const { graphqlHTTP } = require('express-graphql'); const express = require('express'); // 1. 连接数据库 mongoose.connect('mongodb://localhost:27017/graphql-db', { useNewUrlParser: true, useUnifiedTopology: true }); // 2. 定义 Schema const userSchema = new mongoose.Schema({ name: String, email: String }); // 3. 创建 Model const User = mongoose.model('User', userSchema); // 4. 定义 GraphQL Schema const schema = buildSchema(` type User { id: ID! name: String! email: String } type Query { user(id: ID!): User users: [User] } type Mutation { createUser(name: String!, email: String!): User } `); // 5. 定义 Resolver const root = { user: async (args) => { return await User.findById(args.id); }, users: async () => { return await User.find(); }, createUser: async (args) => { const user = new User({ name: args.name, email: args.email }); return await user.save(); } }; // 6. 创建 Express 应用 const app = express(); // 7. 添加 GraphQL 中间件 app.use('/graphql', graphqlHTTP({ schema: schema, rootValue: root, graphiql: true })); // 8. 启动服务器 app.listen(4000, () => { console.log('Running a GraphQL API server at http://localhost:4000/graphql'); });
1.5 客户端访问
可以使用各种 GraphQL 客户端库来访问 GraphQL API,例如:
- Apollo Client: 一个功能强大的 GraphQL 客户端,支持 React、Angular、Vue 等框架。
- Relay: Facebook 开发的 GraphQL 客户端,专注于性能和数据一致性。
- graphql-request: 一个轻量级的 GraphQL 客户端,易于使用。
示例(使用
graphql-request
):import { GraphQLClient, gql } from 'graphql-request'; const endpoint = 'http://localhost:4000/graphql'; const graphQLClient = new GraphQLClient(endpoint); const query = gql` query { users { id name email } } `; graphQLClient.request(query) .then((data) => { console.log(data); }) .catch((error) => { console.error(error); });
1.6 结合 React
可以使用 Apollo Client 或 Relay 等库将 GraphQL 集成到 React 应用中。
示例(使用 Apollo Client):
import React from 'react'; import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery } from '@apollo/client'; // 1. 创建 Apollo Client const client = new ApolloClient({ uri: 'http://localhost:4000/graphql', cache: new InMemoryCache() }); // 2. 定义 GraphQL 查询 const GET_USERS = gql` query { users { id name email } } `; // 3. 创建 React 组件 function Users() { const { loading, error, data } = useQuery(GET_USERS); if (loading) return <p>Loading...</p>; if (error) return <p>Error : {error.message}</p>; return ( <ul> {data.users.map((user) => ( <li key={user.id}> {user.name} ({user.email}) </li> ))} </ul> ); } // 4. 将 Apollo Client 集成到 React 应用中 function App() { return ( <ApolloProvider client={client}> <div> <h2>My first Apollo app 🚀</h2> <Users /> </div> </ApolloProvider> ); } export default App;
2. dva
介绍
dva 是一个基于 Redux、Redux-Saga 和 React-Router 的轻量级前端框架。它简化了 React 应用的状态管理和数据流,并提供了一些常用的工具和约定,使开发更加高效。
核心概念:
- Model: 包含 state、reducers、effects 和 subscriptions。
- state: 应用的状态数据。
- reducers: 纯函数,用于修改 state。
- effects: 处理异步操作,例如 API 请求。
- subscriptions: 监听数据源,例如键盘事件、路由变化等。
- Router: 管理应用的路由。
- Component: React 组件,用于渲染 UI。
- Connect: 将 Model 中的 state 和 dispatch 方法连接到 React 组件。
- Model: 包含 state、reducers、effects 和 subscriptions。
dva 应用的最简结构
import dva from 'dva'; import React from 'react'; const App = () => <div>Hello dva</div>; // 创建应用 const app = dva(); // 注册视图 app.router(() => <App />); // 启动应用 app.start('#root');
数据流图
(建议插入一张 dva 的数据流图,描述 Action -> Reducer -> State -> View 的过程)
dva-cli
dva-cli
已经不推荐使用,建议使用create-umi
来创建 dva 应用。
3. umi
介绍
umi 是一个可插拔的企业级 React 应用框架。它提供了开箱即用的功能,例如路由、构建、部署等,并支持插件扩展,可以满足各种复杂的业务需求。
核心特性:
- 约定式路由: 根据
pages
目录自动生成路由配置。 - 插件机制: 通过插件扩展 umi 的功能。
- TypeScript 支持: 内置 TypeScript 支持。
- Mock 数据: 方便开发和测试。
- 代理: 解决跨域问题。
- dva 集成: 无缝集成 dva 框架。
- Ant Design 集成: 方便使用 Ant Design 组件库。
- 约定式路由: 根据
安装脚手架
mkdir myapp && cd myapp npx @umijs/create-umi-app
目录结构
myapp/ ├── .umi/ # 临时文件,编译产物 ├── mock/ # Mock 数据 ├── pages/ # 页面文件 ├── src/ # 源代码 ├── .umirc.ts # Umi 配置文件 ├── package.json # 项目依赖 └── README.md # 项目说明
路由
umi 会根据
pages
目录自动生成路由配置。(1) 基础路由
在
pages
目录下创建的每个文件都会自动生成一个路由。例如,pages/index.js
会生成/
路由,pages/film.js
会生成/film
路由。(2) 重定向
// pages/index.js import React from 'react'; import { Redirect } from 'umi'; export default () => { return ( <Redirect to="/film" /> ); } // 在 film 中的 _layout.js import { Redirect } from 'umi'; export default function Film(props) { if (props.location.pathname === '/film' || props.location.pathname === '/film/') { return <Redirect to="/film/nowplaying" /> } return ( <div> {props.children} </div> ) }
(3) 嵌套路由
在
pages
目录下创建目录,目录下的文件会自动生成嵌套路由。例如,pages/film/nowplaying.js
会生成/film/nowplaying
路由。 (注意: 如果嵌套路由不生效,尝试重启 umi 应用。)(4) 动态路由
使用
$
符号定义动态路由参数。例如,pages/detail/$id.js
会生成/detail/:id
路由。// pages/detail/$id.js import React from 'react'; import { useParams } from 'umi'; export default () => { const { id } = useParams(); return ( <div> <h1>Detail Page</h1> <p>ID: {id}</p> </div> ); }
(5) 路由拦截
使用
wrappers
属性可以对路由进行拦截,例如进行权限验证。// center.tsx import React from 'react'; const Center = () => { return ( <div> <h1>center</h1> </div> ); } Center.wrappers = ['@/wrappers/auth'] export default Center // auth.tsx import React from 'react' import { Redirect } from 'umi' export default (props: any) => { const isLogin = localStorage.getItem("token") if (isLogin) { return <div>{props.children}</div>; } else { return <Redirect to="/login" />; } }
(6) hash 模式
// .umirc.js export default { history: { type: 'hash' } }
(7) 声明式导航
import React from 'react'; import { NavLink } from 'umi' import style from './index.less' export default function (props: any) { if (props.location.pathname.includes('/detail')) { return <div>{props.children}</div> } return ( <div> <ul> <li> <NavLink to="/film" activeClassName={style.active}>film</NavLink> </li> <li> <NavLink to="/cinema" activeClassName={style.active}>cinema</NavLink> </li> <li> <NavLink to="/center" activeClassName={style.active}>center</NavLink> </li> </ul> {props.children} </div> ); }
(8) 编程式导航
import { history } from 'umi'; history.push(`/detail/${item}`)
Mock 功能
umi 约定
mock
文件夹下的文件或者page(s)
文件夹下的_mock
文件即 mock 文件。// mock/api.js export default { // 支持值为 Object 和 Array 'GET /api/users': { users: [1, 2] }, // GET POST 可省略 '/api/users/1': { id: 1 }, // 支持自定义函数,API 参考 express@4 'POST /api/users/create': (req, res) => { res.end('OK'); }, }
反向代理
// .umirc.js export default { proxy: { '/ajax': { target: 'https://m.maoyan.com', // pathRewrite: { '^/api': '' }, changeOrigin: true } }, }
Antd
// .umirc.ts export default { antd: { //自定义配置 } } //组件页面中使用 import { Button } from 'antd-mobile' <Button type="primary">add</Button>
dva 集成
(1) 同步
// .umirc.ts export default { dva: { //自定义配置 } } // models/kerwin.js export default { //命名空间 namespace: 'kerwin', state: { isShow: true, list: [] }, //处理state--同步 reducers: { //reducer简写, type类型是show的时候自动处理 show(state, { payload }) { return { ...state, ...payload } }, hide(state, { payload }) { return { ...state, ...payload } } }, // yield表示后面的方法执行完以后 call表示调用一个api接口 // put表示一个派发 effects: { *showEffect(payload, { put }) { yield put({ type: 'show', payload: { isShow: true } }) }, *hideEffect(payload, { put }) { yield put({ type: 'hide', payload: { isShow: false } }) } } } //根组件 import { connect } from 'dva'; function BasicLayout(props) { return ( <div > { props.isShow ? ... : null } {props.children} </div> ); } //state.kerwin 命名空间 export default connect(state => state.kerwin)(BasicLayout); //detail.js import { connect, useDispatch } from 'dva'; import { useEffect } from 'react'; function Detail(props) { const dispatch = useDispatch() useEffect(() => { dispatch({ type: "kerwin/hideEffect" //命名空间kerwin }) return () => { dispatch({ type: "kerwin/showEffect"//命名空间kerwin } ) }; }, []) return <div> Detail </div>
} export default connect(state => state.kerwin)(Detail)
* **(2) 异步** ```javascript // models/kerwin.js import { getNowplaying } from '../util/getNowplaying'; //封装的fetch调用接口 export default { ... reducers: { ... changeList(state, { payload }) { return { ...state, ...payload } } }, // 异步 // yield表示后面的方法执行完以后 call表示调用一个api接口 effects: { ... *getListEffect(payload, { put, call }) { let res = yield call(getNowplaying, "test-by-kerwin") yield put({ type: "changeList", payload: { list: res } }) } } } // /util/getNowplaying import { fetch } from 'dva' //dva内置的fetch export async function getNowplaying(value) { console.log(value) //value 是call的第二个参数 var res = await fetch("/ajax/comingList?ci=65&token=&limit=10&optimus_uuid=43388C403C4911EABDC9998C784A573A4F64A16AA5A34184BADE807E506D749E&optimus_risk_level=71&optimus_code=10") var result = await res.json() return result.coming } // nowplaying.js import React, { useEffect } from 'react' import { connect, useDispatch } from 'dva'; function Nowplaying(props) { let { list, loading } = props let dispatch = useDispatch() useEffect(() => { if (list.length === 0) { dispatch({ type: "kerwin/getListEffect" //命名空间kerwin }) } }, [list]) return ( <div> nowplaying--{loading.global ? '正在加载数据...' : ''} { // 遍历list } </div> ) } export default connect(({ kerwin, loading }) => ({ ...kerwin, loading }))(Nowplaying) ```