AboutFE icon indicating copy to clipboard operation
AboutFE copied to clipboard

34、Vue3、React16Hook、React Fiber 及相关探讨

Open CodingMeUp opened this issue 4 years ago • 8 comments

目前已有的模式

逻辑复用在 React 中是经历了很长的一段发展历程的, mixin -> HOC & render-props -> Hook

逻辑重用

相信很多接触过 React Hook 的小伙伴已经对这种模式下组件间逻辑复用的简单性有了一定的认知,自从 React 16.7 发布以来,社区涌现出了海量的 Hook 轮子,以及主流的生态库 react-router,react-redux 等等全部拥抱 Hook,都可以看出社区的同好们对于 Hook 开发机制的赞同。

其实组件逻辑复用在 React 中是经历了很长的一段发展历程的,mixin -> HOC & render-props -> Hook,mixin 是 React 中最早启用的一种逻辑复用方式,因为它的缺点实在是多到数不清(REACT是学院派前端库, mixin多了容易导致覆盖代码不易维护, 跟oop设计模式几乎相反),而后面的两种也有着自己的问题,比如增加组件嵌套啊、props 来源不明确啊等等。可以说到目前为止,Hook 是相对完美的一种方案。

Hook 和 Mixin & HOC 对比

说到这里,还是不得不把官方对于「Mixin & HOC 模式」所带来的缺点整理一下。

  • 渲染上下文中公开的属性的来源不清楚。 例如,当使用多个mixin读取组件的模板时,可能很难确定从哪个mixin注入了特定的属性。

  • 命名空间冲突。 Mixins可能会在属性和方法名称上发生冲突,而HOC可能会在预期的prop名称上发生冲突。

  • 性能问题,HOC和无渲染组件需要额外的有状态组件实例,这会降低性能。 而 「Hook」模式带来的好处则是:

  • 暴露给模板的属性具有明确的来源,因为它们是从 Hook 函数返回的值。

  • Hook 函数返回的值可以任意命名,因此不会发生名称空间冲突。

  • 没有创建仅用于逻辑重用的不必要的组件实例。 当然,这种模式也存在一些缺点,比如 ref 带来的心智负担,详见drawbacks。

React Hook 和 Vue Hook 对比

其实 React Hook 的限制非常多,比如官方文档中就专门有一个章节介绍它的限制:

  • 不要在循环,条件或嵌套函数中调用 Hook
  • 确保总是在你的 React 函数的最顶层调用他们。
  • 遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

而 Vue 带来的不同在于:

  • 与React Hooks相同级别的逻辑组合功能,但有一些重要的区别。 与React Hook不同,setup 函数仅被调用一次,这在性能上比较占优。
  • 对调用顺序没什么要求,每次渲染中不会反复调用 Hook 函数,产生的的 GC 压力较小。
  • 不必考虑几乎总是需要useCallback的问题,以防止传递函数prop给子组件的引用变化,导致无必要的重新渲染。
  • React Hook 有臭名昭著的闭包陷阱问题(甚至成了一道热门面试题,omg),如果用户忘记传递正确的依赖项数组,useEffect和useMemo可能会捕获过时的变量,这不受此问题的影响。 Vue的自动依赖关系跟踪确保观察者和计算值始终正确无效。
  • 不得不提一句,React Hook 里的「依赖」是需要你去手动声明的,而且官方提供了一个 eslint 插件,这个插件虽然大部分时候挺有用的,但是有时候也特别烦人,需要你手动加一行丑陋的注释去关闭它。 我们认可React Hooks的创造力,这也是 Vue-Composition-Api 的主要灵感来源。上面提到的问题确实存在于 React Hook 的设计中,我们注意到Vue的响应式模型恰好完美的解决了这些问题。

顺嘴一题,React Hook 的心智负担是真的很严重,如果对此感兴趣的话,请参考:

使用react hooks带来的收益抵得过使用它的成本吗? - 李元秋的回答 - 知乎 https://www.zhihu.com/question/350523308/answer/858145147 并且我自己在实际开发中,也遇到了很多问题,尤其是在我想对组件用 memo 进行一些性能优化的时候,闭包的问题爆炸式的暴露了出来。最后我用 useReducer 大法解决了其中很多问题,让我不得不怀疑这从头到尾会不会就是 Dan 的阴谋……(别想逃过 reudcer

React Hook + TS 购物车实战(性能优化、闭包陷阱、自定义hook) https://link.zhihu.com/?target=https%3A//juejin.im/post/5e5a57b0f265da575b1bc055

