2023 Vue开发者的React入门

大家好,我是若川。我持续组织了近一年的源码共读活动,感兴趣的可以 点此扫码加我微信 lxchuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列。另外:目前建有江西|湖南|湖北|河南籍前端群,可加我微信进群。欢迎星标我的公众号~不错过推文~


c3b30a7bf9e9b25d603344e399d6bbd0.png

VueReact 都是流行的 JavaScript 框架,它们在组件化、数据绑定等方面有很多相似之处

本文默认已有现代前端开发(Vue)背景,关于 组件化、前端路由、状态管理 概念不会过多介绍

0基础建议详细阅读 Thinking in React-官方文档 了解 React 的设计哲学

  • React 新文档- https://react.dev

  • React 中文文档(翻译中)- https://react.jscn.org

经过本文的学习让没开发过 React 项目的 Vue 开发者可以上手开发现有的 React 项目,完成工作需求开发

React 新文档

React 新文档重新设计了导航结构,让我们更加轻松地找到所需的文档和示例代码 不仅提供了基础知识的介绍,还提供了更加详细的原理介绍和最佳实践,包括:React 组件的设计哲学、React Hooks的原理和用法等

并且提供了在线编辑和运行的功能,方便开发者进行测试和实验

👇 基于 函数组件

cd1750deed929bc0b6acac9a6671ba28.png

初学可以只学 函数组件,You Don't Need to Learn Class Components

cda75cdb91a6ffc1c7e0dc6d168cb7b0.png

👇 interactive sandboxes 可交互沙箱,边做边学

c5531b216a9a9e33d99e249e2720330e.png

Fork 可以单独打开页签

27d09f5a7755e7d71cc795bf255c0b3c.png

JSX 与 SFC

  • Vue 中我们使用 单文件组件(SFC) 编写组件模版 (虽然 Vue 也支持使用 JSX , 但是更鼓励使用SFC)

  • React 中,JSX(JavaScript XML)是一种将HTML语法嵌入到 JavaScript 中的语法扩展。它可以使得我们在 JavaScript 代码中轻松地定义组件的结构和样式,从而提高代码的可读性和可维护性

虽然 ReactVue 在组件定义方式上存在差异,但是它们的组件化思想是相似的

根节点

👇 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}/>

可以看出 ReactJSX语法 学习记忆成本更低一点(当然Vue也不复杂),Vue 更语法糖一些

单向数据流与双向绑定

Vue 中,我们使用 v-bindv-modal对数据进行绑定,无论是来自用户操作导致的变更,还是在某个方法里赋值都能够直接更新数据,不需要手动进行 update 操作

this.data.msg = '直接修改数据后视图更新'

React 中,数据流是单向的,即从父组件传递到子组件,而不允许子组件直接修改父组件的数据。需要调用set 方法更新,当 React 感应到 set 触发时会再次调用 renderdom 进行刷新

msg = "Hello" // ❌ 错误写法

setMsg('Hello'); // ✅ 来自hooks的set写法 后面会介绍

🤔 Vue 本质上底层也是单向的数据流,只不过对使用者来说看起来是双向的,如 v-model 本质也要 set

React Hooks

React HooksReact 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 = 1
  const add = () => count++ // 不会触发重新渲染

  return <div onClick={add}>{count}</div>
}

👇 使用 useState

import { useState } from 'react'

function App() {
  let count = 1
  const [proxyCount, setProxyCount] = useState(count)
  const add = () => setProxyCount(proxyCount+1)

  return <div onClick={add}>{proxyCount}</div>
}

我们分析一下触发数据修改的 函数组件行为

组件会第二次渲染(useState 返回的数组第二项 setProxyCount() 被执行就会触发重新渲染)

  1. 点击按钮,调用 setProxyCount(count + 1) 修改状态,因为状态发生改变,所以,该组件会重新渲染

  2. 组件重新渲染时,会再次执行该组件中的代码逻辑

  3. 再次调用 useState(1),此时 React 内部会拿到最新的状态值而非初始值,比如,该案例中最新的状态值为 2

  4. 再次渲染组件,此时,获取到的状态 count 值为 2

👆 也就是触发重新渲染会让 useState 也重新执行,但是 useState 的参数(初始值)只会在组件第一次渲染时生效

每次的渲染,useState 获取到都是最新的状态值,React 组件会记住每次最新的状态值

useEffect

上面我们分析触发组件重新渲染就可以发现,React 的函数组件没有具体的生命周期钩子

React 更希望我们把组件当作函数,而去关注函数的函数的副作用,而没有实例化过程的钩子

useEffect 就可以很好的帮助我们达到我们想要的效果:

  1. 处理组件第一次渲染时的回调,类似 Vue 中的 mounted

// 第二个参数传一个空数组,表示没有依赖,只会在第一次渲染时执行
useEffect(() => {
  alert('mounted');
}, [])
  1. 通过依赖变更触发的钩子函数,只要有一项依赖发生变化就执行,类似 Vue 中的 watch

function Comp({ title }) {
  const [count, setCount] = useState(0);
  // 第二个参数指定一个数组,放入你想监听的依赖:
  useEffect(() => {
    console.log('title or count has changed.')
  }, [title, count])
}

原则上,函数中用到的所有依赖都应该放进数组里

  1. 组件卸载时执行内部 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.scrollTop
      setY(h)
    }
    window.addEventListener('scroll', scrollHandler)
    return () => window.removeEventListener('scroll', scrollHandler)
  })

  return [y]
}

使用:

const [y] = useWindowScroll()
return <div>{y}</div>
6656a0bf069cc399f3c0057282792009.gif

封装的 Hooks 名称也要用 use 开头(这是一个约束)

状态管理

React状态管理 有很多,入门可以暂时不考虑

或者已有项目使用什么再学习即可,和 Vuex 整体思路差不多

tic-tac-toe 井字棋游戏

最后我们跟着 React 官方文档实现一个井字棋游戏来巩固知识点

使用 Vite 创建项目

9eee4875e36f0f36cc9fdb701d1c744b.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>
  );
}
beb118b27db952455dbbc84219232ffb.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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值