导图社区 React 知识图谱
React 知识图谱,包括生命周期、组件设计、函数组件 / 类组件、状态管理、渲染过程、性能优化、HOOK等等。
编辑于2022-11-16 12:05:02 上海React
核心概念
JSX
JSX 是什么
JSX 是一个 JavaScript 的语法扩展,或者说是一个类似于 XML 的 ECMAScript 语法扩展
React 本身并不强制使用 JSX。在没有 JSX 的时候,React 实现一个组件依赖于使用 React.createElement 函数。
JSX 有什么优势
代码变得更为简洁,而且代码结构层次更为清晰, 因为 React 需要将组件转化为虚拟 DOM 树,所以我们在编写代码时,实际上是在手写一棵结构树。而XML 在树结构的描述上天生具有可读性强的优势, Babel 插件将 JSX 语法的代码还原为 React.createElement 的代码。
设计思想, 关注点分离: 关注点分离在计算机科学中,是将代码分隔为不同部分的设计原则,是面向对象的程序设计的核心概念。其中每一部分会有各自的关注焦点。 关注点分离的价值在于简化程序的开发和维护。当关注点分开时,各部分可以重复使用,以及独立开发和更新。具有特殊价值的是能够稍后改进或修改一段代码,而无须知道其他部分的细节必须对这些部分进行相应的更改。
JSX 对比其他的方案
模版
React 团队认为引入模板是一种不佳的实现。 因为模板分离了技术栈,而非关注点的模板同时又引入了更多的概念。比如新的模板语法、模板指令等
模板字符串
代码结构变得更复杂了,而且开发工具的代码提示也会变得很困难
var box = jsx` <${Box}> ${ shouldShowAnswer(user) ? jsx`<${Answer} value=${false}>no</${Answer}>` : jsx` <${Box.Comment}> Text Content </${Box.Comment}> ` } </${Box}> `;
JXON
语法提示的问题等
React 16 依赖于 @babel/babel-preset-react-app
Bable 如何实现JSX 到JS 的编译
Babel 读取代码并解析,生成 AST,再将 AST 传入插件层进行转换,在转换时就可以将 JSX 的结构转换为 React.createElement 的函数
module.exports = function (babel) { var t = babel.types; return { name: "custom-jsx-plugin", visitor: { JSXElement(path) { var openingElement = path.node.openingElement; var tagName = openingElement.name.name; var args = []; args.push(t.stringLiteral(tagName)); var attribs = t.nullLiteral(); args.push(attribs); var reactIdentifier = t.identifier("React"); //object var createElementIdentifier = t.identifier("createElement"); var callee = t.memberExpression(reactIdentifier, createElementIdentifier) var callExpression = t.callExpression(callee, args); callExpression.arguments = callExpression.arguments.concat(path.node.children); path.replaceWith(callExpression, path.node); }, }, }; };
解析:该步骤将接收代码并生成AST抽象语法树,这期间会经历词法分析和语法分析两个阶段。 转换:经过解析步骤后,接收到AST树,经过一系列的添加、更改、删除操作,转换成我们需要的新的AST结构,这步也是最复杂的一步。 生成:拿到转换后的语法树后,再使用生成器生成最终的代码。
React 17 依赖于 react/jsx-runtime, 并不在需要 React.createElement
原因
必须import "React"
性能优化
Key 和 Ref 属性需要删除掉,在动态的传入{...props} 的时候,会比较麻烦
React.createElement 是一个动态属性,不能作为一个静态scope 中的常量,会消耗一些运行时的资源
default proops , 与创建组件时不是同步的,只有在执行的时候才会注入default props
https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md#detailed-design
生命周期
生命周期是组件从挂载,更新,到销毁的一个过程
挂载阶段
移除constructor
constructor 中并不推荐去处理初始化以外的逻辑;
本身 constructor 并不属于 React 的生命周期,它只是 Class 的初始化函数
通过移除 constructor,代码也会变得更为简洁
getDrivedStateFromProps
当 props 被传入时, 即父组件更新时也会触发
state 发生变化
forceUpdate 触发
UNSAFE_componentWillMount
Subtopic
render
建议不能更新状态,会导致死循环
建议不绑定事件,因为会重复注册
componentDidMount
浏览器端可以任务DOM 已渲染结束
react native 却不是这样
只能确认已挂载,但是并不是渲染完成 ?
更新阶段
UNSAFE_componentWillReceiveProps
被弃用,getDrivedStateFromProps 存在时,不会被调用
getDrivedStateFromProps
shouldComponentUpdate
函数中对 props 和 state 进行浅比较,用来判断是否触发更新
UNSAFE_componentWillUpdate
被废弃
render
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate 方法是配合 React 新的异步渲染的机制,在 DOM 更新发生前被调用,返回值将作为 componentDidUpdate 的第三个参数。
componentDidUpdate
卸载阶段
componentWillUnmount
解除事件绑定以及取消定时操作等
清除闭包?
错误边界
class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; } componentDidCatch(error, errorInfo) { // 你同样可以将错误日志上报给服务器 logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return <h1>Something went wrong.</h1>; } return this.props.children; } }
函数组件 / 类组件
相同点
使用方式和最终呈现效果上都是完全一致的。
不同点
设计思想
函数组件
函数组件的根基是 FP,也就是函数式编程。它属于“结构化编程”的一种,与数学函数思想类似。也就是假定输入与输出存在某种特定的映射关系,那么输入一定的情况下,输出必然是确定的
相较于类组件,函数组件更纯粹、简单、易测试。
类组件
类组件的根基是 OOP(面向对象编程),所以它有继承、有属性、有内部状态的管理。
组件更新
函数组件
函数组件任何情况下都会重新渲染,它并没有生命周期,但官方提供了一种方式优化手段,那就是 React.memo, React.memo 并不是阻断渲染,而是跳过渲染组件的操作并直接复用最近一次渲染的结果,这与 shouldComponentUpdate 是完全不同的
Subtopic
类组件
组件的更新
stata 变更
父组件props 传入时
React.pureComponent 实现 shouldComponentUpdate
生命周期
类组件
类组件通过生命周期包装业务逻辑,这是类组件所特有的,但是同样造成了业务逻辑掺杂在生命周期中的问题
函数组件
recompose , Hook
设计模式
类组件
继承
函数组件
组合
未来趋势
通过探索时间切片与并发模式,以及考虑性能的进一步优化与组件间更合理的代码拆分结构后,认为类组件的模式并不能很好地适应未来的趋势
原因
this 的模糊性
业务逻辑散落在生命周期中
React 的组件代码缺乏标准的拆分方式
而使用 Hooks 的函数组件可以提供比原先更细粒度的逻辑组织与复用,且能更好地适用于时间切片与并发模式。
组件设计
从组件是否关联外部状态纵向划分
无状态
有状态
组件容器
高阶组件
逻辑复用
渲染劫持
加载Loaing
function withLoading(WrappedComponent) { return class extends WrappedComponent { render() { if(this.props.isLoading) { return <Loading />; } else { return super.render(); } } }; }
从组件的结构化职责横向分
原子组件
基础组件
代理组件
基础组件的再封装
样式组件
布局组件
状态管理
state
当调用 setState 函数时,就会把当前的操作放入队列中。React 根据队列内容,合并 state 数据,完成后再逐一执行回调,根据结果更新虚拟 DOM,触发渲染
setState 并非真异步,只是看上去像异步。在源码中,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。
那么什么情况下 isBatchingUpdates 会为 true 呢?在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。 但在 React 无法控制的地方,比如原生事件,具体就是在 addEventListener 、setTimeout、setInterval 等事件中,就只能同步更新。
props
父子组件通信
组件的 props 通常是由父级组件的 state 驱动的,那 state 更新改为同步了,但你无法控制父级什么时候会去变更子组件的 props, 父级在各种场景都有可能会去发起更新
跨多层级通信
context, provider, 高阶函数
全局变量,发布订阅模式
状态管理库
响应式
Mobx
在 Mobx 5 之前,实现监听的方式是采用 Object.defineProperty; 而在 Mobx 5 以后采用了 Proxy 方案。
单向数据流
实现
Flux
Facebook 内部使用
redux
单一数据源,即整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 Store 中。 纯函数 Reducer,即为了描述 Action 如何改变状态树 ,编写的一个纯函数的 Reducer。 state 是只读的,唯一可以改变 state 的方法就是触发 Action,Action 是一个用于描述已发生事件的普通对象。
副作用
middleware
拦截分发的Action并添加额外的复杂行为,还可以添加副作用;
Redux-thunk
允许 Reducer 层直接处理副作用。
Redux Loop
redux 工程化解决方案
国外: rematch
国内: Dva
渲染过程
Virtual Dom
产生的背景
简化开发
按照现在流行的说法叫后端赋能,让后端开发人员能够快速交付页面。
避免跨站点脚本攻击
也就是常说的 XSS, Facebook 拥有庞大的站点,很容易因为一处暴露 XSS 而造成整体风险。XSS 不会直接攻击网页,而是通过嵌入 JavaScript 代码的方式,将恶意攻击附加到用户的请求中来攻击用户。它可以被用作窃取用户信息,或者恶意增删用户的一些资料。
它可以确保在你的应用中,永远不会注入那些并非自己明确编写的内容, 或者转成文本,并不执行, 通过转义来阻止
如果jsx是script标签,这里使用innerHTML来创建元素,避免脚本执行。
innerHTML会有XSS攻击风险,而textContent不会
出现 XSS 漏洞本质上是输入输出验证不充分
React 会持有一棵虚拟 DOM 树,在状态变更后,会触发虚拟 DOM 树的修改,再以此为基础修改真实 DOM。
基本原理: Store 存储了视图层所有的数据,当 Store 变化后会引起 View 层的更新。如果在视图层触发 Action,比如点击一个按钮,当前的页面数据值会发生变化。Action 会被 Dispatcher 进行统一的收发处理,传递给 Store 层。由于 Store 层已经注册过相关 Action 的处理逻辑,处理对应的内部状态变化后,会触发 View 层更新。
Diff 函数
去计算状态变更前后的虚拟 DOM 树差异
import React from 'react';
渲染函数
渲染整个虚拟 DOM 树或者处理差异点
import ReactDOM from 'react-dom';
优势
性能优越
如果大量的直接操作 DOM 则容易引起网页性能的下降,这时 React 基于虚拟 DOM 的 diff 处理与批处理操作,可以降低 DOM 的操作范围与频次,提升页面性能。在这样的场景下虚拟 DOM 就比较快,那什么场景下虚拟 DOM 慢呢?首次渲染或微量操作,虚拟 DOM 的渲染速度就会比真实 DOM 更慢。
规避 XSS
虚拟 DOM 内部确保了字符转义,所以确实可以做到这点,但 React 存在风险,因为 React 留有 dangerouslySetInnerHTML API 绕过转义。
跨平台
跨平台的成本更低。在 React Native 之后,前端社区从虚拟 DOM 中体会到了跨平台的无限前景,所以在后续的发展中,都借鉴了虚拟 DOM。比如:社区流行的小程序同构方案,在构建过程中会提供类似虚拟 DOM 的结构描述对象,来支撑多端转换。
缺点
内存占用较高
因为当前网页的虚拟 DOM 包含了真实 DOM 的完整信息,而且由于是 Object,其内存占用肯定会有所上升。
无法进行极致优化
虽然虚拟 DOM 足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中,虚拟 DOM 无法进行针对性的极致优化,比如实现类似 Google Earth 的场景
延伸
埋点统计与数据记录
手写Virtual DOm ?
diff 算法
Subtopic
生成补丁、更新差异的过程统称为 diff 算法
更新时机
更新发生在setState、Hooks 调用等操作以后
遍历算法
深度优先遍历
是从根节点出发,沿着左子树方向进行纵向遍历,直到找到叶子节点为止。然后回溯到前一个节点,进行右子树节点的遍历,直到遍历完所有可达节点。
实现?
传统的diff 算法的复杂度为 o(n^3), n 为节点的总数
广度优先遍历
则是从根节点出发,在横向遍历二叉树层段节点的基础上,纵向遍历二叉树的层次。
实现?
广度优先遍历可能会导致组件的生命周期时序错乱,而深度优先遍历算法就可以解决这个问题。
优化策略
分治的方式优化
树
忽略节点跨层级操作场景,提升比对效率
树比对的处理手法是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率。
组件
如果组件的 class 一致,则默认为相似的树结构,否则默认为不同的树结构
如果组件是同一类型则进行树比对
如果不是则直接放入补丁中
只要父组件类型不同,就会被重新渲染
元素
同一层级的子节点,可以通过标记 key 的方式进行列表对比
元素比对主要发生在同层级中,通过标记节点操作生成补丁。节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。
对比Vue
Vue 2.x 使用的是snabbdom, 是一个三方的 vitrual dom 的库,所以整体思路与 React 相同。但在元素对比时,如果新旧两个元素是同一个元素,且没有设置 key 时,snabbdom 在 diff 子元素中会一次性对比旧节点、新节点及它们的首尾元素四个节点,以及验证列表是否有变化
利用
根据 diff 算法的设计原则,应尽量避免跨层级节点移动。
通过设置唯一 key 进行优化,尽量减少组件层级深度。因为过深的层级会加深遍历深度,带来性能问题。
可以使用元素在数组中的下标作为 key。这个策略在元素不进行重新排序时比较合适,如果有顺序修改,diff 就会变慢
当基于下标的组件进行重新排序时,组件 state 可能会遇到一些问题。由于组件实例是基于它们的 key 来决定是否更新以及复用,如果 key 是一个下标,那么修改顺序时会修改当前的 key,导致非受控组件的 state(比如输入框)可能相互篡改,会出现无法预期的变动。
设置 shouldComponentUpdate 或者 React.pureComponet 减少 diff 次数。
Reconciler
协调
Reconciler 模块以 React 16 为分界线分为两个版本
Stack Reconciler是 React 15 及以前版本的渲染方案,其核心是以递归的方式逐级调度栈中子节点到父节点的渲染。
https://react.html.cn/docs/implementation-notes.html
Fiber Reconciler是 React 16 及以后版本的渲染方案,它的核心设计是增量渲染(incremental rendering),也就是将渲染工作分割为多个区块,并将其分散到多个帧中去执行。它的设计初衷是提高 React 在动画、画布及手势等场景下的性能表现
reconcilers 模块
它通过抽离公共函数与 diff 算法使声明式渲染、自定义组件、state、生命周期方法和 refs 等特性实现跨平台工作
性能优化
衡量标准
Google 的 Chrome 小组进一步提出了以用户为核心的 RAIL 模型,用更多的数字维度去阐释网页性能
响应:应在 50 毫秒内完成事件处理并反馈给用户; 动画:10 毫秒内生成一帧; 浏览器空闲时间:最大化利用浏览器空闲时间; 加载:在 5 秒内完成页面资源加载且使页面可交互。
工具
Chrome Dev Lighthouse
国际上比较老牌的就是 New Relic
国内 阿里云的 ARMS
采集指标
FCP(First Contentful Paint),首次绘制内容的耗时。首屏统计的方式一直在变,起初是通过记录 window.performance.timing 中的 domComplete 与 domLoading 的时间差来完成,但这并不具备交互意义,现在通常是记录初次加载并绘制内容的时间
TTI(Time to Interact),是页面可交互的时间。通常通过记录 window.performance.timing 中的 domInteractive 与 fetchStart 的时间差来完成
Page Load,页面完全加载时间。通常通过记录 window.performance.timing 中的 loadEventStart 与 fetchStart 的时间差来完成
FPS,前端页面帧率。通常是在主线程打点完成记录。其原理是 requestAnimationFrame 会在页面重绘前被调用,而 FPS 就是计算两次之间的时间差
let lastTime = performance.now() let frame = 0 let lastFameTime = performance.now() const loop = (time) => { const now = performance.now() lastFameTime = now frame++ if (now > 1000 + lastTime) { let fps = Math.round(frame / (( now - lastTime ) / 1000)) frame = 0 lastTime = now console.log(fps) } window.requestAnimationFrame(loop) }
静态资源及API 请求成功率。通常是通过 window.performance.getEntries( ) 来获取相关信息
排查
2C 的页面,那么 FCP、TTI、FPS、Page Load、静态资源及 API 请求成功率等几个指标都很重要,会直接影响关键业务的转化率
TP 50 , TP99 指的是用户百分比
管理后台,更关注的是使用起来功能是否完整,运行是否流畅,对加载速度并没有很高的要求,所以通常只对 FPS 、静态资源及 API 请求成功率这三个指标更为关注
实施
FCP
Loading
骨架屏
骨架屏图片
手写
非侵入式自动生成骨架屏代码
SSR
next.js
TTI
TTI 在实现上,可以优先加载让用户关注的内容,让用户先用起来。策略上主要是将异步加载与懒加载相结合
核心内容在 React 中同步加载
非核心内容采取异步加载的方式延迟加载
内容中的图片采用懒加载的方式避免占用网络资源
Page Load
页面完整加载时间同样可以通过异步加载的方式完成。异步加载主要由 Webpack 打包 common chunk 与异步组件的方式完成
FPS
FPS 主要代表了卡顿的情况,在 React 中引起卡顿的主要原因有长列表与重渲染
长列表的解决方案很成熟,直接使用 react-virtualized 或者 react-window 就可以
重复渲染
问题的症结
React 会构建并维护一套内部的虚拟 DOM 树,因为操作 DOM 相对操作 JavaScript 对象更慢,所以根据虚拟 DOM 树生成的差异更新真实 DOM。 那么每当一个组件的 props 或者 state 发生变更时,React 都会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM。当它们不相同时,React 会更新该 DOM。这个过程被称为协调。 协调的成本非常昂贵,如果一次性引发的重新渲染层级足够多、足够深,就会阻塞 UI 主线程的执行,造成卡顿,引起页面帧率下降
避免无效的重复渲染
分析工具
通过 Chrome 自带的 Performance 分析,主要用于查询 JavaScript 执行栈中的耗时,确认函数卡顿点,由于和重复渲染关联度不高,你可以自行查阅使用文档
通过 React Developer Tools 中的 Profiler 分析组件渲染次数、开始时间及耗时
解决方案
缓存
保证元数据在未改变时,渲染的数据不变的可行
不可变数据
最早的方案是使用ImmutableJS。如果我们无法将 props 或者 state 扁平化,存在多级嵌套且足够深,那么每次修改指定节点时,可能会导致其他节点被更新为新的引用,而ImmutableJS 可以保证修改操作返回一个新引用,并且只修改需要修改的节点
immerjs
手动控制
最后一种解决方案就是自己手动控制,通过使用 shouldComponentUpdate API 来处理
静态资源及 API 请求成功率
静态资源及 API 请求成功率的统计是非常有意义的。两者都有可能出现在用户的机器上失败,但在自己的电脑上毫无问题的情况。导致这个问题的原因千奇百怪
原因
你是直接从前端服务器拉取 JS 与 CSS 资源,还是从 CDN 拉取的? 解析 CDN 与 API 域名存在失败的情况。 运营商对静态资源及 API 请求做了篡改,导致请求失败。
解决
对于静态资源而言,能用 CDN 就用 CDN,可以大幅提升静态资源的成功率。 如果域名解析失败,就可以采取静态资源域名自动切换的方案;还有一个简单的方案是直接寻求 SRE 的协助。 如果有运营商对内容做了篡改,我推荐使用 HTTPS。
收益
技术必须服务于业务,否则就只是技术团队的自嗨
所以从技术角度讲收益,需要从业务实际效益出发。就像开篇所说的:“如果一个移动端页面加载时长超过 3 秒,用户就会放弃而离开。”那么将 TP999 从 5 秒优化到 3 秒以内,就可以得出具体的用户转化率数据
可分析性
预防
人工审查代码的方式,标准称谓是 Code Review。基于 React 写法的易错点,团队内部会总结出一些实践准则,Code Review 的重心放到代码的核心业务逻辑
https://gist.github.com/bigsergey/aef64f68c22b3107ccbc439025ebba12
工具审查的方式,标准称谓是静态代码检查工具。在 JavaScript 世界中,静态代码检查工具主要有 3 个,分别是JSLint、JSHint、ESLint。从生态发展的角度上,支持配置化与插件拓展 ESLint 获得了最终的胜利。基于 ESLint 有不少大厂给出了自己的最佳实践,最经典的规则方案莫过于 Airbnb 的 eslint-config-airbnb。这些规则方案将人工审查工作转化为工具自动化审查,节约了团队内部的时间
兜底
快速的线上定位报错
在发布过程中上传sourcemap 到报错收集平台上 Sentry
Mozila 开源工具
https://github.com/mozilla/source-map
可改变性
代码的可扩展能力
划分边界,模块解离
稳定性
业务分层,核心模块UT 覆盖
依从性
依从性讲的是约束, 统一编码规范与代码风格可以提升易读性,减少认知差异,防止不规范操作埋藏的潜在隐患
Lint 工具合集
针对 JavaScript 的 ESLint; 针对样式的 Stylelint; 针对代码提交的 Commitlint; 针对编辑器风格的 Editorconfig; 针对代码风格的 Prettie
Hook
组件的问题
组件之间难以复用状态逻辑
复查的组件难以理解
生命周期函数没能提供最佳的代码编程实践范式
例如,订阅与取消订阅并没有直接关联在一起,而是通过生命周期函数去使用,这非常的反模式,也就导致组件难以分解,且到处都是状态逻辑
使用限制
不要在循环、条件或嵌套函数中调用 Hook,即顶层作用域调用Hook
原因是因为Hook 的实现是基于链表的,在调用时按顺序加入,如果使用循环,条件活嵌套函数,可能导致取值错误,执行错误的hook。
原理?
在 React 的函数组件中调用 Hook
hook Api
useEffect 和 useLayoutEffect
相同点
函数签名相同 useEffect 先调用 mountEffect,再调用 mountEffectImpl, useLayoutEffect 会先调用 mountLayoutEffect,再调用 mountEffectImpl
useEffect 与 useLayoutEffect 两者都是用于处理副作用,这些副作用包括改变 DOM、设置订阅、操作定时器等。在函数组件内部操作副作用是不被允许的,所以需要使用这两个函数去处理,
底层的函数签名是完全一致的,都是调用的 mountEffectImpl,在使用上也没什么差异,基本可以直接替换,也都是用于处理副作用
差异
大多数场景下可以直接使用useEffect, 如果有直接操作 DOM 样式或者引起 DOM 样式更新的场景更推荐使用 useLayoutEffect
useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景,而 LayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 LayoutEffect 做计算量较大的耗时任务从而造成阻塞。
所有的 Hooks,也就是 useState、useEffect、useLayoutEffect 等,都是导入到了 Dispatcher 对象中。在调用 Hook 时,会通过 Dispatcher 调用对应的 Hook 函数。所有的 Hooks 会按顺序存入对应 Fiber 的状态队列中,这样 React 就能知道当前的 Hook 属于哪个 Fiber,这就是Hooks 链表
// useEffect 调用的底层函数 function mountEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null, ): void { if (__DEV__) { // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests if ('undefined' !== typeof jest) { warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber); } } return mountEffectImpl( UpdateEffect | PassiveEffect | PassiveStaticEffect, HookPassive, create, deps, ); } // useLayoutEffect 调用的底层函数 function mountLayoutEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null, ): void { return mountEffectImpl(UpdateEffect, HookLayout, create, deps); }
第一个参数和第二个参数是不一样的,其中 UpdateEffect、PassiveEffect、PassiveStaticEffect 就是 Fiber 的标记;HookPassive 和 HookLayout 就是当前 Effect 的标记
标记为 HookLayout 的 effect 会在所有的 DOM 变更之后同步调用,所以可以使用它来读取 DOM 布局并同步触发重渲染,但既然是同步,就有一个问题,计算量较大的耗时任务必然会造成阻塞,所以这就需要根据实际情况酌情考虑了
hook 的设计模式
道
hook 抽离状态逻辑的思想
术
React.Memo(sholuldComponentUpdate) 作为高阶函数无法像 React.useMemo 函数一样做到以来控制
由于函数组件每次渲染时都会重新执行,所以常量应该放置到函数外部去,避免每次都重新创建。而如果定义的常量是一个函数,且需要使用组件内部的变量做计算,那么一定要使用 useCallback 缓存函数
useRef: https://medium.com/frontend-digest/6-practical-applications-for-useref-2f5414f4ac68
势
组件内部的逻辑已经被自定义 Hook 完全抽出去了,类似外观模式
零碎
合成事件(事件代理,父组件给子组件代理)
V17 以前
React 给 document 挂上事件监听; DOM 事件触发后冒泡到 document; React 找到对应的组件,造出一个合成事件出来; 并按组件树模拟一遍事件冒泡。
V17 之后
事件委托不再挂在 document 上,而是挂在 DOM 容器上,也就是 ReactDom.Render 所调用的节点上