风不识途

风不识途 查看完整档案

北京编辑  |  填写毕业院校  |  填写所在公司/组织 wanguancs.top 编辑
编辑

学习前端

个人动态

风不识途 发布了文章 · 4月12日

前端知识体系思维导图

前言

hello大家好,我是"风不识途",本文是想分享一下,笔者在自学前端时所学到的知识,总结的思维导图(并不包含前端所有知识点),很多都是结合自己的笔记来进行总结的思维导图哟;

本篇文章适合人群:正在自学前端的小伙伴,可以参考前端大概都需要学习哪些技术,或者是已经学习过的兄弟姐妹,可以结合思维导图对面试、复习串联知识点进行复习;

如需查看源文件👉:已经上传的Github仓库中,对于访问Github比较慢的小伙伴,我也贴心的上传到Gitee仓库了,如果对你有帮助的话👏,那就给个 star ⭐ 鼓励一下吧~

前端三剑客🤺

HTML5&CSS3

HTMLCSS都对入门者比较友善,学一些标签和CSS属性很快就能做出效果,有反馈之后比较有动力往下继续学习,HTML5CSS3新增了许多有用的特性,学习起来相对轻松的;

移动Web

学习移动端的很多适配布局还是很有必要的,你会发现比浮动+定位布局不要太爽,移动端除了适配方案多比较麻烦,除此之外,布局比PC端轻松了许多;

JavaScript基础

JavaScript语法不算难,如果学习过其他语言再上手js也很容易,但js的诡异的特点都是需要靠日常项目填坑逐渐积累的,js库也是五花八门,没有所谓的学会,换成其他语言也是一样,关键是思维,函数编程思维、面向过程、面向对象、设计模式,浩如烟海,深不可测;

WebAPIS

大致的学习一遍,记住一些常用的API,最后再手写一个轮播图哈哈,基本都能了解到很多操作DOMAPI了;

JavaScript高级&ES6

ES6新增的许多特性,在实际开发中几乎每天都在使用,非常有必要掌握;

移动WEB知识点补充

jQuery

jQuery虽然已经过时了,里面封装代码的思想还是可以借鉴下的 (可以选择性跳过)

jQuery知识点分布

版本控制

Git

前端进阶

前端性能优化

Node.js

Node.js基础

Node常用的第三方模块

网络请求

AJAX

前端框架

Vue

React

查看原文

赞 0 收藏 0 评论 0

风不识途 发布了文章 · 4月8日

一文总结redux、react-redux、redux-saga

redux、react-redux、redux-saga总结

前言

hello大家好,我是风不识途,最近一直在整理redux系列文章,发现对于初学者不太友好,关系错综复杂,难倒是不太难,就是比较复杂 (其实写比较少),所以这篇带你全面了解redux、react-redux、redux-thunk还有redux-sage,immutable(多图预警),由于知识点比较多,建议先收藏(收藏等于学会了),对你有帮助的话就给个赞👍

认识纯函数

JavaScript纯函数

  • 函数式编程中有一个概念叫纯函数, JavaScript符合函数式编程的范式, 所以也有纯函数的概念
  • React中,纯函数的概念非常重要,在接下来我们学习的Redux中也非常重要,所以我们必须来回顾一下纯函数
  • 纯函数的定义简单总结一下:

    • 纯函数指的是, 每次给相同的参数, 一定返回相同的结果
    • 函数在执行过程中, 不能产生副作用
  • 纯函数( Pure Function )的注意事项:

    • 在纯函数中不能使用随机数
    • 不能使用当前的时间或日期, 因为结果是会变的
    • 不能使用或者修改全局状态, 比如DOM,文件、数据库等等(因为如果全局状态改变了,它就会影响函数的结果)
    • 纯函数中的参数不能变化,否则函数的结果就会改变

React中的纯函数

  • 为什么纯函数在函数式编程中非常重要呢?

    • 因为你可以安心的写和安心的用
    • 你在写的时候保证了函数的纯度,实现自己的业务逻辑即可,不需要关心传入的内容或者函数体依赖了外部的变量
    • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出
  • React非常灵活,但它也有一个严格的规则:

    • 所有React组件都必须像"纯函数"一样保护它们的"props"不被更改

认识Redux

为什么需要redux

  • JavaScript开发的应用程序, 已经变得非常复杂了:

    • JavaScript需要管理的状态越来越多, 越来越复杂了
    • 这些状态包括服务器返回的数据, 用户操作的数据等等, 也包括一些UI的状态
  • 管理不断变化的state是非常困难的:

    • 状态之间相互存在依赖, 一个状态的变化会引起另一个状态的变化, View页面也有可能会引起状态的变化
    • 当程序复杂时, state在什么时候, 因为什么原因发生了变化, 发生了怎样的变化, 会变得非常难以控制和追踪

React的作用

  • React只是在视图层帮助我们解决了DOM的渲染过程, 但是state依然是留给我们自己来管理:

    • 无论是组件定义自己的state,还是组件之间的通信通过props进行传递
    • 也包括通过Context进行数据之间的共享
    • React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定

  • Redux就是一个帮助我们管理State的容器:

    • ReduxJavaScript的状态容器, 提供了可预测的状态管理
  • Redux除了和React一起使用之外, 它也可以和其他界面库一起来使用(比如Vue), 并且它非常小 (包括依赖在内,只有2kb)

Redux的核心理念-Store

  • Redux的核心理念非常简单
  • 比如我们有一个朋友列表需要管理:

    • 如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的
    • 比如页面的某处通过products.push的方式增加了一条数据
    • 比如另一个页面通过products[0].age = 25修改了一条数据
  • 整个应用程序错综复杂,当出现bug时,很难跟踪到底哪里发生的变化

Redux的核心理念-action

  • Redux要求我们通过action来更新state

    • 所有数据的变化, 必须通过dispatch来派发action来更新
    • action是一个普通的JavaScript对象,用来描述这次更新的typecontent
  • 比如下面就是几个更新friendsaction:

    • 强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追踪、可预测的
    • 当然,目前我们的action是固定的对象,真实应用中,我们会通过函数来定义,返回一个action

Redux的核心理念-reducer

  • 但是如何将stateaction联系在一起呢? 答案就是reducer

    • reducer是一个纯函数
    • reducer做的事情就是将传入的stateaction结合起来来生成一个新的state

Redux的三大原则

  • 单一数据源

    • 整个应用程序的state被存储在一颗object tree中, 并且这个object tree只存储在一个store
    • Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护
    • 单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改
  • State是只读的

    • 唯一修改state的方法一定是触发action, 不要试图在其它的地方通过任何的方式来修改state
    • 这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state
    • 这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题
  • 使用纯函数来执行修改

    • 通过reducer将旧 stateaction 联系在一起, 并且返回一个新的state
    • 随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分
    • 但是所有的reducer都应该是纯函数,不能产生任何的副作用

Redux的基本使用

Redux中核心的API

redux的安装: yarn add redux

  1. createStore 可以用来创建 store对象
  2. store.dispatch 用来派发 action , action 会传递给 store
  3. reducer接收action,reducer计算出新的状态并返回它 (store负责调用reducer)
  4. store.getState 这个方法可以帮助获取 store 里边所有的数据内容
  5. store.subscribe方法可以让让我们订阅 store 的改变,只要 store 发生改变, store.subscribe 这个函数接收的这个回调函数就会被执行

小结

  1. 创建sotore, 决定 store 要保存什么状态
  2. 创建action, 用户在程序中实现什么操作
  3. 创建reducer, reducer 接收 action 并返回更新的状态

Redux的使用过程

  1. 创建一个对象, 作为我们要保存的状态
  2. 创建Store来存储这个state

    • 创建store时必须创建reducer
    • 我们可以通过 store.getState 来获取当前的state
  3. 通过action来修改state

    • 通过dispatch来派发action
    • 通常action中都会有type属性,也可以携带其他的数据
  4. 修改reducer中的处理代码

    • 这里一定要记住,reducer是一个纯函数,不能直接修改state
    • 后面会讲到直接修改state带来的问题
  5. 可以在派发action之前,监听store的变化
import { createStore } from 'redux'

// 1.初始化state
const initState = { counter: 0 }

// 2.reducer纯函数 不能修改传递的state
function reducer(state = initState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { ...state, counter: state.counter + 1 }
    case 'ADD_COUNTER':
      return { ...state, counter: state.counter + action.num }
    default:
      return state
  }
}

// 3.store 参数放一个reducer
const store = createStore(reducer)

// 4.action
const action1 = { type: 'INCREMENT' }
const action2 = { type: 'ADD_COUNTER', num: 2 }

// 5.订阅store的修改
store.subscribe(() => {
  console.log('state发生了改变: ', store.getState().counter)
})

// 6.派发action
store.dispatch(action1)
store.dispatch(action2)

redux原理图

Redux结构划分

  • 如果我们将所有的逻辑代码写到一起, 那么当redux变得复杂时代码就难以维护
  • 对代码进行拆分, 将store、reducer、action、constants拆分成一个个文件

<details>
<summary>拆分目录</summary>

</details>

Redux使用流程

redux

Redux官方流程图

redux-flow

React-Redux的使用

redux融入react代码(案例)

  • redux融入react代码案例:

    • Home组件:其中会展示当前的counter值,并且有一个+1和+5的按钮
    • Profile组件:其中会展示当前的counter值,并且有一个-1和-5的按钮

  • 核心代码主要是两个:

    • componentDidMount 中订阅数据的变化,当数据发生变化时重新设置 counter
    • 在发生点击事件时,调用storedispatch来派发对应的action

自定义connect函数

当我们多个组件使用redux时, 重复的代码太多了, 比如: 订阅state取消订阅state 或 派发action获取state

将重复的代码进行封装, 将不同的statedispatch作为参数进行传递

//  connect.js 
import React, { PureComponent } from 'react'
import { StoreContext } from './context'
/**
 * 1.调用该函数: 返回一个高阶组件
 *      传递需要依赖 state 和 dispatch 来使用state或通过dispatch来改变state
 *
 * 2.调用高阶组件:
 *      传递该组件需要依赖 store 的组件
 *
 * 3.主要作用:
 *      将重复的代码抽取到高阶组件中,并将该组件依赖的 state 和 dispatch
 *      通过调用mapStateToProps()或mapDispatchToProps()函数
 *      并将该组件依赖的state和dispatch供该组件使用,其他使用store的组件不必依赖store
 *
 * 4.connect.js: 优化依赖
 *      目的:但是上面的connect函数有一个很大的缺陷:依赖导入的 store
 *      优化:正确的做法是我们提供一个Provider,Provider来自于我们
 *             Context,让用户将store传入到value中即可;
 */
export function connect(mapStateToProps, mapDispatchToProps) {
  return function enhanceComponent(WrapperComponent) {
    class EnhanceComponent extends PureComponent {
      constructor(props, context) {
        super(props, context)

        // 组件依赖的state
        this.state = {
          storeState: mapStateToProps(context.getState()),
        }
      }

      // 订阅数据发生变化,调用setState重新render
      componentDidMount() {
        this.unsubscribe = this.context.subscribe(() => {
          this.setState({
            centerStore: mapStateToProps(this.context.getState()),
          })
        })
      }

      // 组件被卸载取消订阅
      componentWillUnmount() {
        this.unsubscribe()
      }

      render() {
        // 下面的WrapperComponent相当于 home 组件(就是你传递的组件)
        // 你需要将该组件需要依赖的state和dispatch作为props进行传递
        return (
          <WrapperComponent
            {...this.props}
            {...mapStateToProps(this.context.getState())}
            {...mapDispatchToProps(this.context.dispatch)}
          />
        )
      }
    }
    // 取出Provider提供的value
    EnhanceComponent.contextType = StoreContext
    return EnhanceComponent
  }
}

// home.js
// 定义组件依赖的state和dispatch
const mapStateToProps = state => ({
  counter: state.counter,
})

const mapDispatchToProps = dispatch => ({
  increment() {
    dispatch(increment())
  },
  addNumber(num) {
    dispatch(addAction(num))
  },
})
export default connect(mapStateToProps,mapDispatchToProps)(依赖redux的组件)

react-redux使用

  • 开始之前需要强调一下,reduxreact没有直接的关系,你完全可以在React, Angular, Ember, jQuery, or vanilla JavaScript中使用Redux
  • 尽管这样说,redux依然是和React或者Deku的库结合的更好,因为他们是通过state函数来描述界面的状态,Redux可以发射状态的更新,让他们作出相应。
  • 虽然我们之前已经实现了connectProvider这些帮助我们完成连接redux、react的辅助工具,但是实际上redux官方帮助我们提供了 react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效
  • 安装react-redux

    • yarn add react-redux
// 1.index.js
import { Provider } from 'react-redux'
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

// 2.home.js
import { connect } from 'react-redux'
// 定义需要依赖的state和dispatch (函数需要返回一个对象)
export default connect(mapStateToProps, mapDispatchToProps)(About)

react-redux源码导读

Redux-Middleware中间件

组件中异步操作

  • 在之前简单的案例中,redux中保存的counter是一个本地定义的数据

    • 我们可以直接通过同步的操作来dispatch actionstate就会被立即更新。
    • 但是真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux
  • 网络请求可以在class组件的componentDidMount中发送,所以我们可以有这样的结构:

redux中异步操作

  • 上面的代码有一个缺陷:

    • 我们必须将网络请求的异步代码放到组件的生命周期中来完成
  • 为什么将网络请求的异步代码放在redux中进行管理?

    • 后期代码量的增加,如果把网络请求异步函数放在组件的生命周期里,这个生命周期函数会变得越来越复杂,组件就会变得越来越大
    • 事实上,网络请求到的数据也属于状态管理的一部分,更好的一种方式应该是将其也交给redux来管理

  • 但是在redux中如何可以进行异步的操作呢?

    • 使用中间件 (Middleware)
    • 学习过ExpressKoa框架的童鞋对中间件的概念一定不陌生
    • 在这类框架中,Middleware可以帮助我们在请求和响应之间嵌入一些操作的代码,比如cookie解析、日志记录、文件压缩等操作

理解中间件(重点)

  • redux也引入了中间件 (Middleware) 的概念:

    • 这个<font color='red'>中间件的目的是在dispatchaction和最终达到的reducer之间,扩展一些自己的代码</font>
    • 比如日志记录、调用异步接口、添加代码调试功能等等

redux-middlware

  • redux-thunk是如何做到让我们可以发送异步的请求呢?

    • 默认情况下的dispatch(action)action需要是一个JavaScript的对象
    • redux-thunk可以让dispatch(action函数), action<font color='red'>可以是一个函数</font>
    • 该函数会被调用, 并且会传给这个函数两个参数: 一个dispatch函数和getState函数

      • dispatch函数用于我们之后再次派发action
      • getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态

redux-thunk的使用

  1. 安装redux-thunk

    • yarn add redux-thunk
  2. 在创建store时传入应用了middlewareenhance函数

    • 通过applyMiddleware来结合多个Middleware, 返回一个enhancer
    • enhancer作为第二个参数传入到createStore

      image-20200821182447344

  3. 定义返回一个函数的action

    • 注意:这里不是返回一个对象了,而是一个函数
    • 该函数在dispatch之后会被执行

<details>
<summary>查看代码</summary>
<pre>import { createStore, applyMiddleware } from 'redux'
import reducer from './reducer'
import thunk from 'redux-thunk'<br/>
const store = createStore(
reducer,
applyMiddleware(thunk) // applyMiddleware可以使用中间件模块
)
export default store
</pre></details>

redux-devtools

redux-devtools插件

  • 我们之前讲过,redux可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?

    • redux官网为我们提供了redux-devtools的工具
    • 利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等
  • 使用步骤:

    • 第一步:在浏览器上安装redux-devtools扩展插件
    • 第二步:在redux中集成devtools的中间件
// store.js 开启redux-devtools扩展
import { createStore, applyMiddleware, compose } from 'redux'

// composeEnhancers函数
const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose

// 通过applyMiddleware来结合多个Middleware,返回一个enhancer
const enhancer = applyMiddleware(thankMiddleware)

// 通过enhancer作为第二个参数传递createStore中
const store = createStore(reducer, composeEnhancers(enhancer))

export default store

redux-sage

generator

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

// 生成器函数的定义
// 默认返回: Generator
function* foo() {
  console.log('111')
  yield 'hello'
  console.log('222')
  yield 'world'
  console.log('333')
  yield 'jane'
  console.log('444')
}
// iterator: 迭代器
const result = foo()
console.log(result)

// 使用迭代器
// 调用next,就会消耗一次迭代器
const res1 = result.next()
console.log(res1) // {value: "hello", done: false}
const res2 = result.next()
console.log(res2) // {value: "world", done: false}
const res3 = result.next()
console.log(res3) // {value: "jane", done: false}
const res4 = result.next()
console.log(res4) // {value: undefined, done: true}

redux-sage流程

redux-saga的使用

  • redux-saga是另一个比较常用在redux发送异步请求的中间件,它的使用更加的灵活
  • Redux-saga的使用步骤如下

    1. 安装redux-sage: yarn add redux-saga
    2. 集成redux-saga中间件

      • 引入 createSagaMiddleware 后, 需要创建一个 sagaMiddleware
      • 然后通过 applyMiddleware 使用这个中间件,接着创建 saga.js 这个文件
      • 启动中间件的监听过程, 并且传入要监听的saga
    3. saga.js文件的编写

      • takeEvery:可以传入多个监听的actionType,每一个都可以被执行(对应有一个takeLatest,会取消前面的)
      • put:在saga中派发action不再是通过dispatch, 而是通过put
      • all:可以在yield的时候put多个action
// store.js
import createSageMiddleware from 'redux-saga'
import saga from './saga'
// 1.创建sageMiddleware中间件
const sagaMiddleware = createSageMiddleware()
// 2.应用一些中间件
const enhancer = applyMiddleware(sagaMiddleware)
const store = createStore(reducer,composeEnhancers(enhancer))

sagaMiddleware.run(saga)
export default store

// saga.js
import { takeEvery, put, all } from 'redux-saga/effects'
import { FETCH_HOME_DATA } from './constant'

function* fetchHomeData(action) {
  const res = yield axios.get('http://123.207.32.32:8000/home/multidata')
  const banners = res.data.data.banner.list
  const recommends = res.data.data.recommend.list
  // dispatch action 提交action,redux-sage提供了put
  yield all([
    yield put(changeBannersAction(banners)),
    yield put(changeRecommendAction(recommends)),
  ])
}

function* mySaga() {
  // 参数一:要拦截的actionType
  // 参数二:生成器函数
  yield all([
    takeEvery(FETCH_HOME_DATA, fetchHomeData),
  ])
}

export default mySaga

reducer代码拆分

Reducer代码拆分

  • 我们来看一下目前我们的reducer

    • 当前这个reducer既有处理counter的代码,又有处理home页面的数据
    • 后续counter相关的状态或home相关的状态会进一步变得更加复杂
    • 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等
    • 如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护
  • 因此,我们可以对reducer进行拆分:

    • 我们先抽取一个对counter处理的reducer
    • 再抽取一个对home处理的reducer
    • 将它们合并起来

Reducer文件拆分

  • 目前我们已经将不同的状态处理拆分到不同的reducer中,我们来思考:

    • 虽然已经放到不同的函数了,但是这些函数的处理依然是在同一个文件中,代码非常的混乱
    • 另外关于reducer中用到的constantaction等我们也依然是在同一个文件中;

combineReducers函数

  • 目前我们合并的方式是通过每次调用reducer函数自己来返回一个新的对象
  • 事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并
import { combineReducers } from 'redux'
import { reducer as counterReducer } from './count'
import { reducer as homeReducer } from './home'

export const reducer = combineReducers({
  counterInfo: counterReducer,
  homeInfo: homeReducer,
})
  • 那么combineReducers是如何实现的呢?

    • 它将我们传递的reducer合并成一个对象, 最终返回一个combination函数
    • 在执行combination函数过程中, 会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state

immutableJs

数据可变形的问题

  • React开发中,我们总是会强调数据的不可变性:

    • 无论是类组件中的state,还是reduex中管理的state
    • 事实上在整个JavaScript编码的过程中,数据的不可变性都是非常重要的
  • 数据的可变性引发的问题(案例):

    • 我们明明没有修改obj,只是修改了obj2,但是最终obj也被我们修改掉了
    • 原因非常简单,对象是引用类型,它们指向同一块内存空间,两个引用都可以任意修改
const obj1 = { name: 'jane', age: 18 }
const obj2 = obj1
obj1.name = 'kobe'
console.log(obj2.name) // kobe
  • 有没有办法解决上面的问题呢?

    • 进行对象的拷贝即可:Object.assign或扩展运算符
  • 这种对象的浅拷贝有没有问题呢?

    • 从代码的角度来说,没有问题,也解决了我们实际开发中一些潜在风险
    • 从性能的角度来说,有问题,如果对象过于庞大,这种拷贝的方式会带来性能问题以及内存浪费
  • 有人会说,开发中不都是这样做的吗?

    • 从来如此,便是对的吗?

认识ImmutableJS

  • 为了解决上面的问题,出现了Immutable对象的概念:

    • Immutable对象的特点是只要修改了对象,就会返回一个新的对象,旧的对象不会发生改变;
  • 但是这样的方式就不会浪费内存了吗?

    • 为了节约内存,又出现了一个新的算法:Persistent Data Structure(持久化数据结构或一致性数据结构)
  • 当然,我们一听到持久化第一反应应该是数据被保存到本地或者数据库,但是这里并不是这个含义:

    • 用一种数据结构来保存数据
    • 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费,如何做到这一点呢?结构共享:

  • 安装Immutable: yarn add immutable

ImmutableJS常见API

注意:我这里只是演示了一些API,更多的方式可以参考官网

作用:不会修改原有数据结构,返回一个修改后新的拷贝对象

  • JavaScripImutableJS直接的转换

    • 对象转换成Immutable对象:Map
    • 数组转换成Immtable数组:List
    • 深层转换:fromJS
const im = Immutable
// 对象转换成Immutable对象
const info = {name: 'kobe', age: 18}
const infoIM = im.Map()

// 数组转换成Immtable数组
const names = ["abc", "cba", "nba"]
const namesIM = im.List(names)
  • ImmutableJS的基本操作:

    • 修改数据:set(property, newVal)

      • 返回值: 修改后新的数据结构
    • 获取数据:get(property/index)
    • 获取深层Immutable对象数据(子属性也是Immutable对象): getIn(['recommend', 'topBanners'])
// set方法 不会修改infoIM原有数据结构,返回修改后新的数据结构
const newInfo2IM = infoIM.set('name', 'james')
const newNamesIM = namesIM.set(0, 'why')

// get方法
console.log(infoIM.get('name'))// -> kobe
console.log(namesIM.get(0))// -> abc

结合Redux管理数据

  1. ImmutableJS重构redux

    • yarn add Immutable
    • yarn add redux-immutable
  2. 使用redux-immutable中的combineReducers;
  3. 所有的reducer中的数据都转换成Immutable类型的数据

FAQ

React中的state如何管理

  • 目前项目中采用的state管理方案(参考即可):

    • 相关的组件内部可以维护的状态,在组件内部自己来维护
    • 只要是需要共享的状态,都交给redux来管理和维护
    • 从服务器请求的数据(包括请求的操作) ,交给redux来维护

前言

hello大家好,我是风不识途,最近一直在整理redux系列文章,发现对于初学者不太友好,关系错综复杂,难倒是不太难,就是比较复杂 (其实写比较少),所以这篇带你全面了解redux、react-redux、redux-thunk还有redux-sage,immutable(多图预警),由于知识点比较多,建议先收藏(收藏等于学会了),对你有用的话就给个赞👍

认识纯函数

JavaScript纯函数

  • 函数式编程中有一个概念叫纯函数, JavaScript符合函数式编程的范式, 所以也有纯函数的概念

  • React中,纯函数的概念非常重要,在接下来我们学习的Redux中也非常重要,所以我们必须来回顾一下纯函数

  • 
    纯函数的维基百科定义(了解即可)
    
    ​
    
  • 纯函数的定义简单总结一下:

    
    *   纯函数指的是, 每次给相同的参数, 一定返回相同的结果
        
    *   函数在执行过程中, 不能产生副作用
        
  • 
    **纯函数( `Pure Function` )的注意事项:**
    
    ​
    

React中的纯函数

  • 为什么纯函数在函数式编程中非常重要呢?

    
    *   因为你可以安心的写和安心的用
        
    *   你在写的时候保证了函数的纯度,实现自己的业务逻辑即可,不需要关心传入的内容或者函数体依赖了外部的变量
        
    *   你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出
        
  • React非常灵活,但它也有一个严格的规则:

    
    *   所有React组件都必须像"纯函数"一样保护它们的"props"不被更改
        

认识Redux

为什么需要redux

  • JavaScript开发的应用程序, 已经变得非常复杂了:

    
    *   `JavaScript`**需要管理的状态越来越多**, 越来越复杂了
        
    *   这些状态包括服务器返回的数据, 用户操作的数据等等, 也包括一些`UI`的状态
        
  • 管理不断变化的state是非常困难的:

    
    *   **状态之间相互存在依赖**, 一个状态的变化会引起另一个状态的变化, `View`页面也有可能会引起状态的变化
        
    *   当程序复杂时, `state`在什么时候, 因为什么原因发生了变化, 发生了怎样的变化, 会变得非常难以控制和追踪
        

React的作用

  • React只是在视图层帮助我们解决了DOM的渲染过程, 但是state依然是留给我们自己来管理:

    
    *   无论是组件定义自己的`state`,还是组件之间的通信通过`props`进行传递
        
    *   也包括通过`Context`进行数据之间的共享
        
    *   `React`主要负责帮助我们管理视图,`state`如何维护最终还是我们自己来决定
        

