React的Fiber小记 ⏰

写在最前:绝对是错漏百出的一篇博文,很多内容还没有写demo去验证,特别是浏览器的帧渲染那一块,权当小记。至于为什么想写Fiber,问就是Shopee面试的时候被问惨了,之前实习上班摸鱼的时候看过稀土掘金上一篇感觉很牛逼的文章就在介绍Fiber,但是自己没仔细记录,大概只记得Fiber到底说了什么事,结果就是被人家前端主管问到说不出话。广东流程全部完蛋,失败总是贯穿人生始终😢。

此外,本文内容大量参考了掘金上《React进阶实践指南》中的《调和与Fiber》这篇文章,以及B站上一个讲的很好的老师的视频。


浏览器的帧渲染 & requestIdleCallback

先扯几个无关紧要的概念:

  • 并发:并发指的是在同一时间段内处理多个任务,但不一定是同时执行。它的核心概念是多个任务的交替进行,在某个时间段内,系统可以将多个任务放在一起管理,从而提高效率。例如,在单核 CPU 上,通过时间分片的方式,操作系统可以迅速切换任务,使得这些任务看起来像是同时在进行。尽管在任意时刻只会有一个任务在运行,但由于快速切换,用户可能感觉到多个任务同时在进行
  • 并行:并行指的是同时执行多个任务,通常需要多核或多处理器系统支持。在这种情况下,系统可以真正地同时处理多个任务。例如,在一个双核处理器的系统中,两个任务可以真正地同时被不同的核心处理,这样可以显著加快整体的处理速度。
  • 进程:进程是计算机中正在执行的程序的实例。它是系统分配资源(如内存、文件句柄和 CPU 时间)的基本单位。每个进程都有自己独立的地址空间和资源,进程之间的资源是不共享的。操作系统为每个进程分配所需的资源,包括内存、文件描述符等。操作系统需要保存和恢复额外的上下文信息(如进程状态、内存映射等),因此切换会较为耗时。进程之间的通信相对复杂,通常使用进程间通信(IPC)机制,如管道、消息队列、共享内存和套接字等。
  • 线程:线程是进程的一个执行单元。一个进程可以包含多个线程,这些线程共享进程的资源,如内存和文件句柄。线程在创建和切换时比进程更轻,因为它们共享地址空间和其他资源。同一进程内的线程可以直接访问进程的内存空间和资源,这使得线程之间的通信更高效。上下文切换:线程切换的上下文开销较小,因为不需要切换内存映射等较重的状态。线程之间的通信更为简单,可以直接操作共享内存,但也需要考虑线程安全问题。

上面的东西就当我在放屁,现在开始讲前端。
在大多数现代浏览器中,浏览器会为每一个Tab、插件和其他后台功能分配独立的进程。而每一个Tab里面都有多个线程,不同的线程负责的工作内容各不相同。
但是,浏览器给每个Tab进程分配的,用来处理JS和浏览器内容绘制的主线程只有一个。所以我们常说“JS是单线程的,但是浏览器是多线程的”这甚至可以解释为什么JS要被设计成单线程负责处理浏览器的用户界面、DOM 操作、事件处理和JS代码执行。所有的渲染和布局工作通常在主线程中执行,而且它是唯一可以直接访问 DOM 的线程
除了主线程,常见的还有工作线程渲染线程。工作线程是用来处理异步任务的,比如WebWorker、图像解码、AJAX等,当主线程在解析你的HTML文件等时候,如果碰到如script、link、img之类的标签想要下载其他资源、此时网络请求的发起就是靠的工作线程。至于渲染线程的存在是否与上面所提到的“主线程负责浏览器内容的绘制”是否矛盾,肯定是不矛盾的,因为太耗时的JS代码确实会让页面一直得不到更新,这说明主线程被JS阻塞,浏览器无法执行后序的绘制功能。此处的“渲染线程”应该与GPU相关,即我们常提到的为了优化动画效果而启用的“硬件加速”,或者说“合成层”。AI给出的答案也类似。

接着要说的就是浏览器在每一帧渲染之前都干了些什么事。接下来要说的绝对是这篇文章里最胡说八道的东西,有刻意迎合requestIdleCallback的感觉,但是是我目前能得到的最合理的理解。以后发现错了我再回来修改。

现代浏览器的刷新频率是多为60Hz,即每秒60帧,但实际上,浏览器根据应用运行情况给每一帧“预设”的生命周期是动态的调整的,不一定是16ms(1000ms / 60)。 为什么说是“预设”? 这与requestIdleCallback这个API有关,他的用法如下:

