# React

# 1 Redux 中 reducer 不能做异步操作的原因

  1. 先从 Redux 的设计层面来解释为什么 Reducer 必须是纯函数

如果你经常用 React + Redux 开发,那么就应该了解 Redux 的设计初衷。 Redux 的设计参考了 Flux 的模式,作者希望以此来实现时间旅行,保存应用的历史状态,实现应用状态的可预测。所以整个 Redux 都是函数式编程的范式,要求 reducer 是纯函数也是自然而然的事情,使用纯函数才能保证相同的输入得到相同的输出,保证状态的可预测。所以 Redux 有三大原则:

  • 单一数据源,也就是 state
  • state 是只读, Redux 并没有暴露出直接修改 state 的接口,必须通过 action 来触发修改
  • 使用纯函数来修改 statereducer 必须是纯函数
  1. 下面 再从代码层面来解释为什么 reducer 必须是纯函数

那么 reducer 到底干了件什么事, 在 Redux 的源码中只用了一行来表示:

  currentState = currentReducer(currentState, action)

这一行简单粗暴的在代码层面解释了为什么 currentReducer 必须是纯函数。 currentReducer 就是我们在 createStore 中传入的 reducer (至于为什么会加个 current 有兴趣的可以自己去看源码 ),reducer 是用来计算 state 的,所以它的返回值必须是 state, 也就是我们整个应用的状态,而不是 promise 之类的。

要在 reducer 中加入异步的操作,如果你只是单纯想执行异步操作,不会等待异步的返回,那么在 reducer 中执行的意义是什么。如果想把异步操作的结果反映在 state 中,首先整个应用的状态将变的不可预测, 违背 Redux 的设计原则,其次,此时的 currentState 将会是 promise 之类而不是我们想要的应用状态,根本是行不通的。

# 2 React 中 fiber 是用来做什么的

因为 Javascript 单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入做出相应操作,React 的更新过程就是犯了这个禁忌,而 React Fiber 就是要改变现状。而可以通过分片来破解 Javascript 中同步操作时间过长的问题。

把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务易燃有运行的机会。

React Fiber 把更新过程碎片化,每执行完一段更新过程,就把控制权交还给 React 负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

维护每一个分片的数据结构,就是 Fiber。

# 3 React 生命周期

初始化阶段:

  • getInitialState 获取每个实例的初始化状态
  • getDefaultProps 获取实例的默认属性
  • componentWillMount 组件即将被装载、渲染到页面上
  • render 组件在这里生成虚拟的DOM节点

运行中状态:

  • componentWillReceiveProps 组件将要接收到属性的时候调用
  • shouldComponentUpdate 组件接收到新属性或者新状态的时候(可以返回 false,接收数据后不更新,阻止 render 调用, 后面的函数不会被继续执行了)
  • componentWillUpdate 组件即将更新不能修改属性和状态
  • render 组件重新描绘
  • componentDidUpdate 组件已经更新完成

卸载过程:

  • componentWillUnmount 组件即将销毁

# 4 shouldComponentUpdate 函数有什么作用

shouldComponentUpdate 是一个允许我们自行决定某些组件(以及他们的子组件)是否进行更新的生命周期函数,Reconciliation 的最终目的是尽可能以最有效的方式去根据新的 state 更新UI,如果你已经知道 UI 的哪些状态无需进行改变,就没必要去让 React 去判断它是否该改变。让 shouldComponentUpdate 返回 falseReact 就会让当前的组件和其子组件保持不变。

# 5 当组件的setState函数被调用之后,发生了什么

在代码中调用 setState 函数之后, React 会将传入的参数对象与组件当前的状态合并,然后触发所谓的调和过程(Reconciliation)。

经过调和过程, React 会以相对高效的方式根据新的状态构建 React 元素数并且着手重新渲染整个UI界面。在 React 得到元素树之后, React 会自动计算出新的树和老树的节点差异,然后根据差异对界面进行最小化重渲染。在差异计算算法中, React 能够相对精确地知道哪些位置发生了改变以及应该如何改变,这就保证了按需更新,而不是全部重新渲染。

# 6 在生命周期中的哪一步你应该发起 AJAX 请求

React 下一代调和算法 Fiber 会通过开始或停止渲染的方式优化应用性能,其会影响到 componentWillMount 的触发次数。对于 componentWillMount 这个生命周期的调用次数会变得不确定,React 可能会多次频繁调用 componentWillMount。 如果我们将 AJAX 请求放到 componentWillMount 函数中, 那么显而易见其会被触发多次,自然也就不是好的选择。

