博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
react hooks 的useCallback hell问题总结
阅读量:4084 次
发布时间:2019-05-25

本文共 7548 字,大约阅读时间需要 25 分钟。

很早总结的hooks的问题文章,内部讨论一直没想到啥最优解,发出来看看有没有人有更好的解法

最近rxjs作者ben lesh发了条推  如此推所示,useCallback问题非常严重,社区也讨论了很多做法,但仍然有很多问题。

useCallback问题缘由

先回顾下hook之前组件的写法

class组件

export class ClassProfilePage extends React.Component
{ showMessage = () => { alert('Followed ' + this.props.user); }; handleClick = () => { setTimeout(this.showMessage, 3000); }; render() { return
; }}

functional组件

export function FunctionProfilePage(props) { const showMessage = () => {    alert('Followed ' + props.user);  }; const handleClick = () => {    setTimeout(showMessage, 3000);  }; return (      );}

点击按钮,同时将user由A切换到B时,class组件显示的是B而function组件显示的是A,这两个行为难以说谁更加合理

import React, { useState} from "react";import ReactDOM from "react-dom";import { FunctionProfilePage, ClassProfilePage  } from './profile'import "./styles.css";function App() {  const [state,setState] = useState(1); return (    
state:{state}
// 点击始终显示的是快照值
// 点击始终显示的是最新值
);}const rootElement = document.getElementById("root");ReactDOM.render(
, rootElement);

当你的应用里同时存在Functional组件和class组件时,你就面临着UI的不一致性,虽然react官方说function组件是为了保障UI的一致性,但这是建立在所有组件都是functional组件,事实上这假设几乎不成立,如果你都采用class组件也可能保证UI的一致性(都显示最新值),一旦你页面里混用了class组件和functional 组件(使用useref暂存状态也视为class组件),就存在的UI不一致性的可能

快照 or 最新值

所以function和class最大区别只在于默认情况不同,两者可以相互转换,快照合理还是最新值合理,这完全取决于你的业务场景,不能一概而论

事实上在class里也可以拿到快照值,在function里也可以拿到最新值

class里通过触发异步之前保存快照即可

export class ClassProfilePage extends React.Component
{ showMessage = (message) => { alert('Followed ' +message); }; handleClick = () => { const message = this.props.user // 在触发异步函数之前保存快照 setTimeout(() =>showMessage(message)), 3000); }; render() { return
; }}

function里通过ref 容器存取最新值

export function FunctionProfilePage(props) { const ref = useRef("");  useEffect(() => {    ref.current = props.user;  }); const showMessage = () => { console.log('ref:',ref)    alert("Followed " + props.user +',' + ref.current);  }; const handleClick = () => {    setTimeout(showMessage, 3000);  }; return ;}

其实就是个经典的函数闭包问题

  • 在异步函数执行前可以对闭包访问的自由变量进行快照捕获:实现快照功能
  • 在异步函数执行中可以通过ref读取最新的值