![](https://gitee.com/xmkm/cloudPic/raw/master/img/20201005132319.png)
  • Redux就是一个帮助我们管理State的容器:

    
    *   `Redux`是`JavaScript`的状态容器, 提供了可预测的状态管理
        
  • Redux除了和React一起使用之外, 它也可以和其他界面库一起来使用(比如Vue), 并且它非常小 (包括依赖在内,只有2kb)

Redux的核心理念-Store

  • Redux的核心理念非常简单

  • 比如我们有一个朋友列表需要管理:

    
    *   **如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的**
        
    *   比如页面的某处通过`products.push`的方式增加了一条数据
        
    *   比如另一个页面通过`products[0].age = 25`修改了一条数据
        
  • 整个应用程序错综复杂,当出现bug时,很难跟踪到底哪里发生的变化

Redux的核心理念-action

  • Redux要求我们通过action来更新state

    
    *   **所有数据的变化, 必须通过**`dispatch`来派发`action`来更新
        
    *   `action`是一个普通的`JavaScript`对象,用来描述这次更新的`type`和`content`
        
  • 比如下面就是几个更新friendsaction:

    
    *   强制使用`action`的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追踪、可预测的
        
    *   当然,目前我们的`action`是固定的对象,真实应用中,我们会通过函数来定义,返回一个`action`
        

Redux的核心理念-reducer

  • 但是如何将stateaction联系在一起呢? 答案就是reducer

    
    *   `reducer`是一个纯函数
        
    *   `reducer`做的事情就是将传入的`state`和`action`结合起来来生成一个新的`state`
        

Redux的三大原则

  • 单一数据源

    
    *   整个应用程序的`state`被存储在一颗`object tree`中, 并且这个`object tree`只存储在一个`store`
        
    *   `Redux`并没有强制让我们不能创建多个`Store`,但是那样做并不利于数据的维护
        
    *   单一的数据源可以让整个应用程序的`state`变得方便维护、追踪、修改
        
  • State是只读的

    
    *   唯一修改`state`的方法一定是触发`action`, 不要试图在其它的地方通过任何的方式来修改`state`
        
    *   这样就确保了`View`或网络请求都不能直接修改`state`,它们只能通过`action`来描述自己想要如何修改`state`
        
    *   这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心`race condition`(竟态)的问题
        
  • 使用纯函数来执行修改

    
    *   通过`reducer`将旧 `state` 和 `action` 联系在一起, 并且返回一个新的`state`
        
    *   随着应用程序的复杂度增加,我们可以将`reducer`拆分成多个小的`reducers`,分别操作不同`state tree`的一部分
        
    *   但是所有的`reducer`都应该是纯函数,不能产生任何的副作用
        

Redux的基本使用

Redux中核心的API

redux的安装: yarn add redux

  1. createStore 可以用来创建 store对象

  2. store.dispatch 用来派发 action, action会传递给 store

  3. reducer接收action,reducer计算出新的状态并返回它 (store负责调用reducer)

  4. store.getState 这个方法可以帮助获取 store 里边所有的数据内容

  5. store.subscribe方法可以让让我们订阅 store 的改变,只要 store 发生改变, store.subscribe 这个函数接收的这个回调函数就会被执行

小结

  1. 创建sotore, 决定 store 要保存什么状态

  2. 创建action, 用户在程序中实现什么操作

  3. 创建reducer, reducer 接收 action 并返回更新的状态

Redux的使用过程

  1. 创建一个对象, 作为我们要保存的状态

  2. 创建Store来存储这个state

    
    *   创建`store`时必须创建`reducer`
        
    *   我们可以通过 `store.getState` 来获取当前的`state`
        
  3. 通过action来修改state

    
    *   通过`dispatch`来派发`action`
        
    *   通常`action`中都会有`type`属性,也可以携带其他的数据
        
  4. 修改reducer中的处理代码

    
    *   这里一定要记住,`reducer`是一个**纯函数**,不能直接修改`state`
        
    *   后面会讲到直接修改`state`带来的问题
        
  5. 可以在派发action之前,监听store的变化

import { createStore } from 'redux'

// 1.初始化state
const initState = { counter: 0 }

// 2.reducer纯函数 不能修改传递的state
function reducer(state = initState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, counter: state.counter + 1 }
case 'ADD_COUNTER':
return { ...state, counter: state.counter + action.num }
default:
return state
}
}

// 3.store 参数放一个reducer
const store = createStore(reducer)

// 4.action
const action1 = { type: 'INCREMENT' }
const action2 = { type: 'ADD_COUNTER', num: 2 }

// 5.订阅store的修改
store.subscribe(() => {
console.log('state发生了改变: ', store.getState().counter)
})

// 6.派发action
store.dispatch(action1)
store.dispatch(action2)

redux原理图

Redux结构划分

  • 如果我们将所有的逻辑代码写到一起, 那么当redux变得复杂时代码就难以维护

  • 对代码进行拆分, 将store、reducer、action、constants拆分成一个个文件

拆分目录

Redux使用流程

redux

Redux官方流程图

redux-flow

React-Redux的使用

redux融入react代码(案例)

  • redux融入react代码案例:

    
    *   `Home`组件:其中会展示当前的`counter`值,并且有一个+1和+5的按钮
        
    *   `Profile`组件:其中会展示当前的`counter`值,并且有一个-1和-5的按钮
        

