react 基础

132 阅读12分钟

前端和 JavaScript 是一个奇怪的世界。大量不断推出的新技术的同时,也在被不需要它们的人嘲笑,往往很多人都会这样做。我们有时会对不断涌现的信息、库和讨论感到不知所措,总希望能有一些稳定的东西,就像能让我们可以休整一段时间的避风港。最近 React 似乎有变成 JavaScript 演变海洋中温暖港湾的趋势。

入口文件

我们一般会在项目的src/index.js文件中初始化项目,即创建根节点后将App组件渲染到根节点上。

// src/index.js:
import ReactDOM from 'react-dom'; 

function App() { 
    return <div>app</div> 
} 

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App/>);

jsx

jsx:这个有趣的标签语法既不是字符串也不是 HTML,它是一个 JavaScript 的语法扩展。

注意:因为 JSX 语法上更接近 JavaScript 而不是 HTML,所以 React DOM 使用 camelCase(小驼峰命名)来定义属性的名称,而不使用 HTML 属性名称的命名约定。 例如,JSX 里的 class 变成了 className,而 tabindex 则变为 tabIndex。

可以在jsx中嵌入表达式

function formatName(user) {
  return user.firstName + ' ' + user.lastName;
}

const user = {
  firstName: 'Harper',
  lastName: 'Perez'
};

const element = (
  <h1>
    Hello, {formatName(user)}!  </h1>
);

JSX 中指定属性

// 通过使用引号,来将属性值指定为字符串字面量
const element = <a href="https://www.reactjs.org"> link </a>;
// 使用大括号,来在属性值中插入一个 JavaScript表达式
const element = <img src={user.avatarUrl}></img>;

使用 JSX 指定子元素

const element = <img src={user.avatarUrl} />;
// JSX 标签里能够包含很多子元素
const element = (
  <div>
    <h1>Hello!</h1>
    <h2>Good to see you here.</h2>
  </div>
);

元素渲染

import ReactDOM from 'react-dom'; 

const root = ReactDOM.createRoot(
  document.getElementById('root')
);

const element = <h1>Hello, world</h1>;
root.render(element);

组件 & Props

组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props”),并返回用于描述页面展示内容的 React 元素。

组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。

函数组件

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

class 组件

import React from 'react';

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

渲染组件

import ReactDOM from 'react-dom'; 

function Welcome(props) {  
    return <h1>Hello, {props.name}</h1>;
}

const root = ReactDOM.createRoot(document.getElementById('root'));
// React元素也可以是用户自定义的组件
const element = <Welcome name="Sara" />;
root.render(element);

这个例子中发生了什么:

  1. 我们调用 root.render() 函数,并传入 <Welcome name="Sara" /> 作为参数。
  2. React 调用 Welcome 组件,并将 {name: 'Sara'} 作为 props 传入。
  3. Welcome 组件将 <h1>Hello, Sara</h1> 元素作为返回值。
  4. React DOM 将 DOM 高效地更新为 <h1>Hello, Sara</h1>

注意: 组件名称必须以大写字母开头。 React 会将以小写字母开头的组件视为原生 DOM 标签。例如,<div /> 代表 HTML 的 div 标签,而  则代表一个组件,并且需在作用域内使用 Welcome。

组合组件

组件可以在其输出中引用其他组件。这就可以让我们用同一组件来抽象出任意层次的细节。

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

function App() {
  return (
    <div>
      <Welcome name="Sara" />      
      <Welcome name="Cahal" />      
      <Welcome name="Edite" />    
    </div>
  );
}

Props 的只读性

组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props。

State & 生命周期

不要直接修改 State

// Wrong,不会重新渲染组件
this.state.comment = 'Hello';

而是应该使用 setState():

// Correct
this.setState({comment: 'Hello'});

构造函数是唯一可以给 this.state 赋值的地方。

// src/App.js:
import React from "react";

