React , React-Hook, Redux

1,135 阅读11分钟

32. React

32.1 引入React

第一种方法:

script标签引入

一般不用这个方法

cjs, umd是什么: 模块定义 第二种方法: webpack 方法, 也很麻烦

第三种: create-react-app

目标一: 用cdn方法引入react , 渲染一个可以增加的n

渲染失败:

const React = window.React;
const ReactDOM = window.ReactDOM;

let n = 0;
const App = React.createElement("div", null, [
  n,
  React.createElement(
    "button",
    {
      onClick: () => {
        n += 1;
        console.log(n); //这一句是精髓
        ReactDOM.render(App, document.querySelector("#app")); // 为什么还是不能重新渲染
      }
    },
    "+1"
  )
]);

ReactDOM.render(App, document.querySelector("#app"));

渲染成功:

const React = window.React;
const ReactDOM = window.ReactDOM;

let n = 0;
const App =()=> React.createElement("div", null, [
  n,
  React.createElement(
    "button",
    {
      onClick: () => {
        n += 1;
        console.log(n); //这一句是精髓
        ReactDOM.render(App(), document.querySelector("#app")); 
      }
    },
    "+1"
  )
]);

ReactDOM.render(App(), document.querySelector("#app"));

变量不会重新执行, 函数每次调用就会重新执行

为啥let 不打印6个6;

立即求值, 延迟求值:


虚拟DOM

找到虚拟DOM不同的算法: DOM Diff算法

Facebook babel 作者招进来, 然后让他将jsx集成如babel.js

方法一的缺陷: 在浏览器端, 下载, 翻译jxs, 非常慢

全都要学react 和vue

32.2 组件和元素

32.2.1 元素和组件的区别

元素: 也就是虚拟DOM.

const App = React.createElement("div", null, null)

组件: 返回元素的一个函数, vue里面一个构造选项可以表示一个组件.

const App = ()=>React.createElement("div", null, null)

32.2.2 函数组件和类组件的区别

函数组件的写法:

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

 //调用方法:
<Welcome name="frank"/>
Welcome({"name":"frank"})

类组件的写法:

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

//调用方法:
<Welcome name="frank"/>

32.2.3 Babel理解jsx的逻辑

我们的代码:

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

Babel理解的:

function Welcome(props) {
  return /*#__PURE__*/React.createElement("h1", null, "Hello,", props.name);
}

可以通过babel online查看babel的理解:babeljs.io/repl

React.createElement 的逻辑

  • 如果传入一个字符串div, 就会创建一个虚拟dom.
  • 如果传入一个函数, 就会调用函数,获取其返回值
  • 如果传入一个, 就会调用一个new, 新建一个对象, 调用对象的render方法, 获取其返回值

32.2.4 用两种方法创建组件

function App() {
    let messageForSon ="儿子好"
    return (
        <div className="App">
            爸爸
            <Son messageForSon={messageForSon} />
        </div>
    );
}
class Son extends React.Component{
    constructor() {
        super();
        this.state ={
            n:0
        }
    }
    add(){
        this.setState({n:this.state.n +1})
    }
    render(){
        return(
            <div className="Son">
                消息:{this.props.messageForSon}
                儿子 n:{this.state.n}
                <button onClick={()=>this.add()}>+1</button>
                <GrandSon messageForSon='孙子好'/>
            </div>
        );
    }
}
const GrandSon =(props)=>{
    const [n,setN]=React.useState(0)
    return(
        <div className="Grandson">
            消息:{props.messageForSon}
            孙子 n:{n}
            <button onClick={()=>setN(n+1)}>+1</button>
        </div>
    )
}

踩的坑:

保存:

react-dom.development.js:13231 Uncaught Error: Objects are not valid as a React child (found: object with keys {n}). If you meant to render a collection of children,

原因: jsx语法会自动调用React.CreateElements, 所{this.state}是一个对象, 作为参数就会报错:Objects are not valid as a React child .

//错误的:
return(
    <div id="son">
        儿子 n:{this.state}
        <button onClick={()=>this.add()}>+1</button>
    </div>
);

//正确的:
return(
    <div id="son">
        儿子 n:{this.state.n}
        <button onClick={()=>this.add()}>+1</button>
    </div>
);

踩的坑:

