初探 React

因为公司最近安排的项目都是采用 React 技术栈实现的,所以入了 React 的坑,后面估计会写一个关于 React 的系列。本文主要谈谈我自己关于 React 的一些想法。

React 是什么呢

React is a JavaScript library for building user interfaces.

在 React 文档的最前面就告诉我们了,React 是一个用于构建用户界面的 JavaScript ,抛开 React 周边的一些库,诸如 Redux、React Router等,React 其实是一个非常简单的东西,她就是一个库,并且这个库的关注点在界面,也就是我们常说的 view 层。我们从官方文档给出的一个例子来理解一下 React:

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

其实可以到 React 最终都是通过这个 render 方法将相应的数据转换为视图的,那我们可以将 React 抽象成一个公式:

UI = render(viewModel)

这里的 viewModel 和我们常见的 MVVM 中的 ViewModel 还有所不同,我所理解的这个 viewModel 只是我们在构建 UI 的过程中所建立的「页面模型」,这个「页面模型」包含 DOM、数据以及一些交互。而 React 所做的事情就是将这个 viewModel 转换为我们所需要的界面。

声明式

相信大家对「声明式」这个词语应该不会陌生,在很多关于 React 的文章中都有提到这个词,说起「声明式」就不得不提另外一个概念——「命令式」,我们先来看一下这两者的概念以及区别:

声明式编程是一种编程范式,与命令式编程相对立。它描述目标的性质,让计算机明白目标,而非流程。声明式编程不用告诉计算机问题领域,从而避免随之而来的副作用。而命令式编程则需要用算法来明确的指出每一步该怎么做。

为什么说 React 是声明式的呢?传统我们使用 jQuery 来改变 DOM 我们需要手动去选中某个 DOM 节点,然后再对该节点进行一步一步地操作。如果用 React,我们只需要描述一下该节点所需要的状态是怎样的,然后将这个状态告诉 React,React 就会自动帮我们完成,这样开发者的工作就被简化了。React 实现这种机制采用的是「虚拟 DOM」,关于「虚拟 DOM」我会在后面的文章中专门体现。

JSX

JSX 本质是一种语法糖,这里就不多说了,大家肯定都知道。只要我们写过 React 的代码,就一定写过 JSX,比如:

const element = <h1>Hello, World!</h1>;

很多人其实很反感这种方式,这种将 JS 和 HTML 耦合在一起的方式让代码看上去非常丑陋。说实话我刚开始写 JSX 的时候我也很不喜欢,不过后面也就习惯了,因为 JSX 里面基本上都是一些表达式和方法,没有什么其他的魔法。

那么为什么 React 要使用 JSX 呢?官方文档上的说法是这样的:

React embraces the fact that rendering logic is inherently coupled with other UI logic: how events are handled, how the state changes over time, and how the data is prepared for display.

React 认为渲染逻辑本质上就是与其他 UI 逻辑内在耦合的,比如我们需要在 UI 中绑定事件或者展示数据等。还有一个原因就是 React 推崇直观的编程方式,举个例子,我们在使用 Vue 等框架构建 UI 的时候都会声明一个模板(Vue 也支持 JSX,这里只是举例),然后这个模板会有一些相应的模板语法,比如 Vue 的 v-if、v-for、v-model、@click 等等。使用 React 不用去学习这些模板语法,我们可以直接用 JavaScript 代码来构建 UI。在 React 中其实就没有模板这个概念了,所有的事情我们都通过 JavaScript 来解决,这其实体现了 React 「以 JavaScript 为中心的设计」思想。

模板语法与 JSX 这种方式在社区有很多讨论,各种站队互喷啥的。这两种方式我其实都能接受,其实上面说的那种模板语法我觉得学习起来也没有什么门槛,这两种方式都体现了不同的设计哲学,每种都有其自身的意义。

组件化