requestIdleCallback((deadline) => {console.log("在浏览器‘空闲’的时候,这个回调会被执行")console.log("当前帧剩下的时间为:", deadline.timeRemaining())
}, { timeout: 1000 })
// 如果超过1s还是没有空闲时间,强制调用回调函数

我们给requestIdleCallback传递的回调函数中的deadline是一个对象,deadline上的timeRemaining被调用时可以获取当前帧剩下了多少时间,这就是我认为为什么每一帧的生命周期都是事先被动态的“预设”好的

接下来分享一下我对“阻塞”的看法:我始终认为,在每一帧中,浏览器执行的先后顺序是:

  1. 清空微任务
  2. 执行一个宏任务
  3. requestAnimationFrame
  4. 计算样式 & 布局更新 & 重绘重排

这样的一个线性过程 (虽然肯定有错,没这么简单)。在这一帧内如果上面这个过程执行的耗时没有超过这一帧被预设的“生命周期”,那浏览器就认为主线程是“空闲”的,有时间来执行requestIdleCallback里面的回调。但如果我的JS代码(宏任务+微任务)过分耗时,主线程会一直被阻塞,重绘和重排就一直无法进行,给用户的感觉就是卡顿和掉帧。


Fiber是什么东西

然后分享一下Fiber,这玩意到底有什么用?它是React16引入的新的协调算法,目的是提高React的性能和用户体验。Fiber通过实现更细粒度的任务划分和调度来解决React在更新和渲染过程中的一些性能问题。
对比一下React15的苦日子或许更好理解。在Fiber出现之前,React对VDOM Tree的扫描是从Root节点开始深度遍历,找出需要更新的节点。JS在浏览器里的递归是很可怕的,递归在满足退出条件之前一直无法中断,这意味着主线程可能被JS的执行持续阻塞,无法走到视图更新那一步。随着应用越来越复杂,React这种从Root开始深度遍历的做法会越来越耗时,带来影响就是“卡”。

这里引出一个问题:为什么Vue不需要Fiber?Vue使用双向数据绑定,这意味着模型与视图之间是紧密关联的。任何对模型的更改都会立即反映在视图中,反之亦然。这个特性使得Vue能够以更简单的方式管理视图的更新,不需要复杂的分片更新机制。说白了就是Vue的视图更新的细粒度比React细腻得多,React似乎总是避免不了从某个RootFiber开始深度遍历的命运,但是Vue在组件挂载的时候,通过发布订阅的方法就实现了模型与视图之间的双向数据绑定,当模型被更改,那个整个DOM Tree里哪个DOM应该更新,他捕获得更准确。

所以React顶不住了,搞了个Fiber,这到底是什么东西?从数据结构上看,一个Fiber就是一个VDOM。再加上一些额外的信息。不过在React的更新中确实如此:每个Fiber都是VDOM转化来的。而从功能上看,每个Fiber都是一个小的任务片段,是对整个VDOM Tree更新任务的一种拆分。

既然每个Fiber都是VDOM转化来的,那有VDOM Tree,自然也有Fiber Tree。Fiber Tree的更新可以大致分为下面两个阶段:render(调和)和commit(提交)。值得注意的是,我们常说的diff就是render里面很重要的阶段,或者说白了render就是diff。render的产物是EffectList(副作用链,单向链表),commit的责任就是拿着render在一定的时间内产出的EffectList去执行真实DOM的更新。整颗Fiber Tree完整的render是在多个时间碎片内被完成的,而每完成一次局部的render都会执行一次commit。


Fiber的render

先准备一个demo,这是整篇文章要用到的唯一的例子:

const Component = () => {return (<div id="A_1">Hello World!<div id="B_1"><div id="C_1"></div><div id="C_2"></div></div><div id="B_2"></div></div>)
}

上面提到过,有VDOM Tree就会有Fiber Tree,而在Fiber之前,对VDOM Tree的遍历是深度遍历,准确地说是类似于二叉树的先序遍历,但是每个VDOM的节点可以大于两个,所以VDOM Tree的“先序遍历”应该说是 “根,左到右”。而Fiber Tree的深度遍历顺序是与之前的VDOM Tree一样的。假设我们先给每个Fiber如下的数据结构:

