React 基础

从原生到 React

原生例子

一个 demo,如何用原生的 JavaScript 实现一个点赞按钮(即简单的点击一次更换一次图片)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 将点赞按钮可复用地组件化
class LikeStar {
  constructor() {
    this.state = { isLiked: false; }
    this.redStarSrc = URL_OF_RED_STAR;
    this.whiteStarSrc = URL_OF_WHITE_STAR;
  }
  
  changeStar = () => {
    this.state.isLiked = !this.state.isLiked;
    const imgSrc = this.state.isLiked ? this.redStarSrc : this.whiteStarSrc;
    const $star = this.el.querySelector('.js-star');	// 其中 js-star 是 img 标签的一个 class
    $star.setAttribute('src', imgSrc);
  }
  
  render() {
    this.el = createDOMFromString(`<img class="js-star"> src="${this.whiteStarSrc}"`);
    this.el.addEventListener('click', this.changeStar, false);
    return this.el;
  }
}

// 如何使用刚才定义的 LikeStar 类
$wrapper.apppendChild(new LikeStar().render());

上面的例子使用了 setAttributecreateDOMFromStringaddEventListener 这些原生的 js 方法,实现了一个 demo。

原生存在的问题

上面直接使用原生的方法实现的例子存在这样一些问题:

  1. 复用性不是特别强:组件没有提供传入参数定制化的能力,复用性不强。
  2. DOM 操作与组件方法耦和在一起:当我们的组件越来越复杂时,需要在组件内维护 DOM 的成本会越来越大,同时直接操作 DOM 也会有许多性能问题;它同时也是复用性不强的一种具体体现;

第一个问题是比较好解决的,直接在 class 的构造方法中加入一个 props 参数即可。

对于第二个问题,我们则需要通过一定的抽象,将与 DOM 相关的操作从 LikeStar 中剥离出来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class DOMOperator {
  constructor(props) {
    this.state = {};
    this.props = props || {};
  }
  setState = state => {
    this.state = state;
    const oldEle = this.el;
    this.el = this.renderDOM()
    this.updateUI && this.updateUI(oldEle, this.el);
  }
  renderDOM = () => {
    this.el = createDOMFromString(this.render());
    this.onClick && this.el.addEventListener('click', this.onClick, false);
    return this.el;
  }
}

class LikeStar extends DOMOperator {
  constructor(props) {
    super(props);
    this.state = { isLiked: false; }
  }
  onClick = () => {
    this.setState({ isLiked: !this.state.isLiked })
  }
  render() {
    const { isLiked } = this.state;
    const { className } = this.props;
    const imgSrc = isLiked ? URL_OF_RED_STAR : URL_OF_WHITE_STAR;
    return createDOMFromString(`<img class="js-star ${className}"> src="${imgSrc}"`);
  }
}

const renderDOM = (instance, $parentDOM) => {
  instance.updateUI = (oldEle, newEle) => {
      $parentDOM.insertBefore(newEle, oldEle);
		  $parentDOM.removeChild(oldEle);
  }
  $parentDOM.appendChild(instance.renderDOM());
}
renderDOM( new LikeStar(), $wrapper );

上面的例子通过将 setStateupdateDOM 沉淀到基类中实现了一个 DOM 与组件隔离的设计模式。对于上面的设计模式,我们可以认为 UI 的一个更新流程的抽象过程:

  • 更新状态 setState => 更新抽象的 DOM => 更新 UI;

React 例子

同样的例子,用 React 是如何实现的呢?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
class LikeStar extends React.Component {
  constructor(props) {
    super(props);
    this.state = { isLiked: false };
  }
  onClick = () => {
    this.setState(prev => ({ isLiked: !prev.isLiked }));
  }
  render() {
    const { isLiked } = this.state;
    const { className } = this.props;
    const imgSrc = isLiked ? URL_OF_RED_STAR : URL_OF_WHITE_STAR;
    return (
      <img className={'heart ' + className} onClick={this.onClick} src={imgSrc} />
    );
  }
}

而最后抽象出来的 renderDOM 方法则是由一个叫 ReactDOM 的第三方库实现的:

1
ReactDOM.render( <LikeStar />, $wrapper );

可以看到这个例子就与我们在第二个部门中优化后的代码几乎是一致的了,也就是说,React 相对于原生代码解决了以下的问题:

  1. 使用面向对象的组件化的方式项目化地组织代码结构;
  2. 通过使用 propsstate 这两个核心技术,实现组件的多态性,提高代码的复用性;
  3. 将 DOM 操作与组件的逻辑通过 setState 这个方法解耦,这个是 React 设计的核心优点,它有以下好处:
    1. 解决了当组件过于庞大时大量处理 DOM 的问题,提高了代码的维护性;
    2. React.js 底层通过虚拟 DOM 的方式提高了 DOM 渲染的性能;
    3. 提供了一个组件的设计范式,优化了代码的编写方式;
  4. 通过定义 jsx 语法,实现 js/html 的高效嵌套编写(本质是 React.createElement 的语法糖);

