React入门进阶系列二

870 阅读12分钟

自从看了React入门系列一后,MaMa再也不用担心我被T掉了,可是MaMa又开始担心我的发型了,请问掘友们,该怎么办?

此篇为React入门+进阶系列二,在这里将进一步学习React以及了解相关生态知识

Fragments

React中的一个常见模式是一个组件返回多个元素。Fragments 允许你将子列表分组,而无需向 DOM 添加额外节点

问题产生

就像我们在Vue中的template下书写组件一样,往往需要额外添加一个顶层元素div作为根元素来包裹template标签下的元素,因为组件只能有一个根元素,也就是说<template></template>下面必须只有一个根元素。template自身没有很特别的意义,我们可以打开浏览器控制台输入查看 react 的虚拟 dom 元素,我们会发现$r.render()渲染的元素是一个js对象,这也就是为什么react 中要用一个根元素包裹下面的元素。

// 假设这是vue中的table组件 --- table.vue
<template>
    <div class="root">
       <!-- some elements -->
    </div>
</template>

React也类似的给我们提供了一个Fragments,以便于我们拆分组件元素,而向 DOM 添加额外节点。

class Table extends React.Component {
  render() {
    return (
      <table>
        <tr>
          <Columns />
        </tr>
      </table>
    );
  }
}
class Columns extends React.Component {
  render() {
    return (
      <div>  {/*额外的div元素会被渲染到table中,这是我们不想要的结果*/}
        <td>Hello</td>
        <td>World</td>
      </div>
    );
  }
}
//问题:最终经过ReactDOM.render()渲染后会得到
<table>
  <tr>
    <div>  {/* 额外的div */}
      <td>Hello</td>
      <td>World</td>
    </div>
  </tr>
</table>

采用Fragment解决

class Columns extends React.Component {
  render() {
    return (
      <React.Fragment>  
      {/*如果通过import导入了Fargment组件,可以直接使用<Fragment></Fragment>*/}
        <td>Hello</td>
        <td>World</td>
      </React.Fragment>
    );
  }
}

短语法

//它不支持 key 或属性。
class Columns extends React.Component {
  render() {
    return (
      <>
        <td>Hello</td>
        <td>World</td>
      </>
    );
  }
}

key值

key 是唯一可以传递给 Fragment 的属性

将一个集合映射到一个 Fragments 数组,如果没有绑定key值,React 会发出一个关键警告 ,这和vue中的v-for类似,便于VNode对比,优化性能


 return (
    <dl>
      {props.items.map(item => (
        <React.Fragment key={item.id}>
          <dt>{item.term}</dt>
          <dd>{item.description}</dd>
        </React.Fragment>
      ))}
    </dl>
  );

组合组件

使⽤组件的⽬的就是通过构建模块化的组件,相互组合组件最后组装成⼀个复杂的应⽤,优秀的开发者往往需会首先考虑设计然后代码实现,组件抽象化之后,便可避免将高级组件和低级组件混合在一起,好像有本书就叫《react设计模式与最佳实践》

如: 一个计数器应用,我将计数部分拆分成一个单独的组件counter,然后在counters组件中使用

//counter组件
return (
  <React.Fragment>
    <h2>{this.props.id}</h2>
    {this.props.children}
    <span style={this.styles} className={this.gerBadgeClass()}>
      {this.formatCount()}
    </span>
    <button
      className="btn btn-secondary btn-sm"
      onClick={() => this.handleIncrement({ num: 1 })}
    >
      Increment
    </button>
  </React.Fragment>
);

//counters组件中存放counter组件
import Counter from "./counter";
class Counters extends Component {
  state = {
    counters: [
      { id: 1, value: 1 },
      { id: 2, value: 2 },
      { id: 3, value: 3 },
      { id: 4, value: 4 }
    ]
  };
  render() {
    return (
      <React.Fragment>
        {this.state.counters.map(counter => (
          <Counter
            key={counter.id}
            value={counter.value}
            selected
            id={counter.id}
          >
          </Counter>
        ))}
      </React.Fragment>
    );
  }
}

无状态功能性组件

用大白话说,就是没有state,没有事件,也没有什么方法,只是单纯的调用 props 返回元素(组件),这就是 无状态功能性组件

//此时导航栏是一个类组件
class Navbar extends Component {
  state = {};
  render() {
    return (
      <nav className="navbar navbar-light bf-light">
        <a href="#" className="navbar-brand">
          totalCounters
          <span className="badge badge-pill badge-secondary">
            {this.props.totalCounters}
          </span>
        </a>
      </nav>
    );
  }
}

//转化为无状态功能性组件,采用函数式书写,相比采用class,效率会高很多
const Navbar = props => {
  return (
    <nav className="navbar navbar-light bf-light">
      <a href="#" className="navbar-brand">
        totalCounters
        <span className="badge badge-pill badge-secondary">
          {props.totalCounters}
        </span>
      </a>
    </nav>
  );
};

export default Navbar;

