React文档学习

257 阅读15分钟

有消息说为了增强人员流动让我们去其他项目组待一阵,那边用react,没用过,做个react学习笔记浅学习一下。

后续:到时间了,结果说看那边项目进度在让我们过去。不过我看够呛了,拖着拖着没准这事儿没了。不过趁机了解一下react也不错。

碎片React学习之旅:看React官方文档的时候的摘抄、碎碎念,内容来自官方文档。

快速入门

组件

组件-->React应用

组件:是返回了HTML标签的JS函数。

组件可以调用 ,调用的时候第一个字母大写,如<MyComponents />

JSX

JSX语法是可选的,但多数React会用到JSX。

JSX更严格,一个函数返回一个JSX标签,标签必须闭合,空标签包裹也可以:

<>
<div> 1 <div/> 
<div> 2 <div/> 
</>

JSX里面,if用JS中的if,循环借助 Array.map() 来实现。
究其根本,都需要把HTML放置在一个变量里面,再将存放的HTML标签(变量)返回。

响应事件:onClick={handleClick}

单向数据流

单向数据流(文档里写的是希望组件记住一些信息并展示):

import { useState } from 'react';
conat [count, setCount] = useState(0);

当前的 state(count),以及用于更新它的函数(setCount)。
修改count的值时,可以使用setCount(count+1)的语法。

每个组件有自己的 state ,是分离的。

use开头的函数统称为Hook
比如useState是React内置的Hook。

每个组件的state是独立的,如果想互通,需要将互通的变量提到共同的父组件中定义,状态提升,然后通过传参传递给子组件(子组件是函数返回标签,可以接收参数,vue中通过prop传递,react通过参数传递)。

单向数据流相当于可以修改值在页面回显,但是不能在页面修改值绑定到state中。

类比:vue中 v-model = v-bind + v-on

单项数据绑定:仅 v-bind

描述用户界面

HTML标签第一个字母小写,自定义组件第一个字母大写(定义和调用时)。

组件内不可以定义组件。但是组件内可以调用组件,形成组件的嵌套。

导入时,import xx from '../xx.js'更符合原生ES模块。不过../xx也能用。

JSX 规则

只能返回1个根元素,即使这个根元素是空标签<></>。这个空标签被称为 Fragment,实际上是React的一个组件<Fragment></Fragment>,被简写成JSX的空标签。
空标签是因为,JSX实际上会转换为JavaScript对象,一个函数返回一个对象,如果是多个对象需要用数组包裹。

标签必须闭合。

驼峰命名给大部分属性。
在JavaScript中class是一个保留字(类),所以在JavaScript中返回HTML标签的话,class用className代替
内联style里面的属性也需要驼峰。

由于历史原因,aria-*data-* 属性是以带 - 符号的 HTML 格式书写的。

JSX中使用大括号。大括号里面一般就是变量。

props

定义组件时,props是组件的唯一参数。

示例:

function MyComponent(props){
    const a = props.a;
    const b = props.b;
    console.log(a, b);
    // ...
}
// 等价于:
function MyComponent({ a, b }){
    console.log(a, b);
    // ...
}

// 调用时:

<Mycomponents a='a' b='b'/>

组件的嵌套

我认为类似【vue中的插槽】。

也是通过 props实现的。

function Outer({ children }){
    return (
        <div className="outer-class">
            {children}
        </div>
    )
}

function Inner(){
    return (
        <p>内部内容</p>
    );
}

// 使用:
<Outer>
    <Inner />
</Outer>

这种嵌套的JSX,我们视InnerOuterChildrenProp。是外面那层的一个prop。

条件渲染

在JSX中一般通过if语句&&? :来实现。

if(xx) return <div> 1 </div>

如果不想渲染任何东西,可以返回null

不可以把数字放在 && 左侧:
左边最好是一个布尔类型。

let a = 1;
concole.log(a && 1); // 1
a = 0;
console.log(a && 1); // 0

如上例,本意想a && 1a = 0 时不渲染,但事实上渲染出来了 0 。

切勿将数字放在 && 左侧.

JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。然而,如果左侧是 0,整个表达式将变成左侧的值(0),React 此时则会渲染 0 而不是不进行渲染。

渲染列表

使用filter()筛选需要渲染的组件,使用map()将数组变为组件数组。

keyvuev-forkey一样,唯一标识。

为每个列表项显示多个DOM节点。需要用到之前说的空标签,即<Fragment> </Fragment>。把多个DOM节点放进去,key值写在空标签上

import { Fragment } from 'react'

const listItems = xxArr.map(xx => {
    return (
        <Fragment key={xx.id}>
            <div> 1 </div>
            <div> 2 </div>
        </Fragment>
    );
})

