closertb / closertb.github.io

浏览issue 或 我的网站,即可查看我的所有博客
https://closertb.site
32 stars 0 forks source link

一起学习React18 新特性 #92

Open closertb opened 2 years ago

closertb commented 2 years ago

匆匆过客:React17

React17 新增特性:对使用者来说,无新特性, 官方原话:

The React 17 release is unusual because it doesn’t add any new developer-facing features. Instead, this release is primarily focused on making it easier to upgrade React itself.

事件系统重构

作为一个React 库深度使用者,我个人是特别喜欢这种All In Js的开发方式,整个页面逻辑都可以用js写,而不用html与js之间不断切换、相互设计;

但另一个重要的点就是开发过程不用考虑事件的跨浏览器兼容问题,因为React已经通过合成事件(SyntheticEvent)做了主流浏览器的兼容。

虽说使用方式大部分一致,但和原生事件并不完全相同, 差异在于:

React 17对事件系统做了重构,其改变点:

20220524094816

import React from 'react';
import ReactDOM from 'react-dom';

class Foo extends React.Component {
  enter(e){ console.log('click foo');
  e.stopPropagation(); }
  render() {
    return <div style={{ height: 30, background: 'blue' }} onClick={this.enter}>Foo</div>;
  }
}

export default class Bar extends React.Component {
  enter(e){ console.log('click bar'); }
  componentDidMount() {
    ReactDOM.render(<Foo />, this.refs.c);
  }
  render() {
    return <div style={{ height: 50, background: 'red', color: 'white' }} onClick={this.enter}>Bar <div ref="c"/></div>;
  }
}

看上面的示例,页面 UI 如下图,如果是17以前,当我们点击Foo区域时,期望Bar区域不要响应,纵使加了stopPropagation, 也不能阻止;但随着17的到来,这个bug就可以避免,这也是为渐进式升级做铺垫 20220528234609

承上启下

v17开启了React渐进式升级的新篇章(略微有点虚张声势)

17以后,允许同一个页面上使用不同的 React 版本。根据官方的demo示例,是基于懒加载的特性实现,用法有点类似于SPA微应用的玩法。个人觉得这并没有黑魔法,如果不用二次加载,而期望某个组件用单独的react版本,这该报错还是报错,就像下面这样: 20220528181348

所以通过了解前面两个特性,确实是印证官方那句话:17只是为了给18及以后的版本铺路,为了让用户更简单的去升级版本

React18 新特性

自动批处理(Auto Bacthing)

批处理是 React 将多个状态更新分组到一个重新渲染中,以获得更好的性能(减少render次数)。 在React 18以前,批处理更新只会发生在React事件回调中,而在Promise、setTimeOut、原生事件回调或或任何其他事件内部的更新都没有采用批处理,举个🌰:

// 18以前: .
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // render 2次
}, 1000);

// 18及以后: 
setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // render1次
}, 1000);

绕过黑魔法:flushSync

自定义渲染优先级

在react16退出异步渲染时,提出了渲染优先级的概念,like: 用户输入 > 动画 > 列表展示。在多个渲染任务到来时,它会优先渲染高优先级的任务。

但由于这是一个理想的设想,程序自己是很难判断什么一定是高优先级,因为用户使用场景太复杂了;所以在18中,这个底层能力进一步开放,推出了几个hooks:startTransition、useTransition、 useDeferredValue等,这些hooks 可以让用户自定义渲染优先级

举个例子,输入搜索框,直观点: 20220530095408

下面是伪代码,直接看demo演示直接一点:

import {startTransition} from 'react';
// 高优先级: 输入数据回显
setInputValue(input);

// 使用Transition 对低优先级的渲染进行标记
startTransition(() => {
  setQuery(input);
});

官方的列子,我觉得对图形化的用户会更易感触,有兴趣的可以了解一下:<官方示例>

所以最新版的React将 state 的更新分成了两类:

通过这些hooks 我们可以自定义渲染优先级。

新的入口挂载API:createRoot

// 18 以前:
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);

// 18 以后:
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container); // 
root.render(<App tab="home" />);

值得一提的是,18对老的挂载方式也是兼容的,只是这种用法没法使用新特性(开发环境会提示), 这也是渐进式升级的一部分

11.55.29

渐进式升级

官方原话:从技术上讲,并发渲染是一个突破性的升级。因为并发渲染是可中断的,所以启用它时组件的行为会略有不同,这个比例大概0.2%.

所以官方为了大家更加顺滑的升级到18,提出了渐进式升级,提供一个StrictMode API,使用这个模块包裹的节点,将遵从严格模式,这有助于:

下面的demo, Header 和 Footer 将以正常模式渲染,而被StrictMode包裹的Component节点,将以严格模式渲染

import React from 'react';

function ExampleApplication() {
  return (
    <div>
      <Header />
      <React.StrictMode>
        <div>
          <ComponentOne />
          <ComponentTwo />
        </div>
      </React.StrictMode>
      <Footer />
    </div>
  );
}

严格模式不能自动检测到你应用版本升级带来的副作用,仅可以帮助发现它们,使它们更具确定性,通过故意重复调用一些声明函数来实现(具体请查看官方文档

// 正常模式
* React mounts the component.
  * Layout effects are created.
  * Effects are created.

// 严格模式
* React mounts the component.
    * Layout effects are created.
    * Effect effects are created.
* React simulates effects being destroyed on a mounted component.
    * Layout effects are destroyed.
    * Effects are destroyed.
* React simulates effects being re-created on a mounted component.
    * Layout effects are created
    * Effect setup code runs

虽说这个模块只在开发环境有效,但细心的人会发现,重复的销毁和挂载他还是会带来新的问题,例如:

const [value, setValue] = useState(0);
useEffect(() => {
  setValue(val => val + 1);
})
// 正常模式打印的是1,但严格模式这会打印2;

关于这个问题国内已经有文章开始讨论,react 仓库也有一个 issue是关于这个问题的讨论,感兴趣的可以戳这里

浅析并发渲染

首先在说react渲染之前,复习一个知识点:浏览器中的js执行和UI渲染是在一个线程中顺序发生(包含js执行,事件响应,定时器,UI渲染),且js的执行是单线程(基于eventloop)。

20220603155838

这个单线程就决定了,处理A就处理不了B。

有了这个共识,我们来理一理react渲染史

同步渲染

最早的react(16之前)是同步的,就是指当用户通过 setState 触发一个更新,到更新渲染到页面,会经历两个过程:

由于整个过程是同步的,会一直占据js线程;所以在一个更新的过程中,页面发生的点击、输入等交互事件都会等待,直到这次更新完成,显然这个体验是糟糕的。

异步渲染

为了解决同步渲染阻塞主线程的问题,那就让渲染变的更加灵活--造成这些最大的问题不是性能,而是调度(React Conf 2018)。

基于此一个轰动前端圈的名词诞生-Fiber,新的渲染架构最大的特点就是将以往一条路走到黑的玩法分成了两个阶段:

通过链表表示render阶段节点之间的关系,并加持时间切片的方式,链表上节点是否继续向后比对,取决于当前线程是否空闲; 并且react会将每个更新任务标注优先级,如果新的更新优先级高于正在处理的任务,那么前一次任务就会被打断废弃,从而处理更高优先级的任务;

这里需要明白的是: 一旦一个更新任务被打断废弃,高优先级任务执行完后,这个任务是需要从头再来一遍的计算的。

另一个要记住的点是,是先有Fiber再有hooks,而不是hooks带来了Fiber。

并发渲染

只要你读过React18的一些文章,你可能会听过Concurrent Mode、Concurrent React 或者 Concurrent features, 这些名词都不重要,重要的是明白他的目的。

首先,先纠正一下大家听到异步渲染可能产生的一个错误臆断:异步渲染不是指一个树的两个或多个分支同时被渲染。

动下脚指头想一想,这怎么可能,这本身就是与浏览器渲染原理就是相悖的。

官方原话:

Concurrency is not a feature, per se. It’s a new behind-the-scenes mechanism that enables React to prepare multiple versions of your UI at the same time.

multiple versions of your UI 圈起来,考试要考。

并发渲染成功解决了异步渲染中中断废弃的问题,他是中断可恢复继续的;不过在一些场景,也会存在中断废弃的问题。

这个能力,在以后也能解锁另一个使用场景:状态复用。比如一个tab切换,当从a 切到 b,再从b切回a时,这时候通过某些实现,我们就可以状态复用,快速的在屏幕上展示出a;

并发渲染带来的意义远不止这些(还包括对server,native端),我只捡了点我能看懂的分享给大家。反正官方写的非常美好,我个人还是比较期待。

推荐阅读:

总结

通过对react18 新特性的熟悉及渲染模式的讲解,让我们可以感受到,这个版本将对我们应用的体验将带来肉眼可见的提升,前提是你不滥用,并且会用。

并且随着基于底层的不断暴露和并发模式不断的建设,以后可能我们用的就不是react,而是Remix,Next这种基于react库建立起来的生态框架,这也是React工作组成立起来的意义之一,更好的打造React生态。

很难想象,这居然是是我2022年第一篇文章。今年全中国最迷茫的是中国经济,第二可能就是我了,但愿这是个转折!!!