说在最前面
本文是自己最近再学习React的总结和记录。尽力把每个示例代码都写了出来,尽力写的通俗易懂一些,也算是锻炼自己的讲述能力,据说给别人讲的话学的会更快,所以写下此文。
如有错误或者其他,还望各位批评指正
还没整理完全,趁着没开学,我会抓紧时间学习和更新
5.9更新 Saga基本使用和案例
传送门 好像没用,本地都可以.........
更新部分函数组件的用法,提供基本思路和例子
5.10更新 Hooks所学更新完毕
阿琛的公众号
JSX
简介
在JS里面写的一些标签称为JSX语法
基本语法
在返回的时候不需要加引号
import React from 'react';
import ReactDOM from 'react-dom';
import logo from './logo.svg';
// 基本
const jsx = <h1>hello react</h1>
// 变量
const name = '123456'
const jsxName = <h1>{ name }</h1>
// 函数
const user = { firstName: 'tom', lastName: 'jerry' };
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const jsxFunction = <h2>{ formatName(user) }</h2>
// 对象
const greet = <p>hello, Jerry</p>
const jsxObject = <h2>{ greet }</h2>
// 三元表达式
const showTitle = true;
const title = showTitle ? <h2>{ name }</h2> : null;
const jsxOthers = (<div>{
/* 条件语句句 */
} { title }
</div>);
// 循环
const arr = [1, 2, 3].map(num => <li key={ num }>{ num } </li>)
const jsxArray = (<div> {/* 数组 */ }
<ul>{ arr }</ul>
</div>)
// 注意
const jsxAtt = (<div>
{
/* 属性:静态值⽤用双引号,动态值⽤用花括号;class、for等 要特殊处理理。
* 像class需要写成className,for需要写成htmlFor,并且属性名需要采用驼峰命名法
* */
}
<img src={ logo } style={ { width: 100 } } className="img"/></div>)
// 函数传参
function greeting(name) {
if (name) {
return <h1>Hello, {name}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
let name2 = '测试';
const element = greeting(name2);
// 循环
let names = ['张三','李四','王五'];
let elements = [];
for(let i=0;i<names.length;i++){
elements.push(<li>{names[i]}</li>);
}
export { jsx, jsxName, jsxFunction, jsxObject, jsxOthers, jsxArray,jsxAtt,element,elements }
组件
可以将UI切分成一些独立的、可复用的部件,
class 组件
class组件通常拥有状态和生命周期,继承于Component,实现 render方法
基本
import React, { Component } from 'react';
class Home extends Component {
render() {
return <h1>Hello, Class Components</h1>;
}
}
类组件中的状态管理(秒表实例)
import React, { Component } from 'react';
class State extends Component {
// 使用state属性维护状态,在构造函数中初始化状态
constructor(props) {
super(props);
this.state = { date: new Date(), number: 0 };
// this.changeNumber = this.changeNumber.bind(this)
}
// 组件挂载时启动定时器每秒更新状态
componentDidMount() {
this.timerID = setInterval(() => {
// 使⽤用setState方法更新状态
this.setState({ date: new Date() });
}, 1000);
}
// 组件卸载时停止定时器
componentWillUnmount() {
clearInterval(this.timerID);
}
// 这里不绑定this 会报错 两种方法 第一种 用箭头函数。第二种在constructor进行绑定声明
// setState是异步执行的。执行这个结果是 log 是2不是3 证明是异步执行
changeNumber = () => {
this.setState({ number: this.state.number + 1 } )
this.setState({ number: this.state.number + 2 } )
// 这里不能进行直接修改
console.log(this.state.number)
}
// 同步改变
changeNumber2 = ()=> {
this.setState((nextState)=>{ return { number: nextState.number + 1 } } )
this.setState((nextState)=>{ return { number: nextState.number + 2 } } )
console.log(this.state.number)
// 使用定时器
// setTimeout(() => { this.setState((nextState)=>{ return { number: nextState.number + 1 } } ); console.log(this.state.counter); }, 0);
// 原生事件中修改状态
// componentDidMount(){ document.body.addEventListener('click', this.setState((nextState)=>{ return { number: nextState.number + 1 } } ), false) }
}
render() {
return (
<div>
{ this.state.date.toLocaleTimeString() }
<p>{ this.state.number }</p>
<p>setState是异步执行的</p>
<button onClick={this.changeNumber}>异步改变number</button>
<button onClick={this.changeNumber2}>同步改变number</button>
</div>
)
}
}
状态的设置和改变
在constructor
设置状态。使用setState
来改变状态。示例如上
关于setState
注意,setState是 异步的!!!!
如何证明?
见示例代码中changeNumber
的描述和实现
如何实现同步?
常见的有三种方法:
- 利用函数传递参数
- 使用定时器
- 直接找到DOM元素直接修改
上述三种方法在示例代码中都可以找到。
函数的this 绑定
一般有两种方法
- 在
constructor
里面用bind
方法绑定。例如this.changeNumber = this.changeNumber.bind(this)
- 利用ES6的箭头函数。示例
changeNumber = () => { // 执行代码 }
。(**原因解释:**箭头函数默认指向定义它时,所处上下文的对象的this指向)
function组件
基本
import React from "react";
import User from "./pages/User";
function App() {
return (
<div>
<User />
</div>
);
}
export default App;
后文的 react HOOKs
会做详细解释
组件通讯
props通讯
适用于父子组件通讯
// index.js
ReactDOM.render(<App title="测试代码" />, document.querySelector('#root'));
// App.js
<h2>{this.props.title}</h2> // 输出为 测试代码
Props 的只读性
官网原话 :组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props
状态提升
官方解释: 通常,多个组件需要反映相同的变化数据,这时我们建议将共享状态提升到最近的共同父组件中去。
实例代码
import React from 'react'
class Child_1 extends React.Component{
constructor(props){
super(props)
}
render(){
return (
<div>
<h1>{this.props.value+'child 1'}</h1>
</div>
)
}
}
class Child_2 extends React.Component{
constructor(props){
super(props)
}
render(){
return (
<div>
<h1>{this.props.value+'child 2'}</h1>
</div>
)
}
}
class Parent extends React.Component {
constructor(props){
super(props)
this.state = {
txt:"我是父组件"
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(e){
this.setState({
txt:e.target.value
})
}
render(){
return (
<div>
<input type="text" value={this.state.txt} onChange={this.handleChange}/>
<p>{this.state.txt}</p>
<Child_1 value={this.state.txt}/>
<Child_2 value={this.state.txt}/>
</div>
)
}
}
export default Parent
context 跨组件通讯
跨层级组件之间通信
单层传递
// ContextFather.js
import React from 'react';
import ContextChild from './child';
const Context = React.createContext()
const Provider = Context.Provider
const Consumer = Context.Consumer
const data = {
name: '我是father数据',
}
function ContextApp() {
return (
<div>
<Provider value={ data }>
<Consumer>
{
value => <ContextChild {...value}/>
}
</Consumer>
</Provider>
</div>
)
}
export default ContextApp
// child.js
import React, { Component } from 'react';
class ContextChild extends Component {
constructor(props) {
super(props);
}
render() {
console.log(this.props)
return (<h1>hello {this.props.name}</h1>);
}
}
export default ContextChild
这样可以获取到传递过来的name
值
多层传递
// Context.js
import React from 'react';
const Context = React.createContext()
const Provider = Context.Provider
const Consumer = Context.Consumer
export {Provider,Consumer}
// ContextFather
import React from 'react';
import ContextChild from './child';
import {Consumer,Provider} from './Context';
const data = {
name: '我是father数据',
}
function ContextApp() {
return (
<div>
<Provider value={ data }>
// 这个层级不需要就不用写 Consumer
<ContextChild/>
</Provider>
</div>
)
}
export default ContextApp
// child.js
import React, { Component } from 'react';
import ContextChild2 from './child2';
import {Consumer} from './Context';
class ContextChild extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<h1>我是第一层</h1>
// 这里获取的是一个空值,没有数据
<h2>{this.props.name}</h2>
// 这个层级需要就吧数据传递过去
<Consumer>
{
value => <ContextChild2 {...value}/>
}
</Consumer>
</div>
)
}
}
export default ContextChild
// child2.js
import React, { Component } from 'react';
class ContextChild2 extends Component {
constructor(props) {
super(props);
}
render() {
console.log(this.props)
console.log('我是第二层的')
return (
<div>
<h1>我是第二层的</h1>
<h1>{this.props.name}</h1>
</div>
);
}
}
export default ContextChild2
特别注意
- 不能有多个提供者,例如在每个文件里 写上
Context.js
里面的语句。则会获取不到数据。不同的provider提供的值不一致 - 在React的官方文档中,
Context
被归类为高级部分(Advanced),属于React的高级API,但官方并不建议在稳定版的App中使用Context。不过,这并非意味着我们不需要关注Context。事实上,很多优秀 的React组件都通过Context来完成⾃自己的功能,比如react-redux
redux 类似Vuex
没学过Vue的可以理解为全局状态管理,即你在哪里都能访问的到(我个人是这么理解的)
知识扩展
Reducer
解释: reducer 就是一个纯函数,接收旧的 state
和 action
,返回新的 state
reduce
本例子来源于MDN
const array1 = [1, 2, 3, 4];
const reducer = (total, currentValue) => total + currentValue;
// 1 + 2 + 3 + 4
console.log(array1.reduce(reducer));
// expected output: 10
// 5 + 1 + 2 + 3 + 4
console.log(array1.reduce(reducer, 5));
// expected output: 15
compose(没太理解)
compose
就是执行一系列的任务(函数)
思考
如何输出1 2 3
function f1() { console.log("1"); }
function f2() { console.log("2"); }
function f3() { console.log("3"); }
第一种
f1();f2();f3();
第二种
f3(f2(f1()))
第三种
function compose(...funcs) {
// funcs 是一个函数列表
if (funcs.length === 0) {
console.log('empty');
} else if (funcs.length === 1) {
return funcs[0];
} else {
return funcs.reduce(
(left, right) => (...args) => {
// console.log(args)
// console.log(right)
// console.log(left)
right(left(...args)) // 相当于 f3(f2(f1()))
}
);
}
}
compose(f1,f2,f3)()
Redux
使用步骤
- 需要一个
store
来存储数据 store
里reducer
初始化state
并定义state
修改规则- 通过
dispatch
一个action
来提交对数据的修改 action
提交到reducer
函数里,根据传入的action
的type
,返回新的state
实例代码
// 创建store store/ReduxStore
import { createStore } from 'redux';
// 创建数据并指定行为
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'add':
return state + 1
case 'minus':
return state - 1
default:
return state
}
}
// 创建一个store
const store = createStore(counterReducer)
export default store
// ReduxPage
import React, { Component } from 'react';
import store from '../store/ReduxStore';
export default class ReduxPage extends Component {
componentDidMount() { // 挂在之后
store.subscribe(() => { // 执行订阅
console.log('subscribe');
this.forceUpdate(); // 重新render
//this.setState({}); // 这个也可以,内部方法也是forceUpdate
});
}
// 执行行为
add = () => {
// 派发action
store.dispatch({ type: 'add' });
};
minus = () => {
store.dispatch({ type: 'minus' });
};
stayStatic = () => {
store.dispatch({ type: 'others' });
};
render() {
console.log('store', store);
return (
<div>
<h1>ReduxPage</h1>
<p>获取store数据</p>
<p>{ store.getState() }</p>
<button onClick={ this.add }>+</button>
<button onClick={ this.minus }>-</button>
<button onClick={ this.stayStatic }>+-</button>
</div>
);
}
}
注意
如果点击后 数据没有更新。则是订阅状态没有改变(也就是没有render
)
着重注意点
createStore
创建store
reducer
初始化、修改状态函数getState
获取状态值dispatch
提交更新subscribe
变更订阅
react-redux
以React
的方式写来Redux
提供了两个api
Provider
为后代组件提供store
(类似Context
)connect
为组件提供数据和变更⽅方法
react-thunk
原来的只可以同步执行,安装之后,可以进行异步操作
react-logger
只可以再开发环境下使用。用来输出各个action
store
大家会发现其实还是有 redux
的引入,说明react-redux
也是基于Redux
import { createStore, applyMiddleware,combineReducers } from 'redux';
import logger from 'import React, { Component } from 'react';
import { connect } from 'react-redux';
class ReactReduxPage extends Component {
render() {
const { num, add, minus,asyAdd } = this.props;
// console.log(this.props) // 原本这里面什么都没有,再进行connect映射后出现了
return (
<div>
<h1>ReactReduxPage</h1>
<p>{ num }</p>
<button onClick={ add }>+</button>
<button onClick={ minus }>-</button>
<button onClick={ asyAdd }>asyAdd</button>
</div>
);
}
}
const mapStateToProps = state => {
console.log(state)
return { num: state };
// return { num: state.counter };// 加了combineReducers 之后需要这么做 因为是一个对象
};
const mapDispatchToProps = {
add: () => {
return { type: 'add' };
},
minus: () => {
return { type: 'minus' };
},
asyAdd: ()=> dispatch =>{
setTimeout(()=>{
dispatch ({type: 'add'})
},1000)
}
};
export default connect(
mapStateToProps,//状态映射 mapStateToProps
mapDispatchToProps,//派发事件映射
)(ReactReduxPage)
';
import thunk from 'redux-thunk';
const counterReducer = (state = 0, action) => {
switch (action.type) {
case 'add':
return state + 1
case 'minus':
return state - 1
default:
return state
}
}
// const store = createStore(counterReducer)
// const store = createStore(combineReducers({counter: counterReducer}), applyMiddleware(logger, thunk))
const store = createStore(counterReducer, applyMiddleware(logger, thunk))
export default store
applyMiddleware
提供扩展中间件(有顺序,这个事先执行logger,在执行thunk)
combineReducers
命名空间,当你有多个store的时候,使用这个,就可以用对象的方式访问各个store
ReactReduxPage
import React, { Component } from 'react';
import { connect } from 'react-redux';
class ReactReduxPage extends Component {
render() {
const { num, add, minus,asyAdd } = this.props;
// console.log(this.props) // 原本这里面什么都没有,再进行connect映射后出现了
return (
<div>
<h1>ReactReduxPage</h1>
<p>{ num }</p>
<button onClick={ add }>+</button>
<button onClick={ minus }>-</button>
<button onClick={ asyAdd }>asyAdd</button>
</div>
);
}
}
const mapStateToProps = state => {
console.log(state)
return { num: state };
// return { num: state.counter };// 加了combineReducers 之后需要这么做 因为是一个对象
};
const mapDispatchToProps = {
add: () => {
return { type: 'add' };
},
minus: () => {
return { type: 'minus' };
},
asyAdd: ()=> dispatch =>{
setTimeout(()=>{
dispatch ({type: 'add'})
},1000)
}
};
export default connect(
mapStateToProps,//状态映射 mapStateToProps
mapDispatchToProps,//派发事件映射
)(ReactReduxPage)
当然,如果嫌弃太乱,可以单独抽离出来放。最后导入进来就可以
组件复合
这个也是官方推荐的做法: 我们推荐使用组合而非继承来实现组件间的代码重用。
学过Vue的同学,我个人理解为类似于Vue的插槽
// Layout.js
import React, { Component } from 'react';
import TabBar from '../components/TabBar';
class Layout extends Component{
constructor(props) {
super(props);
}
componentDidMount() {
const {title = '首页'} = this.props
document.title = title
}
// 兼容具名和不具名 compatible
compatibleNamed(children) {
let doc = []
if (children.?typeof) {
doc.push(children)
} else {
for (let item in children){
doc.push(children[item])
}
}
return doc
}
render() {
console.log(this.props)
return (
<div>
<h1>我是布局页面</h1>
{/* 不具名的使用*/}
{/*{*/}
{/* this.props.children*/}
{/*}*/}
{/* 这个是具名的使用*/}
{
this.props.children.btn
}
<h2>兼容过后</h2>
{this.compatibleNamed(this.props.children).map((item,index)=>{
return (
<div key={index}>
{item}
</div>
)
})}
{ this.props.showTab && <TabBar/> }
</div>
)
}
}
export default Layout
// Home.js
import React, { Component } from 'react';
import Layout from '../ComponentCombination/Layout';
class Home extends Component {
render() {
return (
<DisNamed/>
)
}
}
function Named(props) {
return (
<Layout showTab={ false } title='商城首页'>
<h1>我是首页</h1>
</Layout>
)
}
function DisNamed() {
return (
<Layout showTab={ false } title='商城首页'>
{ {
btn: <button>我是按钮</button>
} }
</Layout>
)
}
export default Home
// TabBar.js
import React, { Component } from 'react';
class TabBar extends Component{
render() {
return (
<div>
<h1>我是TabBar</h1>
</div>
)
}
}
export default TabBar
不具名
上面例子容易混淆,写一个简单点的
import React, { Component } from 'react';
import TabBar from '../components/TabBar';
class Layout extends Component{
constructor(props) {
super(props);
}
render() {
console.log(this.props)
return (
<div>
<h1>我是布局页面</h1>
{/* 不具名的使用*/}
{
this.props.children
}
{ this.props.showTab && <TabBar/> }
</div>
)
}
}
export default Layout
this.props.children
所展示的是两个Layout
之间的内容
例如
<Layout>
<h1>hello</h1>
</Layout>
this.props.children
展示的就是<h1>hello</h1>
具名
//Layout
import React, { Component } from 'react';
import TabBar from '../components/TabBar';
class Layout extends Component{
constructor(props) {
super(props);
}
render() {
console.log(this.props)
return (
<div>
<h1>我是布局页面</h1>
{/* 这个是具名的使用*/}
{
this.props.children.btn
}
{ this.props.showTab && <TabBar/> }
</div>
)
}
}
export default Layout
// Home.js
import React, { Component } from 'react';
import Layout from '../ComponentCombination/Layout';
class Home extends Component {
render() {
return (
<Named/>
)
}
}
function DisNamed() {
return (
<Layout showTab={ false } title='首页'>
{ {
btn: <button>我是按钮</button>
} }
</Layout>
)
}
export default Home
在传递具名组件的时候,需要用双{{}}
包裹。使用的就当属性使用就可以了
差异与兼容
具名 和 不具名 的差异在于传递过来的时候this.props.children
是否含有?typeof
属性。如果不含有,则是具名。反之,含有则是不具名
如何兼容?
知道了两者差异。就从差异下手
compatibleNamed(children) {
let doc = []
if (children.?typeof) { // 如果不是具名 直接放到数组里
doc.push(children)
} else {
for (let item in children){ // 如果是具名,则便利children。把里面的内容放进数组里
doc.push(children[item])
}
}
return doc // 返回的是一个数组
}
// 使用
render(){
return (
{this.compatibleNamed(this.props.children).map((item,index)=>{
return (
<div key={index}>
{item}
</div>
)
})}
}
高阶函数(柯里化)
高阶组件是一个工厂函数,它接收一个组件并返回另一个组件。
简单例子
function Child() {
return (
<div>
child
</div>
)
}
// Cmp是组件 props 是组件的props
const foo = Cmp => props =>{
return (
<div style={{boder: '1px solid #ccc'}}>
<Cmp {...props}/>
</div>
)
}
function APP() {
const Foo = foo(Child)
return (
<div>
<Foo/>
</div>
)
}
修改以前Context的例子
创建 高阶组件
import { Consumer } from './Context';
import React from 'react';
//Cum 是组件 props是组件传递的props
const HOC = Cum => props => {
return (
<Consumer>
{ value => <Cum { ...props } { ...value }/> }
</Consumer>
)
}
export default HOC
原来的 Context写法
import React, { Component } from 'react';
import ContextChild2 from './child2';
import HOC from './Hoc Hooks';
import {Consumer} from './Context';
class ContextChild extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<h1>我是第一层</h1>
<h2>{this.props.name}</h2>
<Consumer>
{
value => <ContextChild2 {...value}/>
}
</Consumer>
</div>
)
}
}
export default ContextChild
改用HOC(高阶组件)之后
// child
import React, { Component } from 'react';
import ContextChild2 from './child2';
import HOC from './Hoc Hooks';
class ContextChild extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<h1>我是第一层</h1>
<h2>{this.props.name}</h2>
{/*直接调用*/}
<ContextChild2/>
</div>
)
}
}
export default HOC(ContextChild)
// child2.js
import React, { Component } from 'react';
import HOC from './Hoc Hooks';
class ContextChild2 extends Component {
constructor(props) {
super(props);
}
render() {
console.log(this.props)
console.log('我是第二层的')
return (
<div>
<h1>我是第二层的</h1>
<h1>{this.props.name}</h1>
</div>
);
}
}
export default HOC(ContextChild2)
原本需要写Consumer
现在只需要一个函数就解决了。非常方便
详细解释
可能还是会有点迷瞪。我在详细的说一次
import { Consumer } from './Context';
import React from 'react';
const HOC = Cum => props => {
return (
<Consumer>
{ value => <Cum { ...props } { ...value }/> }
</Consumer>
)
}
export default HOC
Cum
是组件,props
是组件的所携带的参数,value
是provider
所分发的数据.
类比一下下面的.就是把组件当作了参数,通过传入一个组件,返回另一个新的组件
<Consumer>
{
value => <ContextChild {...value}/>
}
</Consumer>
完全修改后
// ContextFather.js
import React from 'react';
import ContextChild from './child';
import {Consumer,Provider} from './Context';
const data = {
name: '我是father数据',
}
function ContextApp() {
return (
<div>
<Provider value={ data }>
{/*<Consumer>*/}
{/* {*/}
{/* value => <ContextChild {...value}/>*/}
{/* }*/}
{/*</Consumer>*/}
<ContextChild/>
</Provider>
</div>
)
}
export default ContextApp
// child.js
import React, { Component } from 'react';
import ContextChild2 from './child2';
import HOC from './Hoc Hooks';
class ContextChild extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<h1>我是第一层</h1>
<h2>{this.props.name}</h2>
<ContextChild2/>
</div>
)
}
}
export default HOC(ContextChild)
// child2.js
import React, { Component } from 'react';
import HOC from './Hoc Hooks';
class ContextChild2 extends Component {
constructor(props) {
super(props);
}
render() {
console.log(this.props)
console.log('我是第二层的')
return (
<div>
<h1>我是第二层的</h1>
<h1>{this.props.name}</h1>
</div>
);
}
}
export default HOC(ContextChild2)
路由
安装
react-router包含3个库,react-router、react-router-dom和react-router-native。
react-router提供最 基本的路由功能,实际使用的时候我们不会直接安装react-router,而是根据应用运行行的环境选择安装
react-router-dom(在浏览器器中使⽤用)
react-router-native(在rn中使用)。
react-router-dom和 react-router-native都依赖react-router,所以在安装时,react-router也会⾃自动安装
npm install --save react-router-dom
基本使用
常用 BrowserRouter 、链接-Link、路由-Route、独占路由Switch、重 定向路由-Redirect
// RouterPage
import React, { Component } from 'react';
import { BrowserRouter, Link, Route,Switch } from 'react-router-dom';
import HomePage from './HomePage';
import UserPage from './UserPage';
import SearchPage from './Search';
import Login from './Login';
export default class RouterPage extends Component {
render() {
return (
<div>
<h1>RouterPage</h1>
<BrowserRouter>
<nav>
<Link to="/">首页 </Link>
<Link to="/user">用户中心 </Link>
<Link to="/login">登陆 </Link>
</nav>
{/* 根路路由要添加exact,实现精确匹配 不加这个可能会出现多次渲染 */ }
<Switch>
{/*匹配到之后就不在继续往下匹配 Switch作用*/}
<Route exact path="/" component={ HomePage }/>
{/*<Route path="/user" component={ UserPage }/>*/}
<Route path="/login" component={ Login }/>
{/*404 页面 一定要放到最后*/}
<Route component={() => <h1>404</h1>} />
</Switch>
</BrowserRouter>
</div>
);
}
}
动态路由
// RouterPage
<Link to="/search/123">详情 </Link>
<Route path="/search/:id" component={SearchPage} />
// Search
import React from 'react';
function SearchPage(props) {
console.log(props) // 有三个 match history location
return(
<div>
<p>通过这样获取到传递的参数</p>
SearchPage{props.match.params.id}
</div>
)
}
export default SearchPage
路由守卫(重点)
案例:比如你需要在登陆之后才能进入到用户页面,这时候就需要用路由守卫。
熟悉Vue的同学应该能知道我想要表达的意思
//RouterPage
...
import RouterGuard from './RoutingGuard';
...
<Link to="/user">用户中心 </Link>
<Link to="/login">登陆 </Link>
<RouterGuard path ='/user' isLogin={true} Cmp={UserPage}/>
...
RouterGuard(重点)
// RouterGuard
import React from 'react';
import { connect } from 'react-redux';
import { Route, Redirect } from 'react-router-dom';
function RouterGuard(props) {
const { Cmp, path, isLogin, ...rest } = props
console.log(rest) // 一些剩下的参数
return (
<Route
{ ...rest }
render={ prop =>{
console.log(prop) // 一些路由的参数
return isLogin ? (<Cmp { ...prop } />) :
(<Redirect to={ { pathname: '/login', redirect:props.location.pathname} }/>
)} }
>
</Route>
)
}
export default connect(
state => {
return {
isLogin: state.user.isLogin,
}
},
)(RouterGuard)
这里面主要是用了渲染函数 render
, 还有一个三元表达式
其他页面代码
// LoginStore 记得用provider 包裹APP
import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
const initialLogin = { isLogin: false, name: null };
const userInfo = (state = {...initialLogin}, action) => {
switch (action.type) {
case 'getUser':
return { ...state,isLogin: false }
case 'LoginSuccess':
return { ...state,isLogin: true,name: '阿琛' }
case 'LoginFail':
return { ...initialLogin }
default:
return state
}
}
const store = createStore(
combineReducers({
user: userInfo
}),
applyMiddleware(logger, thunk))
export default store
// Login
import React from 'react';
import { connect } from 'react-redux';
import {Redirect} from 'react-router-dom'
function Login(props) {
const {location,login,isLogin} = props
console.log(props)
const redirect = location.redirect?location.redirect:'/'
if (isLogin){
return <Redirect to={redirect} />;
}
return (
<div>
<h1>LoginPage</h1>
<button onClick={login}>登陆</button>
</div>
)
}
export default connect(state => ({
isLogin: state.user.isLogin,
}), {
login: () => {
return { type: 'LoginSuccess' };
},
},
)(Login)
React全家桶
前面介绍了好多了,接下来介绍一些还没有介绍的
redux-saga
Redux-saga是Redux的一个中间件,主要集中处理react架构中的异步处理工作,被定义为generator(ES6)的形式,采用监听的形式进行工作。
作用和react-thunk差不多。都是为了解决异步
知识基础
Generato
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函 数完全不同,详细参考参考阮一峰老师讲解
安装
npm install --save redux-saga
案例
也是一个登陆功能
// RouterPage 加上一个saga 登陆
...
<Link to="/login2">Saga登陆 </Link>
<Route path="/login2" component={ SagaLogin }/>
...
//SageLogin
import React,{useState} from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom'
// 这里用的是Hooks 写起来顺手 直接用这个了
function SagaLogin(props) {
let [name,setName] = useState('')
const { location, login, isLogin, loading,error } = props
console.log(props)
const redirect = location.redirect ? location.redirect : '/'
if (isLogin) {
return <Redirect to={ redirect }/>;
}
return (
<div>
<h1>LoginPage</h1>
<p>{name}</p>
{error && <p>{error}</p>}
<input type="text" onChange={(e)=>{setName(e.target.value)}}/>
<button onClick={ () => login(name) }>
{ loading ? '登录中...' : '登录' }
</button>
</div>
)
}
// 这里面的就不再解释了 再说react-thunk 解释过
export default connect(state => (
{
isLogin: state.user.isLogin,
loading: state.user.loading,
error: state.user.error,
}), {
// 这里调用Saga的login 函数,因为有事件监听。触发了loginHandle
login: name => ({ type: 'login', name }),
},
)(SagaLogin)
重点
//Saga 这里是把逻辑分离了出来,只是为了更好维护
import { call, put, takeEvery } from 'redux-saga/effects';
// 模拟登录接口
const UserService = {
login(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (name === '阿琛') {
resolve({ name: '阿琛' });
} else {
reject('用户名或密码错误');
}
}, 1000);
});
},
};
// worker saga
function* loginHandle(action) {
console.log('loginHandle');
try {
// 派发清求 到ReduxSage 使得loading为true
yield put({ type: 'requestLogin' });
// 执行回调函数。获取结果
const res = yield call(UserService.login, action.name);
// 把结果派发回去
yield put({ type: 'requestSuccess', res });
} catch (err) {
yield put({ type: 'requestFailure', err });
}
}
// watcher saga 事件监听 监听login事件
function* mySaga() {
yield takeEvery('login', loginHandle);
}
export default mySaga;
// ReduxSage
import { createStore, combineReducers, applyMiddleware } from 'redux';
import mySaga from './Saga';
import createSagaMiddleware from "redux-saga";
// 创建一个中间件
const sagaMiddleware = createSagaMiddleware();
// 初始化状态
const initialLogin = { isLogin: false, loading: false, name: '', error: '' };
// 定义Reducer
// 注意 必须又default 从action 里面拿到传递过来的参数
function loginReducer(state = { ...initialLogin }, action) {
switch (action.type) {
case 'requestLogin':
return { ...initialLogin, loading: true };
case 'requestSuccess':
return { ...state, isLogin: true, loading: false,name: action.name };
case 'requestFailure':
console.log(action)
return {...state,error: action.err}
default:
return state;
}
}
const store = createStore(combineReducers(
{ user: loginReducer }
),
// applyMiddleware(thunk)
applyMiddleware(sagaMiddleware)
);
// 记得运行一下
sagaMiddleware.run(mySaga)
// 如果按看上面的例子了话,记得换一下store 再index.js 里用provider 来传递store
export default store;
以下的还没有学习或者整理,最近几天会抓紧时间整理出来
Dva
Umi
mobx
生命周期(暂无整理)
React HOOKs
所学基本更新完毕,可能有漏掉的地方。还请指明
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性
解决问题
- 在组件之间复用状态逻辑很难,可能要用到render props和高阶组件,React 需要为共享状态逻辑提供更好的原生途径,Hook 使你在无需修改组件结构的情况下复用状态逻辑
- 复杂组件变得难以理解,Hook 将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
- 难以理解的 class,包括难以捉摸的
this
注意事项
- 只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
- 只能在 React 的函数组件中调用 Hook。不要在其他 JavaScript 函数中调用
useState
- useState 会返回一对值:当前状态和它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并
- useState 唯一的参数就是初始 state
计数器简单实例
import React,{useState} from 'react';
function Counter() {
// useState会返回两个,一个是当前值。一个是改变它的函数
let [state,setState] = useState({num: 0})
let [num,setNum] = useState(0)
// 两种方式 看个人喜好
return (
<div>
<p>{state.num}</p>
<p>{num}</p>
<button onClick={()=>{setState({num: state.num +1})}}>+</button>
<button onClick={()=>{setNum(num + 1)}}>+</button>
</div>
)
}
每次渲染都是独立的闭包
- 每一次渲染都有它自己的 Props and State
- 每一次渲染都有它自己的事件处理函数
- alert会“捕获”我点击按钮时候的状态。
// alter 捕获得是点击时候得状态
function Counter2 () {
// useState 返回两个,一个是当前状态的属性,一个是修改状态的方法。
let [state, setState] = useState({num: 0})
let changeLater = ()=>{ // 这个alter 会捕获当前得状态。即点击按钮时候得状态
setTimeout(()=>{
alert(state.num)
}, 2000)
}
return (
<div>
<p>{ state.num }</p>
<button onClick={ () => setState({num: state.num+1}) }>+</button>
<button onClick={ changeLater }>changeLater</button>
</div>
)
}
函数式更新(解决上例的方法)
// 改变状态注意
function Counter3 () {
// useState 返回两个,一个是当前状态的属性,一个是修改状态的方法。
let [state, setState] = useState({num: 0})
let lazyAdd = ()=>{ // 这个alter 会捕获 当前得状态。即点击按钮时候得状态
setTimeout(()=>{
setState({num: state.num+1})
}, 2000)
}
let lazyAdd2 = ()=>{ // 这个alter 会捕获之后的状态。
setTimeout(()=>{
setState((state) => ({num: state.num+1}))
}, 1000)
}
return (
<div>
<p>{ state.num }</p>
<button onClick={ () => setState({num: state.num+1}) }>+</button>
<button onClick={ lazyAdd }>lazyAdd</button>
<p>如果这样写得话,会获取到点击时候得状态在进行改变</p>
<button onClick={ lazyAdd2 }>lazyAdd2</button>
<p>这样写,改变了整个state,就可以正常相加了</p>
</div>
)
}
如果新的 state 需要通过使用先前的 state 计算得出,那么可以将函数传递给 setState。该函数将接收先前的 state,并返回一个更新后的值
惰性state
- initialState 参数只会在组件的初始渲染中起作用,后续渲染时会被忽略
- 如果初始 state 需要通过复杂计算获得,则可以传入一个函数,在函数中计算并返回初始的 state,此函数只在初始渲染时被调用
// 惰性state
// initialState 初始状态参数只会在组件初始渲染得时候调用,后续被忽略
// 且不会自己整合state,需要自己整合
function Counter4 () {
let [state, setState] = useState(function () {
console.log(' 初始化')
return {num: 0, name:'计算器'}
})
return (
<div>
<p>{state.num}</p>
<button onClick={ () => setState({num: state.num+1}) }>+</button>
<p>只会输出一次log</p>
<p>React.StrictMode 模式下会输出两次log</p>
<p>这是展示他不会整合state</p>
<p>{ state.name }:{state.num}</p>
<p>如果点击上面得button,则name会消失,下面这个则不会</p>
<button onClick={ () => setState({...state, num: state.num+1}) }>+</button>
</div>
)
}
按第一个button 计算器消失的原因: setState
不会自己整合State
,需要我们自己手动进行整合
性能优化
Object.is
MDN,使用Object.is
比较。
function Counter5 () {
let [state, setState] = useState(function () {
return {num: 0, name:'计算器'}
})
console.log('当state发生改变得时候,会重新得渲染这个函数,则会继续得输出')
console.log('Render5 ')
return (
<div>
<p>{state.num}</p>
<button onClick={ () => setState({...state, num: state.num+1}) }>输出+</button>
<p>点击这个不会输出 Render5</p>
<button onClick={ () => setState(state) }>不输出+</button>
<p>原因,内部当发现这个state没有发生改变得时候,将不再会渲染这个</p>
</div>
)
}
做缓存 CallBack
缓存函数
let lastClick;
let lastClick2;
function Counter7(callback, deps) {
let [state, setState] = useState({num: 0})
const contralAdd = ()=> setState({num: state.num+1})
console.log(lastClick === contralAdd)
lastClick = contralAdd // 一直输出false 证明不是同一个函数
/**********************************************************************************/
const contralAdd2 = useCallback(
()=> setState({num: state.num+1}), []
// 这个数组称之为依赖数组,里面放属性,属性发生变化,他就重新渲染
// 如果不写 state.num 那点击下面得按钮只会变化一次。
// 如果写了得话,则会每次改变都会重新渲染
)
console.log(lastClick2 === contralAdd2)
lastClick2 = contralAdd2 // 一直输出true 证明是同一个函数
return (
<div>
<p>{state.num}</p>
<button onClick={ contralAdd }>+</button>
<p>这样得话每次会生成一个新得函数,性能会变低</p>
<button onClick={ contralAdd2 }>+</button>
<p>这样得话更具log显示,得出是一个函数,提高性能</p>
</div>
)
}
三种结果
- 如果点击上面的 加号 log输出
lastClick === contralAdd false
lastClick2 === contralAdd2 true
原因:state.num 发生了改变,重新渲染了 Counter。所以第一个输出false。 第二个因为加了useCallback,依赖数组里面没有添加属性,导致,所以程序认为他没有发生改变,所以输出 true
-
如果点击下面的 + 号 log输出
lastClick === contralAdd false lastClick2 === contralAdd2 true
它同样会输出这个,原因,如果不写依赖属性,那点击下面得按钮只会变化一次。而数值变化了。所以第一个为true 第二个为false,并且再点击这个按钮不会再发生变化
(说的有点不准确,准确来说是发生变化了,因为闭包的原因,导致第二个函数的
state.num
一直指向初始化的0 所以才没有改变) -
添加依赖属性之后。输出结果
lastClick === contralAdd false lastClick2 === contralAdd2 false
这个不用多说了,依赖属性发生了改变。导致两个函数都重新渲染
memo
纯函数,做优化,或者叫缓存,缓存组件
import React,{memo,useMemo,useState} from 'react';
function Child() {
console.log('我是Child')
return (
<h2>我是child</h2>
)
}
function Child2(props) {
console.log('render child2')
console.log(props)
return (
<p>我是Child2{props.name}</p>
)
}
Child = memo(Child)
Child2 = memo(Child2)
function MemoApp() {
let [state,setState] = useState({num: 0})
let [name, setName] = useState('测试')
return(
<div>
<p>{state.num}</p>
<button onClick={()=>{setState({num: state.num +1 })}}>+</button>
<br/>
<input type="text" value={name} onChange={(e)=>{setName(e.target.value)}}/>
<Child/>
<Child2 name={name}/>
</div>
)
}
export { MemoApp }
如果不加上memo
,则只要点击 + 号,Child
就会发生渲染。
通俗解释:只要上次渲染结果和这次相同。就不会渲染,否则渲染
useMemo
也是做缓存用的。缓存数据
let lastData
function UseMemoApp() {
let [state, setState] = useState(0)
let [name, setName] = useState('测试')
const data = state
const data2 = useMemo(() => (state), []);
console.log('data===oldData ', data === lastData);
const addClick = useCallback(() => setState(state + 1), [state])
lastData = data;
return(
<div>
<p>{state}</p>
<p>{data2.state}</p>
<button onClick={addClick}>+</button>
<p>改变input的值,输出true 证明数据做了缓存</p>
<input type="text" value={name} onChange={(e)=>{setName(e.target.value)}}/>
</div>
)
}
上面那个userCallback
可以无视,我就是做了一个缓存。重点关注两个data
改变了input
内的值之后,log 输出true,证明数据缓存成功,数据发生改变,输出false
useReducer
- useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法
- 在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等
例子,计数器
import React, { useReducer } from 'react';
// 初始化状态
const initialState = 0;
// 初始一个reducer state 状态 action 派发状态
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { number: state.num + 1 };
case 'decrement':
return { number: state.num - 1 };
default:
throw new Error();
}
}
// 把数据变成 对象
function init(initialState) {
return { num: initialState }
}
function ReducerCounter() {
const [state, dispatch] = useReducer(reducer, initialState, init);
return (
<>
Count: { state.num }
<p></p>
<button onClick={ () => dispatch({ type: 'increment' }) }>+</button>
<button onClick={ () => dispatch({ type: 'decrement' }) }>-</button>
</>
)
}
export { ReducerCounter }
利用useReducer 写一个useState
自定义hooks
如果函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook。
这里只做一个简单解释。后面会详细介绍
function useStateSelf(initState) {
function init(initialState){
return {number:initialState};
}
// userCallback 做缓存
const reducer = useCallback((state,action) => {
console.log(action)// 看log 输出判断 返回值
console.log(state)
return action.payload // 加{} 需要 写 action.payload
// return payload// 不加{}直接写action
})
let [state,dispatch] = useReducer(reducer, initState,init)
console.log(dispatch)
function setState(payload) {
console.log(payload) // 整个对象 {number : x}
dispatch({payload})
// dispatch(payload)
}
return [state,setState]
}
// 调用
const [state, setState] = useStateSelf(initialState);
return(
<div>
<p>自定义hooks</p>
<p>{state.number}</p>
<button onClick={() => setState({number: state.number + 1 })}>+</button>
<button onClick={() => setState({number: state.number - 1 })}>-</button>
</div>
)
思路分析,首先参照useState
的使用,需要传入一个状态(本例子只考虑数值,没考虑对象)。返回一个数组,状态和改变状态的函数.
useEffect
Effect Hook
可以让你在函数组件中执行副作用操作(数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用)
它跟 class 组件中的 componentDidMount
、componentDidUpdate
和 componentWillUnmount
具有相同的用途,只不过被合并成了一个 API
通过useEffect,你可以告诉 React 组件需要在渲染后执行某些操作?
useEffect 会在每次渲染后都执行 默认情况下,它在第一次渲染之后和每次更新之后都会执行
副作用解释
副作用分为需要清除和不需要清除
- 比如发送网络请求,手动变更 DOM,记录日志,这些都是常见的无需清除的操作
- 需要清除的,例如订阅外部数据源,定时器等。这种情况下,清除工作是非常重要的,可以防止引起内存泄露!
示例代码
//useEffect.js 这样就完成了点击更改页面标题的功能 无需清除
import React, { useEffect, useState } from 'react';
function CounterEffect() {
let [state, setState] = useState(0)
useEffect(()=>{
document.title = state
})
return (
<div>
<p>{state}</p>
<button onClick={()=> setState(state+1)}>+</button>
</div>
)
}
export {CounterEffect}
// 返回一个清理函数
function CounterEffect3() {
let [state, setState] = useState(0)
useEffect(()=>{
let time = setTimeout(()=>{
setState(state +1 )
},1000)
return ()=>{
console.log('清除')
clearTimeout(time)
}
})
return (
<div>
<p>{state}</p>
<button onClick={()=> setState(state+1)}>+</button>
</div>
)
}
React 会在组件卸载的时候执行清除操作
跳过这个开始性能优化
// 跳过这个开始性能优化 做缓存
function CounterEffect3() {
let [state, setState] = useState(0)
// 这个函数会在组件更新或者挂载的时候 执行
// 第二个称之为 依赖数组。跟useCallback等作用差不多
// 如果没有给定依赖项,则仅初始调用一次,不会再调用
useEffect(()=>{
console.log('useEffect调用')
// document.title = state
}, []) // 添加依赖项就会调用 [state]
return (
<div>
<p>{state}</p>
<button onClick={()=> setState(state+1)}>+</button>
</div>
)
}
useRef
// useRef 返回一个可变的 ref 对象 通过ref来访问dom
let last;
let last2;
function Child() {
// 以前的做法
let ref = React.createRef()
console.log('last === ref',last === ref) // false 证明每次都不是一个
last = ref
// 使用hooks之后
let ref2 = useRef()
console.log('last2 === ref2',last2 === ref2) // true 证明是同一个
last2 = ref2
function getFocus() {
ref.current.focus();
}
function getFocus2() {
// `current` 指向已挂载到 DOM 上的文本输入元素
ref2.current.focus();
}
return (
<div>
<input type="text" ref={ref} />
<button onClick={getFocus}>获取焦点</button>
<br/>
<input type="text" ref={ref2} />
<button onClick={getFocus2}>获取焦点2</button>
</div>
)
}
function CounterRef() {
let [state, setState] = useState(0)
return (
<div>
<Child></Child>
<button onClick={ () => setState(state + 1) }>+</button>
</div>
)
}
由此可见,useref
还可以做缓存
useImperativeHandle
前提知识:forwardRef
如果想给一个函数组件增加ref 属性,则需要通过forwardRef方法进行包裹,又称为 转发ref
缺点:
- 用这个方法非常危险。因为破坏了封装的原则。
- 父组件可以直接操作子组件的dom
function Child2(props, ref) { // 不能不写这个props
return (
<div>
<input type="text" ref={ref} />
</div>
)
}
let ChildFor = forwardRef(Child2)
function CounterRef2() {
let ref = useRef()
function getFocus() {
ref.current.focus();
// ref.current.value = '123' // 直接可以操作
}
let [state, setState] = useState(0)
return (
<div>
<p>如果想给一个函数组件增加ref 属性,则需要通过forwardRef方法进行包裹</p>
<ChildFor ref={ref}/>
<button onClick={ () => setState(state + 1) }>+</button>
<button onClick={getFocus}>获取焦点</button>
</div>
)
}
useImperativeHandle
可以让你在使用 ref 时自定义暴露给父组件的实例值
function Child3(props, ref) { // 不能不写这个props
let refObject = useRef()
// 定义了两个方法给ref 父组件操作的实际上是这两个方法
useImperativeHandle(ref, ()=>({
focus(){
refObject.current.focus()
},
setVal(val){
refObject.current.value = val
}
}))
return (
<div>
<input type="text" ref={refObject} />
</div>
)
}
let ChildFor2 = forwardRef(Child3)
function CounterRef3() {
let ref = useRef() // 得到的是 useImperativeHandle 返回的对象
console.log(ref) // 里面有两个方法,即在child3里面定义的方法
function getFocus() {
ref.current.focus();
ref.current.value = '123' // 即使写上也不会有改变
}
function setVal(val) {
ref.current.setVal(val);
}
return (
<div>
<ChildFor2 ref={ref}/>
<button onClick={getFocus}>获取焦点</button>
<button onClick={()=>setVal(123456)}>设置值</button>
</div>
)
}
自定义Hooks
- 函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook
- 是一种复用状态逻辑的方式,它不复用 state 本身
- 每次调用都有一个完全独立的 state
import React, { useState, useEffect } from 'react';
function useBaseCounter() {
// 函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook
const [state,setState] = useState(0)
// 是一种复用状态逻辑的方式,它不复用 state 本身
// 每次调用都有一个完全独立的 state
useEffect(() => {
console.log('开启一个新的定时器')
const $timer = setInterval(()=>{
setState(state+1);
},1000);
return ()=>{
console.log('销毁老的定时器')
clearInterval($timer);
}
});
return state;
}
function CounterTest() {
let num = useBaseCounter()
return (
<>
<p>{num}</p>
</>
)
}
function CounterTest2() {
let num = useBaseCounter()
return (
<>
<p>{num}</p>
</>
)
}
export {CounterTest,CounterTest2}