数组索引最好别当成key,最然如果没有显式指定key,React会把索引当成key。但是当数组增删的时候,会因为索引值是key产生一些微妙bug。

也不要使用随机函数Math.random()来生成key,不然每次渲染都要重新创建DOM元素,费时费力性能不好。也可能导致用户输入丢失。

组件不会把key当成props的一部分,key,如果需要,可以自己传。

保持组件纯粹

把组件当作纯函数来写。

纯函数:给定相同的输入,每次输出都是一样的。只负责自己的任务,不修改在调用前就已经存在的对象或者变量。

在React中,我们可以在渲染时读取三种输入:propsstatecontext,应该始终视这些输入为只读。不要修改。
如果想修改一些内容,应该设置状态、修改状态。

严格模式可以帮助我们书写纯函数的组件。严格模式不会在生产环境生效。

突变:修改了这些希望只是只读的值。

局部mutation,局部突变,在组件内部的变量。

  • 副作用某些事物在特定情况下不得不改变,如更新屏幕、启动动画、更改数据等。
    在React中,副作用通常属于事件处理程序。

  • 事件处理程序:是React在你执行某些操作,如单击按钮时,运行的函数。
    即使事件处理程序是在组件内部定义的,他们也不会在渲染期间运行。
    因此事件处理程序无需是纯函数。

  • 没有合适的事件处理程序,可以调用组建的useEffect方法将其附加到返回的JSX中。
    这会告诉React在渲染结束之后执行它。
    非必要不使用。

添加交互

响应事件

onClick等。

传入的方法一般是handle + 事件名称,如handleMouseEnterhandleClick等。

注意传入方法(handleClick)而非方法的调用(handleClick()

函数也可以作为 props传递。

自定义事件最好也采用 on + 大写首字母单词的形式,和浏览器事件名称的命名方式一样。

PS:正确使用HTML语义化标签。
如:div仅用作展示块,如需点击,使用button,并为button配置onClick事件,而非为div配置onClick事件。

事件传播:冒泡、传播。

在React,除了onScroll仅使用到所附加的JSX标签。其他的事件都会传播、冒泡。

阻止传播e.stopPropAgation(),阻止事件传播到父级元素。

捕获阶段事件-TODO
捕获子元素上的事件,但是执行顺序?
是先执行父元素上的onClickCapture()再执行子元素上的onClick()吗?不太懂

<div onClickCapture={() => { /* 这会首先执行 */ }}>  
    <button onClick={e => e.stopPropagation()} />  
    <button onClick={e => e.stopPropagation()} />  
</div>

e.preventDefault():阻止默认事件,如点击表单内的任何按钮都会触发表单提交事件。

e.stopPropagation():阻止冒泡。

事件处理函数是执行副作用的最佳位置。不同于渲染函数,事件处理函数不需要是纯函数。
最好在事件处理函数中修改一些值。

state

state:组建的记忆。

为什么使用state ,普通变量为什么不行?
1、局部变量无法持久保存,再次渲染这个组件的时候,会重新渲染而不会考虑之前对局部变量的任何修改。
2、更改局部变量不会触发重新渲染,React不知道需要重新渲染。

记录渲染之间的数据触发重新渲染是更新组件需要做到的。
useStateHook提供了这两个功能。

// 引入
import { useState } from 'react'
// 使用
let [count, setCount] = useState(0)

PS:
Hooks—— 以use开头的函数,只能在组件或者自定义Hook的最顶层调用。
不可以在条件、循环、其他嵌套函数里面调用Hook。
Hook是函数,也可以视为关于组件需求的无条件声明。类似在组件顶部导入模块。

state是隔离且私有的,渲染同一个组件多次,每个组件也有其内部的state

渲染提交

初次渲染,React使用appendChild()DOM API将其创建的所有DOM节点放在屏幕上。

重渲染,React在渲染的时候计算,没有差异不做修改,仅修改有差异的组件。

渲染完成且React更新DOM之后,浏览器就会重新绘制屏幕(浏览器渲染)。

一个React应用中,一次屏幕更新会发生以下步骤:触发、渲染、提交。

state 像一个快照

state的值是上一次渲染之后的值,本次setNumber不会立即生效,而是告知下次number渲染的值。

// 假设number上次渲染的结果为0 
<button onClick={() => {  
    setNumber(number + 1);  // React准备在下次将number渲染为`0+1`
    setNumber(number + 2);  // React准备在下次将number渲染为`1+1`
    setNumber(number + 3);  // React准备在下次将number渲染为`2+1`
}}>+3</button>

// 最终结果为,将number渲染结果修改为3.在原基础上+3。只会执行最后一次的setNumber()。

setNumber不会再本次渲染中修改number值。即使放在异步中,number的快照也是上次渲染的最终结果。

把一系列state更新加入队列

state-批处理:事件处理函数及其中任何代码执行完成之后,统一渲染UI。这种特性称为批处理。

第二次渲染之前多次更新同一state:只有最后一次的setNumber生效

如果setNumber的传参不是下一次期待渲染的state值,而是根据队列中前一个state计算下一个state的更新函数,则多次setNumber都可以生效。

import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(n => n + 1); // 不生效
        setNumber(n => n + 1); // 不生效
        setNumber(n => n + 1); // 生效
        // 最终结果: 0 -> 1
      }}>+3</button>
    </>
  )
}