小技巧:在书写某个组件的时候,你知道这个组件是无状态功能性组件的时候,那它他是不会有任何本地的状态,我们需只通过props传递他所需要用到的数据

//假设App组件是一个无状态功能性组件
import React from "react";
const App = () => {
  return null;
};

export default App;

组件之间通信

1. 父子组件通信

通过 props 属性传递,在⽗组件给⼦组件设置props ,然后⼦组件就可以通过 props 访问到⽗组件的数据/⽅法

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: this.props.name //将props存入state
    };
  }
  render() {
    return (
      <div>
        <h3>{this.props.name}</h3>
        <h3>{this.state.name}</h3>
      </div>
    );
  }
}

// 传递props到组件内部
ReactDOM.render(
  <App name="这是我自定义的props内容" />,
  document.getElementById("root")
);

2. 非父子组件通信

Pub/Sub 模式

使⽤全局事件 Pub/Sub 模式,在 componentDidMount ⾥⾯订阅事件,在componentWillUnmount ⾥⾯取消订阅,当收到事件触发的时候调⽤setState 更新 UI

Flux单项数据流架构

上面的Pub/Sub模式在复杂的系统⾥⾯可能会变得难以维护,⼀般来说,对于⽐较复杂的应⽤,推荐使⽤类似 Flux 这种单项数据流架构(此处不深讲Flux的架构,由于此文只是学习react相关内容,所涉及到的周边生态不做过多介绍,我会在下一级标题做简单的概括,读者如需了解,可自行科学上网

Flux

Data Flow 只是⼀种应⽤架构的⽅式,⽐如数据如何存放,如何更改数据,如何通知数据更改等等,所以它不是 React 提供的额外的什么新功能,可以看成是使⽤ React 构建⼤型应⽤的⼀种最佳实践

Flux架构

React强调⾃⼰是MVC架构⾥⾯View层,那么Flux就相当于添加M和C的部分。Flux是Facebook使⽤的⼀套前端应⽤的架构模式,包括以下四个部分:

actions

提供给 dispatcher 传递数据给 store

dispatcher

处理动作分发,维护 Store 之间的依赖关系

stores

数据和逻辑部分

views

React 组件,这⼀层可以看作 controller-views,作为视图同时响应⽤户交互

核心点

Flux的核⼼:单向数据流,所有的状态都由 Store 来维护,通过 Action 传递数据,构成了如上所述的单向数据流循环,所以应⽤中的各部分分⼯就相当明确,⾼度解耦了,这种单向数据流使得整个系统都是透明可预测的

  1. 创建Action 通过定义⼀些 action creator ⽅法根据需要创建 Action提供给 dispatcher

  2. 触发Action View 层通过⽤户交互(⽐如 onClick)会触发 Action

  3. Dispatcher分发Action Dispatcher 会分发触发的 Action 给所有注册的 Store 的回调函数

  4. Store接受Action Store 回调函数根据接收的 Action 更新⾃身数据之后会触发⼀个 change事件通知 View 数据更改了

  5. View更新组件 View 会监听这个 change 事件,拿到对应的新数据并调⽤ setState更新组件 UI

Redux

Redux(oldState) => newState Redux是可预测的状态容器,也是借鉴Flux里面的思想,只是它充分利⽤函数式的特性,让整个实现更加优雅纯粹,使⽤起来也更简单。可以暂且理解为Flux的升级版 传送门(关于函数式编程范式,读者可私下自行了解)

基本原则

  1. 唯一数据源

    所有的组件数据源都是这个 store 树形对象上的一部分数据

  2. 保持状态只读

    要修改 store 只能通过派发一个 action 对象完成,而不能直接修改 state

    redux是通过reducer返回一个新的state对象给redux,进而完成新的状态组装,其本质就是发布订阅模式

  3. 数据改变只能通过纯函数完成

    reducer 纯函数就是根据 state 和 action 的值产生一个新的对象返回给 store

PropTypes类型检查

自 React v15.5 起,React.PropTypes 已移入另一个包中。请使用 prop-types 库 代替。 传送门

要在组件的 props 上进行类型检查,你只需配置特定的 propTypes 属性,通过类型检查捕获大量错误,当然,我们可以使用Flow或者TypeScript来编写应用

import PropTypes from 'prop-types';

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}

Greeting.propTypes = {
  name: PropTypes.string
};

出于性能方面的考虑,propTypes 仅在开发模式下进行检查

/*
JS原生类型
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol
......
*/

defaultProps

class Greeting extends React.Component {
  render() {
    return (
      <h1>Hello, {this.props.name}</h1>
    );
  }
}
// 指定 props 的默认值:
Greeting.defaultProps = {
  name: 'Stranger'
};

Refs

Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素

实际开发中,尽量不要获取真实dom,除非是你需要做很复杂的动画时,不可避免的要去操作;同时需注意,在获取dom语法时,必须放在setState第二个参数回调中 ,因为setState是一个异步函数,有时候会导致页面渲染不及时

Context

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

原本的react应用中,数据是通过props属性自上而下进行传递的,也就是父组件到子组件。 Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。

