首页
Search
1
解决visual studio code (vscode)安装时没有选择安装路径问题
321 阅读
2
如何在 Clash for Windows 上配置服务
216 阅读
3
Linux 下 Bash 脚本 bad interpreter 报错的解决方法
150 阅读
4
Arch Linux 下解决 KDE Plasma Discover 的 Unable to load applications 错误
149 阅读
5
uniapp打包app提示通讯录权限问题,如何取消通讯录权限
113 阅读
clash
服务器
javascript
全部
游戏资讯
登录
Search
加速器之家
累计撰写
1,202
篇文章
累计收到
0
条评论
首页
栏目
clash
服务器
javascript
全部
游戏资讯
页面
搜索到
1202
篇与
的结果
2024-10-20
React18 源码解析之 render()入口方法
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 1. render() 方法的使用 # render() 方法是整个 React 应用的入口方法,所有的 jsx 渲染、hook 的挂载和执行等,都在这个里面。从 React18 开始,render()方法的使用跟之前不一样了。之前的使用方式:import ReactDOM from 'react-dom'; const root = document.getElementById('root'); ReactDOM.render(, root); 新的使用方式:// React18.x import ReactDOM from 'react-dom/client'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render(); 对比后,发现有几点不一样的使用方式: ReactDOM 改成了从react-dom/client引入; render()方法的调用方改变了,从之前的 ReactDOM 变成了 ReactDOM.createRoot() 创建后的实例; render()方法的参数发生了改变,之前是 2 个固定参数加一个可选的 callback,分别是 jsx 组件,dom 节点和可选的 callback,这个 callback 在 dom 渲染完毕后执行;新 render()方法中,只有一个必传的参数,即 jsx 组件,若想实现之前的 callback 功能,这里建议使用 useEffect()。 2. createRoot() # 源码位置:ReactDOMRoot.js#L185。createRoot()函数有两个参数,第 1 个是传入一个 dom 节点,第 2 个是可选的配置参数,我们暂时先不管 options 的配置,先把这些配置代码删去,只看大流程。export function createRoot(container: Element | Document | DocumentFragment, options?: CreateRootOptions): RootType { // 判断container是否是合法的dom元素 if (!isValidContainer(container)) { throw new Error('createRoot(...): Target container is not a DOM element.'); } // 若container为body或已被作为root使用过,则在dev环境发出警告 warnIfReactDOMContainerInDEV(container); let isStrictMode = false; let concurrentUpdatesByDefaultOverride = false; let identifierPrefix = ''; let onRecoverableError = defaultOnRecoverableError; let transitionCallbacks = null; /** * 创建一个 FiberRootNode 类型的节点,fiberRootNode 是整个应用的根节点 * 在react的更新过程中,会有current(当前正在展示)和workInProgress(将要更新的)两个fiber树, * fiberRootNode 默认指向到current, * workInProgress更新并commit完毕后,fiberRootNode会指向到workProgress * 调用链路: createContainer() -> createFiberRoot() -> {new FiberRootNode(), createHostRootFiber()} -> createFiber() -> new FiberNode() * root节点是通过 new FiberRootNode() 初始化出来的实例,属性也非常多, * 当前我们可以只关注其中的两个属性: * root.current: 指向到哪棵fiber树;初始化时会指向到一颗空树,因为刚开始时还没有树; * root.containerInfo: 创建当前节点时的dom节点 */ const root = createContainer( container, ConcurrentRoot, // 1 null, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError, transitionCallbacks, ); // 将DOM节点 container 标记为已被作为root使用过 // 并通过一个属性指向到fiber节点: // container['__reactContainer$'] = root.current; // root为fiber类型的节点 // 这里就形成了互相指向,root.containerInfo = container; markContainerAsRoot(root.current, container); // 获取container的真实element元素,若container是注释类型的元素,则使用其父级元素,否则直接使用container // 大概是因为注释节点无法挂载事件 const rootContainerElement: Document | Element | DocumentFragment = container.nodeType === COMMENT_NODE ? (container.parentNode: any) : container; // 绑定所有可支持的事件到 rootContainerElement 节点上 listenToAllSupportedEvents(rootContainerElement); // 使用ReactDOMRoot实例化一个对象,属性_internalRoot 指向到到 root // 并有两个方法 render() 和 unmount() return new ReactDOMRoot(root); } 我们再提炼下其中的流程: isValidContainer(container): 判断传入的 dom 节点 container 是否是个合法的挂载对象,如普通的 element 节点(如, 等),document 节点,文档片段节点等,都是合法的挂载对象;额外的,注释节点就不是一个合法的挂载对象; warnIfReactDOMContainerInDEV(container): 若 container 为 body 或已被作为 root 使用过,则在 dev 环境发出警告; const root = createContainer(container): 创建一个 FiberRootNode 类型的节点,在 React 中,存在两棵树, FiberRootNode 用来决定指向到哪棵树; markContainerAsRoot(root.current, container): 将 container 标记上,若重复使用,则发出警告; listenToAllSupportedEvents(rootContainerElement): 挂载事件,若传入的 container 是注释类型元素,则使用其父级节点挂载事件;jsx 中的诸如 onClick, onChange 等事件,并不是真的挂载当前节点上的,而是通过事件代理(又称事件委托)的方式,将事件冒泡到根节点上进行处理。 new ReactDOMRoot(root): 最终返回一个 ReactDOMRoot(root) 的实例,render()方法就是这个类的一个实例; 上面的每个函数我们都没有去关注他具体的实现,只是先看下大致的流程,避免因太多深入某一项,导致忘记大局流程,造成思维混乱。我们可以看到上面的createContainer()函数的调用链路很深,一直到最终的 FiberNode() 函数。这里我们仅了解这些函数的大致功能,后续我们会一一进行解析。 3. ReactDOMRoot() 类的实现 # ReactDOMRoot()类还是在当前的文件中:ReactDOMRoot()的实现。类的主体简单,就是将上层创建的 FiberRootNode 类型的节点放到实例的 _internalRoot 属性上。/** * 创建一个实例,并可以调用render()方法 * @param {FiberRoot} internalRoot * @constructor */ function ReactDOMRoot(internalRoot: FiberRoot) { this._internalRoot = internalRoot; } /** * render的入口 * @param {ReactNodeList} children 通过createElement或babel转换后的element结构 * element结构 { $$typeof, type, props, key, ref } * 不过这里如null, boolean等类型,也认为是有效的children类型 */ ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(children: ReactNodeList): void {}; // 卸载 ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount = function(): void {}; 这里用原型链的方式,为 ReactDOMRoot 类添加了两个方法:render() 和 unmout(); 3.1 render() 方法 # 终于讲到了 render() 方法,render() 大部分的操作都是进行参数的校验,避免开发者因之前使用 render() 方法的习惯,造成使用错误。最后调用 updateContainer() 方法来实现后续的操作。/** * render的入口 * @param {ReactNodeList} children 通过createElement或babel转换后的element结构 * element结构 { $$typeof, type, props, key, ref } * 不过这里如null, boolean等类型,也认为是有效的children类型 */ ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(children: ReactNodeList): void { const root = this._internalRoot; // FiberRootNode if (root === null) { // 若root为null,说明该树已被卸载 throw new Error('Cannot update an unmounted root.'); } // 省略一堆的参数校验 updateContainer(children, root, null, null); }; updateContainer() 函数会做很多,如会将 element 结构转为 fiber 树,并最终生成 html 节点渲染到 root.containerInfo 指定的 dom 元素中;将组件中声明的 hook 挂载到 hook 链表中。我们现在再单独看下对参数的校验,这里不影响整体功能,您也可以直接跳过。这些参数的校验,主要是为了给使用之前版本的用户进行提示,毕竟很多开发者对框架的使用有很大的惯性,当 api 的使用方式有变动时,最好给到足够的提示,可以让用户知道怎么去适配最新的使用方式:if (typeof arguments[1] === 'function') { // 第2个参数是function时,给出提示,render方法不再支持callback,而应当放在useEffect()中 // 主要是为了给使用之前版本的用户进行提示 console.error( 'render(...): does not support the second callback argument. ' + 'To execute a side effect after rendering, declare it in a component body with useEffect().', ); } else if (isValidContainer(arguments[1])) { // 若第2个参数是一个挂载dom节点,给出提示,若是通过createRoot创建然后调用render的,第2个参数不用再传入dom节点 // 主要是为了给使用之前版本的用户进行提示 // 之前是ReactDOM.render(, document.getElementById('root'));的用法,但现在不这么使用了 console.error( 'You passed a container to the second argument of root.render(...). ' + "You don't need to pass it again since you already passed it to create the root.", ); } else if (typeof arguments[1] !== 'undefined') { // root.render()只能传入一个参数 console.error('You passed a second argument to root.render(...) but it only accepts ' + 'one argument.'); } // 真实的dom元素 const container = root.containerInfo; if (container.nodeType !== COMMENT_NODE) { // 这里暂时还不没看懂 findHostInstanceWithNoPortals() 函数的原理, // 意思是container中的内容被React之外的方法移除,导致React无法正常工作 // 这里应当使用React提供的unmount()方法来清楚container中的内容 const hostInstance = findHostInstanceWithNoPortals(root.current); if (hostInstance) { if (hostInstance.parentNode !== container) { console.error( 'render(...): It looks like the React-rendered content of the ' + 'root container was removed without using React. This is not ' + 'supported and will cause errors. Instead, call ' + "root.unmount() to empty a root's container.", ); } } } 3.2 unmount() 方法 # unmount() 方法相对来说就简单很多,主要是用来清除数据、卸载 fiber 树等。ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount = function(): void { if (__DEV__) { if (typeof arguments[0] === 'function') { // 若传入了callback参数,则给出提示,要想在组件卸载时进行回调, // 请使用useEffect() console.error( 'unmount(...): does not support a callback argument. ' + 'To execute a side effect after rendering, declare it in a component body with useEffect().', ); } } const root = this._internalRoot; // FiberRootNode节点,我们在new的时候,将其给到了该属性 if (root !== null) { this._internalRoot = null; // 置为空 const container = root.containerInfo; // dom元素 flushSync(() => { // 解除root中的所有fiber节点 updateContainer(null, root, null, null); }); /** * 我们在createRoot中,将root.current给到了container属性,标记container为已使用 * container['__reactContainer$'] = root.current; * 这里我们将其解除指向: * container['__reactContainer$'] = null; */ unmarkContainerAsRoot(container); } }; 4. 总结 # 入口方法 render() 我们初步的流程大致了解了,不过有很多重要的函数都没有展开说,如 createContainer(), listenToAllSupportedEvents(), updateContainer()等等,接下来我们都会一一讲解到。
2024年10月20日
9 阅读
0 评论
0 点赞
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日
12 阅读
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日
9 阅读
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日
7 阅读
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日
9 阅读
0 评论
0 点赞
1
...
79
80
81
...
241