# React 基础

先看看 react 主要的几个特点

  1. 声明式:以声明式编写 UI,代码更加可靠,且方便调试
  2. 组件化:构建管理自身状态的封装组件,然后对齐组合以构成复杂 UI。组件的逻辑使用 JavaScript 进行编写而不使用模板,可以轻松的传递数据,并保持 DOM 的分离。

声明式编程和命令式编程

命令式编程描述代码如何工作,而声明式编程则表明想要实现什么目的。

  • 给我来杯啤酒

命令式用生活中的一个例子来说,就好比当你去和服务员要一杯啤酒时对服务员做这样的指示:

  1. 从架子拿一个玻璃杯
  2. 将杯子放到酒桶前
  3. 打开酒桶开关,将杯子装满
  4. 把杯子递给我

如果用的是声明式,你只需要说:“请给我一杯啤酒”。使用这种方式来点啤酒,我们需要假设服务员知道如何提供啤酒。这是声明式编程工作的一个重要方面。

  • JavaScript 将数组元素大写字符串转成小写

再看看一个 JavaScript 的例子,实现一个将数组中的元素的大写字符串转成小写字符串

toLowerCase(['Foo', 'BAR']); // ['foo', 'bar']

解决这个问题,用命令式来实现就是:

const toLowerCase = input => {
  const output = [];
  for (let i = 0; i < input.length; i++) {
    output.push(input[i].toLowerCase())
  }
  return output()
}

首先我们需要定一个数组,然后遍历输入数组的所有元素,把它们转为小写之后 push 新的数组中,最后将新的数组返回。

如果使用的是声明式:

const toLowerCase = input => input.map(value => value.toLowerCase())

输入的数组的元素会传递到 map 中,然后 map 函数会返回小写值的数组。

相对于之前的方法来说,这个更加的优雅、简洁和易读。另外,声明式编程无需使用变量,也不用在执行过程中持续更新变量的值。事实上,声明式编程往往避免了创建和修改状态。

  • 在 React 的 UI 中

在开发中实现一个这样的需求,展示带有标记的地图。使用 JavaScript 的实现如下:

const map = new google.maps.Map(document.getElementById('map'), {   
  zoom: 4,    
  center: myLatLng, 
}) 
const marker = new google.maps.Marker({   
  position: myLatLng,   
  title: 'Hello World!', 
}) 

marker.setMap(map)

这显然是编程式的,在代码中我们逐条描述了创建地图、创建标志、以及在地图上添加标志的指令。

改用 React 组件,那么方式就是:

<Gmaps zoom={4} center={myLatLng}>
  <Marker position={myLatlng} title="Hello World!" />
</Gmaps>

使用声明式,我们只需要描述我们想要的功能,无须列出实现效果的所有步骤。

声明式的 UI 使得 React 很容易使用,最终的代码也比较简单,这样产生的 bug 更少。

# 1. JSX 语法

所谓的 JSX,就是 JavaScript 的语法扩展(eXtension)。它看起来和 HTML 比较类似,但它并不局限于 HTML 中的元素

  • 它的元素可以是任何一个 React 组件。React 判断一个元素是 HTML 元素还是 React 组件的原则就是看第一个字母是否是大写。
  • 在 JSX 可以通过 onClick 这样的方式给一个元素添加一个事件处理函数。当然在 HTML 中也可以用 onclick,但在 HTML 中直接写 onclick 一直就是为人诟病的,会容易让人混乱,比较倡导使用 jQuery 的方法来添加处理函数。

使用的是 JSX 语法,最后经过 babel 编译成 JavaScript 来执行。

# 为什么使用 JSX

既然不推荐在 HTML 直接使用 onclick,那为什么 JSX 中我们缺使用这样的方式来添加事件处理函数呢?

以前我们

  • 用 HTML 来代表内容
  • 用 CSS 来代表样式
  • 用 JavaScript 来定义交互行为

然后将这三种语言分在不同的文件中,实际上是“把技术分开管理,而不是分而治之”。

React 认为渲染逻辑本质和其他的 UI 逻辑有内在耦合,比如 UI 中需要绑定事件,在某些状态更新需要通知 UI。React 没有将标记(HTML)与逻辑进行分离到不同文件中这种人为的分离方式,而是通过将两者放在称为“组件”的松散耦合单元中,来实现关注点的分离

做同一件事的代码应该高耦合,应该把实现这个功能的所有代码集中在一个文件里。除了在组件中定义交互行为,还可以在 React 组件中定义样式。

通过 React 组件可以把 JavaScript、HTML 和 CSS 的功能都写在一个文件中,实现了真正的组件封装。

class List extends React.Component {
  constructor(props) {
    super(props);
    this.onClickButton = this.onClickButton.bind(this);
    this.state = {count: 0};
  }

  onClickButton() {
    this.setState({count: this.state.count + 1})
  }

  render() {
    const countStyle = {
      fontSize: '14px'
    }
    return (
      <div className="list">
        <button onClick={this.onClickButton}>Click Me</button>
        <div style={countStyle}>
          Click Count: <span id="clickCount">{this.state.count}</span>
        </div>
      </div>
    )
  }
}

会被编译成

return React.createElement('div', {className: 'list'},
  React.createElement('button', /*... h1 children */),
  React.createElement('div', /*... ul children */),
)

