一. 安装React
- npm install -g create-react-app
二. 创建项目
- create-react-app 项目名称
三. 启动
- yarn start
四. 查看webpack配置
- yarn eject
五. 组件的定义方式
- 使用class关键字创建组件的特点:
- 使用class关键字创建的组件,有自己的私有数据(this.state)和生命周期函数;
- 用class关键字创建出来的组件叫做有状态组件【用的最多】
- 使用function关键字创建组件的特点:
- 使用function创建的组件,只有props, 没有自己的私有数据和生命周期函数;
- 用构造函数创建出来的组件:叫做无状态组件【无状态组件用的不多】
无关的东西
const flag = false; 区分 自动播放(false) 和 拖动播放(true)
if(!flag) 等价于 (flag === false) { // if(flag) 拖动了 if(!flag)没有拖动
...逻辑
}
六.生命周期
(Mounting)挂载
-
constructor
-
通过给
this.state
赋值对象来初始化内部的state; -
为事件绑定实例(this);
-
-
rander
这个钩子函数虽然是组件的渲染方法,但并不是真正意义上的把元素渲染成dom的方法,在它之后还有一个把元素渲染成dom的步骤,这一步只是return一个元素对象,这个并不是组件的执行渲染功能的函数,
- 渲染组件 第一次进入时会触发
-
componentDidMount
componentDidMount()
会在组件挂载后(插入 DOM 树中)立即调用。- 依赖于DOM的操作可以在这里进行;
- 在此处发送网络请求最好的地方;(官方建议)
- 可以在此处添加一些订阅(会在componentWillUnmount取消订阅)
(Updating)更新时
-
componentDidUpdate
componentDidUpdate()
会在更新后会被立即调用,首次渲染不会执行此方法。- 当组件更新后,可以在此处对 DOM 进行操作;
- 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求;(例如,当 props 未发生变化时,则不会执行网络请求)
(Unmounting)卸载
componentWillUnmount()
会在组件卸载及销毁之前直接调用。
-
componentWillUnmount
- 在此方法中执行必要的清理操作;
- 例如,清除 timer,取消网络请求或清除在
componentDidMount()
中创建的订阅等;
不常用生命周期
除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数:
- getDerivedStateFromProps:state 的值在任何时候都依赖于 props时使用;该方法返回一个对象来更新state;
- getSnapshotBeforeUpdate:在React更新DOM之前回调的一个函数,可以获取DOM更新前的一些信息(比如说滚动位置);
- shouldComponentUpdate:该生命周期函数很常用,但是我们等待讲性能优化时再来详细讲解;
详细描述:
七. 父传子通信-父传子
-
class类组件
/* 父组件: 通过自定义属性进行传值 子组件: 通过this.props接收数据 */ // 子组件 class Son extends Component { constructor(props) { // 这个constructor 可以有可以无, 因为内部有默认的constructor super(props); // constructor接受到参数, 然后通过props传给父类, this.props = props // super虽然代表父类的构造函数, 但是返回的是子类的实例, 即super内部的this指的是子类 } render() { let {name, age, sex} = this.props // 此时的 this指的是子类的this return ( <div> <h2>子组件接受父组件传来的数据: {name + " " + age + " " + sex}</h2> </div> ) } } // 父组件 export default class App extends Component { render() { return ( <div> <Son name="chenlong" age="23" sex="男"></Son> </div> ) } }
-
function组件
// 子组件 function Son(props) { const {name, age, sex} = props return ( <div> <h2>{name + " " + age + " " + sex}</h2> </div> ) } // 父组件 export default class App extends Component { render() { return ( <div> <Son name="chen" age="23" sex="男"></Son> </div> ) } }
八. 父传子通信-属性验证
-
使用 PropTypes 类型检查
import PropTypes from 'prop-types';
-
function写法
function Son(props) { const { name, age, sex} = props return ( <div> <h2>{name + age + sex}</h2> </div> ) } Son.propTypes = { name: PropTypes.string, age: PropTypes.number, sex: PropTypes.string }
-
class类写法
class Son extends Component{ static propTypes = { // 属性验证 name: PropTypes.string, age: PropTypes.number, sex: PropTypes.string } static defaultProps = { // 默认数据 name:'王小虎', age: '24', sex: '男' } render() { let {name, age, sex} = this.props return ( <div> <h2>{name + age + sex}</h2> </div> ) } }
-
验证没通过报错:
九一. 父传子通信-子传父
/*
父组件: 给子组件传递一个属性名, 并且绑定自己的函数
子组件: 通过触发this.props.父组件传递的属性名('需要传的值')
*/
class Son extends PureComponent {
render() {
return (
<div>
<button onClick={e => {this.handlePush()}}>我是子组件</button> // 1.子组件创建一个事件
</div>
)
}
handlePush(){
this.props.SonComment('我是子组件传来的值') // 3. 子组件调用父组件传来的SonComment函数, 并且携带需要传的值
}
}
export default class App extends PureComponent {
render() {
return (
<div>
<Son SonComment ={info => this.submitComment(info)} /> // 2.父组件自定义一个属性名SonComment
</div>
)
}
submitComment(val){ // 4.由于SonComment 绑定了submitComment事件,所以可以拿到子组件传来的值
console.log('父组件-----' + val)
}
}
九二. 父传子通信-子调用父组件的方法
/*
子组件: 通过接受父组件传递过来的事件名字, 给自己绑定上
父组件: 传递一个事件名字
*/
//写法1 子组件
class Son extends Component {
render() {
const { add } = this.props // 3.接受父组件传递过来的事件名字, 给自己绑定上
return (
<div>
<button onClick={add}>子组件调用父组件+1</button> // 4.绑定在自己身上
</div>
)
}
}
//写法2 子组件
class Son extends Component {
render() {
const { add } = this.props // 3.接受父组件传递过来的事件名字, 给自己绑定上
return (
<div>
<button onClick={this.addComment}>子组件调用父组件+1</button> // 4.绑定在自己身上
</div>
)
}
}
// 父组件
export default class App extends Component {
constructor(props){
super(props)
this.state = {
counter: 0
}
}
render() {
return (
<div>
<h2>{this.state.counter}</h2>
<button onClick={e => {this.add()}}>父组件+1</button>
{/* 2.给子组件传递一个叫add 的值(其实传的是一个函数) */}
<Son add={e => {this.add()}}></Son>
</div>
)
}
add(){ // 1.先创建一个事件
this.setState({
counter: this.state.counter+1
})
}
}
十. Slot组件插槽
-
children实现
-
每个组件都可以获取到
props.children
:它包含组件的开始标签和结束标签之间的内容。比如
-
如果只有一个元素,那么children指向该元素;
-
如果有多个元素,那么children指向的是数组,数组中包含多个元素;
-
弊端
:通过索引值获取传入的元素很容易出错,不能精准的获取传入的原生;
实现方式
:// 父组件 export default class App extends Component { render() { return ( <div> <h2>父组件:</h2> <Navbar> <span>slot-left</span> <span>slot-center</span> <span>slot-right</span> </Navbar> </div> ) } } // 子组件 export default class Navbar extends Component { render() { return ( <div className="nav-wrap"> <div className="nav-item nav-left"> {this.props.children[0]} {/* 通过 props.children取值 */} </div> <div className="nav-item nav-center"> {this.props.children[1]} </div> <div className="nav-item nav-right"> {this.props.children[2]} </div> </div> ) } }
源码分析
: -
-
props实现
- 通过具体的属性名,可以让我们在传入和获取时更加的精准;
实现方式:
// 父组件 App.js export default class App extends Component { render() { const slotLeft = <div>slot-left</div> const slotCenter = <div>slot-center</div> const slotRight = <div>slot-right</div> return ( <div> <h2>父组件:</h2> <h2>props实现:</h2> <Navbar2 slotLeft={slotLeft} slotCenter={slotCenter} slotRight={slotRight}></Navbar2> </div> ) } } // 子组件 Navbar.js export default class Navbar2 extends Component { render() { const { slotLeft, slotCenter, slotRight } = this.props return ( <div className="nav-wrap"> <div className="nav-item nav-left"> {slotLeft} </div> <div className="nav-item nav-center"> {slotCenter} </div> <div className="nav-item nav-right"> {slotRight} </div> </div> ) } }
十一.跨组件通信
-
中间组件层层传递
- 但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)。
- 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。
// 第二层组件 function Navbar(props) { return ( <div> <h2>用户昵称: {props.nickName}</h2> <h2>用户等级: {props.level}</h2> </div> ) } // 第一层组件 class Main extends Component { render() { return ( <div> <Navbar nickName={this.props.nickName} level={this.props.level}></Navbar> <ul> <li>设置1</li> <li>设置2</li> <li>设置3</li> <li>设置4</li> </ul> </div> ) } } // 父组件 export default class App extends Component { constructor(props){ super(props) this.state = { nickName : 'chen', level: 99 } } render() { const { nickName, level } = this.state return ( <div> <Main nickName={nickName} level={level}></Main> <h2>其他内容</h2> </div> ) } }
我这边顺便补充一个小的知识点:Spread Attributes
属性展开:
如果你已经有了一个 props 对象,你可以使用展开运算符
...
来在 JSX 中传递整个 props 对象。以下两个组件是等价的:
但是,如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:
- React提供了一个API:Context;
- Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props;
- Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言
2. Context相关的API
2.1 React.createContext 第一步
const MyContext = React.createContext(defaultValue);
-
创建一个需要共享的Context对象:
-
如果一个组件订阅了Context, 那么这个组件会从离自身最近的那个匹配的
Provider
中读取到当前的context值 -
defaultValue是组件在顶层查找过程中没有找到对应的
Provider
,那么就使用默认值(可以在里面放对象)2.2 Context.Provider
第二步
<MyContext.Provider value={/* 某个值 */}> // value: 代表需要共享的值
-
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化:
-
Provider 接收一个
value
属性,传递给消费组件; -
一个 Provider 可以和多个消费组件有对应关系;
-
多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
-
当 Provider 的
value
值发生变化时,它内部的所有消费组件都会重新渲染;2.3 Class.contextType
第三步
挂载在 class 上的 contextType
属性会被重赋值为一个由 React.createContext()
创建的 Context 对象:
- 这能让你使用
this.context
来消费最近 Context 上的那个值; - 你可以在任何生命周期中访问到它,包括 render 函数中;
MyClass.contextType = MyContext // myclass组件要订阅--> MyContext上的context(也就是共享出来的值)
源码
由于context是源码里已经声明了, 所以可以直接使用这个属性
↑↑↑ 以上是class式组件订阅context的写法
2.4 Context.Consumer 函数式组件订阅context的方法
-
这里,React 组件也可以订阅到 context 变更。这能让你在
函数式组件
中完成订阅 context。 -
这里需要 函数作为子元素(function as child)这种做法;
-
这个函数接收当前的 context 值,返回一个 React 节点;
<MyContext.Consumer> {value => /* 基于 context 值进行渲染*/} </MyContext.Consumer>
3. Context使用过程
-
class写法:
import React, { Component } from 'react' // 1.创建一个共享的Context对象: const UserContext = React.createContext({ nickName: '小陈', // 默认值 level: -1 }) // 孙组件 class Navbar extends Component{ render() { console.log(this.context) return ( <div> <h2>用户昵称: {this.context.nickName}</h2> // 消费context里面的值 <h2>用户等级: {this.context.level}</h2> </div> ) } } Navbar.contextType = UserContext; // 3.把 <UserContext.Provider>组件 value绑定的值,传给Navbar组件, 然后Navbar可以通过 context拿到传来的数据 // 子组件 class Main extends Component { render() { return ( <div> <Navbar ></Navbar> <ul> <li>设置1</li> <li>设置2</li> <li>设置3</li> <li>设置4</li> </ul> </div> ) } } // 父组件 export default class App extends Component { constructor(props){ super(props) this.state = { nickName : 'chen', level: 99 } } render() { return ( <div> {/* 2.接受一个 value属性, 传递个消费组件 */} <UserContext.Provider value={this.state}> <Main></Main> </UserContext.Provider> <h2>其他内容</h2> </div> ) } }
-
function函数组件写法:
什么时候使用Context.Consumer呢?
- 1.当使用value的组件是一个函数式组件时;
- 2.当组件中需要使用多个Context时;
演练一:
import React, { Component } from 'react' // 1.创建一个共享的Context对象: const UserContext = React.createContext({ nickName: '小陈', // 默认值 level: -1 }) // 孙组件 function Navbar() { return ( /*3.使用 Consumer消费订阅的值 */ <UserContext.Consumer> { value => { /* 4.基于context进行渲染 */ return ( <div> <h2>用户昵称: {value.nickName}</h2> <h2>用户等级: {value.level}</h2> </div> ) } } </UserContext.Consumer> ) } // 子组件 class Main extends Component { render() { return ( <div> <Navbar ></Navbar> <ul> <li>设置1</li> <li>设置2</li> <li>设置3</li> <li>设置4</li> </ul> </div> ) } } // 父组件 export default class App extends Component { constructor(props){ super(props) this.state = { nickName : 'chen', level: 99 } } render() { return ( <div> {/* 2.接受一个 value属性, 传递个消费组件 */} <UserContext.Provider value={this.state}> <Main></Main> </UserContext.Provider> <h2>其他内容</h2> </div> ) } }
演练二:
import React, { Component } from 'react' // 1.创建一个共享的Context对象: const UserContext = React.createContext({ nickName: '小陈', // 默认值 level: -1 }) // 2.创建第二个共享的Context对象 const ColorContext = React.createContext({ color:'pink' }) // 孙组件 function Navbar() { return ( /*4.使用 Consumer消费订阅的值 */ <UserContext.Consumer> { value => { /* 5.多个contenxt进行渲染 */ return ( <ColorContext.Consumer> { item =>{ return ( <div> <h2>用户昵称: {value.nickName}</h2> <h2>用户等级: {value.level}</h2> <h2>颜色: {item.color}</h2> </div> ) } } </ColorContext.Consumer> ) } } </UserContext.Consumer> ) } // 子组件 class Main extends Component { render() { return ( <div> <Navbar ></Navbar> <ul> <li>设置1</li> <li>设置2</li> <li>设置3</li> <li>设置4</li> </ul> </div> ) } } // 父组件 export default class App extends Component { constructor(props){ super(props) this.state = { nickName : 'chen', level: 99 } } render() { return ( <div> {/* 3.把值, 传递个消费组件 */} <UserContext.Provider value={this.state}> <ColorContext.Provider value={{color: "red"}}> <Main></Main> </ColorContext.Provider> </UserContext.Provider> <h2>其他内容</h2> </div> ) } }
十二. setState详细解析 和 React性能优化
1.setState
解决控制台打印异步问题:
this.setState({
list: [...this.state.list,info]
},() =>{
console.log(this.state.list)
})
2.SCU优化
-
在App中,我们增加了一个计数器的代码;
-
当点击+1时,会重新调用App的render函数;
-
而当App的render函数被调用时,所有的子组件的render函数都会被重新调用;
import React, { Component } from 'react'; function Header() { console.log("Header Render 被调用"); return <h2>Header</h2> } class Main extends Component { render() { console.log("Main Render 被调用"); return ( <div> <Banner/> <ProductList/> </div> ) } } function Banner() { console.log("Banner Render 被调用"); return <div>Banner</div> } function ProductList() { console.log("ProductList Render 被调用"); return ( <ul> <li>商品1</li> <li>商品2</li> <li>商品3</li> <li>商品4</li> <li>商品5</li> </ul> ) } function Footer() { console.log("Footer Render 被调用"); return <h2>Footer</h2> } export default class App extends Component { constructor(props) { super(props); this.state = { counter: 0, message:'hello world' } } /* shouldComponentUpdate(nextProps, nextState) { if (nextState.counter !== this.state.counter) { return true; } return false; } */ render() { console.log("App Render 被调用"); return ( <div> <h2>当前计数: {this.state.counter}</h2> <button onClick={e => this.increment()}>+1</button> <button onClick={e => this.changeText()}>改变文本</button> <Header/> <Main/> <Footer/> </div> ) } increment() { this.setState({ counter: this.state.counter + 1 }) } changeText() { this.setState({ message: "你好啊,李银河" }) } }
在以后的开发中,我们只要是修改了App中的数据,所有的组件都需要重新render,进行diff算法,性能必然是很低的:
- 事实上,很多的组件没有必须要重新render;
- 它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法;
如何来控制render方法是否被调用呢?
- 通过
shouldComponentUpdate
方法即可;
2.1 shouldComponentUpdate
如何来控制render方法是否被调用呢?
- 通过
shouldComponentUpdate
方法即可;
shouldComponentUpdate(nextProps, nextState) {
if (nextState.counter !== this.state.counter) {
return true;
}
return false;
}
这个时候,我们可以通过实现shouldComponentUpdate来决定要不要重新调用render方法:
- 这个时候,我们改变counter时,会重新渲染;
- 如果,我们改变的是message,那么默认返回的是false,那么就不会重新渲染;
2.2 PureComponent和memo
- 类组件使用 pureComponent来实现
如果所有的类,我们都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量。
我们来设想一下shouldComponentUpdate中的各种判断的目的是什么?
- props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false;
事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?
- 将class基础自PureComponent。
把所有 Component 改成 PureComponent :
import React, { PureComponent } from 'react';
function Header() {
console.log("Header Render 被调用");
return <h2>Header</h2>
}
class Main extends PureComponent {
render() {
console.log("Main Render 被调用");
return (
<div>
<Banner/>
<ProductList/>
</div>
)
}
}
PureComponent的原理是什么呢?
- 对props和state进行浅层比较;
查看PureComponent相关的源码:
react/ReactBaseClasses.js中:
- 在PureComponent的原型上增加一个isPureReactComponent为true的属性
React-reconcilier/ReactFiberClassComponent.js:
这个方法中,调用 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
,这个shallowEqual就是进行浅层比较:
- function组件使用memo来实现
我们需要使用一个高阶组件memo:
import React, { PureComponent,memo } from 'react';
const MemoHeader = memo(function Header() {
console.log("Header Render 被调用");
return <h2>Header</h2>
})
十三. setState不可变的力量
PureComponent使用了性能优化, 如果数据没发生变化, rander不会重新更新
handleName(val){
const newData = [...this.state.list]
newData[val].age +=1
this.setState({
list: newData
})
}
十四. 全局事件传递(bus)
兄弟组件传值
yarn add events
/*
* @Date: 2021-06-23 22:07:26
*/
import React, { Component, PureComponent } from 'react'
import { EventEmitter} from 'events'
// 事件总线: event bus
const eventBus = new EventEmitter()
class SonOne extends PureComponent {
componentDidMount(){// 页面加载监听的事件, 以及参数
// eventBus.addListener('getName',(name,age) => {
// })
eventBus.addListener('getName', this.handleGetNameListener) // 常用写法
}
componentDidUpdate(){ // 页面卸除 取消事件监听
eventBus.removeListener("getName",this.handleGetNameListener)
}
handleGetNameListener(name,age){
console.log(name,age)
}
render() {
return (
<div>
sonOne
</div>
)
}
}
class SonTwo extends PureComponent {
render() {
return (
<div>
SonTwo
<button onClick={e =>this.emmitEvent()}>点击了SonTwo</button>
</div>
)
}
emmitEvent(){
eventBus.emit("getName","陈龙",23) // 触发一个消息
}
}
export default class App extends PureComponent {
render() {
return (
<div>
<SonOne></SonOne>
<SonTwo></SonTwo>
</div>
)
}
}
十五. 全局事件传递(bus)
1. 通过ref操作DOM
import React, { createRef, PureComponent } from 'react'
export default class App extends PureComponent {
constructor(props){
super()
this.titleRef = createRef()
this.titleEl = null
}
render() {
return (
<div>
{/* 方法一: ref */}
<h2 ref="title">String Ref</h2>
{/* 方式二: React.createRef() */}
<h2 ref={this.titleRef}>Hello Create Ref</h2>
{/* 方式三: 传入一个函数 */}
<h2 ref={element => this.titleEl = element}>改变文本</h2>
<button onClick={e => this.changeText()}>改变文本</button>
</div>
)
}
changeText(){
this.refs.title.innerHTML = "你好啊,猜猜猜"
this.titleRef.current.innerHTML = "你好啊,猜猜猜"
this.titleEl.innerHTML = "你好啊,猜猜猜"
}
}
2. 通过ref触发子组件的方法
export default class App extends PureComponent {
constructor(props) {
super(props);
this.counterRef = createRef()
}
render() {
return (
<div>
<Counter ref={this.counterRef}/>
<button onClick={e => this.increment()}>app +1</button>
</div>
)
}
increment() { // 父组件调用子组件的方法
this.counterRef.current.increment();
}
}
函数式组件是没有实例的,所以无法通过ref获取他们的实例:
- 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
- 这个时候我们可以通过
React.forwardRef
,后面我们也会学习 hooks 中如何使用ref;
3.受控组件
在 HTML 中,表单元素(如<input>
、 <textarea>
和 <select>
)之类的表单元素通常自己维护 state,并根据用户输入进行更新。
而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()
来更新。
- 我们将两者结合起来,使React的state成为“唯一数据源”;
- 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;
- 被 React 以这种方式控制取值的表单输入元素就叫做“受控组件”;
例如,如果我们想让前一个示例在提交时打印出名称,我们可以将表单写为受控组件:
十五.高阶组件
1.1 高阶组件的使用
import React, { PureComponent } from 'react'
class App extends PureComponent { // 函数
render() {
return (
<div>
app {this.props.name}
</div>
)
}
}
/* 高阶组件函数方式 */
function enhanceComponent(WrapperComponent){ // 1.高阶组件
function NewComponent(props){
return <WrapperComponent {...props}/>
}
return NewComponent
}
const EnhanceComponent = enhanceComponent(App) // 3. 调用高阶组件
export default EnhanceComponent
1.2 . 利用高阶组件, 实现跨组件通信
const UserContext = React.createContext({
nickName: '小陈', // 这里代表默认数据
level: -1
})
/* 封装的一个高阶函数, 专门实现夸组件通信 */
function withUser(WrapperCpn){
return function(props){
return (
<UserContext.Consumer>
{
value => {
return <WrapperCpn {...props} {...value} />
}
}
</UserContext.Consumer>
)
}
}
/* class类的写法 */
class Home extends PureComponent {
render(){
return <h2> Home组件 {"昵称" + this.props.nickName + '年龄' + this.props.age + '性别' + this.props.gander}</h2>
}
}
/* 函数写法 */
function Footer(props){
const { nickName, age, gander} = props
return <h2> Footer函数组件 {"昵称" + nickName+ '年龄' + age + '性别' + gander}</h2>
}
const UserHome = withUser(Home)
const UserFooter = withUser(Footer)
1.3 渲染判断鉴权(类似访问权限)
/* 高阶函数鉴权 */
function withAuto(WrapperComponent){
const NewCpn = function(props){
if(props.isLogin){ // 如果权限为真,就显示传来的组件页面
return <WrapperComponent {...props}/> // 接受到的组件
}else{
return <Login/>
}
}
NewCpn.displayName = "AutoCpn" // 组件重命名
return NewCpn
}
/* class写法 */
class Login extends PureComponent { // 登录
render(){
return <h2>请先登录!!!</h2>
}
}
const AutoCartPage = withAuto(Cart) // 任何需要鉴权的页面都可以通过 调用 withAuto来鉴权
<AutoCartPage isLogin={true}/>{/* 购物车页面 */}
1.4 声明周期劫持(获取组件渲染时间)
function withTime(WrapperComponent){
return class extends PureComponent{
UNSAFE_componentWillMount(){ // 即将渲染获取一个时间 begin
this.begin = Date.now()
}
componentDidMount(){ // 渲染完成再获取一个时间 end
this.end = Date.now()
const interval = this.end - this.begin
console.log(`${WrapperComponent.name}渲染的时间为:${interval}`)// 获取传来的组件名字
}
render() {
return (
<div>
<WrapperComponent {...this.props}/>
</div>
)
}
}
}
高阶函数(HOC)的意义
十六.组件内容补充
1.1 ref的转发 forwardRef
/* function组件可以使用 forwardRef高阶组件 */
const Son = forwardRef(function(props,ref){
return (
<h2 ref={ref}>我是function函数组件使用ref</h2>
)
})
1.2 Portals的使用
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。
例子: 全局弹窗modal
1.添加一个节点
2.使用ReactDom渲染节点
import React, { PureComponent } from 'react'
import ReactDom from 'react-dom'
class Modle extends PureComponent {
constructor(props){
super(props)
}
render() {
return ReactDom.createPortal(
this.props.children, // 参数1: props.children 可以拿到组件里放的所有子节点
document.getElementById('modal') // 参数2: 是要渲染到哪一个DOM上
)
}
}
export default class App extends PureComponent {
render() {
return (
<div>
<Modle>
<h2>我是全局弹窗</h2>
</Modle>
</div>
)
}
}
1.3 Fragment (类似于vue 的template )
Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点
<div>
<p>我是节点,有div包裹</p>
<> {/* <> 是 Fragment的简写 短语发 */}
<h2>下面的使用了 Fragment没有div包裹</h2>
<div>
{
list.map((item,index) => {
return (
<Fragment key={item.name}>
<div>{item.name}</div>
<p>{item.age}</p>
<hr/>
</Fragment>
)
})
}
</div>
</>
</div>
1.4 StrictMode
StrictMode
是一个用来突出显示应用程序中潜在问题的工具。
- 与
Fragment
一样,StrictMode
不会渲染任何可见的 UI; - 它为其后代元素触发额外的检查和警告;
- 严格模式检查仅在开发模式下运行;它们不会影响生产构建;
可以为应用程序的任何部分启用严格模式:
- 不会对
Header
和Footer
组件运行严格模式检查; - 但是,
ComponentOne
和ComponentTwo
以及它们的所有后代元素都将进行检查;
import React from 'react';
function ExampleApplication() {
return (
<div>
<Header />
<React.StrictMode>
<div>
<ComponentOne />
<ComponentTwo />
</div>
</React.StrictMode>
<Footer />
</div>
);
}
十七.React中的样式选择
1.1 相比而言,React官方并没有给出在React中统一的样式风格:
-
由此,从普通的css,到css modules,再到css in js,有几十种不同的解决方案,上百个不同的库;
-
最好的或者说最适合自己的CSS方案,
-
方案一:内联样式的写法;
- 内联样式的优点:
- 1.内联样式, 样式之间不会有冲突
- 2.可以动态获取当前state中的状态
- 内联样式的缺点:
- 1.写法上都需要使用驼峰标识
- 2.某些样式没有提示
- 3.大量的样式, 代码混乱
- 4.某些样式无法编写(比如伪类/伪元素)
- 内联样式的优点:
-
方案二:普通的css写法;
- 如果我们按照普通的网页标准去编写,那么也不会有太大的问题;
- 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响;
- 但是普通的css都属于全局的css,样式之间会相互影响;
-
方案三:css modules;
- 但是这种方案也有自己的缺陷:
- 引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的;
- 所有的className都必须使用
{style.className}
的形式来编写; - 不方便动态来修改某些样式,依然需要使用内联样式的方式;
- 但是这种方案也有自己的缺陷:
-
方案四:css in js(styled-components);
-
原理就是创建一个样式组件 ,该组件渲染之后是一个自己定义的标签
-
Wrapper组件跟其余的react组件一样,只不过现在他们有了自己的样式
-
CSS-in-JS通过JavaScript来为CSS赋予一些能力,包括类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用、动态修改状态等等;
-
依然CSS预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点;
-
所以,目前可以说CSS-in-JS是React编写CSS最为受欢迎的一种解决方案;
-
-
这里选择方案四做演示:
1.2 css in js使用方法
1.2.1 标签模板字符串(介绍)
可以通过护板字符串的方式对一个函数进行调用
const name = "chen"
const age = "23"
function test(...args){
console.log(args)
}
test`aaa` // ["aaa", raw: Array(1)]
test`my name is ${name} , age is ${age}`
1.2.2 安装styled-components:
yarn add styled-components
1.2.3 styled-components
1.vscod css提示插件: vscode-styled-components
1.2.4 styled-components语法
- 基本语法
/*
* @Date: 2021-06-30 23:01:53
*/
import React, { PureComponent } from 'react'
import styled from 'styled-components'
const HomeWrapper = styled.div`
font-size:50px;
color: red;
.banner {
background-color: pink;
span {
background-color: green;
margin-right: 20px;
cursor: pointer;
&.avtive{
background-color: red;
color: #fff;
}
&:hover{
color: #fff;
}
}
}
`
const TitleWrapper = styled.h4` /* 1.设置一个h2的标签样式(类名如果重复可以用此方法解决) */
text-decoration: underline;
`
export default class Home extends PureComponent {
render() {
return (
<HomeWrapper>
<TitleWrapper>我是home标题</TitleWrapper>{/* 2.使用专属标签 */}
<div className="banner">
<div>我是轮播图</div>
<span className="avtive">1轮播图</span>
<span>2轮播图</span>
<span>3轮播图</span>
<span>4轮播图</span>
</div>
</HomeWrapper>
)
}
}
-
一些标签自带的属性用法
第一种写法:
直接在标签上写属性
import styled from 'styled-components' const InputWrapp = styled.input` background-color: lightblue; border-color: red; ` export default class About extends PureComponent { render() { return ( <div> <h2>我是about标题</h2> <InputWrapp placeholder="请输入文字"></InputWrapp> {/* 直接在标签上写属性 */} </div> ) } }
第二种写法进阶:
好处:
- props穿透
- attrs的使用
- 传入state作为props属性
可以使用 this.state对象的属性
import styled from 'styled-components' const InputWrapp = styled.input.attrs({ // attributes:的缩写, 属性的意思 placeholder:'请输入密码', bColor:'blue' // 定义一个属性值 })` background-color: lightblue; border-color: ${props =>props.bColor}; // 1.可以拿到 attrs里面定义的 属性值, color:${props => props.color};// 2. 可以动态拿到 this.state对象里面的属性 ` export default class About extends PureComponent { constructor(props){ super(props) this.state = { color: 'red' } } render() { return ( <div> <h2>我是about标题</h2> <InputWrapp placeholder="请输入文字"></InputWrapp> <br/> <InputWrapp color={this.state.color}></InputWrapp> </div> ) } }
-
继承样式
const CLbutton = styled.button` padding: 10px 20px; border-radius: 10px; border-color: red; color: red; ` const CLbutton2 = styled(CLbutton)` // 继承CLbutton的样式 border-color: yellow; ` export default class App extends PureComponent { render() { return ( <div> <h2>App</h2> <Home/> <About/> <p>样式的继承</p> <CLbutton>我是按钮</CLbutton> <CLbutton2>我是按钮2</CLbutton2> </div> ) } }
4.主题设置(全局设置样式)
import styled, { ThemeProvider} from 'styled-components' // 1.引用 ThemeProvider主题提供器 const CLbutton = styled.button` padding: 10px 20px; border-radius: 10px; border-color: red; color: red; ` const CLbutton2 = styled(CLbutton)` // 继承CLbutton的样式 border-color: yellow; color:${props => props.theme.themeColor}; // 3. 使用全局属性 ` export default class App extends PureComponent { render() { return ( <ThemeProvider theme={{themeColor:'#01bd83',fontSize:'30px'}}> {/*2. 定义全局属性 */} <h2>App</h2> <Home/> <About/> <p>样式的继承</p> <CLbutton>我是按钮</CLbutton> <CLbutton2>我是按钮2</CLbutton2> </ThemeProvider> ) } }
十八.Classnames组件通过透出一个对象来匹配配置类名
React动态添加样式
npm install classnames --save
yarn add classnames // 或者
import classname from 'classnames'
<h4 className={classname("AA","BB","CC","DD")}>动态添加类名</h4>
<h4 className={classname({"active":isActive, "bar": isBar}, "wrap")}>动态添加类名</h4>
<h4 className={classname("AA",errClass,{"active":isActive})}>动态添加类名</h4>
十九. AntDesign UI
1.2. AntDesign的安装
npm install antd --save
import {Button ,DatePicker } from 'antd' // 组件
import 'antd/dist/antd.css' // 样式
1.3 高级配置
1.3.1修改create-react-app 的默认配置.
那么如何来进行修改默认配置呢?社区目前有两个比较常见的方案:
- react-app-rewired + customize-cra;(这个是antd早期推荐的方案)
- craco;(目前antd推荐的方案)
1.3.2 第一步: 安装creco
yarn add @craco/craco
1.3.3 第二部: 修改package.json文件
- 原本启动时,我们是通过react-scripts来管理的;
- 现在启动时,我们通过craco来管理;
"scripts": {
- "start": "react-scripts start",
- "build": "react-scripts build",
- "test": "react-scripts test",
+ "start": "craco start",
+ "build": "craco build",
+ "test": "craco test",
}
1.3.4 第三部: 在根目录下创建craco.config.js文件用于修改默认配置 (类似于vue.config.js)
module.exports = {
// 配置文件
}
1.3.5 配置主题
按照 配置主题 的要求,自定义主题需要用到类似 less-loader 提供的 less 变量覆盖功能:
- 我们可以引入 craco-less 来帮助加载 less 样式和修改变量;
1.安装 craco-less
:
yarn add craco-less
2.修改craco.config.js中的plugins:
- 使用
modifyVars
可以在运行时修改LESS变量;
const CracoLessPlugin = require('craco-less');
module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { '@primary-color': '#1DA57A' },
javascriptEnabled: true,
},
},
},
},
],
}
3.引入antd的样式时,引入antd.less文件:
// import 'antd/dist/antd.css'
import 'antd/dist/antd.less';
1.3.6 配置别名 @
在项目开发中,某些组件或者文件的层级会较深,
- 如果我们通过上层目录去引入就会出现这样的情况:
../../../../components/button
; - 如果我们可以配置别名,就可以直接从根目录下面开始查找文件:
@/components/button
,甚至是:components/button
;
配置别名也需要修改webpack的配置,当然我们也可以借助于 craco 来完成:
const path = require('path'); // path模块是node.js中处理路径的核心模块
const resolve = dir => path.resolve(__dirname,dir) // __dirname当前craco.congig.js的路径, dir指传传过来的路径, 进行拼接
module.exports = {
...
,
webpack: {
alias: { // 别名
'@': resolve("src"), // @代表: __dirname(当前文件的路径) + src 所以=> @ 等价于 /scr
'components':resolve("src/components")// components代表: 使用components 等价于 components/src
}
}
}
在导入时就可以按照下面的方式来使用了:
import HYTitle from '../../title' // 原来的做法
import HYTitle from '@/components/title' // 使用 @
二十. React网络请求选择
1.Fetch Api
2.Axios的基本使用
2.1 安装 axios
npm i axios
2.2 二次封装axios
请求拦截加token , 响应拦截 判断后端返回的 响应码
二十一. react-transition-group
react-transition-group用来给一个组件的显示和消失添加某种过渡动画
1.0. 介绍
react-transition-group主要包含四个组件:
-
Transition
-
- 该组件是一个和平台无关的组件(不一定要结合CSS);
- 在前端开发中,我们一般是结合CSS来完成样式,所以比较常用的是CSSTransition;
-
CSSTransition
-
- 在前端开发中,通常使用CSSTransition来完成过渡动画效果
-
SwitchTransition
-
- 两个组件显示和隐藏切换时,使用该组件
-
TransitionGroup
-
- 将多个动画组件包裹在其中,一般用于列表中元素的动画;
2.0 CSSTransition使用
2.1 CSSTransition是基于Transition组件构建的:
-
自己通过 className="自定义一个类名" "chen-enter"// 就是显示时的class名字
-
CSSTransition执行过程中, 有三个状态: appear, enter, exit
动画流程 页面首次加载触发的动画 显示时触发的动画 隐藏时触发的动画 动画开始 -appear -enter exit 动画执行 -appear-active -enter-active -exit-active 动画结束 -appear-done -enter-done -exit-done -
然后自己根据这些类名写 需要的CSS动画样式
2.2 CSSTransition常见对应的属性:
-
in:触发进入或者退出状态(显示和隐藏的时候才要in)
-
- 如果添加了
unmountOnExit={true}
,那么该组件会在执行退出动画结束后被移除掉; - 当in为true时,触发进入状态,会添加-enter、-enter-acitve的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;
- 当in为false时,触发退出状态,会添加-exit、-exit-active的class开始执行动画,当动画执行结束后,会移除两个class,并且添加-enter-done的class;
- 如果添加了
-
classNames:动画class的名称
-
- 决定了在编写css时,对应的class名称:比如card-enter、card-enter-active、card-enter-done;
-
timeout:
-
- 过渡动画的时间
-
unmountOnExit:
-
unmountOnExit = {true}
-
是否开始 隐藏DOM 整个节点都隐藏
-
-
appear:
-
- 是否在初次进入添加动画(需要和in同时为true)需要配置 -appear, 所以最好的方法是 -appear 和 -enter 都设置成一个动画 .chen-appear , .chen-enter{}
-
其他属性可以参考官网来学习:
2.3. CSSTransition对应的钩子函数:
主要为了检测动画的执行过程,来完成一些JavaScript的操作
-
onEnter:在进入动画之前被触发;
-
onEntering:在应用进入动画时被触发;
-
onEntered:在应用进入动画结束后被触发;
-
onExit:退出动画前
-
onExiting:退出动画;
-
onExited:退出动画后
3. SwitchTransition
3.1 SwitchTransition可以完成两个组件之间切换的炫酷动画:
- 比如我们有一个按钮需要在on和off之间切换,我们希望看到on先从左侧退出,off再从右侧进入;
- 这个动画在vue中被称之为 vue transition modes;
- react-transition-group中使用SwitchTransition来实现该动画;
3.2 SwitchTransition中主要有一个属性:mode,有两个值
- in-out:表示新组件先进入,旧组件再移除;
- out-in:表示就组件先移除,新组建再进入;
3.3如何使用SwitchTransition呢?
- SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹你想要切换的组件;
- SwitchTransition里面的CSSTransition或Transition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是key属性;
我们来演练一个按钮的入场和出场效果:
4. TransitionGroup
- 当一个React组件添加或者移除一个DOM的时候可以轻易的实现CSS动画
- 你必须为CSSTransitionGroup的子级元素添加一个key属性,即使是渲染一个唯一的子元素的时候。React通过key来判断那个子级元素进入视野或者离开视野(显示或者隐藏)
二十二. React中的纯函数
-
确定的输入,一定会有产生确定的输出 (输出得到的结果,是确定中的值)
-
函数在执行过程中,不能产生副作用
1.0 纯函数需要满足一下几点
- 相同输入总是会返回相同的输出。
- 不产生副作用。
- 不依赖于外部状态。
例子1: 纯函数
function sum(num1, num2){
return num1 + num2
}
sum(20,30)
sum(20,40)
例子2: 不是纯函数
/* 不是纯函数,在执行中产生副作用 */
function print(info){
info.name = "xxx" // 修改了值
console.log(info.name)
}
二十三. Redux
1.0 整体做法
/* 整体写法 */
// const redux = require('redux')
import redux from 'redux' // node.js V13.2才支持
// 1.默认数据
const initialState = {
counter: 0
}
// 2.创建一个 reducer纯函数
function reducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return {...state,counter: state.counter + 1}
case 'DECREMENT':
return {...state,counter: state.counter - 1}
default:
state;
}
}
// 3. store(创建的时候需要传入一个reducer)
const store = redux.createStore(reducer)
// 5.订阅store的修改
store.subscribe(() => {
console.log('counter', store.getState().counter)
})
// 4. 创建actions
const action1 = {type: "INCREMENT"};
const action2 = {type: "DECREMENT"};
// 5.派发actions
store.dispatch(action1)
store.dispatch(action2)
1.2 封装写法(开发模式)
actionCreators.js 派发的函数
/* 3. 创建actions派发的函数
* 导入全局常量派发名字 constants.js
* @Date: 2021-07-04 23:10:05
*/
import {ADD_NUMBER,SUB_NUMBER} from "./constants.js"
export function addAction(num) {
return {
type: ADD_NUMBER, // 4.这里派发的名字是由constants.js管理
num
}
}
export function subAction(num) {
return {
type: SUB_NUMBER,
num
}
}
constants.js 放一些常量
/* 4.在这里用常量保存 axion派发的名字(统一方便管理)
* @Date: 2021-07-04 23:16:33
*/
export const ADD_NUMBER = "ADD_NUMBER"
export const SUB_NUMBER ="SUN_NUMBER"
index.js主要文件移入 redux
/* 1.做中间键(主要文件)
2.引入redux包,
3.导入默认值state 和 纯函数(处理逻辑) reducer.js
* @Date: 2021-07-04 22:48:29
*/
import redux from 'redux'
import reducer from './reducer.js'
/* store(创建的时候需要传入一个reducer)纯函数 */
const store = redux.createStore(reducer)
export default store
reducer.js 用来处理state的文件
/* 2.创建一个默认值 和 纯函数(加需要处理的逻辑)
* @Date: 2021-07-04 22:51:27
*/
import { ADD_NUMBER, SUB_NUMBER} from './constants.js'
// 创建一个默认值
const defaultState = {
counter: 0
}
// 创建一个 reducer纯函数
function reducer(state = defaultState, action) {
switch(action.type) {
case ADD_NUMBER:
return {...state, counter:state.counter + action.num}
case SUB_NUMBER:
return {...state, counter:state.counter - action.num}
default:
return state
}
}
export default reducer
index2.js 业务文件(需要使用redux的文件)
/* 主要使用strore文件
1.导入store index.js
2.导入派发的函数 actionCreators.js
3.订阅store的值
* @Date: 2021-07-04 23:02:03
*/
import store from './store/index.js'
import {addAction, subAction} from './store/actionCreators.js'
// 订阅store的修改
store.subscribe(() => {
console.log('counter', store.getState().counter)
})
// 派发函数
store.dispatch(addAction(10))
store.dispatch(subAction(8))
正常操作: constants .js ===>actionCreators .js ===> reducer.js ===> 业务文件(需要调用redux)派发action
1.3 Redux使用流程
1.4 脚手架里使用Redux
脚手架需要在 componentDidMount里 订阅 store.subscribe
/*
* @Date: 2021-07-05 21:01:27
*/
import React, { PureComponent } from 'react'
import store from '../store'
import { addAction,subAction } from '../store/actionCreators'
export default class Home extends PureComponent {
constructor(prost) {
super(prost)
this.state = {
counter: store.getState().counter
}
}
componentDidMount() { // 订阅(只有订阅了 视图才会更新)
this.unsubscribue = store.subscribe(() => {
this.setState({
counter: store.getState().counter
})
})
}
componentWillUnmount() { // 取消订阅
this.unsubscribue()
}
render() {
const { counter } = this.state
return (
<div>
<span>home</span>
<h2>当前计数: {counter}</h2>
<button onClick={e => this.increment()}>+1</button>
<button onClick={e => this.addNumber(5)}>+5</button>
</div>
)
}
increment(){
store.dispatch(addAction())
}
addNumber(num){
store.dispatch(subAction(num))
}
}
1.5 react-redux 库使用
本库并不是 Redux 内置,需要单独安装。因为一般会和 Redux 一起使用
1.0 下载
npm install --save react-redux
1.1 使用
import store from './store'
import { connect, Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}> // 固定属性store
<App4 />
</Provider>
,document.getElementById('root'));
1.3源码解析
ReactReduxContext来自另外一个文件:
connect函数最终调用的是connectHOC:
1.6 redux 异步请求
在组件中异步请求
1.7 redux异步请求 + 中间件redux-thunk(重要)
1.0 在redux中发送异步请求
1.1 使用步骤
-
1.先下载 redux-thunk中间件
yarn add redux-thunk
-
2.在store里引用中间件
- 3.在组件里 调用一个派遣函数
- 4 派遣一个action
- 中转派遣
两个参数 第一个派遣, 第二个可以拿到 store里面的数据
1.8 redux-saga
1.saga介绍
- takeEvery:可以传入多个监听的actionType,每一个都可以被执行(对应有一个takeLastest,会取消前面的)
- put:在saga中派发action不再是通过dispatch,而是通过put;
- all:可以在yield的时候put多个action;
2. 安装
yarn add redux-saga
3. 使用
import { takeEvery, put, all } from 'redux-saga/effects';
import axios from 'axios'
import { FETCH_HOME_MULTIDATA } from './constants';
import { bologAction } from './actionCreators'
function* fetchHomeMultdata(action) { //
const res = yield axios({
url: 'https://c1998.cn/apis/get/',
params: {
page: 1,
per_page: 10
}
})
const data = res.data.data
yield put(bologAction(data)) // put 代替了 dispatch
}
function* mySaga() {
// yield takeEvery(FETCH_HOME_MULTIDATA, fetchHomeMultdata)
yield all([
yield takeEvery(FETCH_HOME_MULTIDATA, fetchHomeMultdata)
]);
}
export default mySaga
1.9 reducer代码拆分
1.0 我们来看一下目前我们的reducer:
- 当前这个reducer既有处理counter的代码,又有处理home页面的数据;
- 后续counter相关的状态或home相关的状态会进一步变得更加复杂;
- 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;
如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。
我们先抽取一个对counter处理的reducer:
// 1.我们先抽取一个对counter处理的reducer:
const initialCounterState = { // 默认值
counter: 0
}
function counterReducer(state = initialCounterState, action) {
switch(action.type) {
case ADD_NUMBER:
return {...state, counter:state.counter + 1}
case SUB_NUMBER:
return {...state, counter:state.counter + action.num}
case Mult_NUMBER:
console.log('xxx')
return {...state, counter:state.counter * action.num}
default:
return state;
}
}
再抽取一个对home处理的reducer:
// 再抽取一个对home处理的reducer:
const initialHomeState = {
bologList:[]
}
function homeReducer(state = initialHomeState, action) {
switch(action.type) {
case BOLOG_LISTS:
console.log('执行了kk',action)
return {...state, bologList: action.bologList}
default:
return state
}
}
合并起来
// 如果将它们合并起来呢?
function reducer(state = {}, action) {
return {
counterInfo: counterReducer(state.counterInfo, action),
homeInfo: homeReducer(state.homeInfo, action)
}
}
使用
1.1 合并reducers
combineReducers: 合并 redux
2.0 ImmutableJS
2.1 React中的state如何管理
React的作者自己的建议
具体代码看 06-text-react pages8(终极版本)
FAQ:
单向数据流:
二十四. Router路由原理
1.1前端路由原理
URL的hash
<a href="#/home">home</a>
<a href="#/about">about</a>
<div class="router-view"></div>
// 1.获取router-view
const routerViewEl = document.querySelector(".router-view");
// 2.监听hashchange,来显示对应页面的内容
window.addEventListener("hashchange", () => {
switch(location.hash) {
case "#/home":
routerViewEl.innerHTML = "home";
break;
case "#/about":
routerViewEl.innerHTML = "about";
break;
default:
routerViewEl.innerHTML = "default";
}
})
- URL的hash也就是锚点(#), 本质上是改变window.location的href属性;
- 我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;
- hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径。
1.2 HTML5的History
const routerViewEl = document.querySelector(".router-view");
// 2.监听所有的a元素
const aEls = document.getElementsByTagName("a");
for (let aEl of aEls) {
aEl.addEventListener("click", (e) => {
e.preventDefault();
const href = aEl.getAttribute("href");
console.log(href);
history.pushState({}, "", href);
historyChange();
})
}
// 3.监听popstate和go操作
window.addEventListener("popstate", historyChange);
window.addEventListener("go", historyChange);
// 4.执行设置页面操作, 来显示对应的页面内容
function historyChange() {
switch(location.pathname) {
case "/home":
routerViewEl.innerHTML = "home";
break;
case "/about":
routerViewEl.innerHTML = "about";
break;
default:
routerViewEl.innerHTML = "default";
}
}
history接口是HTML5新增的, 它有l六种模式改变URL而不刷新页面:
- replaceState:替换原来的路径;
- pushState:使用新的路径;
- popState:路径的回退;
- go:向前或向后改变路径;
- forword:向前改变路径;
- back:向后改变路径;
二十五. react-router
yran add react-router-dom
1.react-router最主要的API是给我们提供的一些组件:
-
BrowserRouter或HashRouter
-
- Router中包含了对路径改变的监听,并且会将相应的路径传递给子组件;
- BrowserRouter使用history模式;
- HashRouter使用hash模式;
-
Link和NavLink:
-
- 通常路径的跳转是使用Link组件,最终会被渲染成a元素;
- NavLink是在Link基础之上增加了一些样式属性(后续学习);
- to属性:Link中最重要的属性,用于设置跳转到的路径;
-
Route:
-
- Route用于路径的匹配;
- path属性:用于设置匹配到的路径;
- component属性:设置匹配到路径后,渲染的组件;
- exact:精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件;
在App中进行如下演练:
import React, { PureComponent } from 'react'
import {BrowserRouter, Route, Link} from 'react-router-dom'
import Home from './Home'
import About from './About'
import Profile from './Profile'
export default class App extends PureComponent {
render() {
return (
<div>
<BrowserRouter>
<Link to="/">首页</Link>
<Link to="about">关于</Link>
<Link to="/profile">我的</Link>
<Route exact path="/" component={Home}></Route>
<Route path="/about" component={About}/>
<Route path="/profile" component={Profile}/>
</BrowserRouter>
</div>
)
}
}
2. NavLink的使用
路径选中时,对应的a元素变为红色
这个时候,我们要使用NavLink组件来替代Link组件:
- activeStyle:活跃时(匹配时)的样式;
- activeClassName:活跃时添加的class;
- exact:是否精准匹配;
先演示activeStyle:
{/* 自定义router选中高亮类名 */}
<NavLink exact to="/" activeClassName="link-active">首页</NavLink>
<NavLink to="/about" activeClassName="link-active">关于</NavLink>
<NavLink to="/profile" activeClassName="link-active">我的</NavLink>
3. Switch的作用
1.我们来看下面的路由规则:
- 当我们匹配到某一个路径时,我们会发现有一些问题;
- 比如/about路径匹配到的同时,
/:userid
也被匹配到了,并且最后的一个NoMatch组件总是被匹配到;
原因是什么呢?默认情况下,react-router中只要是路径被匹配到的Route对应的组件都会被渲染;
2.但是实际开发中,我们往往希望有一种排他的思想:
- 只要匹配到了第一个,那么后面的就不应该继续匹配了;
- 这个时候我们可以使用Switch来将所有的Route进行包裹即可;
4. Redirect(重定向)
Redirect用于路由的重定向,当这个组件出现时,就会执行跳转到对应的to路径中:
我们这里使用这个的一个案例:
-
用户跳转到User界面;
-
但是在User界面有一个isLogin用于记录用户是否登录:
-
- true:那么显示用户的名称;
- false:直接重定向到登录界面;
App.js中提前定义好Login页面对应的Route:
export default class uesr extends PureComponent {
constructor(props){
super(props)
this.state = {
isLogin: true
}
}
render() {
return this.state.isLogin ? (
<div>
<h2>user</h2>
<h2>用户名:__</h2>
</div>
):<Redirect to="/login"/> // 重定向
}
}
5.路由嵌套
这里我们假设about页面中有两个页面内容:
- 商品列表和消息列表;
- 点击不同的链接可以跳转到不同的地方,显示不同的内容;
export default class About extends PureComponent {
render() {
return (
<div>
<NavLink exact to="/about" activeClassName="link-active">电器</NavLink>
<NavLink exact to="/about/culture" activeClassName="link-active">衣服</NavLink>
<NavLink exact to="/about/contact" activeClassName="link-active">食品</NavLink>
<Switch>
<Route exact path="/about" component={AboutProduct}></Route>
<Route path="/about/culture" component={AboutMessagea}></Route>
<Route path="/about/contact" component={AboutMessageb}></Route>
</Switch>
</div>
)
}
}
6.手动跳转路由
目前我们实现的跳转主要是通过Link或者NavLink进行跳转的,实际上我们也可以通过JavaScript代码
进行跳转。
但是通过JavaScript代码
进行跳转有一个前提:必须获取到history对象。
如何可以获取到history的对象呢?两种方式
- 方式一:如果该组件是通过路由直接跳转过来的,那么可以直接获取history、location、match对象;
- 方式二:如果该组件是一个普通渲染的组件,那么不可以直接获取history、location、match对象;
那么如果普通的组件也希望获取对应的对象属性应该怎么做呢?
- 前面我们学习过高阶组件,可以在组件中添加想要的属性;
- react-router也是通过高阶组件为我们的组件添加相关的属性的;
如果我们希望在App组件中获取到history对象,必须满足以下两个条件:
- App组件必须包裹在Router组件之内;
- App组件使用withRouter高阶组件包裹;
index.js代码修改如下:
该组件是通过路由直接跳转过来的: 可以直接跳转
<button onClick={e => this.pushToProfile()}>前往我的</button>
pushToProfile(){
this.props.history.push("/user")
}
该组件是一个普通渲染的组件: 不能直接跳转会报错
解决:
使用 withRouter()高阶函数 导出的组件可以直接拿到 history
import { Route, Switch, NavLink, withRouter } from 'react-router-dom';
...省略其他的导入代码
class App extends PureComponent {
render() {
console.log(this.props.history);
return (
<div>
...其他代码
<button onClick={e => this.pushToProfile()}>我的</button>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/profile" component={Profile} />
<Route path="/:userid" component={User} />
<Route component={NoMatch} />
</Switch>
</div>
)
}
pushToProfile() {
this.props.history.push("/profile");
}
}
export default withRouter(App); // 使用 withRouter()导出路由
7.动态路由(传递参数)
传递参数有三种方式:
- 动态路由的方式;
- search传递参数;
- to传入对象;
1. 第一种: 动态路由传递 (拼接传递)
- 我们可以直接通过match对象中获取id;
- 这里我们没有使用withRouter,原因是因为Detail本身就是通过路由进行的跳转;
// 1.传递 app.js
<NavLink to={`/detail/${id}`} activeClassName="link-active">动态路由</NavLink>
<Route path="/detail/:id" component={Detail}/>
// 2. 接受参数detail.js
export default class Detail extends PureComponent {
render() {
const match = this.props.match; // 接受参数
console.log(match)
return (
<div>
<h2>Detail: {this.props.match.params.id}</h2>
</div>
)
}
}
2. search传递参数 (query传递 ) /?name=whty&, 已经不推荐了
- 我们在跳转的路径中添加了一些query参数;
1.传递 app.js
<NavLink to="/detail2?name=why&age=18" activeClassName="link-active">search(query)传递</NavLink>
<Route path="/detail2" component={Detail2}/>
export default class Detail extends PureComponent {
render() {
const match = this.props.location.search; // 接受参数
console.log(this.props.location.search)
return (
<div>
<h2>detail2: {match}</h2>
</div>
)
}
}
3.to传递参数
to可以直接传入一个对象
const info = {name:'chen',age:'23', gander: '男'}
// app.js
<NavLink to={{
pathname:'/detail3',
search:"?name=abc",
state:info
}} activeClassName="link-active">to(params)传递</NavLink>
export default class Detail extends PureComponent {
render() {
const match = this.props.location.state; // 接受参数
console.log(this.props.location.state)
return (
<div>
<h2>detail3: {match.name}</h2>
</div>
)
}
}
8.react-router-config 路由统一配置 (类似vue路由配置文件)***
目前我们所有的路由定义都是直接使用Route组件,并且添加属性来完成的。
但是这样的方式会让路由变得非常混乱,我们希望将所有的路由配置放到一个地方进行集中管理:
- 这个时候可以使用react-router-config来完成;
1.安装react-router-config:
yarn add react-router-config
2.创建router/index.js文件:
import Home from '../pages/Home'
import About,{AboutProduct, AboutMessageb, AboutMessagea} from '../pages/About'
import Profile from '../pages/Profile'
const routes = [
{
path:'/',
exact: true,
component: Home
},
{
path:'/about',
component: About,
routes:[ // 子路由
{
path:'/about',
exact: true,
component: AboutProduct
},
{
path:'/about/culture',
component: AboutMessagea
},
{
path:'/about/contact',
component: AboutMessageb
}
]
},
{
path:'/profile',
component: Profile
},
]
export default routes
3. 在app配置路由: 使用路由占位符
import router from './router'
import { renderRoutes } from 'react-router-config'
{renderRoutes(router)} // 类似路由占位符
4. 如果是子组件: 使用路由占位符
- 在跳转到的路由组件中会多一个
this.props.route
属性; - 该
route
属性代表当前跳转到的路由对象,可以通过该属性获取到routes
;
import { renderRoutes } from 'react-router-config'
{renderRoutes(this.props.route.routes)}
9.路由懒加载(优化)
好处: 只有等当前路由使用了, 才去加载当前的页面或组件
1.router.js配置以下代码:
- import CLDdiscover from "@/pages/discover"
+ const CLDdiscover = React.lazy(_ => import("../pages/discover"));
2.APP.js配置以下代码:
3.通过懒加载打包, 里面的js会 分开打
二十六. React Hooks
1. 简单总结一下hooks:
- 它可以让我们在不编写class的情况下使用state以及其他的React特性;
- 但是我们可以由此延伸出非常多的用法,来让我们前面所提到的问题得到解决;
Hook的使用场景:
- Hook的出现基本可以代替我们之前所有使用class组件的地方(除了一些非常不常用的场景);
- 但是如果是一个旧的项目,你并不需要直接将所有的代码重构为Hooks,因为它完全向下兼容,你可以渐进式的来使用它;
- Hook只能在函数组件中使用,不能在类组件,或者函数组件之外的地方使用;
export default function CounterHook() {
// 写法一
// const arr = useState(0)
// const state = arr[0]
// const setState = arr[1]
// 写法二
const [count, setCount] = useState(0)
return (
<div>
<h2>当前计数: {count}</h2>
<button onClick={e => setCount(count + 1)}>+!</button>
</div>
)
}
2. useState解析
HOOK指的是useState,useEffect这样的函数
Hooks是对这类函数的统称
3.认识useState
4.useEffect (代替生命周期)
类似vue里面的 created()
1.0 目前我们已经通过hook在函数式组件中定义state,那么类似于生命周期这些呢?
- Effect Hook 可以让你来完成一些类似于class中生命周期的功能;
- 事实上,类似于网络请求、手动更新DOM、一些事件的监听,都是React更新DOM的一些副作用(Side Effects);
- 所以对于完成这些功能的Hook被称之为 Effect Hook;
2.0 useEffect简介
useEffect
,字面意思可以理解为"执行副作用操作",对比于以前react class
的写法,可以把 useEffect
看做 componentDidMount
,componentDidUpdate
和 componentWillUnmount
这三个函数的组合。
3.0 使用方法
每次都触发
// 相当于 componentDidMount 和 componentDidUpdate
useEffect(() => {
console.log(`You clicked ${count} times`);
});
只需要触发一次
// 由于第二个参数传的是空数组,所以这里相当于componentDidMount
useEffect(() => {
console.log('订阅事件')
},[])
当属性值发生变化才触发
export default function HookUseEffect() {
const [count, setCount] = useState(0);
// 只有当count发生变化的时候才会触发,show发生变化不会触发
useEffect(() => {
console.log('修改Dom', count)
},[count])
return (
<div>
<h2>{count}</h2>
<button onClick={e => setCount(count + 1)}>+1</button>
</div>
)
}
触发, 然后销毁
import React, { useEffect, useState } from 'react'
export default function CustomScorllHook() {
const [count, setCount] = useState(0);
useEffect(() => { // 触发
setCount(10)
return () => { // 销毁
setCount(0)
}
})
return (
<div>
<h2 style={{padding: "1000px 0"}}>当前的位置: {count}</h2>
</div>
)
}
useState默认值如果需要计算 ,可以写成一个回调函数
const [name, setName] = useState(() =>{
return JSON.stringify(window.localStorage.getItem('name'))
})
5.useContext(跨组件通信)
在之前的开发中,我们要在组件中使用共享的Context有两种方式:
- 类组件可以通过
类名.contextType = MyContext
方式,在类中获取context; - 多个Context或者在函数式组件中通过
MyContext.Consumer
方式共享context;
但是多个Context共享时的方式会存在大量的嵌套:
- Context Hook允许我们通过Hook来直接获取某个Context的值(不需要嵌套);
App.js
import React, { PureComponent } from 'react'
import ContextHookDemo2 from './04_useContext的使用/02.useContext写法'
export const UserContext = React.createContext();
export const ThemContext = React.createContext();
export default class App extends PureComponent {
render() {
return (
<div>
<UserContext.Provider value={{name:'chen',age:18}}>
<ThemContext.Provider>
{/* <ContextHookDemo></ContextHookDemo>自己的组件 */}
<ContextHookDemo2/>
</ThemContext.Provider>
</UserContext.Provider>
</div>
)
}
}
在对应的函数式组件中使用Context Hook:
import React,{useContext} from 'react'
import {UserContext,ThemContext } from '../App'
export default function App() {
const user = useContext(UserContext)
return (
<div>
<span>{user.name}</span>
</div>
)
}
6.useRedux
import React,{useState, useReducer} from 'react'
import reducer from './reducer'
export default function useReduxProfile() {
const [state, dispatch] = useReducer(reducer, {counter: 0})
return (
<div>
<h2>当前计数: {state.counter}</h2>
<button onClick={e => {dispatch({type: "increment"})}}>+1</button>
<button onClick={e => {dispatch({type: "decrement"})}}>-1</button>
</div>
)
}
很多人看到useReducer的第一反应应该是redux的某个替代品,其实并不是。
useReducer仅仅是useState的一种替代方案:
- 在某些场景下,如果state的处理逻辑比较复杂,我们可以通过useReducer来对其进行拆分;
- 或者这次修改的state需要依赖之前的state时,也可以使用;
单独创建一个reducer/counter.js文件:
数据是不会共享的,它们只是使用了相同的counterReducer的函数而已。
所以,useReducer只是useState的一种替代品,并不能替代Redux。
7.useCallback性能优化
子组件依赖的函数没有被改变,就不执行(组件的回调函数常用)
1.0 useCallback实际的目的是为了进行性能的优化.
如何进行性能的优化呢?
- useCallback会返回一个函数的 memoized(记忆的) 值;
- 在依赖不变的情况下,多次定义的时候,返回的值是相同的;
2.0 useMemo 和 useCallback 接收的参数都是一样,第一个参数为回调 第二个参数为要依赖的数据
在将一个组件中的函数,传递给子元素进行回调函数使用时, 使用useCallback对函数镜像处理
8.useMemo性能优化
依赖的函数没有发生变化就不会执行
useCallback
是useMemo
的语法糖
例子1:
例子2:
9.useCallback 和 useMemo的区别
1.0 共同作用:
仅仅 依赖数据
发生变化, 才会重新计算结果,也就是起到缓存的作用。
2.0 两者区别:
-
useMemo计算结果是 return回来的值, 主要用于 缓存计算结果的值 ,应用场景如: 需要 计算的状态
-
useCallback 计算结果是 函数, 主要用于 缓存函数,应用场景如: 需要缓存的函数,因为函数式组件每次任何一个 state 的变化 整个组件 都会被重新刷新,一些函数是没有必要被重新刷新的,此时就应该缓存起来,提高性能,和减少资源浪费。
10. useRef
useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变。
最常用的ref是两种用法:
- 用法一:引入DOM(或者组件,但是需要是class组件)元素;
- 用法二:保存一个数据,这个对象在整个生命周期中可以保存不变;
1. 0 用法一: 引用dom
import React,{useRef,PureComponent} from 'react'
class Son extends PureComponent{
render(){
return <h2>SON</h2>
}
}
export default function useDomHook1() {
const titleRef = useRef()
const inputRef = useRef()
const SonRef = useRef()
function changeDom() {
titleRef.current.innerHTML = "Hello world"
inputRef.current.focus()
console.log(SonRef.current,'xxx') // 打印组件的内容 只支持class组件
}
return (
<div>
<h2 ref={titleRef}>RefHookDome1</h2>
<input type="text" ref={inputRef}/>
<button onClick={ changeDom}>DOM替换title</button>
<Son ref={SonRef}/>
</div>
)
}
1.1 用法二:使用ref保存上一次的某一个值
- useRef可以想象成在ref对象中保存了一个.current的可变盒子;
- useRef在组件重新渲染时,返回的依然是之前的ref对象,但是current是可以修改的;
export default function RefHooksDome2() {
const numRef = useRef(10) // red可以默认传入一个值
const [count, setCount] = useState(0)
// 利用useEffect的效果 , 只有count改变了 才执行 numRef.current赋值(记录上一次的数据)
useEffect(() => {
numRef.current = count
},[count])
function addInter() {
setCount(count + 10)
}
return (
<div>
<h2>RefHooksDome2: {numRef.current}</h2>
<h2>计算: {count}</h2>
<button onClick={addInter}>+10</button>
</div>
)
}
11.forwardRef 和useImperativeHandle
1.0 forwardRef
- forwardRef可以用 ref 操作函数组件的所有dom方法
import React, {useRef, forwardRef } from 'react'
const HyInput = forwardRef((props, ref) =>{
return <input ref={ref} type="text" />
})
export default function ForwardRedDome1() {
const inputRef = useRef()
return (
<div>
<HyInput ref={inputRef}/>
<button onClick={e => inputRef.current.focus()}>操作function组件</button>
</div>
)
}
1.2 useImperativeHandle
useImperativeHandle也是用来操作函数组件的
forwardRef 和 useImperativeHandle区别
- forwardRed是将子组件的DOM直接暴露给了父组件
- useImperativeHandle可以只暴露固定的操作:
- 比如只允许父组件可以操作focus,其他并不希望它随意操作
- 相当于子组件暴露一个你只能修改color, 父组件通过ref只能修改color
import React, { useRef, forwardRef, useImperativeHandle } from 'react'
const HyInput = forwardRef((props, ref) => {
const inputRef = useRef()
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus()
}
}))
return <input ref={inputRef} type="text" />
})
export default function ForwardRedDome2() {
const inputRef = useRef()
return (
<div>
<HyInput ref={inputRef} />
<button onClick={e => inputRef.current.focus()}>操作function组件</button>
</div>
)
}
12. useLayoutEffect
useLayoutEffect看起来和useEffect非常的相似,事实上他们也只有一点区别而已:
- useEffect会在渲染的内容更新到DOM上后执行,不会阻塞DOM的更新;
- useLayoutEffect会在渲染的内容更新到DOM上之前执行,会阻塞DOM的更新;
如果我们希望在某些操作发生之后再更新DOM,那么应该将这个操作放到useLayoutEffect。
例子1:
通过 点击更新 count, 此时执行一次dom更新, useEffect监听到count发生变化也会执行内部的setCount来修改DOM, 从而导致两次刷新两次dom操作,出现白屏
useLayoutEffect,可以代替useEffect, 等所有代码执行完毕后, 再进行DOM操作
import React, { useEffect, useLayoutEffect, useState } from 'react'
export default function UseLayoutEffectDom() {
const [count, setCount] = useState(10)
// 点击修改 count为:0 后会去更新dom, useEffect里面监听到count发生改变,也会更新dom 从而导致闪屏
// useEffect(() => {
// if(count == 0){
// setCount(Math.random()*50)
// }
// },[count])
// 使用 useLayoutEffect就不会出现闪屏, useLayoutEffect会等所有代码执行完毕后,再进行DOM的更新;
useLayoutEffect(() => {
if(count == 0){
setCount(Math.random()*50)
}
},[count])
return (
<div>
<h2>计数: {count}</h2>
<button onClick={e => {setCount(0)}}>随机数</button>
</div>
)
}
14. 自定义hooks
1.0练习一: Context的共享
// app.js 创建需要共享的值
<UserContext.Provider value={{name:'chen'}}>
<ThemContext.Provider value={{value:'777'}}>
<UseHooks/>
</ThemContext.Provider>
</UserContext.Provider>
// userHooks.js 创建自定义hook 封装Context
import { useContext } from 'react'
import { UserContext, ThemContext} from '../App'
function useUserContext() {
const user = useContext(UserContext)
const token = useContext(ThemContext)
return [user, token] // 自定义hook返回两个值
}
export default useUserContext
// cont.js 需要使用的共享数据的组件
import React from 'react'
import useUserContext from '../hooks/user-hooks' // 引入自定义的hooks
export default function UserHooks() {
const [user, token] = useUserContext() // 直接获取自定义hook的函数
return (
<div>
<h2>useHooks</h2>
<h4>{user.name}</h4>
<h4>{token.value}</h4>
</div>
)
}
1.1 练习二: 获取滚动距离
// use-scroll.js 创建自定义hooks
import { useEffect, useState} from "react";
function useScrollHook() {
const [scollNumber, setScollNumber] = useState(0)
const getScorll = () => {
setScollNumber(window.scrollY)
}
useEffect(() =>{
document.addEventListener('scroll',getScorll)
return () => {
document.removeEventListener('scroll',getScorll)
}
},[])
return scollNumber
}
export default useScrollHook
// 使用自定义hooks
import React, { useEffect, useState } from 'react'
import useScrollHook from '../hooks/user-scroll-hook' // 导入自定义hooks
export default function CustomScorllHook() {
const scrollTop = useScrollHook()
console.log(scrollTop)
return (
<div style={{padding: "1000px 0"}}>
<h2 style={{position: "fixed", left:0 , top:0}}>当前的位置:{scrollTop}</h2>
</div>
)
}
1.2 练习三: 动态设置缓存
// user-store-hook.js 创建自动以hooks
import { useEffect, useState } from "react";
function useLocalStorage(key) { // 传入缓存名字
const [name, setName] = useState(() => {
return JSON.parse(window.localStorage.getItem('name'))
})
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(name))
},[name])
return [name,setName] // 导出setName是为了好调用这个方法
}
export default useLocalStorage
// 使用自定义hooks
import useLocalStorage from '../hooks/user-store-hook' // 导入自定义hooks
export default function CustomDataLocal() {
// 使用自定义hook封装
const [name, setName] = useLocalStorage('name')
return (
<div>
<h2>CustomDataStoreHook: {name}</h2>
<button onClick={e => setName("大哥666")}>设置</button>
</div>
)
}
二十七: 实战项目使用 redux
1.先下载依赖包
yarn add redux react-redux redux-thunk
2.配置目录:
3.先配置 最大的store
4.配置一个: 合并所有子reudex 的 reducer文件
导入子redux: 这里是配置好了一个 bander , 所以先引用了:
5.通过Provider共享所有 store
6. 配置需要的子redux
-
创建一个bander文件夹 ,并且在文件夹内创建以下四个文件
-
创建一个 Module模块, 里面放左右子 redux; 方便好找
-
子redux 暴露出一个 reducer , 最外层的 reducer 利用 combineTeduers 对此进行合并
7.配置子redux
有两种写法:
第一种: 是通过connect()
- mapStateToProps:更新props————>作为输入源。返回一个对象,key为UI界面对应的名称,value为state处理的结果
- mapDispatchToProps:更新action————>作为输出源。触发action更新reducer,进而更新state, 引起UI的变化
- 缺点: 每次都需要写 mapStateToProps 和 mapDispatchToProps
第二种: 通过useSelector()
- useSelector可以订阅store, 当action被dispatched的时候,会运行selector。可以直接拿到stort
- 优点: 代码简单明了,
- 缺点: useSelector 使用的是 === 来比较, 比较的是两个引用类型 , 所以每次都会没 re-render重新加载(浪费性能)
- 解决: 可以引用一个 shallowEqual(), 让 useSelector进行浅层比较;
整体流程图:
1.对应的代码流程图(connect写法):
2. 对应的代码流程图(hooks写法) 常用!!!!
useSelector()有个bug: 比较的是两个引用地址, 所以 需要 加上 shallowEqual
shallowEqual: 是进行浅层比较(比较的是值)
但是下图没有写: 需要自己加上
8.解决 hooks里使用 useSelector() 的方法
- useSelector()将对前一个选择器结果值和当前结果值进行引用比较。 如果它们不同,则将强制重新渲染组件如果它们相同,则组件将不会重新渲染
1.例子:
-
当我在 home组件里 更改redux里面的值 , about组件被重新渲染了(about组件没有依赖home更改的值)
about组件被重新加载了(这是非常浪费性能的)
2.原因?
因为useSelector是 === 比较两个引用类型, 所以每次都会被重新
3.解决办法: shallowEqual()
shallowEqual比较值相等,或者对象含有相同的属性、且属性值相等。(浅层比较)
源码: 因为 shallowEqual 是浅层比较 比较的是 值相等
二十八. 数据的可变引起的问题 (优化)
1.介绍 Immutable
Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。
Map() : 是浅层比较只会比较一层对象,
fromJS(): 是深层比较
immutable.js
2.为什么要在redux里使用
-
因为在存函数里操作这个默认值,必须要进行浅拷贝一下(然后进行替换赋值)0
-
但是如果数据量很大, 并且全部进行拷贝那就很影响性能了
3.使用 Immutable
1. 下载
yarn add immutable
2.在redux.js 用Map包裹默认值, 并且设置一个key
3.使用 get() 拿刚刚设置的key
4.最后的优化 redux-immutable
Immutable 可以给应用带来极大的性能提升
1.下载
yarn add redux-immutable
2.使用
在最外层的 redux里使用
- 在 最外层的 reducer.js修改一下配置
- 使用