CodingMeUp avatar Apr 22 '20 10:04 CodingMeUp

fiber

React 核心的算法包含了三个核心部分,分别是Scheduler(用于决定何时去执行), Reconciliation(决定哪部分需要更新, 阉割版的vdom数据结构的最小编辑树算法),Renderer(使用 Reconciliation 的计算结果,然后将这部分差异,最小化更新到渲染容器)。

实际上,fiber 做的事情就是将之前 react 更新策略进行了重构。fiber 正是模拟了调用栈,并且通过链表来重新组织,一方面使得我们可以实现 chunks(不用1口气做完,营造出不卡分片效果) 的功能。 另一方面可以和 VDOM 进行很好的对应和映射。那么 fiber 是如何完成“增量更新”呢? 秘诀就是它相当于“重新实现了浏览器调用栈”。

传统调用栈            fiber

  子函数          component type
 函数嵌套              child
  参数                props
 返回地址             parent
  返回值          DOM elements

React只是一个'JavaScript',同时只能做一件事情,这个和 DOS 的单任务操作系统一样的,事情只能一件一件的干。要是前面有一个傻叉任务长期霸占CPU,后面什么事情都干不了,浏览器会呈现卡死的状态,这样的用户体验就会非常差。

对于’前端框架‘来说,解决这种问题有三个方向:

1️⃣ 优化每个任务,让它有多快就多快。挤压CPU运算量 2️⃣ 快速响应用户,让用户觉得够快,不能阻塞用户的交互 3️⃣ 尝试 Worker 多线程

Vue 选择的是第1️⃣, 因为对于Vue来说,使用模板让它有了很多优化的空间,配合响应式机制可以让Vue可以精确地进行节点更新, 读者可以去看一下今年Vue Conf 尤雨溪的演讲,非常棒!;而 React 选择了2️⃣ 。对于Worker 多线程渲染方案也有人尝试,要保证状态和视图的一致性相当麻烦。

React 为什么要引入 Fiber 架构?React V15 下面的一个列表渲染资源消耗情况。整个渲染花费了130ms, 🔴在这里面 React 会递归比对VirtualDOM树,找出需要变动的节点,然后同步更新它们, 一气呵成。这个过程 React 称为 Reconcilation(中文可以译为协调).

在 Reconcilation 期间,React 会霸占着浏览器资源,一则会导致用户触发的事件得不到响应, 二则会导致掉帧,用户可以感知到这些卡顿

React 的 Reconcilation 是CPU密集型的操作, 它就相当于我们上面说的’长进程‘。所以初衷和进程调度一样,我们要让高优先级的进程或者短进程优先运行,不能让长进程长期霸占资源。

所以React 是怎么优化的? 划重点,

🔴为了给用户制造一种应用很快的'假象',我们不能让一个程序长期霸占着资源. 你可以将浏览器的渲染、布局、绘制、资源加载(例如HTML解析)、事件响应、脚本执行视作操作系统的'进程',我们需要通过某些调度策略合理地分配CPU资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。

🔴所以 React 通过Fiber 架构,让自己的Reconcilation 过程变成可被中断。 '适时'地让出CPU执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:

  • 与其一次性操作大量 DOM 节点相比, 分批延时对DOM进行操作,可以得到更好的用户体验。这个在《「前端进阶」高性能渲染十万条数据(时间分片)》 以及司徒正美的《React Fiber架构》 都做了相关实验
  • 司徒正美在《React Fiber架构》 也提到:🔴给浏览器一点喘息的机会,他会对代码进行编译优化(JIT)及进行热代码优化,或者对reflow进行修正.

这就是为什么React 需要 Fiber 😏。

凌波微步

同样来自Link Clark 的 Slider

前面说了一大堆,从操作系统进程调度、到浏览器原理、再到合作式调度、最后谈了React的基本改造工作, 地老天荒... 就是为了上面的小人可以在练就凌波微步, 它脚下的坑是浏览器的调用栈。

React 开启 Concurrent Mode 之后就不会挖大坑了,而是一小坑一坑的挖,挖一下休息一下,有紧急任务就优先去做。