在 React 中,我们可以说一切皆组件,我们在页面上看到的所有东西都可以看成是一个一个组件组合而成的。那么为什么我们需要组件,举一个例子,图片轮播这个组件相信大家在业务中都有用过,按照传统的开发模式,我们实现了一个轮播功能,那我们如果要在其他地方使用这个轮播功能的话,我们需要引入这个轮播的 JS 文件、CSS 文件,同时还需要在使用该功能的页面编写符合条件的 HTML 代码。那有了 React,我们直接实现一个轮播组件,需要用的地方直接引入这个轮播组件就行了,不需要再引入其他的代码了。

组件化使我们构建页面的灵活性以及效率提高了很多。

数据不可变

Shared mutable state is the root of all evil(共享的可变状态是万恶之源)

如果你接触过函数式编程,那么你一定知道不可变数据结构这个概念。其实 React 也是拥抱了函数式的思想,在用 React 进行开发的时候,我们描述一个组件的状态无外乎两个东西—— propsstate。我们知道 React 是严格要求 props 是不可变的,首先是因为 React 的数据是单向流动的,只能从更高一级流向更低一级,如果随意改变,会导致状态混乱,并且很难定位问题。其次,React 中的一个一个组件我们可以将其看作是一个一个的函数,组件的组合其实就是函数的组合,比如我们实现一个组件:

function MyComponent(props) {
  bar(props.foo);
  return (
    <div className="container">
      <h1>{props.foo}</h1>
    </div>
  );
}

因为 props 是不可变的,知道输入之后,我们可以很容易地推断出输出是什么,如果 props 是可以被改变的,那么调用 bar(props.foo) 这个方法之后,我们要想知道最后的输出是什么,就必须要去走一遍 bar 方法的流程。在实际开发之中我们也应该遵循这种原则,开发出的组件尽量「函数式」,即相同的输入永远对应相同的输出。

上面说了 props,接下来谈谈 state,我们知道 state 是可以被改变的,我们通过 state 的变化来维护组件自身的一些状态。即使 state 是可以改变的,但是我们在操作 state 的时候也应该去考虑数据的不可变,举个例子:

class MyComponent extends React.Component {
  constructor(props) {
    super(pops)
    this.state = {
      foo: {
        a: 1,
        b: 2
      },
      bar: null
    }
  }
    ...
}

上面就是我们熟悉的声明 state 的方式,在 React 中,我们通过 setState 方法来对 state 中的值进行修改,当我们在使用 state 中的值的时候一定要避免出现下面的情况:

class MyComponent extends React.Component {
    ...
  handleState() {
    const bar = this.state.foo
    bar.a = 2
    this.setState(pre => ({
      bar: bar
    }))
  }
}

在上面的代码中,我们本意是创建一个新的对象 bar 然后将 state 中的值替换掉,由于 JavaScript 的对象是引用类型,导致我们修改了原来 state 中的 foo,这样就引起了不必要的麻烦,如果我们需要利用原有的对象,正确的做法应该是这样:

class MyComponent extends React.Component {
    ...
  handleState() {
    const bar = {
      ...this.state.foo,
      a: 2
    }
    this.setState(pre => ({
      bar: bar
    }))
  }
}

这里其实也是利用了我们上面所说的数据不可变的思想,如果我们需要修改某个对象,我们直接返回一个具有新的引用的对象,然后再在这个新的对象上面进行操作。简单说来就是不直接修改数据,而是用新数据替换旧数据。这样做可以使得开发更加简单,可以很方便地实现回溯功能,同时当某个对象被改变之后,我们不用去一个一个比较对象中某个属性是否发生了改变,直接比较该对象的引用是否改变就行了,尤其是在使用 shouldComponentUpdate (后面会有专门的文章讲解)会更加方便。

总结

这篇文章只是我自己对 React 作的一个简单的了解,其实上面的每一个点都可以写好几篇文章出来,在后面的文章中我会针对上面的一些知识点做更深层地分析。