React.js 原始用法 - 掌握核心语法

前端框架目前基本还是 Vue.js 和 React.js 两大阵营为主流,尽管 Angular.js 版本升的飞快, 事实就是鲜有人问津, 想用 Angular.js 的人何不直接用 Vue.js 呢. 关于 Vue.js 和 React.js 的数据显示, Vue.js 延续了传统模板(如 JSP, Velocity, Freemarker, Thymeleaf)的用法, 通过自定义 Tag, 许多逻辑写到 HTML 里. 而 React.js 则另辟蹊径, 通过 JSX 语法将 HTML 直接写到 JavaScript 里, 页面显示时不夹杂逻辑.

以前算是用 Vue.js 做过一些小项目, 对 React.js 未有多少了解, 现在也算是初学. 刚开始不想从一个 npx create-react-app my-react-app 的脚手架开始, 而是想从最原始的 HTML 中引入 React.js 的方式开始学习, 这样可以更清晰地了解 React.js 的工作原理. 也是为使用 React.js 拥抱 Vibe Coding 做准备.

基本 React.js Virtual DOM 渲染

<script> 标签引入 react 和 react-dom 两个核心库的方式已经不推荐了, 官方的 CDN Links 已经变成 Legacy 了, 也找不到引用 react@19 的相关链接了. 但本着要明就里的原则还是希望以这种方式演练一下.

下面是使用 react@19 和 react-dom@19 的最简 HTML 示例:

 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4    <meta charset="UTF-8">
 5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6    <title>Test React</title>
 7    <script src="https://unpkg.com/umd-react@19/dist/react.development.js" crossorigin></script>
 8    <script src="https://unpkg.com/umd-react@19/dist/react-dom.development.js" crossorigin></script>
 9</head>
10<body>
11<div id="root"></div>
12
13<script>
14    const root = ReactDOM.createRoot(document.getElementById('root'));
15    root.render(React.createElement('h2', null, 'Hello, React!'));
16</script>
17</body>
18</html>

页面会显示

Hello, React!

可是还没有看到 JSX 的语法啊, 没有 JSX 的 React.js 不能叫做 React.js. 但如果直接把上面的 <script> 中的代码改写成

1const root = ReactDOM.createRoot(document.getElementById('root'));
2root.render(<h2>Hello, React!</h2>);

后是无法正常工作的, 浏览器 Console 中出现的错误是

Uncaught SyntaxError: Unexpected token '<'

因为浏览器并不认识 JSX 语法. 这时就需要引入 Babel 来进行转换了, 在原来引入 reactreact-dom 的基础上还要加上

1<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

但这还不够, 还要把包含 JSX 语法的 <script> 标签加上 type="text/babel" 属性, 这样 Babel 才会对其进行转换. 最终的 HTML 代码如下:

 1<!DOCTYPE html>
 2<html lang="en">
 3<head>
 4    <meta charset="UTF-8">
 5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 6    <title>Test React</title>
 7    <script src="https://unpkg.com/umd-react@19/dist/react.development.js" crossorigin></script>
 8    <script src="https://unpkg.com/umd-react@19/dist/react-dom.development.js" crossorigin></script>
 9    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
10
11</head>
12<body>
13<div id="root"></div>
14
15<script type="text/babel">
16    const root = ReactDOM.createRoot(document.getElementById('root'));
17    root.render(<h2>Hello, React!</h2>);
18</script>
19</body>
20</html>

这样页面也能正确显示 Hello, React! 了, 支持了 JSX 语法. 在此我们可对比一下 Vue.js 和极简代码, 代码中省去了不影响页面显示的元素

下面将演示 React.js 一些功能性扩展.

自定义组件

上面在使用 root.render() 不光可以渲染已有的 HTML 标签(相当于是内置组件), 也可以渲染自定义组件. 下面定义一个简单的组件 Welcome 来演示:

1<script type="text/babel">
2    function Welcome(props) {
3        return <h2>Hello, {props.name}</h2>;
4    }
5
6    const root = ReactDOM.createRoot(document.getElementById('root'));
7    root.render(<Welcome name="React User" />); 
8</script>

这里定义了一个函数组件 Welcome, 它接收 props 作为参数, 并返回一个包含 props.nameh2 标签. 函数组件可用 function 关键字显式定义, 也可定义为一个闭包函数变量, 如上面的 Welcome 函数可改写成

1    const Welcome = (props) => {
2        return <h2>Hello, {props.name}</h2>;
3    }

除渲染函数组件外, 也可以渲染类组件, 例如:

1<script type="text/babel">
2    class Welcome extends React.Component {
3        render() {
4            return <h2>Hello, {this.props.name}</h2>;
5        }
6    }
7    const root = ReactDOM.createRoot(document.getElementById('root'));
8    root.render(<Welcome name="React User" />);
9</script>

这是一个类组件, 和前面函数组件的显示效果是一样的.