![](https://gitee.com/xmkm/cloudPic/raw/master/img/20201005132516.png)
  • 核心代码主要是两个:

    
    *   在 `componentDidMount`中订阅数据的变化,当数据发生变化时重新设置 `counter`
        
    *   在发生点击事件时,调用`store`的`dispatch`来派发对应的`action`
        

自定义connect函数

当我们多个组件使用redux时, 重复的代码太多了, 比如: 订阅state取消订阅state 或 派发action获取state

将重复的代码进行封装, 将不同的statedispatch作为参数进行传递

// connect.js
import React, { PureComponent } from 'react'
import { StoreContext } from './context'
/**

  • 1.调用该函数: 返回一个高阶组件
  •     传递需要依赖 state 和 dispatch 来使用state或通过dispatch来改变state
    *
  • 2.调用高阶组件:
  •     传递该组件需要依赖 store 的组件
    *
  • 3.主要作用:
  •     将重复的代码抽取到高阶组件中,并将该组件依赖的 state 和 dispatch
  •     通过调用mapStateToProps()或mapDispatchToProps()函数
  •     并将该组件依赖的state和dispatch供该组件使用,其他使用store的组件不必依赖store
    *
  • 4.connect.js: 优化依赖
  •     目的:但是上面的connect函数有一个很大的缺陷:依赖导入的 store
  •     优化:正确的做法是我们提供一个Provider,Provider来自于我们
  • Context,让用户将store传入到value中即可;
    */

export function connect(mapStateToProps, mapDispatchToProps) {
return function enhanceComponent(WrapperComponent) {
class EnhanceComponent extends PureComponent {
constructor(props, context) {
super(props, context)

// 组件依赖的state
this.state = {
storeState: mapStateToProps(context.getState()),
}
}

// 订阅数据发生变化,调用setState重新render
componentDidMount() {
this.unsubscribe = this.context.subscribe(() => {
this.setState({
centerStore: mapStateToProps(this.context.getState()),
})
})
}

// 组件被卸载取消订阅
componentWillUnmount() {
this.unsubscribe()
}

render() {
// 下面的WrapperComponent相当于 home 组件(就是你传递的组件)
// 你需要将该组件需要依赖的state和dispatch作为props进行传递
return (
<WrapperComponent
{...this.props}
{...mapStateToProps(this.context.getState())}
{...mapDispatchToProps(this.context.dispatch)}
/>
)
}
}
// 取出Provider提供的value
EnhanceComponent.contextType = StoreContext
return EnhanceComponent
}
}

// home.js
// 定义组件依赖的state和dispatch
const mapStateToProps = state => ({
counter: state.counter,
})

const mapDispatchToProps = dispatch => ({
increment() {
dispatch(increment())
},
addNumber(num) {
dispatch(addAction(num))
},
})
export default connect(mapStateToProps,mapDispatchToProps)(依赖redux的组件)

react-redux使用

  • 开始之前需要强调一下,reduxreact没有直接的关系,你完全可以在React, Angular, Ember, jQuery, or vanilla JavaScript中使用Redux

  • 尽管这样说,redux依然是和React或者Deku的库结合的更好,因为他们是通过state函数来描述界面的状态,Redux可以发射状态的更新,让他们作出相应。

  • 虽然我们之前已经实现了connectProvider这些帮助我们完成连接redux、react的辅助工具,但是实际上redux官方帮助我们提供了react-redux 的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效

  • 安装react-redux

    
    *   `yarn add react-redux`
        

// 1.index.js
import { Provider } from 'react-redux'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

// 2.home.js
import { connect } from 'react-redux'
// 定义需要依赖的state和dispatch (函数需要返回一个对象)
export default connect(mapStateToProps, mapDispatchToProps)(About)

react-redux源码导读

Redux-Middleware中间件

组件中异步操作

  • 在之前简单的案例中,redux中保存的counter是一个本地定义的数据

    
    *   我们可以直接通过同步的操作来`dispatch action`,`state`就会被立即更新。
        
    *   但是真实开发中,`redux`中保存的**很多数据可能来自服务器**,我们需要进行**异步的请求**,再将数据保存到`redux`中
        
  • 网络请求可以在class组件的componentDidMount中发送,所以我们可以有这样的结构:

redux中异步操作

  • 上面的代码有一个缺陷:

    
    *   我们必须将**网络请求**的异步代码放到组件的生命周期中来完成
        
  • 为什么将网络请求的异步代码放在redux中进行管理?

    
    *   后期代码量的增加,如果把网络请求异步函数放在组件的生命周期里,这个生命周期函数会变得越来越复杂,组件就会变得越来越大
        
    *   事实上,**网络请求到的数据也属于状态管理的一部分**,更好的一种方式应该是将其也交给`redux`来管理
        

  • 但是在redux中如何可以进行异步的操作呢?

    
    *   **使用中间件 (Middleware)**
        
    *   学习过`Express`或`Koa`框架的童鞋对中间件的概念一定不陌生
        
    *   在这类框架中,`Middleware`可以帮助我们在**请求和响应之间嵌入一些操作的代码**,比如cookie解析、日志记录、文件压缩等操作
        

理解中间件(重点)

  • redux也引入了中间件 (Middleware) 的概念:

    
    *   这个中间件的目的是在`dispatch`的`action`和最终达到的`reducer`之间,扩展一些自己的代码
        
    *   比如日志记录、**调用异步接口**、添加代码调试功能等等
        

redux-middlware

  • redux-thunk是如何做到让我们可以发送异步的请求呢?

    
    *   默认情况下的`dispatch(action)`,`action`需要是一个`JavaScript`的对象
        
    *   `redux-thunk`可以让`dispatch`(`action`函数), `action`**可以是一个函数**
        
    *   该函数会被调用, 并且会传给这个函数两个参数: 一个`dispatch`函数和`getState`函数
        
        *   `dispatch`函数用于我们之后再次派发`action`
            
        *   `getState`函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态
            

redux-thunk的使用

  1. 安装redux-thunk

    
    *   `yarn add redux-thunk`
        
  2. 在创建store时传入应用了middlewareenhance函数

    
    *   通过`applyMiddleware`来结合多个`Middleware`, 返回一个`enhancer`
        
    *   将`enhancer`作为第二个参数传入到`createStore`中
        
        ![image-20200821182447344](https://gitee.com/xmkm/cloudPic/raw/master/img/20201005132723.png)
        
  3. 定义返回一个函数的action

    
    *   注意:这里不是返回一个对象了,而是一个**函数**
        
    *   该函数在`dispatch`之后会被执行
        

![](https://gitee.com/xmkm/cloudPic/raw/master/img/20201005132817.png)

查看代码

redux-devtools

redux-devtools插件

  • 我们之前讲过,redux可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?

    
    *   `redux`官网为我们提供了`redux-devtools`的工具
        
    *   利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等
        
  • 使用步骤:

    
    *   第一步:在浏览器上安装[redux-devtools](https://chrome.google.com/webstore/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd/related?utm_source=chrome-ntp-icon)扩展插件
        
    *   第二步:在`redux`中集成`devtools`的中间件
        

// store.js 开启redux-devtools扩展
import { createStore, applyMiddleware, compose } from 'redux'

// composeEnhancers函数
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ trace: true }) || compose

// 通过applyMiddleware来结合多个Middleware,返回一个enhancer
const enhancer = applyMiddleware(thankMiddleware)

// 通过enhancer作为第二个参数传递createStore中
const store = createStore(reducer, composeEnhancers(enhancer))

export default store

redux-sage

generator

Generator函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。

// 生成器函数的定义
// 默认返回: Generator
function* foo() {
console.log('111')
yield 'hello'
console.log('222')
yield 'world'
console.log('333')
yield 'jane'
console.log('444')
}
// iterator: 迭代器
const result = foo()
console.log(result)

// 使用迭代器
// 调用next,就会消耗一次迭代器
const res1 = result.next()
console.log(res1) // {value: "hello", done: false}
const res2 = result.next()
console.log(res2) // {value: "world", done: false}
const res3 = result.next()
console.log(res3) // {value: "jane", done: false}
const res4 = result.next()
console.log(res4) // {value: undefined, done: true}

redux-sage流程

redux-saga的使用

  • redux-saga是另一个比较常用在redux发送异步请求的中间件,它的使用更加的灵活

  • Redux-saga的使用步骤如下

    
    1.  安装`redux-sage`: `yarn add redux-saga`
        
    2.  集成`redux-saga`中间件
        
        *   引入 `createSagaMiddleware` 后, 需要创建一个 `sagaMiddleware`
            
        *   然后通过 `applyMiddleware` 使用这个中间件,接着创建 `saga.js` 这个文件
            
        *   启动中间件的监听过程, 并且传入要监听的`saga`
            
    3.  `saga.js`文件的编写
        
        *   `takeEvery`:可以传入多个监听的`actionType`,每一个都可以被执行(对应有一个`takeLatest`,会取消前面的)
            
        *   `put`:在`saga`中派发`action`不再是通过`dispatch`, 而是通过`put`
            
        *   `all`:可以在`yield`的时候`put`多个`action`
            

// store.js
import createSageMiddleware from 'redux-saga'
import saga from './saga'
// 1.创建sageMiddleware中间件
const sagaMiddleware = createSageMiddleware()
// 2.应用一些中间件
const enhancer = applyMiddleware(sagaMiddleware)
const store = createStore(reducer,composeEnhancers(enhancer))

sagaMiddleware.run(saga)
export default store

// saga.js
import { takeEvery, put, all } from 'redux-saga/effects'
import { FETCH_HOME_DATA } from './constant'

function* fetchHomeData(action) {
const res = yield axios.get('http://123.207.32.32:8000/hom...
const banners = res.data.data.banner.list
const recommends = res.data.data.recommend.list
// dispatch action 提交action,redux-sage提供了put
yield all([
yield put(changeBannersAction(banners)),
yield put(changeRecommendAction(recommends)),
])
}

function* mySaga() {
// 参数一:要拦截的actionType
// 参数二:生成器函数
yield all([
takeEvery(FETCH_HOME_DATA, fetchHomeData),
])
}

export default mySaga

reducer代码拆分

Reducer代码拆分

  • 我们来看一下目前我们的reducer

    
    *   当前这个`reducer`既有处理`counter`的代码,又有处理`home`页面的数据
        
    *   后续`counter`相关的状态或`home`相关的状态会进一步变得更加复杂
        
    *   我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等
        
    *   如果将所有的状态都放到一个`reducer`中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护
        
  • 因此,我们可以对reducer进行拆分:

    
    *   我们先抽取一个对`counter`处理的`reducer`
        
    *   再抽取一个对`home`处理的`reducer`
        
    *   将它们合并起来
        

Reducer文件拆分

  • 目前我们已经将不同的状态处理拆分到不同的reducer中,我们来思考:

    
    *   虽然已经放到不同的函数了,但是这些函数的处理依然是在同一个文件中,代码非常的混乱
        
    *   另外关于`reducer`中用到的`constant`、`action`等我们也依然是在同一个文件中;
        

combineReducers函数

  • 目前我们合并的方式是通过每次调用reducer函数自己来返回一个新的对象

  • 事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并

import { combineReducers } from 'redux'
import { reducer as counterReducer } from './count'
import { reducer as homeReducer } from './home'

export const reducer = combineReducers({
counterInfo: counterReducer,
homeInfo: homeReducer,
})

  • 那么combineReducers是如何实现的呢?

    
    *   它将我们传递的`reducer`合并成一个对象, 最终返回一个`combination`函数
        
    *   在执行`combination`函数过程中, 会通过判断前后返回的数据是否相同来决定返回之前的`state`还是新的`state`
        

immutableJs

数据可变形的问题

  • React开发中,我们总是会强调数据的不可变性:

    
    *   无论是类组件中的`state`,还是`reduex`中管理的`state`
        
    *   事实上在整个`JavaScript`编码的过程中,数据的不可变性都是非常重要的
        
  • 数据的可变性引发的问题(案例):

    
    *   我们明明没有修改obj,只是修改了obj2,但是最终obj也被我们修改掉了
        
    *   原因非常简单,对象是引用类型,它们指向同一块内存空间,两个引用都可以任意修改
        

const obj1 = { name: 'jane', age: 18 }
const obj2 = obj1
obj1.name = 'kobe'
console.log(obj2.name) // kobe

  • 有没有办法解决上面的问题呢?

    
    *   进行对象的拷贝即可:`Object.assign`或扩展运算符
        
  • 这种对象的浅拷贝有没有问题呢?

    
    *   从代码的角度来说,没有问题,也解决了我们实际开发中一些潜在风险
        
    *   从性能的角度来说,有问题,如果对象过于庞大,这种拷贝的方式会带来性能问题以及内存浪费
        
  • 有人会说,开发中不都是这样做的吗?

    
    *   从来如此,便是对的吗?
        

认识ImmutableJS

  • 为了解决上面的问题,出现了Immutable对象的概念:

    
    *   `Immutable`对象的特点是只要修改了对象,就会返回一个新的对象,旧的对象不会发生改变;
        
  • 但是这样的方式就不会浪费内存了吗?

    
    *   为了节约内存,又出现了一个新的算法:`Persistent Data Structure`(持久化数据结构或一致性数据结构)
        
  • 当然,我们一听到持久化第一反应应该是数据被保存到本地或者数据库,但是这里并不是这个含义:

    
    *   用一种数据结构来保存数据
        
    *   当数据被修改时,会返回一个对象,但是**新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费**,如何做到这一点呢?结构共享:
        

  • 安装Immutable: yarn add immutable

ImmutableJS常见API

注意:我这里只是演示了一些API,更多的方式可以参考官网

作用:不会修改原有数据结构,返回一个修改后新的拷贝对象

  • JavaScripImutableJS直接的转换

    
    *   对象转换成`Immutable`对象:`Map`
        
    *   数组转换成`Immtable`数组:`List`
        
    *   深层转换:`fromJS`
        

const im = Immutable
// 对象转换成Immutable对象
const info = {name: 'kobe', age: 18}
const infoIM = im.Map()

// 数组转换成Immtable数组
const names = ["abc", "cba", "nba"]
const namesIM = im.List(names)

  • ImmutableJS的基本操作:

    
    *   修改数据:`set(property, newVal)`
        
        *   返回值: 修改后新的数据结构
            
    *   获取数据:`get(property/index)`
        
    *   获取深层`Immutable`对象数据(子属性也是`Immutable`对象): `getIn(['recommend', 'topBanners'])`
        

// set方法 不会修改infoIM原有数据结构,返回修改后新的数据结构
const newInfo2IM = infoIM.set('name', 'james')
const newNamesIM = namesIM.set(0, 'why')

// get方法
console.log(infoIM.get('name'))// -> kobe
console.log(namesIM.get(0))// -> abc

结合Redux管理数据

  1. ImmutableJS重构redux

    
    *   yarn add Immutable
        
    *   yarn add redux-immutable
        
  2. 使用redux-immutable中的combineReducers;

  3. 所有的reducer中的数据都转换成Immutable类型的数据

FAQ

React中的state如何管理

  • 目前项目中采用的state管理方案(参考即可):

    
    *   相关的组件内部可以维护的状态,在组件内部自己来维护
        
    *   只要是需要共享的状态,都交给redux来管理和维护
        
    *   从服务器请求的数据(包括请求的操作) ,交给redux来维护
查看原文

赞 2 收藏 2 评论 0

风不识途 发布了文章 · 4月6日

React中的样式和动画

CSS 概述(理解)

1.组件化天下的CSS

  • 前面说过,整个前端已经是组件化的天下:

    • CSS的设计就不是为组件化而生的
    • 所以在目前组件化的框架中都在需要一种合适的CSS解决方案
  • 在组件化中选择合适的CSS解决方案应该符合以下条件:

    • 可以编写局部csscss具备自己的局部作用域,不会随意污染其他组件内的元素
    • 可以编写动态的css:可以获取当前组件的一些状态,根据状态的变化生成不同的css样式
    • 支持所有的css特性:伪类、动画、媒体查询等
    • 编写起来简洁方便、最好符合一贯的css风格特点

2.浅谈React中的CSS

  • 事实上,css一直是React的痛点,也是被很多开发者吐槽、诟病的一个点
  • 在这一点上,Vue做的要确实要好于React

    • Vue通过在.vue文件中编写 <style><style> 标签来编写自己的样式
    • 通过是否添加 scoped 属性来决定编写的样式是全局有效还是局部有效
    • 通过 lang 属性来设置你喜欢的 less、sass等预处理器
    • 通过内联样式风格的方式来根据最新状态设置和改变css
  • VueCSS上虽然不能称之为完美,但是已经足够简洁、自然、方便了,至少统一的样式风格不会出现多个开发人员、多个项目采用不一样的样式风格。
  • 相比而言,React官方并没有给出在React中统一的样式风格:

    • 由此,从普通的css,到css modules,再到css in js,有几十种不同的解决方案,上百个不同的库
    • 大家一致在寻找最好的或者说最适合自己的CSS方案,但是到目前为止也没有统一的方案

React中的样式

1.内联样式

  • 内联样式是官方推荐的一种css样式的写法:

    • style 接受一个采用小驼峰命名属性的 JavaScript 对象,,而不是 CSS 字符串;
    • 并且可以引用state中的状态来设置相关的样式;
  • 内联样式的优点:

    1. 内联样式, 样式之间不会有冲突
    2. 可以动态获取当前state中的状态
  • 内联样式的缺点

    • 1.写法上都需要使用驼峰标识
    • 2.某些样式没有提示
    • 3.大量的样式, 代码混乱
    • 4.某些样式无法编写(比如伪类/伪元素)
  • 所以官方依然是希望内联合适和普通的css来结合编写

2.普通的css

  • 普通的css我们通常会编写到一个单独的文件,之后再进行引入
  • 这样的编写方式和普通的网页开发中编写方式是一致的:

    • 如果我们按照普通的网页标准去编写,那么也不会有太大的问题
    • 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响
    • 但是普通的css都属于全局的css,样式之间会相互影响
  • 这种编写方式最大的问题是样式之间会相互层叠掉

3.css modules

  • css modules并不是React特有的解决方案,而是所有使用了类似于webpack配置的环境下都可以使用的
  • 如果在其他项目中使用, 那么我们需要自己来进行配置,比如配置webpack.config.js中的modules: true
  • React的脚手架已经内置了css modules的配置:
  • .css/.less/.scss 等样式文件都修改成 .module.css/.module.less/.module.scss
  • 之后就可以引用并且进行使用了
  • css modules确实解决了局部作用域的问题,也是很多人喜欢在React中使用的一种方案

  • 但是这种方案也有自己的缺陷:

    • 引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的
    • 所有的className都必须使用{style.className} 的形式来编写
    • 不方便动态来修改某些样式,依然需要使用内联样式的方式
    • 如果你觉得上面的缺陷还算OK,那么你在开发中完全可以选择使用css modules来编写,并且也是在React中很受欢迎的一种方式

4.认识 CSS in JS

  • 实际上,官方文档也有提到过CSS in JS这种方案:

    • “CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义
    • 注意此功能并不是React的一部分,而是由第三方库提供 React 对样式如何定义并没有明确态度
  • 在传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离

    • 但是在前面的学习中,我们就提到过,React的思想中认为逻辑本身和UI是无法分离的,所以才会有了JSX的语法
    • 样式呢?样式也是属于UI的一部分
    • 事实上CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态
    • 所以React有被人称之为 All in JS
  • 当然,这种开发的方式也受到了很多的批评:

5.认识styled-components

  • 批评声音虽然有,但是在我们看来很多优秀的CSS-in-JS的库依然非常强大、方便:

    • CSS-in-JS通过JavaScript来为CSS赋予一些能力,包括类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用、动态修 改状态等等
    • 虽然CSS预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点
    • 所以,目前可以说CSS-in-JS是React编写CSS最为受欢迎的一种解决方案
  • 目前比较流行的CSS-in-JS的库有哪些呢?

    • styled-components
    • emotion
    • glamorous
  • 目前可以说styled-components依然是社区最流行的CSS-in-JS库,所以我们以styled-components的讲解为主
  • 安装yarn add styled-components

styleld-components

1.ES6标签模板字符串

  • 使用模板字符串来调用函数

  • 如果我们在调用的时候插入其他的变量:

    • 模板字符串被拆分了;
    • 第一个元素是数组,是被模块字符串拆分的字符串组合;
    • 后面的元素是一个个模块字符串传入的内容;
  • styled component中,就是通过这种方式来解析模块字符串,最终生成我们想要的样式的

2.styleld-components的安装和插件

  • styled-components的安装

    • yarn add styled
  • styled-components的插件(这里只演示vscode)

    • 安装: 搜索vscode-styled-components首个插件安装即可
    • 作用: 用于编写css的智能提示和语言高亮

3.styled的基本使用

  • styled-components的本质是通过函数的调用最终创建出一个组件

    • 这个组件会被自动添加上一个不重复的 class
    • styled-components会给该class添加相关的样式
  • 另外,它支持类似于CSS预处理器一样的样式嵌套:

    • 支持直接子代选择器后代选择器,并且直接编写样式
    • 可以通过&符号获取当前元素
    • 直接伪类选择器、伪元素

4.props、attrs属性

  • props可以穿透

  • props可以被传递给styled组件

    • 获取props需要通过 ${}传入一个箭头函数props会作为该函数的参数
    • 这种方式可以有效的解决动态样式的问题
  • 添加 attrs 属性
const JMInput = styled.input.attrs({
  bColor: 'red'// 定义固定属性值
})`
  background-color: purple;
  border-color: ${props => props.bColor};/* 取出attr中定义的bColor */
  color: ${props => props.color}; /* 取出proops传递的color */
`

<JMInput type="password" color={this.state.color} />

<details>
<summary>图示</summary>

</details>

4.styled高级特性

支持样式的继承

  • <details>
    <summary>图示</summary>

    </details>
const JMButton = styled.button`
  font-size: 20px;
  padding: 10px 12px;
  margin: 10px;
`
// 继承自 JMButton 的默认样式
const JMButtonPrimary = styled(JMButton)`
  color: lime;
  background-color: pink;
`

Theme主题共享

  • <details>
    <summary>图示</summary>

    </details>
import styled, { ThemeProvider } from 'styled-components'

// 2.使用提供的Theme
const JMButtonPrimary = styled(JMButton)`
  color: ${props => props.theme.color};
`

export default class App extends PureComponent {
  render() {
    return (
      // 1.提供Theme  
      <ThemeProvider id="app" theme={{ color: 'red', fontSize: '12px' }}>
        <JMButtonPrimary>Primary主要的按钮</JMButtonPrimary>
      </ThemeProvider>
    )
  }
}

classnames 库

React中添加class

  • React在JSX给了我们开发者足够多的灵活性,你可以像编写JavaScript代码一样,通过一些逻辑来决定是否添加某些class

  • 这个时候我们可以借助于一个第三方的库:classnames

    • 很明显,这是一个用于动态添加classnames的一个库

React-Transition-Group

react-transition-group介绍

  • 在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验
  • 当然,我们可以通过原生的CSS来实现这些过渡动画,但是React社区为我们提供了react-transition-group用来完成过渡动画。

    • React曾为开发者提供过动画插件react-addons-css-transition-group,后由社区维护,形成了现在的react-transition-group
    • 这个库可以帮助我们方便的实现组件的 入场 和 离场 动画,使用时需要进行额外的安装:
    • npm i react-transition-group -S
    • yarn add react-transition-group
  • react-transition-group本身非常小,不会为我们应用程序增加过多的负担。

react-transition-group主要组件

react-transition-group主要包含四个组件:

  • Transition

    • 该组件是一个和平台无关的组件(不一定要结合CSS);
    • 在前端开发中,我们一般是结合CSS来完成样式,所以比较常用的是CSSTransition
  • CSSTransition

    • 在前端开发中,通常使用CSSTransition来完成过渡动画效果
  • SwitchTransition

    • 两个组件显示和隐藏切换时,使用该组件
  • TransitionGroup

    • 将多个动画组件包裹在其中,一般用于列表中元素的动画

CSSTransition

  • CSSTransition是基于Transition组件构建的:

    • CSSTransition执行过程中,有三个状态:appear、enter、exit
  • 有三种状态, 需要定义对应的CSS样式:
开始状态执行动画执行结束
-appear-appear-active-appear-done
-enter-enter-active-enter-done
-exit-exit-active-enter-done
  • CSSTransition组件常见对应的属性:

    • in: 触发进入或者退出状态, 根据当前布尔值决定执行动画添加的class
    • intrue时,触发进入状态,会添加-enter、-enter-acitveclass开始执行动画,当动画执行结束后,会移除两个class, 并且添加-enter-doneclass
    • infalse时,触发退出状态,会添加-exit、-exit-activeclass开始执行动画,当动画执行结束后,会移除两个class,并 且添加-enter-doneclass
/* 进入 */
.card-enter,
.card-appear {
  opacity: 0;
  transform: scale(0.6);
}
/* 执行动画 */
.card-enter-active,
.card-appear-active {
  transition: opacity 300ms, transform 300ms;
  opacity: 1;
  transform: scale(1);
}

.card-done,
.card-appear {
}
/* 离开 */
.card-exit {
  opacity: 1;
  transform: scale(1);
}

.card-exit-active {
  opacity: 0;
  transform: scale(0.6);
  transition: opacity 300ms, transform 300ms;
}

.card-exit-done {
  opacity: 0;
}

CSSTransition组件常见属性

属性名描述
classNames决定了在编写css时,对应的class名称:比如card-enter...
timeout过渡动画的时间,是控制添加class或unmountOnExit的时间
appear是否在首次进入添加动画(需要和in同时为true)对应的calss
unmountOnExit退出后卸载组件,组件元素卸载掉

[其他属性可以参考官网来学习](https://reactcommunity.org/re...
)

CSSTransition组件常见的钩子函数

钩子函数描述
onEnter在进入动画之前被触发
onEntering在应用进入动画时被触发
onEntered在应用进入动画结束后被触发
onExit开始退出动画触发
onExiting正在退出退出时触发
onExited退出完成触发

<details>
<summary>下拉查看</summary>

</details>

SwitchTransition

  • SwitchTransition可以完成两个组件之间切换的炫酷动画:

    • 比如我们有一个按钮需要在on和off之间切换 7
    • 我们希望看到on先从左侧退出,off再从右侧进入
    • 这个动画在 vue 中被称之为 vue transition modes
    • react-transition-group中使用SwitchTransition来实现该动画
  • SwitchTransition中主要有一个属性:mode,有两个值

    • in-out:表示新组件先进入,旧组件再移除
    • out-in:表示就组件先移除,新组件再进入
  • 如何使用SwitchTransition呢?

    • SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹你想要切换的组件
    • SwitchTransition里面的CSSTransitionTransition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是key属性

TransitionGroup

  • 当我们有一组动画时,需要将这些CSSTransition放入到一个TransitionGroup中来完成动画:

最后

如果文章对你有帮助求个👍,最近一直在准备redux系列全面总结,这两天准备再整理整理发布,希望到时候各朋友捧个场

CSS 概述(理解)

1.组件化天下的CSS

  • 前面说过,整个前端已经是组件化的天下:

    • CSS的设计就不是为组件化而生的
    • 所以在目前组件化的框架中都在需要一种合适的CSS解决方案
  • 在组件化中选择合适的CSS解决方案应该符合以下条件:

    • 可以编写局部csscss具备自己的局部作用域,不会随意污染其他组件内的元素
    • 可以编写动态的css:可以获取当前组件的一些状态,根据状态的变化生成不同的css样式
    • 支持所有的css特性:伪类、动画、媒体查询等
    • 编写起来简洁方便、最好符合一贯的css风格特点

2.浅谈React中的CSS

  • 事实上,css一直是React的痛点,也是被很多开发者吐槽、诟病的一个点
  • 在这一点上,Vue做的要确实要好于React

    • Vue通过在.vue文件中编写 <style><style> 标签来编写自己的样式
    • 通过是否添加 scoped 属性来决定编写的样式是全局有效还是局部有效
    • 通过 lang 属性来设置你喜欢的less、sass等预处理器
    • 通过内联样式风格的方式来根据最新状态设置和改变css
  • VueCSS上虽然不能称之为完美,但是已经足够简洁、自然、方便了,至少统一的样式风格不会出现多个开发人员、多个项目采用不一样的样式风格。
  • 相比而言,React官方并没有给出在React中统一的样式风格:

    • 由此,从普通的css,到css modules,再到css in js,有几十种不同的解决方案,上百个不同的库
    • 大家一致在寻找最好的或者说最适合自己的CSS方案,但是到目前为止也没有统一的方案

React中的样式

1.内联样式

  • 内联样式是官方推荐的一种css样式的写法:

    • style接受一个采用小驼峰命名属性的 JavaScript 对象,,而不是 CSS 字符串;
    • 并且可以引用state中的状态来设置相关的样式;
  • 内联样式的优点:

    1. 内联样式, 样式之间不会有冲突
    2. 可以动态获取当前state中的状态
  • 内联样式的缺点

    • 1.写法上都需要使用驼峰标识
    • 2.某些样式没有提示
    • 3.大量的样式, 代码混乱
    • 4.某些样式无法编写(比如伪类/伪元素)
  • 所以官方依然是希望内联合适和普通的css来结合编写

2.普通的css

  • 普通的css我们通常会编写到一个单独的文件,之后再进行引入
  • 这样的编写方式和普通的网页开发中编写方式是一致的:

    • 如果我们按照普通的网页标准去编写,那么也不会有太大的问题
    • 但是组件化开发中我们总是希望组件是一个独立的模块,即便是样式也只是在自己内部生效,不会相互影响
    • 但是普通的css都属于全局的css,样式之间会相互影响
  • 这种编写方式最大的问题是样式之间会相互层叠掉

3.css modules

  • css modules并不是React特有的解决方案,而是所有使用了类似于webpack配置的环境下都可以使用的
  • 如果在其他项目中使用, 那么我们需要自己来进行配置,比如配置webpack.config.js中的modules: true
  • React的脚手架已经内置了css modules的配置:
  • .css/.less/.scss 等样式文件都修改成 .module.css/.module.less/.module.scss
  • 之后就可以引用并且进行使用了
  • css modules确实解决了局部作用域的问题,也是很多人喜欢在React中使用的一种方案

  • 但是这种方案也有自己的缺陷:

    • 引用的类名,不能使用连接符(.home-title),在JavaScript中是不识别的
    • 所有的className都必须使用{style.className} 的形式来编写
    • 不方便动态来修改某些样式,依然需要使用内联样式的方式
    • 如果你觉得上面的缺陷还算OK,那么你在开发中完全可以选择使用css modules来编写,并且也是在React中很受欢迎的一种方式

4.认识 CSS in JS

  • 实际上,官方文档也有提到过CSS in JS这种方案:

    • “CSS-in-JS” 是指一种模式,其中 CSS 由 JavaScript 生成而不是在外部文件中定义
    • 注意此功能并不是 _React_ 的一部分,而是由第三方库提供 React 对样式如何定义并没有明确态度
  • 在传统的前端开发中,我们通常会将结构(HTML)、样式(CSS)、逻辑(JavaScript)进行分离

    • 但是在前面的学习中,我们就提到过,React的思想中认为逻辑本身和UI是无法分离的,所以才会有了JSX的语法
    • 样式呢?样式也是属于UI的一部分
    • 事实上CSS-in-JS的模式就是一种将样式(CSS)也写入到JavaScript中的方式,并且可以方便的使用JavaScript的状态
    • 所以React有被人称之为 All in JS
  • 当然,这种开发的方式也受到了很多的批评:

5.认识styled-components

  • 批评声音虽然有,但是在我们看来很多优秀的CSS-in-JS的库依然非常强大、方便:

    • CSS-in-JS通过JavaScript来为CSS赋予一些能力,包括类似于CSS预处理器一样的样式嵌套、函数定义、逻辑复用、动态修 改状态等等
    • 虽然CSS预处理器也具备某些能力,但是获取动态状态依然是一个不好处理的点
    • 所以,目前可以说CSS-in-JS是React编写CSS最为受欢迎的一种解决方案
  • 目前比较流行的CSS-in-JS的库有哪些呢?

    • styled-components
    • emotion
    • glamorous
  • 目前可以说styled-components依然是社区最流行的CSS-in-JS库,所以我们以styled-components的讲解为主
  • 安装yarn add styled-components

styleld-components

1.ES6标签模板字符串

  • 使用模板字符串来调用函数

  • 如果我们在调用的时候插入其他的变量:

    • 模板字符串被拆分了;
    • 第一个元素是数组,是被模块字符串拆分的字符串组合;
    • 后面的元素是一个个模块字符串传入的内容;
  • styled component中,就是通过这种方式来解析模块字符串,最终生成我们想要的样式的

2.styleld-components的安装和插件

  • styled-components的安装

    • yarn add styled
  • styled-components的插件(这里只演示vscode)

    • 安装: 搜索vscode-styled-components首个插件安装即可
    • 作用: 用于编写css的智能提示和语言高亮

3.styled的基本使用

  • styled-components的本质是通过函数的调用最终创建出一个组件

    • 这个组件会被自动添加上一个不重复的 class
    • styled-components会给该class添加相关的样式
  • 另外,它支持类似于CSS预处理器一样的样式嵌套:

    • 支持直接子代选择器后代选择器,并且直接编写样式
    • 可以通过&符号获取当前元素
    • 直接伪类选择器、伪元素

4.props、attrs属性

  • props可以穿透

  • props可以被传递给styled组件

    • 获取props需要通过 ${}传入一个箭头函数props会作为该函数的参数
    • 这种方式可以有效的解决动态样式的问题
  • 添加 attrs 属性

const JMInput = styled.input.attrs({
bColor: 'red'// 定义固定属性值
})`
background-color: purple;
border-color: ${props => props.bColor};/ 取出attr中定义的bColor /
color: ${props => props.color}; / 取出proops传递的color /
`

<JMInput type="password" color={this.state.color} />

图示

4.styled高级特性

支持样式的继承

  • 图示

const JMButton = styled.button`
font-size: 20px;
padding: 10px 12px;
margin: 10px;
`
// 继承自 JMButton 的默认样式
const JMButtonPrimary = styled(JMButton)`
color: lime;
background-color: pink;
`

Theme主题共享

  • 图示

import styled, { ThemeProvider } from 'styled-components'

// 2.使用提供的Theme
const JMButtonPrimary = styled(JMButton)`
color: ${props => props.theme.color};
`

export default class App extends PureComponent {
render() {
return (
// 1.提供Theme
<ThemeProvider id="app" theme={{ color: 'red', fontSize: '12px' }}>
<JMButtonPrimary>Primary主要的按钮</JMButtonPrimary>
</ThemeProvider>
)
}
}

classnames 库

React中添加class

  • React在JSX给了我们开发者足够多的灵活性,你可以像编写JavaScript代码一样,通过一些逻辑来决定是否添加某些class

  • 这个时候我们可以借助于一个第三方的库:classnames

    • 很明显,这是一个用于动态添加classnames的一个库

React-Transition-Group

react-transition-group介绍

  • 在开发中,我们想要给一个组件的显示和消失添加某种过渡动画,可以很好的增加用户体验
  • 当然,我们可以通过原生的CSS来实现这些过渡动画,但是React社区为我们提供了react-transition-group用来完成过渡动画。

    • React曾为开发者提供过动画插件react-addons-css-transition-group,后由社区维护,形成了现在的react-transition-group
    • 这个库可以帮助我们方便的实现组件的 入场 和 离场 动画,使用时需要进行额外的安装:
    • npm i react-transition-group -S
    • yarn add react-transition-group
  • react-transition-group本身非常小,不会为我们应用程序增加过多的负担。

react-transition-group主要组件

react-transition-group主要包含四个组件:

  • Transition

    • 该组件是一个和平台无关的组件(不一定要结合CSS);
    • 在前端开发中,我们一般是结合CSS来完成样式,所以比较常用的是CSSTransition
  • CSSTransition

    • 在前端开发中,通常使用CSSTransition来完成过渡动画效果
  • SwitchTransition

    • 两个组件显示和隐藏切换时,使用该组件
  • TransitionGroup

    • 将多个动画组件包裹在其中,一般用于列表中元素的动画

CSSTransition

  • CSSTransition是基于Transition组件构建的:

    • CSSTransition执行过程中,有三个状态:appear、enter、exit
  • 有三种状态, 需要定义对应的CSS样式:

开始状态

执行动画

执行结束

-appear

-appear-active

-appear-done

-enter

-enter-active

-enter-done

-exit

-exit-active

-enter-done

  • CSSTransition组件常见对应的属性:

    • in: 触发进入或者退出状态, 根据当前布尔值决定执行动画添加的class
    • intrue时,触发进入状态,会添加-enter、-enter-acitveclass开始执行动画,当动画执行结束后,会移除两个class, 并且添加-enter-doneclass
    • infalse时,触发退出状态,会添加-exit、-exit-activeclass开始执行动画,当动画执行结束后,会移除两个class,并 且添加-enter-doneclass

/ 进入 /
.card-enter,
.card-appear {
opacity: 0;
transform: scale(0.6);
}
/ 执行动画 /
.card-enter-active,
.card-appear-active {
transition: opacity 300ms, transform 300ms;
opacity: 1;
transform: scale(1);
}

.card-done,
.card-appear {
}
/ 离开 /
.card-exit {
opacity: 1;
transform: scale(1);
}

.card-exit-active {
opacity: 0;
transform: scale(0.6);
transition: opacity 300ms, transform 300ms;
}

.card-exit-done {
opacity: 0;
}

CSSTransition组件常见属性

属性名

描述

classNames

决定了在编写css时,对应的class名称:比如card-enter...

timeout

过渡动画的时间,是控制添加class或unmountOnExit的时间

appear

是否在首次进入添加动画(需要和in同时为true)对应的calss

unmountOnExit

退出后卸载组件,组件元素卸载掉

其他属性可以参考官网来学习

CSSTransition组件常见的钩子函数

钩子函数

描述

onEnter

在进入动画之前被触发

onEntering

在应用进入动画时被触发

onEntered

在应用进入动画结束后被触发

onExit

开始退出动画触发

onExiting

正在退出退出时触发

onExited

退出完成触发

下拉查看

SwitchTransition

  • SwitchTransition可以完成两个组件之间切换的炫酷动画:

    • 比如我们有一个按钮需要在on和off之间切换 7
    • 我们希望看到on先从左侧退出,off再从右侧进入
    • 这个动画在 vue 中被称之为 vue transition modes
    • react-transition-group中使用SwitchTransition来实现该动画
  • SwitchTransition中主要有一个属性:mode,有两个值

    • in-out:表示新组件先进入,旧组件再移除
    • out-in:表示就组件先移除,新组件再进入
  • 如何使用SwitchTransition呢?

    • SwitchTransition组件里面要有CSSTransition或者Transition组件,不能直接包裹你想要切换的组件
    • SwitchTransition里面的CSSTransitionTransition组件不再像以前那样接受in属性来判断元素是何种状态,取而代之的是key属性

TransitionGroup

  • 当我们有一组动画时,需要将这些CSSTransition放入到一个TransitionGroup中来完成动画:

最后

如果文章对你有帮助求个👍,最近一直在准备redux系列全面总结,这两天准备再整理整理发布,希望到时候各朋友捧个场

查看原文

赞 0 收藏 0 评论 0

风不识途 发布了文章 · 4月6日

setState详解与React性能优化

setState的同步和异步

1.为什么使用setState

  • 开发中我们并不能直接通过修改state 的值来让界面发生更新

    • 因为我们修改了 state 之后, 希望 React 根据最新的 Stete 来重新渲染界面, 但是这种方式的修改 React 并不知道数据发生了变化
    • React 并没有实现类似于 Vue2 中的 Object.defineProperty 或者 Vue3 中的Proxy的方式来监听数据的变化
    • 我们必须通过 setState 来告知 React 数据已经发生了变化
  • 疑惑: 在组件中并没有实现 steState 方法, 为什么可以调用呢?

    • 原因很简单: setState方法是从 Component 继承过来的

2.setState异步更新

<details>
<summary>setState是异步更新的</summary>
<img data-original="https://gitee.com/xmkm/cloudPic/raw/master/img/20201001214531.png" />
</details>

  • 为什么setState设计为异步呢?

  • 简单的总结: setState设计为异步, 可以显著的提高性能

    • 如果每次调用 setState 都进行一次更新, 那么意味着 render 函数会被频繁的调用界面重新渲染, 这样的效率是很低的
    • 最好的方法是获取到多个更新, 之后进行批量更新
  • 如果同步更新了 state, 但还没有执行 render 函数, 那么stateprops不能保持同步

    • stateprops不能保持一致性, 会在开发中产生很多的问题

3.如何获取异步的结果

  • 如何获取 setState 异步更新state后的值?
  • 方式一: setState的回调

    • setState接收两个参数: 第二个参数是回调函数(callback), 这个回调函数会在state更新后执行

  • 方式二: componentDidUpdate生命周期函数

3.setState一定是异步的吗?

  • 其实可以分成两种情况
  • 在组件生命周期或React合成事件中, setState是异步的
  • setTimeou或原生DOM事件中, setState是同步的
  • 验证一: 在setTimeout中的更新 —> 同步更新

  • 验证二: 在原生DOM事件 —> 同步更新

4.源码分析

setState的合并

1.数据的合并

  • 通过setState去修改message,是不会对其他state 中的数据产生影响的

    • 源码中其实是有对 原对象新对象 进行合并的

2.多个state的合并

  • 当我们的多次调用setState, 只会生效最后一次state

  • setState合并时进行累加: 给setState传递函数, 使用前一次state中的值

React 更新机制

1.React 更新机制

  • 我们在前面已经学习React的渲染流程:

  • 那么 React 的更新流程呢?

2.React 更新流程

情况一: 对比不同类型的元素

  • 节点为不同的元素React会拆卸原有的树并且建立起新的树

    • 当一个元素从 <a> 变成 <img>,从 <Article> 变成 <Comment>,或从 <button> 变成 <div>都会触发一个完整的重建流程
    • 当卸载一棵树时,对应的DOM节点也会被销毁,组件实例将执行 componentWillUnmount() 方法
    • 当建立一棵新的树时,对应的 DOM 节点会被创建以及插入到 DOM 中,组件实例将执行 componentWillMount() 方法,紧接着 componentDidMount() 方法
  • 比如下面的代码更改:

    • React 会销毁 Counter 组件并且重新装载一个新的组件,而不会对Counter进行复用

情况二: 对比同一类型的元素

  • 当比对两个相同类型的 React 元素时,React 会保留 DOM 节点仅对比更新有改变的属性
  • 比如下面的代码更改:

    • 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 className 属性

  • 比如下面的代码更改:

    • 当更新 style 属性时,React 仅更新有所改变的属性。
    • 通过比对这两个元素,React 知道只需要修改 DOM 元素上的 color 样式,无需修改 fontWeight

  • 如果是同类型的组件元素:

    • 组件会保持不变,React会更新该组件的props,并且调用componentWillReceiveProps()componentWillUpdate() 方法
    • 下一步,调用 render() 方法,diff 算法将在之前的结果以及新的结果中进行递归

情况三: 对子节点进行递归

  • 在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation

    • 我们来看一下在最后插入一条数据的情况:👇

    • 前面两个比较是完全相同的,所以不会产生mutation
    • 最后一个比较,产生一个mutation,将其插入到新的DOM树中即可
  • 但是如果我们是在前面插入一条数据:

    • React会对每一个子元素产生一个mutation,而不是保持 <li>星际穿越</li><li>盗梦空间</li> 的不变
    • 这种低效的比较方式会带来一定的性能问题

React 性能优化

1.key的优化

  • 我们在前面遍历列表时,总是会提示一个警告,让我们加入一个key属性:

  • 方式一:在<font color='red'>最后</font>位置插入数据

    • 这种情况,有无key意义并不大
  • 方式二:在<font color='red'>前面</font>插入数据

    • 这种做法,在没有 key 的情况下,所有的<li>都需要进行修改
  • 在下面案例: 当子元素 (这里的li元素) 拥有 key

    • React 使用 key 来匹配原有树上的子元素以及最新树上的子元素
    • <details>
      <summary>下面这种场景下, key为 111 和 222 的元素仅仅进行位移,不需要进行任何的修改</summary>
      <img data-original="04-React性能优化与SetState详解.assets/image-20200815145010219.png" style="zoom:80%;" />
      </details>
    • key333 的元素插入到最前面的位置即可

key的注意事项:

  • key应该是唯一的
  • key不要使用随机数(随机数在下一次render时,会重新生成一个数字)
  • 使用index作为key,对性能是没有优化的

2.render函数被调用

  • 我们使用之前的一个嵌套案例:

    • 在App中,我们增加了一个计数器的代码
  • 当点击 +1 时,会重新调用 Apprender 函数

    • 而当 App 的 render函数被调用时,所有的子组件的 render 函数都会被重新调用

  • 那么,我们可以思考一下,在以后的开发中,我们只要是修改 了App中的数据,所有的子组件都需要重新render,进行 diff 算法,性能必然是很低的:

    • 事实上,很多的组件没有必须要重新render
    • 它们调用 render 应该有一个前提,就是依赖的数据(state、 props) 发生改变时再调用自己的render方法
  • 如何来控制 render 方法是否被调用呢?

    • 通过shouldComponentUpdate方法即可

3.shouldComponentUpdate

React给我们提供了一个生命周期方法 shouldComponentUpdate(很多时候,我们简称为SCU),这个方法接受参数,并且需要有返回值;主要作用是:控制当前类组件对象是否调用render方法
  • 该方法有两个参数:

    • 参数一: nextProps 修改之后, 最新的 porps 属性
    • 参数二: nextState 修改之后, 最新的 state 属性
  • 该方法返回值是一个 booolan 类型

    • 返回值为true, 那么就需要调用 render 方法
    • 返回值为false, 那么不需要调用 render 方法
  • 比如我们在App中增加一个message属性:

    • JSX中并没有依赖这个message, 那么它的改变不应该引起重新渲染
    • 但是通过setState修改 state 中的值, 所以最后 render 方法还是被重新调用了
// 决定当前类组件对象是否调用render方法
// 参数一: 最新的props
// 参数二: 最新的state
shouldComponentUpdate(nextProps, nextState) {
  // 默认是: return true
  // 不需要在页面上渲染则不调用render函数
  return false
}

4.PureComponent

  • 如果所有的类, 我们都需要手动来实现 shouldComponentUpdate, 那么会给我们开发者增加非常多的工作量

    • 我们设想一下在shouldComponentUpdate中的各种判断目的是什么?
    • props 或者 state 中数据是否发生了改变, 来决定shouldComponentUpdate返回 true false
  • 事实上 React 已经考虑到了这一点, 所以 React 已经默认帮我们实现好了, 如何实现呢?

    • <font color='red'>将 class 继承自 PureComponent</font>
    • 内部会进行浅层对比最新的 stateporps , 如果组件内没有依赖 porpsstate 将不会调用render
    • 解决的问题: 比如某些子组件没有依赖父组件的stateprops, 但却调用了render函数

5.shallowEqual方法

这个方法中,调用 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState),这个 shallowEqual 就是进行浅层比较:

6.高阶组件memo

  • 函数式组件如何解决render: 在没有依赖 stateprops 但却重新渲染 render 问题
  • 我们需要使用一个高阶组件memo

    • 我们将之前的Header、Banner、ProductList都通过 memo 函数进行一层包裹
    • Footer没有使用 memo 函数进行包裹;
    • 最终的效果是,当counter发生改变时,Header、Banner、ProductList的函数不会重新执行,而 Footer 的函数会被重新执行
import React, { PureComponent, memo } from 'react'

// MemoHeader: 没有依赖props,不会被重新调用render渲染
const MemoHeader = memo(function Header() {
  console.log('Header被调用')
  return <h2>我是Header组件</h2>
})

React知识点总结脑图

查看原文

赞 15 收藏 8 评论 1

风不识途 发布了文章 · 4月6日

React组件化开发

认识组件化

1.组件化思想

  • 当人们面对复杂问题的处理方式:

    • 将复杂的问题进行拆解, 拆分成很多个可以处理的小问题
    • 将其放在整体当中,你会发现大的问题也会迎刃而解

  • 其实上面的思想就是分而治之的思想:

    • 分而治之是软件工程的重要思想,是复杂系统开发和维护的基石
    • 而前端目前的模块化和组件化都是基于分而治之的思想

2.什么是组件化开发呢?

  • 组件化也是类似的思想:

    • 如果我们将一个页面中全部逻辑放在一起, 处理起来会变得非常复杂, 不利于后续管理及扩展
    • 但如果我们将一个页面拆分成一个个小的功能模块, 每个功能完成自己这部分独立功能, 那么整个页面的管理和维护变得非常容易

React组件化开发

  • 我们需要通过组件化的思想来思考整个应用程序:

    • 我们将一个完整的页面分成很多个组件
    • 每个组件都用于实现页面的一个功能块

3.React的组件化

  • 组件化是 React 的核心思想,前面我们封装的 App 本身就是一个组件

    • 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
    • 任何的应用都会被抽象成一颗组件树

  • 组件化思想的应用:

    • 尽可能的将页面拆分成一个个小的、可复用的组件
    • 这样让我们的代码更加方便组织和管理,并且扩展性也更强

4.React组件分类

  • React的组件相对于 Vue 更加的灵活和多样,按照不同的方式可以分成很多类组件:

    • 根据组件的定义方式,可以分为:函数组件(Functional Component)和类组件(Class Component)
    • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component)和有状态组件(Stateful Component)
    • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)
  • 这些概念有很多重叠,但是它们最主要是关注数据逻辑UI展示的分离:

    • 函数组件、无状态组件、展示型组件主要关注UI的展示
    • 类组件、有状态组件、容器型组件主要关注数据逻辑