class App extends React.Component {
    constructor() {
        super();
        this.state = {
            count: 0,
        };
    }
    changeCount = () => {
        this.setState((state, props) => ({
            count: state.count + 1,
        }));
    };
    render() {
        const { count } = this.state;
        return (
            <div className="wrapper">
                <p>hello word!</p>
                <p>数量:{count}</p>
                <button
                  onClick={() => {
                    this.changeCount();
                  }}>修改 count</button>
            </div>
        );
    }
}

export default App;

state 的更新可能是异步的

出于性能考虑,React 可能会把多个 setState() 调用合并成一个调用。

因为 this.props 和 this.state 可能会异步更新,所以你不要依赖他们的值来更新下一个状态。

// Wrong, 此代码可能会无法更新计数器
this.setState({
    counter: this.state.counter + this.props.increment,
});

要解决这个问题,可以让 setState() 接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数

// Correct
this.setState((state, props) => ({
    counter: state.counter + props.increment,
}));

// 或
// Correct
this.setState(function(state, props) {
    return {
        counter: state.counter + props.increment,
    };
});

State 的更新会被合并
当你调用 setState() 的时候,React 会把你提供的对象合并到当前的 state。

constructor(props) {
    super(props);
    this.state = {
        posts: [],      
        comments: [],    
    };
}

然后你可以分别调用 setState() 来单独地更新它们

componentDidMount() {
    fetchPosts().then(response => {
        this.setState({
            posts: response.posts      
        });
    });

    fetchComments().then(response => {
        this.setState({
            comments: response.comments      
        });
    });
}

这里的合并是浅合并,所以 this.setState({comments}) 完整保留了 this.state.posts, 但是完全替换了 this.state.comments

拓展
React中setState方法详解
深入理解React 组件状态(State)

状态提升

在 React 中,将多个组件中需要共享的 state 向上移动到它们的最近共同父组件中,便可实现共享 state。这就是所谓的“状态提升”。

React 的状态提升就是用户对子组件操作,子组件不改变自己的状态,通过自己的props把这个操作改变的数据传递给父组件,改变父组件的状态,从而改变受父组件控制的所有子组件的状态,这也是React单项数据流的特性决定的。

function tryConvert(temperature, convert) {
    const input = parseFloat(temperature);
    if (Number.isNaN(input)) {
        return '';
    }
    const output = convert(input);
    const rounded = Math.round(output * 1000) / 1000;
    return rounded.toString();
}
class TemperatureInput extends React.Component {
    constructor(props) {
        super(props);
        this.handleChange = this.handleChange.bind(this);
    }

    handleChange(e) {
        this.props.onTemperatureChange(e.target.value);  
    }

    render() {
        const temperature = this.props.temperature;    const scale = this.props.scale;
        return (
            <fieldset>
                <legend>Enter temperature in { scaleNames[scale] }:</legend>
                <input value={ temperature }
                    onChange={ this.handleChange } />
            </fieldset>
        );
    }
}
function BoilingVerdict(props) {
    if (props.celsius >= 100) {
        return <p>The water would boil.</p>;  
    }
    return <p>The water would not boil.</p>;
}
class Calculator extends React.Component {
    constructor(props) {
        super(props);
        this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
        this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
        this.state = { 
            temperature: '', 
            scale: 'c', 
        };  
    }

    handleCelsiusChange(temperature) {
        this.setState({ 
            scale: 'c', 
            temperature, 
        });  
    }

    handleFahrenheitChange(temperature) {
        this.setState({ 
            scale: 'f', 
            temperature, 
        });  
    }

    render() {
        const scale = this.state.scale;    
        const temperature = this.state.temperature;    
        const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;  
        const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
        return (
            <div>
                <TemperatureInput
                    scale="c"
                    temperature={ celsius }          
                    onTemperatureChange={ this.handleCelsiusChange } />        
                <TemperatureInput
                    scale="f"
                    temperature={ fahrenheit }          
                    onTemperatureChange={ this.handleFahrenheitChange } />        
                <BoilingVerdict
                    celsius={ parseFloat(celsius) } />      
            </div>
        );
    }
}

