本章主要用于记录一些 React
相关知识点,因为最近在复习 React
相关内容,发现版本迭代了许多,废弃了很多 API
,也添加了一些新的方法(比如生命周期钩子等),所以就简单的在这里汇总整理一下,也算是查漏补缺
回调函数中的 this 通常,我们在 React
当中的事件处理是下面这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 class Toggle extends React .Component { constructor (props) { super (props) this .state = { isToggleOn : true } this .handleClick = this .handleClick.bind(this ) } handleClick() { this .setState(state => ({ isToggleOn: !state.isToggleOn })) } render() { return <button onClick ={this.handleClick} > {this.state.isToggleOn ? 'ON' : 'OFF'}</button > } }
我们之所以要在构造函数当中进行显式的 this
绑定,这是因为在 JavaScript
中,Class
的方法默认不会绑定 this
,如果你忘记绑定 this.handleClick
并把它传入了 onClick
,当你调用这个函数的时候 this
的值为 undefined
,如果不想显式的执行绑定操作的话,我们经常还会采用下面这种方式,即使用箭头函数的方式
1 2 3 4 5 6 7 8 9 10 class LoggingButton extends React .Component { handleClick() { console .log('this is:' , this ) } render() { return <button onClick ={() => this.handleClick()}>Click me</button > } }
此语法问题在于每次渲染 LoggingButton
时都会创建不同的回调函数,在大多数情况下,这没什么问题,但如果该回调函数作为 prop
传入子组件时,这些组件可能会进行额外的重新渲染,所以建议使用下面这种方式来进行绑定,即使用 Class fields
语法
1 2 3 4 5 6 7 8 9 10 class LoggingButton extends React .Component { handleClick = () => { console .log('this is:' , this ) } render() { return <button onClick ={this.handleClick} > Click me</button > } }
事件处理程序参数传递 在循环中,通常我们会为事件处理函数传递额外的参数,例如 id
是你要删除那一行的 id
,以下两种方式都可以向事件处理函数传递参数
1 2 3 4 5 <button onClick ={(e) => this.deleteRow(id, e)}>Delete Row</button > // or <button onClick ={this.deleteRow.bind(this, id )}> Delete Row</button >
在这两种情况下,React
的事件对象 e
会被作为第二个参数传递,如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind
的方式,事件对象以及更多的参数将会被隐式的进行传递
组件通信的几种方式
我们先来看看需要组件之进行通信的几种情况
父组件向子组件通信
子组件向父组件通信
跨级组件通信
没有嵌套关系组件之间的通信
父组件向子组件通信 通常通过 props
向子组件传递需要的信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Child.propTypes = { name: PropTypes.string.isRequired, } export default function Child ({ name } ) { return <h1 > Hello, {name}</h1 > } export default class Parent extends Component { render() { return <Child name ="zhangsan" /> } }
子组件向父组件通信 主要是利用回调函数的方式,另外也可以利用自定义事件机制,这个我们会在下面一起来进行介绍,这里我们主要来看回调函数的方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 export default class List extends Component { static propTypes = { hideConponent: PropTypes.func.isRequired } render() { return ( <div> List <button onClick={this .props.hideConponent}>隐藏 List</button> </ div> ) } } export default class App extends Component { constructor (...args) { super (...args) this .state = { isShowList: false } } showConponent = () => { this .setState({ isShowList: true , }) } hideConponent = () => { this .setState({ isShowList: false , }) } render() { return ( <div> <button onClick={this .showConponent}>显示 Lists 组件</button> { this.state.isShowList ? <List hideConponent={this.hideConponent} / > : null } </div> ) } }
观察一下实现方法,可以发现它与传统回调函数的实现方法一样,而且 setState
一般与回调函数均会成对出现,因为回调函数即是转换内部状态的函数传统
另外我们也可以使用 onRef
,原理与上面的方式是一致的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 export default class Child extends React .Component { state = { name: '初始值' } componentDidMount() { this .props.onRef(this , this .state.name) } click = () => { this .setState({ name: '改变后的值' }) }; render() { return ( <div> <div> <div>{this .state.name}</div> </ div> </div> ) } } / / App class App extends Component { handleParentClick = (ref) => { console.log(ref.state.name) ref.click() } render() { return <Child onRef={this.handleParentClick} / > } }
跨级组件通信 有两种实现方式
层层组件传递 props
例如 A
组件和 B
组件之间要进行通信,先找到 A
和 B
公共的父组件,A
先向 C
组件通信,C
组件通过 props
和 B
组件通信,此时 C
组件起的就是中间件的作用
使用 Context
Context
是一个全局变量,像是一个大容器,在任何地方都可以访问到,我们可以把要通信的信息放在 Context
上,然后在其他组件中可以随意取到
但是 React
官方不建议使用大量 Context
,尽管他可以减少逐层传递,但是当组件结构复杂的时候,我们并不知道 Context
是从哪里传过来的
而且 Context
是一个全局变量,全局变量正是导致应用走向混乱的罪魁祸首
这里我们主要来看看 Context
的使用方式,比如下面这个示例,其中 ListItem
是 List
的子组件,List
是 App
的子组件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 export default class ListItem extends Component { static contextTypes = { color: PropTypes.string, } static propTypes = { value: PropTypes.string, } render() { const { value } = this .props return ( <li style={{ background : this .context.color }}> <span>{value}</span> </ li> ) } } export default class List extends Component { static childContextTypes = { color: PropTypes.string, } static propTypes = { list: PropTypes.array, } getChildContext() { return { color: 'red' , } } render() { const { list } = this .props return ( <div> <ul> { list.map((entry, index ) => <ListItem key ={ `list- ${index }`} value ={entry.text} /> ) } </ul > </div> ) } }
1 2 3 4 5 6 7 8 9 10 11 const list = [ { text : '题目一' , }, { text : '题目二' , }, ] export default class App extends Component { render() { return <List list ={list} /> } }
没有嵌套关系的组件通信 如果在业务逻辑不是十分复杂的情况下推荐使用自定义事件机制,这里以常用的 发布/订阅 模式举例,借用 Node.js
当中的 Events
模块的浏览器版实现,比如我们要实现这样一个功能,点击 List2
中的一个按钮,改变 List1
中的信息显示,其中 List1
和 List2
没有任何嵌套关系,App
是他们的父组件
我们首先需要安装 events
包
1 $ npm install events --save
然后我们新建一个 events.js
1 2 3 4 import { EventEmitter } from 'events' export default new EventEmitter()
接下来进行使用即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 export default class List extends Component { constructor (props) { super (props) this .state = { message: 'List1' , } } componentDidMount() { this .eventEmitter = emitter.addListener('changeMessage' , (message) => { this .setState({ message, }) }) } componentWillUnmount() { emitter.removeListener(this .eventEmitter) } render() { return <div > {this.state.message}</div > } } export default class List2 extends Component { handleClick = (message ) => { emitter.emit('changeMessage' , message) } render() { return <div > <button onClick ={this.handleClick.bind(this, 'List2 ')}> 点击我改变 List1 组件中显示信息</button > </div > } } export default class App extends Component { render() { return ( <div> <List1 /> <List2 /> </div> ) } }
自定义事件是典型的发布订阅模式,通过向事件对象上添加监听器和触发事件来实现组件之间的通信
总结
父组件向子组件通信使用 props
子组件向父组件通信使用回调函数或者自定义事件
跨级组件通信使用层层组件传递 props
的方式或是 Context
没有嵌套关系组件之间的通信推荐使用自定义事件
但是在进行组件通信的时候,主要还是看业务的具体需求来选择最为合适的,当业务逻辑复杂到一定程度,可以考虑引入 Mobx 或 Redux 等状态管理工具
对比 我们在上面介绍了一些比较常见的处安置方式,但是 React
当中的传参方式并不只有这些,下表列举了一些在 React
当中可以使用的传参方式,我们可以对比一下它们之间的优缺点,然后根据实际场景选择使用
方法
优点
缺点
props
不需要引入外部插件
兄弟组件通讯需要建立共同父级组件,较为麻烦
Provider
,Consumer
和 Context
不需要引入外部插件,跨多级组件或者兄弟组件通讯利器
状态数据状态追踪麻烦
EventEmitter
可支持兄弟,父子组件通讯
要引入外部插件
路由传参
可支持兄弟组件传值,页面简单数据传递非常方便
父子组件通讯无能为力
onRef
可以在获取整个子组件实例,使用简单
兄弟组件通讯麻烦,官方不建议使用
ref
同 onRef
同 onRef
Redux
建立了全局的状态管理器,兄弟父子通讯都可解决
引入了外部插件
Mobx
建立了全局的状态管理器,兄弟父子通讯都可解决
引入了外部插件
Flux
建立了全局的状态管理器,兄弟父子通讯都可解决
引入了外部插件
Hook
16.x
新的属性,可支持兄弟,父子组件通讯
需要结合 Context
一起使用
slot
支持父向子传标签
下面我们来看看 Redux
,Mobx
和 Flux
三者之间简单的对比,详细内容可以参考 Redux、Flux 和 React-Redux 三者之间的区别
Redux
核心模块 action
,reducer
,store
store
和更改逻辑是分开的,并且只有一个 store
没有调度器的概念,而且容器组件是有联系的
状态是不可改变的,更多的是遵循函数式编程思想
Mobx
核心模块 action
,reducer
,Derivation
有多个 store
设计更多偏向于面向对象编程和响应式编程,通常将状态包装成可观察对象,一旦状态对象变更,就能自动获得更新
Flux
核心模块 store
,Reduce
,Container
有多个 store
React.lazy React.lazy
函数能让你像渲染常规组件一样处理动态引入(的组件),在使用之前
1 import OtherComponent from './OtherComponent'
而使用之后
1 const OtherComponent = React.lazy(() => import ('./OtherComponent' ))
不过更为常见的方式是搭配 React.Suspense
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 import React, { Suspense } from 'react' const OtherComponent = React.lazy(() => import ('./OtherComponent' ))function MyComponent ( ) { return ( <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent / > </Suspense> </ div> ) }
React.lazy()
接收一个函数作为参数,该函数需要返回一个 Promise
对象,reslove
后返回一个模块,模块的默认导出对象作为渲染的 React
组件,例如
1 2 3 4 5 6 7 import React from 'react' function OtherComponent ( ) { return <h1 > Hello World</h1 > } export default OtherComponent
如何支持有名导出的模块 使用 React.lazy()
加载的模块,如果其中的 React
组件不是默认导出话,可能会报以下错误
1 2 3 Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
这是因为 React.lazy()
目前只支持默认导出(Default Export
),不支持有名导出(Named Exports
),假如在 OtherComponent
中导出了多个组件,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 import React, { Component } from 'react' export class AComponent extends Component { render() { return <div > Hello</div > } } export class BComponent extends Component { render() { return <div > World</div > } }
我们在 OtherComponent
组件当中分别导出了 AComponent
和 BComponent
两个组件,在不修改 OtherComponent
的前提下,可以这样写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const OtherComponent = React.lazy(() => { return new Promise ((resolve, reject ) => { import ('./OtherComponent' ).then(module => { resolve({ default : module .AComponent }) }).catch(err => { reject(err) }) }) }) export default class Test extends Component { render() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <OtherComponent / > </Suspense> </ div> ) } }
搭配 Webpack 实现代码分割 借助 Webpack
的 Code Splitting 功能,使用动态 import()
引入的模块会被自动拆分为异步加载的 chunk
,如果希望自定义 chunk
的文件名,可以在 import()
中加入 Webpack
特定的注释,如下
1 2 3 const OtherComponent = React.lazy( () => import ( './OtherComponent' ) )
例如如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const AComponent = React.lazy(() => import ( './AComponent' ))const BComponent = React.lazy(() => import ( './BComponent' ))export default class Test extends Component { render() { return ( <div> <Suspense fallback={<div>Loading...</div>}> <AComponent / > <BComponent /> </Suspense> </ div> ) } }
运行后可以在控制台的 Network
选项当中发现 A-component.chunk.js
和 B-component.chunk.js
setState() 是同步还是异步 我们从一个简单的例子开始看起,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 class App extends Component { state = { val : 0 } increment = () => { this .setState({ val : this .state.val + 1 }) console .log(this .state.val) } render() { return <div onClick ={this.increment} > {`Counter is ${this.state.val}`}</div > } }
运行以后我们可以发现,输出的值仍为 0
,所以在本节当中,我们就来简单的探讨一下这个问题,其实在 React
中,setState()
的使用场景一般有以下这些
合成事件中 setState()
生命周期函数中的 setState()
原生事件中的 setState()
setTimeout
中的 setState()
setState()
中的批量更新
所以下面我们就分别来看看这些不同情况下的结果
合成事件中 setState() 在 JSX
中常见的 onClick()
、onChange()
这些其实本质上都是合成事件,也就是属于 React
来进行管辖的范围,就比如上面的例子,我们可以知道它的结果为 0
生命周期函数中的 setState() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class App extends Component { state = { val : 0 } componentDidMount() { this .setState({ val : this .state.val + 1 }) console .log(this .state.val) } render() { return <div > {`Counter is ${this.state.val}`}</div > } }
其实还是和合成事件一样,当 componentDidmount()
执行的时候,React
内部并没有更新,这就导致在 componentDidmount()
中调用完 setState()
以后去 console.log()
拿到的结果还是更新之前的值
原生事件中的 setState() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class App extends Component { state = { val : 0 } changeValue = () => { this .setState({ val : this .state.val + 1 }) console .log(this .state.val) } componentDidMount() { document .body.addEventListener('click' , this .changeValue, false ) } render() { return <div > {`Counter is ${this.state.val}`}</div > } }
原生事件是指非 React
合成事件,比如上面的 addEventListener()
,它相较于合成事件,会直接触发点击事件,所以当你在原生事件中 setState()
后,能同步拿到更新后的 state
值
setTimeout 中的 setState() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 class App extends Component { state = { val : 0 } componentDidMount() { setTimeout(_ => { this .setState({ val : this .state.val + 1 }) console .log(this .state.val) }, 0 ) } render() { return <div > {`Counter is ${this.state.val}`}</div > } }
在 setTimeout()
中去使用 setState()
并不算是一个单独的场景,它是随着外层所决定的,因为你可以在合成事件中使用 setTimeout()
,可以在钩子函数中使用 setTimeout()
,也可以在原生事件中使用 setTimeout()
,但是不管是哪个场景下,基于 EventLoop
的模型下, 在 setTimeout()
当中去 setState()
总能拿到最新的 state
值
setState 中的批量更新 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class App extends Component { state = { val : 0 } batchUpdates = () => { this .setState({ val : this .state.val + 1 }) this .setState({ val : this .state.val + 1 }) this .setState({ val : this .state.val + 1 }) } render() { return <div onClick ={this.batchUpdates} > {`Counter is ${this.state.val}`}</div > } }
在调用 setState()
的时候 React
内部会创建一个更新队列,通过 firstUpdate/lastUpdate/lastUpdate.next
等方式去维护一个更新队列,在最终的 performWork
当中,相同的 key
会被覆盖,所以只会对最后一次的 setState()
进行更新,而如果我们使用一些别的方式,如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class App extends React .Component { state = { val : 0 } componentDidMount() { this .setState({ val : this .state.val + 1 }) console .log(this .state.val) this .setState({ val : this .state.val + 1 }) console .log(this .state.val) setTimeout(_ => { this .setState({ val : this .state.val + 1 }) console .log(this .state.val); this .setState({ val : this .state.val + 1 }) console .log(this .state.val) }, 0 ) } render() { return <div > {this.state.val}</div > } }
结合上面分析的,钩子函数中的 setState()
无法立即拿到更新后的值,所以前两次都是输出 0
,当执行到 setTimeout()
里的时候,前面两个 state
的值已经被更新,由于 setState()
批量更新的策略, this.state.val
只对最后一次的生效,为 1
,而在 setTimeout()
中 setState()
是可以同步拿到更新结果,所以 setTimeout()
中的两次输出 2
,3
,最终结果就为 0,0,2,3
总结 简单来说,有时表现出异步,有时表现出同步
setState()
只在合成事件和钩子函数当中可以理解为异步的,在原生事件和 setTimeout()
中都是同步的
setState()
的异步并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的异步,当然可以通过第二个参数 setState(partialState, callback)
中的 callback
拿到更新后的结果(这个可以参考官方文档当中的 State 的更新可能是异步的 )
setState()
的批量更新优化也是建立在异步(合成事件、钩子函数)之上的,在原生事件和 setTimeout()
中不会批量更新,在异步中如果对同一个值进行多次 setState()
,setState()
的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState()
多个不同的值,在更新时会对其进行合并批量更新
如果使用一种比较好的记忆方式来进行概括的话,就是
React
管得到的就是异步,管不到的就是同步
最后我们在简单的提及一个使用 setState()
的小技巧,那就是它还可以接收第二个参数,作用是在 state
值改变后进行调用
1 2 3 4 5 this .setState( { count : 3 }, () => { } )
监听数据变化 在 React
的 16.x
之前的版本我们可以使用 componentWillReceiveProps()
(当然现在还暂未移除,不过调整成了 UNSAFE_componentWillReceiveProps()
)
1 2 3 4 5 componentWillReceiveProps(nextProps){ if (this .props.visible !== nextProps.visible) { } }
但是有一点需要注意的就是,有些时候 componentWillReceiveProps()
在 props
值未变化也会触发,因为在生命周期的第一次 render()
后不会被调用,但是会在之后的每次 render()
中被调用(当父组件再次传送 props
)
在 React
的 16.x
以后的版本当中我们可以使用 getDerivedStateFromProps()
这个静态方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 export default class App extends React .Component { state = { countOne: 1 , changeFlag: '' } clickOne = () => { let { countOne } = this .state this .setState({ countOne : countOne + 1 }) } static getDerivedStateFromProps(nextProps) { console .log(`变化执行` ) return { changeFlag: 'state 值变化执行' } } render() { const { countOne, changeFlag } = this .state return ( <div> <div> <Button type="primary" onClick={this .clickOne}>点击加 1 </Button><span>countOne 值为{countOne}</ span> <div>{changeFlag}</div> </ div> </div> ) } }
使用 React Hook 来检查网络连接状态 我们都知道,在 JavaScript
当中有一个 Navigator 对象,它包含当前浏览器的状态和特性,比如定位、userAgent
和一些其他的属性,其中就包括当前是否处于网络连接状态,这里我们需要获取的就是 onLine
这个属性(这里需要注意它是驼峰命名的形式),下面我们就来看看如何在 Hook
中来使用
显然我们的首要任务是需要一些状态来跟踪记录我们是否在线的状态以及把它从我们的自定义 Hook
中 return
出来,如下
1 2 3 4 function useNetwork ( ) { const [isOnline, setOnline] = useState(window .navigator.onLine) return isOnline }
当组件正常挂载时这样做没有问题,但是如果当用户在渲染完成之后掉线我们该怎么做呢?所幸的是,我们可以监听两个事件,触发时以更新状态,为了达到这个效果我们需要使用 useEffect Hook
1 2 3 4 5 6 7 8 function useNetwork ( ) { const [isOnline, setNetwork] = useState(window .navigator.onLine) useEffect(() => { window .addEventListener('offline' , _ => setNetwork(window .navigator.onLine)) window .addEventListener('online' , _ => setNetwork(window .navigator.onLine)) }) return isOnline }
如你所见我们监听了两个事件,offline
和 online
(这里就不是驼峰命名的形式了),当事件触发的时候会随之更新状态,但是我们都知道在处理 useEffect
的时候应该 return
一个清理函数,这样 React
就可以帮助我们移除事件的监听,所以我们就不能直接在 addEventListener
当中使用箭头函数的方式了,而是应该传递同一个函数,这样 React
才能明确是哪一个监听器应该被移除,下面是我们最终版本的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function useNetwork ( ) { const [isOnline, setNetwork] = useState(window .navigator.onLine) const updateNetwork = () => { setNetwork(window .navigator.onLine) } useEffect(() => { window .addEventListener('offline' , updateNetwork) window .addEventListener('online' , updateNetwork) return () => { window .removeEventListener('offline' , updateNetwork) window .removeEventListener('online' , updateNetwork) } }) return isOnline }
如何确保一个对象仅被创建一次 比如一个常见的使用场景就是创建初始 state
需要花费大量的计算的时候,比如下面这个示例
1 2 3 4 5 function Table (props ) { const [rows, setRows] = useState(createRows(props.count)) }
为了避免重新创建被忽略的初始 state
,我们可以传一个函数给 useState
1 2 3 4 5 function Table (props ) { const [rows, setRows] = useState(() => createRows(props.count)) }
这样一来 React
只会在首次渲染时调用这个函数,但是有时候我们也可能想要避免重新创建 useRef()
的初始值,比如下面这个例子
1 2 3 4 5 function Image (props ) { const ref = useRef(new IntersectionObserver(onIntersect)) }
但是 useRef
不会像 useState
那样可以接受一个特殊的函数重载,所以针对这种情况,我们可以编写自己的函数来创建并将其设为惰性的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function Image (props ) { const ref = useRef(null ) function getObserver ( ) { if (ref.current === null ) { ref.current = new IntersectionObserver(onIntersect) } return ref.current } }
这样一来我们就避免了在一个对象被首次真正需要之前就先创建了它
路由传参的几种方式 params 1 2 3 4 5 <Route path='/path/:name' component={Search} /> <link to="/path/2" ></Link > this .props.history.push({pathname : '/path/' + name})
读取参数使用 this.props.match.params.name
query 1 2 3 4 5 <Route path='/query' component={Search} /> <Link to={{pathname : '/query' , query : { name : 'zhangsan' }}}> this .props.history.push({pathname : '/query' , query : { name : 'zhangsan' }})
读取参数使用 this.props.location.query.name
state 1 2 3 4 5 <Route path='/sort ' component={Search} /> <Link to={{pathname : '/sort' , state : { name : 'zhangsan' }}}> this .props.history.push({pathname : '/sort' , state : { name : 'zhangsan' }})
读取参数使用 this.props.location.query.state
search 1 2 3 4 5 <Route path='/web/search' component={Search} /> <link to="web/search?id=123" ></Link > this .props.history.push({ pathname : `/web/search?id=${row.id} ` })
读取参数使用 this.props.location.search
,但是需要注意这种方式在 react-router-dom^4.2.2
下存在一些问题,即传参跳转页面会空白,刷新才会加载出来
优缺点
params
在 HashRouter
和 BrowserRouter
路由中刷新页面参数都不会丢失
state
在 BrowserRouter
中刷新页面参数不会丢失,在 HashRouter
路由中刷新页面会丢失
query
在 HashRouter
和 BrowserRouter
路由中刷新页面参数都会丢失
query
和 state
都可以传递对象
require.context() 这一个是 Webpack
当中的 API
,但是因为 React
工程是基于 Webpack
打包的,所以在 React
当中也可以使用,它的功能是创建我们自己的 context
,该函数接受三个参数,一个要搜索的目录,一个标记表示是否还搜索其子目录,以及一个匹配文件的正则表达式,语法如下
1 require .context(directory, useSubdirectories = true , regExp = /^\.\/.*$/ , mode = 'sync' )
两个简单的示例
1 2 3 4 5 require .context('./test' , false , /\.test\.js$/)require .context('../' , true , /\.stories\.js$/)
此外,一个 context module
会导出一个(require
)函数,此函数可以接收一个参数 request
,另外导出的函数也有三个属性 resolve
,keys
和 id
resolve
是一个函数,它返回 request
被解析后得到的模块 id
keys
也是一个函数,它返回一个数组,由所有可能被此 context module
处理的请求组成
如果我们想引入一个文件夹下面的所有文件,或者引入能匹配一个正则表达式的所有文件,这个功能就会很有帮助
1 2 3 4 5 6 function importAll (r ) { r.keys().forEach(r) } importAll(require .context('../components/' , true , /\.js$/)) const cache = {}
1 2 3 4 5 6 function importAll (r ) { r.keys().forEach(key => cache[key] = r(key)) } importAll(require .context('../components/' , true , /\.js$/))
ReactDOM.createPortal() ReactDOM.createPortal()
提供了一种将子节点渲染到存在于父组件以外的 DOM
节点的方案,也就是说组件的 render()
函数返回的元素会被挂载在它的父级组件上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 import React from 'react' import ReactDOM from 'react-dom' import { Button } from 'antd' const modalRoot = document .bodyclass Modal extends React .Component { constructor (props) { super (props) this .el = document .createElement('div' ) this .el.style.width = '200px' this .el.style.height = '200px' this .el.style.backgroundColor = 'green' this .el.style.position = 'absolute' this .el.style.top = '200px' this .el.style.left = '400px' } componentDidMount() { modalRoot.appendChild(this .el) } componentWillUnmount() { modalRoot.removeChild(this .el) } render() { return ReactDOM.createPortal(this .props.children, this .el) } } function Child ( ) { return <div className ="modal" > 这个是通过 ReactDOM.createPortal 创建的内容</div > } export default class App extends React .Component { constructor (props) { super (props) this .state = { clicks : 0 } this .handleClick = this .handleClick.bind(this ) } handleClick() { this .setState(prevState => ({ clicks: prevState.clicks + 1 })) } render() { return ( <div> <Button onClick={this .handleClick}>点击</Button> <p>点击次数为 {this.state.clicks}</ p> <Modal> <Child /> </Modal> </ div> ) } }
取消请求 在 React
中如果当前正在发出请求的组件从页面上卸载了,理想情况下这个请求也应该取消掉,那么如何把请求的取消和页面的卸载关联在一起呢?在这种情况下可以考虑利用 useEffect
的清理函数搭配 Ajax
或是 Fetch
的取消请求 API
(使用方式可以参考我们之前整理过的 终止请求 )
所以有了这两个方式以后,我们就可以结合 React
来封装一个 useFetch
的自定义 Hook
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 export function useFetch = (config, deps ) => { const abortController = new AbortController() const [loading, setLoading] = useState(false ) const [result, setResult] = useState() useEffect(() => { setLoading(true ) fetch({ ...config, signal: abortController.signal }).then(res => setResult(res)) .finally(_ => setLoading(false )) }, deps) useEffect(() => { return () => abortController.abort() }, []) return { result, loading } }
如上,如果在此时路由发生切换或是 Tab
发生切换等场景下,被卸载掉的组件发出的请求也会被中断
深比较依赖 我们通常在使用 useEffect
的时候需要传入所依赖的 Hook
,最理想的状况是所有依赖都在真正发生变化的时候才去改变自身的引用地址,但是有些依赖可能在每次渲染都会重新生成一个引用,但是内部的值却没变,这可能会让 useEffect
对于依赖的『浅比较』没法正常工作,也就是我们有时候会遇到的无限循环问题
1 2 3 4 5 6 7 8 9 const getDep = () => { return { foo: 'bar' , } } useEffect(() => { }, [getDep()])
在上面的示例当中,由于 getDeps
函数返回的对象每次执行都是一个全新的引用,所以会导致触发无限更新的问题,这里有一个比较取巧的解决方式,那就是把依赖转为字符串
1 2 3 4 5 6 7 8 9 10 11 const getDep = () => { return { foo: 'bar' , } } const dep = JSON .stringify(getDeps())useEffect(() => { }, [dep])
这样一来对比的就是字符串 { foo: 'bar' }
的值,而不再是对象的引用,所以只有在值真正发生变化时才会触发更新,当然最好还是采用社区提供的解决方案 useDeepCompareEffect
,它选用深比较策略,对于对象依赖来说,它会逐个对比 key
和 value
,但是在性能上会有所牺牲,useDeepCompareEffect
大致原理如下
1 2 3 4 5 6 7 8 9 10 11 import { isEqual } from 'lodash' export function useDeepCompareEffect (fn, deps ) { const trigger = useRef(0 ) const prevDeps = useRef(deps) if (!isEqual(prevDeps.current, deps)) { trigger.current++ } prevDeps.current = deps return useEffect(fn, [trigger.current]) }
真正传入 useEffect
用以更新的是 trigger
这个数字值,使用 useRef
来保留上一次传入的依赖,每次都利用 lodash
的 isEqual
方法对本次依赖和旧依赖进行深比较,如果发生变化,则让 trigger
的值增加
另外我们也可以采用 fast-deep-equal 这个库,根据官方的 benchmark
对比,它比 lodash
的效率高七倍左右