📖二、React高级特性
1、函数组件
我们先来了解 class
组件和函数组件分别是什么样的。先看 class
组件,代码如下:
// class 组件 class List extends React.Component { constructor(props) { super(props) } redner() { const { list } = this.props return <ul>{list.map((item, index) => { return <li key={item.id}> <span>{item.title}</span> </li> })}</ul> } } 复制代码
函数组件的形式如下:
// 函数组件 function List(props) { const { list } = props return <ul>{list.map((item, idnex) => { return <li key={item.id}> <span>{item.title}</span> </li> })}</ul> } 复制代码
现在我们来梳理以下, class
组件和函数组件两者之间的区别。所谓函数组件,具有以下特点:
- 只是一个纯函数,它输入的是
props
,输出的是JSX
; - 函数组件没有实例,没有生命周期,也没有 state ;
- 函数组件不能扩展其他方法。
相反地, class
组件就拥有函数组件相异的特点。
2、非受控组件
在上述表单模块,我们谈论到了受控组件,那接下来,我们就来谈论非受控组件。
所谓非受控组件,就是 input
里面的值,不受到 state
的控制。下面我们先来看几种场景。
(1)input
先来看一段代码:
import React from 'react' class App extends React.Component { constructor(props) { super(props) this.state = { name: '星期一研究室', flag: true, } this.nameInputRef = React.createRef() // 创建 ref } render() { // input defaultValue return <div> {/* 使用 defaultValue 而不是 value ,使用 ref */} <input defaultValue={this.state.name} ref={this.nameInputRef}/> {/* state 并不会随着改变 */} <span>state.name: {this.state.name}</span> <br/> <button onClick={this.alertName}>alert name</button> </div> } alertName = () => { const elem = this.nameInputRef.current // 通过 ref 获取 DOM 节点 alert(elem.value) // 不是 this.state.name } } export default App 复制代码
此时浏览器的显示效果为:
大家可以看到,如果是非受控组件,那么需要使用 defaultValue
去控制组件的值。且最终 input
框里面的内容不论我们怎么改变,都不会影响到 state
的值。
(2)checkbox
对于复选框 checkbox
来说,先看以下代码:
import React from 'react' class App extends React.Component { constructor(props) { super(props) this.state = { name: '星期一研究室', flag: true, } } render() { // checkbox defaultChecked return <div> <input type="checkbox" defaultChecked={this.state.flag} /> <p>state.name: { this.state.flag === true ? 'true' : 'false' }</p> </div> } } export default App 复制代码
此时浏览器的显示效果如下:
大家可以看到,复选框如果当非受控组件来使用的使用,那么使用 defaultCkecked
来对值进行控制。同时,我们也看到了,最终不管 checked
的值如何改变, state
的值都不受影响。
(3)file
先来看一段代码:
import React from 'react' class App extends React.Component { constructor(props) { super(props) this.state = { name: '星期一研究室', flag: true, } this.fileInputRef = React.createRef() } render() { // file return <div> <input type="file" ref={this.fileInputRef}/> <button onClick={this.alertFile}>alert file</button> </div> } alertFile = () => { const elem = this.fileInputRef.current // 通过 ref 获取 DOM 节点 alert(elem.files[0].name) } } export default App 复制代码
此时浏览器的显示效果为:
在上面的代码中,我们使用通过 ref
去获取 DOM
节点,接着去获取到文件的名字。像 file
这种类型的固定,值并不会一直固定的,所以也是一个非受控组件。
(4)总结梳理
setState
只能处理类似于前端的显示和渲染相关的,像文件上传这种交互类型的就处理不了。下面我们来梳理下非受控组件的几大使用场景。具体如下:
- 必须手动操作
DOM
元素,setState
并无法手动操作DOM
元素; - 文件上传类型
<input type=file>
; - 某些富文本编辑器,需要传入
DOM
元素。
受控组件 vs 非受控组件的区别如下:
- 优先使用受控组件,符合
React
设计原则; - 必须操作
DOM
时,再使用非受控组件。
3、Portals
(1)为什么要用 Portals ?
一般情况下,组件默认会按照既定层次嵌套渲染。类似下面这样:
<div id="root"> <div> <div> <div class="model">Modal内容</div> </div> </div> </div> 复制代码
大家可以看到,这样不断嵌套,但里面却只有一层区域的内容是有用的。从某种程度上来说,是非常不好的。那我们想做的事情是,如何让组件渲染到父组件以外呢?
这个时候就需要用到 Portals
。
(2)如何使用
先来看一段代码:
import React from 'react' import ReactDOM from 'react-dom' import './style.css' class App extends React.Component { constructor(props) { super(props) this.state = { } } render() { // 正常渲染 // return <div className="modal"> // {this.props.children} {/* vue slot */} // </div> // 使用 Portals 渲染到 body 上。 // fixed 元素要放在 body 上,有更好的浏览器兼容性。 return ReactDOM.createPortal( <div className="modal">{this.props.children}</div>, document.body // DOM 节点 ) } } export default App 复制代码
style.css 代码如下:
.modal { position: fixed; width: 300px; height: 100px; top: 100px; left: 50%; margin-left: -150px; background-color: #000; /* opacity: .2; */ color: #fff; text-align: center; } 复制代码
此时,我们来看下浏览器节点的渲染效果。具体如下:
大家可以看到,通过使用 ReactDOM.createPortal()
,来创建 Portals
。最终 modals
节点成功脱离开父组件,并渲染到组件外部。
(3)使用场景
现在,我们来梳理一些 Portals
常见的场景。
Portals
常用于解决一些 css
兼容性问题。通常使用场景有:
overflow:hidden;
触发bfc
;- 父组件
z-index
值太小; position:fixed
需要放在body
第一层级。
4、context
(1)使用场景
有时候我们经常会有一些场景出现切换的频率很频繁,比如语言切换、或者是主题切换,那如何把对应的切换信息给有效地传递给每个组件呢?
使用 props
,又有点繁琐;使用 redux
,又太小题大做了。
因此,这个需要我们可以用 react
中的 context
。
(2)举例阐述
先来看一段代码:
import React from 'react' // 创建 Context 填入默认值(任何一个 js 变量) const ThemeContext = React.createContext('light') // 底层组件 - 函数是组件 function ThemeLink (props) { // const theme = this.context // 会报错。函数式组件没有实例,即没有 this // 函数式组件可以使用 Consumer return <ThemeContext.Consumer> { value => <p>link's theme is {value}</p> } </ThemeContext.Consumer> } // 底层组件 - class 组件 class ThemedButton extends React.Component { // 指定 contextType 读取当前的 theme context。 // static contextType = ThemeContext // 也可以用 ThemedButton.contextType = ThemeContext render() { const theme = this.context // React 会往上找到最近的 theme Provider,然后使用它的值。 return <div> <p>button's theme is {theme}</p> </div> } } ThemedButton.contextType = ThemeContext // 指定 contextType 读取当前的 theme context。 // 中间的组件再也不必指明往下传递 theme 了。 function Toolbar(props) { return ( <div> <ThemedButton /> <ThemeLink /> </div> ) } class App extends React.Component { constructor(props) { super(props) this.state = { theme: 'light' } } render() { return <ThemeContext.Provider value={this.state.theme}> <Toolbar /> <hr/> <button onClick={this.changeTheme}>change theme</button> </ThemeContext.Provider> } changeTheme = () => { this.setState({ theme: this.state.theme === 'light' ? 'dark' : 'light' }) } } export default App 复制代码
此时浏览器的显示效果为:
在上图中,我们做到了主题的切换。现在,我们来分析下上述的代码。
首先,我们创建了一个 Context
,也就是 ThemeContext
,并传入了 light
值。
其次,核心在 <Toolbar />
组件。 Toolbar
现有组件为 ThemedButton
和 ThemeLink
。其中,我们先指定 ThemedButton
的 contextType
去读取当前的 ThemeContext
,那么就取到了默认值 light
。
接着,来到了 ThemeLink
组件。 ThemeLink
是一个函数式组件,因此,我们可以直接使用 ThemeContext.Consumer
来对其进行传值。
上面两个组件的值都取到了,但那只是 ThemeContext
的初始值。取到值了之后呢,我们还要修改值, React
会往上找到最近的 ThemeContext.Provider
,通过 value={this.state.theme}
这种方式,去修改和使用 ThemeContext
最终使用的值 。
5、异步组件(懒加载)
在项目开发时,我们总是会不可避免的去加载一些大组件,这个时候就需要用到异步加载。在 vue
中,我们通常使用 import()
来加载异步组件,但在 react
就不这么使用了。
React
通常使用 React.lazy
和 React.Suspense
来加载大组件。
如下代码所示:
import React from 'react' const ContextDemo = React.lazy(() => import('./ContextDemo')) class App extends React.Component { constructor(props) { super(props) } render() { return <div> <p>引入一个动态组件</p> <hr /> <React.Suspense fallback={<div>Loading...</div>}> <ContextDemo/> </React.Suspense> </div> // 1. 强制刷新,可看到 loading (看不到就限制一下 chrome 的网速,Performance的network) // 2. 看 network 的 js 加载 } } export default App 复制代码
首先我们使用 import
去导入我们要加载的组件。之后使用 React.lazy
去将这个组件给进行注册,也就是 ContextDemo
。最后使用 React.Suspense
来加载 ContextDemo
。至此,我们完成了该异步组件的加载。
6、性能优化
(1)shouldComponentUpdate(简称SCU)
先来看下面这一段代码:
shouldComponentUpdate(nextProps, nextState) { if (nextState.count !== this.state.count || nextProps.text !== this.props.length) { return true // 可以渲染 } return false // 不重复渲染 } 复制代码
在 React
中,默认的是,当父组件有更新,子组件也无条件更新。那如果每回都触发更新,肯定不太好。
因此,这个时候我们需要用到 shouldComponentUpdate
,判断当属性有发生改变时,可以触发渲染。当属性不发生改变时,也就是前后两次的值相同时,就不触发渲染。
那这个时候我们 需要思考一个问题: SCU
一定要每次都用吗?答案其实不是肯定的。
我们会去用 SCU
,从某种层面上来讲就是为了优化。因此,我们需要依据当前的开发场景,有需要的时候再去优化。
现在,我们来总结一下 SCU
的使用方式,具体如下:
SCU
默认返回true
,即React
默认重新渲染所有子组件;- 必须配合 “不可变值” 一起使用;
- 可先不用
SCU
,有性能问题时再考虑使用。
(2)PureComponent和React.memo
PureComponent
在 react
中的使用形式如下:
class List extends React.PureComponent { constructor(props) { super(props) } render() { const { list } = this.props return <ul>{list.map((item, index) => { return <li key={item.id}> <span>{item.title}</span> </li> })}</ul> } shouldComponentUpdate() {/*浅比较*/} } 复制代码
如果我们使用了 PureComponent
,那么 SCU
会进行浅层比较,也就是一层一层的比较下去。
下面我们来看 memo
。 memo
,顾名思义是备忘录的意思。在 React
中的使用形式如下:
function MyComponent(props) { /* 使用props 渲染 */ } function areEqual(prevProps, nextProps) { /* 如果把 nextProps传入render方法的返回结果 与 preProps传入render方法的返回结果 一致的话,则返回true, 否则返回false */ } export default React.memo(MyComponent, areEqual); 复制代码
memo
,可以说是函数组件中的 PureComponent
。同时,使用 React.memo()
的形式,将我们的函数组件和 areEqual
的值进行比较,最后返回一个新的函数。
值得注意的是,在 React
中,浅比较已经使用于大部分情况,一般情况下,尽量不要做深度比较。
(3)不可变值
在 React
中,用于做不可变值的有一个库: Immutable.js
。这个库有以下几大特点:
- 彻底拥抱“不可变值”
- 基于共享数据(不是深拷贝),速度好
- 有一定的学习和迁移成本,按需使用
下面来看一个使用例子:
const map1 = Immutable.Map({ a: 1, b: 2, c: 3 }) const map2 = map1.set('b', 50) map1.get('b') // 2 map2.get('b') // 50 复制代码
基本上现在在开发中都用这个库来处理不可变值的问题。在实际使用中,可以看官方文档按需使用即可。
7、关于组件公共逻辑的抽离
在 React
中,对于组件公共逻辑的抽离主要有三种方式要了解。具体如下:
mixin
,已被React
弃用- 高阶组件
HOC
- Render Props
下面将讲解高阶组件 HOC
和 Render Props
。
(1)高阶组件 HOC
先看一段代码:
// 高阶组件不是一种功能,而是一种设计模式 // 1.传入一个组件 Component const HOCFactory = (Component) => { class HOC extends React.Component { // 在此定义多个组件的公共逻辑 render() { // 2.返回拼接的结果 return <Component {this.props} /> } } return HOC } const EnhancedComponent1 = HOCFactory(WrappedComponent1) const EnhancedComponent2 = HOCFactory(WrappedComponent2) 复制代码
高阶组件 HOC
是传入一个组件,返回一个新的组件,见上方代码的 1和2
。
下面来看一个例子,如下代码所示:
import React from 'react' // 高阶组件 const withMouse = (Component) => { class withMouseComponent extends React.Component { constructor(props) { super(props) this.state = { x: 0, y: 0 } } handleMouseMove = (event) => { this.setState({ x: event.clientX, y: event.clientY }) } render() { return ( <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}> {/* 1. 透传所有 props 2. 增加 mouse 属性 */} <Component {...this.props} mouse={this.state}/> </div> ) } } return withMouseComponent } const App = (props) => { const { x, y } = props.mouse // 接收 mouse 属性 return ( <div style={{ height: '500px' }}> <h1>The mouse position is ({x}, {y})</h1> </div> ) } export default withMouse(App) // 返回高阶函数 复制代码
此时浏览器的显示结果为:
在上面的代码中,我们用 定义了高阶组件 withMouse
,之后它通过 <Component {...this.props} mouse={this.state}/>
这种形式,将参数 props 和 props 的 mouse 属性给透传出来,供子组件 App
使用。
值得注意的是,在 react
中,还有一个比较常见的高阶组件是 redux connect
。用一段代码来演示:
import { connect } from 'react-redux'; // connect 是高阶组件 const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList) export default VisibleTodoList 复制代码
现在,我们来看下 connect
的源码,具体如下:
export const connect =(mapStateToProps, mapDispatchToProps) => (WrappedComponent) => { class Connect extends Component { constructor() { super() this.state = { allProps: {} } } /* 中间省略 N 行代码 */ render () { return <WrappedComponent {...this.state.allProps} /> } } return Connect } 复制代码
大家可以看到, Connect
也是同样地,传入一个组件,并返回一个组件。
(2)Render Props
先来看一段代码:
// Render Props 的核心思想 // 1.通过一个函数,将class组件的state作为props,传递给纯函数组件 class Factory extends React.Component { constructor() { tihs.state = { /* state 即多个组件的公共逻辑的数据 */ } } /* 2.修改 state */ render() { return <div>{this.props.render(this.state)}</div> } } const App = () => { // 3.在这里使用高阶组件,同时将高阶组件中的render属性传递进来 <Factory render={ /* render 是一个函数组件 */ (props) => <p>{props.a}{props.b} …</p> } /> } export default App; 复制代码
在上面的高阶组件 HOC
中,最终返回的结果也是一个高阶组件。但在 Render Props
中,我们把 Factory
包裹在定义的 App
组件中,最终再把 App
返回。
值得注意的是,在 Vue
中有类似于高阶组件的用法,但没有像 Render Props
类似的用法,这一点需要稍微留意一下。
下面来看一个例子,具体代码如下:
import React from 'react' import PropTypes from 'prop-types' class Mouse extends React.Component { constructor(props) { super(props) this.state = { x: 0, y: 0 } } handleMouseMove = (event) => { this.setState({ x: event.clientX, y: event.clientY }) } render() { return ( <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}> {/* 将当前 state 作为 props ,传递给 render (render 是一个函数组件) */} {this.props.render(this.state)} </div> ) } } Mouse.propTypes = { render: PropTypes.func.isRequired // 必须接收一个 render 属性,而且是函数 } const App = (props) => ( <div style={{ height: '500px' }}> <Mouse render={ /* render 是一个函数组件 */ ({ x, y }) => <h1>The mouse position is ({x}, {y})</h1> }/> </div> ) /** * 即,定义了 Mouse 组件,只有获取 x y 的能力。 * 至于 Mouse 组件如何渲染,App 说了算,通过 render prop 的方式告诉 Mouse 。 */ export default App 复制代码
此时,浏览器的显示效果如下:
在上面的代码中,通过 this.props.render(this.state)
这种形式,将 Mouse
组件中的属性传递给 App
,并让 App
成功使用到 Mouse
的属性值。
(3)HOC vs Render Props
现在,我们来梳理下 HOC
和 Render Props
的区别,具体如下:
- HOC:模式简单,但会增加组件层级
- Render Props:代码简洁,学习成本较高
- 各有各的优缺点,根据实际场景按需使用即可
📚三、Redux和React-router
1、Redux
(1)Redux概念简述
对于 react
来说,它是一个非视图层的轻量级框架,如果要用它来传递数据的话,则要先父传子,然后再慢慢地一层一层往上传递。
但如果用 redux
的话,假设我们想要某个组件的数据,那这个组件的数据则会通过 redux
来存放到 store
中进行管理。之后呢,通过 store
,再来将数据一步步地往下面的组件进行传递。
值得注意的是,我们可以视 Redux
为 Reducer
和 Flux
的结合。
(2)Redux的工作流程
Redux
,实际上就是一个数据层的框架,它把所有的数据都放在了 store
之中。我们先来看一张图:
大家可以看到中间的 store
,它里面就存放着所有的数据。继续看 store
向下的箭头,然后呢,每个组件都要向 store
里面去拿数据。
我们用一个例子来梳理整张图,具体如下:
- ①整张图上有一个
store
,它存放着所有的数据,也就是存储数据的公共区域; - ②每个组件,都要从
store
里面拿数据; - ③假设现在有一个场景,模拟我们要在图书馆里面借书。那么我们可以把
react Component
理解为借书人,之后呢,借书人要去找图书馆管理员才能借到这本书。而借书这个过程中数据的传递,就可以把它视为是Action Creators
,可以理解为 “你想要借什么书” 这句话。 - ④
Action Creatures
去到store
。这个时候我们把store
当做是图书馆管理员,但是,图书馆管理员是没有办法记住所有图书的数据情况的。一般来说,它都需要一个记录本,你想要借什么样的书,那么她就先查一下;又或者你想要还什么书,她也要查一下,需要放回什么位置上。 - ⑤这个时候就需要跟
reducers
去通信,我们可以把reducers
视为是一个记录本,图书馆管理员用这个记录本来记录需要的数据。管理员store
通过reducer
知道了应该给借书人Components
什么样的数据。
(3)react-redux
React-redux
中要了解的几个点是 Provider
、 Connect
、 mapStateToProps
和 mapDisptchToProps
。
来看以下代码:
import React from 'react' import { Provider } from 'react-redux' import { createStore } from 'redux' import todoApp from './reducers' import App from './components/App' let store = createStore(todoApp) export default function() { return <Provider store={store}> <App /> </Provider> } 复制代码
react-redux
提供了 Provider
的能力,大家可以看到最后部分的代码, Provider
将 <App />
包裹起来,其实也就是说为它包裹的所有组件提供 store
能力,这也是 Provider
发挥的作用。
再来看一段代码:
import { connect } from 'react-redux' import { toggleTodo } from '../actions' import TodoList from '../components/TodoList' // 不同类型的 todo 列表 const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) } } const mapStateToProps = state => { // state 即 vuex 的总状态,在 reducer/index.js 中定义 return { // 根据完成状态,筛选数据 todos: getVisibleTodos(state.todos, state.visibilityFilter) } } const mapDispatchToProps = dispatch => { return { // 切换完成状态 onTodoClick: id => { dispatch(toggleTodo(id)) } } } // connect 高阶组件,将 state 和 dispatch 注入到组件 props 中 const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList) export default VisibleTodoList 复制代码
在上面的代码中, connect
将 state
和 dispatch
给注入到组件的 props
中,将属性传递给到 TodoList
组件。
(4)异步action
redux
中的同步action
如下代码所示:
// 同步 action export const addTodo = text => { // 返回 action 对象 return { type: 'ADD_TODO', id: nextTodoId++, text } } 复制代码
redux
中的异步action
如下代码所示:
// 异步 action export const addTodoAsync = text => { // 返回函数,其中有 dispatch 参数 return (dispatch) => { // ajax 异步获取数据 fetch(url).thne(res => { // 执行异步 action dispatch(addTodo(res.text)) }) } } 复制代码
(5)Redux数据流图
Redux
的单项数据流图如下所示:
关于 Redux
更详细内容,可查看这篇文章:Redux从入门到进阶,看这一篇就够了!
2、React-router
(1)路由模式
React-router
和 vue-router
一样,都是两种模式。具体如下:
hash
模式(默认),如http://abc.com/#/user/10
H5 history
模式,如http://abc.com/user/20
- 后者需要
server
端支持,因此无特殊需求可选择前者
hash模式的路由配置如下代码所示:
import React from 'react' import { HashRouter as Router, Switch, Route } from 'react-router-dom' function RouterComponent() { return( <Router> <Switch> <Route exact path="/"> <Home /> </Route> <Route exact path="/project/:id"> <Project /> </Route> <Route path="*"> <NotFound /> </Route> </Switch> </Router> ) } 复制代码
History模式的路由配置如下:
import React from 'react' import { BrowserRouter as Router, Switch, Route } from 'react-router-dom' function RouterComponent() { return( <Router> <Switch> <Route exact path="/"> <Home /> </Route> <Route exact path="/project/:id"> <Project /> </Route> <Route path="*"> <NotFound /> </Route> </Switch> </Router> ) } 复制代码
注意,hash
和 history
的区别在于 import 中的 HashRouter
和 BrowserRouter
。
关于 hash
和 history
相关的内容,进一步了解可查看这篇文章:浅谈前端路由原理hash和history
(2)路由配置
Ⅰ. 动态路由
假设现在有父组件 RouterComponent
,具体代码如下:
function RouterComponent() { return( <Router> <Switch> <Route exact path="/"> <Home /> </Route> <Route exact path="/project/:id"> <Project /> </Route> <Route path="*"> <NotFound /> </Route> </Switch> </Router> ) } 复制代码
其中,在这个组件中还有一个 Project
组件,需要进行动态传参。
继续,我们来看下子组件 Project
组件时如何进行动态传参的。具体代码如下:
import React from 'react' import { Link, useParams } from 'react-router-dom' function Project() { // 获取 url 参数,如 '/project/100' const { id } = useParams() console.log('url param id', id) return ( <div> <Link to="/">首页</Link> </div> ) } 复制代码
大家可以看到,在 React
中,通过 const { id } = useParams()
这样的形式,来进行动态传参。
还有另外一种情况是跳转路由。请看以下代码:
import React from 'react' import { useHistory } from 'react-router-dom' function Trash() { let history = useHistory() function handleClick() { history.push('/') } return ( <div> <Button type="primary" onClick={handleClick}>回到首页</Button> </div> ) } 复制代码
大家可以看到,通过使用 useHistory
,让点击事件跳转到首页中。
Ⅱ. 懒加载
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'; import React, { Suspence, lazy } from 'react'; const Home = lazy(() => import('./routes/Home')); const About lazy(() => import('./routes/About')); const App = () => { <Router> <Suspense fallback={<div>Loading……</div>}> <Switch> <Route exact path="/" component={Home}/> <Route path="/about" component={About}/> </Switch> </Suspense> </Router> } 复制代码
在 React
中,我们可以直接用 lazy()
包裹,对页面的内容进行懒加载。当然,还有另外一种情况是,加载类似于首页初次加载页面 Loading
的那种效果,在 react
中可以使用 <Suspense>
来解决。
🗞️四、结束语
在上面的文章中,我们讲解了 react
的基本使用以及高级特性。同时,还讲解了 react
的周边插件, Redux
和 React-router
。
前端在做 react
的项目时,总是脱离不开以上文章所涉及到的知识点,唯一的区别在于基本使用的内容用的较多,而高级特性的使用场景相对会少一些。
希望通过上文的讲解,小伙伴们有所收获🥂