报错: react一直加载不出来, 感觉像是服务器死机了

原因:在定义GrandSon的时候,里面引用了GrandSon,造成了死循环,死机

const GrandSon =()=>{
    const [n,setN]=React.useState(0)
    return(
        <div className="Grandson">
            孙子 n:{n}
            <button onClick={()=>setN(n+1)}>+1</button>
            这里是错的:
            <GrandSon/>
        </div>
    )
}

32.2.5 使用props

//方法一: 传递
<Son messageForSon={messageForSon} />
//方法一: 接收
<div className="Son">
    消息:{this.props.messageForSon}
    儿子 n:{this.state.n}
    <button onClick={()=>this.add()}>+1</button>
    <GrandSon messageForSon='孙子好'/>
</div>

//方法二:传递
<GrandSon messageForSon='孙子好'/>
//方法二:接收
const GrandSon =(props)=>{
    const [n,setN]=React.useState(0)
    return(
        <div className="Grandson">
            消息:{props.messageForSon}
            孙子 n:{n}
            <button onClick={()=>setN(n+1)}>+1</button>
        </div>
    )
}

32.2.6 使用state

//方法一:
constructor() {
        super();
        this.state ={
            n:0
        }
    }
    add(){
        this.setState({n:this.state.n +1})
    }

//方法二:
const GrandSon =(props)=>{
    const [n,setN]=React.useState(0)
    return(
        <div className="Grandson">
            消息:{props.messageForSon}
            孙子 n:{n}
            <button onClick={()=>setN(n+1)}>+1</button>
        </div>
    )
}

为什么不能直接修改n, 因为react没有监听n, 必须使用setState告诉浏览器进行重新渲染

因为setState是异步方法, 所有这样写更好

//原来的
this.setState({n:this.state.n +1})  //不会马上修改state, 等会再修改
//新的
this.setState(state=>{
    return {n:state.n+1}
}  
)

函数组件setState不会改变n, 而是产生一个新n

this.State({n:1})不会修改其他属性. 但是this.State({n:1, user:{name:1,age:16}}) 但是user不会合并以前数学

这种写法不推荐: 因为修改n,会修改m

const [state, setState]  = React.useState({
	n:0, m:0
});

32.2.7 事件绑定

用函数组件, 就不会被this折磨

image-20210208160651238

this的面试题需要画画.

32.3 组件

32.3.1 创建class组件的两种方式

创建class组件

过时的:

import React from 'react'
const A = React . createclass ({
   render(){
       return(
           <div>hi</div>
       )
   }
})

class组件:

class Son extends React.Component{
    constructor(props) {
        super(props);
        this.state ={
            n:0
        }
    }
    add(){
        this.setState({n:this.state.n +1})
    }
    render(){
        return(
            <div className="Son">
                消息:{this.props.messageForSon}
                儿子 n:{this.state.n}
                <button onClick={()=>this.add()}>+1</button>
                <GrandSon messageForSon='孙子好'/>
            </div>
        );
    }
}

注意的几点:

  • extends、constructor、super强行记忆,别问为什么
  • 不能改props, 外部数据应该由外部更新
  • componentWillReceiveProps: props更新的回调函数, 已经过时了, 被弃用了
  • props: 接收外部数据, 函数
  • 钩子: 也就是特殊函数

32.3.2 setState:延迟更新陷阱

例如:

onClick = ()=>{
    this.setState({
        x:this.state.x+1,//2
    })
    this.state.x//1
    this.setState({
        x:this.state.x+1,//2
    })
}

因为onClick中的语句执行完, 才会更新state. 导致**闹钟同时响效应*

解决办法: 函数形式写更新state

    onClick2 = ( ) => {
        this.setState((state)=>({x:state.X+1}))
        this.setState((state)=>({x:state.X+1}))
}

注:this.setState(???,fn),成功更新后,会调用fn函数

####32.3.3 生命周期函数

函数列表:

  • constructor()
  • shouldComponentUpdate()
  • render()
  • componentDidMount()
  • componentDidUpdate()
  • componentWillUnmount()

constructor

用途:

  • 初始化 props

  • 初始化state,但此时不能调用setState

  • 用来写 bind this

       constructor(){
        /*其他代码略*/
        this.onClick = this.onClick.bind(this)
    }
    
  • 可不写