生命周期

组件一般会经历以下几个状态

  • Mount 此状态,组件被实例化并创建到 DOM 中,可以加入组件的特殊方法,react 会自动调用这些方法,这写方法我们叫做生命周期钩子,允许我们勾住某个特定的时刻。
  //此状态按顺序调用以下三个方法
  constructor;
  render;
  componentDidMount;
  • Update 这个状态发生在 state 或者组件的 props 改变时,一次调用
//此状态按顺序调用以下两个方法
render;
componentDidUpdate;
  • unmounting 当组件从 DOM 中移出,比如删除
componentWillUnmount;

挂载阶段

  1. constructor 只会调用一次,所以是设置属性的最佳时机
  2. render 把组件渲染到虚拟 DOM 中,react 拿到之后就输出为浏览器的真实 DOM
  3. componentDidMount 钩子在组件被加入 DOM 之后调用,此时组件已存在与 DOM 中了。这是 ajax 请求的好时机
不能在无状态功能性组件中使用生命周期钩子,除非使用类组件

更新阶段

整个组件树被渲染,并不意味着整个都 DOM 更新了。当组件渲染时,实际上得到的是一个 react 元素,这个元素更新了虚拟 DOM,react 会拿到新的虚拟 DOM 和旧的虚拟 DOM 做对比,这就是 react 为什么不直接更新页面 DOM,而是在内存中比较这两个虚拟 DOM 对象?react 会有意识的知道哪些组件被修改,使用 diff 算法,针对性的更新真实的 DOM

class App extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (prevProps.counter.value !== this.props.counter.value) {
      //发送ajax请求更新数据
    }
  }
}

卸载阶段

一个组件从 DOM 卸载的时候调用

class App extends React.Component {
  componentWillUnmount() {
    //如果你设置了计时器或者监听器,可以在组件移除之前做一些清除工作,同时做一些内存优化
  }
}

注意点

在组件更新阶段,需注意props和state的shouldComponentUpdate生命钩子,当一个子组件从父组件接受了参数,只要父组件的render函数被执行,子组件这个生命周期就会执行。所以shouldComponentUpdate存在的意义就是,询问你的组件需要更新吗,此函数要求返回一个布尔值,当为true时,才会执行componetWillUpdate生命钩子,

路由react-router-dom

react本身只是关注页面渲染的库,使用路由我们需要使用react-router-dom

安装react-router-dom

cnpm install react-router-dom --save

使用

import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom"; //导入BrowserRouter组件
import "./index.css";
import App from "./App";
import registerServiceWorker from "./registerServiceWorker";
ReactDOM.render(
  {/*包裹根组件*/}
  <BrowserRouter> 
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);
registerServiceWorker();
//使用路由组件
import {Route} from 'react-router-dom'  //导入Route组件
<div className="content">
    <Route path="/" component={Home} />
    <Route path="/products" component={Products} />
    <Route path="/posts" component={Posts} />
    <Route path="/admin"  component={Dashboard} />
</div>
</Switch>

问题

router的匹配算法是检测当前的地址是否以给定的字符串开始,如果匹配正确,则渲染component对应的组件,所以在上面路由中,我们请求/products的时候也会匹配到首页/这个路径,所以这时候 首页组件Home会和当前路由匹配的组件会同时出现在页面上,解决办法:

  • 方法一:exact
    <Route path="/"exact component={Home} />
  • 方法二:Switch会匹配一条符合的子规则
//导入Switch组件
import {Route,Switch} from 'react-router-dom'  
 <Switch>
    <Route path="/" component={Home} />
    <Route path="/products" component={Products} />
    <Route path="/posts" component={Posts} />
    <Route path="/admin"  component={Dashboard} />
  </Switch>

采用Link组件转为单页面跳转

//原本navbar.jsx 
const NavBar = () => {
  return (
    <ul>
      <li>
        <a href="/">Home</a>
      </li>
      <li>
        <a href="/products">Products</a>
      </li>
      <li>
        <a href="/posts/2018/06">Posts</a>
      </li>
      <li>
        <a href="/admin">Admin</a>
      </li>
    </ul>
  );
};

//link替换a标签。href改为to,点击路由页面信息都在bundle文件中,这时候就不会刷新页面了

import { Link } from "react-router-dom";
const NavBar = () => {
  return (
    <ul>
      <li>
        <Link to="/">Home</Link>
      </li>
      <li>
        <Link to="/products">Products</Link>
      </li>
      <li>
        <Link to="/posts/2018/06">Posts</Link>
      </li>
      <li>
        <Link to="/admin">Admin</Link>
      </li>
    </ul>
  );
};

/*
Link组件是如何模拟实现的?

Link下是a标签,有各自的onClick=fn(),阻止默认行为,这就是浏览器没发起请求的原因
取而代之,只会更新地址栏的url,路由就会匹配url,此时就会渲染新的组件
*/

后续学习

  • react中

  • 组件库使用 Antd

  • 数据状态管理框架 Redux, Mobx ,Dva等等