React 创建组件

1.类组件

  • 类组件的定义由如下要求:

    • 组件的名称是大写字符开头 (无论类组件还是函数组件)
    • 类组件需要继承自: React.Component
    • 类组件必须实现 render 函数
  • 使用 class 定义一个组件:

    • constructor是可选的,我们通常在 constructor 中初始化一些数据
    • this.state中维护的就是我们组件内部的数据
    • render() 方法是 class 组件中唯一必须实现的方法

2.render函数的返回值

render函数被调用时, 它会检查 this.propsthis.state 的变化并返回以下类型之一
  • React元素

    • 通常通过 JSX 创建
    • 例如: <div/> 会被 React 渲染为 DOM 节点, \<MyComponent/>会被 React 渲染为自定义组件
    • 无论是 <div/> 还是 <MyComponent/> 均为 React 元素
  • 数组或 fragments: 使得 render 方法可以返回多个元素
  • Portals: 可以渲染子节点到不同的 DOM 子树中
  • 字符串或数值类型: 他们在 DOM 中会被渲染为文本节点
  • 布尔类型或null: 什么都不渲染

3.函数组件

函数组件是使用 function 来进行定义的函数, 只是这个函数会返回和类组件中 render 函数一样的内容
  • 函数组件的特点 (后面会讲hooks, 就不一样了)

    • 没有生命周期, 也会被更新并挂载, 但是没有生命周期函数
    • 没有 this (组件实例)
    • 没有内部状态 (state)

React 生命周期

1.认识生命周期

很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期

React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能

  • 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段:

    • 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程
    • 比如更新阶段(Update),组件状态发生变化,重新更新渲染的过程
    • 比如卸载过程(Unmount),组件从DOM树中被移除的过程
  • React内部为了告诉我们当前处于哪些阶段,会对组件内部实现的预定函数进行回调这些函数就是生命周期函数

    • 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调
    • 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调
    • 比如实现componentWillUnmount函数:组件卸载及销毁之前,就会回调
  • 我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数

    • (后面我们可以通过hooks来模拟一些生命周期的回调)

2.生命周期解析

  • 我们先来学习一下最基础、最常用的生命周期函数:

react生命周期

3.生命周期函数应用场景

Constructor

  • 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数
  • constructor 中通常只做两件事情:

    • 通过给 this.state 赋值对象来初始化内部 state
    • 为事件绑定this

componentDidMount

  • componentDidMount() 会在组件挂载后 ( 插入DOM树中 ) 立即调用
  • componentDidMount()中通常进行哪些操作?

    • 依赖于DOM的操作可以在这里进行
    • 在此处发送网络请求就最好的地方 (官方建议)
    • 可以在此处添加一些订阅 (会在componentWillUnmount取消订阅)

componentDidUpdate

  • componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行

    • 当组件更新后,可以对此 DOM 进行操作
    • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求
      (例如,当 props 未发生变化时,则不会执行网络请求)

componentWillUnmount

  • componentWillUnmount 会在组件卸载及销毁之前调用

    • 在此方法执行必要的清理操作
    • 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅

3.不常用的生命周期函数

除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数
  • getDerivedStateFromProps

    • state 的值在任何时候都依赖于 props 时使用
    • 该方法返回一个对象来更新state
  • getSnapshotBeforeUpdate

    • React 更新 DOM 之前回调的一个函数
    • 可以获取 DOM 更新前的一些信息 (比如说: 滚动位置)

React 组件的嵌套

1.认识组件的嵌套

  • 组件之间存在嵌套关系:

    • 在之前的案例中,我们只是创建了一个组件App
    • 如果我们一个应用程序将所有逻辑放在一个组件中, 那么这个组件就会变的非常臃肿和难以维护
    • 所以组件化的核心思想应该是对组件进行拆分, 拆分成一个个小的组件
    • 在将这些组件组合嵌套在一起, 最终形成我们的应用程序

module

  • 上面的嵌套逻辑如下

    • App组件是Header、Main、Footer组件的父组件
    • Main组件是Banner、ProductList组件的父组件

2.认识组件间的通信

  • 在开发过程中,我们会经常遇到需要组件之间相互进行通信

    • 比如 App 可能使用了多个Header组件,每个地方的Header展示的内容不同,那么我们就需要使用者传递给 Header 一些数据,让其进行展示
    • 又比如我们在 Main 组件中一次请求了 Banner 数据和 ProductList 数据,那么就需要传递给它们来进行展示
  • 总之,在一个 React 项目中,组件之间通信是非常重要的环节

React 父子组件间通信

1.父传子组件-props

父组件在展示子组件, 可能会传递一些数据给子组件

  • 父组件通过<font color='red'> 属性=值</font> 的形式给子组件传递数据
  • 子组件通过 <font color='red'>props</font> 参数获取父组件传递过来的数据

<details>
<summary>函数组件传递Props</summary>

</details>

2.属性验证-propTypes

  • 对于传递给子组件的数据, 有时候我们可能希望进行数据的类型校验, 特别是对于大型项目来说

    • 当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证
    • 但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-type库来进行参数验证
  • 使用 propTypes 来进行对 props 的验证, 首先: 导入 prop-types
  • 我们这里只做的 <font color='red'>props 类型的校验</font> 和 <font color='red'>props 的默认值</font> (更多的验证方式可以参考官网)

    • 比如某个 props 属性是必须传递的使用: propTypes.string.isRequired
  • 如果没有传递props, 我们希望有默认值使用: 类名.defaultProps = {}
// 1.导入prop-types来进行属性校验
import propTypes from 'prop-types'

// 2.类型校验: 使用prop-types来校验父组件传递的props类型是否符合预期
// 如果是类中可以: static propTypes = { 进行类型校验 }
ChildCpn.propTypes = {
  // name属性是必传的
  name: propTypes.string.isRequired,
  age: propTypes.number,
  height: propTypes.number,
}

// 3.默认值,父组件没有传递props时的默认值
ChildCpn.defaultProps = {
  name: 'hali',
  age: 21,
  height: 1.77,
}

3.子组件传递父组件-函数传递

  • 当子组件需要向父组件传递消息:

    • vue 中是通过自定义事件来完成的
    • React 中同样还是通过 props 传递消息
    • 只是让父组件给子组件传递一个回调函数(callback),在子组件调用这个函数

      • 注意 this 绑定问题
// 父组件
render() {
  return (
    <div>
      <h2>当前计数: {this.state.counter}</h2>
      {/* 子传父: 让子组件来调用父组件中的方法 */}
      {/* 操作步骤: 父组件传递一个回调函数,让子组件来进行调用 */}
      <Counter increment={e => this.increment()} />
    </div>
  )
}

increment() {
  this.setState({
    counter: this.state.counter + 1,
  })
}

// 子组件
class Counter extends Component {
  render() {// 调用父组件传递的函数
    return <button onClick={this.props.increment}>+</button>
  }
}

React 非父子组件通信

1.Context介绍

非父子组件数据的共享:

在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递

但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)

如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。

  • 如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

    • React提供了一个APIContext
    • Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
    • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言

2.Context使用

React.createContext

  • 作用: 创建一个需要全局共享对象的数据 (需要跨组件间通信的数据)
  • 介绍: 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider 中读取到当前的context
  • defaultValue是组件在顶层查找过程中没有找到对应的Provider那么就使用默认值

Context.Provider

  • 介绍:每个 Context 对象都会返回一个Provider React 组件,它允许消费组件订阅 context 的变化
  • 传递value:Provider 接收一个 value 属性,传递给消费组件
  • 一个 Provider 可以和多个消费组件有对应关系
  • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
  • Providervalue 值发生变化时,它内部的所有消费组件都会重新渲

Class.contextType

  • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象
  • 这能让你使用 this.context 来消费最近 Context 上的那个值
  • 你可以在任何生命周期中访问到它,包括 render 函数中

Context.Consumer

  • 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件完成订阅 context
  • 这里需要 函数作为子元素(function as child)这种做法
  • 这个函数接收当前的 context 值,返回一个 React 节点

3.组件通信图示

react组件通信

React 通信补充

1.React slot

slot翻译为插槽: 在 React 中没有插槽概念, 因为 React 太灵活了, 那在React如何实现插槽功能呢?

使用过 Vue 的朋友知道, 如果我们向一个组件中插入内容时, 可以在子组件中预留插槽, 内容由父组件决定

Children 实现插槽功能

父组件在使用子组件时, 将需要展示的内容用子组件包裹起来

子组件中通过: props.children[0] 来取得父组件要展示的内容

<details>
<summary>理解</summary>
<p>前面我们讲过:</p>
<li> render 函数中 return 的JSX 代码最终会转换成 React.createElement('tabName', 'config', children)</li>
<li>而第三个参数 children 就是: 咱们在元素中插入的子元素, 在React源码中将 ChildrenArr放到了props属性中的children中, 所以说子组件可以通过props.children来接收到父组件插入的子元素</li>
</details>

props 实现具名插槽

在插入插槽内容时, 你会发现不能实现像Vue中的指定插槽插入内容(具名插槽)

在React中可以通过 属性(props) 来指定你要插入的内容, 然后在子组件中使用 props 取出指定的JSX元素插入到指定位置

总结

  • Children使用场景: 当只有一个默认内容时, 直接插入到子元素即可
  • props指定slot使用场景: 当有多个插槽内容时, 使用 props 形式传递

2.属性展开

如果你已经有了一个 props 对象,你可以使用展开运算符 ... 来在 JSX 中传递整个 props 对象
function Profile(props) {
  return (
    <div>
      {/* 在 JSX 中传递整个 props 对象。以下两个组件是等价的 */}
      <ProfileHeader nickname={props.nickname} level={props.level}/> 
      <ProfileHeader {...props}/>
    </div>
  )
} 

events 事件总线

events

  • 前面通过Context主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,应该如何操作呢?

    • 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus) 来完成操作
    • 在React中,我们可以依赖一个使用较多的events 来完成对应的操作
  • 我们可以通过npm或者yarn来安装events:yarn add events
  • events常用的API:

    • 创建 EventEmitter 对象:eventBus对象;
    • 发出事件:eventBus.emit("事件名称", 参数列表);
    • 监听事件:eventBus.addListener("事件名称", 监听函数);
    • 移除事件:eventBus.removeListener("事件名称", 监听函数);
// 1.创建全局事件总线
const eventBus = new EventEmitter()

// 2.发射事件
emitHomeEvent() {
  eventBus.emit('sayHello', 'hello home', 123)
}

// 3.监听事件
componentDidMount() {
  eventBus.addListener('sayHello', this.handleSayHelloListener)
}

// 4.卸载事件
componentWillUnmount() {
  eventBus.removeListener('sayHello', this.handleSayHelloListener)
}

refs

1.如何使用ref

React的开发模式中,通常情况下不需要、也不建议直接操作DOM元素,但是某些特殊的情况,确实需要获取到DOM进行某些操作: 文本选择或媒体播放;触发强制动画;集成第三方 DOM 库;
  • 如何创建refs来获取对应的DOM呢?目前有三种方式:
  • 方式一:传入字符串

    • 使用时通过 this.refs.传入的字符串格式获取对应的元素
  • 方式二:传入一个对象

    • 对象是通过 React.createRef() 方式创建出来的;
    • 使用时获取到创建的对象其中有一个current属性就是对应的元素
  • 方式三:传入一个函数

    • 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存
    • 使用时,直接拿到之前保存的元素对象即可
import React, { PureComponent, createRef } from 'react'
// ...
constructor(props) {
  super(props)
  this.titleRef = createRef()
  this.titleEl = null
}
render() {
  return (
    <div>
      {/* <h2 ref=字符串/对象/函数方式> hello react</h2> */}
      <h2 ref="titleRef">hello react</h2>
      <h2 ref={this.titleRef}>hello react</h2>
      <h2 ref={arg => (this.titleEl = arg)}>hello react</h2>
      <button onClick={e => this.changeText()}>改变文本</button>
    </div>
  )
}

changeText() {
  // 1.通过refs来操作DOM,有三种方式
  // 方式一: 字符串
  this.refs.titleRef.innerHTML = 'hello jean'
  // 方式二: 对象
  this.titleRef.current.innerHTML = 'hello JavaScript'
  // 方式三: 函数
  this.titleEl.innerHTML = 'hello TypeScript'
}

2.ref的类型

  • ref 的值根据节点的类型而有所不同:
  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
  • ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
  • 你不能在函数组件上使用ref属性,因为他们没有实例
constructor(props) {
  this.counterRef = createRef()
}


render() {
  return (
    <div>
      <Counter ref={this.counterRef} />
      <button onClick={e => this.appIncrementCount()}>APP的按钮</button>
    </div>
  )
}

// 通过ref来获取类组件对象
appIncrementCount() {
  // 调用子组件方法
  this.counterRef.current.increment()
}
函数式组件是没有实例的,所以无法通过ref获取他们的实例:
但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref;

受控组件与非受控组件

1.认识受控组件

  • 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的state
  • 比如下面的HTML表单元素:

    • 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
    • 在React中,并没有禁止这个行为,它依然是有效的;
    • 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
    • 实现这种效果的标准方式是使用“受控组件”;

2.受控组件基本演练

  • 在 HTML 中,表单元素(如\<input>、 \<textarea> 和 \<select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新
  • 而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState() 来更新

    • 我们将两者结合起来,使Reactstate成为“唯一数据源”;
    • 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;
    • React 以这种方式控制取值的表单输入元素就叫做“受控组件”;
  • 由于在表单元素上设置了 value 属性,因此显示的值将始终为 this.state.value,这使得 React 的 state 成为唯一数据源。
  • 由于 handleUsernameChange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新

3.受控组件的其他演练

  • textarea标签

    • texteare标签和input比较相似:
  • select标签

    • select标签的使用也非常简单,只是它不需要通过selected属性来控制哪一个被选中,它可以匹配state的value来选中。
  • 处理多个输入

    • 多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:
      这里我们可以使用ES6的一个语法:计算属性名(Computed property names)

3.非受控组件(了解)

  • React推荐大多数情况下使用 受控组件 来处理表单数据:

    • 一个受控组件中,表单数据是由 React 组件来管理的;
    • 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;
  • 如果要使用非受控组件中的数据,那么我们需要使用 ref 来从DOM节点中获取表单数据

    • 我们来进行一个简单的演练:
    • 使用ref来获取input元素;
    • 在非受控组件中通常使用defaultValue来设置默认值
    • <details>
      <summary>了解即可</summary>

      </details>

高阶组件

1.认识高阶函数

  • 什么是高阶组件呢?它和高阶函数非常相似,我们可以先来回顾一下什么是: 高阶函数
  • 高阶函数的维基百科定义(至少满足以下条件之一):

    • 接受一个或多个函数作为输入
    • 输出一个函数
  • JavaScript中比较常见的filter、map、reduce都是高阶函数
  • 那么什么是高阶组件呢?

    • 高阶组件的英文是 Higher-OrderComponents,简称为 HOC
    • 官方的定义: 高阶组件是参数为组件, 返回为新的组件
  • 我们可以进行如下的解析:

    • 首先, 高阶组件本身不是一个组件, 而是一个函数
    • 其次, 这个函数的参数是一个组件, 返回值是一个组件

2.高阶组件的定义

  • 高阶组件调用类似于:

  • <details>
    <summary>高阶函数的编写过程类似于这样</summary>

    </details>
  • 组件的名称问题:

    • 在ES6中,类表达式中类名是可以省略的
    • 组件的名称都可以通过displayName来修改

高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式

高阶组件在一些React第三方库中非常常见:

  • 比如redux中的connect (后续会讲到)
  • 比如react-router中的withRouter (后续会讲到)

3.高阶组件应用场景

应用一: props的增强

  • 不修改原有代码的情况下,添加新的props

  • 利用高阶组件来共享Context

应用二: 渲染判断鉴权

  • 在开发中,我们可能遇到这样的场景:

    • 某些页面是必须用户登录成功才能进行进入;
    • 如果用户没有登录成功,那么直接跳转到登录页面;
  • 这个时候,我们就可以使用高阶组件来完成鉴权操作

应用三: 生命周期劫持

  • 我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:

4.高阶组件的意义

  • 我们会发现利用高阶组件可以针对某些React代码进行更加优雅的处理
  • 利用高阶组件可以完成多个组件中的共同功能
  • 其实早期的React有提供组件之间的一种复用方式是mixin目前已经不再建议使用:

    • Mixin 可能会相互依赖,相互耦合,不利于代码维护
    • 不同的Mixin中的方法可能会相互冲突
    • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
  • 当然,(高阶组件)HOC 也有自己的一些缺陷:

    • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难
    • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突
  • Hooks的出现,是开创性的,它解决了很多React之前的存在的问题
  • 比如this指向问题、比如hoc的嵌套复杂度问题等等

4.ref的转发(获取函数式组件DOM)

  • 在前面我们学习ref时讲过,ref不能应用于函数式组件

    • 因为函数式组件没有实例,所以不能获取到对应的组件对象
  • 但是,在开发中我们可能想要获取函数式组件中某个元素的DOM,这个时候我们应该如何操作呢?

    • 方式一:直接传入ref属性 (错误的做法)
    • 方式二:通过forwardRef高阶函数

Portals

Portals的使用

  • 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到idrootDOM元素上的)
  • Portal提供了一种将子节点渲染到存在于父组件之外的 DOM 节点

    • 参数一: child 是任何可渲染的 React 子元素, 例如一个元素,字符串或 fragment;
    • 参数二: container 是一个DOM 元素
    • ReactDOM.createPortal(child, container)

  • 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点
  • 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的

Fragment

Fragment的使用

  • 在之前的开发中,我们总是在一个组件中返回内容时包裹一个div根元素

  • 我们希望可以不渲染这样根元素div应该如何操作呢?

    • <details>
      <summary>使用Fragment</summary>

      </details>
    • Fragment允许你将子列表分组, 无需向 DOM 天添加额外节点
  • React还提供了Fragment短语法:

    • 短语法使用: <></>
    • <details>
      <summary>下拉查看</summary>

      </details>
    • 注意: 如果我们需要在Fragment中添加属性或者key, 那么就不能使用短语法

StrictMode

StrictMode 介绍

  • StrictMode 是一个用来突出显示应用程序中潜在问题的工具

    • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI
    • 它为其后代元素触发额外的检查和警告
    • 严格模式检查仅在开发模式下运行;它们不会影响生产构建
  • 可以为应用程序的任何部分启用严格模式:

    • 不会对 Header 和 Footer 组件运行严格模式检查
    • 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查

严格模式检查的是什么?

到底检测什么呢?

  1. 识别不安全的生命周期
  2. 使用过时的ref API
  3. 使用废弃的findDOMNode方法

    • 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了
  4. 检查意外的副作用

    • 这个组件的 constructor 会被调用两次
    • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用
    • 在生产环境中,是不会被调用两次的
  5. 检测过时的context API

    • 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context
    • 目前这种方式已经不推荐使用,大家可以自行学习了解一下它的用法

认识组件化

1.组件化思想

  • 当人们面对复杂问题的处理方式:

    • 将复杂的问题进行拆解, 拆分成很多个可以处理的小问题
    • 将其放在整体当中,你会发现大的问题也会迎刃而解

  • 其实上面的思想就是分而治之的思想:

    • 分而治之是软件工程的重要思想,是复杂系统开发和维护的基石
    • 而前端目前的模块化和组件化都是基于分而治之的思想

2.什么是组件化开发呢?

  • 组件化也是类似的思想:

    • 如果我们将一个页面中全部逻辑放在一起, 处理起来会变得非常复杂, 不利于后续管理及扩展
    • 但如果我们将一个页面拆分成一个个小的功能模块, 每个功能完成自己这部分独立功能, 那么整个页面的管理和维护变得非常容易

React组件化开发

  • 我们需要通过组件化的思想来思考整个应用程序:

    • 我们将一个完整的页面分成很多个组件
    • 每个组件都用于实现页面的一个功能块

3.React的组件化

  • 组件化是 React 的核心思想,前面我们封装的 App 本身就是一个组件

    • 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
    • 任何的应用都会被抽象成一颗组件树

  • 组件化思想的应用:

    • 尽可能的将页面拆分成一个个小的、可复用的组件
    • 这样让我们的代码更加方便组织和管理,并且扩展性也更强

4.React组件分类

  • React的组件相对于 Vue 更加的灵活和多样,按照不同的方式可以分成很多类组件:

    • 根据组件的定义方式,可以分为:函数组件(Functional Component)和类组件(Class Component)
    • 根据组件内部是否有状态需要维护,可以分成:无状态组件(Stateless Component)和有状态组件(Stateful Component)
    • 根据组件的不同职责,可以分成:展示型组件(Presentational Component)和容器型组件(Container Component)
  • 这些概念有很多重叠,但是它们最主要是关注数据逻辑UI展示的分离:

    • 函数组件、无状态组件、展示型组件主要关注UI的展示
    • 类组件、有状态组件、容器型组件主要关注数据逻辑

React 创建组件

1.类组件

  • 类组件的定义由如下要求:

    • 组件的名称是大写字符开头 (无论类组件还是函数组件)
    • 类组件需要继承自: React.Component
    • 类组件必须实现 render 函数
  • 使用 class 定义一个组件:

    • constructor是可选的,我们通常在 constructor 中初始化一些数据
    • this.state中维护的就是我们组件内部的数据
    • render() 方法是 class 组件中唯一必须实现的方法

2.render函数的返回值

render函数被调用时, 它会检查 this.propsthis.state 的变化并返回以下类型之一
  • React元素

    • 通常通过 JSX 创建
    • 例如:<div/> 会被 React 渲染为 DOM节点, <MyComponent/>会被 React 渲染为自定义组件
    • 无论是 <div/> 还是 <MyComponent/> 均为 React 元素
  • 数组或 fragments: 使得 render 方法可以返回多个元素
  • Portals: 可以渲染子节点到不同的 DOM 子树中
  • 字符串或数值类型: 他们在 DOM 中会被渲染为文本节点
  • 布尔类型或null: 什么都不渲染

3.函数组件