梳理一下当你对输入框内容进行编辑时会发生些什么:
① React 会调用 DOM 中 <input> 的 onChange 方法。在本实例中,它是 TemperatureInput 组件的 handleChange 方法;
TemperatureInput 组件中的 handleChange 方法会调用 this.props.onTemperatureChange(),并传入新输入的值作为参数。其 props 诸如 onTemperatureChange 之类,均由父组件 Calculator 提供;
③ 起初渲染时,用于摄氏度输入的子组件 TemperatureInput 中的 onTemperatureChange 方法与 Calculator 组件中的 handleCelsiusChange 方法相同,而,用于华氏度输入的子组件 TemperatureInput 中的 onTemperatureChange 方法与 Calculator 组件中的 handleFahrenheitChange 方法相同。因此,无论哪个输入框被编辑都会调用 Calculator 组件中对应的方法;
④ 在这些方法内部,Calculator 组件通过使用新的输入值与当前输入框对应的温度计量单位来调用 this.setState() 进而请求 React 重新渲染自己本身;
⑤ React 调用 Calculator 组件的 render 方法得到组件的 UI 呈现。温度转换在这时进行,两个输入框中的数值通过当前输入温度和其计量单位来重新计算获得;
⑥ React 使用 Calculator 组件提供的新 props 分别调用两个 TemperatureInput 子组件的 render 方法来获取子组件的 UI 呈现;
⑦ React 调用 BoilingVerdict 组件的 render 方法,并将摄氏温度值以组件 props 方式传入;
⑧ React DOM 根据输入值匹配水是否沸腾,并将结果更新至 DOM。我们刚刚编辑的输入框接收其当前值,另一个输入框内容更新为转换后的温度值。

拓展
状态提升
react中状态提升
react 的状态提升

Refs 转发

转发 refs 到 DOM 组件

import React from "react";

const MyButton = React.forwardRef((props, ref) => {
  const handleClick = (e) => {
    console.log(e.current); // <button>点击我</button>
  };
  return (
    <button ref={ref} onClick={() => handleClick(ref)}>
      {props.children}
    </button>
  );
});
const ref = React.createRef();

function App() {
  return (
    <div className="App">
      <MyButton ref={ref}>点击我</MyButton>
    </div>
  );
}

export default App;

上述示例发生情况的逐步解释:

  1. 我们通过调用 React.createRef 创建了一个 React ref 并将其赋值给 ref 变量。
  2. 我们通过指定 ref 为 JSX 属性,将其向下传递给 <MyButton ref={ref}>
  3. React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数。
  4. 我们向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性。
  5. 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点。

注意:第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref
Ref 转发不仅限于 DOM 组件,你也可以转发 refs 到 class 组件实例中。 在高阶组件中转发 refs

hook

① useState

// src/App.js:
import { useState } from "react";

function App() {
    const [val, setVal] = useState("初始化数据");
    const [count, setCount] = useState(0);
    
    const handleVal = () => {
        setVal("数据被修改");
    };
    const handleCount = () => {
        setCount(count + 1);
    };
    return (
        <div className="App">
            <p>{val}</p>
            <button onClick={() => handleVal()}>修改数据</button>
            <p>数量:{count}</p>
            <button onClick={() => handleCount()}>修改数量</button>
        </div>
    );
}

② useEffect

useEffect 用于处理组件中的 effect (副作用),用来取代生命周期函数,通常用于请求数据,事件处理,订阅等相关操作。

useEffect(()=>{ //副作用函数
    return ()=>{ // 返回函数

    }
},[依赖参数])

依赖参数不同时有不同的效果:
为空: 组件的任何更新,该 useEffect 对应的返回函数和函数都执行;
为空数组: 不监听组件的更新;
数组中有具体依赖:对应的依赖数据,有变化的时候,才会执行(如果数组中有多个元素,即使只有一个元素发生变化,React 也会执行 effect)。

