首页
Search
1
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
71 阅读
2
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
52 阅读
3
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
42 阅读
4
如何在 Clash for Windows 上配置服务
40 阅读
5
如何在 IOS Shadowrocket 上配置服务
40 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,061
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
1061
篇与
的结果
2024-10-20
React18 源码解析之 hooks 的挂载
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 在之前讲解函数 beginWork() 时,稍微说了下 renderWithHooks() 的流程,不过当时只说了中间的Component(props)的执行部分,并没有讲解函数组件中的 hooks 是如何挂载的,这里我们详细讲解下。 1. hooks 的简单样例 # 我们先来看段 hooks 实际应用的代码:import { useEffect, useState } from 'react'; function App() { const [count, setCount] = useState(0); useEffect(() => { const timer = setInterval(() => { setCount(count => count + 1); }, 1000); return () => clearInterval(timer); }, []); useEffect(() => { console.log(`count is ${count}`); }, [count]); return {count}; } 这里用到了 useState, useEffect 两个 hook,同时则 useEffect()中,还涉及到了依赖项的对比更新,和回调 return 的处理。我们可以看到 App() 中使用的两个 hook 是从 React 导出来的。 2. hooks 的导出 # react 源码对应的位置是 packages/react/index.js,从这里寻找后发现,所有的 hooks 都是从 packages/react/src/ReactHooks.js 中导出来的。所有的 hooks 里都会执行一个 resolveDispatcher() 方法,如 useState()这个 hook:export function useState(initialState: (() => S) | S): [S, Dispatch] { const dispatcher = resolveDispatcher(); return dispatcher.useState(initialState); } 可见实际上执行的是 dispatcher.useState(),这里面会通过执行 resolveDispatcher() 得到一个 dispatcher,然后调用该对象上的 useState() 方法。我们来看下 resolveDispatcher() 的执行流程:import ReactCurrentDispatcher from './ReactCurrentDispatcher'; function resolveDispatcher() { /** * 在执行element转fiber节点的过程中,FunctionComponent会执行 renderWithHooks(), * renderWithHooks() 内部会判断 current 来决定是用 mount,还是update, * 共用变量 ReactCurrentDispatcher 的位置: packages/react/src/ReactSharedInternals.js */ const dispatcher = ReactCurrentDispatcher.current; // Will result in a null access error if accessed outside render phase. We // intentionally don't throw our own error because this is in a hot path. // Also helps ensure this is inlined. return ((dispatcher: any): Dispatcher); } 对,就是直接从 ReactCurrentDispatcher.current 中取出数据,给到 dispatcher。那么这上面的数据是什么呢,又是在哪里挂载上数据的呢? 3. renderWithHooks # 我们在之前讲解如何把 jsx 转为 fiber 节点中讲到过 renderWithHooks(),但没有讲 hooks 是如何运作的。在 renderWithHooks() 中有一段代码:function renderWithHooks() { currentlyRenderingFiber = workInProgress; // 将当前函数组件对应的fiber节点给到 currentlyRenderingFiber 变量 // 根据是否是初始化挂载,来决定是初始化hook,还是更新hook // 将初始化或更新hook的方法给到 ReactCurrentDispatcher.current 上, // 稍后函数组件拿到的hooks,都是从 ReactCurrentDispatcher.current 中拿到的 // 共用变量 ReactCurrentDispatcher 的位置: packages/react/src/ReactSharedInternals.js ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate; // 执行函数 let children = Component(props, secondArg); } 我们知道 React 中维护着两棵树,若 current 节点或 current.memoizedState 为空,说明现在没有这个 fiber 节点,或者该节点之前没有对应的 hooks,那么我们就调用 mount 方式来初始 hooks,否则就调用 update 方式来更新 hooks。mount 阶段的 hooks 仅仅是用来进行 hooks 节点的生成,然后形成链表挂载在函数的 fiber 节点上。update 阶段,则相对来说稍微复杂一些,可能会有触发函数二次执行渲染的可能。我们在函数组件中使用的 useState(), useEffect()等,仅仅是先挂了一个名字,具体比如是执行 mountState(),还是 updateState()?是在 renderWithHooks()的函数逻辑里,到执行Component()之前,才会判断的。具体源码位置:ReactFiberHooks.old.js#L446。上面第 2 节函数 resolveDispatcher() 使用的 ReactCurrentDispatcher 和当前 renderWithHooks()里的 ReactCurrentDispatcher ,引用的是同一个对象,因此在这里挂载数据后,在第 2 节中就可以直接读取出来。HooksDispatcherOnMount 和 HooksDispatcherOnUpdate 两个的区别在于: HooksDispatcherOnMount:这里面所有的 hooks 都是用来进行初始化的,即一边执行,一边将这些 hooks 添加到单向链表中; HooksDispatcherOnUpdate:顺着刚才的单向链表按顺序来执行; 4. hooks 的挂载 # 我们这里不讲每个具体的 hook 的使用方式和内部原理,主要是来说下这些 hooks 放在哪儿,是以一种怎样的方式存储的。在 packages/react-reconciler/src/ReactFiberHooks.old.js 中,观察下诸如 mountState(), mountEffect(), mountRef() 等几个 mount 阶段的 hooks,都会先调用 mountWorkInProgressHook() 来得到一个 hook 节点。如:returns {[*, Dispatch]} */ function mountState( initialState: (() => S) | S, ): [S, Dispatch] { const hook = mountWorkInProgressHook(); /** * 忽略中间的代码 **/ return [hook.memoizedState, dispatch]; } function mountEffectImpl(fiberFlags, hookFlags, create, deps): void { const hook = mountWorkInProgressHook(); /** * 忽略后续的代码 **/ } function mountRef(initialValue: T): {|current: T|} { // 创建一个hook,并将其放到hook链表中 const hook = mountWorkInProgressHook(); /** * 忽略后续的代码 **/ } 接下来看看 mountWorkInProgressHook() 函数中都做了啥。function mountWorkInProgressHook(): Hook { // 创建一个hook节点 const hook: Hook = { memoizedState: null, baseState: null, baseQueue: null, queue: null, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list // 若这是链表的第一个hook节点,则使用 currentlyRenderingFiber.memoizedState 指针指向到该hook // currentlyRenderingFiber 是在 renderWithHooks() 中赋值的,是当前函数组件对应的fiber节点 currentlyRenderingFiber.memoizedState = workInProgressHook = hook; } else { // Append to the end of the list // 若这不是链表的第一个节点,则放到列表的最后即可 workInProgressHook = workInProgressHook.next = hook; } // 返回这个hook节点 return workInProgressHook; } workInProgressHook 指针永远指向到链表的最后一个 hook 节点,若 workInProgressHook 为 null,说明该链表上还没有节点。mountWorkInProgressHook()是为 currentlyRenderingFiber 指向的 fiber 节点构建 hooks 的链表。currentlyRenderingFiber 就是 workInProgress 指向的那个 fiber 节点,这里是在 renderWithHooks() 中进行赋值的。 若 workInProgressHook 为 null,说明链表为空,则 currentlyRenderingFiber.memoizedState 指向到该节点; 若不为空,说明链表上已经有节点了,直接放到该链表的后面,并让 workInProgressHook 重新指向到最后的那个节点; 每调用一次创建 hook 的函数,不论是什么 hook,只要是这个函数组件里的,都会将其添加到 hooks 的链表中。即一个函数组件所有的 hooks 节点会形成链表,并存放在currentlyRenderingFiber.memoizedState上,下次使用时,可以从该属性中获取链表的头指针。我们也注意到这里有两个 memoizedState 属性,但这两个属性所在的对象是不一样的。一个是 fiber 节点上的,一个是 hook 节点上的。如我们在第 1 节中的样例,React 会把 1 个 useState(), 2 个 useEffect(),一共三个 hooks,按照顺序形成 hooks 链表。我在之前学习到这个位置时,当时稍微有个小疑问,一个函数组件里有多个 hooks,而像 useState()这种 hook,又会多次执行诸如 setState()的操作,那这些操作放在哪里呢?是新形成了一个 hook 节点吗?还是怎样?这里到时候当我们了解 useState()这个 hook 时,就会明白了。这里简单说下,只有真正的 hook 才会放到链表上,而某个 hook 的具体操作,如多次执行 setState(),则会放到 hook.queue 的属性上。 5. hooks 的更新 # 我们在初始节点已经把所有的 hooks 都挂载在链表中了,那更新时,hooks 是怎么更新的呢?在更新阶段,所有的 hooks 都会进入到 update 阶段,比如 useState()内部会执行 updateState(),useEffect()内部会执行 updateEffect()等。二这些 hooks 的 update 阶段执行的函数里,都会执行函数 updateWorkInProgressHook()。updateWorkInProgressHook()函数的作用,就是从 hooks 的链表中获取到当前位置,上次渲染后和本次将要渲染的两个 hook 节点: currentHook: current 树中的那个 hook;即当前正在使用的那个 hook; workInProgressHook: workInProgress 树中的那个 hook,即将要执行的 hook; 为什么会需要两个 hook 呢,因为很多 hook 都有依赖项,拿到前后两个 hook 后,可以通过对比依赖项是否发生了变化,再来决定这个 hook 是否继续执行,是否需要进行重新的刷新。 5.1 updateWorkInProgressHook 源码 # 我们来看下 updateWorkInProgressHook()函数的源码:function updateWorkInProgressHook(): Hook { // This function is used both for updates and for re-renders triggered by a // render phase update. It assumes there is either a current hook we can // clone, or a work-in-progress hook from a previous render pass that we can // use as a base. When we reach the end of the base list, we must switch to // the dispatcher used for mounts. // 机翻:此函数用于更新和由渲染阶段更新触发的重新渲染。它假设有一个可以克隆的当前钩子, // 或者一个可以用作基础的上一个渲染过程中的正在进行的钩子。当我们到达基本列表的末尾时, // 我们必须切换到用于装载的调度程序。 let nextCurrentHook: null | Hook; /** * 获取current树的下一个需要执行的hook * 1. 若当前没有正在执行的hook; * 2. 若当前有执行的hook,则获取其下一个hook即可; */ if (currentHook === null) { const current = currentlyRenderingFiber.alternate; // workInProgress对应的current节点 if (current !== null) { /** * 若current节点不为空,则从current获取到hooks的链表 * 注:hooks链表存储在memoizedState属性中 */ nextCurrentHook = current.memoizedState; } else { nextCurrentHook = null; } } else { /** * 因为当前的 updateWorkInProgressHook() 会多次执行,当第一次执行时,就已经获取到了hooks的头指针, * 这里只需要通过next指针就可以获取到下一个hook节点 */ nextCurrentHook = currentHook.next; } /** * workInProgressHook: 当前正在执行的hook; * nextWorkInProgressHook: 下一个将要执行的hook; * * 若 workInProgressHook 为空,则使用头指针,否则使用其next指向的hook, * 不过这两种方式得到的 nextWorkInProgressHook 有可能为空 **/ let nextWorkInProgressHook: null | Hook; if (workInProgressHook === null) { nextWorkInProgressHook = currentlyRenderingFiber.memoizedState; } else { nextWorkInProgressHook = workInProgressHook.next; } /** * 若 nextWorkInProgressHook 不为空,直接使用; * 若为空,则从对应的current fiber节点的hook里,克隆一份; **/ if (nextWorkInProgressHook !== null) { // There's already a work-in-progress. Reuse it. /** * 若下一个hook节点不为空,则将 workInProgressHook 指向到该节点 */ workInProgressHook = nextWorkInProgressHook; nextWorkInProgressHook = workInProgressHook.next; currentHook = nextCurrentHook; // currentHook指针同步向下移动 } else { // Clone from the current hook. // https://github.com/wenzi0github/react/issues/1 if (nextCurrentHook === null) { throw new Error('Rendered more hooks than during the previous render.'); } currentHook = nextCurrentHook; // currentHook指针向下一个移动 const newHook: Hook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, baseQueue: currentHook.baseQueue, queue: currentHook.queue, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list. currentlyRenderingFiber.memoizedState = workInProgressHook = newHook; } else { // Append to the end of the list. workInProgressHook = workInProgressHook.next = newHook; } } return workInProgressHook; } 这个函数稍微有点长,但几句话总结该函数的流转: 初始时,在 renderWithHooks()中,将 workInProgress.memoizedState 设置为空,相当于 currentlyRenderingFiber.memoizedState 设置为 null; 若 workInProgress 树中的 fiber 节点的下一个 hook 存在,则直接使用,否则就从对应的 current 的 fiber 节点克隆过来,然后把这些 hook 构建出新的链表放到 currentlyRenderingFiber.memoizedState 上,方便下次更新时使用; 对应的 current fiber 节点的里 hook 也同步向后移动,因此每次得到的都是两个 hook:currentHook 和 workInProgressHook; 6. 几个问题的汇总 # 通过观察上面的源码,我们也能理解下面的几个问题了。 6.1 如何得到下一个 hook # 这里有个逻辑上的判断: 若 workInProgressHook 为空,说明这个函数组件刚开始执行,之前还没有 hook 要执行,因此从 currentlyRenderingFiber.memoizedState 中获取一个 hook 节点; 若 workInProgressHook 不为空,说明已经按照链表顺序执行了几个 hooks 了,那么这里直接通过 workInProgressHook.next 获取下一个将要执行的 hook; 但 nextWorkInProgressHook 有可能为空,也有可能不为空。目前暂时还不清楚什么情况下不为空,个人猜测是在执行 hooks 的过程中,又产生了新的更新,所以导致所有的 hooks 重新执行。重新执行时,截止到目前,所有的 hook 节点都是存在的。刚开始 nextWorkInProgressHook 指向的是上次执行的 hook 的节点,然后再接着获取 next 指向的下一个节点,就是当前我们要使用的节点。 6.2 hook 为什么只能在函数组件顶层进行声明 # 这些 hooks 的调用顺序,就是他们挂载在链表上的顺序。在二次渲染时,也会按照挂载的顺序来执行,那么再次执行 hook 的顺序就是第一次挂载节点的顺序是一样的。这就正好说明了一个问题:hook 为什么只能在函数组件顶层进行声明。因为每个 hook 都是按照顺序,依次从链表中获取的。React 本身是不知道你函数组件内部逻辑的,假如放到了 if 判断、循环、或者函数中,每次的渲染,都可能会因为不同的执行逻辑,导致某些 hook 不执行,进而导致 hook 的错乱。
2024年10月20日
4 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之 useCallback 和 useMemo
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 React 中有两个 hooks 可以用来缓存函数和变量,提高性能,减少资源浪费。那什么时候会用到 useCallback 和 useMemo 呢? 1. 使用场景 # useCallback 和 useMemo 的使用场景稍微有点不一样,我们分开来说明。 1.1 useCallback # 我们知道 useCallback 可以缓存函数体,在依赖项没有变化时,前后两次渲染时,使用的函数体是一样的。很多同学觉得用 useCallback 包括函数,可以减少函数创建的开销 和 gc。但实际上,现在 js 在执行一些闭包或其他内联函数时,运行非常快,性能非常快。若不恰当的使用 useCallback 包括一些函数,可能还会适得其反,因为 React 内部还需要创建额外的空间来缓存这些函数体,并且还要监听依赖项的变化。如下面的这种方式其实就很没有必要:function App() { // 没必要 const handleClick = useCallback(() => { console.log(Date.now()); }, []); return click me; } 那 useCallback 在什么场景下会用到呢? 函数作为其他 hook 的依赖项时(如在 useEffect()中); 函数作为 React.memo()(或 shouldComponentUpdate )中的组件的 props; 我们来一一看下。 1.1.1 作为其他 hook 的依赖项 # 我们经常会有请求数据的场景,然后在 useEffect() 中触发:function App() { const [state, setState] = useState(); const requestData = async () => { // fetch setState(); }; useEffect(() => { requestData(); }, []); } 我们在官方脚手架 create-react-app 写这段代码时,他就会给出提示,大致意思是 useEffect 使用了外部的变量,需要将其添加到依赖中,即:useEffect(() => { requestData(); }, [requestData]); // 将 requestData 添加到依赖项中 如若只是把它添加到依赖项中,再执行代码时,会发现代码陷入了无限循环。这是因为函数 requestData() 在每次 render()时,都是重新定义的,导致依赖项发生了变化,就会执行里面的 requestData(),进而触发 setState()进行下次的渲染,陷入无限循环。为什么明明是同一个函数体,两个变量却不一样呢?比如下面的这个例子:const funcA = () => { console.log('www.xiabingbao.com'); }; const funcB = () => { console.log('www.xiabingbao.com'); }; console.log(funcA === funcB); // false 他们的函数体仅仅是看起来是一样的,但实际上是完全独立的两个个体。上面的 requestData()同理,每次都是重新声明一个新的,跟之前的函数肯定就不一样了。这个时候,我们就需要把 requestData()用useCallback()包裹起来:const requestData = useCallback(async () => { // fetch setState(); }, []); 这就能保证函数 requestData()在多次渲染过程中是一致的(除非依赖项发生变化)。 1.1.2 作为 React.memo()等组件的 props; # 有一些我们是需要向子组件传入回调函数的场景,比如 onClick, onSuccess, onClose 等。function Count({ onClick }) { const [count, setCount] = useState(count); const handleClick = () => { const nextCount = count + 1; setCount(nextCount); onClick(nextCount); }; console.log('Count render', Date.now()); return click me; } function App() { const [now, setNow] = useState(0); const handleClick = count => { console.log('App count', count); }; return ( setNow(Date.now())}>set new time ); } 函数组件 中的 handleClick 传给了子组件 ,当父级组件触发更新时,子组件也会执行,只不过 state 没有变化而已。那么如何避免子组件必须要的刷新呢?这里我们就需要用到 React.memo 了(注意,这里不是 useMemo())。 React.memo()可接受 2 个参数,第一个参数为纯函数的组件;第二个参数是 compare(prevProps, nextProps)函数(可选),用于自行实现功能,对比 props ,控制是否刷新。 我们用React.memo()包裹住函数组件后,只需要保证传入的 props 不发生变化,那么函数组件就不会二次执行。const MemoCount = React.memo(); 那传入的各种 callback 就得用useCallback()来封装了,如上面的 handleClick:const handleClick = useCallback(count => { console.log('App count', count); }, []); 1.2 useMemo # useMemo() 与 useCallback() 的功能很像,只不过 useMemo 用来缓存函数执行的结果,而 useCallback()用来缓存函数体。 1.2.1 useMemo 的使用 # 如每次渲染时都要执行一段很复杂的运算,或者一个变量需要依赖另一个变量的运算结果,就都可以使用useMemo()。比如有一个计算百分比的场景:用户可以在某个项目中,捐赠自己的虚拟金币,不过项目接收的虚拟金币有上限,然后实时显示该项目的受捐进度。同时,进度展示这里,还有几个其他的规则: 进度的百分比的数字显示整数,向下取整; 只要有捐助行为,则百分比至少为 1%; 进度不能超过 100%(最后一次的捐赠可能会超过上限); 在某个组件获取进度百分比的时候,我们这里可以封装到useMemo()中,因为进度的百分比只跟当前进度和总上限有关系。const curPercent = useMemo(() => { if (progress === 0 || topLimit === 0) { return 0; } const percent = (progress * 100) / topLimit; if (percent = 100) { return 100; } return Math.floor(percent); }, [progress, topLimit]); 若当前进度和总上限没有变化时,则不用重新计算百分比。 1.2.2 其他变体 # 其实我们可以看到,useMemo()类似于 useEffect() 和 useState() 的组合体:const [curPercent, setCurPercent] = useState(0); useEffect(() => { if (progress === 0 || topLimit === 0) { setCurPercent(0); return; } const percent = (progress * 100) / topLimit; if (percent = 100) { setCurPercent(100); return; } setCurPercent(Math.floor(percent)); }, [progress, topLimit]); 相应地,若遇到上面需要用 useEffect 和 useState 实现的场景,就可以直接用useMemo()来实现。而且,useCallback()也是可以用 useMemo()来实现的。因为 useMemo()返回的是函数执行的结果,那我们返回的结果就是一个函数不就行了。const handleClick = useMemo(() => { // 返回一个函数 return () => { console.log(Date.now()); }; }, []); hanleClick(); 2. 源码 # 我们了解了 useCallback() 和 useMemo() 的基本用法之后,再来了解下他们源码的实现。我们在之前 renderWithHooks 的章节中也了解到,所有的 hooks 在内部实现时,都区分了 mount 阶段和 update 阶段,useCallback()和 useMemo() 两个 hooks 也不例外。 2.1 useCallback 的源码 # useCallback()在 React 内部实现时,分成了 mountCallback()和 updateCallback()。 mountCallback: 生成 hook 节点,并存储回调函数 callback 和依赖项 deps; updateCallback: 新的依赖项与之前存储的依赖项进行对比,若没有变化,则直接返回,否则存储新的回调函数和依赖项; 2.1.1 mountCallback # 初始化时很简单,就是把传入的 callback 和依赖项 deps 存储起来。/** * useCallback的创建 * @param callback * @param deps * @returns {T} */ function mountCallback(callback: T, deps: Array | void | null): T { const hook = mountWorkInProgressHook(); // 创建一个新的hook节点 const nextDeps = deps === undefined ? null : deps; hook.memoizedState = [callback, nextDeps]; // 直接将callback和依赖项进行存储 return callback; } 可以看到,这里用数组的方式,把 callback 和依赖项存储到了 hook 节点的 memoizedState 属性上,然后返回这个 callback。因此我们执行 useCallback()的返回值就是这个传入 callback。 2.1.2 updateCallback # updateCallback 的实现相对来说,也比较简单,关键点就在于依赖项的对比。/** * useCallback的更新 * @param callback * @param deps * @returns {T|*} */ function updateCallback(callback: T, deps: Array | void | null): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; // 取出上次存储的数据: [callback, prevDeps] // 若之前的数据不为空 if (prevState !== null) { if (nextDeps !== null) { /** * 若依赖项不为空,且前后两个依赖项没有发生变化时, * 则直接返回之前的callback(prevState[0]); * 有个 areHookInputsEqual() 我们先不关心细节,只需要知道是用来对比依赖项的 */ const prevDeps: Array | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { // 若依赖项没有变化,则返回之前存储的callback return prevState[0]; } } } /** * 若依赖项为空,或者依赖项发生了变动,则重新存储callback和依赖项 * 然后返回最新的callback */ hook.memoizedState = [callback, nextDeps]; return callback; } 若前后两个依赖项都不为空,且依赖项没有发生变动,则直接返回之前存储的 callback,达到了缓存的目的。若依赖项为空,或者依赖项发生了变化,则重新存储 callback 和依赖项,然后返回最新的 callback。因此,若不设置依赖项,或者依赖项一直在变,则无法达到缓存的目的。这里有个工具函数 areHookInputsEqual(),该函数的作用,就是用来对比前后两个依赖项中所有的数据是否发生了变化,只要有一项的数据发生了变化(相同位置前后的两个数据不相等),则认为依赖项产生了变动。 2.2 useMemo 的源码 # useMemo()的实现,与 useCallback 很相似,只不过在 useMemo()中,执行了 callback,然后缓存的是其返回的结果。useMemo()在 React 内部实现时,分成了 mountMemo()和 updateMemo()。 mountMemo: 生成 hook 节点,并存储回调函数 callback 执行的结果和依赖项 deps; updateMemo: 新的依赖项与之前存储的依赖项进行对比,若没有变化,则直接返回,否则存储新的回调函数的执行结果和依赖项; 2.2.1 mountMemo # 初始节点源码的实现:/** * useMemo的创建 * @param nextCreate * @param deps 依赖项 * @returns {T} */ function mountMemo(nextCreate: () => T, deps: Array | void | null): T { const hook = mountWorkInProgressHook(); // 在链表的末尾创建一个hook节点 const nextDeps = deps === undefined ? null : deps; /** * 计算useMemo里callback的返回值 * 这是与 useCallback() 不同的地方,这里会执行回调函数callback */ const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; // 将返回值和依赖项进行存储 return nextValue; // 返回执行callback()的返回值 } 我们从源码中可以看到,在 mountMemo()里,会执行回调函数 callback(),然后存储该函数的返回结果。 2.2.2 updateMemo # 在了解 updateCallback()的源码后,updaeMemo()的源码也很好理解。/** * useMemo的更新 * @param nextCreate * @param deps * @returns {T|*} */ function updateMemo(nextCreate: () => T, deps: Array | void | null): T { const hook = updateWorkInProgressHook(); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState; if (prevState !== null) { // Assume these are defined. If they're not, areHookInputsEqual will warn. if (nextDeps !== null) { const prevDeps: Array | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { // 若依赖项没有变化,则返回之前存储的结果 return prevState[0]; } } } // 重新计算callback的返回结果,并进行存储 const nextValue = nextCreate(); hook.memoizedState = [nextValue, nextDeps]; return nextValue; } 当依赖项不为空,且没有变化时,直接返回之前存储的数据;否则执行最新的回调函数,然后存储该函数最新的返回结果,并返回。 3. 总结 # 这是 React 源码内部实现起来比较简单的 hooks,我们先做个开胃菜,后续比如 useState(), useEffect() 等 hooks,整体的逻辑会更加复杂一些。参考链接: Are Hooks slow because of creating functions in render? https://segmentfault.com/a/1190000022651514 https://zhuanlan.zhihu.com/p/56975681
2024年10月20日
6 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之 hook 的依赖项更新机制
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 我们在之前讲解 useCallback()和 useMemo()中,稍微说了下 areHookInputsEqual() 的功能。这篇文章我们来详细讲解下。在如 useEffect(), useMemo(), useCallback() 等 hooks 中,第 2 个参数是依赖项,那么这些 hooks 是如何根据依赖项进行更新的呢。 1. 使用场景 # 这几个 hooks 在 update 更新阶段里,几乎都有两个判断的逻辑: 判断新的依赖项 nextDeps 是否为 null,若为 null 直接跳过,执行后续的更新逻辑; 若新的依赖项 nextDeps 不为 null,则与之前的依赖项 prevDeps 里的每项比较,看是否产生了变化,若依赖项没有变化,则使用缓存的数据,若任意一项产生了变化,则执行后续的更新逻辑; 如下面的这段代码:const nextDeps = deps === undefined ? null : deps; if (nextDeps !== null) { // if (areHookInputsEqual(nextDeps, prevDeps)) { // 若依赖项没有变化,则返回之前得到的结果 return prevState[0]; } } // update 由此可见,若没有设置依赖项,或设置的依赖项为 null,则该 hook 每次渲染时都会执行;若依赖项任何一项都没有变化,使用上一次渲染的结果。那么 areHookInputsEqual() 是如何进行对比的? 2. 源码 # 我们来看下去掉调试代码之后的代码,结构比较简单,容易理解。源码地址: ReactFiberHooks.old.js#L326/** * 比较两个依赖项中的每一项是否有变化 * 任意一项产生了变化,则返回 alse,表示两个依赖项不相等 * 若全部都一样,没有变化,则返回 true * @param nextDeps 新的依赖项 * @param prevDeps 之前旧的依赖项 * @returns {boolean} */ function areHookInputsEqual(nextDeps: Array, prevDeps: Array | null) { // 删除测试环境下的警告代码 // 若 prevDeps 为n ull,或 prevDeps.length 与 nextDeps.length不 相等时,会产生警告 /** * 比较 prevDeps 和 nextDeps 的每一项, * 这里的 is 是 Object.is 的代称,并进行的 polyfill */ for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { if (is(nextDeps[i], prevDeps[i])) { // 若两个元素相同,则继续比较 continue; } // 若相同位置的两个数据不一样,说明依赖项产生了变化,直接返回false return false; } // 所有的依赖项都相等,返回true return true; } 这里为什么用 Object.is()来进行对比,而不是双等号或者三等号呢?我们来看Object.is()与==和===有什么不同之处。React 源码中 Object.is 地址:objectIs.jsObject.is 的官方 MDN 地址: Object.is Object.is() 与 == 不同。==无法区分 falsly 值(假值),即如空字符串、false,数字 0,undefined, null 等,均会判定为 true,而 Object.is 则不会强制转换两边的值。 Object.is() 与 === 也不相同。差别是它们对待有符号的零和 NaN 不同,例如,=== 运算符(也包括 == 运算符)将数字 -0 和 +0 视为相等,而将 Number.NaN 与 NaN 视为不相等。 areHookInputsEqual() 是用来比较 hook 的依赖项是否产生了变化,若任意一项变了,则返回 false,hook 会重新执行;若所有的依赖项都一样,则返回 true,则 hook 还使用之前缓存的数据。为了使比较的结果更加准确,这里选择了使用Object.is()。我们再看下调试代码里说了什么:function areHookInputsEqual(nextDeps: Array, prevDeps: Array | null) { if (__DEV__) { if (ignorePreviousDependencies) { // Only true when this component is being hot reloaded. // 在 renderWithHooks() 中: // Used for hot reloading: // ignorePreviousDependencies = current !== null && current.type !== workInProgress.type; // 若current的fiber节点与workInProgress的fiber节点不一样,则将ignorePreviousDependencies设置为true // 表示需要忽略之前的依赖项 // 然后这里直接返回false,表示前后的依赖项不相同 return false; } } // 能执行到这里,说明nextDeps不为空(若为空时就已经直接执行了) // 但若prevDeps为空,则给出警告, // 当前hook 在此渲染期间收到了最后一个参数,但在前一次渲染期间没有收到。 即使最后一个参数是可选的,它的类型也不能在渲染之间改变。 if (prevDeps === null) { if (__DEV__) { console.error( '%s received a final argument during this render, but not during ' + 'the previous render. Even though the final argument is optional, ' + 'its type cannot change between renders.', currentHookNameInDev, ); } return false; } if (__DEV__) { // Don't bother comparing lengths in prod because these arrays should be // passed inline. // 若nextDeps和prevDeps都不为空,但两者的数组长度不一样,则给出警告 if (nextDeps.length !== prevDeps.length) { console.error( 'The final argument passed to %s changed size between renders. The ' + 'order and size of this array must remain constant.\n\n' + 'Previous: %s\n' + 'Incoming: %s', currentHookNameInDev, `[${prevDeps.join(', ')}]`, `[${nextDeps.join(', ')}]`, ); } } // 上面的校验都通过后,则开始比较每一项是否发生了变化 // for-Object.is } 3. 总结 # 所有有依赖项的 hooks,在对比前后依赖项是否发生变动时,都是用 areHookInputsEqual() 来进行对比的。有很多同学会把 json 结构或者 object 类型的变量放到依赖项中,这就会存在一个问题,每次在进行依赖项对比时,两个 object 类型的变量都是不相等的,不管他们之间的 key 或者 value 是否发生变化,每次都会执行 hook 中的回调函数。因此,为了避免这种情况,我们不建议直接把 object 类型的变量放到依赖项中。若是依赖 key 都是已知的,这里建议是把每个 key 都拆分出来,分别放到依赖项中;若 key 是不明确的,或者动态变化的,可以先对 key 进行字典排序,然后再进行依赖项设置。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之 useRef
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 useRef()这个 hook,可以用来存储任何类型的数据。注意,我们这里讲的是 useRef(),他是一个 hook,不是 React 组件上的ref属性。 1. 它的用法 # 我们先来了解下 useRef() 这个 hook 的简单用法。function App() { const domRef = useRef(null); // 存储dom元素 const startMovePointRef = useRef({ x: -1, y: -1 }); // 在移动场景中,存储开始移动时的坐标 // 按下鼠标时,记录下坐标 const handleMouseDown = event => { startMovePointRef.current = { x: event.clientX, y: event.clientY, }; }; return ; } function useInterval(callback, delay) { const callbackRef = useRef(); useEffect(() => { callbackRef.current = callback; }); } 从上面的几个例子中可以看到,useRef()中可以用来存储任何类型的数据,比如 dom 元素,object 类型,回调函数等。甚至连new Map()也可以存储。用 useState() 这个 hook 也能起到存储数据的效果呀,这两个 hook 有什么区别呢? 2. useRef()的特性 # 这个 hook 的主要特点有: 可以存储任何类型的数据; 存储的数据,在组件的整个生命周期内都有效,而且只在生命周期内有效,组件被销毁后,存储的数据也就被销毁了; 内容被修改时,不会引起组件的重新渲染; 内容被修改,是会立即生效的; 内容的读写操作,都是在 current 属性上操作的,没有额外的 get, set 等方法; 知道 useRef() 这个 hook 的几个特点后,我们再对比下 useState() 和 全局变量的区别。 useRef() useState() 全局变量 存储的数据类型 全部 全部 全部 数据的生命周期 当前所在组件的生命周期 当前所在组件的生命周期 当前页面的生命周期 组件被多次引用时 每个数据都是独立的 每个数据都是独立的 共享该数据 是否引起组件重新渲染 否 是 否 是否立即生效 立即生效 下次渲染时生效 立即生效 因此,若要存储的一些数据,没必要渲染到视图中的数据,可以存储到useRef()中。比如上面样例中的回调函数 callback,DOM 元素,一些坐标数据等等。 3. 源码 # 我们在文章React18 源码解析之 hooks 的挂载中也知道,所有的 hooks 的使用,氛围初始创建和更新两个阶段。 3.1 初始创建阶段 # useRef() 内部的实现比较简单,我们直接看源码:function mountRef(initialValue: T): {| current: T |} { // 创建一个hook,并将其放到hook链表中 const hook = mountWorkInProgressHook(); // 存储数据,并返回这个数据 const ref = { current: initialValue }; hook.memoizedState = ref; return ref; } 可以看到,不管什么类型的数据,都是放在 object 类型中的 current 属性上,然后存储到 hook 节点的 memoizedState 中。这个 hook 并不会引起其他的行为(如组件的二次渲染等),只是单纯的存储数据。 3.2 更新阶段 # 源码:function updateRef(initialValue: T): {| current: T |} { const hook = updateWorkInProgressHook(); return hook.memoizedState; } 更新阶段的源码也很简单,直接返回 hook 节点上 memoizedState 属性的内容。综合上面初始创建和更新两个阶段的源码,我们也知道,想要在useRef()上存储或使用数据时,都是在.current属性上操作。 4. 总结 # 了解完 useRef() 的源码后,我们再回头看他的特性时,就能更好地理解了。 可以存储任何类型的数据; 存储的数据,在组件的整个生命周期内都有效,而且只在生命周期内有效,组件被销毁后,存储的数据也就被销毁了; 内容被修改时,不会引起组件的重新渲染; 内容被修改,是会立即生效的; 内容的读写操作,都是在 current 属性上操作的,没有额外的 get, set 等方法; 在官方上,有段关于 useRef() 的介绍,这里摘抄一下: 它(useRef())创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: ...} 对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现
2024年10月20日
4 阅读
0 评论
0 点赞
2024-10-20
建立以企业 IM 为中心的沟通协作模式
现在还有不少的中小企业,因为各种各样的原因,还会使用微信、QQ 等偏个人的沟通工具,来进行工作上的交流。但微信、QQ 等毕竟是属于个人的账号,在工作中,进行沟通协作时,还是有很多的不便。我建议各个公司,如果有条件的话,一定要更换成企业级的沟通协作软件,比如企业微信、钉钉、飞书等。因为这些企业 IM 不只是单纯的聊天沟通,他更是一个完整的协作平台。有的老板会觉得,在微信上沟通不是挺好的吗?而且好多同事关系都在微信上了,迁移会很麻烦。但越是有这种想法,在后期替换的成本也就越高。因为随着团队的壮大和公司内各个基础服务的完善,若不及时迁移的话,会有更多的人员和服务被绑定在微信上。我建议使用企业协作沟通软件,而不是微信、QQ 等个人交流软件,主要也是基于以下几点的考虑。 1. 人员管理方便且更注重隐私 # 在企业 IM 中,所有的成员都是在后台统一管理的,人员的信息、架构、权限等,管理起来更加方便。在通讯录中,公司所有人员的信息,都是实名制的,理论上都是公开透明的,可以看到每个人真实的姓名、头像和所属部门。在多人协作沟通上会方便很多。比如我想找某个同学沟通相关工作,我不必一定有某个人的微信才行,我只需要去通讯录中找到该同学即可。甚至于,我们都不一定找到具体的某个人,定位到该部门,也能解决不少问题。而在微信上,很多同学的头像昵称比较生活化,比如有的用动漫人物做头像,有的用动物,有的用孩子照片做头像;而昵称上也是多种多样。在成员的确认上会增加一些负担。而且,微信、QQ 等是属于个人的私人账号,可能也会因各种原因产生被封禁的风险。若该账号被封禁后,那他在工作交流中,就会存在很多不便之处。要么得等到解封,要么就得重新注册一个新的账号。 2. 各平台更加聚焦 # 企业 IM,他是一个完整的协作平台,将各个平台进行打通。我们可以把多平台集成进去,至少可以放置一个该平台的入口。即所有的工作都在这个企业 IM 上就可以完成。即实现工作的沉浸式体验,不必在各个系统之间来回切换。我们使用微信沟通,就意味着无法将公司内的各个平台聚焦起来,无法形成一个比较好的聚合平台,比较分散。想要做什么工作,还得专门去寻找这个平台。其实可以把飞书等这类企业 IM 理解为在云端构建了一个工作平台,把所有的工作场景都整合在一个平台上,最大程度降低「切换成本」,把「沉浸式」的爽感做出来了。 3. 消息订阅方便 # 我们在工作中,需要很多的平台进行辅助,当这个平台上有数据发生变动时,需要及时地通知到相关人员,比如服务器的告警、错误率的告警、禅道平台上的需求变动等等。各种的企业 IM 均提供了很多接入通知订阅的接口,方便不同平台功能的接入。提升了各个平台之间的协作效率。但微信在接入通知上就很不方便。 4. 信息更加安全 # 在微信、QQ 等相对个人的通讯交流软件中,很多信息都是因为办公和生活没分开,导致信息发错、或者是使用文件传输助手等,造成信息上的泄露传播。而使用企业 IM,则所有的信息都是保留在企业端,不存在发送到公司外部的情况。同时,即使有人恶意截图传播,也可以通过各种明水印、暗水印等技术手段进行追踪,方便查找责任人。避免了公司内信息的泄露和传播。目前也有很多国企、央企等企业,转为使用企业 IM,如央视、国家最高检察院、中国石油、中国银行等。这也证明了这些第三方企业 IM 的隐私保护能力,是能够得到这些国央企的认可的。 5. 企业账号的体系 # 当有新成员加入到当前企业后,就会自动拥有该企业的账号,公司内所有的内部系统,都以企业账号进行关联,可以把知识沉淀到公司内,而不用随着个人的账号进行流转。同时,当他离职,离开这个企业时,他任何所加入的群聊,会自动全部退出,司内的任何系统和资料,他也都无法继续登录使用。避免了隐私问题,和公司资产的流失。我们以企业微信为例,在企业微信中办公的企业,在使用如腾讯文档相关的产品时,默认是以该员工的企业账号登录的,并且该账号创建的文档,默认是对该公司内所有的企业账号开放权限的,而且只有该公司的企业账号才有权限。而是用个人账号创建的文档,如果权限放的太大,就可能存在泄露的风险,让外部的人看到;若权限管的太死,在人员变动时,又得手动增减人员,很麻烦。现在几乎所有的企业 IM 都提供了员工认证的 API,然后可以基于这些 api 实现满足自己公司的独立的认证系统。其他的内网网站都可以跳转到这个认证系统上进行认证,通过后,再调回刚才的网站。避免了各种私人信息的出现。我在文章里并没有刻意地去宣传哪个企业 IM 更好,大家都要去安装使用那个。我举的企业微信的例子,也是我更了解他,其他的软件,我也至少浅尝辄止。最后,还是回到前面的一段话:我建议各个公司,如果有条件的话,一定要更换成企业级的沟通协作软件,比如企业微信、钉钉、飞书等。因为这些企业 IM 不只是单纯的聊天沟通,他更是一个完整的协作平台。
2024年10月20日
4 阅读
0 评论
0 点赞
1
...
53
54
55
...
213