shouldComponentUpdate

目标一: 阻止render

  • ·用途: 返回true表示不阻止UI更新 返回false表示阻止UI更新
  • 面试常问 v问:shouldComponentUpdate有什么用? 答:它允许我们手动判断是否要进行组件更新,我们可以根据应用场景灵活地设置返回值,以避免不必要的更新

为什么说shouldComponentShould 会阻止render 而不是阻止渲染更新页面

如下图:

{n:1}, {n:1}地址不一样, 所有不是同一个对象, React会render, 但是render后生成的虚拟DOM一样,不会更新页面, shouldComponentUpdate,可以用来阻止render

具体代码:

shouldComponentUpdate(newProps,newState){
    if(newState.n=this.state.n){
    	return false
    }else{
        return true
    }
}

我们除了自己比较, 还有更简单的做法: 使用React.pureComponent

目标二: purecomponent 新旧state 对比


render:

易错:

  • render只能单个元素, 可以用React.Fragment标签包裹, 这个标签不会出现在html中. 简写<></>

  • 返回多个列表元素一定要加key

    render(){
        return this.state.array.map(n→<span key={n}>{n}</span>)
    }
    

componentDidMount

目标三: 获取挂载组件的宽度: getBoundingClient , 依赖Dom元素

先声明变量, 防止程序找不到

首次执行渲染会 执行componentDidMount


componentDidUpdate

  • **面试官会问:**可以在哪个生命周期发起ajax请求?追问还可以在哪?

    一般在componentDidMount, 也可以在componentDidUpdate, 但是这里一般用于更新数据

  • 在此处setState可能会引起无限循环,除非放在if里

  • 若shouldCom...Update返回false,则不触发此钩子


componentWilUnmount

用途: 组件将要被移出页面然后被销毁时执行代码 unmount 过的组件不会再次 mount 举例

  • 如果你在c..DidMount里面监听了window scroll 那么你就要在c..WillUnmount里面取消监听
  • 如果你在c..DidMount里面创建了Timer 那么你就要在c..WillUnmount里面取消Timer
  • 如果你在c..DidMount里面创建了AJAX请求 那么你就要在c..WillUnmount里面取消请求
  • 否则你就是菜逼 原则:谁污染谁治理

钩子执行顺序:

目标四: 自己画一下钩子执行顺序

前面加了 UNSAFE_ 前缀的生命周期,都是被弃用的

32.4 函数组件

创建方式

const Hello = props =><div>{props.message}</div>

函数组件模拟生命周期:

useEffect的作用

  • 解决副作用: 当渲染一次, 就会重新执行组件里面所有代码
  • 生命周期的作用

目标一: 测试渲染调用所有代码

import React from 'react'
const Test =()=>{
    const [n, setN] = React.useState(0)
    const addN =()=> setN(n+1)
    console.log("执行了一次")
    return (
        <>
            <div>n: {n}</div>
            <div>
                <button onClick={addN}>+1</button>
            </div>
        </>
    )
}
export default Test

副作用: 每次更新state都会执行函数组件里面的代码, 就算虚拟Dom一样, 也会重新执行, useEffect用于指定哪些函数变量,才会执行哪些代码


useEffect模拟声明周期