for(var i=0;i<10;i++){   setTimeout(() => console.log('val:',i)) // 拿到的是最新值}for(var i=0;i<10;i++){  setTimeout(((val) => console.log('val:',val)).bind(null,i)); // 拿到的是快照}const ref = {current: null}for(var i=0;i<10;i++){   ref.current = i;  setTimeout(((val) => console.log('val:',ref.current)).bind(null,ref)); // 拿到的是最新值}for (var i = 0; i < 10; i++) { // 拿到的是快照 let t = i;  setTimeout(() => { console.log("t:", t);  });}

重渲染机制

虽然functional和class组件在快照处理方式不一致,但是两者的重渲染机制,并没有大的区别

class重渲染触发条件,此处暂时不考虑采用shouldComponentUpdate和pureComponent优化

  • this.setState : 无条件重渲染,不进行新旧比较
  • this.forceUpdate: 无条件重渲染,不进行新旧比较
  • 父组件render带动子组件render: 无条件,和props是否更新无关
  • 祖先组件context变动: 不做props变动假设

我们发现react默认的重渲染机制压根没有对props做任何假设,性能优化完全交给框架去做,react-redux 基于shouldComponent, mobx-react 基于this.forceUpdatehooks 来做一些性能优化

带来的问题

我们发现即使不用hooks本身functional组件和class组件表现就存在较大差异,由于hook目前只能在function组件里使用,这导致了一些本来是functional组件编程思维的问题反映到了hooks上。

hooks的使用引入了两条强假设,导致了编程思维的巨大变动

  • 只能在functional组件里使用: 导致我们需要处理最新值的问题
  • 副作用(包括rerender和effect)基于新旧值的reference equality : 强制我们使用immutable进行编程

上述两条带来了很大的心智负担

Stale closure 与 infinite loop

这两个问题是硬币的两面,通常为了解决一个问题,可能导致另外一个问题

一个最简单的case就是一个组件依赖了父组件的callback,同时内部useffect依赖了这个callback

如下是一个典型的搜索场景

function Child(props){ console.log('rerender:') const [result,setResult] = useState('') const { fetchData } = props;  useEffect(() => {    fetchData().then(result => {      setResult(result);    })  },[fetchData]) return (    
query:{props.query}
result:{result}
)}export function Parent(){ const [query,setQuery] = useState('react'); const fetchData = () => { const url = 'https://hn.algolia.com/api/v1/search?query=' + query return fetch(url).then(x => x.text()) } return (
setQuery(e.target.value)} value={query} />
)}

上述代码存在的一个问题就是,每次Parent重渲染都会生成一个新的fetchData,因为fetchData是Child的useEffect的dep,每次fetchData变动都会导致子组件重新触发effect,一方面这会导致性能问题,假如effect不是幂等的这也会导致业务问题(如果在effect里上报埋点怎么办)

解决思路1:

不再useEffect里监听fetchData: 导致stale closure 问题 和页面UI不一致

useEffect(() => {    fetchData().then(result => {      setResult(result);    })  },[]) // 去掉fetchData依赖

此时一方面父组件query更新,但是子组件的搜索并未更新但是子组件的query显示却更新了,这导致了子组件的UI不一致

解决思路2:

在思路1的基础上加强刷token

// childuseEffect(() => { fetchData().then(result => {      setResult(result);    })},[refreshToken]);// parent

问题:

  • 如果子组件的effect较多,需要建立refreshToken和effect的映射关系
  • 触发eslint-hook的warning,进一步的可能触发eslint-hook的auto fix功能,导致bug
  • fetchData仍然可能获取的是旧的闭包?

为了更好的语义化和避免eslint的报错,可以自定义封装useDep来解决

useDepChange(() =>   fetchData().then(result => {      setResult(result);    })  },[fetchData])},[queryToken]); // 只在dep变动的时候触发,约等于componentWillReceiveProps了
  • 实际上是放弃了eslint-hook的 exhaustive检查,可能会导致忘记添加某些依赖,需要写代码时非常仔细了

解决思路3:

useCallback包裹fetchData, 这实际上是把effect强刷的控制逻辑从callee转移到了caller

// parent const fetchData = useCallback(() => { const url = 'https://hn.algolia.com/api/v1/search?query=' + query return fetch(url).then(x => x.text())  },[query]);// child  useEffect(() => {    fetchData().then(result => {      setResult(result);    })  },[fetchData])

问题:

  • 如果child的useEffect里依赖了较多的callback,需要所有的callback都需要进行useCallback包装,一旦有一个没用useCallback包装,就前功尽弃
  • props的不可控制,Parent的fetchData很可能是从其他组件里获取的,自己并没有控制fetchData不可变的权限,这导致千里之外的一个祖先组件改变了fetchData,导致Child最近疯狂刷新effect,这就需要将callback做层层useCallback处理才能避免该问题
  • 官方说useCallback不能做语义保障,而且存在cache busting的风险
  • 组件API的设计:我们发现此时设计组件时需要关心传进来的组件是否是可变的了,但是在接口上并不会反馈这种依赖
  // onClick改变会触发Button的effect吗?

