【React】 hooks

作者: woow_wu7 | 来源:发表于2020-01-10 19:09 被阅读0次

    我的掘金
    https://juejin.im/user/5c6e6a11f265da2ddd4a5a56/posts

    导航

    [深入01] 执行上下文
    [深入02] 原型链
    [深入03] 继承
    [深入04] 事件循环
    [深入05] 柯里化 偏函数 函数记忆
    [深入06] 隐式转换 和 运算符
    [深入07] 浏览器缓存机制(http缓存机制)
    [深入08] 前端安全
    [深入09] 深浅拷贝
    [深入10] Debounce Throttle
    [深入11] 前端路由
    [深入12] 前端模块化
    [深入13] 观察者模式 发布订阅模式 双向数据绑定
    [深入14] canvas
    [深入15] webSocket
    [深入16] webpack
    [深入17] http 和 https
    [深入18] CSS-interview
    [深入19] 手写Promise
    [深入20] 手写函数

    [react] Hooks

    [部署01] Nginx
    [部署02] Docker 部署vue项目
    [部署03] gitlab-CI

    [源码-webpack01-前置知识] AST抽象语法树
    [源码-webpack02-前置知识] Tapable
    [源码-webpack03] 手写webpack - compiler简单编译流程
    [源码] Redux React-Redux01
    [源码] axios
    [源码] vuex
    [源码-vue01] data响应式 和 初始化渲染
    [源码-vue02] computed 响应式 - 初始化,访问,更新过程
    [源码-vue03] watch 侦听属性 - 初始化和更新

    大纲
    useState
    useEffect
    useLayoutEffect
    useCallback
    useMemo
    useRef(useImperativeHandle)
    useImperativeHandle(useRef)
    useReducer
    useContext
    Context
    React.memo
    --
    useFetch
    如何封装一个useFetch
    hooks中父组件如何获取子组件中的方法 useImperativeHandle forwardRef
    --
    复习:
    js中的注释规范
    手写call,applay,bind
    逻辑运算符 && ||

    useState

    • function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]
    useState
    
    function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>]
    type Dispatch<A> = (value: A) => void;
    type SetStateAction<S> = S | ((prevState: S) => S);
    
    - 参数
      - 参数是一个值或者函数
      - 参数是函数时,返回值作为初始值,(当初始值需要复杂运算时使用)
        - 该函数仅在初始渲染时执行一次,以获得初始状态。在以后的组件渲染中,不会再调用,从而跳过昂贵的操作。!!!!
    - 返回值
      - 返回值是一个数组
      - 第一个成员:返回设置过后的state
      - 第二个成员:setter函数(本质上就是一个dispatcher,底层就是useReducer)
        - 该函数的参数可以是一个值,或者是一个签名为 (prevState: S) => S 的函数
      - 即返回一个状态值和一个函数来更新状态
    - 注意事项
      - 仅在顶层调用hook,不能在循环,条件,嵌套函数中 useState(),在多个useState()调用中,渲染之间的调用顺序必须相同。
        - 为什么要在顶层顺序调用,而不能在循环,条件,嵌套中调用???????
          - 因为内部的各个state都会用一个队列来维护(类似数组)
          - 并且队列中的state和setter函数是一一对应的,在上面三种情况中可以会改变一一对应的关系,造成错误的预期
      - !!!
      - 过时的状态,闭包是一个从外部作用域捕获变量的函数。(解决办法,通过函数返回值,如setCount(count => count+1))
      - 闭包(例如事件处理程序,回调)可能会从函数组件作用域中捕获状态变量。 
        由于状态变量在渲染之间变化,因此闭包应捕获具有最新状态值的变量。否则,如果闭包捕获了过时的状态值,则可能会遇到过时的状态问题。
    - 复杂的状态
      - 当有多个状态完成同一项任务时,或者状态过多时,建议使用 useReducer
    - 关于react组件的重新渲染
      -  class  中重新渲染:是render()和willmout dimount等钩子函数重新执行
      - function中重新渲染:整个组件函数重新执行
    - 思考
      - 当函数执行完后,所有内存都会被释放掉,函数中声明的变量也会被释放掉,那useState如何能保存状态呢???????
      - 或者说,每次重新渲染,为什么useState能返回记住的状态????
    - 答案
      - useState利用闭包,在函数内部创建一个当前函数组件的状态。并提供一个修改该状态的方法。
    
    
    
    
    
    
    - 实例
    ------------------
    (1) 例子1
      - 过时的状态
      - setCount参数是值和函数的区别
    
    <div  onClick={() => setTimeout(() => setCount(count+1), 3000)}> ----------- // 在3秒内多次点击,多次渲染,但始终 count =1
      点击执行定时器,set函数参数是一个值的情况
    </div>
    <div onClick={() => setTimeout(() => setCount(count => count+1), 3000)}> --- // 在3秒内多次点击,时间到后,会记录每次点击的值
      点击执行定时器,set函数参数是一个函数的情况
    </div>
    <div>{count}</div>
    
    
    
    
    
    ------------------
    (2) 例子2
      - 如果是同一个引用,组件不会重新渲染
    
    const [isReRender, setIsReRender] = useState({render: true});
    setIsReRender(isReRender) -------------------- 不会重新渲染,内部使用函数记忆,缓存了上一次的值,相同直接使用
    -
    setIsReRender(() => ({render: true})) -------- 会重新渲染,因为是一个新的不同引用的对象
    -
    isReRender.render = false;
    setIsReRender(isReRender) -------------------- 不会重新渲染,赋值之后再比较,引用没变
    
    
    
    
    
    ------------------
    (3) 例子3
    - useState返回的state在整个生命周期中保持不变,所以可以用来固定变量的引用,在不使用改变函数的时候
    - 当然也可以使用useRef去固定变量
    const [a] = useState(0); // 注意,这只使用赋值state,不需要改变函数
    
    
    
    
    
    ------------------
    (4) 例子4!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    function CaseOne() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        setInterval(() => {
          console.log(count, 'in')
          setCount(count+1)
          // 这里有外层函数CaseOne,里层函数setInterval的回调函数,并且里层使用了外层的count,setCount两个变量,形成闭包
          // 定时器每次执行,都是使用的第一次执行时声明并赋值的count,count是第一次渲染中的闭包中的count,值永远是0 
          // 所以 setCount的参数永远是1,在这个例子中
        }, 1000)
      }, [])
      console.log(count, 'out')
      return (
        <div style={{background: 'yellow'}}>
          <div>经典的use-state</div>
        </div>
      )
    }
    解析:
    1. useState()的初始参数,只会在第一次执行的时候,赋值给count变量,以后的渲染就不再有作用
    2. useState()的返回值count,在除了第一次使用useState的初始参数外,以后的渲染将使用上一次的setCount的返回值
    具体过程:
    第一次渲染
      - useState(0)执行,返回[0, setter函数]赋值给了count和setCount
      - 外层count => 0
      - 1s后定时器执行,形成闭包,闭包中的变量 count是0,setCount(0+1)
    第二次渲染:
      - count和setCount从新声明并赋值,count是上一次setCount的返回值,即 1
      - 外层count => 1
      - 注意:定时器再次执行,count还是引用的第一次执行生成的闭包中的count是0,setCount(0+1)
      - 注意:第二次的setCount和第一次的setCount的参数都是0+1,即参数没有发生变化,这种情况,react将不会在重新渲染,即不会有第三次渲染了
             即外层的count将不再打印,保持1
    
    
    
    
    
    ------------------
    (5) 例子5
    - useState在不用setter函数修改,而是直接修改state时,可以用来缓存引用类型的变量
      - 如果是原始类型的值,由于是const不能被修改,如果用let每次都是新的引用,重渲染会被初始化
    function CaseTwo() {
      const [a] = useState({count: 1}) // 引用类型的变量
      let [c] = useState(10) // 原始类型的值,使用的是let
      const [b, setB] = useState(0)
      return (
        <div style={{background: 'yellow'}}>
          <div>CaseTwo</div>
          <div onClick={() => {
            a.count = 22 // 直接修改引用类型的值,注意不是用useState返回的setter函数修改
            c = 33 // 直接修改原始类型的值
            setB(b => b+1) // 重新渲染
          }}>点击重新渲染</div>
          <div>{a.count}</div> // 打印22
          <div>{b}</div> // 打印10,每次被重新初始化,这种情况要固定住值的话,可以使用useRef !!!!!!!!!!!
          <div>{c}</div>
        </div>
      )
    }
    

    经典的例子:https://juejin.im/post/5d71104b6fb9a06b0a2796b3
    https://juejin.im/post/5dd1df576fb9a01fdc1304a8
    https://mp.weixin.qq.com/s/Bo0Q6FDn4a8QuI1WseRF2g
    解析:https://imweb.io/topic/5c7d58b0baf81d795209497e
    useState源码:https://juejin.im/post/5dc6e1b35188251ab9183c7d
    使用介绍:https://juejin.im/post/5d985deae51d4577f9285c2f

    useEffect

    • 执行副作用的hook
    • 什么是副作用:除了和渲染ui相关的意外的逻辑都是副作用(当dom渲染完执行的和ui无关的逻辑)
    useEffect
    function useEffect(effect: EffectCallback, deps?: DependencyList): void;
      - type EffectCallback = () => (void | (() => void | undefined));
      - type DependencyList = ReadonlyArray<any>;
    
    
    参数:
    - 第一个参数:函数
      - 每一次render之后,执行副作用,和清除上一次副作用
      - 该函数的返回值就是清除函数(清除上一次的副作用)
    - 第二个参数:数组
      - 当这几个依赖有一个要更新,effect里面也会重新生成一个新的副作用并执行副作用
      - 如果没有更新,则不会执行。
      - 如果第二个参数不传,表示没有依赖项,则每次render后都会执行
      - 注意:第二个参数是一个空数组 [ ] 时!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        - 如果传一个空数组,表示useEffect的回调只执行一次,类似于componentDidMount()
        - useEffect每次渲染后都会执行,只是useEffect依赖是空数组,每次对比没有变化,所以Effect的回调不再执行
        - 是useEffect的回调只执行一次,而不是useEffect只执行一次,useEffect每次渲染后都会执行
        - 依赖的比较,只是简单的比较值或者引用是否相等
        - 所以
          - 很明显,useEffect可以模仿(didMount,didUpdate),返回值可以模仿(willUnmount)
    - 什么是副作用
      - 和渲染ui无关的逻辑都是副作用
      - 当DOM渲染完成之后,我还要执行一段别的逻辑,这一段别的逻辑就是副作用
    
    
    useEffect执行的时机:
    - 每次渲染完成后,执行useEffect
      - setEffect有点类似于componentDidMount,componentDidUpdate,componentWillUnmount的结合
    
    
    
    注意事项:
    1. useEffect中用到的状态,官网推荐都应该写到依赖数组中去,不管引用的是基础类型值、还是对象甚至是函数。
    2. useEffect的第一个参数函数,如果返回一个函数,该函数就是清理函数
    
    3.清除函数执行的时机:
    - useEffect的第一个参数函数,如果返回一个函数,该函数就是清理函数(清除一些副作用,如定时器等)
    - 执行的时机:!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    - 第一次渲染,不会执行清除函数,所以 31 -----------------------------------(只不过定时器的缘故,132一起打印了)
    - 第二次渲染,在渲染完成后,立即执行清除函数,再执行Effect函数的回调函数,所以 321
    - 总结:再每次渲染完成后,立即执行清除函数清除上一次的副作用(第一次渲染除外)
    
    打印顺序为:31 321 321 321
    const [count, setCount] = useState(0);
    useEffect(() => {
      const timer = setInterval(() => {
        console.log('1=======')
        setCount(count+1)
      }, 1000)
      return () => {
        console.log('2=======')
        clearInterval(timer)
      }
    }, [count])
    console.log('3=======')
    

    https://juejin.im/post/5cd839adf265da03587c1bf7
    https://juejin.im/post/5d970784518825095012b771

    useLayoutEffect

    • 在所有的 DOM 变更之后同步调用 effect,可以使用它来读取 DOM 布局并同步触发重渲染,在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
    • useLayoutEffect 与 componentDidMount、componentDidUpdate 的调用阶段是一样的,注意只是调用时机一样
    • seLayoutEffect是同步地,而useEffect是异步的
    • useLayoutEffect可以解决屏幕跳动的问题
    useEffect
    
    function App2 () {
      const [count, setCount] = useState(0)
      useEffect(() => {
        if (count===0) {
          setCount(count => Math.random())
        }
      }, [count])
      return (
        <div>
          <div onClick={() => setCount(count => 0)}>点击更新</div>
          {count}
        </div>
      )
    }
    结果:渲染结果是先变成0,后变成随机数
    过程:
    1.  setCount(count => 0),从新render,渲染0,执行useEffect,setCount(count => Math.random()),重新渲染得到随机数
    
    
    
    
    
    
    
    ----------------------
    useLayoutEffect
    
    function App2 () {
      const [count, setCount] = useState(0)
      useLayoutEffect(() => {
        if (count===0) {
          setCount(count => Math.random())
        }
      }, [count])
      return (
        <div>
          <div onClick={() => setCount(count => 0)}>点击更新</div>
          {count}
        </div>
      )
    }
    结果:不会变成0,而是直接显示随机数字
    过程:
    1.  setCount(count => 0),重新render,浏览器并没有渲染完成,
    2. useLayoutEffect执行,阻塞渲染,setCount(count => Math.random()),重新render,
    3. useLayoutEffect执行,没有逻辑,渲染随机数
    
    

    useCallback

    • 缓存函数(或者说缓存住函数变量),(或者说固定函数的引用)
    useCallback
    
    - 固定函数的引用,即缓存函数
    - function useCallback<T extends (...args: any[]) => any>(callback: T, deps: DependencyList): T;
    - 参数:
      - 第一个参数:回调函数
      - 第二个参数:依赖项
    - 返回值
      - 返回值是一个T类型的函数
    
    
    - 说明
      - 一般情况下,组件重新渲染,函数就会被重新声明
      - 但是用useCallback包装的函数,只有在依赖项改变的时候,才'相当于'重新声明,未改变时,是缓存的上一个的函数引用
    
    
    - 注意点
    1. const a = useCallback(() => {...}, [])
      - 这种情况 a 永远不会重新声明,因为它的依赖项是空
    
    
    - 性能优化 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
      - 配合React.memo优化
      - React.memo
        - React.memo是当props改变时才会重新渲染,仅仅依赖于被传入的props
        - 而当props中包含函数的时候,函数的引用可以用useCallback控制,即useCallback可以控制props中的函数是否改变!!!!!!
    
    
    案例
    (1) 配合React.memo做性能优化
    父组件:
    function UseCallback() {
      const [count, setCount] = useState(0)
      const [color, setColor] = useState('yellow')
      const callBackChangeColor = useCallback(() => { 
        // callBackChangeColor 函数的更新仅仅依赖于color改变
        // 如果color不更新的话,callBackChangeColor的引用就不会改变
        // 又因为子组件更新仅仅依赖于callBackChangeColor函数的更新
        const index = Math.floor(Math.random()*4)
        setColor(['black', 'white', 'blue', 'green', 'Purple'][index])
      }, [color])
      return (
        <div style={{background: '#FF4500', padding: '20px', margin: '10px 0'}}>
          <div style={{color}}>USE_CALLBACK</div>
          <div onClick={() => setCount(count => count+1)}>父组件改变count</div> // 当count改变时,子组件并不会重新渲染
          <div>{count}</div>
          <ChildUseCallback setColor={callBackChangeColor} /> // 子组件
        </div>
      )
    }
    子组件:
    const ChildUseCallback = React.memo(({setColor}) => { // React.memo组件的更新仅仅依赖于setColor函数的改变
      const changeColor = () => {
        setColor()
      }
      console.log('子组件执行了')
      return (
        <div style={{background: '#FA8072'}}>
          <div>CHILD_USE_CALLBACK</div>
          <div onClick={changeColor}>改变颜色</div>
        </div>
      )
    })
    

    useMemo

    • 缓存变量,类似于计算属性
    • 使用的时候要考虑两点
      • 要记住的函数开销大吗?大才考虑使用useMemo
      • 返回的是原始值吗?不是原始值时才考虑useMemo
    • 当useEffect的函数中要使用不会再改变的props,但是不在依赖项时,ESLint Hook会发出警告
      • 解决办法是,用useRef固定住不需要再改变的某些props=> 具体请点击下面的链接
    useMemo
    - function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
    - 参数
      - 第一个参数是一个函数,返回值类型是T类型
        - 通常情况下类似这样 const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b])
        - 注意函数返回函数,内层函数必须有返回值!!!!!!!!!!!!!
      - 第二个参数是数组或者undefined,即依赖项
    - 返回值
      - T类型的变量
    
    
    
    案例:
    父组件:
    function UseCallback() {
      const [count, setCount] = useState(0)
      const [color, setColor] = useState('yellow')
      const [isBoolean, setIsBoolean] = useState(false)
      const callBackChangeColor = useCallback(() => {
        const index = Math.floor(Math.random()*4)
        setColor(['black', 'white', 'blue', 'green', 'Purple'][index])
      }, [color])
      const aOrBFn = () => {
        return isBoolean ? 'A' : 'B'
      }
      const cacheIsBoolean = useMemo(() => aOrBFn(), [isBoolean])  // useMemo
      // useMemo,当isBoolean改变时重新执行aOrBFn函数,同时在子组件没有依赖的state变化时,不重新渲染
      // 例如:
      //   1. 当count变化时,父组件重新渲染,但是子组件没有依赖cont,不希望重新渲染
      //   2. 子组件重新渲染只依赖两个,一是 cacheIsBoolean 变量,二是callBackChangeColor函数
      //   3. 只希望依赖的两个props改变,才重新渲染子组件,所以使用useCallback固定函数,用useMemo固定变量,同时用React.memo优化子组件
      return (
        <div style={{background: '#FF4500', padding: '20px', margin: '10px 0'}}>
          <div style={{color}}>USE_CALLBACK</div>
          <div onClick={() => setCount(count => count+1)}>父组件改变 => Count</div>
          <div>{count}</div>
          <div onClick={() => setIsBoolean(boo => !boo)}>父组件改变 => Boolean</div> // isBoolean改变,触发useMemo的参数函数重新计算
          <div>{isBoolean + ''}</div>
          <ChildUseCallback setColor={callBackChangeColor} cacheIsBoolean={cacheIsBoolean}/> // 子组件依赖cacheIsBoolean
        </div>
      )
    }
    子组件:
    const ChildUseCallback = React.memo(({setColor, cacheIsBoolean}) => {
      const changeColor = () => {
        setColor()
      }
      console.log('子组件执行了')
      return (
        <div style={{background: '#FA8072'}}>
          <div>CHILD_USE_CALLBACK</div>
          <div onClick={changeColor}>改变颜色</div>
        </div>
      )
    })
    

    哪些情况要避免使用useMemo https://www.infoq.cn/article/mM5bTiwipPPNPjhjqGtr

    useRef

    useRef
    
    - const refContainer = useRef(initialValue);
    - function useRef<T = undefined>(): MutableRefObject<T | undefined>;
    - current属性
      - useRef.current属性,被初始化为传入的参数(initialValue)
    - 返回值
      - 返回一个可变的ref对象
      - 返回的ref对象,在组件的整个生命周期内保持不变
    
    
    (1) 作用
    - 保存任何可变的值,在组件整个生命周期内保持不变(每次渲染时,返回同一个ref对象)
    - 即还可以绑定DOM节点和子组件 (保存任何可变值包括DOM节点,子组件等)
    - 相当于class中的普通属性
    
    
    (2) 注意点
    - 改变 useRef.current 属性不会触发组件重新渲染,类似于class组件中声明的属性
    
    
    (3) 实例
    - 主要测试:绑定DOM 和 固定变量
    function UseRef() {
      const renderTimes = useRef(0); // 组件渲染的次数测试,useRef.current的初始值时0,(相当于外部的全局变量,能固定住值)
      renderTimes.current++; // 每次render都 +1
      const inputRef = useRef(null) // 绑定dom测试
      console.log('renderTime:', renderTimes.current)  // 1  2
      console.log('useRef.current:', inputRef.current) // null <input />
      const focus = () => {
        inputRef.current.focus()
      }
      return (
        <div style={{background: '#FFBBFF'}}>
          <div>USE_REF</div>
          <input ref={inputRef} />
          <button onClick={focus}>获取input焦点</button>
        </div>
      )
    }
    
    

    useRef使用场景 https://juejin.im/post/5d27cdaaf265da1baf7d1715

    useContext

    • 注意:在function组件中,必须必须使用useContext才能拿到context实例中的值,而不能像在class组件中那样直接获取
    useContext
    - 注意:在function组件中,必须必须使用useContext才能拿到context实例中的值,而不能像在class组件中那样直接获取
    
    function useContext<T>(context: Context<T>/*, (not public API) observedBits?: number|boolean */): T;
    interface Context<T> {
      Provider: Provider<T>;
      Consumer: Consumer<T>;
      displayName?: string;
    }
    
    
    (1)
    const value = useContext(MyContext);
    - 参数
      - 一个context对象,(React.createContext的返回值)
    - 返回值
      - context的当前值
      - 注意:context的值由( 上层组件 )中距离最近的 <MyContext.Provider> 的 value prop 决定
    - 原理:
      - 当上层最近的<MyContext.Provider>更新时,该hoos会触发 `重新渲染`,并使用最新的 value 值
    - 场景
      - 避免数据的层层传递
    - 本质
      - useContext实际上是class中的 <context实例.Consumer> 或者 static contextType = context实例  的语法糖
      - useContext只是让你能够读取context的值,以及订阅context的变化
    
    
    
    ---------------
    例子:
    
    import React, {useContext, useState} from 'react';
    const TestContext = React.createContext('100') // React.createContext('100')创建一个context实例,初始值为‘100’
    // 父组件
    function UseContext() {
      const [fontWeight, setFontWight] = useState('400')
      return (
        <div style={{background: '#1E90FF'}}>
          <div>USE_CONTEXT</div>
          <div onClick={() => setFontWight('700')}>点击改变context</div>
          <TestContext.Provider value={fontWeight}> // context实例提供Provider组件,组件提供value值,当value变化时,组件会重新渲染
            <ContextChildOne />
          </TestContext.Provider>
        </div>
      )
    }
    // 子组件
    function ContextChildOne() {
      return (
        <div>
          <div>ContextChildOne</div>
          <ContextChildTwo />
        </div>
      )
    }
    // 孙子组件
    function ContextChildTwo() {
      const fontWeightTwo = useContext(TestContext) // 使用useContext
      return (
        <div>
          <div style={{fontWeight: fontWeightTwo}}>ContextChildTwo</div>
        </div>
      )
    }
    
    export default UseContext;
    

    class组件中如何使用context:https://react.docschina.org/docs/context.html
    官网useContext https://react.docschina.org/docs/hooks-reference.html#usecontext

    Context

    • React.createContext
    • Class.contextType
    • Context.Provider
    • Context.Consumer
    • Context.displayName
    Context
    - context提供了一个无需为每层组件手动添加props,就能在组件树之间进行数据传递的方法
    - 注意:如果组件的属性需要层层传递,而且只有最后一个组件需要使用到传递的属性,可以使用控制反转
      - 控制反转:直接传递整个在最后需要使用的组件,减少props的个数和传递的层数
    - api
    
    
    1. React.createContext
      - 创建一个context对象,注意是对象
      - 当react渲染一个( 订阅 )了这个( context对象 )的组件时,这个组件会从离自身最近的(Provider )中读取到当前的context
      - 只有订阅了这个context对象的组件所在的组件树中没有匹配到 Provider 时,defaultValue才会生效。
      - 注意:将undefined传递给provider的value,消费组件的defaultValue不会生效
    
    
    2. Context.Provider
    - <Context.Provider value={...}>
    - 每个Context对象都有一个Provider对象组件,它允许消费组件`订阅context的变化`
    - Provider接收一个value属性,传递给消费组件
    - 一个Provider可以和多个消费组件有对应关系
    - 多个Provider也可以嵌套使用,里层会覆盖外层的数据
    - 注意:当Provider的value值发生变化时,它内部的所有消费组件都会从新渲染。!!!!!!!!!!!!!!!!!
    - Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
    
    
    --------------------
    3. Class.contextType     
    - `提供给this.context来消费`
    - MyClass.contextType = MyContext; // MyContext是context实例对象,MyClass是组件,contextType组件的静态属性
    - contextType属性也可以写在组件内部,static ContextType = MyContext
    - 说明:
    - 挂载在 class 上的 `contextType` 属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。
    - 这能让你使用 this.context 来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
     - 如果你正在使用实验性的 public class fields 语法,你可以使用 `static` 这个类属性来初始化你的 `contextType`。
    class MyClass extends React.Component {
      static contextType = MyContext;
      render() {
        let value = this.context;
        /* 基于这个值进行渲染工作 */
      }
    }
    
    
    
    
    --------------------
    4. Context.Consumer
    - React 组件也可以订阅到 context 变更。这能让你在 函数式组件 中完成订阅 context
    - 代码案例:
    
    const GlobalThem = React.createContext({color: 'red'}) // 创建context对象,初始值参数只是在消费组件没有找到Provider时生效
    function App2() {
      return (
        <div>
          <GlobalThem.Provider value={{color: 'blue'}}> 
            // Provider提供给消费组件订阅context,把value传递给消费组件,并在value变化时重新渲染
            <Child />
          </GlobalThem.Provider>
        </div>
      )
    }
    function Child() {
      return (
        <div>
          <Grandson />
        </div>
      )
    }
    function Grandson() {
      return (
        <GlobalThem.Consumer> 
          // Consumer主要用于函数式组件,在class组件中可以使用Class.contextType包装,用this.context获取
          // 消费组件订阅context,下面的value参数就是context的值
          {
            value => (
              <div> // 返回一个组件
                {value.color}
              </div>
            )
          }
        </GlobalThem.Consumer>
      )
    
    
    --------------------
    5. Context.displayName
    - context 对象接受一个名为 displayName 的 property,类型为字符串。
    - React DevTools 使用该字符串来确定 context 要显示的内容。
    
    const MyContext = React.createContext(/* some value */);
    MyContext.displayName = 'MyDisplayName';
    <MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
    <MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
    

    React.memo

    React.memo
    
    const MyComponent = React.memo(function MyComponent(props) {
      /* 使用 props 渲染 */
    });
    
    - React.memo()为高阶组件,与React.pureComponent()相似,但是memo只作用于函数组件,不适用于class组件
    - React.memo()在props相同的情况下,并不会重新渲染
      - 即 React 将跳过渲染组件的操作并直接复用最近一次渲染的结果
      - 即 ( 函数记忆 ),缓存结果值
    

    useReducer

    useReducer
    
    const [state, dispatch] = useReducer(reducer, initialArg, init);
    - 参数
      - reducer
        - (state, action) => newState,接受一个state和action,返回一个新的state
      - initialArg
        - reducer中的state的初始值,大多数情况下是一个对象作为合理的数据类型
    - 返回值
      - 返回当前的 state 和 dispatch 函数
    - 使用场景
      - 当组件中state比较多时,useReducer可以聚合各个state
      - 当各个层级组件中的数据需要频繁的共享时
    
    --------------
    实例:
    const initialState = {count: 0};
    
    function reducer(state, action) {
      switch (action.type) {
        case 'increment':
          return {count: state.count + 1};
        case 'decrement':
          return {count: state.count - 1};
        default:
          throw new Error();
      }
    }
    
    function Counter() {
      const [state, dispatch] = useReducer(reducer, initialState); // useReducer
      return (
        <>
          Count: {state.count}
          <button onClick={() => dispatch({type: 'decrement'})}>-</button> // 点击触发dispatch函数,派发一个action
          <button onClick={() => dispatch({type: 'increment'})}>+</button>
        </>
      );
    }
    

    如何封装一个useFetch

    useFetch
    
    
    (1) 版本1
    export function useFetch(fetchFn, fnParams={}) {
      const [data, setData] = useState([])
      const [params, setParams] = useState(fnParams)
      const [isError, setIsError] = useState(false)
      const [isLoading, setIsLoading] = useState(false)
    
      useEffect(() => {
        const fetchData = async() => {
          setIsLoading(true)
          setIsError(false)
          try {
            const res = await fetchFn(params)
            setData(res.data)
          } catch(err) {
            setIsError(true)
          }
          setIsLoading(false)
        }
    
        fetchData()
      }, [params]) // fetchData依赖params的更新,params改变则重新执行fetchData
    
      const doFetch = (params) => { // 对外暴露更新params的函数
        setParams(params) // params改变触发useEffect更新
      }
    
      return {data, isError, isLoading, doFetch}
    }
    
    
    
    
    ------------------------------------------------------------------------------------
    (2)版本2
    - 版本1中各个state都是完成同一个副作用,可以将他们聚合在一个state中
    - 使用 useReducer 优化
    
    (use-fetch.js)
    import {useState, useEffect, useReducer} from 'react';
    import {FetchReducer, fetchType} from '../../store/reducers/fetch-reducer';
    export function useFetch(fetchFn, fnParams={}) {
      const [params, setParams] = useState(fnParams)
      const [state, dispatch] = useReducer(FetchReducer, {
        data: [],
        isError: false,
        isLoading: false
      })
      useEffect(() => {
        const fetchData = async() => {
          dispatch({type: fetchType.FETCH_INIT}) // 开始,dispatch => action
          try {
            const res = await fetchFn(params)
            dispatch({type: fetchType.FETCH_SUCCESS, payload: res.data}) // 成功,数据payload
          } catch(err) {
            dispatch({type: fetchType.FETCH_FAIL}) // 失败
          }
        }
        fetchData()
      }, [params])
      const doFetch = (params) => {
        setParams(params)
      }
      return {doFetch, ...state} // 展开state,暴露给外部
    }
    
    (fetch-reducer.js)
    export const fetchType = {
      FETCH_INIT: 'FETCH_INIT',
      FETCH_SUCCESS: 'FETCH_SUCCESS',
      FETCH_FAIL: 'FETCH_FAIL'
    }
    
    export const FetchReducer = (state, action) => {
      switch(action.type) {
        case fetchType.FETCH_INIT:
          return {
            ...state,
            isError: false,
            isLoading: true
          }
        case fetchType.FETCH_SUCCESS:
          return {
            ...state,
            isError: false,
            isLoading: false,
            data: action.payload
          }
        case fetchType.FETCH_SUCCESS:
          return {
            ...state,
            isError: true,
            isLoading: false
          }
        default:
          return {
            ...state
          }
      }
    }
    
    

    hooks中父组件如何获取子组件中的方法

    • useImperativeHandle
    • forwardRef
    • imperative:势在必行的,急切,急迫
    hooks中父组件如何获取子组件中的方法
    - useImperativeHandle
    - forwardRef
    
    
    (1) useImperativeHandle
    - useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。
    - imperative:势在必行的,急切,急迫
    - `注意:useImperativeHandle需要和forwardRef一起使用`
    
    (2) forwardRef
    - `React.forwardRef` 会创建一个React组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。
    - 即传递转发 ref 属性给子组件,`这样父组件就能直接绑定子组件的子组件或者孙子组件`
    - 参数:一个渲染组件
    - 渲染组件的参数:props和ref
    
    
    
    import React, {useRef, useImperativeHandle, forwardRef} from 'react';
    import ReactDOM from 'react-dom';
    import './index.css';
    import App from './App';
    import * as serviceWorker from './serviceWorker';
    
    // 父组件
    const App2 = () => {
      const childRef = useRef(null)
      const getChildFn = () => {
        childRef.current.go() // 在current中直接获取子组件暴露出来的方法
      }
      return (
        <>
          <div>父组件</div>
          <div onClick={getChildFn}>点击获取子组件中的方法</div>
          <Child ref={childRef}></Child> // 通过ref绑定子组件
        </>
      )
    }
    
    
    // 子组件
    const Child =  forwardRef((props, ref) => { // 用forwardRef包装的组件,接收props,和ref参数
      useImperativeHandle(ref, () => ({ // 暴露给父组件的方法go
        go: () => {
          console.log('用forwardRef高阶组件包装后,组件能接收props和ref');
          console.log('useImperativeHandle的回调中返回的对象中的方法提供给父组件使用');
        }
      }))
      return (
        <div>
          子组件
        </div>
      )
    })
    

    https://zh-hans.reactjs.org/docs/react-api.html#reactforwardref

    复习:

    js中的注释规范

    @version 16.8.0
    @see https://reactjs.org/docs/hooks-reference.html#usecontext
    
    
    函数相关
    @param  {string}  address  地址
    @param {number} [num] 中括号表示参数可选
    @returns  void
    @desc  描述
    @deprecated  指示一个函数已经废弃,而且在将来的代码版本中将彻底删除。要避免使用这段代码
    
    
    @date     2014-04-12
    @author   QETHAN<qinbinyang@zuijiao.net>
    @copyright代码的版权信息
    @overview对当前代码文件的描述。
    

    http://www.jsphp.net/js/show-9-26-1.html
    https://segmentfault.com/a/1190000000502593

    复习

    手写call

    1. 原生的call
    - Function.prototype.call()
    - 参数:
      - 第一个参数:this将要绑定的对象,当第一个参数是空,null,undefined,则默认传入全局对象
      - 后面的参数:传入函数的参数
    - 主要的作用:
      - 绑定函数中 this 所指定的对象
      - 执行函数(把绑定this的对象以外的参数,传入使用的函数)
      - 注意:fn是可以有返回值的
    - 注意点:
      - eval(string)将字符串当作语句来执行,参数是一个字符串
    
    
    - 实现思路:
    - 在对象上添加一个函数
    - 在对象上执行这个函数,此时函数中的this指向的就是这个对象(this运行时才能确定,运行时所在的对象)
    - 执行完后,删除这个函数属性
    
    
    简单版:
    useEffect(() => {
      const obj = {
        name: 'wu',
        age: 20
      };
      function fn (address) {
        return this.name + this.age + address
      }
      Function.prototype._call = function(obj) {
        if (typeof this !== 'function') { // _call方法必须在函数对象上调用
          throw new Error('the _call method must called with a function object')
        }
        const context = obj ? obj : window; // 如果参数是null,undefined,或者为空,则会默认传入全局对象,比如window
        context.fn = this // 因为下面调用时,this指向了fn
        const res = context.fn(...Array.from(arguments).slice(1)) // 除去第一个参数,执行函数,如果有返回值,需要返回
        delete context.fn // 执行后,删除
        return res
      }
      const res = fn._call(obj, 'hangzhou')
      console.log(res, 'res')
    }, [])
    

    手写 apply

    apply
    - 和call方法大致相同,绑定this,除了绑定的对象之外的参数传入函数,并执行函数
    - 和call的区别:传入函数的参数是一个数组,所以理论上call的性能比apply好
    
    useEffect(() => {
      const obj = {name: 'wang', age: 20}
      function fn (name, age) {
        return {
          name: name || this.name,
          age: age || this.age
        }
      }
      Function.prototype._apply = function(obj) {
      if (typeof this !== 'function') { // _call方法必须在函数对象上调用
          throw new Error('the _call method must called with a function object')
        }
        const context = obj ? obj : window;
        context.fn = this
        const res = context.fn(...[...arguments].slice(1).flat(Infinity))
        // 数组实例.flat(Infinity)表示展开多层数组,最后只剩一层
        // 注意:Infinity首字母要大写
        delete context.fn
        return res
      }
      const res = fn._apply(obj, 'zhang')
      console.log(res, 'res')
    }, [])
    

    手写bind

    bind
    - 将函数体内的this绑定到某个对象上,并返回一个新的函数
    - 参数:
      - 第一个参数:方法所要绑定的对象
      - 后面的参数:将会作为参数传递给函数
      - 注意:返回的函数仍然可以传参
        - 如果bind方法传递两个参数(传给函数的就是一个),那么返回的函数的第一个参数,将作为第二个参数传递给函数
    - bind函数的特点:
      - 绑定this
      - 返回新函数
      - 可以传参(bind函数传参,返回的新函数也可以传参)
      - 返回的是新函数,还可以通过 new 命令调用
    
    
    
    
    - 代码实现:
    
    (1) 简单版
    useEffect(() => {
      // bind模拟实现
      const obj = {
        name: 'wang',
        age: 20
      };
      function fn(address, sex) {
        return this.name + this.age + address + sex;
      }
      Function.prototype._bind = function(obj) {
        const context = this; // 这里的this指向 fn 函数(this在调用时确定指向,_bind是供fn调用)
        const bindParams = Array.prototype.slice.call(arguments, 1); 
        // 将类数组对象转成真是的数组,call除了绑定this,还可以向fn传参
        // _bind函数的参数收集
        return function() { 
          // _bind返回一个新函数
          const resParams = Array.prototype.slice(arguments); 
          // 返回的函数接收的参数收集
          return context.apply(obj, bindParams.concat(bindParams)) // 拼接_bind和返回函数的参数,因为是数组,用apply
        }
      }
      const newFn = fn.bind(obj, 'chongqing')
      const res = newFn('man');
      console.log(res, 'res')
    }, [])
    
    
    
    
    
    (2) _bind返回的参数,考虑new命令调用的情况
    - 特点:
    - 当bind返回的新函数,使用new操作符调用时,bind所指定的this对象会失效,因为构造函数中的this指向的是实例对象
    - 虽然this指向失效,但是参数仍然是有效的
    useEffect(() => {
      const obj = {
        name: 'wang',
        age: 20
      }
      function fn(address, sex) {
        console.log(this.name, 'this.name')
        return this.name + this.age + address + sex
      }
      Function.prototype._bind = function(obj) {
        // _bind方法只能在函数对象上调用
        if (typeof this !== 'function') {
          throw new Error('bind must be called on the funciton object')
        }
        // 寄生继承,防止实例和原型上重复的属性和方法,和修改 prototype
        // 原理:用中间函数来中转,
          // closure.prototype => ParasiticFn实例
          // ParasiticFn.prototype => fn.prototype
            // 这样 closure的实例可以访问closure函数中的属性,访问ParasiticFn.protype上的属性
            // 而ParasiticFn本身没有任何属性和方法
        // parasitic:寄生
        const ParasiticFn = function(){};
        ParasiticFn.prototype = this.prototype
        closure.prototype = new ParasiticFn()
    
        const bindParams = Array.prototype.slice.call(arguments, 1)
        const context = this // fn
        function closure() {
          const closureParams = Array.prototype.slice.call(arguments)
          return context.apply(this instanceof ParasiticFn ? this : obj, bindParams.concat(closureParams)) // 组合参数,并调用,返回返回值
          // 注意:
          // 1. closure() 函数中的this,根据调用方式的不同,this的指向会不同, (注意这里的this和closure函数外的this指向也不同)
          // 2. 通过new命令调用时,closure被当作构造函数,this指向实例对象
          // 3. 通过普通函数调用时候,this指向的是传入的需要绑定的对象
          // 4. this instanceof ParasiticFn ? this : obj的意思是:通过new调用,绑定实例对象,其他情况绑定obj对象
          // 5. // 通过什么方式调用新函数,就绑定什么对象
        }
    
        return closure
      }
      const newFn = fn._bind(obj, 'chongqign')
    
      const resOrdinary = newFn('man')
      console.log(resOrdinary, 'resOrdinary')
      // wang this.name
      // wang20chongqignman resOrdinary
    
      const resConstructor = new newFn('woman')
      console.log(resConstructor, 'resConstructor')
      // undefined "this.name"
      // {}
    })
    

    https://github.com/mqyqingfeng/Blog/issues/12

    逻辑运算符

    &&

    &&
    运算规则:
    - 如果第一个运算子的布尔值是true  => 返回第二个运算子的值 
    - 如果第一个运算子的布尔值是false => 返回第一个运算子的值,且不对第二个运算子求值
    短路:
    - 跳过第二个运算子的运算叫做短路
    多个连用:
    - 返回第一个为false的表达式的值
    
    
    总结:
    - 执行到哪里就返回当前的值
    - 判断的是布尔值,返回的是值
    false && 2 => false // 执行到false就不执行了,返回布尔值是false的表达式的值 false
    '' && 2    => '' // 执行到第一个表达是布尔值是false,不再执行,返回当前表达式的值 ''
    true && 1 && [] && 0 && {} => 0 // 执行到 0 时,布尔值时false,不再执行,返回当前的值 0
    

    ||

    ||
    运算规则
    - 如果第一个运算子的布尔值是true,则返回第一个表达式的值,且不再对后面的运算子求值
    - 如果第一个运算子的布尔值是false,则返回第二个运算子的值(如果只有两个运算子时)
    - 当多个||一起用时,还是执行到什么地方就返回当前的表达式的值
    

    相关文章

      网友评论

        本文标题:【React】 hooks

        本文链接:https://www.haomeiwen.com/subject/kbwyoctx.html