Vue
和 React
都是流行的 JavaScript
框架,它们在组件化、数据绑定等方面有很多相似之处
本文默认已有现代前端开发(Vue
)背景,关于 组件化、前端路由、状态管理 概念不会过多介绍
0基础建议详细阅读 Thinking in React-官方文档 了解 React
的设计哲学
React 新文档- https://react.dev
React 中文文档(翻译中)- https://react.jscn.org
经过本文的学习让没开发过 React
项目的 Vue
开发者可以上手开发现有的 React 项目,完成工作需求开发
React 新文档
React
新文档重新设计了导航结构,让我们更加轻松地找到所需的文档和示例代码 不仅提供了基础知识的介绍,还提供了更加详细的原理介绍和最佳实践,包括:React
组件的设计哲学、React Hooks
的原理和用法等
并且提供了在线编辑和运行的功能,方便开发者进行测试和实验
👇 基于 函数组件
![a081914671caf9bcc1451729aed3f0b7.png](https://img-blog.csdnimg.cn/img_convert/a081914671caf9bcc1451729aed3f0b7.png)
初学可以只学 函数组件,You Don't Need to Learn Class Components
![8af6770232610464c8c3696d601725e4.png](https://img-blog.csdnimg.cn/img_convert/8af6770232610464c8c3696d601725e4.png)
👇 interactive sandboxes
可交互沙箱,边做边学
![f30b663648db6b5d69f28a5e54dd5fa9.png](https://img-blog.csdnimg.cn/img_convert/f30b663648db6b5d69f28a5e54dd5fa9.png)
Fork
可以单独打开页签
![f09918844cba4ef6fdad68723b8caec5.png](https://img-blog.csdnimg.cn/img_convert/f09918844cba4ef6fdad68723b8caec5.png)
JSX 与 SFC
在
Vue
中我们使用单文件组件(SFC)
编写组件模版 (虽然Vue
也支持使用JSX
, 但是更鼓励使用SFC
)在
React
中,JSX(JavaScript XML)
是一种将HTML语法嵌入到JavaScript
中的语法扩展。它可以使得我们在JavaScript
代码中轻松地定义组件的结构和样式,从而提高代码的可读性和可维护性
虽然 React
和 Vue
在组件定义方式上存在差异,但是它们的组件化思想是相似的
根节点
👇 Vue
<template><div>同级节点1</div><div>同级节点2</div>
</template>
👇 React
const App = (<><div>同级节点1</div><div>同级节点2</div></>
)const App = (<React.Fragment><div>同级节点1</div><div>同级节点2</div></React.Fragment>
)
条件渲染
👇 Vue
<div v-if="show">条件渲染</div>
<div v-show="show">条件渲染</div>
👇 React
{show ? <div>条件渲染</div> : null
}
循环语句
👇 Vue
<ul><li v-for="i in list" :key="i.id">{i.name}</li>
</ul>
👇 React
<ul>{ list.map(i => <li key={i.id}>{i.name}</li>) }
</ul>
表单绑定
👇 Vue
<input v-model="value"/>
👇 React
<input value={value} onChange={onChange}/>
可以看出 React
的 JSX语法
学习记忆成本更低一点(当然Vue
也不复杂),Vue
更语法糖一些
单向数据流与双向绑定
在 Vue
中,我们使用 v-bind
、v-modal
对数据进行绑定,无论是来自用户操作导致的变更,还是在某个方法里赋值都能够直接更新数据,不需要手动进行 update
操作
this.data.msg = '直接修改数据后视图更新'
在 React
中,数据流是单向的,即从父组件传递到子组件,而不允许子组件直接修改父组件的数据。需要调用set
方法更新,当 React
感应到 set
触发时会再次调用 render
对 dom
进行刷新
msg = "Hello" // ❌ 错误写法setMsg('Hello'); // ✅ 来自hooks的set写法 后面会介绍
🤔 Vue
本质上底层也是单向的数据流,只不过对使用者来说看起来是双向的,如 v-model
本质也要 set
React Hooks
React Hooks
是 React 16.8
版本中引入的特性,它可以让我们在 函数组件 中使用状态(state)
和其他 React
特性
Hooks
本质是一些管理组件状态和逻辑的 API
,它允许开发者在 函数式组件 中使用状态、副作用和钩子函数,可以更加方便地管理组件状态、响应式地更新DOM、使用上下文等
在没有 Hooks
前, 函数组件 不能拥有状态,只能做简单功能的UI(静态元素展示),大家使用 类组件 来做状态组件
因为 函数组件 更加匹配 React
的设计理念 UI = f(data)
,也更有利于逻辑拆分与重用的组件表达形式,为了能让 函数组件 可以拥有自己的状态,Hooks
应运而生
组件的逻辑复用
在 Hooks
出现之前,React
先后尝试了 mixins混入
,HOC高阶组件
,render-props
等模式。但是都有各自的问题,比如 mixins
的数据来源不清晰,高阶组件的嵌套问题等等
class组件自身的问题
class组件就像一个厚重的‘战舰’ 一样,大而全,提供了很多东西,有不可忽视的学习成本,比如各种生命周期,this指向问题等等
useState
参数接受一个默认值,返回
[value, setValue]
的元组(就是约定好值的JavaScript
数组),来读取和修改数据
👇 不使用 Hooks
的静态组件,当点击修改数据,视图不会重新渲染
function App() {let count = 1const add = () => count++ // 不会触发重新渲染return <div onClick={add}>{count}</div>
}
👇 使用 useState
import { useState } from 'react'function App() {let count = 1const [proxyCount, setProxyCount] = useState(count)const add = () => setProxyCount(proxyCount+1)return <div onClick={add}>{proxyCount}</div>
}
我们分析一下触发数据修改的 函数组件行为:
组件会第二次渲染(useState
返回的数组第二项 setProxyCount()
被执行就会触发重新渲染)
点击按钮,调用
setProxyCount(count + 1)
修改状态,因为状态发生改变,所以,该组件会重新渲染组件重新渲染时,会再次执行该组件中的代码逻辑
再次调用
useState(1)
,此时React
内部会拿到最新的状态值而非初始值,比如,该案例中最新的状态值为2
再次渲染组件,此时,获取到的状态
count
值为2
👆 也就是触发重新渲染会让 useState
也重新执行,但是 useState
的参数(初始值)只会在组件第一次渲染时生效
每次的渲染,useState
获取到都是最新的状态值,React 组件会记住每次最新的状态值
useEffect
上面我们分析触发组件重新渲染就可以发现,React
的函数组件没有具体的生命周期钩子
React
更希望我们把组件当作函数,而去关注函数的函数的副作用,而没有实例化过程的钩子
useEffect
就可以很好的帮助我们达到我们想要的效果:
处理组件第一次渲染时的回调,类似
Vue
中的mounted
// 第二个参数传一个空数组,表示没有依赖,只会在第一次渲染时执行
useEffect(() => {alert('mounted');
}, [])
通过依赖变更触发的钩子函数,只要有一项依赖发生变化就执行,类似
Vue
中的watch
function Comp({ title }) {const [count, setCount] = useState(0);// 第二个参数指定一个数组,放入你想监听的依赖:useEffect(() => {console.log('title or count has changed.')}, [title, count])
}
原则上,函数中用到的所有依赖都应该放进数组里
组件卸载时执行内部
return
的函数
import { useEffect } from "react"const App = () => {useEffect(() => {const timerId = setInterval(() => {console.log('定时器在运行')}, 1000)return () => { // 用来清理副作用的事情clearInterval(timerId)}}, [])return <div>内部有定时器</div>
}
我们常见的副作用 1. 数据请求ajax发送 2. 手动修改dom 3. localstorage操作
自定义 Hooks
获取滚动距离y:
import { useState, useEffect } from "react"export function useWindowScroll () {const [y, setY] = useState(0)useEffect(() => {const scrollHandler = () => {const h = document.documentElement.scrollTopsetY(h)}window.addEventListener('scroll', scrollHandler)return () => window.removeEventListener('scroll', scrollHandler)})return [y]
}
使用:
const [y] = useWindowScroll()
return <div>{y}</div>
![69b52b6088b502552088fca1769bea69.gif](https://img-blog.csdnimg.cn/img_convert/69b52b6088b502552088fca1769bea69.gif)
封装的 Hooks
名称也要用 use
开头(这是一个约束)
状态管理
React
的 状态管理 有很多,入门可以暂时不考虑
或者已有项目使用什么再学习即可,和 Vuex
整体思路差不多
tic-tac-toe 井字棋游戏
最后我们跟着 React
官方文档实现一个井字棋游戏来巩固知识点
使用 Vite
创建项目
![455015af4e8073a587fcb05f4864b99d.png](https://img-blog.csdnimg.cn/img_convert/455015af4e8073a587fcb05f4864b99d.png)
pnpm create vite react-tic-tac-toe --template react
cd react-tic-tac-toe
pnpm i
pnpm dev
👇 vite.config.js
非常简洁
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'export default defineConfig({plugins: [react()],
})
👇 修改入口文件 main.jsx
import React, { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import "./index.css";import App from "./App";const root = createRoot(document.getElementById("root"));
root.render(<StrictMode><App /></StrictMode>
);
👇 util.js
计算当前棋局是否有获胜
// 计算当前棋局是否有获胜
export function calculateWinner(squares) {const lines = [[0, 1, 2],[3, 4, 5],[6, 7, 8],[0, 3, 6],[1, 4, 7],[2, 5, 8],[0, 4, 8],[2, 4, 6],];for (let i = 0; i < lines.length; i++) {const [a, b, c] = lines[i];if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {return squares[a];}}return null;
}
👇 Square.jsx
正方形按钮组件
// 正方形按钮组件
export default function Square({ value, onSquareClick }) {return (<button className="square" onClick={onSquareClick}>{value}</button>);
}
👇 App.jsx
import { useState } from 'react';
import { calculateWinner } from './util.js'
import Square from './Square'function Board({ xIsNext, squares, onPlay }) {function handleClick(i) {if (calculateWinner(squares) || squares[i]) {return;}const nextSquares = squares.slice();if (xIsNext) {nextSquares[i] = 'X';} else {nextSquares[i] = 'O';}// 执行父组件的落子事件onPlay(nextSquares);}const winner = calculateWinner(squares);let status;if (winner) {// 胜利提示status = '获胜方是: ' + winner;} else {// 下一步提示status = 'Next player: ' + (xIsNext ? 'X' : 'O');}return (<><div className="status">{status}</div><div className="board-row"><Square value={squares[0]} onSquareClick={() => handleClick(0)} /><Square value={squares[1]} onSquareClick={() => handleClick(1)} /><Square value={squares[2]} onSquareClick={() => handleClick(2)} /></div><div className="board-row"><Square value={squares[3]} onSquareClick={() => handleClick(3)} /><Square value={squares[4]} onSquareClick={() => handleClick(4)} /><Square value={squares[5]} onSquareClick={() => handleClick(5)} /></div><div className="board-row"><Square value={squares[6]} onSquareClick={() => handleClick(6)} /><Square value={squares[7]} onSquareClick={() => handleClick(7)} /><Square value={squares[8]} onSquareClick={() => handleClick(8)} /></div></>);
}export default function Game() {const [history, setHistory] = useState([Array(9).fill(null)]);const [currentMove, setCurrentMove] = useState(0);const xIsNext = currentMove % 2 === 0;const currentSquares = history[currentMove];// 棋盘落子function handlePlay(nextSquares) {const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];// 记录落子历史,用于恢复棋局setHistory(nextHistory);setCurrentMove(nextHistory.length - 1);}// 恢复棋局到第几步function jumpTo(nextMove) {setCurrentMove(nextMove);}// 历史落子列表按钮展示,用于点击恢复棋局const moves = history.map((squares, move) => {let description;if (move > 0) {description = 'Go to move #' + move;} else {description = 'Go to game start';}return (<li key={move}><button onClick={() => jumpTo(move)}>{description}</button></li>);});return (<div className="game"><div className="game-board"><Board xIsNext={xIsNext} squares={currentSquares} onPlay={handlePlay} /></div><div className="game-info"><ol>{moves}</ol></div></div>);
}
![78bd388618bc5655a66f3466de644eec.png](https://img-blog.csdnimg.cn/img_convert/78bd388618bc5655a66f3466de644eec.png)
深入学习任一前端框架都不容易,让我们一起加油吧!
参考资料
React 新文档- https://react.dev
React 中文文档(翻译中)- https://react.jscn.org
给 Vue 开发的 React 上手指南- https://juejin.cn/post/6952545904087793678
无缝切换?从Vue到React- https://zhuanlan.zhihu.com/p/609120596
How to Learn React in 2023- https://www.freecodecamp.org/news/how-to-learn-react-in-2023
想了解更多转转公司的业务实践,点击关注下方的公众号吧!