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 变量的函数,它们的名称并不是useState
API 的一部分,你可以取自己想要的名字。传入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
useRef
和 useState
的作用非常像,也是用于为组件定义可变的状态变量,只是用法稍有不同
refState = useRef(0); // refState.current = 0
useRef
接收一个变量的初值,生成的状态变量储存在返回值的 .current
属性中。与 useState
不同,你可以直接对 refState.current
进行修改。
虽然useRef
可以替代useState
的功能,但在大多数情况下还是建议使用useState
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。