React 是一个用于构建用户界面的 JavaScript 库。它起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架都不满意,就决定自己写一套,用来架设Instagram 的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了

本文的大部分内容都可以在 React 的官方文档上找到,如果你想更加系统地学习 React,强烈建议你阅读一下官方文档。

Hello Word

一个最简单的 React 实例如下:

index.html

<div id="root"></div>

index.js

const element = <h1>Hello, world!</h1>;  # JSX

ReactDOM.render(
  element,
  document.getElementById('root')
);

上面的例子会在网页中打印出 Hello, world!
index.html 中定义了一个用作“根” DOM 节点的 <div> 元素, index.js 中使用 ReactDOM.render() 将一个 React 组件渲染到该“根” DOM 节点上。

index.js 文件的第一行有些特别,这种将 JS 与 html 混写在一起的语法是 JS 的一种扩展,被称为 JSX。由于这种将 html 元素和 JS 逻辑混写在一起方式能够很好地体现 React 的“组件化”设计思想,这里强烈推荐你在 React 中使用 JSX 语法,尽管这不是必须的。

React 组件

在传统的前端开发方式中,html 与 JS 分别负责网页的内容呈现(网页上需要显示哪些元素)和元素背后的行为逻辑,这种代码组织虽然层次结构非常清晰,但逻辑上却不够直观。React 将元素的呈现代码(html)与其本身的逻辑代码(JS)先进行组合,从而抽象出一个带有行为的逻辑的元素,即组件。这种代码组织方式更加符合“所见即所得”的直观感受。此外,由于很多元素在大部分场景下对应的行为逻辑都是一致的,我们 可以可以很容易地实现出标准化的 React 组件,在不同的场景下复用它们。

希望下面的例子可以帮你对 React 的组件化有更具体的感受:

下面的两份代码均在页面上渲染了一个按钮,被点击后会在控制台记录按钮上显示的文本。

写法一. 使用 JS 和 html 将内容呈现和元素逻辑分开
index.html

<!DOCTYPE html>
<html>
  <body>
    <div id="root">
      <button id="btn">Click me</button>
      <script src="index.js"></script> # 引入 js
    </div>
  </body>
</html>

index.js

let btn = document.getElementById("btn");
btn.addEventListener("click", function() {
  console.log(btn.innerText);
});

写法二. 使用 React 组件
react_index.html

<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
  </body>
</html>

react_index.js

function LogButton(props) {
  function LogText() {
    console.log(props.text);
  }
  return <button onClick={LogText}>{props.text}</button>;
}

ReactDOM.render(
  <LogButton text="Hello world!" />,
  document.getElementById('root')
);

写法二中的 react_index.js 中定义了一个函数组件 LogButton,该函数接收一个参数 props,包含了组件需要的所有属性,并返回一个 html 元素。在该例中,LogButton只有一个.text属性, 元素自带的逻辑 LogText即被点击后将按钮上的显示文本写入控制台日志。

除函数组件外,React 还支持类组件,可以参阅React 的官方文档

Props 与 State

上一节提到,React的组件接收一个参数 props,该参数包含了组件所需要的所有属性。需要特别注意的是,Props 是只读的,即 React 组件决不能修改自身的 Props。但在很多场景下,我们需要动态的 UI,React提供的 state 来支持这种需求。

下面的例子实现了一个计时器,在按下计时按钮后,显示已经过的秒数:

import React, { useState, useEffect, useRef } from "react";

function SecondsCounter() {
  const [text, setText] = useState("start");
  const [timingState, setTimingState] = useState(false);
  const [time, setTime] = useState(0);
  const timer = useRef(0);

  useEffect(() => {
    if (timingState) {
      timer.current = setInterval(
        () => {setTime(t => t + 1)}, 1000
      );
    } else {
      clearInterval(timer.current);
    }
  }, [timingState]);

  function clickHandle() {
    if (timingState) {
      setTimingState(false);
      setText('start');
    } else {
      setTimingState(true);
      setText('pause')
    }
  }
  return (
    <div>
      <button onClick={clickHandle}>{text}</button>
      <p>{time} seconds had passed.</p>
    </div>
  );
}

ReactDOM.render(
  <SecondsCounter />,
  document.getElementById('root')
);

