首页
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 源码解析之 lanes 模型中的位运算
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 在 lanes 模型中,我们可以通过各种的位运算,实现不同 lane 的组合,这里我们来了解下 lanes 模型中涉及到的几种。 1. 位运算的基本使用 # 位运算主要包含了按位与、按位或、异或、取反、左移、右移等操作。 1.1 左移的操作 # 在 lanes 模型,有很多对 1 的左移操作,如:const lane = 1
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之 fiber 等数据结构
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 我们稍微了解下 React 中的几个结构。 我们这里仅了解其中的转换过程,后续我们再了解两棵 fiber 树是如何进行对比的。 1. jsx 结构 # 我们在 React 中写的类似于 html 的结构就被称为 JSX,但他并不是 html,而是一个 JavaScript 的语法扩展。即他是 js,而不是 html。官方文档: JSX 简介 深入 JSX const App = () => { const handleClick = () => { console.log('click'); }; return ( hello world ); }; 不过这里我们不深入 jsx 的使用方式,主要说下 JSX 的作用。jsx 是 js 的语法糖,方便我们开发者的维护。最后实际上会被 React(React16.x 及之前)或 babel 编译(React17.0 及更新)成用 createElement 编译的结构。一开始我写 jsx 时也不太习惯,觉得把逻辑和模板混合到一起太乱了,还是 Vue 中的模板+逻辑+样式的组合更好。后来写多了以后,发现 jsx 其实也挺香的,比如它没有额外语法糖的记忆,各种语法跟 js 本身就很像;同时,因为 typescript 给开的后门,jsx 对 ts 的支持程度很高。而且 React 中并不是用文件来分割组件的,我们可以在一个文件里,编写多个组件。同样的,我们在 React 中像下面这样写的效果是一样的:createElement('div', { onClick: handleClick }, createElement('p', null, 'hello world')); 但这种方式使用起来确实不方便。 2. element 结构 # 上面提到会将 jsx 编译成由 createElement()函数组成的一个嵌套结果。那么 createElement 里具体都干了什么呢?在 React16 及之前,createElement()方法是 React 中的一个方法,因此有些同学就会有疑问,在写.jsx的组件时,本来没用到 React 中的方法,但还是要引入 React。就如上面的代码,在 React16 及之前,要在头部显式地将 React 引入进来的。import React from 'react'; 最终转换出的代码是:React.createElement('div', { onClick: handleClick }, React.createElement('p', null, 'hello world')); 但从 React17 开始,React 和 babel 合作,将 jsx 的转换工作放到了编译工具 babel 中。新的 JSX 转换不会将 JSX 转换为 React.createElement,而是自动从 React 的 package 中引入新的入口函数并调用。假设你的源代码如下:function App() { return Hello World; } 下方是新 JSX 被转换编译后的结果:// 由编译器引入(禁止自己引入!) import { jsx as _jsx } from 'react/jsx-runtime'; function App() { return _jsx('h1', { children: 'Hello world' }); } 注意,此时源代码无需引入 React 即可使用 JSX 了!若仍然要使用 React 提供的 Hook 等功能,还是需要引入 React 的。可以看到新 jsx()和之前的 React.createElement()方法转换出来的结构稍微有点区别。之前的 React.createElement()方法里,子结构会通过第三个参数进行传入;而在 jsx()方法中,这里将子结构放到了第二个参数的 children 字段里,第 3 个字段则用于传入设置的 key 属性。若子结构中只有一个子元素,那么 children 就是一个 jsx(),若有多个元素时,则会转为数组:const App = () => { return jsx('div', { children: jsx('p', { children: [ jsx('span', { className: 'dd', children: 'hello world', }), _jsx('span', { children: '123', }), ], }), }); }; 这里有个 babel 的在线网站,我们可以编写一段 React 代码,能实时看到通过 babel 编译后的效果:React 通过 babel 实现新的 jsx 转换。若 jsx 的转换方式还是旧版的,请在左侧的配置中,将 React Runtime 设置为 automatic 。那么 jsx()方法里具体是怎么执行的呢?最后返回了样子的数据呢?源码位置:jsx()。jsx()方法会先进行一系列的判断,相关链接: 介绍全新的 JSX 转换。jsx()方法中,会经过一些判断,将 key 和 ref 两个比较特殊的属性单独提取出来。/** * 将jsx编译为普通的js树形结构 * @param {string|function} type 若节点为普通html标签时,type为标签的tagName,若为组件时,即为该函数 * @param {object} config 该节点所有的属性,包括children * @param {string?} maybeKey 显式地设置的key属性 * @returns {*} */ export function jsx(type, config, maybeKey) { let propName; // Reserved names are extracted const props = {}; let key = null; let ref = null; // 若设置了key,则使用该key if (maybeKey !== undefined) { if (__DEV__) { checkKeyStringCoercion(maybeKey); } key = '' + maybeKey; } // 若config中设置了key,则使用config中的key if (hasValidKey(config)) { if (__DEV__) { checkKeyStringCoercion(config.key); } key = '' + config.key; } // 提取设置的ref属性 if (hasValidRef(config)) { ref = config.ref; } // Remaining properties are added to a new props object // 剩余属性将添加到新的props对象中 for (propName in config) { if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) { props[propName] = config[propName]; } } /** * 我们的节点有有三种类型: * 1. 普通的html标签,type为该标签的tagName,如div, span等; * 2. 当前是Function Component节点时,则type该组件的函数体,即可以执行type(); * 3. 当前是Class Component节点,则type为该class,可以new出一个实例; * 而type对应的是Function Component时,可以给该组件添加defaultProps属性, * 当设置了defaultProps,则将未明确传入的属性给到props里 */ // Resolve default props if (type && type.defaultProps) { const defaultProps = type.defaultProps; for (propName in defaultProps) { if (props[propName] === undefined) { props[propName] = defaultProps[propName]; } } } /** * 参数处理完成后,就调用ReactElement()方法返回一个object结构 */ return ReactElement(type, key, ref, undefined, undefined, ReactCurrentOwner.current, props); } ReactElement()方法的作用就是返回一个 object 结构,我们这里把所有的提示代码都去掉:/** * Factory method to create a new React element. This no longer adheres to * the class pattern, so do not use new to call it. Also, instanceof check * will not work. Instead test $$typeof field against Symbol.for('react.element') to check * if something is a React Element. */ const ReactElement = function(type, key, ref, self, source, owner, props) { const element = { // This tag allows us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // 用来标识当前是否是React元素 /** * 我们的节点有有三种类型: * 1. 普通的html标签,type为该标签的tagName,如div, span等; * 2. 当前是Function Component节点时,则type该组件的函数体,即可以执行type(); * 3. 当前是Class Component节点,则type为该class,可以通过该type,new出一个实例; * 而type对应的是Function Component时,可以给该组件添加defaultProps属性, * 当设置了defaultProps,则将未明确传入的属性给到props里 */ // Built-in properties that belong on the element type: type, key: key, ref: ref, props: props, // Record the component responsible for creating this element. _owner: owner, }; return element; }; 上面方法注释的大概意思是:现在不再使用类的方式 new 出一个实例来,因此不再使用 instanceOf 来判断是否是 React 元素;而是判断 $$typeof 字段是否等于Symbol.for('react.element')来判断。我们已经知道 $$typeof 字段的作用是为了标识 React 元素的,但他的值为什么用 Symbol 类型呢?可以参考这篇文章:为什么 React 元素有一个$$typeof 属性?到目前位置,我们已经知道了 jsx 在传入 render()方法之前,会编译成什么样子。我们在*.jsx文件中,先直接输出下 jsx 的结构:console.log( hello world , ); 在控制台里就能看到这样的结构:const element = { $$typeof: Symbol(react.element), key: null, props: { children: { // 当children有多个时,会转为数组类型 $$typeof: Symbol(react.element), key: null, props: { children: 'hello world', // 文本节点没有类型 }, ref: null, type: 'span', }, }, ref: null, type: 'div', }; 我们再输出一个完整的组件,如一个 App 组件如下:const App = ({ username }) => { return ( hello {username} ); }; 分别输出下 App 和:console.log(, App); 单纯的App是一个函数,function 类型,但这里不能直接执行App(),会报错的;而则是一个 json 结构,object 类型的,其本来的方法则存放到了 type 字段中。我们在上面的代码中已经说了 type 字段的含义,这里再说下跟 type 相关的 children 字段。当 type 为 html 标签时,children 就其下面所有的子节点。当只有一个子节点时,children 为 object 类型,当有多个子节点时,children 是 array 类型。有些同学可能一时反应不过来,觉得组件的 children 是其内部返回的 jsx 结构。这是不对的。这里我们要把组件也当做一个跟普通 html 标签一样的标签来对待,组件的 children 就是该组件标签包裹的内容。组件里的内容,可以通过执行type字段对应的 function 或 class 来获得。如:const Start = ( this is app children ); 这里标签里的 p 标签才是他的 children。因此,在传入到 render()方法时,就是这样子的一个 object 类型的 element 结构的元素。 3. fiber 结构 # 在上面通过 babel 转换后的 element 结构的数据,会在 render()方法中的某个阶段将其转为 fiber 结构。render()方法里具体怎样转换的,我们以后的文章再讲,这里我们只是看下 fiber 节点的结构。 3.1 单个 fiber 的属性 # 我们先看看一个 fiber 节点都有哪些属性,这些属性都是什么含义。/** * 创建fiber节点 * @param {WorkTag} tag * @param {mixed} pendingProps * @param {null | string} key * @param {TypeOfMode} mode * @constructor */ function FiberNode(tag: WorkTag, pendingProps: mixed, key: null | string, mode: TypeOfMode) { // Instance this.tag = tag; // 当前节点的类型,如 FunctionComponent, ClassComponent 等 /** * 这个字段和 react element 的 key 的含义和内容有一样(因为这个 key 是 * 从 react element 的key 那里直接拷贝赋值过来的),作为 children 列表 * 中每一个 item 的唯一标识。它被用于帮助 React 去计算出哪个 item 被修改了, * 哪个 item 是新增的,哪个 item 被删除了。 * @type {string} */ this.key = key; /** * fiber 中的 elmentType 与 element 中的type一样 * this.elementType = element.type */ this.elementType = null; /** * 当前fiber节点的元素类型,与React Element里的type类型一样,若是原生的html标签, * 则 type 为该标签的类型('div', 'span' 等);若是自定义的Class Component或 * Function Component等,则该type的值就是该class或function,后续会按照上面的tag字段, * 来决定是用new初始化一个实例(当前是 Class Component),然后执行该class内 * 的render()方法;还是执行该type(当前是 Function Component),得到其返回值; */ this.type = null; /** * 1. 若当前fiber节点是dom元素,则对应的是真实DOM元素; * 2. 若当前是function component,则值为null; * 3. 若当前是class component,则值为class初始化出来的实例; * 4. 若当前是 host component,即树的根节点,stateNode为 FiberRootNode; */ this.stateNode = null; /** * 下面的return, child和sibling都是指针,用来指向到其他的fiber节点, * React会将jsx编译成的element结构,转为以fiber为节点的链表结构, * return: 指向到父级fiber节点; * child: 指向到该节点的第1个子节点; * sibling: 指向到该节点的下一个兄弟节点; * 如图所示:https://www.xiabingbao.com/upload/386262ff06785779c.jpg */ this.return = null; this.child = null; this.sibling = null; this.index = 0; this.ref = null; this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null; this.mode = mode; // Effects this.flags = NoFlags; // 该节点更新的优先级,若为NoFlags时,则表示不更新 this.subtreeFlags = NoFlags; // 子节点的更新情况,若为NoFlags,则表示其子节点不更新,在diff时可以直接跳过 this.deletions = null; // 子节点中需要删除的节点 this.lanes = NoLanes; this.childLanes = NoLanes; /** * 双缓冲:防止数据丢失,提高效率(之后Dom-diff的时候可以直接比较或者使用 * React在进行diff更新时,会维护两颗fiber树,一个是当前正在展示的,一个是 * 通过diff对比后要更新的树,这两棵树中的每个fiber节点通过 alternate 属性 * 进行互相指向。 */ this.alternate = null; } React 中大到组件,小到 html 标签,都会转为 fiber 节点构建的 fiber 链表。 3.2 fiber 树的构成 # jsx 中的所有节点都会转为 fiber 节点,那他们是怎么组合起来的呢?正如上面代码中的注释中说到的,每个 fiber 节点都有 3 个指针: return: 指向到父级的 fiber 节点; child: 指向到该节点的第 1 个子节点;若想访问其他的子节点,可以通过下面的sibling指针来访问; sibling: 指向到该节点的下一个兄弟节点; 如图所示:并列的节点,会形成单向链表,父级节点只会指向到这个单向链表的头节点。正如上图中的 p 标签和 span 标签。 4. 为什么要使用 fiber 结构 # 为什么要使用 fiber 链表?这里我们稍微了解下,后面会详细介绍 fiber 链表如何进行 diff 每个 fiber 节点的。 4.1 Stack Reconciler # 在 React 15.x 版本以及之前的版本,Reconciliation 算法采用了栈调和器( Stack Reconciler )来实现,但是这个时期的栈调和器存在一些缺陷:不能暂停渲染任务,不能切分任务,无法有效平衡组件更新渲染与动画相关任务的执行顺序,即不能划分任务的优先级(这样有可能导致重要任务卡顿、动画掉帧等问题)。Stack Reconciler 的实现。 4.2 Fiber Reconciler # 为了解决 Stack Reconciler 中固有的问题,以及一些历史遗留问题,在 React 16 版本推出了新的 Reconciliation 算法的调和器—— Fiber 调和器(Fiber Reconciler)来替代栈调和器。Fiber Reconciler 将会利用调度器(Scheduler)来帮忙处理组件渲染/更新的工作。此外,引入 fiber 这个概念后,原来的 react element tree 有了一棵对应的 fiber node tree。在 diff 两棵 react element tree 的差异时,Fiber Reconciler 会基于 fiber node tree 来使用 diff 算法,通过 fiber node 的 return、child、sibling 属性能更方便的遍历 fiber node tree,从而更高效地完成 diff 算法。fiber 调度的优点: 能够把可中断的任务切片处理; 能够调整任务优先级,重置并复用任务; 可以在父子组件任务间前进后退切换任务; render 方法可以返回多个元素(即可以返回数组); 支持异常边界处理异常; 5. 总结 # fiber 结构是 React 整体的一个基础,两棵状态树的 遍历、diff 对比,任务优先级的判断等,都是基于 fiber 结构来实现的。其实我们在上面的讲解中,已经解决了几个常见的问题,如: 5.1 为什么 React17 需要显式地引入 React,而之后不用了? # 这是因为在 React17 之前,createElement() 方法是在放在 React 中的,只要涉及到 jsx 的,都需要引入 React,才能使用该方法。而从 React17 开始,修改了 jsx 的编译方式。 5.2 Virtual Dom 是什么? # 这里我们介绍了 3 种数据结构,那么 React 中说的虚拟 DOM(Virtual DOM)指的是哪一个呢?实际上指的是 element 这个数据结构,用 js 对象描述真实 dom 的 js 对象。 优点:处理了浏览器的兼容性,防范 xss 攻击,跨平台,差异化更新,减少更新的 dom 操作; 缺点:额外的内存,初次渲染不一定快;因为要进行后续一系列的构建、hooks 的搭建等,才会渲染 DOM;会比直接操作 DOM 要慢一些;
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
JavaScript 中的 Promise 异步并发控制
我们在开发的过程中,经常会遇到一些并发的情况,而如果并发量比较大时,需要进行限制。比如可能出现的场景: 传入多个异步请求,但最多只能触发 limit 个请求;额外的功能,所有的请求都执行完后,返回成功; 生成一个新函数,调用多次发起请求,但有并发限制; 多个 Promise 按顺序执行,这个其实可以认为并发数限制是 1,但我们可以用另一种方式来实现; 1. 多个异步请求的并发限制 # 有一系列的异步请求,比如爬虫抓取、后端接口请求、图片加载等场景,需要限制下并发请求的数量。这里要考虑下结果的处理,是每个请求完成后就可以了,还是要收集到所有的结果,类似于 Promise.all() 的效果。 1.1 递归的方式 # 思路大概是:首先发起 limit 个的请求,哪个完成了就递归发起下一个异步请求,所有的请求都完成后,则整体返回一个 Promise。如果不需要收集所有的数据,则不用写这个 Promise。/** * 递归方式实现异步并发控制 * @param arr 所有的数据集合,如请求的url等 * @param limit 限制并发的个数 * @param iteratorFn 对每个数据的处理 */ const promiseLimitByDepth = (arr: T[], limit: number, iteratorFn: (item: T, urls?: T[]) => Promise) => { const { length } = arr; const result = []; let i = 0; let finishedNum = 0; // 完成的个数 // 若不考虑最后数据的收集,可以不写这个Promise return new Promise((resolve) => { const request = async (index: number) => { kk.show(arr[index], index); const p = Promise.resolve().then(iteratorFn(arr[index])); const res = await p; result[index] = res; finishedNum++; if (finishedNum === length) { resolve(result); } if (i < length) { request(i); i++; } }; for (; i < limit; i++) { request(i); } }); }; 这里我们用 setTimeout 来模拟下异步请求。const newFetch = (delay) => { return new Promise((resolve) => { setTimeout(() => { resolve(delay); }, delay); }); }; 调用方式:promiseLimitByDepth([2000, 1000, 3000, 2500, 1200, 5000, 3500, 2300], 2, (num) => { return newFetch(num); }).then(console.log); 您可以查看 demo:递归实现的异步并发控制。在 demo 中可以看到,控制着同一时刻的请求个数,某一个请求结束后,再启动下一个请求。上面的代码还可以用来控制图片的加载:const arr = []; let i = 10; while (i--) { arr.push(`https://www.xiabingbao.com/upload/368662d904df5cbe4.jpg?t=${Math.random()}`); } promiseLimitByDepth(arr, 2, (url) => { return new Promise((resolve) => { const img = new Image(); img.src = url; // 这里暂时只考虑成功的情况 img.onload = resolve; }); }).then(console.log); 我们从图片加载的瀑布流里可以看到,每次最多只加载 2 张图片: 1.2 循环的方式 # 使用循环的方式,肯定得用到 async-await 了。const promiseLimitByCycle = async (arr: T[], limit: number, iteratorFn: (item: T, arr?: T[]) => Promise) => { const { length } = arr; const result: Promise[] = []; const runningList: Promise[] = []; // 正在执行的异步任务 for (const url of arr) { const p = Promise.resolve().then(iteratorFn(url)); // 转为promise result.push(p); // 若limit大于length,则不再进行控制,直接用Promise.all()即可 if (limit { // promise p 执行完毕时,会触发这个,这个是后执行的,先执行的是下面的push操作 const index = runningList.indexOf(e); // 当p执行成功的时候,从runningList中删除该Promise,同时也会触发下面的Promise.race() return runningList.splice(index, 1); }); // promise e 是 p执行的过程,若p执行成功,则e.value就是p.then()里的return的值 runningList.push(e); // 超过限制,则先存储起来 if (runningList.length >= limit) { // 哪个先完成,都会触发race,然后进入下一层循环 await Promise.race(runningList); } } } // 所有的都完成了,才最后返回结果 return Promise.all(result); }; 上面有段代码比较绕,我们再单独拿出来讲解下:// Promise是可以链式调用的,then()本身返回的就是Promise // 因此e是p.then()的返回值,e自己也是Promise // e.then()什么时候执行,取决于p.then()什么执行,又再取决于p什么时候执行 // const e = p.then()是同步执行的,因此先得到的变量e,再执行的p.then()里的操作 // 当p执行完成后,则就执行p.then()里的操作,找出e所在的位置并进行删除 // e.then()回调里的值据说splice()的返回值,其实就是e,但这里我们并不用关心他的返回值是什么 const e = p.then(() => { const index = runningList.indexOf(e); return runningList.splice(index, 1); }); runningList.push(e); // 这里监听的是runningList,即里面的某个e完成了,就会触发Promise.race() // 若e完成了,必然p也是完成了的 await Promise.race(runningList); 这里充分用到了Promise.all()和Promise.race()的特性,来实现的。 2. 新函数的并发限制 # 我们来简单描述下题目:创建返回一个新函数,在调用这个新函数产生异步请求时,有并发的限制。// 创建返回一个新函数,在调用这个新函数产生异步请求时,限制并发的数量 // 问,如何实现这个create方法? const createFetch = (limit) => { return () => {}; }; const newFetch = createFetch(2); // 最多只能并发2个 newFetch(url); newFetch(url); newFetch(url); newFetch(url); 这里参考了 npm 包 p-queue 的源码,并对其进行了精简。新函数 newFetch() 每次都是要返回一个 Promise 的,就看什么时候执行 resolve(),并启动下一个。const createFetch = (limit) => { let runningNum = 0; // 当前正在进行的数量 const queue = []; // 所有将要执行的任务队列 // 尝试启动下一个任务 const tryNextOne = () => { if (queue.length === 0) { return false; } if (runningNum < limit) { // 若没有达到限制,则直接启动 const job = queue.shift(); if (!job) { return false; } job(); return true; } return false; }; // 返回一个新函数,新函数里直接返回一个Promise return (url, iteratorFn) => { return new Promise((resolve) => { // 定义一个函数,但不立即执行 const run = async () => { runningNum++; // 启动一个任务,数量+1 const result = await Promise.resolve(iteratorFn(url)); resolve(result); runningNum--; // 完成一个任务,数量-1 tryNextOne(); // 启动下一个任务 }; queue.push(run); // 将所有的任务,都推送到队列中 tryNextOne(); // 启动队列中任务的入口 }); }; }; 我们用 sleep() 函数模拟下:const sleep = (delay) => { return new Promise((resolve) => { setTimeout(() => { resolve(delay); }, delay); }); }; const newFetch = createFetch(2); for (let i = 0; i < 10; i++) { console.log(`${i} start`); newFetch(i, async (i) => { await sleep(600 + 10 * i); return `${i}`; }).then((i) => { console.log(`${i} end`); }); } 3. 多个异步任务的顺序执行 # 我们其实把上面实现的一些函数,并发数量设置为 1,就是多个异步任务的顺序执行了。不过我们这里还有一些其他的方式。 3.1 async-wait # 把所有的异步任务都放到数组中,然后用 async-wait 的方式来控制:const arr = [600, 500, 400, 700, 300, 450]; const asyncLoop = async (arr, iteratorFn) => { const result = []; for (const item of arr) { console.log(`${item} start`); const res = await Promise.resolve(iteratorFn(item)); console.log(`${res} end`); result.push(res); } return result; }; asyncLoop(arr, (item) => { return sleep(item); }); 3.2 纯 Promise # 如果不使用 async-await,用 Promise 可以实现吗?Promise 是异步的,在一个同步流程中,是无法等待这个 Promise 完成的,因此这里我用递归的方式来实现的。const promiseLoop = (arr, iteratorFn) => { const result = []; return new Promise((allResolve) => { const run = (index = 0) => { if (index < arr.length) { return new Promise((resolve) => { const p = Promise.resolve(iteratorFn(arr[index])); p.then((res) => { console.log(res); result.push(res); resolve(res); if (index + 1 < arr.length) { // 上一个Promise完成后,启动下一个 run(index + 1); } else { // 若全部都完成了,则执行最外层的Promise allResolve(result); } }); }); } }; run(); }); }; 使用方式与上面的一样:promiseLoop(arr, (item) => { return sleep(item); }).then(console.log); 4. 同时请求,但按顺序尽快输出 # 如并发请求一些数据,结果按照请求顺序依次输出,而且要尽可能早的输出结果。如 a,b,c 三个请求并发请求: a 需要 200ms; b 需要 100ms; c 需要 300ms; 即使 b 先完成,也得等着 a 完成输出结果后,b 再输出,c 稍后完成后,再输出 c 的结果。等所有的请求都执行完毕后,再整体按照顺序返回请求的结果。我实现的思路是在后面的请求先完成的,则将结果先存储起来,等前面的请求完成后,再一并输出。// 并发请求但顺序输出 const concurrentAndSyncLog = (arr, iteratorFn) => { const { length } = arr; const list = new Array(length).fill({ fulfilled: false, value: null }); // fulfilled表示数据是否已准备好 let showStart = 0; // 开始输出的位置 let fulfilledNum = 0; // 完成的个数 return new Promise((resolve) => { for (let i = 0; i < length; i++) { const p = Promise.resolve(iteratorFn(arr[i])); p.then((result) => { list[i] = { fulfilled: true, value: result }; fulfilledNum++; if (i === showStart) { let j = showStart; while (j < length) { if (list[j].fulfilled) { // 输出所有完成的数据 console.log(list[j].value); } else { // 当前位置的数据还没准备好,直接停止,并设置下次输出的位置 showStart = j; break; } j++; } } if (fulfilledNum >= length) { resolve(list.map((item) => item.value)); } }); } }); }; 调用:concurrentAndSyncLog([200, 100, 300], sleep).then(console.log); // 200, 100, 300 // [200, 100, 300] 5. 总结 # JavaScript 中对 Promise 的异步并发的控制,更多地是考察我们对 Promise 中一些知识点的运用和和深刻理解。比如 Promise.race(),Promise.all()等方法的使用,还有 Promise 的链式调用、等待机制等。我们之前在之前的文章实现 Promise 的 first 等各种变体中,也是运用了 Promise 的各种机制,来实现一些 Promise 本身不支持的功能。这篇文章希望能更加加深我们 Promise 的理解。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
在 JavaScript 中循环和定时输出一系列的内容
我们在上一篇文章中聊了多种 Promise 的并发控制,于是就衍生出了另一个问题:定时输出一系列的内容,这里可能是数组中的数据,也可能是其他的。我们来看看有哪些实现方法。既然是定时输出,必然涉及到 setTimeout 或者 setInterval 的运用。一个很经典的错误案例:// 使用var声明变量 for (var i = 0; i < 10; i++) { setTimeout(() => { console.log(i); }, 500 * i); } var声明的变量存在变量提升的问题,而且 for 循环是同步任务,当执行 setTimeout 时,for 循环已执行完毕,因此输出的全是 10。那有什么解决方案吗? 1. 闭包 # 在指定 setTimeout 时,外层包一个闭包,当 setTimeout 向外层寻找时,找到该闭包就停止了,而每个闭包中的环境是独立的。for (var i = 0; i < 10; i++) { ((j) => { setTimeout(() => { console.log(j); }, 500 * j); })(i); } 2. 使用 let 来声明变量 # 主要考虑 let 的块级作用域和 eventloop 事件循环机制。如:// 使用let声明变量 for (let i = 0; i < 10; i++) { setTimeout(() => { console.log(i); }, 500 * i); } let声明的变量是块级作用域,setTimeout 向外层寻找到的变量 i 就是当时循环时的那个 i 的变量。 3. setTimeout 本身就可以传参 # 可能我们用 setTimeout 后续的参数比较少,其实 setTimeout() 函数,从第 3 个参数开始,都是传入到回调里的数据。如:setTimeout( (username, age) => { console.log(`my name is ${username}, my age is ${age}`); // my name is jack, my age is 28 }, 500, // 延迟时间 'jack', // 从这里开始,都是参数,并且可以无限个 28, ); 因此,我们可以把 for 循环改成:for (var i = 0; i < 10; i++) { setTimeout( (j) => { console.log(j); }, 500 * i, i, ); } 4. bind # 我们可以 setTimeout 中的回调函数,通过 bind()方法再生成一个:for (var i = 0; i < 10; i++) { setTimeout( ((j) => { console.log(j); }).bind(null, i), 500 * i, ); } 里面拆开一下:const fn = (j) => { console.log(j); }; const callback = fn.bind(null, i); setTimeout(callback, 500); bind()本身就是用闭包来实现的。 5. Promise # 我们可以把 setTimeout 封装成一个 Promise,然后再在 for 循环里使用。const sleep = (delay) => { return new Promise((resolve) => setTimeout(resolve, delay)); }; 循环所在的函数改为async-await的结构:const start = async () => { for (var i = 0; i < 10; i++) { // for-of也可以 await sleep(500); console.log(i); } }; start(); 我们用了普通的 for 循环,其实for-of也是可以的。但forEach(), map()等方法就不可以了,如:const start = async () => { // for (var i = 0; i < 10; i++) { // // for-of也可以 // await sleep(500); // console.log(i); // } const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; // 错误的方式,请不要使用 arr.forEach(async (item) => { await sleep(500); console.log(item); }); }; start(); 虽然输出了 0-9,但是是同时输出的,两个输出之间没有间隔。这是因为 forEach()中,每次循环的 callback 都是独立执行的,async 只控制其自己内部的 await,并不能控制其他的循环。可以看下 V8 源码中的实现,Array.prototype.forEach()实际上调用的 V8 中的ArrayForEach()。从远吗中也能看到,这是对数组的每一项都调用了 callback。上面的写法,我们拆分一下就好理解了:const callback = async (item) => { await sleep(500); console.log(item); }; arr.forEach(callback); 若我们自己来实现 forEach() 方法时:Array.prototype.forEach = function (callback, thisArg) { const context = thisArg ?? null; const arr = this; for (let i = 0; i < arr.length; i++) { callback.call(context, arr[i], i, arr); } }; 顺带地,我们也就知道了为什么break,return等终止循环的语句在 forEach()中没有效果了。因为这些操作语句的作用范围仅是限制在回调函数 callback 的内部,并不会影响到外层的循环。既然 Promise 和循环可以结合,那么 Promise 和递归也可以结合。这里我们就不拆解了。 6. 总结 # 我们用了多种方法来实现这样的功能,不同的方法接触到的知识点也不一样。
2024年10月20日
4 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之虚拟 DOM 转为 fiber 树
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 我们在文章 React18 源码解析之 fiber 等数据结构 中讲解了 jsx, element 和 fiber 的基本结构。这里我们主要讲下如何将 jsx 转为 fiber 节点组成的 fiber 树。我们为便于理解整个转换的过程,会做一些流程上的精简: 大部分只考虑初始渲染阶段,因此诸如副作用的收集等暂时就不考虑了,不过偶尔也会涉及到一点两棵 fiber 树的对比; 忽略各种任务的优先级的调度; React 中各个节点的类型很多,如函数组件、类组件、html 节点、Suspense 类型的组件、使用 lazy()方法的动态组件等等,不过我们这里主要讲解下函数组件、类组件、html 节点这 3 个; render()方法是我们整个应用的入口,我们就从这里开始。我们在之前的文章React18 源码解析之 render()入口方法中,只是讲解了 render()方法的挂载方式。这里我们会深入了解到从 jsx 转为 fiber 的整个过程。 1. 起始 # 在开始讲解前,我们先定义下要渲染的 React 组件,方便我们后续的理解:const FuncComponent = () => { return ( this is function component ); }; class ClassComponent extends React.Component { render() { return this is class component; } } function App() { return ( 123 ); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(); 我们编写这个结构主要是为了看下,嵌套的、并列的标签,流程是如何流转的,中间经历了多少个复杂的过程,才将 element 转为 fiber 节点。 2. 初始的 element 如何处理? # 我是强烈建议使用debug-react项目来进行调试的,因为里面的分支和各种变量很多,若只是单纯地硬看,很容易懵,不知道这些变量具体是什么值!那么运行起一个 React 项目后,可以在某些关键节点、变量,打断点输出,就方便很多。render()传入的 element 和后续的 element 的操作是不一样的。其他的 element 都可以通过执行函数组件或者类组件的实例来得到,而初始的 element 是直接提供的。我们直接输出下:console.log(); 可以看到,element 结构并不是把 React 所有的 jsx 都组织起来,形成巨大的嵌套结构,他只是当前某个节点里的 jsx 结构。若当前是函数组件、类组件等,内部的 jsx 可以通过执行属性 type 对应的函数或类的实例来得到,然后继续递归下去,最终把所有的 jsx 都全部解析出来。入口函数 render() 传入的参数,就是上面的 element 结构。内部通过属性_internalRoot得到整个应用的根节点 root,然后又调用了 updateContainer():// children就是我们传入的,即通过jsx编译后的element结构 ReactDOMRoot.prototype.render = function(children: ReactNodeList) { const root = this._internalRoot; // FiberRootNode updateContainer(children, root, null, null); }; 我们去掉 dev 代码和任务优先级的调度,看下 updateContainer() 主要的流程:/** * 将element结构转为fiber树 * @param {ReactNodeList} element 虚拟DOM树 * @param {OpaqueRoot} container FiberRootNode 节点 * @param {?React$Component} parentComponent 在React18传到这里的是null * @param {?Function} callback render()里的callback,不过从React18开始就没了,传入的是null * @returns {Lane} */ export function updateContainer(element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component, callback: ?Function,): Lane { /** * current: * const uninitializedFiber = createHostRootFiber(tag, isStrictMode, concurrentUpdatesByDefaultOverride,); */ // FiberRootNode.current 现在指向到当前的fiber树, // 若是初次执行时,current树只有hostFiber节点,没有其他的 const current = container.current; const eventTime = requestEventTime(); const lane = requestUpdateLane(current); // 结合 lane(优先级)信息,创建 update 对象,一个 update 对象意味着一个更新 /** * const update: Update = { * eventTime, * lane, * tag: UpdateState, * payload: null, * callback: null, * next: null, * }; * @type {Update} */ const update = createUpdate(eventTime, lane); update.payload = { element }; // 处理 callback,这个 callback 其实就是我们调用 ReactDOM.render 时传入的 callback // 不过从React18开始,render不再传入callback了,即这里的if就不会再执行了 callback = callback === undefined ? null : callback; if (callback !== null) { update.callback = callback; } /** * 将update添加到current的更新链表中 * 执行后,得到的是 current.updateQueue.shared.pending = sharedQueue * sharedQueue是React中经典的循环链表, * 将下面的update节点插入这个shareQueue的循环链表中,pending指针指向到最后插入的那个节点上 */ enqueueUpdate(current, update, lane); /** * 这里调用的链路很深,做了很多事情,如: * 流程图: https://docs.qq.com/flowchart/DS0pVdnB0bmlVRkly?u=7314a95fb28d4269b44c0026faa673b7 * scheduleUpdateOnFiber() -> ensureRootIsScheduled(root) -> performSyncWorkOnRoot(root) * -> renderRootSync(root) -> workLoopSync() */ /** * 这里传入的current是HostRootFiber的fiber节点了,虽然他的下面没有其他fiber子节点, * 但它的updateQueue上有element结构,可以用来构建fiber节点 * 即 current.updateQueue.shared.pending = sharedQueue,element结构在sharedQueue其中的一个update节点, * 其实这里只有一个update节点 */ const root = scheduleUpdateOnFiber(current, lane, eventTime); if (root !== null) { entangleTransitions(root, current, lane); } return lane; } 我们再梳理下函数 updateContainer()的流程: updateContainer(element, container)传入了两个参数,element 就是 jsx 编译后的 element 结构,而 container 表示的是 FiberRootNode,整个应用的根节点,并不是 DOM 元素; container.current 指向的就是目前唯一的一棵 fiber 树的根节点,并 current 变量存储该节点; 将 element 结构放到 current 节点的属性中,方便后续的构建:current.updateQueue.shared.pending = [{payload:{element}}];pending 是一个环形链表,element 就放在这个环形链表的节点中,在初始更新阶段,只有这一个 update 节点; 调用 scheduleUpdateOnFiber(current);该方法内部将 element 取出,构建出下一个 fiber 节点; 3. scheduleUpdateOnFiber # 接下来我们看下 scheduleUpdateOnFiber 函数。markUpdateLaneFromFiberToRoot(fiber, lane)方法会将当前 fiber 节点往上知道 FiberRootNode 所有节点赋值 lane 这个优先级,同时返回整个应用的根节点 FiberRootNode。我们忽略中间任务调度的步骤,直接进入到 ensureRootIsScheduled(root) 函数中,这里的 root 参数就是整个应用的根节点 FiberRootNode。这个函数里也是一堆的任务调度,我们快进到 performConcurrentWorkOnRoot.bind(null, root),这里面又快进到了 renderRootSync(root)。 4. renderRootSync # 这里有一个很重要的准备操作,这里的 root 是整个应用的根节点,即 FiberRootNode,会传入到函数 prepareFreshStack() 中,主要是为了接下来的递归,初始化一些数据和属性:/** * 整个应用目前只有 FiberRootNode和current两个节点,current树只有一个根节点,就是current自己; * 另一棵树还没有创建,结构是这样: https://mat1.gtimg.com/qqcdn/tupload/1659715740891.png * prepareFreshStack() 函数的作用,就是通过current树的根节点创建出另一棵树的根节点, * 并将这两棵树通过 alternate 属性,实现互相的指引 * workInProgressRoot: 是将要构建的树的根节点,初始时为null,经过下面 prepareFreshStack() 后, * root.current给到workInProgressRoot, * 即使第二次调用了,这里的if逻辑也是不会走的 * workInProgress初始指向到workInProgressRoot,随着构建的深入,workInProgress一步步往下走 */ if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) { /** * 将整个应用的根节点和将要更新的fiber树的根节点赋值到全局变量中 * root是当前整个应用的根节点 */ prepareFreshStack(root, lanes); } 我们看下函数 prepareFreshStack() 的实现:/** * 准备新堆栈,返回「更新树」的根节点 * @param root * @param lanes * @returns {Fiber} */ function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber { root.finishedWork = null; root.finishedLanes = NoLanes; workInProgressRoot = root; // 整个React应用的根节点,即 FiberRootNode /** * prepareFreshStack()个人认为是只在初始化时执行一次,root是整个应用的根节点,而root.current就是默认展示的那棵树, * 在初始化时,current 树其实也没内容,只有这棵树的一个根节点; * 然后利用current的根节点通过 createWorkInProgress()方法 创建另一棵树的根节点rootWorkInProgress * createWorkInProgress()方法内则判断了 current.alternate 是否为空,来决定是否可以复用这个节点, * 在render()第一次调用时,root.current.alternate 肯定为空,这里面则会调用createFiber进行创建 */ const rootWorkInProgress = createWorkInProgress(root.current, null); // 初始执行时,workInProgress指向到更新树的根节点, // 在mount阶段,workInProgress是新创建出来的,与current树的根节点workInProgressRoot,肯定是不相等的 workInProgress = rootWorkInProgress; workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes; workInProgressRootExitStatus = RootInProgress; workInProgressRootFatalError = null; workInProgressRootSkippedLanes = NoLanes; workInProgressRootInterleavedUpdatedLanes = NoLanes; workInProgressRootRenderPhaseUpdatedLanes = NoLanes; workInProgressRootPingedLanes = NoLanes; workInProgressRootConcurrentErrors = null; workInProgressRootRecoverableErrors = null; return rootWorkInProgress; } 总结下 prepareFreshStack() 的作用: 将整棵树的根节点 root 给到 workInProgressRoot; createWorkInProgress() 利用 current 节点创建出「更新树」的根节点;整个函数在这里我们就不展开讲了,大致内容就是判断 current.alternate (即 current 互相对应的那个节点)是否为空,若为空则创建出一个新节点;若不为空,则直接复用之前的节点,然后将新特性给到这个节点(不过我们这里传入的是 null); workInProgress 指针初始指向到「更新树」的根节点,在接下来的递归操作中,该指针一直在变动; 准备好之后,我们再回到函数 renderRootSync(),就可以顺着 workInProgress 指针往下进行了,这里我们就进入到了函数 workLoopSync() 中。 5. workLoopSync # 该函数很简单,就是一个 while 循环,每次循环时,都会执行函数 performUnitOfWork() ,然后操作 workInProgress 指向的那个 fiber 节点,直到 workInProgress 为 null。我们刚才在上面的 prepareFreshStack() 中,workInProgress 指针指向到了「更新树」的根节点 rootWorkInProgress(即跟 current 树根节点长得一样的那个节点),这个 fiber 节点里的 updateQueue.shared.pending 中的一个 update 里,存放着 element 结构。function workLoopSync() { // Already timed out, so perform work without checking if we need to yield. // 已经超时了,所以即使需要让出时,也不再做检查,直到把workInProgress执行完 while (workInProgress !== null) { performUnitOfWork(workInProgress); } } 6. performUnitOfWork # 处理 workInProgress 指向的那个 fiber 节点。function performUnitOfWork(unitOfWork: Fiber): void { /** * 初始mount节点时,unitOfWork 是上面workLoopConcurrent()中传入的 workInProgress, * unitOfWork.alternate 指向的是 current */ const current = unitOfWork.alternate; let next; /** * current为当前树的那个fiber节点 * unitOfWork为 更新树 的那个fiber节点 * 在初始mount节点,current和unitOfWork都是fiberRoot节点 * 在第一次调用beginWork()时,element结构通过其一系列的流程,创建出了第一个fiber节点,即对应的fiber节点(我们假设 是最外层的元素) * next就是第一个fiber节点,然后next给到workInProgress,接着下一个循环 */ next = beginWork(current, unitOfWork, subtreeRenderLanes); unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { // unitOfWork已经是最内层的节点了,没有子节点了 // If this doesn't spawn new work, complete the current work. completeUnitOfWork(unitOfWork); } else { workInProgress = next; } } 将当前 current 节点和更新树的节点,都传给 beginWork(current, unitOfWork) 函数。简单来说,beginWork 会根据当前 fiber 节点中的 element 结构,创建出新的 fiber 节点,workInProgress 再指向到这个新 fiber 节点继续操作,直到所有的数据都操作完。具体的操作流程,我们单独开一篇文章进行讲解。这里我们只需要知道,诸如函数组件、类组件也是 fiber 节点,也是整棵 fiber 树的一部分。其内部的 jsx(element)再继续转为 fiber 节点。若 beginWork()返回的 next 是 null,说明当前节点 workInProgress 已经是最内层的节点了,就会进入到函数 completeUnitOfWork() 中。可以看到执行的流程是深度优先,即若当前 fiber 还能构造出子节点,即一直向下构造。直到没有子节点后,才会流转到兄弟节点和父级节点。以我们上面的写的 React 组件为例,workInProgress 指向是的树的根节点,这个根节点没有具体的 jsx 结构。const FuncComponent = () => { return ( this is function component ); }; class ClassComponent extends React.Component { render() { return this is class component; } } function App() { return ( 123 ); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(); 第一次执行后函数 beginWork() 后,得到 workInProgress 子元素里 element 结构转换出来的 fiber 节点,就是 对应的 fiber 节点,然后给到变量 next;workInProgress 再移动这个 fiber 节点上; 继续执行函数 beginWork(),得到子元素的 fiber 节点,即 div 标签对应的 fiber 节点; 继续执行函数 beginWork(),得到 div 标签子元素的 fiber 节点,若子元素是一个数组,beginWork() 会把所有的子元素都转为 fiber 节点,并形成单向链表,然后返回这个链表的头指针。这里,div 标签里有 3 个并列的元素,即, 和 div 标签,beginWork()会节点的不同类型,创建出不同的 fiber 节点,然后形成链表,再回到这个链表的第 1 个节点;即 workInProgress 指向了生成的 fiber 节点; 继续执行函数 beginWork(),就会到里的 jsx 对应的 fiber 节点,即 p 标签(beginWork()只会一层一层的构建),workInProgress 再指向到 p 标签对应的 fiber 节点,继续构建 span 和文本对应的 fiber 节点; 文本再往内,就没有节点了,即 next 得到的是 null,这时就会进入到 completeUnitOfWork() 函数,通过该函数的调度,workInProgress 又回到了的兄弟节点; 继续执行函数 beginWork(),得到 里的 jsx 对应的 fiber 节点,即 p 标签; 我们接下来再看下函数 completeUnitOfWork() 是如何流转 workInProgress 指针的。 7. completeUnitOfWork # 当前节点和当前所有的子节点都执行完了,就会调用该方法。现在我们只关心整个流程的流转问题。/** * 当前 unitOfWork 已没有子节点了 * 1. 若还有兄弟节点,将 workInProgress 指向到其兄弟节点,继续beginWork()的执行; * 2. 若所有的兄弟节点都处理完了(或者没有兄弟节点),就指向到其父级fiber节点;回到1; * 3. 直到整个应用根节点的父级(根应用没有父级节点,所以为null),才结束; */ function completeUnitOfWork(unitOfWork: Fiber): void { let completedWork = unitOfWork; do { const current = completedWork.alternate; const returnFiber = completedWork.return; // 每个节点都有一个return属性,指向到其父级节点 // 若有兄弟节点,则继续执行兄弟节点 const siblingFiber = completedWork.sibling; // 该节点下一个兄弟节点 if (siblingFiber !== null) { // If there is more work to do in this returnFiber, do that next. workInProgress = siblingFiber; return; } // 当前节点和兄弟节点全部遍历完毕,则返回到其父节点 // Otherwise, return to the parent completedWork = returnFiber; // Update the next thing we're working on in case something throws. workInProgress = completedWork; } while (completedWork !== null); // while的作用就是若该节点没有兄弟节点,能够一直往上找父级节点, } 通过这张图,我们可以更加直观地理解整个流转流程:图中的数字,就是 workInProgress 指针行进的顺序。 当有子节点时,就一直往下构建其子节点;若子节点有多个,则一并都构建出来; 若没有子节点,则优先查询是否有兄弟节点,若有,则流转到兄弟节点(如图中的 5 和 8),回到 1; 若没有兄弟节点,则回到其父级节点(红色箭头),然后查询父级节点是否有兄弟节点,若有则回到 2,若没有,则继续回到父级节点; 所有的节点都遍历执行完了,workLoopSync()中的 while()循环也就停止了,workInProgress 也指向到了 null,然后就可以进入到 DOM 渲染的 commit 阶段了。 8. 总结 # 到这里,我们把 element 转为 fiber 节点的大致流程过了一遍。主要了解到以下几个知识点: element 结构从一开始并不是一个巨大的嵌套结构,而是执行组件后,才能得到这个组件里的 element 结构; 转成 fiber 节点的过程,深度优先的原则,优先执行其第 1 个节点,然后再执行兄弟节点,再回到父级节点; 每次将 element 结构转为 fiber 节点时,只转当前 fiber 节点里的 element 最直接的子节点,若还有更深的子节点,则等着一会儿 workInProgress 流转到这里的时候,再执行; 若当前层级的 element 结构是一个数组,即有多个元素时,则会一并全部进行转换; 接下来的文章,我们会再详细介绍下函数 beginWork(),了解不同的 element 结构如何转成 fiber 节点的。
2024年10月20日
3 阅读
0 评论
0 点赞
1
...
51
52
53
...
213