如果我们将 AJAX 请求放置到生命周期的其他函数中, 我们并不能保证请求仅在组件挂载完毕后才会要求响应。如果我们的数据请求在组件挂载之前就完成,并且调用了 setState 函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount 函数中进行 AJAX 请求则能有效避免这个问题。

# 7 createElement 与 cloneElement 的区别是什么

createElement 函数是 JSX 编译之后使用的创建 React Element 的 函数, 而 cloneElement 则是用于复制某个 元素 并传入新的 Props。

# 8 传入 setState 函数的第二个参数的作用是什么

该函数会在 setState 函数调用完成并且组件开始重渲染的时候被调用,我们可以用来该函数来监听渲染是否完成:

  this.setState(
    {
      username: 'fangyuan'
    },
    () => console.log('setState has finished and the component has re-rendered.')
  )

# 9 浅谈 useEffect

一、说说 useEffect, 什么是副作用? 第二个参数为 【】 是什么意思?

1.什么是 useEffect

React 官方文档解释:Effect Hook 可以让你在函数组件中执行 副作用 操作

2.什么是副作用

副作用(side effect): 数据获取,数据订阅,以及手动更改 React 组件中的 DOM 都属于副作用。

因为我们渲染出的页面都是静态的,任何在其之后的操作都会对它产生影响,所以称之为副作用。

副作用又分为两种:(1)无需清楚的副作用 (2)需要清楚的副作用

(1)无需清楚的副作用:

有时候,我们只想在 React 更新 DOM 之后运行一些额外的代码。比如发送网络请求,手动变更DOM,记录日志,这些都是常见的无需清楚的操作。因为我没在执行完这些操作之后,就可以忽略他们了。

(2)需要清除的副作用: 之前,我们研究了如何使用不需要清除的副作用,还有一些副作用的需要清除的。例如外部数据源,添加DOM事件。这种情况下,清除工作是非常重要的,可以防止引起内存泄露

在这个例子中,给鼠标的click事件添加了一个监听器,

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

  const MouseTracker: React.FC = () => {
    const [ positions, setPositions ] = useState({ x: 0, y: 0 })

    useEffect(() => {
      const updateMouse = (e: mouseEvent) => {
        console.log('inner')
        setPositions({ x: e.clientX, y: e.clientY })
      }
      document.addEventListener('click', updateMouse)
    })

    return (
      <p>X: {positions.x}, Y: {positions.y}</p>
    )
  }
  
  export default MouseTracker

点击鼠标,会在屏幕打印出鼠标当前的位置,并在控制台打印出inner。但是这里存在一个问题,打印inner的次数并不是线性增加(点一次增加一次),而是点一次,inner被打印了多次。这是因为我们在每次更新渲染页面的时候都会调用 useEffect 的回调函数,这样就会添加越来越多的 click 时间,而没有清楚。

如何清除?

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

  const MouseTracker: React.FC = () => {
    const [ positions, setPositions ] = useState({ x: 0, y: 0})

    useEffect(() => {
      const updateMouse = (e: MouseEvent) => {
        console.log('inner')
        setPositions({ x: e.clientX, y: e.clientY })
      }
      document.addEventListener('click', updateMouse)
      return () => {
        document.removeEventListener('click', updateMouse)
      }
    })

    return (
      <p>X: {positions.x}, Y: {positions.y}</p>
    )
  }

  export default MouseTracker

可以发现,对比之前的代码,这里多 return 一个函数, 可以在这个函数中做清除操作, 现在点鼠标之后,inner被打印的次数就是线性增加,点一次,inner被打印一次,而不是被打印多次。

3. useEffect的第二个参数? (控制 useEffect 的执行)

  • 第一个参数为一个函数: 这个函数的目的就是为了告诉 React 组件需要在渲染后执行哪些操作,这个函数会在DOM更新之后被调用,use Effect 默认在每次渲染之后都会执行,但是也可以手动控制它的执行。

  • 第二个参数(一个数组): 当数组中的任意一项变化的时候,useEffect会被重新执行。 如果传递一个空数组 [],告诉 useEffect 不依赖于 state、props中任意值,useEffect就只会运行一次。

    如果数组中有值

      import React, { useState, useEffect } from 'react'
    
      const LikeButton: React.FC = () => {
        const [ like, setLike ] = useState(0)
        const [on, setOn ] = useState(true)
    
        useEffect(() => {
          console.log('document title effect is running')
          document.title = `点击了${like}`
        }, [like])
    
        return (
          <>
            <button onClick={()=>{setLike(like + 1)}}>{like}</button>.
            <button onClick={()=>{setOn(!on)}}>{on ? 'ON' : 'OFF'}</button>
          </>
        )
      }
    

    这里的依赖为 like, 只有当 like 变化时, useEffect 才会执行, 而当 on 变化时, useEffect 不会执行.

    running图