useEffect

  • 模拟componentDidMount useEffect(()=>{ console.log('第一次渲染')},[])

  • 模拟 componentDidUpdate useEffect(()=>{ console.log('任意属性变更')}) useEffect(()=>{ console.log('n变了')},n)

  • 模拟 componentWillUnmount

    useEffect(()=>{
        console.log('第一次渲染')
        return()=>{
        	console.log('组件要死了')
        }
    }
    

目标二: 模拟 willUnmount

import React,{useEffect}from 'react'
const Test =()=>{
    const [childVisble, setchildVisble] = React.useState(true)
    const changechildVisble =()=> setchildVisble(!childVisble)

    return (
        <>
            {childVisble?<Child/>:null}
            <div>
                <button onClick={changechildVisble}>show</button>
                <button onClick={changechildVisble}>hide</button>
            </div>
        </>
    )
}
const Child =()=>{
    useEffect(()=>{
        console.log("组件变化了")
        return ()=>{
            console.log("组件挂了")
        }
    })
    return(
        <div>child</div>
    )
}
export default Test

目标三: 自定义hook, 实现只在第二次渲染,执行

import React,{useEffect,useState}from 'react'

const Test =()=>{
    const [n, setN]= useState(0)
    const addN =()=> setN(n+1)
    const useUpdate =(fn, m)=>{
        const [count, setcount]= useState(0)
        useEffect(()=>setcount(count+1),[m])
        useEffect(()=>{
            if(count>1){
                fn()
            }
        },[count])

    }
    useUpdate(()=>console.log("我不在第一次执行"),n)
    return (
        <>
            <div>n: {n}</div>
            <div>
                <button onClick={addN}>+1</button>
            </div>
        </>
    )
}

export default Test

32.5 Hooks 原理解析

问自己几个问题

  1. 执行setN的时候会发生什么?n会变吗App()会重新执行吗

    • setN一定会修改数据X,将n+1存入X
    • setN 一定会触发<App/>重新渲染(re-render)
  2. 如果App()会重新执行,那么 useState(0)的时候,n每次的值会有不同吗?

    useState 肯定会从×读取n的最新值

32.5.1 简易版useState

代码如下:

let _state
const myUseState =(initValue)=>{
      //!!!!  这里不用||保底值, 考虑到_State为0时,会被迫初始化
    _state = _state === undefined ? initValue:_state
    const setState=(newState)=>{
        _state = newState

        //这里可以让函数重新渲染
        render()
    }
    
    //!!!! 这里返回值的写法
    return [_state, setState]
}

//!!!! 这里注意渲染函数的简单实现
const render  =()=>ReactDOM.render(
    <Test/>,
    document.getElementById('root')
);

const Test =()=>{
    const [n, setN]= myUseState(0)//useState(0)
    const addN =()=> setN(n+1)

    return (
        <>
            <div>n: {n}</div>
            <div>
                <button onClick={addN}>+1</button>
            </div>
        </>
    )
}

export default Test

32.5.2 解决两个state不兼容的问题

先看这一段代码:

let index = 0
const f1=()=>{
  let current1 = index
  const f2 = ()=>{
    console.log(current1)
  }
  index +=1
  return f2
}

let cur1 = f1()
let cur2 = f1()
cur1()  // 0
cur2()  // 1
cur1()  // 0
cur2()  // 1

f1执行完, 局部变量current1 生命周期结束. current在f2中被固定为常量, 只要调用curl1 , 里面的current1永远是0, 和外面的current1已经没有关系了

具体看兼容代码:

let _state = []
let index = 0
const myUseState =(initValue)=>{
    const currentIndex = index
    _state[currentIndex] = _state[currentIndex] === undefined ? initValue:_state[currentIndex]
    const setState=(newState)=>{
        //!!!! curreantIndex 被setState 固定下来
        _state[currentIndex] = newState
        render()
    }
    index +=1
    return [_state[currentIndex], setState]
}

const render  =()=> {
    index = 0
    ReactDOM.render(
        <Test/>,
        document.getElementById('root')
    )
}

useState不能放在if里面

例如代码:

function App() {
    const [n, setN] = React.useState(0);
    let m, setM;
    
     // useState 不能放在if函数里面.
    if (n % 2 === 1) {
        [m, setM] = React.useState(0)
    }
    return (
        <div className="App">
            <p>{n}</p>
            <p>
                <button onClick={() => setN(n + 1)}>+1</button>
            </p>
            <p>{m}</p>
            <p>
                <button onClick={() => setM(m + 1)}>+1</button>
            </p>
        </div>
    );
}

否则会如下报错:

React Hook " React . useState " is called conditionally. React Hooks must be called in the exact same order in every component render . ( react hooks / rules-of-hooks ) eslint

原因:

因为state存在数组里面, 不同的useState, 对应的index不同, 所以必须要确认一致的useState顺序


解决全局_State, index的问题:

总结:

  • 每个函数组件对应一个React节点*
  • 每个节点保存着state和index
  • useState 会读取 state[index]
  • index 由 useState 出现的顺序决定
  • setState 会修改 state,并触发更新

32.5.3 setN分身问题

情景一:

  1. n=0, 然后:const log = ( )=>setTimeout(()=>console.log("n:${n}",3000);,设置三秒后打印n
  2. setN(n+1)
  3. 最后打印n:0

原因: setN不改变N, n=0, n=1 都存在于内存中, 也就是setN分身问题


如何解决setN分身问题?

  1. 使用Ref: 能够在一个组件内重新渲染的时候,Ref的值不变

    function App() {
        const nRef = React.useRef(0) // nRef: {current: 0}
    	const log = ( )=>setTimeout(()=>console.log(`n:${nRef.current}`,3000);
        return (
            <div className="App">
                <p>{n}</p>
                <p>
                    <button onClick={() => (nRef.current += 1)}>+1</button>
                </p>
            </div>
        );
    }
    

    但是Ref的值变化, 不会引起组件重新渲染

    nRef.current: 变化不会重新渲染, 可以调用nRef, 重新调用一下setState:

    +  const [n, setN]= useState(0)
    
    - <button onClick={() => (nRef.current += 1)}>+1</button>
    + <button onClick={() => {nRef.current += 1,setN(nRef.current)}}>+1</button>
    
  2. useRef贯穿一个组件始终, useContext贯穿所有组件始终:

    const themeContext = React.createContext(null);
    
    function App() {
      const [theme, setTheme] = React.useState("red");
      return (
        <themeContext.Provider value={{ theme, setTheme }}>
          <div className={`App ${theme}`}>
            <p>{theme}</p>
            <div>
              <ChildA />
            </div>
            <div>
              <ChildB />
            </div>
          </div>
        </themeContext.Provider>
      );
    }
    

总结

  • 每次重新渲染,组件函数就会执行
  • 对应的所有state都会出现「分身」
  • 如果你不希望出现分身
  • 可以用 useRef/useContext等

32.7 redux

一个程序员不喜欢用原生js, 那你就推荐他使用vanilla js 这个库, 体积又小, 功能又强大.

count接收两个参数: state, action, 前者是之前的状态, 后者是要做的动作.

store: 存储state的地方, 获取state的方法: store.getState(), 修改state的方法: store.dispatch({type:"add"})

不能在用钱规则counter里面使用setTimeout

state就像钱, store就像是我的管家, 我想查看我的金额, 存钱,取钱都需要通过管家store, counter就是之前定好的规则, 我想存钱取钱,通知管家, 管家就会按照之前定好的规则操作.

React-redux

能让你随时访问store , 不会混乱.

  1. 先用ProVided 任命store为App总管家

  2. 然后App组件, 以及子组件中, 通过connect告诉Store, 我需要哪些信息, 我血药哪些操作

  3. 然后再this.props中获取需要的东西

32.8 React Hook

####32.8.1 setState

注意:

  1. setState, userReducer都不会合并属性.
  2. 由于setN分身问题, 所以在setN(n=>n+1)里面尽量用匿名函数

32.8.2 useReducer

不合并属性, 需要用展开语法:

如何代替redux?

  1. 建立管家:

  2. 定好规则:

  3. 创建Context

    const Context = createContext(null)

  4. 创建读写api:

  5. 将读写api 放到Context里面:

  6. 各个组件可以使用读写api:

数据更新的时候, Context是逐级通知的

32.8.3 useEffect

effective: 副作用: 对环境的改变就是副作用.

多个effect , 按照出现的顺序执行


useLayoutEffect

改变浏览器外观后执行useEffect, useLayoutEffect在实际Dom生成进行截胡, 执行完之后,才改变浏览器外观

大部分的时候不会在useEffect里面改变外观.但是会改变用户看到浏览器改变的时间

为了用户体验, 优先useEffect

32.8.4 useMemo

修改n, 但是Child只依赖m, 也会再执行一遍:

具体写法: 只要props 不变, 就不会重新执行

有一个bug , 传值没问题, 传函数地址就有问题:

解决方法: 函数也用useMemo包裹:

useCallback 是 useMemo的语法糖

32.8.5 useRef

组件重新渲染也不会变的变量:

改变count.current, 不会自动render

forwardRef

能够将ref参数传递到函数组件.

32.8.6 自定义Hook

把hook写在一起, 然后把读和写接口暴露出去:

尽量封装在一起, 不要在组件上面写一堆hook函数

32.8.7 stale closure

如果一个函数包含了一个闭包, 在执行多次这个函数, 将产生多个个不同的的闭包, 如果仅记住第一个闭包, 这个闭包有可能过时.