上例中,number初始化为0,在按钮点击事件中三次更新number,每次+1,则点击一次按钮,number最终 +3, 更新函数累计生效。

如果 click事件中,是如下更新

    setNumber(n => n + 1); // 不生效
    setNumber(n => n + 1); // 不生效
    setNumber(n => n + 1); // 不生效
    setNumber(n); // 生效
    // 最终结果:0 -> 0

只有最后一次setNumber(n);生效。每次点击按钮,number值不变。

即更新函数之后再通过传递state值期待下次渲染,则更新函数无效。

    setNumber(n + 1);// 不生效
    setNumber(n + 1); // 生效 
    setNumber(n => n + 1); // 生效
    // 最终结果:0 -> 2
    setNumber(n + 1); // 不生效
    setNumber(n + 1); // 不生效
    setNumber(n => n + 1); // 不生效
    setNumber(n + 1); // 生效 
    // 最终结果:0 -> 1

如果队列中是则将下次渲染替换为
如果是更新函数,则累加。

/*
    baseState: 初始 state
    queue: 队列
*/
export function getFinalState(baseState, queue) {
  let res = baseState;

  // TODO: 对队列做些什么...
  const len = queue.length
  function isNum(e){ return typeof e === 'number' }
  queue.forEach(e=>{
    if(isNum(e)) res = e
    else res = e(res) 
  })

  return res;
}

PS:更新函数必须是纯函数,并仅返回结果,不在内部进行其他操作。
更新函数在渲染期间执行。

通常用变量的首字母作为更新函数的参数命名。如:

setLastName(ln => ln + '11`');

更新state中的对象\数组

不能obj.xx直接更新。
需要迂回一下。

setObjXx({
    ...objXx,
    a:'a'
})

或者

const tempObj = {}

tempObj.a = 'a'
tempObj.b = 'b'

setObjXx(tempObj)

数组同理。
整个替换而非截取单个属性进行更改。

状态管理

使用State响应输入

声明式UI:通过一个变量控制是否 disabled、 展示什么内容。

命令式UI:状态变更之后,通过JS来更新UI。

Vue和React都是声明式UI。

选取State结构

合并关联的State
如果两个变量总是一起发生变化,可以把这两个变量合并为一个。

const [x,setX] = useState(0);
const [y,setY] = useState(0);
// 替换为:
const [position,setPosition] = useState({x:0, y:0})

避免矛盾的state
如果一个东西有不同的状态,如一个东西有“存在”、“不存在”两种状态。不要用“is存在”和“is不存在”两个变量代指,否则修改其中一个变量时要将另一个变量同时修改,因为“存在”与“不存在”不可能同时为true。
用一个state变量来控制,state值有“存在”、“不存在”两种状态。

避免冗余的state
存在变量a、变量b、a与b的和sum。
可以将变量sun删除。因为可以从a、b的值计算出来。

// 在代码中可以使用常量来计算出sum
const sum = a+b;
// 则sum时在渲染阶段计算出来的

不要再state中镜像props 如果想要给props起个更简洁的名字,可以用常量接收

function Fun({ aaProp }){
    const aa = aaProp;
    // 而非
    const [aa, setAa] = useState(aaProp);
    // 因为,state只会在第一次渲染的时候初始化,
    // 如果后续父组件更新了aaProp的值,子组件Fun接收不到
}

避免重复的state

const [arr, setArr] = useState([
  { title: 'a', id: 0 },
  { title: 'b', id: 1 },
  { title: 'c', id: 2 }
 ])
// 不可取:修改arr[0]的时候容易遗漏
const [item, setItem] = useState(arr[0])
// 可取:
const [itemId, setItemId] = useState(0)
const item = arr.find(e => e.id === itemId )

避免深度嵌套的state
不然更新数据很麻烦

在组件间共享状态

