React 基础
markdown 格式目录图
声明式UI、命令式UI
🤔 声明式路由、编程式路由,编程式就是命令式?
前端框架特点:
- 组件化(是个前端框架都会追求组件化)
- jsx(利用 babel 转译为正常的 命令式dom操作语句)
- 跨平台(虚拟DOM)
import React from 'react'
import ReactDOM from 'react-dom'
// 引入根组件App
import App from './App'
// 通过调用ReactDOM的render方法渲染App根组件到id为root的dom节点上
ReactDOM.render(<App />, document.getElementById('root'))
👆 引入的 React
并没用实际调用的地方(用于告诉转译器(babel
)当前文件会有 jsx语法
需要转译?)
cra脚手架
默认配置使用 babel
插件转译语法 @babel/plugin-transform-react-jsx
JSX 语法
- 必须有根节点,可以用
<></>
幽灵节点解决 - JSX语法可以不用
()
包裹如👆的ReactDOM.render(<App />,xx)
,JSX需要换行编写时才用()
包裹 - 注释
export default function App() {
return (
<div>
{/* 条件渲染字符串 */}
</div>
)
}
- 样式 style
export default function App() {
return <div style={{ color: 'red' }}></div>
}
const a = { color: 'red' }
export default function App() {
return <div style={a}></div>
}
- 原 HTML 标签属性 如class、for 与 js语法冲突的需要写出其他如
className
htmlFor
export default function App() {
return <div className="title"></div>
}
👇 我们最简化的函数组件甚至可以写为
export default const App = () => <div>Hello App</div>
函数组件
JSX语法转译器 会根据标签首字母是否大写来判断是组件还是原HTML标签
👆 的所有示例代码都是函数组件
如:<App />
转译器判断到是大写标签去执行相应的函数 function App()
获取具体的UI(因此函数组件必须有return
) 直到全是原HTML标签没有组件标签
在没有 Hooks 之前,函数组件 被称为 无状态组件
类组件
class App extends React.Component {
render () {
return <div></div>
}
}
👇 和函数组件对比
function App() {
return <div></div>
}
简单使用的话,仅仅是
- class语法(
extends
、render(){}
) - function语法(
return
)
的区别,JSX语法没有区别
同理转译器 判断到 是大写标签去执行相应的class类,最终由基类调用render()
获取具体的UI
事件绑定
函数组件事件绑定
function App () {
// 定义事件回调函数
const clickHandler = (e) => console.log('事件被触发了', e)
return (
<>
{/* 传入未执行的函数 如需自定义参数则用箭头函数包裹 */}
<button onClick={clickHandler}>click me!</button>
<button onClick={ () => clickHandler('1') }>click me!</button>
</>
)
}
👆 传入未执行的函数 如需自定义参数则用箭头函数包裹
类组件事件绑定
存在 this 指向问题
class App extends React.Component {
// class Fields
clickHandler = (e) => {
// 这里的this指向是当前的组件实例对象
// 可以非常方便的通过this关键词拿到组件实例身上的其他属性或者方法
console.log(this)
}
// 非箭头函数时 this指向调用方(jsx标签调用会是undefined)
clickHandler1 () {
// 这里的this 不指向当前的组件实例对象而指向undefined 存在this丢失问题
console.log(this)
}
render () {
return (
<div>
<button onClick={this.clickHandler1}>❌click me</button>
<button onClick={() => this.clickHandler1()}>✅click me</button>
<button onClick={this.clickHandler}>✅click me</button>
<button onClick={() => this.clickHandler('1')}>✅click me</button>
</div>
)
}
}
类组件状态
🧐 ???在React hook出来之前,函数式组件是没有自己的状态的,所以我们统一通过类组件来讲解
用 vue 举🌰 vue2组件没有data、vue3组件没有useRef useReactive, 只有 props、插槽传递的就是无状态静态组件(没有组件内部可以自己控制的变量)
类组件状态
class App extends React.Component {
state = {
count: 1,
}
add = () => {
this.setState({ count: this.state.count + 1 })
}
render () {
return <div onClick={ this.add }>{this.state.count}</div>
}
}
class内方法不使用箭头函数时需要手动(bind)处理this问题
class App extends React.Component {
constructor(props) {
super(props)
this.count = 1
}
add(){
this.setState({ count: this.state.count + 1 })
}
render () {
return <div onClick={ this.add.bind(this) }>{this.state.count}</div>
}
}
class App extends React.Component {
constructor(props) {
super(props)
this.count = 1
this.add = this.add.bind(this) // 实例化时先处理this,在下面调用时就是处理后的方法
}
add(){
this.setState({ count: this.state.count + 1 })
}
render () {
return <div onClick={ this.add }>{this.state.count}</div>
}
}
可以看出 类组件 使用状态2种方式1.state 2.constructor
使用(this.state.xx
)和修改(this.setState
由基类提供看不到声明语句)都是相同的
区别在于 state
是 constructor
的语法糖,不需要手动继承基类,编写 super(porps)
setState 由基类提供,调用将触发1. 修改state中的变量值 2. 触发jsx的DOM更新渲染
🤔 直接修改state而不使用setState:虽然数据确实变化了,但是不会触发jsx的DOM重新渲染
🤔 React 把组件状态设计成不可变的目的
👇 使用箭头函数省略手动解决this问题
class App extends React.Component {
constructor(props) {
super(props)
this.count = 1
}
add = () => {
this.setState({ count: this.state.count + 1 })
}
render () {
return <div onClick={ this.add }>{this.state.count}</div>
}
}
🤔 这不是单纯的箭头函数 而是 class fields语法-MDN
👇 常规的class语法
class Aclass{
constructor() {}
fn1() {}
}
const fn1 = ()=>{}
而 class fields 不用声明 fn1 = ()=>{}
重学 class 语法
公有静态字段(多个类共用):在每个类里只存在一份,而不会存在于你创建的每个类的实例中的属性,存放缓存数据、固定结构数据或者其他你不想在所有实例都复制一份的数据 公有实例字段
class Aclass {
instanceField = '公有实例字段'
static staticField = '公有静态字段'
}
// 未实例化类直接获取公有静态字段
console.log(Aclass.staticField) // --> ‘公有静态字段’
多次实例化 静态字段 不会重复创建而是多个实例共享
原理:在声明一个类的时候,使用 Object.defineProperty() 方法将公有静态字段添加到类的构造函数中。在类被声明之后,可以从类的构造函数访问公有静态字段
🤔:看起来 公有实例字段和公有静态字段都可以实现省略 super(props) 而 React 为什么选择用公有实例字段?
通信
父子通信
import React from 'react'
// 函数式子组件
function FunctionSon(props) {
return <div>{props.msg}</div>
}
// 类子组件
class ClassSon extends React.Component {
render() {
return <div>{this.props.msg}</div>
}
}
// 类父组件
class App extends React.Component {
state = {
message: 'this is parent message'
}
render() {
return (
<div>
<FunctionSon msg={this.state.message} />
<ClassSon msg={this.state.message} />
</div>
)
}
}
export default App
类子组件直接通过基类的 this.props.xx(看不到声明)
获取
函数子组件通过函数参数获取 FunctionSon(xxx)
并且 React 的 props 通信支持传递 JSX(组件标签) 也就类似 Vue 的插槽
props 传递的 JSX,会在编译时转化为 ReactElement vNode(虚拟DOM对象)
而组件标签中的内容(函数组件)则会自动作为 props.children 等同于标签属性传递写法 使用方需要自己根据内容使用,不限死为组件
这里我们就会发现 函数子组件可以轻松的定义props的默认值(通过函数参数定义) 也可以使用 组件 原型定义 defaultProps
function App() {
return <div></div>
}
App.defaultProps = {
msg: 'default msg'
}
而 类组件 因为没有声明props而是直接使用基类 this.props.xx
因此只能额外编写 static defaultProps = {}
class List extends Component {
static defaultProps = {
pageSize: 10
}
render() {
return <div>此处展示props的默认值:{this.props.pageSize}</div>
}
}
class List extends Component {
render() {
return <div>此处展示props的默认值:{this.props.pageSize}</div>
}
}
List.defaultProps = {
pageSize: 10
}
TODO: 🤔 为什么这里 React 就用 公有静态字段了 其他的 state 却是 公有实例字段
子传父 也跟变量一样,父把操作父组件的函数通过 props 传递给子组件调用,就是子传父(本质还是props)
跨级通信 Context
import React, { createContext } from 'react'
// 1. 创建Context对象
const { Provider, Consumer } = createContext()
// 3. 消费数据
function ComC() {
return (
<Consumer >
{value => <div>{value}</div>}
</Consumer>
)
}
function ComA() {
return (
<ComC/>
)
}
// 2. 提供数据
class App extends React.Component {
state = {
message: 'this is message'
}
render() {
return (
<Provider value={this.state.message}>
<div className="app">
<ComA />
</div>
</Provider>
)
}
}
export default App
受(ReactState)控组件
结合 类组件状态 理解 受控组件
input
的行为可以很好的解释 React受控组件 的概念
非受控组件:input的状态不受react组件state中的状态控制,获取和修改只允许通过原生dom操作输入框的值
input框的状态被React组件状态控制,则 input 就是受控组件
React组件状态在state中,input表单的状态在value中,React将state与表单元素的值(value)绑定到一起,由state的值来控制表单元素的值,从而保证单一数据源特性
props校验
有 ts 后没必要
组件库开发者,不能限制了用户使用ts,而仍然要类型检验时使用!
prop-types 原理
import PropTypes from 'prop-types'
const FunctionComp = props => {
const msg = props.msg
return <div>{msg}</div>
}
FunctionComp.propTypes = {
msg: PropTypes.string // <-- ✨ set prop type check
}
check at runtime 编译时不检查(ts可以)
TODO: 原理, 给 函数组件的原型上直接挂载 propTypes 属性,内部为状态的类型配置,即可开启 类型校验 如何实现?
Vue 就是把这个包的指责内置了,因此 Vue 的props配置项更多
生命周期
注意,只有类组件才有生命周期(类组件 实例化 函数组件 不需要实例化)
Hooks
没有 Hooks 前大家使用 类组件 时,也是有 函数组件的(并不是后面和 Hooks 一同出现),而那时候的 函数组件 不能拥有状态,只能做简单功能的UI(静态元素展示)
经过多年的实战,函数组件是一个更加匹配React的设计理念 UI = f(data),也更有利于逻辑拆分与重用的组件表达形式
为了能让函数组件可以拥有自己的状态,从react v16.8开始,Hooks应运而生
- 组件的逻辑复用 在hooks出现之前,react先后尝试了 mixins混入,HOC高阶组件,render-props等模式
但是都有各自的问题,比如mixin的数据来源不清晰,高阶组件的嵌套问题等等
- class组件自身的问题 class组件就像一个厚重的‘战舰’ 一样,大而全,提供了很多东西,有不可忽视的学习成本,比如各种生命周期,this指向问题等等
而我们更多时候需要的是一个轻快灵活的'快艇'
useState
🤔 为什么不使用 Hooks 的 函数组件 不能拥有状态
function App() {
let count = 1
const add = () => count++ // 不会触发重新渲染
return <div onClick={add}>{count}</div>
}
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()
执行触发重新渲染)
- 点击按钮,调用 setCount(count + 1) 修改状态,因为状态发生改变,所以,该组件会重新渲染
- 组件重新渲染时,会再次执行该组件中的代码逻辑
- 再次调用 useState(1),此时 React 内部会拿到最新的状态值而非初始值,比如,该案例中最新的状态值为 2
- 再次渲染组件,此时,获取到的状态 count 值为:2
👆 也就是触发重新渲染会让 useState 也重新执行,但是 useState的参数(初始值)只会在组件第一次渲染时生效。本次的渲染,useState 获取到都是最新的状态值,React 组件会记住每次最新的状态值
包括函数 变量等都会重新创建,消耗内存(重复操作如一些枚举常量)
let num = 1
function App(){
num++
if(num / 2 === 0){
const [name, setName] = useState('cp')
}
const [list,setList] = useState([])
}
// 俩个hook的顺序不是固定的,这是不可以的!!!
要在函数组件的最外层使用
不能嵌套在if/for/其它函数中(react按照hooks的调用顺序识别每一个hook???) 可以通过开发者工具查看hooks状态
useEffect
副作用
副作用是相对于主作用来说的,一个函数除了主作用,其他的作用就是副作用。对于 React 组件来说,主作用就是根据数据(state/props)渲染 UI,除此之外都是副作用(比如,手动修改 DOM)
更好理解的现象是,每次组件更新都会触发,类似生命周期,如果不使用这个 Hooks 会执行多次,而副作用操作一般都希望可以动态(可以自定义数组内的变量变化才执行)触发
在 dom 渲染后执行,因此可以通过 useRef 获取组件或元素 dom
如发送请求一般只在创建时执行一次(要写上空数组) 如果不使用 Hooks 直接写外层将会每次更新都请求
但是 Hooks 的出现,就是希望没用生命周期的概念,而是只关注 函数 和 副作用,来编写代码
也可以对比 vue 的 computed
常见的副作用
- 数据请求 ajax发送
- 手动修改dom
- localstorage操作
TODO: useEffect 包裹会产生副作用的逻辑, 可以把副作用处理掉?原理
import { useEffect, useState } from 'react'
function App() {
const [count, setCount] = useState(0)
// 立即执行 重新渲染也会执行
useEffect(()=>{
document.title = `当前已点击了${count}次` // 副作用 DOM操作
})
return (
<button onClick={() => { setCount(count + 1) }}>{count}</button>
)
}
👇 useEffect 第二个参数可以控制匹配具体导致重新渲染的变量才执行,如果是空数组将只在首次渲染时执行一次
useEffect(() => {
console.log('副作用执行了')
}, [count])
清除副作用
因为 函数组件 没有生命周期的概念, 所以清除副作用不应该考虑销毁的概念 👇
在副作用函数中的末尾return一个新的函数,在新的函数中编写清理副作用的逻辑 注意执行时机为:
- 组件卸载时自动执行
- 组件更新时,下一个useEffect副作用函数执行之前自动执行
import { useEffect, useState } from "react"
const App = () => {
const [count, setCount] = useState(0)
useEffect(() => {
const timerId = setInterval(() => {
setCount(count + 1)
}, 1000)
return () => { // 用来清理副作用的事情
clearInterval(timerId)
}
}, [count])
return <div>{count}</div>
}
// ❌ 不可以直接在useEffect的回调函数外层直接包裹 await ,因为异步会导致清理函数无法立即返回
useEffect(async ()=>{
const res = await axios.get('xxx')
console.log(res)
},[])
// ✅
useEffect(()=>{
async function fetchData(){
const res = await axios.get('xxx')
console.log(res)
}
fetchData()
},[])
useMemo
const sum = useMemo(() => {
return data.reduce((acc, val) => acc + val, 0);
}, [data]);
和
const [sum, setSum] = useState(0);
useEffect(() => {
setSum(data.reduce((acc, val) => acc + val, 0));
}, [data]);
👆2种方式没有区别
React.memo()
用来处理一遍函数组件,接收一个函数组件
export const Parent = () => {
const [userName, setUserName] = useState("faisal");
const [count, setCount] = useState(0);
const add = () => setCount((count) => count + 1);
return (
<>
<Children userName={userName} />
<button onClick={add}> {count} </button>
</>
);
};
const Children = ({ userName }) => {
console.log("rendered", userName);
return <div> {userName} </div>;
};
👆 因为 count 的值与 Children 无关。我们希望子组件只渲染一次,但是,每次单击按钮时它都会重新执行
// 接收一个函数组件
const Children = ({ userName }) => {
console.log("rendered", userName);
return <div> {userName} </div>;
};
const ChildrenMemo = React.memo(Children)
// or 👇
const Children = React.memo(({ userName }) => {
console.log("rendered");
return <div> {userName}</div>;
});
👆 现在,无论您单击该按钮多少次,它只会在必要时执行
🤔 TODO: 那所有组件都套一个不好吗?
useRef
获取原生DOM/类组件实例
执行 useRef 函数并传入null,返回值为一个对象 内部有一个current属性存放拿到的dom对象(组件实例) 通过ref 绑定 要获取的元素或者组件
function App() {
const domRef = useRef(null)
const classCompRef = useRef(null)
return <div ref={ domRef }></div>
// return <classComp ref={ classCompRef }></classComp>
}
函数组件由于没有实例,不能使用ref获取,如果想获取组件实例,必须是类组件
useContext
上面,我们用到了 const { Provider, Consumer } = createContext()
<Provider>
和<Consumer>
组件标签,来包裹需要跨层级通信的组件,在包裹后的作用域内可以共享指定的数据
使用 useContext Hooks 可以省略包裹<Consumer>
的步骤
import { createContext, useContext } from 'react'
// 1. 创建Context对象
const {Provider} = createContext()
function Foo() {
return <div>Foo <Bar/></div>
}
function Bar() {
// 3. 底层组件通过useContext函数获取数据 ✨ 而不是包裹<Consumer>
const name = useContext(Context)
return <div>Bar {name}</div>
}
function App() {
return (
// 2. 顶层组件通过Provider 提供数据
<Provider value={'this is name'}>
<div><Foo/></div>
</Provider>
)
}
export default App
自定义Hooks
简单的返回特定结构的函数 不算有意义的Hooks,自定义的 Hooks 需要配合 React 内置Hooks使用 才有意义
获取滚动距离Y const [y] = useWindowScroll()
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]
}
❌ 假如不使用 useEffect, 可以在组件首次渲染时获取到y,也可以在适用方组件每次更新时获取到y,但是我们希望自己决定y的更新,如滚动页面时更新,而不是组件重新渲染时更新,这就是 useEffect 的作用,自定义更新时间,自定义触发时间,脱离使用方组件的控制,也是很多副作用(全局变量window事件的场景)
不使用 useEffect 也可以
每次修改message数据的时候 都会自动往LocalStorage同步一份 const [message, setMessage] = useLocalStorage(key,defaultValue)
import { useEffect, useState } from 'react'
export function useLocalStorage (key, defaultValue) {
const [message, setMessage] = useState(defaultValue)
// 每次只要message变化 就会自动同步到本地ls
useEffect(() => {
window.localStorage.setItem(key, message)
}, [message, key])
return [message, setMessage]
}
Router
内管页面 路由配置一般结构是 layout-一级路由 内容二级路由
这是因为我们从路径上看 localhost:3000/login
localhost:3000/list
这2个路径(路由)要有不同的布局list是内管布局,login不套布局
这种时候就要把layout作为 / 根路由,但是/login也会匹配到根路由的布局
所以做成二级路由 匹配到一级路由 /login 就不会进入 /根路径的二级路由
mobox
模块化 利用到了React <context>
自己看官方文档吧