解决思路4:

使用useEventCallback作为逃生舱,这也是官方文档给出的一种用法

// childuseEventCallback(() => {  fetchData().then(result => {     setResult(result);  });},[fetchData]);function useEventCallback(fn, dependencies) {  const ref = useRef(() => {    throw new Error('Cannot call an event handler while rendering.');  });  useEffect(() => {    ref.current = fn;  }, [fn, ...dependencies]);  return useCallback(() => {    const fn = ref.current;    return fn();  }, [ref]);}

这仍然存在问题,

解决思路5:

拥抱mutable,实际上这种做法就是放弃react的快照功能(变相放弃了concurrent mode ),达到类似vue3的编码风格

实际上我们发现hook + mobx === vue3, vue3后期的api实际上能用mobx + hook进行模拟(某种程度上更加简洁)

问题就是: 可能放弃了concurrent mode (concurrent mode更加关注的是UX,对于一般业务开发效率和可维护性可能更加重要)

 

 

调用者约定:

  • 父组件传递给子组件的callback: 永远获取到的是父组件的最新state (通过useObservable|useRef)

被调用者约定

  • 不要把callback作为useEffect的依赖:因为我们已经限定了callback永远是最新的,实际上避免了陈旧闭包问题,所以不需要把callback作为depdency
  • 代码里禁止直接使用useEffect:只能使用自定义封装的hook,(因为useEffect会触发eslint-hook的warning,每次都禁止不好,且useEffect没有那么语义化)如可以使用如下hook
    • useMount: 只在mount触发(更新不触发)
    • useUpdateEffect: 只在更新时触发(mount不触发)
    • useDepChange: dep改变时触发,功能和useEffect类似,不会触发wanring
// parent.jsexport observer(function VueParent(){ const [state] = useState(observable({    query: 'reqct'  })) const fetchData = () => { const url = 'https://hn.algolia.com/api/v1/search?query=' + state.query return fetch(url).then(x => x.text())  } return (    
state.query = e.target.value} value={state.query} />
)})// child.jsexport function observer(VueChild(props){ const [result,setResult] = useState('') useMount(() => { props.fetchData().then(result => { setResult(result); }) }) useUpdateEffect(() => { props.fetchData().then(result => { setResult(result); }) },[props.query]) /* 或者使用useDepChange useUpdateEffect(() => { props.fetchData().then(result => { setResult(result); }) },[props.query]) */ return (
query: {props.query}
result:{result}
)})

转载地址:http://vvqni.baihongyu.com/

你可能感兴趣的文章
【数据结构周周练】008 二叉树的链式创建及测试
查看>>
《软件体系结构》 第九章 软件体系结构评估
查看>>
《软件过程管理》 第六章 软件过程的项目管理
查看>>
《软件过程管理》 第九章 软件过程的评估和改进
查看>>
《数据库系统概论》 第三章 关系数据库标准语言SQL
查看>>
《计算机网络》第五章 运输层 ——TCP和UDP 可靠传输原理 TCP流量控制 拥塞控制 连接管理
查看>>
堆排序完整版,含注释
查看>>
二叉树深度优先遍历和广度优先遍历
查看>>
生产者消费者模型,循环队列实现
查看>>
IA32时钟周期的一些内容
查看>>
获得github工程中的一个文件夹的方法
查看>>
《PostgreSQL技术内幕:查询优化深度探索》养成记
查看>>
PostgreSQL查询优化器详解之逻辑优化篇
查看>>
STM32中assert_param的使用
查看>>
IO口的作用
查看>>
归档与解归档
查看>>
为什么button在设置标题时要用一个方法,而不像lable一样直接用一个属性
查看>>
字符串的截取
查看>>
Tensorflow入门资料
查看>>
剑指_用两个栈实现队列
查看>>