函数组件是使用 function 来进行定义的函数, 只是这个函数会返回和类组件中 render 函数一样的内容
  • 函数组件的特点 (后面会讲hooks, 就不一样了)

    • 没有生命周期, 也会被更新并挂载, 但是没有生命周期函数
    • 没有 this (组件实例)
    • 没有内部状态 (state)

React 生命周期

1.认识生命周期

很多的事物都有从创建到销毁的整个过程,这个过程称之为是生命周期

React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能

  • 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段:

    • 比如装载阶段(Mount),组件第一次在DOM树中被渲染的过程
    • 比如更新阶段(Update),组件状态发生变化,重新更新渲染的过程
    • 比如卸载过程(Unmount),组件从DOM树中被移除的过程
  • React内部为了告诉我们当前处于哪些阶段,会对组件内部实现的预定函数进行回调这些函数就是生命周期函数

    • 比如实现componentDidMount函数:组件已经挂载到DOM上时,就会回调
    • 比如实现componentDidUpdate函数:组件已经发生了更新时,就会回调
    • 比如实现componentWillUnmount函数:组件卸载及销毁之前,就会回调
  • 我们谈React生命周期时,主要谈的类的生命周期,因为函数式组件是没有生命周期函数

    • (后面我们可以通过hooks来模拟一些生命周期的回调)

2.生命周期解析

  • 我们先来学习一下最基础、最常用的生命周期函数:

react生命周期

3.生命周期函数应用场景

Constructor

  • 如果不初始化 state 或不进行方法绑定,则不需要为 React 组件实现构造函数
  • constructor 中通常只做两件事情:

    • 通过给 this.state 赋值对象来初始化内部 state
    • 为事件绑定this

componentDidMount

  • componentDidMount() 会在组件挂载后 ( 插入DOM树中 ) 立即调用
  • componentDidMount()中通常进行哪些操作?

    • 依赖于DOM的操作可以在这里进行
    • 在此处发送网络请求就最好的地方 (官方建议)
    • 可以在此处添加一些订阅 (会在componentWillUnmount取消订阅)

componentDidUpdate

  • componentDidUpdate() 会在更新后会被立即调用,首次渲染不会执行

    • 当组件更新后,可以对此 DOM 进行操作
    • 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求 (例如,当 props 未发生变化时,则不会执行网络请求)

componentWillUnmount

  • componentWillUnmount 会在组件卸载及销毁之前调用

    • 在此方法执行必要的清理操作
    • 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅

3.不常用的生命周期函数

除了上面介绍的生命周期函数之外,还有一些不常用的生命周期函数
  • getDerivedStateFromProps

    • state 的值在任何时候都依赖于 props 时使用
    • 该方法返回一个对象来更新state
  • getSnapshotBeforeUpdate

    • React 更新 DOM 之前回调的一个函数
    • 可以获取 DOM 更新前的一些信息 (比如说: 滚动位置)

React 组件的嵌套

1.认识组件的嵌套

  • 组件之间存在嵌套关系:

    • 在之前的案例中,我们只是创建了一个组件App
    • 如果我们一个应用程序将所有逻辑放在一个组件中, 那么这个组件就会变的非常臃肿和难以维护
    • 所以组件化的核心思想应该是对组件进行拆分, 拆分成一个个小的组件
    • 在将这些组件组合嵌套在一起, 最终形成我们的应用程序

module

  • 上面的嵌套逻辑如下

    • App组件是Header、Main、Footer组件的父组件
    • Main组件是Banner、ProductList组件的父组件

2.认识组件间的通信

  • 在开发过程中,我们会经常遇到需要组件之间相互进行通信

    • 比如 App 可能使用了多个Header组件,每个地方的Header展示的内容不同,那么我们就需要使用者传递给 Header 一些数据,让其进行展示
    • 又比如我们在 Main 组件中一次请求了 Banner 数据和 ProductList数据,那么就需要传递给它们来进行展示
  • 总之,在一个 React 项目中,组件之间通信是非常重要的环节

React 父子组件间通信

1.父传子组件-props

父组件在展示子组件, 可能会传递一些数据给子组件

  • 父组件通过 属性=值 的形式给子组件传递数据
  • 子组件通过 props 参数获取父组件传递过来的数据

函数组件传递Props

2.属性验证-propTypes

  • 对于传递给子组件的数据, 有时候我们可能希望进行数据的类型校验, 特别是对于大型项目来说

    • 当然,如果你项目中默认继承了Flow或者TypeScript,那么直接就可以进行类型验证
    • 但是,即使我们没有使用Flow或者TypeScript,也可以通过 prop-type库来进行参数验证
  • 使用 propTypes 来进行对 props 的验证, 首先: 导入 prop-types
  • 我们这里只做的 props 类型的校验 和 props 的默认值 (更多的验证方式可以参考官网)

    • 比如某个 props 属性是必须传递的使用: propTypes.string.isRequired
  • 如果没有传递props, 我们希望有默认值使用: 类名.defaultProps = {}

// 1.导入prop-types来进行属性校验
import propTypes from 'prop-types'

// 2.类型校验: 使用prop-types来校验父组件传递的props类型是否符合预期
// 如果是类中可以: static propTypes = { 进行类型校验 }
ChildCpn.propTypes = {
// name属性是必传的
name: propTypes.string.isRequired,
age: propTypes.number,
height: propTypes.number,
}

// 3.默认值,父组件没有传递props时的默认值
ChildCpn.defaultProps = {
name: 'hali',
age: 21,
height: 1.77,
}

3.子组件传递父组件-函数传递

  • 当子组件需要向父组件传递消息:

    • vue 中是通过自定义事件来完成的
    • React 中同样还是通过 props 传递消息
    • 只是让父组件给子组件传递一个回调函数(callback),在子组件调用这个函数

      • 注意 this 绑定问题

// 父组件
render() {
return (
<div>
<h2>当前计数: {this.state.counter}</h2>
{/ 子传父: 让子组件来调用父组件中的方法 /}
{/ 操作步骤: 父组件传递一个回调函数,让子组件来进行调用 /}
<Counter increment={e => this.increment()} />
</div>
)
}

increment() {
this.setState({
counter: this.state.counter + 1,
})
}

// 子组件
class Counter extends Component {
render() {// 调用父组件传递的函数
return <button onClick={this.props.increment}>+</button>
}
}

React 非父子组件通信

1.Context介绍

非父子组件数据的共享:

在开发中,比较常见的数据传递方式是通过props属性自上而下(由父到子)进行传递

但是对于有一些场景:比如一些数据需要在多个组件中进行共享(地区偏好、UI主题、用户登录状态、用户信息等)

如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。

  • 如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

    • React提供了一个APIContext
    • Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props
    • Context 设计目的是为了共享那些对于一个组件树而言是“全局”的数据,例如当前认证的用户、主题或首选语言

2.Context使用

React.createContext

  • 作用: 创建一个需要全局共享对象的数据 (需要跨组件间通信的数据)
  • 介绍: 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 Provider中读取到当前的context
  • defaultValue是组件在顶层查找过程中没有找到对应的Provider那么就使用默认值

Context.Provider

  • 介绍:每个 Context 对象都会返回一个Provider React 组件,它允许消费组件订阅 context 的变化
  • 传递value:Provider 接收一个 value 属性,传递给消费组件
  • 一个 Provider 可以和多个消费组件有对应关系
  • 多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据;
  • Providervalue 值发生变化时,它内部的所有消费组件都会重新渲

Class.contextType

  • 挂载在 class 上的 contextType 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象
  • 这能让你使用 this.context 来消费最近 Context 上的那个值
  • 你可以在任何生命周期中访问到它,包括 render 函数中

Context.Consumer

  • 这里,React 组件也可以订阅到 context 变更。这能让你在 函数式组件完成订阅 context
  • 这里需要 函数作为子元素(function as child)这种做法
  • 这个函数接收当前的 context 值,返回一个 React 节点

3.组件通信图示

react组件通信

React 通信补充

1.React slot

slot翻译为插槽: 在 React 中没有插槽概念, 因为 React 太灵活了, 那在React如何实现插槽功能呢?

使用过 Vue 的朋友知道, 如果我们向一个组件中插入内容时, 可以在子组件中预留插槽, 内容由父组件决定

Children 实现插槽功能

父组件在使用子组件时, 将需要展示的内容用子组件包裹起来

子组件中通过: props.children[0] 来取得父组件要展示的内容

理解

props 实现具名插槽

在插入插槽内容时, 你会发现不能实现像Vue中的指定插槽插入内容(具名插槽)

在React中可以通过 属性(props) 来指定你要插入的内容, 然后在子组件中使用 props 取出指定的JSX元素插入到指定位置

总结

  • Children使用场景: 当只有一个默认内容时, 直接插入到子元素即可
  • props指定slot使用场景: 当有多个插槽内容时, 使用 props 形式传递

2.属性展开

如果你已经有了一个 props 对象,你可以使用展开运算符 ... 来在 JSX 中传递整个 props 对象

function Profile(props) {
return (
<div>
{/ 在 JSX 中传递整个 props 对象。以下两个组件是等价的 /}
<ProfileHeader nickname={props.nickname} level={props.level}/>
<ProfileHeader {...props}/>
</div>
)
}

events 事件总线

events

  • 前面通过Context主要实现的是数据的共享,但是在开发中如果有跨组件之间的事件传递,应该如何操作呢?

    • 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus) 来完成操作
    • 在React中,我们可以依赖一个使用较多的events 来完成对应的操作
  • 我们可以通过npm或者yarn来安装events:yarn add events
  • events常用的API:

    • 创建 EventEmitter 对象:eventBus对象;
    • 发出事件:eventBus.emit("事件名称", 参数列表);
    • 监听事件:eventBus.addListener("事件名称", 监听函数);
    • 移除事件:eventBus.removeListener("事件名称", 监听函数);

// 1.创建全局事件总线
const eventBus = new EventEmitter()

// 2.发射事件
emitHomeEvent() {
eventBus.emit('sayHello', 'hello home', 123)
}

// 3.监听事件
componentDidMount() {
eventBus.addListener('sayHello', this.handleSayHelloListener)
}

// 4.卸载事件
componentWillUnmount() {
eventBus.removeListener('sayHello', this.handleSayHelloListener)
}

refs

1.如何使用ref

React的开发模式中,通常情况下不需要、也不建议直接操作DOM元素,但是某些特殊的情况,确实需要获取到DOM进行某些操作: 文本选择或媒体播放;触发强制动画;集成第三方 DOM 库;
  • 如何创建refs来获取对应的DOM呢?目前有三种方式:
  • 方式一:传入字符串

    • 使用时通过 this.refs.传入的字符串格式获取对应的元素
  • 方式二:传入一个对象

    • 对象是通过 React.createRef() 方式创建出来的;
    • 使用时获取到创建的对象其中有一个current属性就是对应的元素
  • 方式三:传入一个函数

    • 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存
    • 使用时,直接拿到之前保存的元素对象即可

import React, { PureComponent, createRef } from 'react'
// ...
constructor(props) {
super(props)
this.titleRef = createRef()
this.titleEl = null
}
render() {
return (
<div>
{/ <h2 ref=字符串/对象/函数方式> hello react</h2> /}
<h2 ref="titleRef">hello react</h2>
<h2 ref={this.titleRef}>hello react</h2>
<h2 ref={arg => (this.titleEl = arg)}>hello react</h2>
<button onClick={e => this.changeText()}>改变文本</button>
</div>
)
}

changeText() {
// 1.通过refs来操作DOM,有三种方式
// 方式一: 字符串
this.refs.titleRef.innerHTML = 'hello jean'
// 方式二: 对象
this.titleRef.current.innerHTML = 'hello JavaScript'
// 方式三: 函数
this.titleEl.innerHTML = 'hello TypeScript'
}

2.ref的类型

  • ref 的值根据节点的类型而有所不同:
  • ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性
  • ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性
  • 你不能在函数组件上使用ref属性,因为他们没有实例

constructor(props) {
this.counterRef = createRef()
}


render() {
return (
<div>
<Counter ref={this.counterRef} />
<button onClick={e => this.appIncrementCount()}>APP的按钮</button>
</div>
)
}

// 通过ref来获取类组件对象
appIncrementCount() {
// 调用子组件方法
this.counterRef.current.increment()
}

函数式组件是没有实例的,所以无法通过ref获取他们的实例: 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素; 这个时候我们可以通过 React.forwardRef ,后面我们也会学习 hooks 中如何使用ref;

受控组件与非受控组件

1.认识受控组件

  • 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的state
  • 比如下面的HTML表单元素:

    • 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
    • 在React中,并没有禁止这个行为,它依然是有效的;
    • 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
    • 实现这种效果的标准方式是使用“受控组件”;