无论哪种组件, 返回的内容都只能有一个根节点, 如果有多个节点, 则需要用一个父节点包裹起来, 或者用虚拟节点 React.Fragment 包裹起来, 例如:

 1<script type="text/babel">
 2    function App() {
 3        return (
 4            <React.Fragment>
 5                <h2>topic</h2>
 6                <Welcome name="React User" />
 7            </React.Fragment>
 8        );
 9    }
10</script>

注意到上面, 组件也可以嵌套使用别的组件, 就像是 HTML 标签一样的方式使用. <react.Fragment> 也可以简写成 <></> 的形式, 例如:

1function App() {
2    return (
3        <>
4            <h2>topic</h2>
5            <Welcome name="React User" />
6        </>
7    );
8}

现在多是使用函数组件加上 Hooks 的方式来编写组件, 类组件已经很少使用了. 如果组件变的复杂或者要让功能清晰, 可复用, 可以把组件拆分到单独的 js 文件中, 可以想见 React.js 项目中基本就是一些 js 文件组成的, 而 Vue.js 项目含有许多 .vue 文件.

组件中插值

这就相当于在模板中插值的概念, 在 JSX 语法中, 可以使用 {} 来包裹 JavaScript 表达式, 例如:

 1const fruits = ['apple', 'banana', 'cherry'];
 2function App() {
 3    return (
 4        <ul>
 5            {
 6                fruits.map((fruit, index) => (
 7                    <li key={index}>{fruit}</li>
 8                ))
 9            }
10        </ul>
11    );
12}

也可以事先生成一个变量, 然后在 JSX 中插入该变量, 例如:

 1function App() {
 2    const fruitItems = fruits.map((fruit, index) => (
 3        <li key={index}>{fruit}</li>
 4    ));
 5    return (
 6        <ul>
 7            {fruitItems}
 8        </ul>
 9    );
10}

可以自己来感受哪种方式更适合自己.

事件处理与状态管理

Vue.js 中, data 中定义的数据与网页显示是双向绑定的, 而在 React.js 中, 需要使用 useState 来定义状态变量, 并通过事件处理函数来更新状态. 例如:

 1<script type="text/babel">
 2    function App() {
 3        const [name, setName] = React.useState('World');
 4        return (
 5            <div>
 6                <h2>Hello {name}</h2>
 7                <input value={name} onChange={(e) => setName(e.target.value)}/>
 8            </div>
 9        );
10    }
11    const root = ReactDOM.createRoot(document.querySelector('#root'));
12    root.render(<App/>);
13</script>

我们在函数组件中新加了一个状态, 并拆解为属性和更新方法, React.useState('World') 中的 World 是初始值. 这样打开网页时会显示 Hello World, 在输入框中输入内容时, h2 标签中的内容也会随之变化.

注间: React.js 的 JSX 代码中只要是 {} 中的内容被视为 JavaScript 代码, 而且不要像写 HTML 那样两边加双引号, 用了双引号就变成纯字符串了. 比如写成 <input value="{name}" ... /> 就不对了, 页面上会原样显示 {name}, onChange={} 也是同样的规则.

如果 onChange 事件处理函数比较复杂, 可以在函数组件中单独定义一个函数, 然后在 JSX 中引用该函数, 例如:

 1function App() {
 2    const [name, setName] = React.useState('World');
 3    const updateName = (e) => {
 4        setName(e.target.value)
 5    };
 6    return (
 7        <div>
 8            <h2>Hello {name}</h2>
 9            <input value={name} onChange={updateName}/>
10        </div>
11    );
12}

这与 Vue.js 的双向绑定相比, 我们只需关注要变化的状态, 以及清楚变化的方向, 可避免内部过多的事件通知.

组件中使用 CSS 样式

如果还像以前那样在组件中用 style 属性定指定字符串形式的样式, 会有问题. 像下面的代码

1function App() {
2    return (
3        <h2 style="color: red">Hello World!</h2>
4    );
5}

浏览时会报错

Uncaught Error: The style prop expects a mapping from style properties to values, not a string. For example, style={{marginRight: spacing + 'em'}} when using JSX.

原因是 style 属性的值应该是一个对象, 而不是字符串. 正确的写法是:

1return (
2    <h2 style={{"color": "red"}}>Hello World!</h2>
3)

不要把它想成什么双大括号语法, 而要分开来理解, 外层的 {}JSX 语法中表示插值的意思, 内层的 {} 才是 JavaScript 对象的定义方式. 比如, 该样式有个属性的话, 写成下面的风格就来理解了.

 1    function App() {
 2        return (
 3            <h2 style={
 4                {
 5                    "color": "red",
 6                    "fontSize": "24px"
 7                }
 8            }>Hello World!</h2>
 9        );
10    }

或者把整个 style 属性提取到一个变量中, 这样更容易使用状态来交互, 例如:

1    function App() {
2        const style = {
3            color: 'red',
4            fontSize: '24px'
5        };
6        return (
7            <h2 style={style}>Hello World!</h2>
8        );
9    }

并且注意, React.js 中的 CSS 属性名是它们在 JS 中名称, 而且在 css 中的名名, 例如 font-size 应该写成 fontSize, background-color 应该写成 backgroundColor.

