「React Practice」Base


「React Practice」系列教程是学习王沛老师的【React 实战进阶】课程的学习记录,demo 参考来自https://codesandbox.io/s/6n20nrzlxz
非原创,仅作为学习记录。

React 组件(props + state => view)

  • React 组件一般不提供方法,而是某种状态机;
  • React 组件可以理解为一个纯函数;
  • 单向数据绑定;

组件类型

  • 受控组件:表单元素状态由使用者维护;
  • 非受控组件:表单元素状态 DOM 自身维护;

创建组件,单一职责原则

  • 每个组件只做一件事;
  • 如果组件变得复杂,就需要拆分成小组件(拆分复杂度,优化性能,无需重新渲染整个大组件);

数据状态管理,DRY 原则

  • 能计算得到的状态不要单独储存;
  • 组件尽量无状态,所需数据通过 props 获取;

JSX,不是模版语言,而是语法糖

在 JavaScript 代码内直接写 HTML 标记。
本质是动态创建组件的语法糖

const name = 'htt';
const element = <h1>Hello, {name}</h1>; // 更直观

||
||
\/

const name = 'htt';
const element = React.createElement('h1', null, 'Hello, ', name);

在 JSX 内使用表达式

  • JSX 本身也是表达式
  • 在属性中使用表达式
  • 延展属性
  • 表达式作为子元素

优点

  • 声明式创建界面的直观
  • 代码动态创建界面的灵活
  • 无需学习新的模版语言

规范:自定义组件以大写字母开头

  • React 认为小写的 tag 是原生 DOM 节点
  • JSX 标记可以直接使用属性语法,例如 <menu.item />

React 生命周期

image.png
图片来源:https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
commit:React 把当前的状态映射到 DOM,实际的更新 DOM 节点。

挂载时:

  • constructor :用于初始化内部状态,较少使用;唯一可以直接赋值修改 state 的地方;
  • getDerivedStateFromProps :当 state 需要从 props 初始化时使用;尽量不要使用:维护两者状态一致性会增加复杂度;每次 render 都会调用;典型场景:表单控件获取默认值;
  • componentDidMount :UI 渲染完成后调用;只执行一次;典型场景:获取外部资源;

更新时:传入新的 props/内部 state 改变/force update;

  • shouldComponentUpdate :决定 Virtual DOM 是否需要重绘,无需变化可以返回 false;一般由 PureComponent 自动实现;典型场景:性能优化;
  • getSnapshotBeforeUpdate :在 render 之前调用,state 已更新;典型场景:获取 render 之前的 DOM 状态;注:getSnapshotBeforeUpdate() should be used with componentDidUpdate();
  • componentDidUpdate:每次 UI 更新时调用;典型场景:页面需要根据 props 变化重新获取数据;

卸载时:

  • componentWillUnmount :组件移除时调用;典型场景:资源释放;

DEMO

import React from "react";

export default class Clock extends React.Component {
  constructor(props) {
    super(props);
    console.log("Clock constructed");
    this.state = { date: new Date() };
  }

  componentDidMount() {
    console.log("Clock did mount");
    this.timerID = setInterval(() => this.tick(), 1000);
  }

  componentWillUnmount() {
    console.log("Clock will unmount");
    clearInterval(this.timerID);
  }

  componentDidUpdate() {
    console.log("Clock did update");
  }

  tick() {
    this.setState({
      date: new Date(),
    });
  }

