Skip to content

React 基础

markdown 格式目录图

B站视频-柴柴_前端教书匠

视频笔记-语雀

声明式UI、命令式UI

🤔 声明式路由、编程式路由,编程式就是命令式?

前端框架特点:

  • 组件化(是个前端框架都会追求组件化)
  • jsx(利用 babel 转译为正常的 命令式dom操作语句)
  • 跨平台(虚拟DOM)

jsx
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 语法

  1. 必须有根节点,可以用<></>幽灵节点解决
  2. JSX语法可以不用()包裹如👆的ReactDOM.render(<App />,xx),JSX需要换行编写时才用()包裹
  3. 注释
jsx
export default function App() {
  return (
    <div>
      {/* 条件渲染字符串 */}
    </div>
  )
}
  1. 样式 style
jsx
export default function App() {
  return <div style={{ color: 'red' }}></div>
}
jsx
const a = { color: 'red' }
export default function App() {
  return <div style={a}></div>
}
  1. 原 HTML 标签属性 如class、for 与 js语法冲突的需要写出其他如 className htmlFor
jsx
export default function App() {
  return <div className="title"></div>
}

👇 我们最简化的函数组件甚至可以写为

jsx
export default const App = () => <div>Hello App</div>

函数组件

JSX语法转译器 会根据标签首字母是否大写来判断是组件还是原HTML标签

👆 的所有示例代码都是函数组件

如:<App /> 转译器判断到是大写标签去执行相应的函数 function App()

获取具体的UI(因此函数组件必须有return) 直到全是原HTML标签没有组件标签

在没有 Hooks 之前,函数组件 被称为 无状态组件

类组件

jsx
class App extends React.Component {
  render () {
    return <div></div>
  }
}

👇 和函数组件对比

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

简单使用的话,仅仅是

  • class语法(extendsrender(){})
  • function语法(return)

的区别,JSX语法没有区别

同理转译器 判断到 是大写标签去执行相应的class类,最终由基类调用render() 获取具体的UI

事件绑定

函数组件事件绑定

jsx
function App () {
  // 定义事件回调函数
  const clickHandler = (e) => console.log('事件被触发了', e)
  return (
    <>
      {/* 传入未执行的函数 如需自定义参数则用箭头函数包裹 */}
      <button onClick={clickHandler}>click me!</button>
      <button onClick={ () => clickHandler('1') }>click me!</button>
    </>
  )
}

👆 传入未执行的函数 如需自定义参数则用箭头函数包裹

类组件事件绑定

存在 this 指向问题

jsx
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、插槽传递的就是无状态静态组件(没有组件内部可以自己控制的变量)

类组件状态

jsx
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问题

jsx
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>
  }
}
jsx
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由基类提供看不到声明语句)都是相同的

区别在于 stateconstructor 的语法糖,不需要手动继承基类,编写 super(porps)

setState 由基类提供,调用将触发1. 修改state中的变量值 2. 触发jsx的DOM更新渲染

🤔 直接修改state而不使用setState:虽然数据确实变化了,但是不会触发jsx的DOM重新渲染

🤔 React 把组件状态设计成不可变的目的

👇 使用箭头函数省略手动解决this问题

jsx
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语法

js
class Aclass{
  constructor() {}
  fn1() {}
}

const fn1 = ()=>{} 而 class fields 不用声明 fn1 = ()=>{}

重学 class 语法

探寻public class fields

公有静态字段(多个类共用):在每个类里只存在一份,而不会存在于你创建的每个类的实例中的属性,存放缓存数据、固定结构数据或者其他你不想在所有实例都复制一份的数据 公有实例字段

js
class Aclass {
  instanceField = '公有实例字段'
  static staticField = '公有静态字段'
}

// 未实例化类直接获取公有静态字段
console.log(Aclass.staticField) // --> ‘公有静态字段’

多次实例化 静态字段 不会重复创建而是多个实例共享

原理:在声明一个类的时候,使用 Object.defineProperty() 方法将公有静态字段添加到类的构造函数中。在类被声明之后,可以从类的构造函数访问公有静态字段