2.受控组件基本演练

  • 在 HTML 中,表单元素(如<input>、 <textarea> 和 <select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新
  • 而在 React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState() 来更新

    • 我们将两者结合起来,使Reactstate成为“唯一数据源”;
    • 渲染表单的 React 组件还控制着用户输入过程中表单发生的操作;
    • React 以这种方式控制取值的表单输入元素就叫做“受控组件”;
  • 由于在表单元素上设置了 value 属性,因此显示的值将始终为this.state.value,这使得 React 的 state 成为唯一数据源。
  • 由于 handleUsernameChange 在每次按键时都会执行并更新 React 的 state,因此显示的值将随着用户输入而更新

3.受控组件的其他演练

  • textarea标签

    • texteare标签和input比较相似:
  • select标签

    • select标签的使用也非常简单,只是它不需要通过selected属性来控制哪一个被选中,它可以匹配state的value来选中。
  • 处理多个输入

    • 多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法: 这里我们可以使用ES6的一个语法:计算属性名(Computed property names)

3.非受控组件(了解)

  • React推荐大多数情况下使用 受控组件 来处理表单数据:

    • 一个受控组件中,表单数据是由 React 组件来管理的;
    • 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;
  • 如果要使用非受控组件中的数据,那么我们需要使用 ref 来从DOM节点中获取表单数据

    • 我们来进行一个简单的演练:
    • 使用ref来获取input元素;
    • 在非受控组件中通常使用defaultValue来设置默认值
    • 了解即可

高阶组件

1.认识高阶函数

  • 什么是高阶组件呢?它和高阶函数非常相似,我们可以先来回顾一下什么是: 高阶函数
  • 高阶函数的维基百科定义(至少满足以下条件之一):

    • 接受一个或多个函数作为输入
    • 输出一个函数
  • JavaScript中比较常见的filter、map、reduce都是高阶函数
  • 那么什么是高阶组件呢?

    • 高阶组件的英文是 Higher-OrderComponents,简称为 HOC
    • 官方的定义: 高阶组件是参数为组件, 返回为新的组件
  • 我们可以进行如下的解析:

    • 首先, 高阶组件本身不是一个组件, 而是一个函数
    • 其次, 这个函数的参数是一个组件, 返回值是一个组件

2.高阶组件的定义

  • 高阶组件调用类似于:

  • 高阶函数的编写过程类似于这样

  • 组件的名称问题:

    • 在ES6中,类表达式中类名是可以省略的
    • 组件的名称都可以通过displayName来修改

高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式

高阶组件在一些React第三方库中非常常见:

  • 比如redux中的connect (后续会讲到)
  • 比如react-router中的withRouter (后续会讲到)

3.高阶组件应用场景

应用一: props的增强

  • 不修改原有代码的情况下,添加新的props

  • 利用高阶组件来共享Context

应用二: 渲染判断鉴权

  • 在开发中,我们可能遇到这样的场景:

    • 某些页面是必须用户登录成功才能进行进入;
    • 如果用户没有登录成功,那么直接跳转到登录页面;
  • 这个时候,我们就可以使用高阶组件来完成鉴权操作

应用三: 生命周期劫持

  • 我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:

4.高阶组件的意义

  • 我们会发现利用高阶组件可以针对某些React代码进行更加优雅的处理
  • 利用高阶组件可以完成多个组件中的共同功能
  • 其实早期的React有提供组件之间的一种复用方式是mixin目前已经不再建议使用:

    • Mixin 可能会相互依赖,相互耦合,不利于代码维护
    • 不同的Mixin中的方法可能会相互冲突
    • Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
  • 当然,(高阶组件)HOC 也有自己的一些缺陷:

    • HOC需要在原组件上进行包裹或者嵌套,如果大量使用HOC,将会产生非常多的嵌套,这让调试变得非常困难
    • HOC可以劫持props,在不遵守约定的情况下也可能造成冲突
  • Hooks的出现,是开创性的,它解决了很多React之前的存在的问题
  • 比如this指向问题、比如hoc的嵌套复杂度问题等等

4.ref的转发(获取函数式组件DOM)

  • 在前面我们学习ref时讲过,ref不能应用于函数式组件

    • 因为函数式组件没有实例,所以不能获取到对应的组件对象
  • 但是,在开发中我们可能想要获取函数式组件中某个元素的DOM,这个时候我们应该如何操作呢?

    • 方式一:直接传入ref属性 (错误的做法)
    • 方式二:通过forwardRef高阶函数

Portals

Portals的使用

  • 某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的DOM元素中(默认都是挂载到idrootDOM元素上的)
  • Portal提供了一种将子节点渲染到存在于父组件之外的 DOM 节点

    • 参数一: child 是任何可渲染的 React 子元素, 例如一个元素,字符串或 fragment;
    • 参数二: container 是一个DOM 元素
    • ReactDOM.createPortal(child, container)

  • 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点
  • 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的

Fragment

Fragment的使用

  • 在之前的开发中,我们总是在一个组件中返回内容时包裹一个div根元素

  • 我们希望可以不渲染这样根元素div应该如何操作呢?

    • 使用Fragment

    • Fragment允许你将子列表分组, 无需向 DOM 天添加额外节点
  • React还提供了Fragment短语法:

    • 短语法使用: <></>
    • 下拉查看

    • 注意: 如果我们需要在Fragment中添加属性或者key, 那么就不能使用短语法

StrictMode

StrictMode 介绍

  • StrictMode 是一个用来突出显示应用程序中潜在问题的工具

    • 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI
    • 它为其后代元素触发额外的检查和警告
    • 严格模式检查仅在开发模式下运行;_它们不会影响生产构建_
  • 可以为应用程序的任何部分启用严格模式:

    • 不会对 Header 和 Footer 组件运行严格模式检查
    • 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查

严格模式检查的是什么?

到底检测什么呢?

  1. 识别不安全的生命周期
  2. 使用过时的ref API
  3. 使用废弃的findDOMNode方法

    • 在之前的React API中,可以通过findDOMNode来获取DOM,不过已经不推荐使用了
  4. 检查意外的副作用

    • 这个组件的 constructor 会被调用两次
    • 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用
    • 在生产环境中,是不会被调用两次的
  5. 检测过时的context API

    • 早期的Context是通过static属性声明Context对象属性,通过getChildContext返回Context对象等方式来使用Context
    • 目前这种方式已经不推荐使用,大家可以自行学习了解一下它的用法
查看原文

赞 0 收藏 0 评论 0

风不识途 发布了文章 · 4月6日

基于React全家桶开发「网易云音乐PC」项目实战(三)

前言

hello大家好,我是"风不识途",很长时间没有更新了~,很多朋友一直在催更新(其实没有),本人最近在实习实在有点忙=.=,有时间的话可以将面试&实习经历总结一下。好了回归正题,在前面我们已经完成了转嵌套路由的点击跳转切换了和轮播图,如果首次阅读本系列请点击,下面我们开始完成首页的主体内容+音乐播放器▶需要完成内容如下↓。

image-20210102105716704

项目预览和源码

  • 在线预览地址👉:www.wanguancs.top
  • 项目Gihub地址👉: Musci 163 如果觉得项目还不错的话 👏,就给个 star ⭐ 鼓励一下吧~

首页主体内容

1.热门推荐头部组件

Content主体布局

要完成的预览图如下↓

header组件封装「前言」

  • <details>
    <summary>查看需要封装的header组件</summary>

    </details>
  • 为什么封装:由于在当前页面下有多个类似的的头部(header)组件
  • 在当前页面中有的header组件是没有keywords关键字(也就是热门推荐后面的分类):
  • <details>

    <summary>点击查看多个<code>header</code>组件<b>差异</b></summary>


    </details>

"头部(header)组件"封装

  • 组件存放路径(参考):将header封装到src->components->ThemeHeader文件夹下
  • 组件需要依赖传递的props:

    • 组件的title(标题),必传;
    • keyword(关键字分类) (可以先写死数据),非必传;
  • 使用propTypes传递默认值
// theme-header-rcm.js
import propTypes from 'prop-types'
// ...
// 指定传递props
ThemeHeaderRmc.propTypes = {
  // title属性必填(左侧标题)
  title: propTypes.string.isRequired,
  // 关键字(非必传,左侧关键字)
  keywords: propTypes.array
}
// 指定默认值
ThemeHeaderRmc.defaultProps  = {
  keywords: []
}

实现效果

<details>
<summary>RecommendWrapper首页外层结构划分(参考)</summary>

</details>

2.热门推荐模块->发送网络请求

  • 这个时候我们就可以来发送网络请求,将请求下来的数据保存在redux的store
  • 热门推荐API接口↓:

一个组件需要发送网络的基本步骤

// 1.在网络请求对应文件中封装对应的函数
// 2.修改"当前"组件目录下store中的reducer (前提在actionTypes定义常量名)
// 3.在actionCreator文件中添加action发送网络请求
// 4.在组件中使用dispatch action 测试网络请求数据
// 5(可选).定义常量 constans 用于控制limit的数量(方便后期维护修改)
// 6.在组件中使用useSelector展示数据
详细步骤如下👇
  1. 网络请求接口的封装: src/service/recommend.js

    export function getHotRecommends(limit) {
      return request({
        url: "/personalized",
        params: {
          limit
        }
      })
  2. <details>
    <summary>修改redux</summary>

    </details>
  3. <details>
    <summary>添加actionCreator</summary>

    </details>
  4. 定义常量用于控制limit的数量, 好处是如果有一天想修改数量直接在常量文件中修改即可
  5. <details>
    <summary>在组件中使用useSelector展示数据</summary>

    </details>

3.歌曲封面(song cover)组件封装

  • 公共组件位置: src/components/song-cover/index.js

<details>
<summary>点击查看 歌曲封面(song cover)组件封装: </summary>

</details>

接口字段: 
    封面图片: picUrl
    播放数量: playCount
    封面名字: name
    封面底部文字: copywriter
song cover组件布局思路👇👇👇

  • 将保存在redux中热门推荐的八条数据,进行遍历(外层包裹div, 并使用flex布局, flex-wrap换行)

4.热门推荐组件完成效果

5.新碟上架

网络请求

  • 新碟上架接口:

  • 将请求的数据放到reudx里面

    1. 封装网络接口请求
    2. 修改redux,添加state,添加case语句
    3. 添加actionCreatorgetNewAlbumsAction

      • 用于发送网络请求
    4. 组件派发该action测试
    5. 添加actionCreatorchangeNewAlbumAction

      • 用于在网络请求中派发该action

    <details>
    <summary>查看保存的state</summary>

    </details>

新碟上架组件布局

  • 轮播图: 使用Carousel(走马灯)控件

数据截取逻辑代码

//1.数据方面:为了保证轮播图的每1个页有5条数据:
//  对数据进行截取: 在遍历第一页中0-5 第二页5-10数据
//  注意:slice(方法不包括截取目标number) 
// 2.布局方面:
//  对class name为npage及进行flex布局
<Carousel dots={false} ref={albumRef}>
  {[0, 1].map(item => {
    return (
      <div key={item} className="page">
        {newAlbums.slice(item * 5, (item + 1) * 5).map(cItem => {
          return (
            <div key={cItem.id} className="c-item">
              {cItem.name}
            </div>
          )
        })}
      </div>
    )
  })}
</Carousel>
  • 待完善效果🤔

AlbumCover组件封装

// AlbumCover组件要求(数据是不固定的):  宽高和bgp(背景图片横纵坐标)由调用者传递
// 因为在其他的页面中使用该组件的尺寸和bgp是不同的
<Carousel dots={false} ref={albumRef}>
...
<AlbumCover
  key={cItem.id}
  info={cItem}
  size={100}
  bgp="-570px"
>
  {cItem.name}
</AlbumCover>
...
</Carousel>
  • 完成效果😏

6.榜单

榜单说明

请求榜单数据

  • 榜单数据API: /top/list?idx=0已废弃,项目接口又重新改了一遍

    • 0: 云音乐飙升榜
    • 2: 云音乐新歌榜
    • 3: 云原创歌曲榜
  • 榜单数据API:

  • 注意事项↓
发送网络请求将请求的数据放到redux中state中 (详细步骤不在展开,和上面步骤一样)
注意: 根据不同 id 请求不同榜单
在派发action时可以使用switch根据不同的 id 派发不同的action

榜单组件(top-ranking)封装

  • 要完成的组件封装如下👇

  • 要实现效果

    • 刚开始: icons的父元素固定的width为0, hover后给固定的宽度
    • 鼠标划过这行item 让文字溢出隐藏显示...,并显示icons
    • 鼠标离开显示原本效果,隐藏icons,固定歌曲名字宽度即可

完成效果

7.主体右侧

暂时不做登录具体功能,先只做数据渲染。登录模块布局比较简单就略过了;

入驻歌手(settle-singer)

  • 入驻歌手API:

    • /artist/list?limit=5&cat=5001
    • 示例:http://123.57.176.198:3000/artist/list?limit=5&cat=5001
  • 返回的JSON如下
{
    picUrl(pin):"http://p4.music.126.net/LCWqYYKoCEZKuAC3S3lIeg==/109951165034938865.jpg"
    followed(pin):false
    briefDesc(pin):""
    name(pin):"薛之谦"
    id(pin):5781
    alias(pin):
    musicSize(pin):275
    accountId(pin):97137413
    picId_str(pin):"109951165034938865"
    img1v1Id_str(pin):"109951165034950656"
}

热门主播

  • hot-artist

    • 接口没找到,那就先写死吧,在:src/common/local-data.js 文件已经写好了
    • 返回的JSON如下
{
  picUrl: 'http://p1.music.126.net/H3QxWdf0eUiwmhJvA4vrMQ==/1407374893913311.jpg',
  name: '陈立',
  position: '心理学家、美食家陈立教授',
  url: '/user/home?id=278438485',
},

音乐播放

音乐播放组件(app-play-bar)

播放器组件说明:我们在网易云音乐官网切换页面时,会发现音乐播放一直是固定在下面的,和路由切换没有关系

  • 组件存放位置: 所以我们将app-play-bar组件封装到 src/psges 文件夹📂中

image-20210103144821061

布局参考

PlayBar组件布局采用固定定位: 
 PlayerWrapper↓
     内容(content)分了三个部分:↓
         Control(左侧)
             三个按钮.添加背景图,外层采用flex布局
         PlayIInfo(中间)
             两个部分(上、下),下面滑动条采用andt组件库Slider组件,找到类名覆盖样式即可
         Opertaor(右侧)
             两部分,外层采用flex布局

Slider组件(进度条)样式覆盖

Slider组件样式更改(覆盖)
  1.外层包裹背景图和宽高样式..
  2.mairign边距为0
  3.设置背景颜色为透明
  4.设置鼠标滑动时的背景图
  5.设置圆点的样式覆盖

完成效果

  • 图片和一些动态获取的数据暂时先写死

歌曲播放数据请求

  • 先固定播放一首歌曲
  • 歌曲API接口:/song/detail?ids=167876

  • 请求下来的数据放在哪呢?

    • 请求下的由于是歌曲信息所以就放在player文件夹下store文件夹📂下的store中进行保存
    • rducer中使用immutable管理state
    • 在项目根目录,导入player文件夹下的reducer,进行combine(合并)
step1
    添加player中reducer默认state的 currentSong: {}
    发送网络请求: player.js
    将数据保存在reducer中

step2
    在plaer组件使用redux中请求来的数据: 
    useSelector
    更改player(播放器)组件的固定数据
        currentSong.al.picUrl   图片
        currentSong.name         歌曲名字
        currentSong.ar[0].name  作者
        currentSong.dt  (歌曲总时长,格式化)
    导入时间格式化工具(转换时间格式)
        formatDate(duration, "mm:ss")

播放音乐功能

音乐播放逻辑

下面我们开始做音乐播放功能
  step1 
      添加 audio 标签
    点击 ▶播放按钮 监听click事件,添加src属性↓
  step2 音乐播放
      使用useRef,获取audio的dom元素
      封装: 歌曲播放`API`接口,id作为参数
      之后点击 播放▶按钮 动态设置scr属性,调用play方法开始播放
  step3 歌曲时间显示
      创建组件局部状态: currentTime 用于更改当前播放时间
      audio元素有一个OnTimeUpdate事件,当歌曲事件发生变化就会被回调
          事件参数: e.currentTime属性(用于获取当前播放时间)
          对秒数->对时间进行格式化->formatDate(currentTime, 'mm:ss')
  step4 进度条滚动
    控制andt的Slider组件的value值: 当前播放的进度=当前时间/总时长*100

拖动滑块逻辑

下面我们开始做拖动滑块,播放对应的进度歌曲
  需求:
    Slider组件滑动时: 当前时间会发生改变
    Slider组件抬起时: 当前歌曲进度发生改变
  step1
    1.Slider组件提供了2个api: 
      (1)onChange: 当滑块被拉动时触发,函数的参数是拖动的value
      (2)onAfterChange: 当滑抬起时触发,函数的参数是抬起时的value
    2.progress状态用于保存当前Slider进度,当歌曲播放触发更改进度
      (1)onChange事件参数的value为当前滑动的进度值: 更改setProgress进度
      (2)当我们播放音乐时,拖动滑块时,会有bug(进度条被拉到前面了)
        (2.1)原因: 这是因为我们在onChange事件中,和timeUpdate事件中都在更改"progress"进度值,在歌曲播放时触发TimeUpdate事件中也更改了"progress"进度
        (2.2)解决: 组件中创建一个用于标识是否正在改变的state,如果不是在change那么就在歌曲播放事件中更改progress进度,最后在抬起事件中再将标识change变量更改为false
  step2 
    1.设置歌曲的src属性,放到uesEffect当中依赖于currentSong
    2.播放暂停功能,背景图切换

FAQ

progress进度: 1-100
currentTime: 要的是毫秒数
audioRef.current.currentTime: 要的是总秒数

歌曲播放具体功能完善

点击页面上的一首歌播放音乐

  • 音乐播放逻辑

  • 1.在reducer中添加需要的字段

    • currentSongIndex 记录当前播放音乐的索引
    • playList 播放列表
  • 2.请求歌曲详情逻辑

    • <details>
      <summary>下拉查看</summary>

      </details>
  • 3.当一个组件内部的actionCreator被其他组件使用时(参考)

    // 将添加歌曲action导出
    export {
      reducer,
      actionCreator
    }
  1. 完成效果

    • <details>
      <summary>下拉查看</summary>

      </details>

单曲循环或顺序播放或列表循环

  • 当前歌曲播放完毕后,决定下一首是顺序播放还是单曲循环等等
第一种思路: 创建一个播放列表数组,决定下一首播放什么音乐
  如果是顺序播放,直接把源数组拷贝,如果是随机播放,将顺序打乱
第二种思路: 决定下一首播放什么音乐,让当前currentSongIndex + 1,
  设计顺序的数据结构(sequence)
    0 顺序播放 
    1 随机播放
    2 单曲循环 
  背景图切换
  歌曲列表显示个数

点击按钮播放上一首或下一首

  • 点击按钮: 播放上一首或下一首音乐
  • 两个按钮监听点击事件: 都使用同一个函数,传递不同的tab(标记),处理不同的逻辑

    • 因为需要派发action,所以放到actionCreator里编写
    • 单曲循环也是切换到下一首的, 所以它们的逻辑一样
  • 切换歌曲的实现思路(参考):

    • 根据playSequence决定是顺序播放还是随机播放
    • 根据播放顺序选择下一首音乐

      • 随机播放 ...
      • 顺序播放 ...
    • 获取需要播放的音乐
    • 更改当前播放的索引
    • 更改当前播放的音乐

next-music

决定下一首音乐播放的顺序

  • 给音频元素监听: onEnded事件(歌曲播放完后触发)
  • 当前歌曲播放完后只有两种情况:

    • 第一种情况: 单曲循环

      • 设置当前播放时间为0:audioRef.current.currentTime = 0
    • 第二种情况: 切换下一首音乐(根据playSequence决定是随机播放还是顺序播放)

其他细节补充

  1. 点击切换歌曲顺序图标按钮后: 切换对应图标 0顺序播放1随机播放2单曲循环
  2. setIsPlaying修改状态时为什么要添加随机值?

    • 如果当前是播放状态: 添加下一首音乐时, 还是播放状态, 设置的值还是true
    • 如果这一次的值和上一次的值时相同的, 就不会执行依赖于isPlayinguseEffect回调
    • 所以每次更新isPlaying时, 需要显示的更新isPlaying
  3. 点击切换顺序按钮后, 悬浮当前播放的顺序文本提示, 单曲循环还是随机播放等等

    • Tooltip文字提示组件, 鼠标经过显示气泡, 内容是单曲循环还是随机播放等等

歌词显示

对请求下来的歌词分析

歌词数据API接口

[00:00.000] 作曲 : 许嵩 -> {time: 毫秒, content: "歌词内容"}
[00:01.000] 作词 : 许嵩
[00:22.240]天空好想下雨
[00:24.380]我好想住你隔壁
[00:26.810]傻站在你家楼下
[00:29.500]抬起头数乌云
[00:31.160]如果场景里出现一架钢琴
[00:33.640]我会唱歌给你听\n[00:35.900]哪怕好多盆水往下淋\n[00:41.060]夏天快要过去\n[00:43.340]请你少买冰淇淋\n[00:45.680]天凉就别穿短裙\n[00:47.830]别再那么淘气\n[00:50.060]如果有时不那么开心\n[00:52.470]...
  • 咱们会发现请求下来的歌词是有规律的: \n 为换行

请求歌词数据的时机

  • 什么时候请求歌词数据:

    • 组件被渲染完成切换歌曲时
    • 请求当前播放音乐的歌词或者点击页面上的歌曲

封装歌词解析工具函数(逻辑思路)

1.使用slice切割字符串
2.创建正则解析规则: 将"[00:26.810]傻站在你家楼下..."  解析成->  00:26.810 
3.注意: 最后一行也有\n在遍历时加个判断,如果为不为空执行下面操作
4.获取正则解析的3个时间转换为毫秒, 分钟:秒数:00*10 000就是毫秒(加个判断*1转换为number类型)
5.将获取的3个毫秒数相加: 当前歌曲播放的总时长(毫秒)
6.获取当前播放的歌词: replace方法 (完成效果如下👇)
  [
    0: {totalTime: 0, content: "作曲 : 许嵩"}
    1: {totalTime: 1000, content: "作词 : 许嵩"}
    2: {totalTime: 22240, content: "天空好想下雨"}
    3: {totalTime: 24380, content: "我好想住你隔壁"}
  ]
7.将数据保存到redux当中
8.注意: 在切换歌曲时有可能会报 Cannot read property '1' of null , 这是因为从result读取属性时没找到, 加一个if判断,如果result没有值的话, 执行关键字continue跳转到判断条件重新执行

歌词解析代码

const parseExp = /\[([0-9]{2}):([0-9]{2})\.([0-9]{2,3})\]/
export function parseLyric(lyrics) {
  if(!lyrics) return
  const lineStrings = lyrics.split('\n')
  const lyricList = []
  for (const line of lineStrings) {
    if (line) {
      const result = parseExp.exec(line)
      if(!result) continue
      const time1 = result[1] * 60 * 1000
      const time2 = result[2] * 1000
      const time3 = result[3].length > 2 ? result[3] * 1 : result[3] * 1000
      // 当前歌曲播放的总时长(毫秒)
      const totalTime = time1 + time2 + time3
      const content = line.replace(parseExp, '').trim()
      const lineObj = {totalTime, content};
      lyricList.push(lineObj)
    }
  }
  return lyricList
}

完成效果如下图

lyric-result

拿到当前播放的歌词

在歌曲播放的时候会有一个 currentTime 变量, 
拿到这个变量和当前播放的歌词中的 time 进行比对,
小于歌词中time,之后获取索引值-1(要展示的歌词是前一句)

在timeUpdate事件中: 获取当前播放的歌词
    1.获取歌词的索引
       2.遍历歌词数组.Length
       3.判断当前播放时间小于歌词播放时间,获取当前循环的索引
       注意: 注意时间问题,转换为毫秒(current)进行对比判断
       4.从歌词数组取出索引拿到当前播放的歌词
       优化: 对for循环进行优化
       5.对歌词进行管理: 由于歌词在多处使用,使用redux进行管理
         优化: dispatch action 过于频繁. 
         解决: index如果没有变不需要dispatch(index和currentLyricIndex对比)

实现效果

lyric-show

展示歌词

  • 使用Antd Message组件
  • 修改内置样式
  • 实现效果

show-lyric

  • 到现在我们已经完成「网易云音乐PC」首页基本功能,相信你对React全家桶已经是比较熟练了,接下来想往哪方面扩展可以自行补充完善功能(不过相信能看到这里的小伙伴估计没几个)😂;
  • 如果文章中有哪部分不明白的或写的不好或是有什么建议欢迎大家提出🤗,希望大家共同进步;

最后

  • 非常感谢王红元老师的React核心技术实战让我学习到很多 React 的知识。
  • 非常感谢后台提供者Binaryify,接口很稳定,文档很完善
查看原文

赞 0 收藏 0 评论 0

风不识途 发布了文章 · 2月24日

React组件化开发

\#\# 认识组件化

\#\#\# 1.组件化思想

- 当人们面对复杂问题的处理方式:
  - 将复杂的**问题进行拆解**, 拆分成很多个可以处理的小问题
  - 再**将其放在整体当中**,你会发现大的问题也会迎刃而解

- 其实上面的思想就是分而治之的思想:
  - 分而治之是软件工程的重要思想,是复杂系统开发和维护的基石
  - 而前端目前的模块化和组件化都是基于分而治之的思想





\#\#\# 2.什么是组件化开发呢?

- 组件化也是类似的思想:
  - 如果我们将一个页面中全部逻辑放在一起, 处理起来会变得非常复杂, 不利于后续管理及扩展
  - 但如果我们**将一个页面拆分成一个个小的功能模块**, 每个功能**完成自己这部分独立功能**, 那么整个页面的管理和维护变得非常容易

React组件化开发

- 我们需要通过组件化的思想来思考整个应用程序:
  - 我们将一个完整的页面分成很多个组件
  - 每个组件都用于实现页面的一个功能块





\#\#\# 3.React的组件化

- 组件化是 React 的核心思想,前面我们封装的 App 本身就是一个组件
  - 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
  - 任何的应用都会被抽象成一颗组件树

- 组件化思想的应用:
  - 尽可能的将页面拆分成一个个小的、可复用的组件
  - 这样让我们的代码更加方便组织和管理,并且扩展性也更强





\#\#\# 4.React组件分类

- **React的组件**相对于 Vue **更加的灵活和多样**,按照不同的方式可以分成很多类组件:
  - 根据组件的定义方式,可以分为:**函数组件**(Functional Component)和**类组件**(Class Component)
  - 根据组件内部是否有状态需要维护,可以分成:**无状态组件**(Stateless Component)和**有状态组件**(Stateful Component)
  - 根据组件的不同职责,可以分成:**展示型组件**(Presentational Component)和**容器型组件**(Container Component)
- 这些概念有很多重叠,但是它们最主要是关注**数据逻辑**和**UI展示**的分离:
  - 函数组件、无状态组件、展示型组件主要关注**UI的展示**
  - 类组件、有状态组件、容器型组件主要关注**数据逻辑**













\#\# React 创建组件

\#\#\# 1.类组件

- 类组件的定义由如下要求:
  - 组件的名称是大写字符开头 (无论类组件还是函数组件)
  - 类组件需要**继承自**: \`React.Component\`
  - 类组件必须实现 \`render \` 函数
- 使用 \`class \` 定义一个组件:
  - \`constructor\`是可选的,我们通常在 \`constructor\` 中初始化一些数据
  - \`this.state\`中维护的就是我们组件内部的数据
  - \`render()\` 方法是 \`class \` 组件中唯一必须实现的方法





\#\#\# 2.render函数的返回值

\> 当\`render\`函数被调用时\, 它会检查 \`this.props\` 和 \`this.state\` 的变化并返回**以下类型之一**

- **React元素**
  - 通常通过 \`JSX \` 创建
  - 例如:\` \<div/>\` 会被 \`React \` 渲染为 \`DOM  \`节点, \`\\<MyComponent/>\`会被 \`React \` 渲染为自定义组件
  - 无论是 \`\<div/>\` 还是  \`\<MyComponent/>\` 均为 \`React \` 元素
- **数组或 fragments**: 使得 \`render \` 方法可以返回多个元素
- **Portals**: 可以渲染子节点到不同的 \`DOM \` 子树中
- **字符串或数值类型**: 他们在 \`DOM \` 中会被渲染为文本节点
- **布尔类型或null**: 什么都不渲染





\#\#\# 3.函数组件

\> 函数组件是使用 \`function\` 来进行定义的函数\, 只是这个函数会返回和类组件中 \`render\` 函数一样的内容

- 函数组件的特点 (后面会讲hooks\, 就不一样了)
  - 没有生命周期, 也会被更新并挂载, 但是没有生命周期函数
  - 没有 this (组件实例)
  - 没有内部状态 (state)













\#\# React 生命周期

\#\#\# 1.认识生命周期

\> 很多的事物都有从**创建到销毁的整个过程**,这个过程称之为是**生命周期**

\> React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能

- 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段:
  - 比如装载阶段(\`Mount\`),组件第一次在DOM树中被渲染的过程
  - 比如更新阶段(\`Update\`),组件状态发生变化,重新更新渲染的过程
  - 比如卸载过程(\`Unmount\`),组件从DOM树中被移除的过程

- React内部为了告诉我们**当前处于哪些阶段**,会**对组件内部实现的预定函数进行回调**,**这些函数就是生命周期函数**
  - 比如实现\`componentDidMount\`函数:组件已经挂载到DOM上时,就会回调
  - 比如实现\`componentDidUpdate\`函数:组件已经发生了更新时,就会回调
  - 比如实现\`componentWillUnmount\`函数:组件卸载及销毁之前,就会回调
- 我们谈React生命周期时,主要谈的**类的生命周期**,因为**函数式组件是没有生命周期函数**的
  - (后面我们可以通过hooks来模拟一些生命周期的回调)





\#\#\# 2.生命周期解析

- 我们先来学习一下最基础、**最常用的生命周期函数:**

react生命周期





\#\#\# 3.生命周期函数应用场景

\#\#\#\# Constructor

- 如果不初始化 \`state\` 或不进行方法绑定,则不需要为 \`React\` 组件实现构造函数
- \`constructor \` 中通常只做两件事情:
  - 通过给 \`this.state\` 赋值对象来**初始化内部 state**
  - 为事件**绑定this**





\#\#\#\# componentDidMount

- \`componentDidMount()\` 会在**组件挂载后** ( 插入DOM树中 ) 立即调用
- \`componentDidMount()\`中通常进行哪些操作?
  - 依赖于DOM的操作可以在这里进行
  - 在此处发送网络请求就最好的地方 (官方建议)
  - 可以在此处添加一些订阅 (会在componentWillUnmount取消订阅)





\#\#\#\# componentDidUpdate

- \`componentDidUpdate()\` 会在**更新后会被立即调用**,首次渲染不会执行
  - 当组件更新后,可以对此 DOM 进行操作
  - 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求
    (例如,当 props 未发生变化时,则不会执行网络请求)





\#\#\#\# componentWillUnmount

- \`componentWillUnmount\` 会在组件**卸载及销毁之前调用**
  - 在此方法执行必要的清理操作
  - 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅





\#\#\# 3.不常用的生命周期函数

\> 除了上面介绍的生命周期函数之外,还有一些**不常用的生命周期函数**

- \`getDerivedStateFromProps\`
  - \`state\` 的值在任何时候都依赖于 \`props\` 时使用
  - 该方法返回一个对象来更新state
- \`getSnapshotBeforeUpdate\`
  - 在 **React 更新 DOM 之前**回调的一个函数
  - 可以获取 DOM 更新前的一些信息 (比如说: 滚动位置)

- [更多的生命周期相关内容\,可以参考官网](https://zh-hans.reactjs.org/d...)













\#\# React 组件的嵌套

\#\#\# 1.认识组件的嵌套

- 组件之间存在嵌套关系:
  - 在之前的案例中,我们只是创建了一个组件App
  - 如果我们一个应用程序将所有逻辑放在一个组件中, 那么这个组件就会变的非常臃肿和难以维护
  - 所以组件化的核心思想应该是对**组件进行拆分**, **拆分成一个个小的组件**
  - 在将这些组件**组合嵌套在一起**, 最终形成我们的应用程序

module

- 上面的嵌套逻辑如下
  - App组件是Header、Main、Footer组件的父组件
  - Main组件是Banner、ProductList组件的父组件





\#\#\# 2.认识组件间的通信

- 在开发过程中,我们会经常遇到需要**组件之间相互进行通信**
  - 比如 \`App \` 可能使用了多个\`Header\`组件,每个地方的\`Header\`展示的内容不同,那么我们就需要使用者传递给 \`Header\` 一些数据,让其进行展示
  - 又比如我们在 \`Main\` 组件中一次请求了 \`Banner\` 数据和 \`ProductList \`数据,那么就需要传递给它们来进行展示
- 总之,在一个 \`React \` 项目中,组件之间通信是非常重要的环节









\#\# React 父子组件间通信

\#\#\# 1.父传子组件-props

父组件在展示子组件, 可能会传递一些数据给子组件

- 父组件通过\<font color='red'\> **属性=值**\</font\> 的形式给子组件传递数据
- 子组件通过 \<font color='red'\>**props**\</font\> 参数获取父组件传递过来的数据

\<details>
\<summary>\<b>函数组件传递Props\</b>\</summary>

\</details>









\#\#\# 2.属性验证-propTypes

- 对于传递给子组件的数据\, 有时候我们可能希望进行**数据的类型校验**\, 特别是对于大型项目来说
  - 当然,如果你项目中默认继承了\`Flow\`或者\`TypeScript\`,那么直接就可以进行类型验证
  - 但是,即使我们没有使用\`Flow\`或者\`TypeScript\`,也可以通过 \`prop-type\` **库来进行参数验证**
- 使用 \`propTypes \` 来进行对 \`props\` 的验证\, 首先: 导入 \`prop-types\`
- 我们这里只做的 \<font color='red'\>props 类型的校验\</font\> 和 \<font color='red'\>props 的默认值\</font\>  ([更多的验证方式可以参考官网](https://zh-hans.reactjs.org/d...))
  - 比如某个 **props 属性是必须传递**的使用: \`propTypes.string.isRequired\`
- 如果没有传递\`props\`\, 我们希望有**默认值**使用: \`类名.defaultProps = {}\`

\`\`\`js
// 1.导入prop-types来进行属性校验
import propTypes from 'prop-types'

// 2.类型校验: 使用prop-types来校验父组件传递的props类型是否符合预期
// 如果是类中可以: static propTypes = { 进行类型校验 }
ChildCpn.propTypes = {
  // name属性是必传的
  name: propTypes.string.isRequired,
  age: propTypes.number,
  height: propTypes.number,
}

// 3.默认值,父组件没有传递props时的默认值
ChildCpn.defaultProps = {
  name: 'hali',
  age: 21,
  height: 1.77,
}
\`\`\`





\#\#\# 3.子组件传递父组件-函数传递

- 当子组件需要向父组件传递消息:
  - 在 \`vue\` 中是通过自定义事件来完成的
  - 在 \`React\` 中同样还是通过 \`props\` 传递消息
  - 只是让**父组件给子组件传递一个回调函数(callback),在子组件调用这个函数**
    - 注意 \`this\` 绑定问题

\`\`\`jsx
// 父组件
render() {
  return (
    \<div>
      \<h2>当前计数: {this.state.counter}\</h2>
      {/* 子传父: 让子组件来调用父组件中的方法 */}
      {/* 操作步骤: 父组件传递一个回调函数,让子组件来进行调用 */}
      \<Counter increment={e => this.increment()} />
    \</div>
  )
}

increment() {
  this.setState({
    counter: this.state.counter + 1,
  })
}

// 子组件
class Counter extends Component {
  render() {// 调用父组件传递的函数
    return \<button onClick={this.props.increment}>+\</button>
  }
}
\`\`\`













\#\# React 非父子组件通信

\#\#\# 1.Context介绍

\> 非父子组件数据的共享:

\> 在开发中,比较常见的**数据传递方式是通过props属性自上而下**(由父到子)**进行传递**

\> 但是对于有一些场景:比如**一些数据需要在多个组件中进行共享**(地区偏好、UI主题、用户登录状态、用户信息等)

\> 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。

- 如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:
  - \`React\`提供了一个\`API\`:\`Context \`
  - \`Context\` 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 \`props\`
  - \`Context\` 设计目的是为了共享那些对于一个组件树而言是**“全局”的数据**,例如当前认证的用户、主题或首选语言





\#\#\# 2.Context使用

\#\#\#\# React.createContext

- 作用: 创建一个需要**全局共享对象的数据** (需要跨组件间通信的数据)
- 介绍: 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 \`Provider \`中读取到当前的\`context\`值
- \`defaultValue\`是组件在顶层查找过程中**没有找到对应的**\`Provider\`,**那么就使用默认值**





\#\#\#\# Context.Provider

- 介绍:**每个 Context 对象都会返回一个\`Provider React\` 组件**,它允许**消费组件订阅 context 的变化**
- 传递value:\`Provider\` 接收一个 \`value\` 属性,传递给消费组件
- 一个 \`Provider\` 可以和多个消费组件有对应关系
- 多个 \`Provider\` 也可以嵌套使用,里层的会覆盖外层的数据;
- 当 \`Provider\` 的 \`value\` 值发生变化时,它内部的所有消费组件都会重新渲





\#\#\#\# Class.contextType

- 挂载在 \`class\` 上的 \`contextType\` 属性会被重赋值为一个由 \`React.createContext()\` 创建的 \`Context\` 对象
- 这能让你使用 \`this.context\` 来消费最近 \`Context\` 上的那个值
- 你可以在任何生命周期中访问到它,包括 \`render\` 函数中





\#\#\#\# Context.Consumer

- 这里,\`React\` 组件也可以订阅到 \`context\` 变更。这能让你在 **函数式组件** 中**完成订阅 context**
- 这里需要 函数作为子元素(function as child)这种做法
- 这个函数接收当前的 \`context\` 值,返回一个 \`React\` 节点





\#\#\# 3.组件通信图示

react组件通信













\#\# React 通信补充

\#\#\# 1.React slot

\> \`slot\`翻译为插槽: 在 \`React\` 中没有插槽概念\, 因为 \`React\` 太灵活了\, 那在React如何实现插槽功能呢?

\> 使用过 \`Vue\` 的朋友知道\, 如果我们向一个组件中插入内容时\, 可以在子组件中预留插槽\, 内容由父组件决定

\#\#\#\# Children 实现插槽功能

\> 父组件在使用子组件时\, 将需要展示的内容用**子组件包裹起来**

\> 子组件中通过:  \`props.children[0]\` 来取得父组件要展示的内容

\> \<details\>
\> \<summary\>\<b\>理解\</b\>\</summary\>
\> \<p\>前面我们讲过:\</p\>
\> \<li\> render 函数中 return 的JSX 代码最终会转换成 \<code\>React.createElement('tabName'\, 'config'\, children)\</code\>\</li\>
\> \<li\>而第三个参数 children 就是: 咱们在元素中插入的子元素\, 在React源码中将 \<code\>ChildrenArr\</code\>放到了\<code\>props\</code\>属性中的\<code\>children\</code\>中\, 所以说子组件可以通过\<code\>props.children\</code\>\<b\>来接收到父组件插入的子元素\</b\>\</li\>
\> \</details\>





\#\#\#\# props 实现具名插槽

\> 在插入插槽内容时\, 你会发现不能实现像Vue中的指定插槽插入内容(具名插槽)

\> 在React中可以通过 **属性(props)** 来指定你要插入的内容\, 然后在子组件中使用 **props 取出指定的JSX元素插入到指定位置**

\#\#\#\# 总结

- \`Children\`使用场景: 当只有**一个默认内容**时\, 直接插入到子元素即可
- \`props\`指定\`slot\`使用场景: 当有多个插槽内容时\, 使用 \`props\` 形式传递





\#\#\# 2.属性展开

\> 如果你已经有了一个 props 对象,你可以使用展开运算符 \`...\` 来在 JSX 中传递整个 props 对象

\`\`\`jsx
function Profile(props) {
  return (
    \<div>
      {/* 在 JSX 中传递整个 props 对象。以下两个组件是等价的 */}
      \<ProfileHeader nickname={props.nickname} level={props.level}/>
      \<ProfileHeader {...props}/>
    \</div>
  )
}
\`\`\`













\#\# events 事件总线

\#\#\# events

- 前面通过\`Context\`主要实现的是数据的共享,但是在开发中如果有**跨组件之间的事件传递**,应该如何操作呢?

  - 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus) 来完成操作
  - 在React中,我们可以依赖一个使用较多的**库** \`events\` 来完成对应的操作

- 我们可以通过npm或者yarn来安装events:\`yarn add events\`

- events常用的API:

  - 创建 \`EventEmitter\` 对象:\`eventBus\`对象;

  - 发出事件:\`eventBus.emit\`("事件名称", 参数列表);

  - 监听事件:\`eventBus.addListener\`("事件名称", 监听函数);

  - 移除事件:\`eventBus.removeListener\`("事件名称", 监听函数);

\`\`\`jsx
// 1.创建全局事件总线
const eventBus = new EventEmitter()

// 2.发射事件
emitHomeEvent() {
  eventBus.emit('sayHello', 'hello home', 123)
}

// 3.监听事件
componentDidMount() {
  eventBus.addListener('sayHello', this.handleSayHelloListener)
}

// 4.卸载事件
componentWillUnmount() {
  eventBus.removeListener('sayHello', this.handleSayHelloListener)
}
\`\`\`













\#\# refs

\#\#\# 1.如何使用ref

\> React的开发模式中,通常情况下不需要、也不建议直接操作DOM元素,但是某些特殊的情况,确实需要获取到DOM进行某些操作: 文本选择或媒体播放;触发强制动画;集成第三方 DOM 库;

- 如何创建\`refs\`来获取对应的\`DOM\`呢?目前有三种方式:

- 方式一:传入字符串

  - 使用时通过 \`this.refs.传入的字符串\`格式获取对应的元素

- 方式二:传入一个对象

  - 对象是通过 \`React.createRef()\` 方式创建出来的;

  - 使用时获取到创建的对象其中有一个\`current\`属性就是对应的元素

- 方式三:传入一个函数

  - 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存

  - 使用时,直接拿到之前保存的元素对象即可

\`\`\`jsx
import React, { PureComponent, createRef } from 'react'
// ...
constructor(props) {
  super(props)
  this.titleRef = createRef()
  this.titleEl = null
}
render() {
  return (
    \<div>
      {/* <h2 ref=字符串/对象/函数方式> hello react\</h2> */}
      \<h2 ref="titleRef">hello react\</h2>
      \<h2 ref={this.titleRef}>hello react\</h2>
      \<h2 ref={arg => (this.titleEl = arg)}>hello react\</h2>
      \<button onClick={e => this.changeText()}>改变文本\</button>
    \</div>
  )
}

changeText() {
  // 1.通过refs来操作DOM,有三种方式
  // 方式一: 字符串
  this.refs.titleRef.innerHTML = 'hello jean'
  // 方式二: 对象
  this.titleRef.current.innerHTML = 'hello JavaScript'
  // 方式三: 函数
  this.titleEl.innerHTML = 'hello TypeScript'
}
\`\`\`





\#\#\# 2.ref的类型

- \`ref\` 的值根据节点的类型而有所不同:
- 当 \`ref\` 属性用于 \`HTML\` 元素时,构造函数中使用 \`React.createRef()\` 创建的 \`ref\` 接收底层 \`DOM\` 元素作为其 \`current\` 属性

- 当 \`ref\` 属性用于自定义 \`class\` 组件时,\`ref\` 对象接收组件的挂载实例作为其 \`current\` 属性

- **你不能在函数组件上使用** **ref** **属性**,因为他们没有实例

\`\`\`jsx
constructor(props) {
  this.counterRef = createRef()
}



render() {
  return (
    \<div>
      \<Counter ref={this.counterRef} />
      \<button onClick={e => this.appIncrementCount()}>APP的按钮\</button>
    \</div>
  )
}

// 通过ref来获取类组件对象
appIncrementCount() {
  // 调用子组件方法
  this.counterRef.current.increment()
}
\`\`\`

\> 函数式组件是没有实例的,所以无法通过ref获取他们的实例:
\> 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素;
\> 这个时候我们可以通过 \`React.forwardRef\` ,后面我们也会学习 \`hooks\` 中如何使用ref;













\#\# 受控组件与非受控组件

\#\#\# 1.认识受控组件

- 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的\`state\`
- 比如下面的HTML表单元素:
  - 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;
  - 在React中,并没有禁止这个行为,它依然是有效的;
  - 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;
  - 实现这种效果的标准方式是使用“受控组件”;





\#\#\# 2.受控组件基本演练

- 在 HTML 中,表单元素(如\\<input\>、 \\<textarea\> 和 \\<select\>)之类的表单元素通常自己维护 state,并根据用户输入进行更新
- 而在 React 中,**可变状态**(mutable state)通常保存在**组件的 state 属性**中,并且只能通过使用 setState() 来更新

  - 我们将两者结合起来,使\`React\`的\`state\`成为“唯一数据源”;
  - 渲染表单的 \`React\` 组件还控制着用户输入过程中表单发生的操作;
  - 被 \`React\` 以这种方式控制取值的表单输入元素就叫做“**受控组件**”;
- 由于在表单元素上设置了 \`value\` 属性,因此显示的值将始终为\` this.state.value\`,这使得 React 的 state 成为唯一数据源。
- 由于 handleUsernameChange 在每次按键时都会执行并**更新 React 的 state**,因此**显示的值将随着用户输入而更新**





\#\#\# 3.受控组件的其他演练

- textarea标签
  - texteare标签和input比较相似:
- select标签
  - select标签的使用也非常简单,只是它不需要通过selected属性来控制哪一个被选中,它可以匹配state的value来选中。
- 处理多个输入
  - 多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:
    这里我们可以使用ES6的一个语法:计算属性名(Computed property names)





\#\#\# 3.非受控组件(了解)

- React推荐大多数情况下使用 受控组件 来处理表单数据:

  - 一个受控组件中,表单数据是由 React 组件来管理的;

  - 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;

- 如果要使用非受控组件中的数据,那么我们需要使用 ref 来从DOM节点中获取表单数据

  - 我们来进行一个简单的演练:

  - 使用ref来获取input元素;

  - 在非受控组件中通常使用defaultValue来设置默认值

  - \<details>
    \<summary>\<b>了解即可\</b>\</summary>
   
    \</details>













\#\# 高阶组件

\#\#\# 1.认识高阶函数

- 什么是高阶组件呢?它和高阶函数非常相似,我们可以先来回顾一下什么是: 高阶函数

- 高阶函数的维基百科定义(至少满足以下条件之一):

  - 接受一个或多个函数作为输入

  - 输出一个函数

- \`JavaScript\`中比较常见的\`filter、map、reduce\`都是高阶函数

- 那么什么是高阶组件呢?

  - 高阶组件的英文是 **Higher-Order** **Components**,简称为 HOC
  - 官方的定义: **高阶组件是参数为组件, 返回为新的组件**

- 我们可以进行如下的解析:

  - **首先, 高阶组件本身不是一个组件, 而是一个函数**
  - **其次, 这个函数的参数是一个组件, 返回值是一个组件**





\#\#\# 2.高阶组件的定义

- **高阶组件调用**类似于:

 

- \<details\>
  \<summary>\<b>高阶函数的编写过程类似于这样\</b>\</summary>
 
  \</details>

- 组件的名称问题:

  - 在ES6中,**类表达式中**类名是可以省略的
  - 组件的名称都可以通过\`displayName\`来修改

\> 高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式

\> 高阶组件在一些React第三方库中非常常见:

\> - 比如redux中的connect (后续会讲到)
\> - 比如react-router中的withRouter (后续会讲到)





\#\#\# 3.高阶组件应用场景

\#\#\#\# 应用一: props的增强

- **不修改原有代码的情况下,添加新的props**

- **利用高阶组件来共享Context**





\#\#\#\# 应用二: 渲染判断鉴权

- 在开发中,我们可能遇到这样的场景:

  - 某些页面是必须用户登录成功才能进行进入;

  - 如果用户没有登录成功,那么直接跳转到登录页面;

- 这个时候,我们就可以使用高阶组件来完成鉴权操作





\#\#\#\# 应用三: 生命周期劫持

- 我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:





\#\#\# 4.高阶组件的意义

- 我们会发现利用高阶组件可以针对某些React代码进行更加优雅的处理
- 利用高阶组件可以完成多个组件中的共同功能
- 其实早期的React有提供组件之间的一种复用方式是\`mixin\`,**目前已经不再建议使用: **

  - Mixin 可能会相互依赖,相互耦合,不利于代码维护
  - 不同的Mixin中的方法可能会相互冲突
  - Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
- 当然,(高阶组件)HOC 也有自己的一些缺陷:

  - HOC需要在原组件上进行**包裹**或者**嵌套**,如果大量使用HOC,将会产生非常多的**嵌套**,这**让调试变得非常困难**

  - HOC可以劫持\`props\`,在不遵守约定的情况下也可能造成冲突
- \`Hooks\`的出现,是开创性的,它解决了很多React之前的存在的问题
- 比如this指向问题、比如hoc的嵌套复杂度问题等等





\#\#\# 4.ref的转发(获取函数式组件DOM)

- 在前面我们学习\`ref\`时讲过,\`ref\`**不能应用于函数式组件**:
  - 因为**函数式组件没有实例**,所以**不能获取到对应的组件对象**
- 但是,在开发中我们可能想要**获取函数式组件中某个元素的DOM**,这个时候我们应该如何操作呢?
  - 方式一:直接传入ref属性 (错误的做法)
  - **方式二:**通过**\`forwardRef高阶函数 \`**













\#\# Portals

\#\#\# Portals的使用

- 某些情况下,我们**希望渲染的内容独立于父组件**,甚至是**独立于当前挂载到的DOM元素中**(默认都是挂载到\`id\`为\`root\`的\`DOM\`元素上的)
- \`Portal\`提供了一种将子节点渲染到存在于父组件之外的 DOM 节点
  - 参数一: \`child\` 是任何可渲染的 React 子元素, 例如一个元素,字符串或 fragment;
  - 参数二: \`container\` 是一个DOM 元素
  - \`ReactDOM.createPortal(child, container)\`

- 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点
- 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的













\#\# Fragment

\#\#\# Fragment的使用

- 在之前的开发中,我们**总是在一个组件中**返回内容时**包裹一个div根元素**

- 我们**希望可以不渲染这样根元素div**应该如何操作呢?

  - \<details>
    \<summary>\<b>使用\<code>Fragment\</code>\</b>\</summary>
   
    \</details>

  - \`Fragment\`允许你将子列表分组, 无需向 DOM 天添加额外节点

- React还提供了\`Fragment\`的**短语法**:

  - **短语法**使用: \`<></>\`

  - \<details>
    \<summary>下拉查看\</summary>
   
    \</details>

  - 注意: 如果我们需要在\`Fragment\`中添加属性或者\`key\`, 那么就不能使用短语法













\#\# StrictMode

\#\#\# StrictMode 介绍

- \`StrictMode \` 是一个用来突出显示应用程序中潜在问题的**工具**

  - 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI
  - 它为其后代元素触发额外的检查和警告
  - 严格模式检查仅在开发模式下运行;*它们不会影响生产构建*

- 可以为应用程序的任何部分启用严格模式:

  - 不会对 Header 和 Footer 组件运行严格模式检查

  - 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查





\#\#\# 严格模式检查的是什么?

**到底检测什么呢?**

1. 识别不安全的生命周期

2. 使用过时的\`ref API\`

3. 使用废弃的\`findDOMNode\`方法
   - 在之前的React API中,可以通过\`findDOMNode\`来获取DOM,不过已经不推荐使用了

4. 检查意外的副作用

   - 这个组件的 \`constructor \` 会被调用两次

   - 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用

   - 在生产环境中,是不会被调用两次的

5. 检测过时的\`context API\`

   - 早期的\`Context\`是通过\`static\`属性声明\`Context\`对象属性,通过\`getChildContext\`返回\`Context\`对象等方式来使用\`Context\`的

   - 目前这种方式已经不推荐使用,大家可以自行学习了解一下它的用法

React组件化开发

认识组件化
-----

\#\#\# 1.组件化思想

* 当人们面对复杂问题的处理方式:

    * 将复杂的**问题进行拆解**, 拆分成很多个可以处理的小问题

    * 再**将其放在整体当中**,你会发现大的问题也会迎刃而解

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164732.png)

* 其实上面的思想就是分而治之的思想:

    * 分而治之是软件工程的重要思想,是复杂系统开发和维护的基石

    * 而前端目前的模块化和组件化都是基于分而治之的思想

\#\#\# 2.什么是组件化开发呢?

* 组件化也是类似的思想:

    * 如果我们将一个页面中全部逻辑放在一起, 处理起来会变得非常复杂, 不利于后续管理及扩展

    * 但如果我们**将一个页面拆分成一个个小的功能模块**, 每个功能**完成自己这部分独立功能**, 那么整个页面的管理和维护变得非常容易

\![React组件化开发](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164738.png)

* 我们需要通过组件化的思想来思考整个应用程序:

    * 我们将一个完整的页面分成很多个组件

    * 每个组件都用于实现页面的一个功能块

\#\#\# 3.React的组件化

* 组件化是 React 的核心思想,前面我们封装的 App 本身就是一个组件

    * 组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用

    * 任何的应用都会被抽象成一颗组件树

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164748.png)

* 组件化思想的应用:

    * 尽可能的将页面拆分成一个个小的、可复用的组件

    * 这样让我们的代码更加方便组织和管理,并且扩展性也更强

\#\#\# 4.React组件分类

* **React的组件**相对于 Vue **更加的灵活和多样**,按照不同的方式可以分成很多类组件:

    * 根据组件的定义方式,可以分为:**函数组件**(Functional Component)和**类组件**(Class Component)

    * 根据组件内部是否有状态需要维护,可以分成:**无状态组件**(Stateless Component)和**有状态组件**(Stateful Component)

    * 根据组件的不同职责,可以分成:**展示型组件**(Presentational Component)和**容器型组件**(Container Component)

* 这些概念有很多重叠,但是它们最主要是关注**数据逻辑**和**UI展示**的分离:

    * 函数组件、无状态组件、展示型组件主要关注**UI的展示**

    * 类组件、有状态组件、容器型组件主要关注**数据逻辑**

React 创建组件
----------

\#\#\# 1.类组件

* 类组件的定义由如下要求:

    * 组件的名称是大写字符开头 (无论类组件还是函数组件)

    * 类组件需要**继承自**: \`React.Component\`

    * 类组件必须实现 \`render\` 函数

* 使用 \`class\` 定义一个组件:

    * \`constructor\`是可选的,我们通常在 \`constructor\` 中初始化一些数据

    * \`this.state\`中维护的就是我们组件内部的数据

    * \`render()\` 方法是 \`class\` 组件中唯一必须实现的方法

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164846.png)

\#\#\# 2.render函数的返回值

\> 当\`render\`函数被调用时\, 它会检查 \`this.props\` 和 \`this.state\` 的变化并返回**以下类型之一**

* **React元素**

    * 通常通过 \`JSX\` 创建

    * 例如:\`\<div/>\` 会被 \`React\` 渲染为 \`DOM\`节点, \`\<MyComponent/>\`会被 \`React\` 渲染为自定义组件

    * 无论是 \`\<div/>\` 还是 \`\<MyComponent/>\` 均为 \`React\` 元素

* **数组或 fragments**: 使得 \`render\` 方法可以返回多个元素

* **Portals**: 可以渲染子节点到不同的 \`DOM\` 子树中

* **字符串或数值类型**: 他们在 \`DOM\` 中会被渲染为文本节点

* **布尔类型或null**: 什么都不渲染

\#\#\# 3.函数组件

\> 函数组件是使用 \`function\` 来进行定义的函数\, 只是这个函数会返回和类组件中 \`render\` 函数一样的内容

* 函数组件的特点 (后面会讲hooks, 就不一样了)

    * 没有生命周期, 也会被更新并挂载, 但是没有生命周期函数

    * 没有 this (组件实例)

    * 没有内部状态 (state)

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164900.png)

React 生命周期
----------

\#\#\# 1.认识生命周期

\> 很多的事物都有从**创建到销毁的整个过程**,这个过程称之为是**生命周期**
\>
\> React组件也有自己的生命周期,了解组件的生命周期可以让我们在最合适的地方完成自己想要的功能

* 生命周期是一个抽象的概念,在生命周期的整个过程,分成了很多个阶段:

    * 比如装载阶段(\`Mount\`),组件第一次在DOM树中被渲染的过程

    * 比如更新阶段(\`Update\`),组件状态发生变化,重新更新渲染的过程

    * 比如卸载过程(\`Unmount\`),组件从DOM树中被移除的过程

* React内部为了告诉我们**当前处于哪些阶段**,会**对组件内部实现的预定函数进行回调**,**这些函数就是生命周期函数**

    * 比如实现\`componentDidMount\`函数:组件已经挂载到DOM上时,就会回调

    * 比如实现\`componentDidUpdate\`函数:组件已经发生了更新时,就会回调

    * 比如实现\`componentWillUnmount\`函数:组件卸载及销毁之前,就会回调

* 我们谈React生命周期时,主要谈的**类的生命周期**,因为**函数式组件是没有生命周期函数**的

    * (后面我们可以通过hooks来模拟一些生命周期的回调)

\#\#\# 2.生命周期解析

* 我们先来学习一下最基础、**最常用的生命周期函数:**

\![react生命周期](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164906.png)

\#\#\# 3.生命周期函数应用场景

\#\#\#\# Constructor

* 如果不初始化 \`state\` 或不进行方法绑定,则不需要为 \`React\` 组件实现构造函数

* \`constructor\` 中通常只做两件事情:

    * 通过给 \`this.state\` 赋值对象来**初始化内部 state**

    * 为事件**绑定this**

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164914.png)

\#\#\#\# componentDidMount

* \`componentDidMount()\` 会在**组件挂载后** ( 插入DOM树中 ) 立即调用

* \`componentDidMount()\`中通常进行哪些操作?

    * 依赖于DOM的操作可以在这里进行

    * 在此处发送网络请求就最好的地方 (官方建议)

    * 可以在此处添加一些订阅 (会在componentWillUnmount取消订阅)

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164920.png)

\#\#\#\# componentDidUpdate

* \`componentDidUpdate()\` 会在**更新后会被立即调用**,首次渲染不会执行

    * 当组件更新后,可以对此 DOM 进行操作

    * 如果你对更新前后的 props 进行了比较,也可以选择在此处进行网络请求 (例如,当 props 未发生变化时,则不会执行网络请求)

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164924.png)

\#\#\#\# componentWillUnmount

* \`componentWillUnmount\` 会在组件**卸载及销毁之前调用**

    * 在此方法执行必要的清理操作

    * 例如,清除 timer,取消网络请求或清除在 componentDidMount() 中创建的订阅

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164934.png)

\#\#\# 3.不常用的生命周期函数

\> 除了上面介绍的生命周期函数之外,还有一些**不常用的生命周期函数**

* \`getDerivedStateFromProps\`

    * \`state\` 的值在任何时候都依赖于 \`props\` 时使用

    * 该方法返回一个对象来更新state

* \`getSnapshotBeforeUpdate\`

    * 在 **React 更新 DOM 之前**回调的一个函数

    * 可以获取 DOM 更新前的一些信息 (比如说: 滚动位置)

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164944.png)

* [更多的生命周期相关内容\,可以参考官网](https://zh-hans.reactjs.org/docs/react-component.html)

React 组件的嵌套
-----------

\#\#\# 1.认识组件的嵌套

* 组件之间存在嵌套关系:

    * 在之前的案例中,我们只是创建了一个组件App

    * 如果我们一个应用程序将所有逻辑放在一个组件中, 那么这个组件就会变的非常臃肿和难以维护

    * 所以组件化的核心思想应该是对**组件进行拆分**, **拆分成一个个小的组件**

    * 在将这些组件**组合嵌套在一起**, 最终形成我们的应用程序

\![module](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902164959.png)

* 上面的嵌套逻辑如下

    * App组件是Header、Main、Footer组件的父组件

    * Main组件是Banner、ProductList组件的父组件

\#\#\# 2.认识组件间的通信

* 在开发过程中,我们会经常遇到需要**组件之间相互进行通信**

    * 比如 \`App\` 可能使用了多个\`Header\`组件,每个地方的\`Header\`展示的内容不同,那么我们就需要使用者传递给 \`Header\` 一些数据,让其进行展示

    * 又比如我们在 \`Main\` 组件中一次请求了 \`Banner\` 数据和 \`ProductList\`数据,那么就需要传递给它们来进行展示

* 总之,在一个 \`React\` 项目中,组件之间通信是非常重要的环节

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165028.png)

React 父子组件间通信
-------------

\#\#\# 1.父传子组件-props

父组件在展示子组件, 可能会传递一些数据给子组件

* 父组件通过 **属性=值** 的形式给子组件传递数据

* 子组件通过 **props** 参数获取父组件传递过来的数据

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165035.png)





**函数组件传递Props**





\#\#\# 2.属性验证-propTypes

* 对于传递给子组件的数据, 有时候我们可能希望进行**数据的类型校验**, 特别是对于大型项目来说

    * 当然,如果你项目中默认继承了\`Flow\`或者\`TypeScript\`,那么直接就可以进行类型验证

    * 但是,即使我们没有使用\`Flow\`或者\`TypeScript\`,也可以通过 \`prop-type\` **库来进行参数验证**

* 使用 \`propTypes\` 来进行对 \`props\` 的验证, 首先: 导入 \`prop-types\`

* 我们这里只做的 props 类型的校验 和 props 的默认值 ([更多的验证方式可以参考官网](https://zh-hans.reactjs.org/docs/typechecking-with-proptypes.html))

    * 比如某个 **props 属性是必须传递**的使用: \`propTypes.string.isRequired\`

* 如果没有传递\`props\`, 我们希望有**默认值**使用: \`类名.defaultProps = {}\`

// 1.导入prop-types来进行属性校验
import propTypes from 'prop-types'

// 2.类型校验: 使用prop-types来校验父组件传递的props类型是否符合预期
// 如果是类中可以: static propTypes = { 进行类型校验 }
ChildCpn.propTypes = {
 // name属性是必传的
 name: propTypes.string.isRequired,
 age: propTypes.number,
 height: propTypes.number,
}

// 3.默认值,父组件没有传递props时的默认值
ChildCpn.defaultProps = {
 name: 'hali',
 age: 21,
 height: 1.77,
}

\#\#\# 3.子组件传递父组件-函数传递

* 当子组件需要向父组件传递消息:

    * 在 \`vue\` 中是通过自定义事件来完成的

    * 在 \`React\` 中同样还是通过 \`props\` 传递消息

    * 只是让**父组件给子组件传递一个回调函数(callback),在子组件调用这个函数**

        * 注意 \`this\` 绑定问题

// 父组件
render() {
 return (
 \<div>
 \<h2>当前计数: {this.state.counter}\</h2>
 {/* 子传父: 让子组件来调用父组件中的方法 */}
 {/* 操作步骤: 父组件传递一个回调函数,让子组件来进行调用 */}
 \<Counter increment={e => this.increment()} />
 \</div>
 )
}

increment() {
 this.setState({
 counter: this.state.counter + 1,
 })
}

// 子组件
class Counter extends Component {
 render() {// 调用父组件传递的函数
 return \<button onClick={this.props.increment}>+\</button>
 }
}

React 非父子组件通信
-------------

\#\#\# 1.Context介绍

\> 非父子组件数据的共享:
\>
\> 在开发中,比较常见的**数据传递方式是通过props属性自上而下**(由父到子)**进行传递**
\>
\> 但是对于有一些场景:比如**一些数据需要在多个组件中进行共享**(地区偏好、UI主题、用户登录状态、用户信息等)
\>
\> 如果我们在顶层的App中定义这些信息,之后一层层传递下去,那么对于一些中间层不需要数据的组件来说,是一种冗余的操作。

* 如果层级更多的话,一层层传递是非常麻烦,并且代码是非常冗余的:

    * \`React\`提供了一个\`API\`:\`Context\`

    * \`Context\` 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 \`props\`

    * \`Context\` 设计目的是为了共享那些对于一个组件树而言是**“全局”的数据**,例如当前认证的用户、主题或首选语言

\#\#\# 2.Context使用

\#\#\#\# React.createContext

* 作用: 创建一个需要**全局共享对象的数据** (需要跨组件间通信的数据)

* 介绍: 如果一个组件订阅了Context,那么这个组件会从离自身最近的那个匹配的 \`Provider\`中读取到当前的\`context\`值

* \`defaultValue\`是组件在顶层查找过程中**没有找到对应的**\`Provider\`,**那么就使用默认值**

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165129.png)

\#\#\#\# Context.Provider

* 介绍:**每个 Context 对象都会返回一个\`Provider React\` 组件**,它允许**消费组件订阅 context 的变化**

* 传递value:\`Provider\` 接收一个 \`value\` 属性,传递给消费组件

* 一个 \`Provider\` 可以和多个消费组件有对应关系

* 多个 \`Provider\` 也可以嵌套使用,里层的会覆盖外层的数据;

* 当 \`Provider\` 的 \`value\` 值发生变化时,它内部的所有消费组件都会重新渲

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165135.png)

\#\#\#\# Class.contextType

* 挂载在 \`class\` 上的 \`contextType\` 属性会被重赋值为一个由 \`React.createContext()\` 创建的 \`Context\` 对象

* 这能让你使用 \`this.context\` 来消费最近 \`Context\` 上的那个值

* 你可以在任何生命周期中访问到它,包括 \`render\` 函数中

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165139.png)

\#\#\#\# Context.Consumer

* 这里,\`React\` 组件也可以订阅到 \`context\` 变更。这能让你在 **函数式组件** 中**完成订阅 context**

* 这里需要 函数作为子元素(function as child)这种做法

* 这个函数接收当前的 \`context\` 值,返回一个 \`React\` 节点

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165144.png)

\#\#\# 3.组件通信图示

\![react组件通信](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165148.png)

React 通信补充
----------

\#\#\# 1.React slot

\> \`slot\`翻译为插槽: 在 \`React\` 中没有插槽概念\, 因为 \`React\` 太灵活了\, 那在React如何实现插槽功能呢?
\>
\> 使用过 \`Vue\` 的朋友知道\, 如果我们向一个组件中插入内容时\, 可以在子组件中预留插槽\, 内容由父组件决定

\#\#\#\# Children 实现插槽功能

\> 父组件在使用子组件时\, 将需要展示的内容用**子组件包裹起来**
\>
\> 子组件中通过: \`props.children[0]\` 来取得父组件要展示的内容
\>

\>
\> **理解**
\>

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165153.png)

\#\#\#\# props 实现具名插槽

\> 在插入插槽内容时\, 你会发现不能实现像Vue中的指定插槽插入内容(具名插槽)
\>
\> 在React中可以通过 **属性(props)** 来指定你要插入的内容\, 然后在子组件中使用 **props 取出指定的JSX元素插入到指定位置**

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165157.png)

\#\#\#\# 总结

* \`Children\`使用场景: 当只有**一个默认内容**时, 直接插入到子元素即可

* \`props\`指定\`slot\`使用场景: 当有多个插槽内容时, 使用 \`props\` 形式传递

\#\#\# 2.属性展开

\> 如果你已经有了一个 props 对象,你可以使用展开运算符 \`...\` 来在 JSX 中传递整个 props 对象

function Profile(props) {
 return (
 \<div>
 {/* 在 JSX 中传递整个 props 对象。以下两个组件是等价的 */}
 \<ProfileHeader nickname={props.nickname} level={props.level}/>
 \<ProfileHeader {...props}/>
 \</div>
 )
}

events 事件总线
-----------

\#\#\# events

* 前面通过\`Context\`主要实现的是数据的共享,但是在开发中如果有**跨组件之间的事件传递**,应该如何操作呢?

    * 在Vue中我们可以通过Vue的实例,快速实现一个事件总线(EventBus) 来完成操作

    * 在React中,我们可以依赖一个使用较多的**库** \`events\` 来完成对应的操作

* 我们可以通过npm或者yarn来安装events:\`yarn add events\`

* events常用的API:

    * 创建 \`EventEmitter\` 对象:\`eventBus\`对象;

    * 发出事件:\`eventBus.emit\`("事件名称", 参数列表);

    * 监听事件:\`eventBus.addListener\`("事件名称", 监听函数);

    * 移除事件:\`eventBus.removeListener\`("事件名称", 监听函数);

// 1.创建全局事件总线
const eventBus = new EventEmitter()

// 2.发射事件
emitHomeEvent() {
 eventBus.emit('sayHello', 'hello home', 123)
}

// 3.监听事件
componentDidMount() {
 eventBus.addListener('sayHello', this.handleSayHelloListener)
}

// 4.卸载事件
componentWillUnmount() {
 eventBus.removeListener('sayHello', this.handleSayHelloListener)
}

refs
----

\#\#\# 1.如何使用ref

\> React的开发模式中,通常情况下不需要、也不建议直接操作DOM元素,但是某些特殊的情况,确实需要获取到DOM进行某些操作: 文本选择或媒体播放;触发强制动画;集成第三方 DOM 库;

* 如何创建\`refs\`来获取对应的\`DOM\`呢?目前有三种方式:

* 方式一:传入字符串

    * 使用时通过 \`this.refs.传入的字符串\`格式获取对应的元素

* 方式二:传入一个对象

    * 对象是通过 \`React.createRef()\` 方式创建出来的;

    * 使用时获取到创建的对象其中有一个\`current\`属性就是对应的元素

* 方式三:传入一个函数

    * 该函数会在DOM被挂载时进行回调,这个函数会传入一个 元素对象,我们可以自己保存

    * 使用时,直接拿到之前保存的元素对象即可

import React, { PureComponent, createRef } from 'react'
// ...
constructor(props) {
 super(props)
 this.titleRef = createRef()
 this.titleEl = null
}
render() {
 return (
 \<div>
 {/* <h2 ref=字符串/对象/函数方式> hello react\</h2> */}
 \<h2 ref="titleRef">hello react\</h2>
 \<h2 ref={this.titleRef}>hello react\</h2>
 \<h2 ref={arg => (this.titleEl = arg)}>hello react\</h2>
 \<button onClick={e => this.changeText()}>改变文本\</button>
 \</div>
 )
}

changeText() {
 // 1.通过refs来操作DOM,有三种方式
 // 方式一: 字符串
 this.refs.titleRef.innerHTML = 'hello jean'
 // 方式二: 对象
 this.titleRef.current.innerHTML = 'hello JavaScript'
 // 方式三: 函数
 this.titleEl.innerHTML = 'hello TypeScript'
}

\#\#\# 2.ref的类型

* \`ref\` 的值根据节点的类型而有所不同:

* 当 \`ref\` 属性用于 \`HTML\` 元素时,构造函数中使用 \`React.createRef()\` 创建的 \`ref\` 接收底层 \`DOM\` 元素作为其 \`current\` 属性

* 当 \`ref\` 属性用于自定义 \`class\` 组件时,\`ref\` 对象接收组件的挂载实例作为其 \`current\` 属性

* **你不能在函数组件上使用** **ref** **属性**,因为他们没有实例

constructor(props) {
 this.counterRef = createRef()
}



render() {
 return (
 \<div>
 \<Counter ref={this.counterRef} />
 \<button onClick={e => this.appIncrementCount()}>APP的按钮\</button>
 \</div>
 )
}

// 通过ref来获取类组件对象
appIncrementCount() {
 // 调用子组件方法
 this.counterRef.current.increment()
}

\> 函数式组件是没有实例的,所以无法通过ref获取他们的实例: 但是某些时候,我们可能想要获取函数式组件中的某个DOM元素; 这个时候我们可以通过 \`React.forwardRef\` ,后面我们也会学习 \`hooks\` 中如何使用ref;

受控组件与非受控组件
----------

\#\#\# 1.认识受控组件

* 在React中,HTML表单的处理方式和普通的DOM元素不太一样:表单元素通常会保存在一些内部的\`state\`

* 比如下面的HTML表单元素:

    * 这个处理方式是DOM默认处理HTML表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面;

    * 在React中,并没有禁止这个行为,它依然是有效的;

    * 但是通常情况下会使用JavaScript函数来方便的处理表单提交,同时还可以访问用户填写的表单数据;

    * 实现这种效果的标准方式是使用“受控组件”;

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165252.png)

\#\#\# 2.受控组件基本演练

* 在 HTML 中,表单元素(如\<input>、 \<textarea> 和 \<select>)之类的表单元素通常自己维护 state,并根据用户输入进行更新

* 而在 React 中,**可变状态**(mutable state)通常保存在**组件的 state 属性**中,并且只能通过使用 setState() 来更新

    * 我们将两者结合起来,使\`React\`的\`state\`成为“唯一数据源”;

    * 渲染表单的 \`React\` 组件还控制着用户输入过程中表单发生的操作;

    * 被 \`React\` 以这种方式控制取值的表单输入元素就叫做“**受控组件**”;

* 由于在表单元素上设置了 \`value\` 属性,因此显示的值将始终为\`this.state.value\`,这使得 React 的 state 成为唯一数据源。

* 由于 handleUsernameChange 在每次按键时都会执行并**更新 React 的 state**,因此**显示的值将随着用户输入而更新**

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165257.png)

\#\#\# 3.受控组件的其他演练

* textarea标签

    * texteare标签和input比较相似:

* select标签

    * select标签的使用也非常简单,只是它不需要通过selected属性来控制哪一个被选中,它可以匹配state的value来选中。

* 处理多个输入

    * 多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法: 这里我们可以使用ES6的一个语法:计算属性名(Computed property names)

\#\#\# 3.非受控组件(了解)

* React推荐大多数情况下使用 受控组件 来处理表单数据:

    * 一个受控组件中,表单数据是由 React 组件来管理的;

    * 另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理;

* 如果要使用非受控组件中的数据,那么我们需要使用 ref 来从DOM节点中获取表单数据

    * 我们来进行一个简单的演练:

    * 使用ref来获取input元素;

    * 在非受控组件中通常使用defaultValue来设置默认值

    *

        **了解即可**

高阶组件
----

\#\#\# 1.认识高阶函数

* 什么是高阶组件呢?它和高阶函数非常相似,我们可以先来回顾一下什么是: 高阶函数

* 高阶函数的维基百科定义(至少满足以下条件之一):

    * 接受一个或多个函数作为输入

    * 输出一个函数

* \`JavaScript\`中比较常见的\`filter、map、reduce\`都是高阶函数

* 那么什么是高阶组件呢?

    * 高阶组件的英文是 **Higher-Order** **Components**,简称为 HOC

    * 官方的定义: **高阶组件是参数为组件, 返回为新的组件**

* 我们可以进行如下的解析:

    * **首先, 高阶组件本身不是一个组件, 而是一个函数**

    * **其次, 这个函数的参数是一个组件, 返回值是一个组件**

\#\#\# 2.高阶组件的定义

* **高阶组件调用**类似于:

    \![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165349.png)

*

    **高阶函数的编写过程类似于这样**

* 组件的名称问题:

    * 在ES6中,**类表达式中**类名是可以省略的

    * 组件的名称都可以通过\`displayName\`来修改

\> 高阶组件并不是React API的一部分,它是基于React的组合特性而形成的设计模式
\>
\> 高阶组件在一些React第三方库中非常常见:
\>
\> * 比如redux中的connect (后续会讲到)
\>
\> * 比如react-router中的withRouter (后续会讲到)
\>

\#\#\# 3.高阶组件应用场景

\#\#\#\# 应用一: props的增强

* **不修改原有代码的情况下,添加新的props**

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165355.png)

* **利用高阶组件来共享Context**

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165357.png)

\#\#\#\# 应用二: 渲染判断鉴权

* 在开发中,我们可能遇到这样的场景:

    * 某些页面是必须用户登录成功才能进行进入;

    * 如果用户没有登录成功,那么直接跳转到登录页面;

* 这个时候,我们就可以使用高阶组件来完成鉴权操作

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165400.png)

\#\#\#\# 应用三: 生命周期劫持

* 我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165402.png)

\#\#\# 4.高阶组件的意义

* 我们会发现利用高阶组件可以针对某些React代码进行更加优雅的处理

* 利用高阶组件可以完成多个组件中的共同功能

* 其实早期的React有提供组件之间的一种复用方式是\`mixin\`,**目前已经不再建议使用:**

    * Mixin 可能会相互依赖,相互耦合,不利于代码维护

    * 不同的Mixin中的方法可能会相互冲突

    * Mixin非常多时,组件是可以感知到的,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性

* 当然,(高阶组件)HOC 也有自己的一些缺陷:

    * HOC需要在原组件上进行**包裹**或者**嵌套**,如果大量使用HOC,将会产生非常多的**嵌套**,这**让调试变得非常困难**

    * HOC可以劫持\`props\`,在不遵守约定的情况下也可能造成冲突

* \`Hooks\`的出现,是开创性的,它解决了很多React之前的存在的问题

* 比如this指向问题、比如hoc的嵌套复杂度问题等等

\#\#\# 4.ref的转发(获取函数式组件DOM)

* 在前面我们学习\`ref\`时讲过,\`ref\`**不能应用于函数式组件**:

    * 因为**函数式组件没有实例**,所以**不能获取到对应的组件对象**

* 但是,在开发中我们可能想要**获取函数式组件中某个元素的DOM**,这个时候我们应该如何操作呢?

    * 方式一:直接传入ref属性 (错误的做法)

    * **方式二:**通过**\`forwardRef高阶函数\`**

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165406.png)

Portals
-------

\#\#\# Portals的使用

* 某些情况下,我们**希望渲染的内容独立于父组件**,甚至是**独立于当前挂载到的DOM元素中**(默认都是挂载到\`id\`为\`root\`的\`DOM\`元素上的)

* \`Portal\`提供了一种将子节点渲染到存在于父组件之外的 DOM 节点

    * 参数一: \`child\` 是任何可渲染的 React 子元素, 例如一个元素,字符串或 fragment;

    * 参数二: \`container\` 是一个DOM 元素

    * \`ReactDOM.createPortal(child, container)\`

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165410.png)

* 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点

* 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的

Fragment
--------

\#\#\# Fragment的使用

* 在之前的开发中,我们**总是在一个组件中**返回内容时**包裹一个div根元素**

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165413.png)

* 我们**希望可以不渲染这样根元素div**应该如何操作呢?

    *

        **使用\`Fragment\`**

    * \`Fragment\`允许你将子列表分组, 无需向 DOM 天添加额外节点

* React还提供了\`Fragment\`的**短语法**:

    * **短语法**使用: \`<></>\`

    *

        下拉查看

    * 注意: 如果我们需要在\`Fragment\`中添加属性或者\`key\`, 那么就不能使用短语法

StrictMode
----------

\#\#\# StrictMode 介绍

* \`StrictMode\` 是一个用来突出显示应用程序中潜在问题的**工具**

    * 与 Fragment 一样,StrictMode 不会渲染任何可见的 UI

    * 它为其后代元素触发额外的检查和警告

    * 严格模式检查仅在开发模式下运行;\_它们不会影响生产构建\_

* 可以为应用程序的任何部分启用严格模式:

    * 不会对 Header 和 Footer 组件运行严格模式检查

    * 但是,ComponentOne 和 ComponentTwo 以及它们的所有后代元素都将进行检查

\![](https://gitee.com/xmkm/cloudPic/raw/master/img/20200902165452.png)

\#\#\# 严格模式检查的是什么?

**到底检测什么呢?**

1. 识别不安全的生命周期

2. 使用过时的\`ref API\`

3. 使用废弃的\`findDOMNode\`方法

    * 在之前的React API中,可以通过\`findDOMNode\`来获取DOM,不过已经不推荐使用了

4. 检查意外的副作用

    * 这个组件的 \`constructor\` 会被调用两次

    * 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用

    * 在生产环境中,是不会被调用两次的

5. 检测过时的\`context API\`

    * 早期的\`Context\`是通过\`static\`属性声明\`Context\`对象属性,通过\`getChildContext\`返回\`Context\`对象等方式来使用\`Context\`的

    * 目前这种方式已经不推荐使用,大家可以自行学习了解一下它的用法

查看原文

赞 0 收藏 0 评论 0

风不识途 收藏了问题 · 1月18日

redux结合immutable.js和redux-persist报错

clipboard.png

store.js

import { createStore, compose, applyMiddleware } from "redux";
import { routerMiddleware } from "connected-react-router/immutable";
import { createMigrate, persistStore, persistReducer } from "redux-persist";
import createEncryptor from "redux-persist-transform-encrypt";
import immutableTransform from "redux-persist-transform-immutable";
import storage from "redux-persist/es/storage";
import createSagaMiddleware from "redux-saga";
import logger from "redux-logger";
import { createBrowserHistory } from "history";
import createRootReducer from "../reducers";
import rootSaga from "../sagas";
import config from "../../config/base.conf";
import { authTokenMiddleware } from "../middleware/authTokenMiddleware";

export const history = createBrowserHistory();
// create the router history middleware
const historyRouterMiddleware = routerMiddleware(history);
// create the saga middleware
const sagaMiddleware = createSagaMiddleware();

// 组合middleware
const middleWares = [sagaMiddleware, historyRouterMiddleware, logger, authTokenMiddleware];

const migrations = {
  0: state => {
    return {
      ...state,
      device: undefined
    };
  },
  2: state => {
    return {
      device: state.device
    };
  }
};

const encryptor = createEncryptor({
  secretKey: "hiynn",
  onError: function(error) {}
});

const persistConfig = {
  transforms: [encryptor, immutableTransform()],
  key: config.persist,
  storage,
  version: 2,
  migrate: createMigrate(migrations, { debug: false })
};

const finalReducer = persistReducer(persistConfig, createRootReducer(history));

export default function configureStore(preloadedState) {
  const store = createStore(finalReducer, preloadedState, compose(applyMiddleware(...middleWares)));
  let persistor = persistStore(store);
  sagaMiddleware.run(rootSaga);
  return { persistor, store };
}

reducer.js

// import { combineReducers } from "redux";
import { combineReducers } from "redux-immutable";
import { connectRouter, LOCATION_CHANGE } from "connected-react-router/immutable";
import layoutPageReducer from "./layoutPageReducer";
import authReducer from "./authReducer";

export default history =>
  combineReducers({
    router: connectRouter(history),
    layoutPageReducer,
    authReducer
  });

authReducer.js

import { handleActions } from "redux-actions";
import { authTypes } from "../actions/authAction";
import moment from "moment";
import { Map } from "immutable";

const initState = Map({
  user: null,
  token: ""
});

const authReducer = handleActions(
  {
    [authTypes.AUTH_SUCCESS]: (state, action) => {
      return state.set("user", action.data.user).set("token").action.data.token;
    },
    [authTypes.SIGN_OUT]: (state, action) => {
      return state.set("user", null).set("token", "");
    }
  },
  initState
);

export default authReducer;

风不识途 收藏了文章 · 1月18日

前端下载二进制流文件

平时在前端下载文件有两种方式,一种是后台提供一个 URL,然后用 window.open(URL) 下载,另一种就是后台直接返回文件的二进制内容,然后前端转化一下再下载。

由于第一种方式比较简单,在此不做探讨。本文主要讲解一下第二种方式怎么实现。

Blob、ajax(axios)

mdn 上是这样介绍 Blob 的:

Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据

具体使用方法

axios({
  method: 'post',
  url: '/export',
})
.then(res => {
  // 假设 data 是返回来的二进制数据
  const data = res.data
  const url = window.URL.createObjectURL(new Blob([data], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}))
  const link = document.createElement('a')
  link.style.display = 'none'
  link.href = url
  link.setAttribute('download', 'excel.xlsx')
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
})

打开下载的文件,看看结果是否正确。

在这里插入图片描述

一堆乱码...

一定有哪里不对。

最后发现是参数 responseType 的问题,responseType 它表示服务器响应的数据类型,由于后台返回来的是二进制数据,所以我们要把它设为 arraybuffer
接下来再看看结果是否正确。

axios({
  method: 'post',
  url: '/export',
  responseType: 'arraybuffer',
})
.then(res => {
  // 假设 data 是返回来的二进制数据
  const data = res.data
  const url = window.URL.createObjectURL(new Blob([data], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}))
  const link = document.createElement('a')
  link.style.display = 'none'
  link.href = url
  link.setAttribute('download', 'excel.xlsx')
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
})

在这里插入图片描述

这次没有问题,文件能正常打开,内容也是正常的,不再是乱码。

根据后台接口内容决定是否下载文件

作者的项目有大量的页面都有下载文件的需求,而且这个需求还有点变态。

具体需求如下

  1. 如果下载文件的数据量条数符合要求,正常下载(每个页面限制下载数据量是不一样的,所以不能在前端写死)。
  2. 如果文件过大,后台返回 { code: 199999, msg: '文件过大,请重新设置查询项', data: null },然后前端再进行报错提示。

先来分析一下,首先根据上文,我们都知道下载文件的接口响应数据类型为 arraybuffer。返回的数据无论是二进制文件,还是 JSON 字符串,前端接收到的其实都是 arraybuffer。所以我们要对 arraybuffer 的内容作个判断,在接收到数据时将它转换为字符串,判断是否有 code: 199999。如果有,则报错提示,如果没有,则是正常文件,下载即可。具体实现如下:

axios.interceptors.response.use(response => {
    const res = response.data
    // 判断响应数据类型是否 ArrayBuffer,true 则是下载文件接口,false 则是正常接口
    if (res instanceof ArrayBuffer) {
        const utf8decoder = new TextDecoder()
        const u8arr = new Uint8Array(res)
        // 将二进制数据转为字符串
        const temp = utf8decoder.decode(u8arr)
        if (temp.includes('{code:199999')) {
            Message({
                // 字符串转为 JSON 对象
                message: JSON.parse(temp).msg,
                type: 'error',
                duration: 5000,
            })

            return Promise.reject()
        }
    }
    // 正常类型接口,省略代码...
    return res
}, (error) => {
    // 省略代码...
    return Promise.reject(error)
})

更多文章,敬请关注

查看原文

风不识途 赞了文章 · 1月18日

前端下载二进制流文件

平时在前端下载文件有两种方式,一种是后台提供一个 URL,然后用 window.open(URL) 下载,另一种就是后台直接返回文件的二进制内容,然后前端转化一下再下载。

由于第一种方式比较简单,在此不做探讨。本文主要讲解一下第二种方式怎么实现。

Blob、ajax(axios)

mdn 上是这样介绍 Blob 的:

Blob 对象表示一个不可变、原始数据的类文件对象。Blob 表示的不一定是JavaScript原生格式的数据

具体使用方法

axios({
  method: 'post',
  url: '/export',
})
.then(res => {
  // 假设 data 是返回来的二进制数据
  const data = res.data
  const url = window.URL.createObjectURL(new Blob([data], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}))
  const link = document.createElement('a')
  link.style.display = 'none'
  link.href = url
  link.setAttribute('download', 'excel.xlsx')
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
})

打开下载的文件,看看结果是否正确。

在这里插入图片描述

一堆乱码...

一定有哪里不对。

最后发现是参数 responseType 的问题,responseType 它表示服务器响应的数据类型,由于后台返回来的是二进制数据,所以我们要把它设为 arraybuffer
接下来再看看结果是否正确。

axios({
  method: 'post',
  url: '/export',
  responseType: 'arraybuffer',
})
.then(res => {
  // 假设 data 是返回来的二进制数据
  const data = res.data
  const url = window.URL.createObjectURL(new Blob([data], {type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}))
  const link = document.createElement('a')
  link.style.display = 'none'
  link.href = url
  link.setAttribute('download', 'excel.xlsx')
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
})

在这里插入图片描述

这次没有问题,文件能正常打开,内容也是正常的,不再是乱码。

根据后台接口内容决定是否下载文件

作者的项目有大量的页面都有下载文件的需求,而且这个需求还有点变态。

具体需求如下

  1. 如果下载文件的数据量条数符合要求,正常下载(每个页面限制下载数据量是不一样的,所以不能在前端写死)。
  2. 如果文件过大,后台返回 { code: 199999, msg: '文件过大,请重新设置查询项', data: null },然后前端再进行报错提示。

先来分析一下,首先根据上文,我们都知道下载文件的接口响应数据类型为 arraybuffer。返回的数据无论是二进制文件,还是 JSON 字符串,前端接收到的其实都是 arraybuffer。所以我们要对 arraybuffer 的内容作个判断,在接收到数据时将它转换为字符串,判断是否有 code: 199999。如果有,则报错提示,如果没有,则是正常文件,下载即可。具体实现如下:

axios.interceptors.response.use(response => {
    const res = response.data
    // 判断响应数据类型是否 ArrayBuffer,true 则是下载文件接口,false 则是正常接口
    if (res instanceof ArrayBuffer) {
        const utf8decoder = new TextDecoder()
        const u8arr = new Uint8Array(res)
        // 将二进制数据转为字符串
        const temp = utf8decoder.decode(u8arr)
        if (temp.includes('{code:199999')) {
            Message({
                // 字符串转为 JSON 对象
                message: JSON.parse(temp).msg,
                type: 'error',
                duration: 5000,
            })

            return Promise.reject()
        }
    }
    // 正常类型接口,省略代码...
    return res
}, (error) => {
    // 省略代码...
    return Promise.reject(error)
})

更多文章,敬请关注

查看原文

赞 15 收藏 6 评论 3

认证与成就

  • 获得 20 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-01-26
个人主页被 1.1k 人浏览