上面的例子会在页面上渲染出一个按钮和按秒为单位的计时结果,初始状态下,按钮上显示的内容为 start,点击按钮后,计时开始,按钮上显示的内容变为 pause,再次点击后,计时暂停,按钮上的本文变回 start。

这里从 React 库引入了 useState,useEffect,useRef 三个函数,这些以 use 开头的函数被称为 HOOK,下面逐一介绍它们的用法:

useState

useState是最常用的 HOOK 之一,可以定义一个组件能够改变的 state,基本使用方式为:

const [state, setState] = useState(0);

useState会返回一个数组,数组的第一个元素是一个 state 变量,第一个元素是用于设置该 state 变量的函数,它们的名称并不是useStateAPI 的一部分,你可以取自己想要的名字。传入useState的参数是该 state 变量的初始值,这里是 0,故该 state 是一个 number 类型的变量,当然你也可以定义其他类型的 state 变量,例如传递一个字符串给 useState 来获取一个字符串类型的 state,你甚至可以给 useState 对象来定义一个复杂 state

const [complexState, setComplexState] = useState({name: "Tom", age: 18})

想要改变 state 时,可以使用 useState 返回数组的第二个元素,但请注意,不要直接修改 state

const [state, setState] = useState(0);
setState(1); // 将 state 设置为 1
state = 2; // error!!!不能直接修改 state

state 可能是异步的
处于性能考虑,react 并不会在 setState 调用后立刻修改 state,而是将所有的 setState 进行合并后再执行,这可能导致一个令新手困惑的情况

const [state, setState] = useState(0);
setState(state + 1); // 此时 state 的值是 0
setState(state + 1); // 此时 state 的值仍是 0

在上述代码执行完毕,渲染下一帧时,state 的值并不是 2 而是 1。但有时候我们想要的可能不是这种效果,例如在上面的计时器例子中,我们想要 time 每隔一秒就自动加 1,可以将一个函数传入 setState

const [time, setTime] = useState(0);
setTime(time => time + 1);

这里 time => time + 1 是一个简化的箭头函数,箭头函数是 ES6标准新增的一种函数,箭头函数语法直观,定义匿名函数非常方便
例如,对于函数

function add(a, b) {
  return a + b; 
}

相应的箭头函数的写法

(a, b) => {
  return a + b;
}

当箭头函数的返回体只有一条返回语句时,可以简写为

(a, b) => a + b;
更多有关箭头函数和 JS 的知识可以参阅JavaScript教程

useEffect

useEffect 可以让你在函数组件中执行副作用操作,所谓副作用,指的是每帧UI界面在渲染后都会执行的顺带逻辑,其基本的使用方式如下

useEffect(effectFunction);

比如在上面计时器的例子中,SecondsCounter 组件在每次渲染后都会检查 timingState 变量,如果目前正处于计时状态(即 timingState 为真),则启动一个周期为 1000ms 的计时器,来定期调用匿名箭头函数
() => {setTime(t => t + 1)}, 增加 time 表示的秒数。

在默认情况下,useEffect(effectFunction) 会在界面每次渲染后都调用其中的副作用函数 effectFunction,细心地读者可能会发现,这会导致在每一帧渲染后,effectFunction 都会启动一个新的计时器,这显然不是我们想要的。对此,可以向 useEffect 传入第二个参数,该参数是一个列表,其中包含了若干个变量,只有当这些变量在前后两帧发生变化后,useEffect 才会执行其中的 effectFunction,在该例中,只有当timingState 发生改变后(意味着计时器从暂停状态进入到计时状态),我们才会设置新的计时器。

useEffect 的第二个列表参数中的内容不仅可能会程序的逻辑产生影响,也关乎程序的执行效率。常见的 useEffect 列表参数用法有以下几种

useEffect(effect); // 每次渲染后均执行该 effect
useEffect(effect, []); // 只在组件加载第一次渲染后执行该 effect
useEffect(effect, [a]); // 在组件加载第一个渲染后或者变量 a 发生变化后执行该 effect

useRef

useRefuseState 的作用非常像,也是用于为组件定义可变的状态变量,只是用法稍有不同

refState = useRef(0); // refState.current = 0

useRef 接收一个变量的初值,生成的状态变量储存在返回值的 .current 属性中。与 useState 不同,你可以直接对 refState.current 进行修改。

虽然 useRef 可以替代 useState 的功能,但在大多数情况下还是建议使用 useState

zhanghao
15 声望1 粉丝