简介

什么是 React.js?

  • React 是一个声明式,高效且灵活的用于构建用户界面的 JavaScript 库。
  • 不同于框架,框架提供了一整套的解决方案,而 React 的定位只是一个轻量库。
  • React.js 只有与 Reduxreact-router 合起来才能称为一个框架。

React.js 的特点:

  1. 声明式的视图层:声明式侧重于描述一个组件的特点,而传统的命令式则侧重于具体的实现过程;

  2. 简单的更新流程:只需要调用 setState 即可,也就说 React 实现了数据更新到 UI 更新的单向更新机制

  3. 灵活的渲染实现:React.js 通过虚拟 DOM 作为视图组件到 UI 的中介,React 不关心 虚拟 DOM 到更新 UI 的具体实现,这一过程需要通过第三方库具体地实现。比如:react-dom 用于浏览器渲染、React-Native 用于手机终端渲染;

  4. 高效的 DOM 操作:只操作 虚拟 DOM(一个 JavaScript 对象) 而非具体的 DOM,优异的 DOM diff 算法。

基本概念

jsx 语法

标签类型:

  • DOM 类型标签(首字母必须小写开头):React.js 为了优化开发体验,使用了原生的 DOM 标签进行定义。实际上在 React.js 底层进行了一层处理;
  • React 组件类型标签(首字母必须大写开头):jsx 使用不同的处理方式,因此需要于上者严格区分。

jsx 中的 JavaScript 表达式:

  • 可以通过表达式给标签赋值,可以通过表达式定义子组件(比如定义循环);
  • 需要注意的一点是 jsx 是不支持多行表达式的,因此需要将表达式抽象成一个方法出来;

DOM 标签属性:

  • class => className:为了避免与 ES6 的 class 发生冲突,DOM 中的 class 关键字在 React 中被命名为 className
  • onclick => onClick:为了保持编码风格的一致性,都改名为驼峰的命名方式,同理还有 onFocusonBlur 等;
  • 自定义标签属性:取决于组件的 propsPS:在 jsx 中使用 'str'{'str'} 定义字符串都是合法的,建议使用前者;

组件

组件的类型:

  • 类组件(使用 ES6 的 class 语法),函数组件(接收 props 作为参数,返回一个 ReactNode,注意首字母需要大写);
  • 建议有状态组件使用类组件的定义方式,无状态组件使用函数组件的定义方式。

props 属性的校验:

  • React.Component 支持对 props 进行校验(校验结果是在控制台抛出 warning),校验的方式是通过定义一个 static 的成员对象 propTypes,React 支持的所有组件都定义在一个叫 prop-types 的第三方库中。
  • 另外,我们还可以通过定义 static 的成员对象 defaultProps 给每个 prop 赋予一个默认值。

下面给出一个 props 属性校验的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import PropTypes from 'prop-types'

class CommentItems extends React.Component
{
  static propTypes = {
    userImg: PropTypes.string.isRequired, // 必选字符串项
    likeIcon: PropTypes.number,						// 数字类型
  }
	static defaultProps = { userImg: '', likeIcon: 0, }
}

state 更新:

  • state 更新不能直接赋值,需要使用 setState 方法进行更新;
  • setState 方法有两种调用方式,一种是直接传入对象,一种是传入以 (prevState, props) 为函数的一个参数;
  • setState 调用后并不会立即更新,而是在一个组件生命之后将所有更改批量更新,函数的第二个参数就是 callback 函数。

组件样式的编写:

  • React 提供了使用 className(可以使用第三方库 classnames) 和 css-in-js 两种方式对组件样式进行更新;
  • 官方推荐使用 className 这种简单方式进行样式编写,css-in-js 应该只在特定场景下使用;

元素

元素:

  • 元素是 React 中最小的组成单元,它直接描述了希望看到的内容,可以通过第三方库直接渲染成一个 DOM 元素;
  • React 中的元素是不可变对象,这意味着它和它的子元素都是不可变的,在更新时使用创建一个新的元素的策略进行更新;

组合和元素(React.ComponentReact.Element):

  • 组件最核心的作用是返回 React 元素。
  • class 类型的组件中的 render 方法返回的就是元素,function 本身返回的也是元素。

一个区别二者的例子:

1
<Parent> <Children>我是子组件</Children> </Parent>

我们知道 children 会被持有在 parentprops 中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Children from './Children'

class Parent extends React.Component
{
  /* 正确的编写方式 1 */
  render() {
    const { tip } = this.props;
    return <Children tip={tip} />					/// => 因为 Children 是组件
  }
  
  /* 错误的编写方式 2 */
  render() {
    const { tip, children } = this.props;
    return <children tip={tip} />					/// => 因为 children 是元素
  }
  
  /* 正确的编写方式 3 */
  render() {
    const { tip, children } = this.props;
    return React.cloneElement(children, { tip });
  }
}