函数组件与 Hooks(钩子)

函数组件如果是不纯的(会带来副作用), 那么就需要使用 Hooks(钩子) 来处理状态和副作用等问题. 例如之前的 useState 就是一个常用的 Hook.

下面来看什么时候需要用到 useEffect 这个 Hook. 假设我们有一个组件, 希望它在第次从网络上加载数据, 然后改变某一状态. 进而反应到页面上, 例如, 我们很可能会写出类似下面的代码

 1    function App() {
 2        const [name, setName] = React.useState('World');
 3
 4        const fetchData = () => {
 5            setName('React User');
 6        };
 7
 8        console.log("before fetching data")
 9        fetchData();
10        console.log("after fetching data")
11
12        return (
13            <h2>Hello {name}</h2>
14        );
15    }

那么恭喜你, 你自然而然的掉了 React 给下设下的陷阱. 打开浏览器的 Console, 你会发现疯狂的一段输出

1before fetching data
2after fetching data
3before fetching data
4after fetching data
5before fetching data
6........

最后看到

 1Uncaught Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
 2    at renderWithHooksAgain (react-dom-client.development.js:5614:17)
 3    at renderWithHooks (react-dom-client.development.js:5532:21)
 4    at updateFunctionComponent (react-dom-client.development.js:8897:19)
 5    at beginWork (react-dom-client.development.js:10522:18)
 6    at runWithFiberInDEV (react-dom-client.development.js:1520:30)
 7    at performUnitOfWork (react-dom-client.development.js:15132:22)
 8    at workLoopSync (react-dom-client.development.js:14956:41)
 9    at renderRootSync (react-dom-client.development.js:14936:11)
10    at performWorkOnRoot (react-dom-client.development.js:14462:44)
11    at performWorkOnRootViaSchedulerTask (react-dom-client.development.js:16216:7)
12    at MessagePort.performWorkUntilDeadline (scheduler.development.js:45:48)

这还算不错, 浏览器聪明的制止了死循环.

出现这个死循环的原因是当第一次执行该 <App/> 组件时, fetchData() 函数被调用, 它调用了 setName('React User'), 这会触发组件重新渲染, 重新渲染时又调用了 fetchData() 函数, 又调用了 setName('React User'), 这样就子子孙孙无穷尽也.

为解决这个问题, useEffect 这个 Hook 就该派上用场. useEffect 允许我们在函数组件中执行副作用操作, 并且可以指定依赖项数组. 只有其中的某个依赖项发生变化时, 副作用才会重新执行. 如果我们希望副作用只在组件挂载时执行一次, 可以传入一个空数组作为依赖项. 把前面高亮的代码改写成下面这样:

1   React.useEffect(() => {
2       console.log("before fetching data")
3       fetchData();
4       console.log("after fetching data")
5   }, []);

再刷新页面, 你会发现 Console 中只输出了一次

before fetching data after fetching data

React.js 16.8 版本引入了 Hooks 的概念, 除了上面介绍的 useStateuseEffect 外,: 还有许多其他常用的 Hooks, 如下:

  • useRef: DOM 引用、存放不触发渲染的可变值.
  • useMemo: 缓存计算结果,避免重复重算.
  • useCallback: 缓存函数引用,常配合 React.memo.
  • useContext: 跨层共享数据:主题/用户/配置等.
  • useReducer: 复杂状态逻辑:表单、状态机、聚合更新.
  • useId: 生成稳定 id,表单/ARIA,React 18 后更常见.
  • useImperativeHandle: 封装组件库时暴露方法给父组件.

结束语

掌握了这些核心语法之后, 在 JS 采了 ES6 的模块化 import 语法后, 也能理解 React.js 项目是如何组织和运作起来的. 现代的 React.js 项目自然是不会用上面这种原始的 HTML 引入方式了, 而是通过 npm 包管理工具来安装 reactreact-dom 包. 对于自定义组件也是用 import 来引入的. 像 create-react-app 脚手架创建的项目中 index.js 长得是下面这个样子

 1import React from 'react';
 2import ReactDOM from 'react-dom/client';
 3import './index.css';
 4import App from './App';
 5
 6const root = ReactDOM.createRoot(document.getElementById('root'));
 7root.render(
 8  <React.StrictMode>
 9    <App />
10  </React.StrictMode>
11);

此外, 在使用 IntelliJ IDEA 以 Markdown 格式写作此篇日志时, 发现编辑器的 AI 功能太强大了, 每次都能预判我下面一至两行要写什么, 真是太恐怖了. 而且在准备演示代码时也能帮我一行行推断出来, 当然还是要亲自执行验证相关的代码.

从 WordPress 的在线程写作切换来本地写作, AI 确实能能发挥很大的作用, 目前 AI 提示的文字还是仅供参考, 如果总是 Tab 键接收 AI 的建议, 恐将失去自己思考的训练了.

永久链接 https://yanbin.blog/react-js-bare-usage/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。