开启 Concurrent Mode 后,我们可以得到以下好处(详见Concurrent Rendering in React):

  • 快速响应用户操作和输入,提升用户交互体验
  • 让动画更加流畅,通过调度,可以让应用保持高帧率
  • 利用好I/O 操作空闲期或者CPU空闲期,进行一些预渲染。 比如离屏(offscreen)不可见的内容,优先级最低,可以让 React 等到CPU空闲时才去渲染这部分内容。这和浏览器的preload等预加载技术差不多。
  • 用Suspense 降低加载状态(load state)的优先级,减少闪屏。 比如数据很快返回时,可以不必显示加载状态,而是直接显示出来,避免闪屏;如果超时没有返回才显式加载状态。

但是它肯定不是完美的,因为浏览器无法实现抢占式调度,无法阻止开发者做傻事的,开发者可以随心所欲,想挖多大的坑,就挖多大的坑。 为了共同创造美好的世界,我们要严律于己,该做的优化还需要做: 纯组件、虚表、简化组件、缓存...

尤雨溪在今年的Vue Conf一个观点让我印象深刻:如果我们可以把更新做得足够快的话,理论上就不需要时间分片了。

时间分片并没有降低整体的工作量,该做的还是要做, 因此React 也在考虑利用CPU空闲或者I/O空闲期间做一些预渲染。所以跟尤雨溪说的一样:React Fiber 本质上是为了解决 React 更新低效率的问题,不要期望 Fiber 能给你现有应用带来质的提升, 如果性能问题是自己造成的,自己的锅还是得自己背.

这可能是最通俗的 React Fiber(时间分片) 打开方式

时间切片的实现和调度

CodingMeUp avatar Jun 29 '20 16:06 CodingMeUp

[实战] 为了学好 React Hooks, 我抄了 Vue Composition API, 真香

Vue Composition API 是 Vue 3.0 的一个重要特性,和 React Hooks 一样,这是一种非常棒的逻辑组合/复用机制。尽管初期受到不少争议,我个人还是比较看好这个 API 提案,因为确实解决了 Vue 以往的很多痛点, 这些痛点在它的 RFC 文档中说得很清楚。动机和 React Hooks 差不多,无非就是三点:

① 逻辑组合和复用 ② 更好的类型推断。完美支持 Typescript ③ Tree-shakable 和 代码压缩友好

如果你了解 React Hooks 你会觉得 VCA 身上有很多 Hooks 的影子, 毕竟官方也承认 React Hooks 是 VCA 的主要灵感来源,但是 Vue 没有完全照搬 React Hooks,而是基于自己的数据响应式机制,创建出了自己特色的逻辑复用原语, 辨识度也是非常高的。

VCA 官方 RFC 文档已经很详细列举了它和 React Hooks 的差异:

  1. 总的来说,更符合惯用的 JavaScript 代码直觉。这主要是 Immutable 和 Mutable 的数据操作习惯的不同。
// Vue: 响应式数据, 更符合 JavaScript 代码的直觉, 就是普通的对象操作
const data = reactive({count: 1})
data.count++

// React: 不可变数据, JavaScript 原生不支持不可变数据,因此数据操作会 verbose 一点
const [count, setCount] = useState(1)
setCount(count + 1)
setCoung(c => c + 1)

// React: 或者使用 Reducer, 适合进行一些复杂的数据操作
const initialState = {count: 0, /* 假设还有其他状态 */};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {...state, count: state.count + 1};
    case 'decrement':
      return {...state, count: state.count - 1};
    default:
      return state
  }
}
const [state, dispatch] = useReducer(reducer, initialState)
dispatch({type: 'increment'})

② 不关心调用顺序和条件化。React Hooks 基于数组实现,每次重新渲染必须保证调用的顺序,否则会出现数据错乱。VCA 不依赖数组,不存在这些限制。 ③ 不用每次渲染重复调用,减低 GC 的压力。 每次渲染所有 Hooks 都会重新执行一遍,这中间可能会重复创建一些临时的变量、对象以及闭包。而 VCA 的setup 只调用一次。

// React
function MyComp(props) {
  const [count, setCount] = useState(0)
  const add = () => setCount(c => c+1)  // 这些内联函数每次渲染都会创建
  const decr = () => setCount(c => c-1)

  useEffect(() => {
    console.log(count)
  }, [count])

  return (<div>
    count: {count}
    <span onClick={add}>add</span>
    <span onClick={decr}>decr</span>
  </div>)
}

④ 不用考虑 useCallback/useMemo 问题。 因为问题 ③ , 在 React 中,为了避免子组件 diff 失效导致无意义的重新渲染,我们几乎总会使用 useCallback 或者 useMemo 来缓存传递给下级的事件处理器或对象