// 1.组件挂载完成及更新完成做某事(模拟 class组件的 DidMount和 DidUpdate);
useEffect(()=>{
    console.log("组件挂载完成之后及更新完成之后执行");
})

// 2.组件挂载完之后做某事(模拟 class组件的 DidMount);
useEffect(()=>{
    console.log("组件挂载完之后执行");
}, []); // 第二个参数是 [](不依赖于任何 state)

// 3.组件更新完做某事(模拟 class组件的 DidUpdate);
useEffect(() => {
    console.log('更新了')
}, [count, name]) // 第二个参数就是依赖的 state

// 4. 组件挂载完之后做某事(模拟 class 组件的 DidMount);
useEffect(() => {
    let timerId = window.setInterval(() => {
        console.log(Date.now())
    }, 1000)

    // 返回一个函数
    // 4.组件即将卸载做某事(模拟 WillUnMount组件销毁的时候 停止计时器);
    return () => {
        console.log("组件即将卸载时执行");
        window.clearInterval(timerId)
    }
}, [])
useEffect(
    () => {
        const subscription = props.source.subscribe();
        return () => {
            subscription.unsubscribe();
        };
    },
    [props.source], // 只有当 `props.source`改变后才会重新创建订阅
);
// src/App.js:
import React, { useEffect, useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log(`点击了${count}次`);
  });
  return (
    <div>
      <p>已经点击{count}次</p>
      <button onClick={() => setCount(count + 1)}>添加次数</button>
    </div>
  );
}

export default App;

拓展
使用 Effect Hook
useEffect模拟组件生命周期
useEffect使用

③ useContext

拓展
React的useContext的使用
useContext的基本用法
useContext 使用指南

④ useReducer

useReducer 是 useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)

const [state, dispatch] = useReducer(reducer, initialArg, init);

useReducer接收两个参数:
① 第一个参数:reducer函数;
② 第二个参数:初始化的 state。返回值为最新的 state和 dispatch函数(用来触发 reducer函数,计算对应的 state)。

在某些场景下,useReducer 会比 useState 更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。

reducer
reducer 本质是一个纯函数,没有任何 UI和副作用。这意味着相同的输入(state、action),reducer函数无论执行多少遍始终会返回相同的输出(newState)。因此通过 reducer函数很容易推测 state的变化,并且也更加容易单元测试。

count 有可能只是 state中的一个属性。针对这种场景我们可以使用 ES6的结构赋值:

// 返回一个 newState (newObject)
function countReducer(state, action) {
    switch(action.type) {
        case 'add':
            return { ...state, count: state.count + 1; }
        case 'sub':
            return { ...state, count: state.count - 1; }
        default: 
            return count;
    }
}

简单来说 reducer是一个函数(state, action) => newState:接收当前应用的 state和触发的动作 action,计算并返回最新的 state。

关于上面这段代码有两个重要的点需要我们记住:
① reducer处理的 state对象必须是 immutable,这意味着永远不要直接修改参数中的 state对象,reducer函数应该每次都返回一个新的 state object;
② 既然 reducer要求每次都返回一个新的对象,我们可以使用 ES6中的解构赋值方式去创建一个新对象,并复写我们需要改变的 state属性,如上例。

action
用来表示触发的行为。

① 用 type来表示具体的行为类型(登录、登出、添加用户、删除用户等);
② 用 payload携带的数据(如增加书籍,可以携带具体的book信息),我们用 addBook的 action为例:

const action = {
    type: 'addBook',
    payload: {
        book: {
            bookId,
            bookName,
            author,
        }
    }
}
function bookReducer(state, action) {
    switch(action.type) {
        // 添加一本书
        case 'addBook':
            const { book } = action.payload;
            return {
                ...state,
                books: {
                    ...state.books,
                    [book.bookId]: book,
                }
            };
        case 'sub':
            // ....
        default: 
            return state;
    }
}