生命周期与事件处理

挂载阶段

下述方法从上往下以此调用:

方法一般作用调用时机与效果
constructor(props)1. state 的初始化,直接赋值而非使用 setState
2. 通过 bind 进行方法绑定;
ES6 class 的初始化方法
static getDerivdStateProps(props, state) => obj or null让组件在 props 变化时更新 state每次执行渲染前都会调用。
如果返回值设为 obj 那么执行之后组件的 state 会被设置为 {...this.state, ...obj}
render:唯一必须要实现的方法。描述希望在页面上看到的 UI 效果返回类型:元素、数组/fragmentsPorrtals、字符串或数值、布尔型或 null。
componentDidMount1. 依赖于 DOM 节点的初始化的任务;
2. 需要通过网络请求获取数据;
在组件挂载(即插入 DOM 树)之后立即调用。

更新阶段

组件的更新需要外部的触发,一般有以下三种方式:props 更新、setState 方法调用、forceUpdate 方法调用。

在组件更新触发时,下面的方法从上往下依次调用:

方法一般作用调用时机与效果
static getDerivdStateProps(props, state) => obj or null注意参数是更新之后的值
shouldComponentUpdate(nextProps, nextState) => bool决定组件是否继续执行更新过程返回的值如果是 false,则组件不会继续执行后续函数的更新过程(forceUpdate 时会跳过这个函数的执行)
render
getSnapshotBeforeUpdate(prevProps, prevState) => snapshot or null需要在更新 DOM 前保存当前 DOM 一些状态值时使用在最近一次渲染输出(即提交到 DOM 节点)前调用
componentDidUpdate(prevProps, prevState, snapshot) => void对更新后的 DOM 进行操作在更新后会被立即调用

卸载阶段

执行一个 componentWillUnmount 方法。

事件处理

React 事件与原生 DOM 事件:

  • React 事件命名统一使用小驼峰式,而不是纯小写;
  • React 事件在 jsx 语法中需要传入一个 js 的函数而不是一个字符串;
  • React 中的事件时合成事件,并不是原生的 DOM 事件,如有需要,可以使用 e.nativeEvent 访问;
  • 要阻止事件的默认行为,在 React 中必须显示地调用 preventDefault 方法;

this 的处理:ES6 并不会将函数自动绑定到当前定义的对象中,因此在使用需要调用 this 的函数作为事件处理函数时,需要对 this 进行特殊的处理。React 中主要有以下三种处理方式:

  1. 箭头函数,比如:(e) => this.handleEvent(e)
  2. 函数绑定,在 constructor 中将函数通过 bind 函数绑定到当前对象,比如:this.handleEvent = this.handleEvent.bind(this)
  3. class field 还处于草案阶段,需要引入 babel 的插件,比如 handleEvent = () => {}

列表与表单

列表

React 使用 key 属性来标记列表中的每个元素,当列表数据发生变化时,React 就可以通过 key 知道哪些元素发生了变化,从而只渲染发生变化的元素,提高渲染效率

key 的制定原则:

  • key 应该放在就近的数组上下文中,指定给最上层的标签的属性;
  • 数组元素中使用 key 只在其兄弟节点中是独一无二的;
  • 不建议使用索引来作为 key 值,因为这样会导致性能变差,还可能引起组件状态问题;

受控表单

一个表单的值是由 React 来进行管理的,那么它就是一个受控组件。

具体实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class NameForm extends React.Component {
  // ...
  handleChange = event => {
    this.setState({ value: event.target.value });
  }
  // ...
  render() {
    return ( <input onChange={this.handleChange} />		/* 交由 NameForm 处理 */ );
  }
}

非受控表单

如果一个表单的状态仍然由表单元素自己管理,而不是交给 React.js 组件管理,那么他就是一个非受控组件。

下面是一个非受控组件的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class FlavorForm extends React.Component {
  constructor (props) {
    // ...
    this.inputMangoRef = React.createRef();
  }
  handleSubmit = event => {
    event.preventDefault();
    // ...
    console.log(this.inputMangoRef.current.checked);
  }
  render () {
    return (
      <form onSubmit={this.handleSubmit}>
        <input type="checkbox" value="mango" name="mango" ref={this.inputMangoRef} />
      </form>
    )
  }
}

refs

Refs 是 React 中访问 DOM 节点,获取 render 方法中创建的 React 元素的一种方法。

Refs 有以下两种使用方式:

  1. React.createRef:上面“非受控表单”的例子就是使用的这种方式进行创建的;
  2. 回调函数:给 ref 属性值设置为一个函数,它会以 ref 为参数调用这个函数(注意:如果是以匿名函数的方式定义这个函数,那么每次更新时他会被执行两次,其中第一次是 null,所以为出于性能考虑不建议这么定义)

注意事项:

  1. 函数组件不能使用 refs
  2. 切勿过度使用 refs