// React
function MyComp(props) {
  const [count, setCount] = useState(0)
  const add = useCallback(() => setCount(c => c+1), [])
  const decr = useCallback(() => setCount(c => c-1), [])

  useEffect(() => {
    console.log(count)
  }, [count])

  return (<SomeComplexComponent count={count} onAdd={add} onDecr={decr}/>)
}

// Vue: 没有此问题, 通过对象引用存取最新值
createComponent({
  setup((props) => {
    const count = ref(0)
    const add = () => count.value++
    const decr = () => count.value--
    watch(count, c => console.log(c))

    return () => <SomeComplexComponent count={count} onAdd={add} onDecr={decr}/>
  })
})

⑤ 不必手动管理数据依赖。在 React Hooks 中,使用 useCallback、useMemo、useEffect 这些 Hooks,都需要手动维护一个数据依赖数组。当这些依赖项变动时,才让缓存失效。

这往往是新手接触 React Hooks 的第一道坎。你要理解好闭包,理解好 Memoize 函数 ,才能理解这些 Hooks 的行为。这还不是问题,问题是这些数据依赖需要开发者手动去维护,很容易漏掉什么,导致bug。

VCA 由于不存在 ④ 问题,当然也不存在 ⑤问题。 Vue 的响应式机制可以自动、精确地跟踪数据依赖,而且基于对象引用的不变性,我们不需要关心闭包问题。

基本 API 类比

首先,你得先了解 React Hooks 和 VCA。最好的学习资料是它们的官方文档。下面简单类比一下两者的 API:

  React Hooks Vue Composition API