最后会创建这样的对象,它们就是“react 的元素”,描述你所在页面上看到的内容。react 通过读取这些对象,用来创建最后的 DOM,并保持随时更新。

{
  type: 'div',
  props: {
    className: 'list',
    children: [
      //...
    ]
  }

}

# Virtual DOM

接下来我们看看 react 的工作理念是怎么样的。这里我们用 jQuery 来对比一下

<html>
  <body>
    <button id="clickMe">Click Me</button>
    <div>Click Count: <span id="clickCount">0</span>
    <script src="https://code.jquery.com/jquery-1.12.4.min.js">
    <script src="./clickCounter.js"></script>
  </body>
</html>

// clickCounter.js
$(function() {
  $('#clickMe').click(function() {
    const $clickCount = $('#clickCount');
    const count = parseInt($clickCount.text(), 10);
    $clickCount.text(count + 1); 
  })
})

在👆这种实现的方式中,是一种命令式的方式,首先我们找到了按钮,然后挂上一个匿名函数。在事件的处理函数中,选中那个需要修改的 DOM 元素,读取其中的文本值,加 1 之后,修改这个 DOM 元素。

这种方式确实比较容易理解,不过在大型项目中,这种模式会造成代码结构复杂难以维护。

而 React 相对会更加“高级”,我们并不需要“选中一些 DOM 然后做一些事情的操作”。我们只需要关心“我想要显示什么”,而不用操心“怎么做”。

React 的理念,可以归结为一个公式

UI = render(data)
  • UI 就是用户界面,它是 render 函数执行的结果
  • render 是一个纯函数,只接受 data(数据)

用户看到的界面完全取决于输入的数据。当我们想要更新页面的时候,要做的就是更新数据,用户界面就会自动的作出响应,这也是 React 为什么叫 React 的原因。

在每次 render 函数被调用时,并不是简单的把整个组件重新绘制一次,因为那样明显是比较浪费的。React 利用了 Virtual DOM,让每次渲染时只重新渲染最少的 DOM。

  1. DOM 是结构化文本(HTML文本)的抽象表达形式,每个 HTML 中的元素都对应一个 DOM 节点,基于这种形式,DOM 节点自然就构成了一个树形结构——DOM树。
  2. 浏览器为了渲染 HTML,会先用 HTML 来构建 DOM 树,然后根据 DOM 树渲染用户看见的界面,当需要修改界面时,就去修改 DOM 树上的节点。
  3. 为了提升性能,我们需要“减少 DOM 操作”。因为一个 DOM 操作,可能会引起浏览器对网页进行重新布局、绘制
  4. React 不会直接将 JSX 语句用来构建 DOM 树,而是构建 Virtual DOM。DOM 树是 HTML 的抽象,那么 Virtual DOM 就是 DOM 树的抽象。它不会直接触及浏览器,而只是存在 JavaScript 空间的一个树形结构。
  5. 每次渲染时,会对比这一次产生的 Virtual DOM 和上一次渲染的 Virtual DOM,如果发现差别,修改真正的 DOM 树时只需要修改差别中的部分就行。

所以在上面的计数中,当用户点击之后,这一次的重新渲染,实际也只会更新 span 元素中的内容而已 0 -> 1。

# 2. 组件

React 的组件使用的是一个名为 render() 方法。

  • 可以向组件传递参数,组件内部可以通过 this.props 获取。
  • 组件内部也可以维护自己的状态,可以通过 this.state 访问,当组件的数据变化时,组件会再次调用 render() 方法重新渲染更新
import React, { Component } from 'react';

class TodoApp extends Component {
  constructor(props) {
    super(props);
    // 定义自己的状态
    this.state = { items: [], text: '' };
    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  render() {
    return (
      <div>
        <h3>TODO</h3>
        <TodoList items={this.state.items} />
        <form onSubmit={this.handleSubmit}>
          <label htmlFor="new-todo">
            What needs to be done?
          </label>
          <input
            id="new-todo"
            onChange={this.handleChange}
            value={this.state.text}
          />
          <button>
            Add #{this.state.items.length + 1}
          </button>
        </form>
      </div>
    );
  }

  handleChange(e) {
    // 更新 state,重新进行渲染
    this.setState({ text: e.target.value });
  }

  handleSubmit(e) {
    e.preventDefault();
    if (this.state.text.length === 0) {
      return;
    }
    const newItem = {
      text: this.state.text,
      id: Date.now()
    };
    this.setState(state => ({
      items: state.items.concat(newItem),
      text: ''
    }));
  }
}

class TodoList extends React.Component {
  render() {
    return (
      <ul>
        {this.props.items.map(item => (
          <li key={item.id}>{item.text}</li>
        ))}
      </ul>
    );
  }
}

ReactDOM.render(
  <TodoApp />,
  document.getElementById('todos-example')
);

这里还有几个小知识🤔️

  • importComponentComponent 作为所有组件的基类,提供了很多组件共有的功能。可以通过 extents Component 来创建组件类
  • 另外我们有必要 import React,看起来没有被用到?事实上这很有必要,在使用 JSX 的代码文件中,即使我们用不到 React,也需要将它倒入,因为 JSX 最后会被编译成依赖于 React 的表达式。例如前面看到的React.createElement