【React】特性

271 阅读11分钟

目录

  1. 生命周期
  2. fiber
  3. React hooks
  4. Redux
  5. setState

一、 生命周期

在新版本中,React 官方对生命周期有了新的 变动建议:

  • 使用getDerivedStateFromProps 替换 componentWillMountcomponentWillReceiveProps
  • 使用getSnapshotBeforeUpdate替换componentWillUpdate
  • 避免使用componentWillReceiveProps

其实该变动的原因,正是由于上述提到的 Fiber。首先,从上面我们知道 React 可以分成 reconciliationcommit 两个阶段,对应的生命周期如下:

  • reconciliation:

    • componentWillMount
    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
  • commit:

    • componentDidMount
    • componentDidUpdate
    • componentWillUnmount

在 Fiber 中,reconciliation 阶段进行了任务分割,涉及到 暂停 和 重启,因此可能会导致 reconciliation 中的生命周期函数在一次更新渲染循环中被 多次调用 的情况,产生一些意外错误。

新版的建议生命周期如下:

class Component extends React.Component {
  // 替换 `componentWillReceiveProps` ,
  // 初始化和 update 时被调用
  // 静态函数,无法使用 this
  static getDerivedStateFromProps(nextProps, prevState) {}
  
  // 判断是否需要更新组件
  // 可以用于组件性能优化
  shouldComponentUpdate(nextProps, nextState) {}
  
  // 组件被挂载后触发
  componentDidMount() {}
  
  // 替换 componentWillUpdate
  // 可以在更新之前获取最新 dom 数据
  getSnapshotBeforeUpdate() {}
  
  // 组件更新后调用
  componentDidUpdate() {}
  
  // 组件即将销毁
  componentWillUnmount() {}
  
  // 组件已销毁
  componentDidUnmount() {}
}

  • 使用建议:

    • constructor初始化 state;

    • componentDidMount中进行事件监听,并在componentWillUnmount中解绑事件;

    • componentDidMount中进行数据的请求,而不是在componentWillMount

    • 需要根据 props 更新 state 时,使用getDerivedStateFromProps(nextProps, prevState)

      • 旧 props 需要自己存储,以便比较;
    public static getDerivedStateFromProps(nextProps, prevState) {
    	// 当新 props 中的 data 发生变化时,同步更新到 state 上
    	if (nextProps.data !== prevState.data) {
    		return {
    			data: nextProps.data
    		}
    	} else {
    		return null1
    	}
    }
    
    
    • 可以在componentDidUpdate监听 props 或者 state 的变化,例如:
    componentDidUpdate(prevProps) {
    	// 当 id 发生变化时,重新获取数据
    	if (this.props.id !== prevProps.id) {
    		this.fetchData(this.props.id);
    	}
    }
    
    
    • componentDidUpdate使用setState时,必须加条件,否则将进入死循环;
    • getSnapshotBeforeUpdate(prevProps, prevState)可以在更新之前获取最新的渲染数据,它的调用是在 render 之后, update 之前;
    • shouldComponentUpdate: 默认每次调用setState,一定会最终走到 diff 阶段,但可以通过shouldComponentUpdate的生命钩子返回false来直接阻止后面的逻辑执行,通常是用于做条件渲染,优化渲染的性能。

二、fiber

Fiber 其实可以算是一种编程思想,在其它语言中也有许多应用(Ruby Fiber)。核心思想是 任务拆分和协同,主动把执行权交给主线程,使主线程有时间空挡处理其他高优先级任务。

当遇到进程阻塞的问题时,任务分割异步调用缓存策略 是三个显著的解决思路。

三、 React hooks

React Hooks(React 16.8)

React 中通常使用 类定义 或者 函数定义 创建组件:

在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。

  • 好处:

    • 1、跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;

    • 2、类定义更为复杂:

      • 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
      • 时刻需要关注this的指向问题;
      • 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
    • 3、状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。

  • 注意:

    • 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
    • 只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
    • 不能在useEffect中使用useState,React 会报错提示;
    • 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;
  • 重要钩子*:

    • 状态钩子 (useState): 用于定义组件的 State,其到类定义中this.state的功能;
    // useState 只接受一个参数: 初始状态
    // 返回的是组件名和更改该组件对应的函数
    const [flag, setFlag] = useState(true);
    // 修改状态
    setFlag(false)
    	
    // 上面的代码映射到类定义中:
    this.state = {
    	flag: true	
    }
    const flag = this.state.flag
    const setFlag = (bool) => {
        this.setState({
            flag: bool,
        })
    }
    
    • 生命周期钩子 (useEffect):

    类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做componentDidMountcomponentDidUpdatecomponentWillUnmount的结合。

    • useEffect(callback, [source])接受两个参数

      • callback: 钩子回调函数;
      • source: 设置触发条件,仅当 source 发生改变时才会触发;
      • useEffect钩子在没有传入[source]参数时,默认在每次 render 时都会优先调用上次保存的回调中返回的函数,后再重新调用回调;
    useEffect(() => {
    	// 组件挂载后执行事件绑定
    	console.log('on')
    	addEventListener()
    	
    	// 组件 update 时会执行事件解绑
    	return () => {
    		console.log('off')
    		removeEventListener()
    	}
    }, [source]);
    
    
    // 每次 source 发生改变时,执行结果(以类定义的生命周期,便于大家理解):
    // --- DidMount ---
    // 'on'
    // --- DidUpdate ---
    // 'off'
    // 'on'
    // --- DidUpdate ---
    // 'off'
    // 'on'
    // --- WillUnmount --- 
    // 'off'
    
    
    • 通过第二个参数,我们便可模拟出几个常用的生命周期:

      • componentDidMount: 传入[]时,就只会在初始化时调用一次;
      const useMount = (fn) => useEffect(fn, [])
      
      
      • componentWillUnmount: 传入[],回调中的返回的函数也只会被最终执行一次;
      const useUnmount = (fn) => useEffect(() => fn, [])
      
      
      • mounted: 可以使用 useState 封装成一个高度可复用的 mounted 状态;
      const useMounted = () => {
          const [mounted, setMounted] = useState(false);
          useEffect(() => {
              !mounted && setMounted(true);
              return () => setMounted(false);
          }, []);
          return mounted;
      }
      
      
      • componentDidUpdate: useEffect每次均会执行,其实就是排除了 DidMount 后即可;
      const mounted = useMounted() 
      useEffect(() => {
          mounted && fn()
      })
      
      
  • 其它内置钩子:

    • useContext: 获取 context 对象

    • useReducer: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux:

      • 并不是持久化存储,会随着组件被销毁而销毁;
      • 属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;
      • 配合useContext的全局性,可以完成一个轻量级的 Redux;(easy-peasy)
    • useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;

    • useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;

    • useRef: 获取组件的真实节点;

    • useLayoutEffect:

      • DOM更新同步钩子。用法与useEffect类似,只是区别于执行时间点的不同。
      • useEffect属于异步执行,并不会等待 DOM 真正渲染后执行,而useLayoutEffect则会真正渲染后才触发;
      • 可以获取更新后的 state;
  • 自定义钩子(useXxxxx): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子,如上面的useMounted。又例如,我们需要每个页面自定义标题:

function useTitle(title) {
  useEffect(
    () => {
      document.title = title;
    });
}

// 使用:
function Home() {
	const title = '我是首页'
	useTitle(title)
	
	return (
		<div>{title}</div>
	)
}

四、Redux

