有消息说为了增强人员流动让我们去其他项目组待一阵,那边用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里面的属性也需要驼峰。
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,我们视Inner为Outer的ChildrenProp。是外面那层的一个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 && 1在 a = 0 时不渲染,但事实上渲染出来了 0 。
切勿将数字放在
&&左侧.JavaScript 会自动将左侧的值转换成布尔类型以判断条件成立与否。然而,如果左侧是
0,整个表达式将变成左侧的值(0),React 此时则会渲染0而不是不进行渲染。
渲染列表
使用filter()筛选需要渲染的组件,使用map()将数组变为组件数组。
key和vue中v-for的key一样,唯一标识。
为每个列表项显示多个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中,我们可以在渲染时读取三种输入:props,state 和 context,应该始终视这些输入为只读。不要修改。
如果想修改一些内容,应该设置状态、修改状态。
严格模式可以帮助我们书写纯函数的组件。严格模式不会在生产环境生效。
突变:修改了这些希望只是只读的值。
局部mutation,局部突变,在组件内部的变量。
-
副作用:某些事物在特定情况下不得不改变,如更新屏幕、启动动画、更改数据等。
在React中,副作用通常属于事件处理程序。 -
事件处理程序:是React在你执行某些操作,如单击按钮时,运行的函数。
即使事件处理程序是在组件内部定义的,他们也不会在渲染期间运行。
因此事件处理程序无需是纯函数。 -
没有合适的事件处理程序,可以调用组建的
useEffect方法将其附加到返回的JSX中。
这会告诉React在渲染结束之后执行它。
非必要不使用。
添加交互
响应事件
onClick等。
传入的方法一般是handle + 事件名称,如handleMouseEnter、handleClick等。
注意传入方法(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的改变不会引起组件重新渲染。
更多的是用ref操作DOM元素。
正常变量定义一般用state。