把state放在公共父组件上。通过props向下传递至子组件,同时将父组件中的方法(函数)也可以传递下去,作为子组件事件触发时的方法(函数)

受控组件:关键信息是通过props经由父组件传递下去的就是受控组件。父组件可控制子组件的激活状态、显示状态。

非受控组件:没有太多配置,但是组合在一起使用就没有那么灵活了。

对state进行保留和重置

假设有一个组件Aa。可以用 a ? <Aa propsA="1"> : <Aa propsA="2">来控制展示什么样的内容。

也可以<Aa key="1" propsA="1"> 来控制。

key不止用于列表渲染,也可以是不同组件之间做区分。

key变化时,这个组件会重新渲染。

迁移状态逻辑至Reducer中

useReducer是React内置Hook之一。

替代state进行状态管理。主要是整合状态逻辑。把针对某个状态的增删改查等操作整合在一个函数里,通过调用函数、向函数传参 type = 增 |删|改|查为依据对这个state进行操作。

使用:

import { useReducer } from 'react'
// 初始化aa的值
const initAa = [ {id:1}, {id:2} ] 
// 用useReducer定义一个名为aa的state
// 在useReducer(aaReducer, initAa) 中,initAa成为aaReducer的第一个入参
// useReducer的返回值为一个名为aa的state、一个用来“派发”用户操作给useReducer的dispatch函数
const [aa, dispatch] = useReducer(aaReducer, initAa) 

// aa的reduce函数
// 入参:一个是aa值、一个是dispatch函数的传参
function aaReducer(aa, action){
    switch (action.type){
        case 'added' : { 
            return [ ...aa, { id: action.id, text: action.text } ]
        }
        case 'changed' : { return [...略...] }
        case 'deleted' : { return [...略...] }
        ...
        default: { throw Error('未知 action: ' + action.type); }
    }
}

// 调用reducer函数type为added的方法来操作名为aa的state
function handleAdd(text){
    // dispatch 函数的参数(对象)就是aaReducer的第二个参数。
    dispatch({
        type: 'added',
        id: 3,
        text: text,
        ...
    })
}
// 在组件中使用
export default function AaApp() {
    return (
        <button @click="handleAdd('新增的text')"> 新增操作 </button>
    )
}
  • 一个好的reducers
  • reducers必须是纯粹的(输入相同、输出相同),与状态更新函数类似,reducers在渲染时运行,actions会排队直到下次渲染。
  • 每个action都描述了一个单一的用户交互、即使这会引起数据的多个变化。重视操作而非操作的影响。

使用Immer库简化reducer。通过useImmerReducer可以像操作数组一样操作state。

case 'added' : { 
    return [ ...aa, { id: action.id, text: action.text } ]
}

可以修改为:

case 'added' :{
    aa.push( { id: action.id, text: action.text } )
    break; 
}

Reducer应该是纯净的,所以不应该修改state。而 Immer提供了一种特殊的deaft对象,可以通过它安全修改state。
在底层,Immer会给予当前state创建一个副本,这就是为什么通过 useImmerReducer来管理reducers是、可以修改第一个参数,且不需要返回一个新的state的原因。

  • PS:

  • Redux(一个组件)全局管理state。

  • redux之于React,犹如Vuex之于Vue。

使用Context深层传递参数

创建context:

AaContext.js文件 :

import { createContext } from 'react';
export const AaContext = createContext(100); // 1是默认值

提供context

Father.js:

import { useContext } from 'react';
import { AaContext } from './AaContext.js'

export default function Father({ size, children }){
    // 提供size透传,size写在Father组件,透传给children组件。 
    // AaContext需要包含住子组件
    return (
    <Father>
        <AaContext.Provider value={size}> 
            {children}
        </AaContext.Provder>
    </Father>
    )
}

使用Context

Child.js

import { useContext } from 'react';
import { AaContext } from './AaContext.js';

export default function Child(){
    const size = useContext(AaContext);
    return <img src={xxxx} width={size} height={size}>
}

展示一下使用Father组件的时候:

App.js

import Father from './Father'
import Child from './Child'

export default function Page(){
    return (
        <Father size={150}>
            <Child></Child>
        </Father>
    )
}
  • 组合使用Reducer和Context拓展应用

应急方案

使用ref引用值

组件每次渲染,state值和ref都会被保留,不会重新渲染。

常规变量、如const a = 1则会被重新渲染。

ref与state的不同在于,ref值可以直接修改,而不像state需要用setState特定方法来调用。并且ref的改变不会引起组件重新渲染。

image.png

更多的是用ref操作DOM元素。

正常变量定义一般用state。