type Fiber = {return: Fiber, // 当前Fiber的父Fiberchild: Fiber, // 当前Fiber的第一个子Fibersibling: Fiber, // 当前Fiber的下一个兄弟Fibertag: TAG_TEXT | TAG_HOST | TAG_ROOT, // 当前Fiber类型:文本 | 原生 | 根stateNode:null | HTMLElement, // 当前Fiber对应的真实DOMalternate: Fiber, // 在上一次更新中当前Fiber的“副本”,用来做新旧两次更新中的diff的对比props: {children: VDOM[], // 与当前Fiber对应的VDOM的children},nextEffect: Fiber, // 指向下一个产生副作用的FiberfirstEffect: Fiber, // 指向第一个产生副作用的FiberlastEffect: Fiber, // 指向最后一个产生副作用的Fiber
}

然后我们写一个函数performUnitWork,它处理当前Fiber,并返回下一个待处理的Fiber:

function performUnitOfWork(currentFiber) {// 处理当前FiberbeginWork(currentFiber);// 然后寻找下一个待处理的Fiber// 如果当前的Fiber有子Fiber,子Fiber就是下一个处理的Fiberif (currentFiber.child) {return currentFiber.child;}// 当前Fiber没有子Fiber,则认为当前Fiber已经处理完毕completeUnitOfWork(currentFiber);// 继续寻找:优先找当前Fiber的兄弟Fiber// 如果没有则找他的父Fiber的下一个兄弟Fiberwhile (currentFiber) {// 如果当前Fiber有兄弟Fiber,则接下来处理它的兄弟Fiberif (currentFiber.sibling) {return currentFiber.sibling;}// 如果当前Fiber已经没有兄弟Fiber,则他的父Fiber被视为已经处理完毕currentFiber = currentFiber.return;completeUnitOfWork(currentFiber);}
}

我们不纠结于上面算法的实现过程,只要知道它实现了一种树的“先序遍历”即可 (用数据结构那门课教的二叉树的根左右递归来写也很容易,但我懒)。需要注意的是,如果在寻找下一个要处理的Fiber的过程中,我们发现当前Fiber已经没有子Fiber,那我们就认为当前Fiber已处理完毕,执行相应操作。考虑我们上面的demo,那我们Fiber Tree的处理流程如下:A1—Hello World—B1—C1—C2—B2。

然后我们实现我们的beginWork,用来处理当前待操作的Fiber:

function beginWork(currentFiber) {switch (currentFiber.tag) {// 根Fiber: 根据当前Fiber创建子Fiber树case TAG_ROOT: {reconcileChildren(currentFiber);break;}// 文本Fiber: 如果当前文本Fiber的真实DOM还不存在,则创建真实DOMcase TAG_TEXT: {!currentFiber.stateNode && (currentFiber.stateNode = createDOM(currentFiber))break;}// 原生DOM节点Fiber: 如果当前Fiber的真实DOM还不存在,则创建真实DOM && 创建子Fiber树case TAG_HOST: {!currentFiber.stateNode && (currentFiber.stateNode = createDOM(currentFiber))reconcileChildren(currentFiber);break;}default:break;}
}

接下来实现reconcileChildren,这是重头戏,他根据传入的Fiber,来把与他对应VDOM的所有子VDOM都转化成Fiber,并把第一个子Fiber与当前Fiber连接。而且diff就发生在这个阶段。diff把处于相同层级,拥有相同标签类型和相同key的Fiber认为“同一节点”,以复用。但在这个例子中我们忽略key,这玩意加进来少说都得多写一百行代码 (主要是太难了我不会)。而且注意我们对reconcileChildren的描述:“把当前Fiber对应VDOM的所有子VDOM都转化成Fiber”,reconcileChildren处理的只是“儿子”这一层,涉及不到“孙子”这一层,这就是对diff中 “相同层级” 的一个说明。

function reconcileChildren(currentFiber) {const newChildren = currentFiber.props.children;let newChildIndex = 0; // 新子Fiber的索引let oldFiber = currentFiber.alternate && currentFiber.alternate.child; // 旧Fiber的第一个子Fiberlet prevSibling = null; // 前一个兄弟Fiberwhile (newChildIndex < newChildren.length || oldFiber) {let newChild = newChildren[newChildIndex]; // 新的VDOMlet newFiber = null;const sameType = oldFiber && newChild && oldFiber.type === newChild.type; // 类型相同let tag = undefined; // 节点类型if (newChild && newChild.type === ELEMENT_TEXT) {/// <div>Hello, world!</div>里面的Hello, world!tag = TAG_TEXT;} else if (newChild && typeof newChild.type === 'string') {// 原生DOM节点tag = TAG_HOST;}// 执行的是更新的操作,tag、type、stateNode是可以复用的if (sameType) {newFiber = {...oldFiber,props: newChild.props,return: currentFiber,alternate: oldFiber,effectTag: UPDATE,nextEffect: null}} else if (!sameType && newChild) {newFiber = {tag,type: newChild.type, // 节点类型: div, span...props: newChild.props,stateNode: null, // 页面内对应的真实DOMreturn: currentFiber, // 父FibereffectTag: PLACEMENT, // 副作用类型: 新增节点nextEffect: null, // 指向下一个副作用, effectList是一个单链表}} else if (!sameType && oldFiber) {oldFiber.effectTag = DELETION;deletions.push(oldFiber); // React全局会维护一个deletions数组,用来记录需要删除的Fiber}if (oldFiber) { oldFiber = oldFiber.sibling }if (newFiber) {if (newChildIndex === 0) {// 第一个子节点currentFiber.child = newFiber;} else {prevSibling.prevSibling = newFiber;}prevSibling = newFiber;}newChildIndex++;}
}

reconcileChildren的关键就是那个while循环,他迭代的是一条链表。始终记住:reconcileChildren处理的只是“儿子”这一层

然后在render中最后一个需要实现的函数就是completeUnitOfWork,它在performUnitWork(处理当前Fiber并返回下一个待处理的Fiber)中被调用。在performUnitWork中我们规定:“当发现当前Fiber已经没有子Fiber,那我们就认为该Fiber已处理完毕”,需要执行这个Fiber副作用收集。这里的“副作用”是什么东西呢?简单理解就是:当前Fiber处理完后,浏览器在之后的渲染中要做出的有关于这个Fiber的更新。 上面提到过每个Fiber都对应一个VDOM,所以每个Fiber都可能产出自己的Effect,在一个Fiber被处理完后我们要收集他的Effect,以构建EffectList。说到底,EffectList就是render的目标,commit就是拿着EffectList,从表头到表尾,更新真实DOM。

但是有一个很有趣的地方值得注意:Fiber遍历的顺序和EffectList构建的顺序是不一样的。Fiber的遍历是类似于先序遍历的深度优先,他先处理的一直是根节点然后才是子节点,这很好理解:想一下我们平时用React开发的时候,组件的生命周期总是父组件先开始,然后才是子组件。但是EffectList构建的顺序却类似于后序遍历,他是从叶子结点开始的,是一种 “左到右,根” 的顺序。以上面的demo为例,EffectList的构建顺序如下:Hello World—C1—C2—B1—B2—A1。所以我们的completeUnitOfWork就是为了实现一个“后序遍历”的算法,实现的过程不必细究,甚至用数据结构教的算法更好懂。

function completeUnitOfWork(currentFiber) {const returnFiber = currentFiber.return;if (returnFiber) {if (!returnFiber.firstEffect) {returnFiber.firstEffect = currentFiber.firstEffect}if (currentFiber.lastEffect) {if (returnFiber.lastEffect) {returnFiber.lastEffect.nextEffect = currentFiber.firstEffect}returnFiber.firstEffect = currentFiber.lastEffect}if (currentFiber.effectTag) {if (returnFiber.lastEffect) {returnFiber.lastEffect.nextEffect = currentFiber} else {returnFiber.firstEffect = currentFiber}returnFiber.firstEffect = currentFiber}}
}

上面这些就是React的render中一些比较关键的思想。总结起来就是:整个Fiber Tree的render的目的就是收集Effect,但完整的render是在多个时间碎片内完成的。如何分片我们在commit说。


Fiber的commit

如何解决Fiber Tree的遍历带来的阻塞问题才是核心。直接上代码:

let nextUnidOfWork = currentFiber; // 下一个要处理的Fiber
function workLoop(deadline) {// 是否把控制权归还给主线程的渲染let shouldYield = false; while (nextUnitOfWork && !shouldYield) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);// 或许当前帧渲染剩下的空余时间shouldYield = deadline.timeRemaining() < 1;}if (!nextUnitOfWork) {console.log('render is done')commitRoot();// 在有限的时间内,能处理多少个fiber就处理多少个,反正我要提交了// 我要把控制权归还给主线程的渲染,让它继续渲染下一帧,直到渲染结束,这个过程都控制权都一直在浏览器那里,Js代码无法执行}requestIdleCallback(workLoop, { timeout: 1000 });
}
requestIdleCallback(workLoop, { timeout: 1000 });

上面的代码就很直观了,我们在浏览器每一帧的空闲时间内,能处理多少个Fiber就处理多少个Fiber,而且我们全局维护下一个需要处理的Fiter,保证我们的遍历不用从头开始。commitRoot的作用也很直观,拿着当前不完整的EffectList去更新真实DOM。


Fiber的其他功能

  • 任务优先级 (这个应该是每个Effect节点上的额外属性。高优先级的任务,如处理用户输入,动画等,会在EffectList里排靠前的位置;内存清理、日志记录等低优先级的任务靠后。)
  • 错误边界 (没了解,哈哈。好困五点半了我要抽根烟睡觉了。记录完Fiber,Shopee真的痛的一批啊🤯)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/62733.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JavaScript 前端开发:从入门到精通的奇幻之旅

目录 一、引言 二、JavaScript 基础 &#xff08;一&#xff09;变量与数据类型 &#xff08;二&#xff09;运算符 &#xff08;三&#xff09;控制结构 三、函数 &#xff08;一&#xff09;函数定义与调用 &#xff08;二&#xff09;函数作用域 &#xff08;三&am…

安装Fcitx5输入框架和输入法自动部署脚本(来自Mark24)-Ubuntu通用

在Ubuntu22.04上安装rime中文输入法的基本教程 上述文章接近废弃。 使用新逻辑配置基本的Fcitx5的输入法。 安装 第一步&#xff0c;下载相关组件 sudo nala install vim sudo nala install ruby sudo nala install fcitx5-rime第二步&#xff0c;设置语言为Fcitx5 而非 默认…

【FAQ】使用Node.js 镜像 构建本地项目

在nodejs官方并没有提供使用node.js构建本地项目的方法&#xff0c;但是通过阅读官方文档&#xff0c;可以发现&#xff0c;官方在包管理器界面提供了如下语句 所以node.js容器是可以执行语句的 下面通过docker 的 -w 、-v 参数设置容器工作目录和目录映射&#xff08;实现本…

C/C++语言基础--C++智能指针(unique_ptr、shared_ptr、week_ptr)

本专栏目的 更新C/C的基础语法&#xff0c;包括C的一些新特性 前言 在C、C语言中&#xff0c;最经典的特性就是指针&#xff0c;他和内存相关&#xff0c;但是我们常常申请内存后忘记释放而导致内存泄漏&#xff0c;C提供了智能指针去解决这个内存泄漏问题&#xff1b;C语言…

Nature Methods | 人工智能在生物与医学研究中的应用

Nature Methods | 人工智能在生物与医学研究中的应用 生物研究中的深度学习 随着人工智能&#xff08;AI&#xff09;技术的迅速发展&#xff0c;尤其是深度学习和大规模预训练模型的出现&#xff0c;AI在生物学研究中的应用正在经历一场革命。从基因组学、单细胞组学到癌症生…

队列-链式描述(C++)

定义 使用链表描述队列时&#xff0c;通常包含以下几个基本要素&#xff1a; 队头指针&#xff08;Front Pointer&#xff09;&#xff1a;指向队列中第一个&#xff08;即最早进入队列的&#xff09;元素的节点。队尾指针&#xff08;Rear Pointer&#xff09;&#xff1a;指…

Flutter 之 InheritedWidget

InheritedWidget 是 Flutter 框架中的一个重要类&#xff0c;用于在 Widget 树中共享数据。它是 Flutter 中数据传递和状态管理的基础之一。通过 InheritedWidget&#xff0c;你可以让子 Widget 在不需要显式传递数据的情况下&#xff0c;访问祖先 Widget 中的数据。这种机制对…

Python 深度学习框架介绍

Python 是深度学习领域的主流编程语言&#xff0c;拥有许多强大的深度学习框架&#xff0c;广泛用于学术研究、工业应用和生产环境中。以下是一些最流行的 Python 深度学习框架&#xff0c;它们各自具有独特的功能和特点&#xff1a; 1. TensorFlow 开发公司&#xff1a;Google…

MySQL技巧之跨服务器数据查询:进阶篇-从A服务器的MySQ数据库复制到B服务器的SQL Server数据库的表中

MySQL技巧之跨服务器数据查询&#xff1a;进阶篇-从A服务器的MySQ数据库复制到B服务器的SQL Server数据库的表中 基础篇已经描述&#xff1a;借用微软的SQL Server ODBC 即可实现MySQL跨服务器间的数据查询。 而且还介绍了如何获得一个在MS SQL Server 可以连接指定实例的MyS…

Flutter 指纹识别

在这篇博客中&#xff0c;我们将介绍如何使用 Flutter 的 local_auth 插件在 Android 和 iOS 设备上实现指纹识别功能。通过这一步一步的实现&#xff0c;我们将学习如何检查设备是否支持生物识别、如何触发指纹验证&#xff0c;并处理可能出现的错误。 效果图&#xff08;因为…

CentOS 9 配置静态IP

文章目录 1_问题原因2_nmcli 配置静态IP3_使用配置文件固定IP4_重启后存在的问题5_nmcli 补充 1_问题原因 CentOS 7 于 2014年6月发布&#xff0c;基于 RHEL 7&#xff0c;并在 2024年6月30日 结束维护。 CentOS 9 作为目前的最新版本&#xff0c;今天闲来闲来无事下载下来后…

数据结构(三)——双向链表的介绍以及实现

前言 前面两期数据结构的文章我们介绍了顺序表和单向链表&#xff0c;那么本篇博文我们将来了解双向链表&#xff0c;作为最好用的一种链表&#xff0c;双向链表有什么特殊之处呢&#xff0c;接下来就让我们一起了解一下吧。 下面是前两篇数据结构的文章&#xff1a; 数据结…

Oracle--表空间Tablespace

在 Oracle 数据库中&#xff0c;表空间&#xff08;Tablespace&#xff09; 是一种逻辑存储结构&#xff0c;用于组织和管理数据库中物理存储数据文件的方式。以下是表空间相关操作的详细介绍&#xff0c;包括创建、修改、删除、查询以及常见问题处理。 1. 表空间的作用 提供逻…

cmake一些常用指令

cmake常用的一些命令 推荐网址&#xff1a;CMake 保姆级教程&#xff08;上&#xff09; | 爱编程的大丙 cmake_minimum_required(VERSION 3.0) project(CALC) # 增加-stdc11 set(CMAKE_CXX_STANDARD 11) # 指定输出的路径 set(HOME ${CMAKE_CURRENT_SOURCE_DIR}) # 可执行文…

面阵相机的使用和注意事项

引言 面阵相机&#xff08;Area Scan Camera&#xff09;是一种广泛应用于工业视觉、医学成像、安防监控以及科研领域的图像采集设备。与线扫相机不同&#xff0c;面阵相机的传感器包含多行像素&#xff08;例如1280x1024、1920x1080等&#xff09;&#xff0c;能够在一个曝光…

损失函数分类

1. NLLLoss&#xff08;负对数似然损失&#xff09; 定义&#xff1a; 直接对预测的概率 p(yi) 的负对数求平均。通常配合 Softmax 使用&#xff0c;输入为对数概率。 优点&#xff1a; 对离散分类问题效果良好。更灵活&#xff0c;用户可以自行计算 Softmax。 缺点&#x…

python冒号是什么意思

例如&#xff1a; user: User User.objects.filter(iddata.get(uid)).first() 变量名后面的冒号是&#xff1a;类型注解&#xff0c;3.6以后加入的&#xff0c;冒号右边是类型&#xff0c;仅仅是注释&#xff0c;有些鸡肋。 变量注释的语法&#xff1a;注释变量类型,明确指出…

ESLint v9.0.0 新纪元:探索 eslint.config.js 的奥秘 (4)

从 v9.0.0 开始&#xff0c;官方推荐的配置文件格式是 eslint.config.js&#xff0c;并且支持 ESM 模块化风格&#xff0c;可以通过 export default 来导出配置内容。 // eslint.config.js export default [{rules: {semi: "error","prefer-const": "…

【Vue3】【Naive UI】< a >标签

【Vue3】【Naive UI】< a >标签 超链接及相关属性其他属性 【VUE3】【Naive UI】&#xff1c;NCard&#xff1e; 标签 【VUE3】【Naive UI】&#xff1c;n-button&#xff1e; 标签 【VUE3】【Naive UI】&#xff1c;a&#xff1e; 标签 <a> 标签HTML中的一个锚&…

打字指法和键盘按键功能简介

打字指法和键盘按键功能简介 一、打字指法简介&#xff08;附视频演示&#xff09; 基本要领和练习步骤&#xff1a; 手指位置&#xff1a;正常情况下&#xff0c;大拇指放在空格键上&#xff0c;其余四个手指分别放在 ASDF 和 JKL; 键上。 打字姿势&#xff1a;打字时手指…