状态 const [value, setValue] = useState(0) or useReducer const state = reactive({value: 0}) or ref(0)
状态变更 setValue(1) orsetValue(n => n + 1) or dispatch state.value = 1 or state.value++
状态衍生 useMemo(() => derived, [deps]) computed(() => derived)
对象引用 const foo = useRef(0); + foo.current = 1 const foo = ref(0) + foo.value = 1
挂载 useEffect(() => {/挂载/}, []) onBeforeMount(() => {/挂载前/}) + onMounted(() => {/挂载后/})
卸载 useEffect(() => {/挂载/; return () => {/卸载/}}, []) onBeforeUnmount(() => {/卸载前/}) + onUnmounted(() => {/卸载后/})
重新渲染 useEffect(() => {/更新/}) onBeforeUpdate(() => {/更新前/}) + onUpdated(() => {/更新后/})
异常处理 目前只有类组件支持(componentDidCatch 或 static getDerivedStateFromError onErrorCaptured((err) => {/异常处理/})
依赖监听 useEffect(() => {/依赖更新/}, [deps]) const stop = watch(() => {/自动检测数据依赖, 更新.../})
依赖监听 + 清理 useEffect(() => {/.../; return () => {/清理/}}, [deps]) watch(() => [deps], (newVal, oldVal, clean) => {/更新/; clean(() => {/* 清理*/})})
Context 注入 useContext(YouContext) inject(key) + provider(key, value)

对比上表,我们发现两者非常相似,每个功能都可以在对方身上找到等价物。 React Hooks 和 VCA 的主要差别如下:

  • 数据方面Mutable vs ImmutableReactive vs Diff
  • 更新响应方面。React Hooks 和其组件思维一脉相承,它依赖数据的比对来确定依赖的更新。而Vue 则基于自动的依赖订阅。这点可以通过对比 useEffect 和 watch 体会。
  • 生命周期钩子。React Hooks 已经弱化了组件生命周期的概念,类组件也废弃了componentWillMountcomponentWillUpdatecomponentWillReceiveProps 这些生命周期方法。 一则我们确实不需要这么多生命周期方法,React 做了减法;二则,Concurrent 模式下,Reconciliation 阶段组件可能会被重复渲染,这些生命周期方法不能保证只被调用一次,如果在这些生命周期方法中包含副作用,会导致应用异常, 所以废弃会比较好。Vue Composition API 继续沿用 Vue 2.x 的生命周期方法.

其中第一点是最重要的,也是最大的区别(思想)。这也是为什么 VCA 的 'Hooks' 只需要初始化一次,不需要在每次渲染时都去调用的主要原因: 基于Mutable 数据,可以保持数据的引用,不需要每次都去重新计算

CodingMeUp avatar Jun 30 '20 07:06 CodingMeUp

  1. React 与 JSX React是基于JSX,JSX则几乎拥有了JS所有的灵活度,在开发中我们也可以深感其便利,Dom中开一个大括号就可以写JS逻辑判断,甚至使用map动态生产列表。 JSX通过Balel编译形成数组,再使用React.createElement创建为节点。由此可见,JSX在React中的使用非常灵活。React对v-dom的处理也非常灵活,一切皆是组件。 但是,过度灵活就会付出更多的性能代价用于Diff Tree,虽然可以从组件层面进行Diff,避免渲染,但在组件内部,还是要逐一进行比较的,Reat很难去追踪一个组件里,哪一部分Dom是静态。为此,React16推出Fiber更新策略,采用时间分片和任务优先调度去解决长时间Diff导致主线程被占用,从而导致页面动画渲染以及各种点击事件被阻塞,带给用户卡顿感的问题。

  2. Vue 与 template Vue 3中使用Block Tree(区块树),将template划分为结构性指令v-for、v-if内部和外部,使用Array去追踪会发生改变的节点,极大的减少不必要的Diff操作。 Vue数据绑定,基于defineProperty,进行set,get。但JS希望对象更加稳定,而不是经常会被改动,这样有利于性能优化,因此Vue3将向ES6中的Proxy迁移,Proxy严格来说只是在被操作对象外部进行了一个包装拦截。因此,即使Fiber很优秀,但Vue可以通过更快,而不需要使用Fiber。···大概就是只要我跑得够快,Fiber就追不上我

CodingMeUp avatar Jul 03 '20 03:07 CodingMeUp

Mixin & HOC vs Hook

,这里用 compose 函数组合了好几个 HOC,其中还有 connect 这种 接受几个参数返回一个接受组件作为函数的函数 这种东西,如果你是新上手(或者哪怕是 React 老手)这套东西的人,你会在 「这个 props 是从哪个 HOC 里来的?」,「这个 props 是外部传入的还是 HOC 里得到的?」这些问题中迷失了大脑,最终走向堕落

「Mixin & HOC 模式」所带来的缺点整理一下。

  • 渲染上下文中公开的属性的来源不清楚。 例如,当使用多个 mixin 读取组件的模板时,可能很难确定从哪个 mixin 注入了特定的属性。
  • 命名空间冲突。 Mixins 可能会在属性和方法名称上发生冲突,而 HOC 可能会在预期的 prop 名称上发生冲突。
  • 性能问题,HOC 和无渲染组件需要额外的有状态组件实例,这会降低性能。

而 「Hook」模式带来的好处则是:

  • 暴露给模板的属性具有明确的来源,因为它们是从 Hook 函数返回的值。
  • Hook 函数返回的值可以任意命名,因此不会发生名称空间冲突。
  • 没有创建仅用于逻辑重用的不必要的组件实例。

当然,这种模式也存在一些缺点,比如 ref 带来的心智负担

CodingMeUp avatar Jul 03 '20 03:07 CodingMeUp

Vue2 目前存在的问题

抛出 Vue2 的代码模式下存在的几个问题。

  • 随着功能的增长,复杂组件的代码变得越来越难以维护。 尤其发生你去新接手别人的代码时。 根本原因是 Vue 的现有 API 通过「选项」组织代码,但是在大部分情况下,通过逻辑考虑来组织代码更有意义。
  • 缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机制。
  • 类型推断不够友好。

CodingMeUp avatar Jul 03 '20 03:07 CodingMeUp

自定义HOOK

  • useFetch
import { useState, useEffect } from 'react';

const useFetch = (url = '', options = null) => {
 const [data, setData] = useState(null);
 const [error, setError] = useState(null);
 const [loading, setLoading] = useState(false);

 useEffect(() => {
   let isMounted = true;

   setLoading(true);

   fetch(url, options)
     .then(res => res.json())
     .then(data => {
       if (isMounted) {
         setData(data);
         setError(null);
       }
     })
     .catch(error => {
       if (isMounted) {
         setError(error);
         setData(null);
       }
     })
     .finally(() => isMounted && setLoading(false));

   return () => (isMounted = false);
 }, [url, options]);

 return { loading, error, data };
};

export default useFetch;
  const { loading, error, data = [] } = useFetch(
    'https://hn.algolia.com/api/v1/search?query=react'
  );

CodingMeUp avatar May 15 '21 12:05 CodingMeUp

感觉vue3+jsx比较不错,不过享受不到template的优势了

Mrcxt avatar Mar 14 '22 09:03 Mrcxt