用 reducer 重写 useState 计数器示例

// useState计数器
function Counter({initialCount}) {
    const [count, setCount] = useState(initialCount);
    return (
        <>
            Count: {count}
            <button onClick={() => setCount(initialCount)}>Reset</button>
            <button onClick={() => setCount(prevCount => prevCount - 1)}>-</button>
            <button onClick={() => setCount(prevCount => prevCount + 1)}>+</button>
        </>
    );
}
//	reducer 计数器
const initialState = {count: 0};

const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return {count: state.count + 1};
        case 'decrement':
            return {count: state.count - 1};
        default:
            throw new Error();
    }
};

function Counter() {
    const [state, dispatch] = useReducer(reducer, initialState);
    return (
        <>
            Count: {state.count}
            <button onClick={() => dispatch({type: 'decrement'})}>-</button>
            <button onClick={() => dispatch({type: 'increment'})}>+</button>
        </>
    );
}

拓展
useReducer 介绍和基本使用
这一次彻底搞定useReducer-基础篇

⑤ useRef

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

const refContainer = useRef(initialValue);

① 返回一个可变的 ref 对象,该对象只有个 current 属性,初始值为传入的参数( initialValue );
② 返回的 ref 对象在组件的整个生命周期内保持不变;
③ 当更新 current 值时并不会 re-render ,这是与 useState 不同的地方;
④ 更新 useRef 是 side effect (副作用),所以一般写在 useEffect 或 event handler 里;
⑤ useRef 类似于类组件的 this;
⑥ 可以很方便地保存任何可变值,其类似于在 class 中使用实例字段的方式。

// src/App.js:
import React, { useRef } from "react";

function App() {
    const refDom = useRef();
    const refCount = useRef(8);

    const handleRefDom = () => {
        console.log(refDom); // {current: div}
        console.log(refDom.current); // <div>refDom 元素</div>
    };
    const handleRefCount = () => {
        console.log(refCount); // {current: 8}
    };
    return (
        <div>
            <div ref={refDom}>refDom 元素</div>
            <button onClick={() => handleRefDom()}>refDom 按钮</button>
            <button onClick={() => handleRefCount()}>获取 ref 数据</button>
        </div>
    );
}

export default App;

拓展
Hook API索引
useRef使用总结
useRef详细总结
useRef 的使用

⑥ useMemo

useMemo是针对一个函数,是否多次执行;
useMemo主要用来解决使用 React hooks产生的无用渲染的性能问题;
在方法函数,由于不能使用 shouldComponentUpdate 处理性能问题,react hooks 新增了useMemo。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

返回一个 memoized 值。

useMemo使用
① 如果useMemo(fn, arr)第二个参数匹配,并且其值发生改变,才会多次执行执行,否则只执行一次,如果为空数组 [ ],fn只执行一次;
② 如果没有提供第二个参数依赖项数组,useMemo 在每次渲染时都会计算新的值。

案例
第一次进来时,控制台显示rich child,当无限点击按钮时,控制台不会打印rich child。但是当改变props.name为props.isChild时,每点击一次按钮,控制台就会打印一次rich child

import React, { useState, useMemo } from "react";

function App() {
    let [isChild, setChild] = useState(false);

    return (
        <div>
            <Child isChild={isChild} name="child" />
            <button onClick={() => setChild(!isChild)}>改变Child</button>
        </div>
    );
}

let Child = (props) => {
    console.log(props); // {isChild: false, name: 'child'}
    let getRichChild = () => {
        console.log("rich child");
        return "rich child";
    };

    let richChild = useMemo(() => {
        return getRichChild();
    }, [props.name]);

    return (
        <div>
            isChild: {props.isChild ? "true" : "false"}
            <br />
            {richChild}
        </div>
    );
};

export default App;

拓展
useMemo
useMemo和useCallback的区别及使用场景