Redux 是一个 数据管理中心,可以把它理解为一个全局的 data store 实例。它通过一定的使用规则和限制,保证着数据的健壮性、可追溯和可预测性。它与 React 无关,可以独立运行于任何 JavaScript 环境中,从而也为同构应用提供了更好的数据同步通道。

  • 核心理念:

    • 单一数据源: 整个应用只有唯一的状态树,也就是所有 state 最终维护在一个根级 Store 中;

    • 状态只读: 为了保证状态的可控性,最好的方式就是监控状态的变化。那这里就两个必要条件:

      • Redux Store 中的数据无法被直接修改;
      • 严格控制修改的执行;
    • 纯函数: 规定只能通过一个纯函数 (Reducer) 来描述修改;

  • 大致的数据结构如下所示:

  • 理念实现:

    • Store: 全局 Store 单例, 每个 Redux 应用下只有一个 store, 它具有以下方法供使用:

      • getState: 获取 state;
      • dispatch: 触发 action, 更新 state;
      • subscribe: 订阅数据变更,注册监听器;
    // 创建
    const store = createStore(Reducer, initStore)
    
    
    • Action: 它作为一个行为载体,用于映射相应的 Reducer,并且它可以成为数据的载体,将数据从应用传递至 store 中,是 store 唯一的数据源
    // 一个普通的 Action
    const action = {
    	type: 'ADD_LIST',
    	item: 'list-item-1',
    }
    
    // 使用:
    store.dispatch(action)
    
    // 通常为了便于调用,会有一个 Action 创建函数 (action creater)
    funtion addList(item) {
    	return const action = {
    		type: 'ADD_LIST',
    		item,
    	}
    }
    
    // 调用就会变成:
    dispatch(addList('list-item-1'))
    
    
    • Reducer: 用于描述如何修改数据的纯函数,Action 属于行为名称,而 Reducer 便是修改行为的实质;
    // 一个常规的 Reducer
    // @param {state}: 旧数据
    // @param {action}: Action 对象
    // @returns {any}: 新数据
    const initList = []
    function ListReducer(state = initList, action) {
    	switch (action.type) {
    		case 'ADD_LIST':
    			return state.concat([action.item])
    			break
    		defalut:
    			return state
    	}
    }
    

    注意:

    1. 遵守数据不可变,不要去直接修改 state,而是返回出一个 新对象,可以使用 assign / copy / extend / 解构 等方式创建新对象;
    2. 默认情况下需要 返回原数据,避免数据被清空;
    3. 最好设置 初始值,便于应用的初始化及数据稳定;
  • 进阶:

    • React-Redux: 结合 React 使用;

      • <Provider>: 将 store 通过 context 传入组件中;

      • connect: 一个高阶组件,可以方便在 React 组件中使用 Redux;

          1. store通过mapStateToProps进行筛选后使用props注入组件
          1. 根据mapDispatchToProps创建方法,当组件调用时使用dispatch触发对应的action
    • Reducer 的拆分与重构:

      • 随着项目越大,如果将所有状态的 reducer 全部写在一个函数中,将会 难以维护
      • 可以将 reducer 进行拆分,也就是 函数分解,最终再使用combineReducers()进行重构合并;
    • 异步 Action: 由于 Reducer 是一个严格的纯函数,因此无法在 Reducer 中进行数据的请求,需要先获取数据,再dispatch(Action)即可,下面是三种不同的异步实现:

五、setState

在了解setState之前,我们先来简单了解下 React 一个包装结构: Transaction:

  • 事务 (Transaction):

    • 是 React 中的一个调用结构,用于包装一个方法,结构为: initialize - perform(method) - close。通过事务,可以统一管理一个方法的开始与结束;处于事务流中,表示进程正在执行一些操作;

  • setState: React 中用于修改状态,更新视图。它具有以下特点:

  • 异步与同步: setState并不是单纯的异步或同步,这其实与调用时的环境相关:

    • 合成事件生命周期钩子(除 componentDidUpdate) 中,setState是"异步"的;

      • 原因: 因为在setState的实现中,有一个判断: 当更新策略正在事务流的执行中时,该组件更新会被推入dirtyComponents队列中等待执行;否则,开始执行batchedUpdates队列更新;

        • 在生命周期钩子调用中,更新策略都处于更新之前,组件仍处于事务流中,而componentDidUpdate是在更新之后,此时组件已经不在事务流中了,因此则会同步执行;
        • 在合成事件中,React 是基于 事务流完成的事件委托机制 实现,也是处于事务流中;
      • 问题: 无法在setState后马上从this.state上获取更新后的值。

      • 解决: 如果需要马上同步去获取新值,setState其实是可以传入第二个参数的。setState(updater, callback),在回调中即可获取最新值;

    • 原生事件setTimeout 中,setState是同步的,可以马上获取更新后的值;

      • 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而setTimeout是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步;
  • 批量更新: 在 合成事件生命周期钩子 中,setState更新队列时,存储的是 合并状态(Object.assign)。因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新;

  • 函数式: 由于 Fiber 及 合并 的问题,官方推荐可以传入 函数 的形式。setState(fn),在fn中返回新的state对象即可,例如this.setState((state, props) => newState);

    • 使用函数式,可以用于避免setState的批量更新的逻辑,传入的函数将会被 顺序调用
  • 注意事项:

    • setState 合并,在 合成事件 和 生命周期钩子 中多次连续调用会被优化为一次;

    • 当组件已被销毁,如果再次调用setState,React 会报错警告,通常有两种解决办法:

      • 将数据挂载到外部,通过 props 传入,如放到 Redux 或 父级中;
      • 在组件内部维护一个状态量 (isUnmounted),componentWillUnmount中标记为 true,在setState前进行判断;

参考

总结