🤔:看起来 公有实例字段和公有静态字段都可以实现省略 super(props) 而 React 为什么选择用公有实例字段?

通信

父子通信

jsx
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

jsx
function App() {
  return <div></div>
}
App.defaultProps = {
  msg: 'default msg'
}

而 类组件 因为没有声明props而是直接使用基类 this.props.xx 因此只能额外编写 static defaultProps = {}

jsx
class List extends Component {
  static defaultProps = {
    pageSize: 10
  }
  render() {
    return <div>此处展示props的默认值:{this.props.pageSize}</div>
  }
}
jsx
class List extends Component {
  render() {
    return <div>此处展示props的默认值:{this.props.pageSize}</div>
  }
}
List.defaultProps = {
  pageSize: 10
}

TODO: 🤔 为什么这里 React 就用 公有静态字段了 其他的 state 却是 公有实例字段

子传父 也跟变量一样,父把操作父组件的函数通过 props 传递给子组件调用,就是子传父(本质还是props)

跨级通信 Context

jsx
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 原理

jsx
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应运而生

  1. 组件的逻辑复用 在hooks出现之前,react先后尝试了 mixins混入,HOC高阶组件,render-props等模式

但是都有各自的问题,比如mixin的数据来源不清晰,高阶组件的嵌套问题等等

  1. class组件自身的问题 class组件就像一个厚重的‘战舰’ 一样,大而全,提供了很多东西,有不可忽视的学习成本,比如各种生命周期,this指向问题等等

而我们更多时候需要的是一个轻快灵活的'快艇'

useState

🤔 为什么不使用 Hooks 的 函数组件 不能拥有状态

jsx
function App() {
  let count = 1
  const add = () => count++ // 不会触发重新渲染

  return <div onClick={add}>{count}</div>
}
jsx
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. 点击按钮,调用 setCount(count + 1) 修改状态,因为状态发生改变,所以,该组件会重新渲染
  2. 组件重新渲染时,会再次执行该组件中的代码逻辑
  3. 再次调用 useState(1),此时 React 内部会拿到最新的状态值而非初始值,比如,该案例中最新的状态值为 2
  4. 再次渲染组件,此时,获取到的状态 count 值为:2

👆 也就是触发重新渲染会让 useState 也重新执行,但是 useState的参数(初始值)只会在组件第一次渲染时生效。本次的渲染,useState 获取到都是最新的状态值,React 组件会记住每次最新的状态值

包括函数 变量等都会重新创建,消耗内存(重复操作如一些枚举常量)

jsx
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

常见的副作用

  1. 数据请求 ajax发送
  2. 手动修改dom
  3. localstorage操作

TODO: useEffect 包裹会产生副作用的逻辑, 可以把副作用处理掉?原理

jsx
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 第二个参数可以控制匹配具体导致重新渲染的变量才执行,如果是空数组将只在首次渲染时执行一次

jsx
useEffect(() => {    
  console.log('副作用执行了')  
}, [count])

清除副作用

因为 函数组件 没有生命周期的概念, 所以清除副作用不应该考虑销毁的概念 👇

在副作用函数中的末尾return一个新的函数,在新的函数中编写清理副作用的逻辑 注意执行时机为:

  1. 组件卸载时自动执行
  2. 组件更新时,下一个useEffect副作用函数执行之前自动执行
jsx
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>
}
jsx
// ❌ 不可以直接在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

js
const sum = useMemo(() => {
  return data.reduce((acc, val) => acc + val, 0);
}, [data]);

js
const [sum, setSum] = useState(0);

useEffect(() => {
  setSum(data.reduce((acc, val) => acc + val, 0));
}, [data]);

👆2种方式没有区别

React.memo()

用来处理一遍函数组件,接收一个函数组件

js
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 无关。我们希望子组件只渲染一次,但是,每次单击按钮时它都会重新执行

js
// 接收一个函数组件
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 绑定 要获取的元素或者组件

jsx
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> 的步骤

jsx
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()

jsx
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)

jsx
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>

自己看官方文档吧