  render() {
    return (
      <div>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
import React, { PureComponent } from "react";
import PropTypes from "prop-types";

export default class SnapshotSample extends PureComponent {
  state = {
    messages: [],
  };

  handleNewMessage() {
    this.setState((prev) => ({
      messages: [`msg ${prev.messages.length}`, ...prev.messages],
    }));
  }

  componentDidMount() {
    for (let i = 0; i < 20; i++) this.handleNewMessage();
    this.interval = window.setInterval(() => {
      if (this.state.messages.length > 200) {
        window.clearInterval(this.interval);
        return;
      }
      this.handleNewMessage();
    }, 1000);
  }
  componentWillUnmount() {
    window.clearInterval(this.interval);
  }

  getSnapshotBeforeUpdate() {
    return this.rootNode.scrollHeight;
  }

  componentDidUpdate(prevProps, prevState, prevScrollHeight) {
    const scrollTop = this.rootNode.scrollTop;
    if (scrollTop < 5) return;
    this.rootNode.scrollTop =
      scrollTop + (this.rootNode.scrollHeight - prevScrollHeight);
  }

  render() {
    return (
      <div className="snapshot-sample" ref={(n) => (this.rootNode = n)}>
        {this.state.messages.map((msg) => (
          <div>{msg}</div>
        ))}
      </div>
    );
  }
}

Virtual DOM,JSX 运行基础

工作原理

Diff:对前后两个 DOM 树进行广度优先分层比较,对节点进行一层一层的比较;复杂度为 O(n) ,否则两棵树的比较复杂度为 O(n);
针对 UI 这种树结构相对稳定的场景下不会造成较大的性能问题;

  • 组件的 DOM 结构相对稳定;
  • 类型相同的兄弟节点可以被唯一标识(key 属性,可以提高性能);

DEMO

https://supnate.github.io/react-dom-diff/index.html

高阶组件&函数作为子组件(设计模式)

实现更多场景的组件复用。

高阶组件(HOC,Higher Order Component)

接受组件作为参数,返回新的组件。

可以实现一些通用函数,但自身并不包含 UI。

// 高阶组件
import React from "react";

export default function withTimer(WrappedComponent) {
  return class extends React.Component {
    state = { time: new Date() };
    componentDidMount() {
      this.timerID = setInterval(() => this.tick(), 1000);
    }

    componentWillUnmount() {
      clearInterval(this.timerID);
    }

    tick() {
      this.setState({
        time: new Date(),
      });
    }
    render() {
      return <WrappedComponent time={this.state.time} {...this.props} />;
    }
  };
}

// 需要调用的组件
import React from "react";
import withTimer from "./withTimer.jsx";

export class showTimer extends React.Component {
  render() {
    return <h2>{this.props.time.toLocaleString()}</h2>;
  }
}
export default withTimer(showTimer); // 暴露被高阶包装过的组件

函数作为子组件

用法:

class MyComponent extends React.Component {
  render() {
    return <div>{this.props.children("HTT")}</div>;
  }
}

// 调用时
<MyComponent>{(name) => <div>{name}</div>}</MyComponent>;

Demo:

import React, { PureComponent } from "react";
import PropTypes from "prop-types";

export default class AdvancedTabSelector extends PureComponent {
  static propTypes = {
    value: PropTypes.object,
    options: PropTypes.array,
    onChange: PropTypes.func,
    children: PropTypes.func,
  };

  static defaultProps = {
    value: null,
    options: [],
    onChange: () => {},
    children: () => {},
  };

  render() {
    const { options, value, onChange } = this.props;
    return (
      <div className="tab-selector">
        <ul>
          {options.map((opt) => (
            <li
              key={opt.value}
              className={`tab-item ${
                opt.value === this.props.value ? "selected" : ""
              }`}
              onClick={() => this.props.onChange(opt.value)}
            >
              {opt.name}
            </li>
          ))}
        </ul>

        {this.props.value && this.props.children(this.props.value)}
      </div>
    );
  }
}

const colors = [
  { name: "Red", value: "red" },
  { name: "Blue", value: "blue" },
  { name: "Orange", value: "orange" },
];

const animals = [
  { name: "Tiger", value: "tiger" },
  { name: "Elephant", value: "elephant" },
  { name: "Cow", value: "cow" },
];

export class AdvancedTabSelectorSample extends PureComponent {
  state = {
    color: null,
  };
  render() {
    return (
      <div>
        <h3>Select color: </h3>
        <AdvancedTabSelector
          options={colors}
          value={this.state.color}
          onChange={(c) => this.setState({ color: c })}
        >
          {(color) => (
            <span
              style={{
                display: "inline-block",
                backgroundColor: color,
                width: "40px",
                height: "40px",
              }}
            />
          )}
        </AdvancedTabSelector>

        <h3>Select animal: </h3>
        <AdvancedTabSelector
          options={animals}
          value={this.state.animal}
          onChange={(c) => this.setState({ animal: c })}
        >
          {(animal) => (
            <img width="100px" src={require(`../../images/${animal}.png`)} />
          )}
        </AdvancedTabSelector>
      </div>
    );
  }
}

Context API(React 16.3 新特性,之前版本都是内部 API)

  • 根节点:Provider
  • 子节点:Consumer

无需手动去监听组件外部状态的变化然后去重新更新组件,context api 会自动监听状态变化。

import React from "react";

const enStrings = {
  submit: "Submit",
  cancel: "Cancel",
};

const cnStrings = {
  submit: "提交",
  cancel: "取消",
};
const LocaleContext = React.createContext(enStrings);

// 用于提供上下文数据,并切换上下文
class LocaleProvider extends React.Component {
  state = { locale: cnStrings };
  toggleLocale = () => {
    const locale = this.state.locale === enStrings ? cnStrings : enStrings;
    this.setState({ locale });
  };
  render() {
    return (
      <LocaleContext.Provider value={this.state.locale}>
        <button onClick={this.toggleLocale}>切换语言</button>
        {this.props.children}
      </LocaleContext.Provider>
    );
  }
}

class LocaledButtons extends React.Component {
  render() {
    return (
      <LocaleContext.Consumer>
        {(locale) => (
          <div>
            <button>{locale.cancel}</button> <button>{locale.submit}</button>
          </div>
        )}
      </LocaleContext.Consumer>
    );
  }
}

export default () => (
  <div>
    <LocaleProvider>
      <div>
        <LocaledButtons />
      </div>
    </LocaleProvider>
    {/* 这个组件就没有locale属性 */}
    <LocaledButtons />
  </div>
);

脚手架工具

• create-react-app(入门,小项目,简易项目)
• Rekit(基于 create-react-app 提供了更多功能,适合大项目)
• Codesandbox(在线创建)

打包和部署

打包(webpack->loader)

• 编译 ES6 语法特性,编译 JSX;
• 整合资源,例如图片,less/sass;
• 优化代码体积;
注:
• 设置 nodejs 环境为 production;
• 禁用开发专用代码,比如 logger;
• 设置应用根路径;


文章作者: 阿汪同学
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 阿汪同学 !
评论
  目录