React思想

始终整体“刷新”页面,无需关心细节。

React特点

  • 1个新概念

组件的方式去描述UI

  • 4个必须API

    • ReactDOM.render 方法让 React 组件渲染到某个具体的 DOM 节点
    • 组件的 render 方法,渲染对应的组件
    • 组件的 setState 方法,用于改变组件状态,触发组件的render方法重新渲染组件
    • 组件之间可以通过 props 给 React 组件传递参数(数据只能从上往下流动)
  • 单向数据流

  • 完善的错误提示

React解决了UI细节问题,数据模型如何解决?

image.png
image.png

react组件的概念

有属性和状态最终组成一个View。一个组件的状态有2种,一个是外部传过来的属性,一个是内部维护的状态。
所以组件可以理解为一个纯函数,输入的参数一样,渲染的UI结果一定一样。
image.png

何时创建组件?单一职责原则

  • 每个组件只做一件事
  • 如果组件变得复杂,那么应该拆分成小组件。拆分成小组件有2个好处:

    • 组件发复杂度分散出去
    • 更为重要:性能问题。当一个组件很大的时候,任何一个状态变化都需要刷新,但是如果组件拆分足够细的化,很多组件都不会刷新,只有对应的组件刷新。

      数据状态管理:DRY原则

  • 能计算得到的状态就不要单独存储。计算过程的数据不要存state,而是用的时候直接计算。比如,ajax请求的数据,未返回可以根据未返回数据判断是否loading,而不是存一个状态。

  • 组件尽量无状态,所需数据通过props获取。让你的组件尽量是一个纯组件,这样可以具备更好的性能,可以更好的被重用。

JSX:不是模板语言,只是一种语法糖

  • 在JavaScript代码中直接写HTML标记
  • JSX的本质:动态创建组件的语法糖(纯JS语法,并不是模板语言)

image.png

  • 在JSX中使用表达式

image.png

  • JSX语法约定:自定义组件以大写字母开头

    • React认为小写的tag是原生DOM节点,比如div
    • 大写字母开头为自定义组件
    • JSX标记可以直接使用属性语法,例如,属性语法不需要遵循大写约定

      React的生命周期及其使用场景

  • react ^16.3版本

image.png

  • react ^16.4版本

image.png
图片来源:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
上述俩个版本的区别在于getDerivedStateFromProps, 其方法的介绍参考如下文章:https://juejin.cn/post/6844903857550688269

三个阶段

  • Render阶段:用于计算一些当前的状态
  • Pre-commit阶段:没有真正的更新DOM,但是可以读取DOM节点的内容
  • Commit阶段:React把当前的状态映射到DOM的时候,需要实际的更新DOM节点

    三个类型

  • 创建时

    • constructor:一个组件的构造函数,也就是说一个组件更新到界面之前,它需要构造创建出来
      • constructor就是js标准的类的构造函数,用于初始化内部的状态,但其实在React内是很少使用构造函数内的方法的,因为React需要做一些初始化的事情可能会在其他的生命周期中去做
      • 唯一可以修改state的地方,其他地方修改state都需要调用setState方法
    • getDerivedStateFromProps:这个是react^16.3版本新引入的一个API,它用于从外部的属性去初始化内部的一些状态
      • 当state需要从props初始化时使用
      • 尽量不要使用:维护俩者状态一致性会增加复杂度。因为如果你需要从props获取值,你可以直接计算获取,而不需要在state里面存储,因为一旦存储就需要维护俩者的一致性,会增加复杂度,容易出bug。(题外话:react维护者调侃说,之所以命名这么长就是不推荐大家使用)
      • 每次render都会调用(替代原有的componentWillReceiveProps,也是这个机制)
      • 典型场景:表单控件获取默认值。表单除了用户输入值外,还会有一个初始的默认值,一旦用户修改之后就不再有用了。一开始的内部state是来自外部的初始值
    • render:描述UI的DOM结构的地方,也是组件唯一需要必须定义的一个生命周期方法,因为你需要描述你的UI
    • componentDidMount:发起AJax请求,定义一些外部的资源等,做一些副作用的事情
      • UI渲染完成后调用:这个时候知道所有的UI渲染完成了,在这里可以安全的操作DOM节点以及触发一些ajax请求,获取一些外部的资源
      • 这个方法在整个生命周期中只执行一次,如果你的组件只获取一次外部资源,就在这里执行
      • 典型场景:获取外部资源
  • 更新时(发生场景:组件有一个新的属性传进来;内部修改了state,修改内部状态调用setState;调用forceUpdate时,即使内部状态没有发生改变,依然想更新UI,调用setState强制组件刷新)

    • getDerivedStateFromProps:这个是react^16.3版本新引入的一个API,它用于从外部的属性去初始化内部的一些状态
    • shouldComponentUpdate:告诉组件你是否真的需要render,这是一个用户介入的过程,这里可以做一些性能优化的事情,返回false不需要render,返回true需要render,继续下面的流程
      • 决定Virtual DOM是否要重绘
      • 一般可以由PureComponent自动实现
      • 典型场景:性能优化
    • render:计算虚拟dom,虚拟dom用来维持UI的状态,从而进行一些diff计算
    • getSnapshotBeforeUpdate:React^16.3引入的API
      • 在页面render之前调用,state已经更新
      • 典型场景:获取render之前的DOM状态
    • React更新DOM和refs(引用):这个周期不需要我们关心
    • componentDidUpdate
      • 每次UI更新时被调用
      • 典型场景:页面需要props变化重新获取数据
  • 卸载时
    • componentWillUnmount
      • 组件移除时被调用
      • 典型场景:资源释放

React基础

理解Virtual DOM及key属性的作用

JSX的运行基础:Virtual DOM
image.png
计算2棵树的差异,标准运算的复杂度是o(n)^3,运算量比较大。这么搞得复杂度,性能不满足UI的性能需求。
Facebook工程师做了一个算法的优化,复杂度降到了o(n),完全变成了线性的算法。
image.png
拿到前后俩个状态的DOM树之后,一层一层的进行比较。

组件设计模式:高阶组件和函数组件作为子组件

  • 高阶组件
    • 对已有组件进行封装,形成一个新的组件。新的组件会包含一些自己的应用逻辑,这个逻辑会产生一些新的状态,这个状态会传给已有的组件
    • 高阶组件一般不会有自己的UI展现,只是为它封装的组件提供一些额外的功能或者数据

image.png
请注意,HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用。
withTimer.js

  1. export default function withTimer(WrappedComponent) {
  2. return class extends React.Component {
  3. state = {time: new Date()};
  4. componentDidMount() {
  5. this.timerID = setInterval(()=>this.tick(),1000);
  6. }
  7. componentWillUnmount() {
  8. clearInterval(this.timerID);
  9. }
  10. tick() {
  11. this.setState({
  12. time: new Date()
  13. });
  14. }
  15. render() {
  16. return <WrappedComponent time={this.state.time} {...props} />;
  17. }
  18. }
  19. }

ChatApp.js

  1. import React from "react"
  2. import withTimer from "../withTimer"
  3. class MessageList extends React.PureComponent {
  4. render() {
  5. return <ul>{this.props.messages.map(msg => <li>{msg}</li>)}</ul>;
  6. }
  7. }
  8. export class ChatApp extends React.Component {
  9. state = {
  10. messages: [],
  11. inputMsg: "",
  12. };
  13. handleInput = evt => {
  14. this.setState({
  15. inputMsg: evt.target.value,
  16. });
  17. };
  18. handleSend = () => {
  19. const text = this.state.inputMsg;
  20. if (text) {
  21. const newMessages = [...this.state.messages, text];
  22. this.setState({
  23. messages: newMessages,
  24. inputMsg: "",
  25. });
  26. }
  27. };
  28. render() {
  29. return (
  30. <div>
  31. <MessageList messages={this.state.messages} />
  32. <div>
  33. <input value={this.state.inputMsg} />
  34. <button onClick={this.handleSend}>Send</button>
  35. </div>
  36. <h2>{this.props.time.toLocaleString()}</h2>
  37. </div>
  38. );
  39. }
  40. }
  41. export default withTimer(ChatApp);

注意上文代码里的state并没有放到constructor里面,因为messages和inputMsg是不需要通过初始化赋值。
实例的属性,变量初始值需要在new的时候初始化,就放在constructor里面。

  1. class People {
  2. age =0 ;
  3. constructor(name) {
  4. this.name = name;
  5. }
  6. }
  7. const Erina = new People('erina');

image.png
知识扩展:

  • 函数作为子组件
    • 新的设计模式,不是react自身的特性
    • 下图代码,不是把一个节点作为children,而是把一个函数作为children

image.png“函数作为子组件” 是一个组件,这个组件接收一个函数作为其子组件。由于 React 的属性(property) 类型,该模式可以简单地实现并且值得推广。
JavaScript代码:

  1. class MyComponent extends React.Component {
  2. render() {
  3. return (
  4. <div>
  5. {this.props.children('erina')}
  6. </div>
  7. )
  8. }
  9. }
  10. MyComponent.propTypes = {
  11. children: React.PropTypes.func.isRequired
  12. };

注: 从 React v15.5 开始 ,React.PropTypes 助手函数已被弃用,我们建议使用 prop-types 库 来定义contextTypes。 即你需要手动引入 import PropTypes from 'prop-types';
这就函数作为子组件!通过使用函数作为子组件,我们将父组件和子组件分离,让使用者决定如何将参数应用于子组件。例如:
JavaScript 代码:

  1. <MyCompnent>
  2. {(name) => (
  3. <div>{name}</div>
  4. )}
  5. </MyCompnent>