首页
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
leetcode 的单向链表与数组的转换
在 leetcode 的单向链表的题目中,通常会以数组的形式给出数据,导致我们在本地调试时,非常不方便。跟之前我们修改二叉树的样例一样:将 leetcode 中二叉树的数组结构转为真实的树结构。这里我们写两个转换程序,实现单向链表和数组的双向转换。在 C++ 的语言中,leetcode 官方给出的链表结构:struct ListNode { int val; ListNode *next; ListNode() : val(0), next(nullptr) { } ListNode(int x) : val(x), next(nullptr) { } ListNode(int x, ListNode *next) : val(x), next(next) { } }; 1. 数组转单向链表 # 访问单向链表通常有循环和递归两种方式,这里转换时,我们也用两种方式来实现。 1.1 循环的方式 # 采用循环的方式,最需要注意的一点是头指针的处理,头指针的指向是不能跟着循环一起移动,需要单独处理。/** * 数组转单向链表的循环方式 * @param {vector} nums 数组 * @return {ListNode*} 构建的链表 */ ListNode *vectorToListNode(vector nums){ if (nums.empty()) { return nullptr; } auto head = new ListNode(nums[0]); // 头指针 auto prev = head; for (int i = 1; i < nums.size(); i++) { auto node = new ListNode(nums[i]); // 创建一个新节点 prev->next = node; // 上一个节点的next指向到当前节点 prev = prev->next; // 将指针从上个节点移动到当前节点 } return head; } 1.2 递归的方式 # 使用递归的方式时,我这里传了一个下标过去,表示当前处理的是哪个节点。/** * 数组转单向链表的递归方式 * @param {vector} nums 数组 * @param {?int} index 数组的下标 * @return {ListNode*} 构建的链表 */ ListNode *vectorToListNode(vector nums, int index = 0){ if (index >= nums.size()) { return nullptr; } auto node = new ListNode(nums[index]); // 递归下一个节点,并用next指向到下一个节点 node->next = vectorToListNode(nums, index + 1); return node; } 上面无论是哪种转换方式,使用方式都是一样的。vector nums = {1, 2, 3, 4, 5}; auto head = vectorToListNode(nums); while (head) { cout val next; } 2. 单向链表转数组 # 链表转数组最简单的方式就是循环的方式了,直到链表的最后一个节点截止。/** * 单向链表转数组 * @param {ListNode*} head 链表的头指针 * @return {vector} 数组 */ vector ListNodeToVector(ListNode *head){ vector nums; while (head) { nums.push_back(head->val); head = head->next; } return nums; } 3. 测试 # 我们可以在 leetcode 中选择一个题目来测试下:剑指 Offer 06. 从尾到头打印链表。
2024年10月20日
5 阅读
0 评论
0 点赞
2024-10-20
js 对象中深层数据的key的扁平与展开之间的转换
实在没想好这个标题怎么起,这是一个面试题,是编写两个函数,实现下面两个 Object 的对象的互相转换。const obj = { a: { b: { d: ['d1', 'd2', 'd3'], }, c: { e: 'e1', }, }, f: [{ g: 'g1' }, [1, 2, 3]], h: null, }; const obj2 = { 'a.b.d[0]': 'd1', 'a.b.d[1]': 'd2', 'a.b.d[2]': 'd3', 'a.c.e': 'e1', 'f[0].g': 'g1', 'f[1][0]': 1, 'f[1][1]': 2, 'f[1][2]': 3, h: null, }; 可以看到,变量 obj 是每个单独的属性都是展开的;而变量 obj2 是把所有的属性都收缩起来,最后的 key 是最深的那个层级的值,数组类型的,将转为[0]这种格式的。 1. 收缩所有的 key 成字符串 # 把 obj 中所有的 key 收缩成一个字符串,像这种无限层级的结构,递归是最好的方式了。每次递归时,都把之前拼接好的 key 传递下去。 1.1 使用全局变量 # 最开始我想到的办法是使用全局变量 result,当值为基本类型时,说明递归结束,然后将 key 和最终的值,推送到 result 中。递归全部结束后,则返回这个 result。这里还要注意的是 key 的拼接: 点.不能在最前面; 若当前的类型为数组,则使用中括号[]拼接; 若当前的类型是 object,则使用点.拼接; 代码如下:const flat = (param) => { if (!param || typeof param !== 'object') { return param; } const isArray = Array.isArray(param); const result = isArray ? [] : {}; // 设置全局变量 const find = (param, parentKey = '') => { for (let key in param) { // 判断param的类型 const isArray = Array.isArray(param); // 根据类型来拼接key const curKey = isArray ? `${parentKey}[${key}]` : `${parentKey}${parentKey === '' ? '' : '.'}${key}`; const item = param[key]; if (item && typeof item === 'object') { // 若值为array或object,则继续递归 find(item, curKey); } else { // 若为普通类型,本次递归结束,推送数据 result[curKey] = item; } } }; find(param); return result; }; 我们在循环的时候,并没有区分 param 具体是什么类型,全部都用的for-in进行循环。 1.2 键值合并 # 还有一种不使用全局变量的方式,这里将递归后返回的结构与当前结构进行合并。const flat = (param, parentKey = '') => { if (!param || typeof param !== 'object') { // 达到最深的一层 result[parentKey] = param; return; } const isArray = Array.isArray(param); result = isArray ? [] : {}; // 每次的递归都重新生成一个result for (let key in param) { const curKey = isArray ? `${parentKey}[${key}]` : `${parentKey}${parentKey === '' ? '' : '.'}${key}`; const item = param[key]; if (item && typeof item === 'object') { // 将当前的result与递归后返回的数据进行合并 result = { ...result, ...flat(item, curKey) }; } else { result[curKey] = item; } } return result; }; 调用:console.log(flat(obj)); 您可以查看样例obj 类型的深层合并与展开,并实际修改数据查看效果。 2. 展开所有的 key # 这里我们进行反向的转换,将所有已合并的 key,拆解成展开的结构。我之前用了一套很麻烦的解法才实现这个功能,不过在写完第 3 节的查找功能后,又有了新的灵感,这里重新实现下。对于每个单独的 key 的操作: 拆分字符串 key,如d[0].e这种,先解析成d.[0].e,然后用split('.')把一个字符串 key 拆分到数组 keyArr 中; 循环这个 keyArr(只循环到倒数第 2 项),对数组中的每一项 item 进行处理;这里额外用一个临时变量(如 tempObj)来进行深层次的创建; 当前 tempObj[item] 是什么类型,需要通过下一个 item 才能知晓,如下一个 item 是[\d+]这种,表示当前是数组类型,否则是 object 类型;若该 tempObj[item] 对应的类型不存在,则创建; 进入到下一个循环之前,tempObj = tempObj[item]; 达到数组 keyArr 的最后一项时,将 key 对应的那个 value 给到这一项; 具体实现如下:const parse = (param) => { if (typeof param !== 'object') { return param; } const result = Array.isArray(param) ? [] : {}; /** * 获取这个key,因数组的key是用中括号[1]包括的,这里我们要获取中间的数字 * 若是纯英文字符串,则直接返回 * */ const getCurKey = (curKey) => { return curKey.startsWith('[') ? curKey.match(/\d+/)[0] : curKey; }; /** * @param {string} key * @param {any} val * */ const setKey = (key, val) => { const keyArr = key .replace(/\[(\d+)\]/g, ($1, $2) => { if ($1) { return `.[${$2}]`; } return $1; }) .split('.'); const { length } = keyArr; let i = 0; let tempObj = result; // 当前key是什么类型,还要依赖下一个key才能判断, // 如d.[0],说明d是一个数组; // 若想是d.e这种结构,说明d是一个object; while (i < length - 1) { const isArray = keyArr[i + 1].startsWith('['); // 若下一个key是以`[`开头的,我们认为当前key是一个数组 const item = getCurKey(keyArr[i]); if (!tempObj[item]) { // 若这个key还没创建 obj[item] = isArray ? [] : {}; } tempObj = tempObj[item]; i++; } // 最后的这个key不是用来创建结构的,仅用来赋值操作 // 如d[1] = 1, d['e'] = 2等 tempObj[getCurKey(keyArr[length - 1])] = val; }; for (let key in param) { setKey(key, param[key]); } return result; }; 调用:console.log(parse(obj2)); 您可以查看样例 obj 类型的深层合并与展开,并实际修改数据查看效果。我在上面的代码中,很多地方都用到了对象引用的特性,即对于数组和 object 类型这两种数据结构而言,当多个变量指向同一个地址时,改变其中变量的值,其他变量的值也会同步更新。const result = Array.isArray(param) ? [] : {}; let tempObj = result; while (i < length) { tempObj = tempObj[item]; } tempObj[getCurKey(keyArr[length - 1])] = val; 如上面 ↑ 这段代码,变量 result 要么是数组类型,要么是 Object 类型,同时变量 tempObj 又指向到了 result,即变量 tempObj 和变量 result 都指向到了同一个内存,那么改变变量 tempObj 中的数值时,变量 result 中的值也会同步修改。变量 tempObj 随着 while 循环一步步递进,就把嵌套的每一层数组都关联起来了,在给最内层数组赋值时,其实就相当于在操作变量 result。目前我是这样实现的,不过总感觉还有更好的解法,应该有可以不依赖对象引用的特性的解法,但还没想出来。 3. 查找字符串 key 对应的值 # 大致的意思,对一个 obj 类型的数据,如何获取字符串 key 对应的值。如给到一个变量 obj 和一个字符串 key:const obj = { a: { b: { d: ['d1', 'd2', 'd3'], }, c: { e: 'e1', }, }, f: [{ g: 'g1' }, [1, 2, 3]], h: null, }; const key = 'a.b.d[1]'; 这里循环和递归两种方式都可以实现,不过无论是循环还是递归,都是要把 key 拆解开的。但查找操作要比上面第 2 节中的创建操作简单的多,只要遇到不存在的 key,直接返回即可。在字符串 key 中,a.b.d[1]这种的数组下标,其实我们直接转成a.b.d.1这样更方便一些。无论是纯字符串还是数字,对 object 和数组都可以进行查找,只是找到找不到的问题罢了。而且转换之后,格式还统一了,统一处理即可。 3.1 采用 reduce() 的方式 # 很多同学喜欢用 reduce()方法,可以提高下逼格,虽然返回的结果没问题,但 reduce()是无法终止循环的,即使中间某个属性为空了,还得把所有的属性都遍历完才能结束,如:const findByReduce = (obj, key) => { if (!obj || typeof obj !== 'object') { return null; } return key .replace(/\[(\d+)\]/g, ($1, $2) => { if ($1) { // 将d[1]转为d.1格式 return `.${$2}`; } return $1; }) .split('.') .reduce((prev, curKey) => { if (prev && prev[curKey]) { return prev[curKey]; } return null; }, obj); }; 调用 findByReduce()函数:findByReduce(obj, 'a.b.d[2]'); // d3 findByReduce(obj, 'z.b.d[2]'); // null 在查找字符串z.b.d[2]中,第一个属性 z 就已经是 null 了,但我们还是要遍历完,才会有最终的返回。 3.2 普通循环方式 # 基于上面 reduce()方法无法中断的问题,这里我们可以改为 for 或 while 等循环。const findByLoop = (obj, key) => { if (!obj || typeof obj !== 'object') { return null; } const arr = key .replace(/\[(\d+)\]/g, ($1, $2) => { if ($1) { return `.${$2}`; } return $1; }) .split('.'); for (let i = 0; i < arr.length; i++) { if (obj[arr[i]]) { // 若存在该key,则向内查找 obj = obj[arr[i]]; } else { // 不存在该key,直接返回null return null; } } // 循环完毕,返回最后一个值即可 return obj; }; 调用方式与上面的一样:find(obj, 'a.b.d[1]'); // d2 3.3 递归的方式 # 如果我们经历了第 1 节和第 2 节的洗礼,这里用递归的方式就简单很多了。const findByDeep = (obj, key, curIndex = 0) => { if (!obj || typeof obj !== 'object') { return null; } const arr = key .replace(/\[(\d+)\]/g, ($1, $2) => { if ($1) { return `.${$2}`; } return $1; }) .split('.'); if (curIndex === arr.length - 1) { // 达到最后一个属性,返回该值 return obj[arr[curIndex]]; } if (obj[arr[curIndex]]) { return findByDeep(obj[arr[curIndex]], key, curIndex + 1); } return null; }; 调用:findByDeep(obj, 'f[1][0]'); // 1 4. 总结 # 这是一道很好的题目,涉及到的点很多,就我个人而言,一时半会儿没想出来解法。
2024年10月20日
2 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之搭建调试环境
我们在阅读 React 源码时,如果很生硬地去看,阅读起来可能会非常困难,不知道整个代码的流转流程是什么,某个函数的输入是什么,又返回了什么,等等。因此,我们使用 create-react-app 脚手架搭建一个建议的项目,来调试 React18 源码。在调试某个函数时,就可以直接打断点或者输出一些 log,来帮助我们理解源码。这里我已经搭建好了一个调试环境:wenzi0github/debug-react,您可以直接拉取下来使用。注意,这里要先删除yarn.lock,再执行npm i命令。因为我这里是在公司内初始化的,npm 源默认成了腾讯的内部下载源。如果您想自己搭建一下,可以顺着下面的步骤一步步来进行操作。 1. 初始化项目并弹出配置 # 首先创建一个项目:$npx create-react-app debug-react 我们需要修改很多 webpack 相关的配置,这里把配置弹出来,修改起来更方便一些。$yarn eject 2. 引入 React 源码 # 建议从官方仓库 facebook/react fork 一份到自己的名下,这样修改起来还方便一些。如我自己 fork 出来的仓库地址:wenzi0github/react。在 src 目录中引入 react 源码,大概结构如下:src react # react源码 App.js index.js 进入到 react 源码的目录,安装 react 所需要的 npm 包:$npm i #or $yarn install 我这里把 debug-react 和 fork 出来的 react 源码放到了两个 Git 仓库中,因此需要在在 debug-react 项目的.gitignore文件中,将 src/react 添加到忽略目录中。若您希望都放在一个 Git 仓库中,则可以不修改这里。 3. 修改 React 中的相关代码 # react 源码在项目中无法直接使用,这里需要稍微修改下。注意,我这里的 React 的版本是18.1.0;若是其他版本,修改方式可能会有些差异。 请注意 React 版本上的差异! 3.1 eslint 的修改 # 在.eslintrc.js中, 把 extends: ['fbjs', 'prettier'] 的数组设置为空; plugins 中的 react 注释掉; rules 中的no-unused-vars设置为 OFF; rules 中的react-internal/no-production-logging 设置为 OFF; 具体如下:// 我们忽略其他未修改的属性 module.exports = { extends: [], // ['fbjs', 'prettier'], debug-react 的需要 plugins: [ "jest", "no-for-of-loops", "no-function-declare-after-return", "react", // 'react', // debug-react 的需要 "react-internal", ], rules: { "no-unused-vars": OFF, // [ERROR, {args: 'none'}], debug-react 的需要 "react-internal/no-production-logging": OFF, // ERROR, debug-react 的需要 }, }; 后续在调试的过程,若还有其他 eslint 方面的报错,可以在这个文件里将其对应的规则关闭掉,然后重启即可。 3.2 源码的修改 # 3.2.1 packages/scheduler/index.js # 新增如下代码:export { unstable_flushAllWithoutAsserting, unstable_flushNumberOfYields, unstable_flushExpired, unstable_clearYields, unstable_flushUntilNextPaint, unstable_flushAll, unstable_yieldValue, unstable_advanceTime, unstable_setDisableYieldValue, } from "./src/forks/SchedulerMock"; 3.2.2 packages/react-reconciler/src/ReactFiberHostConfig.js # 注释掉 throw error 的代码,并新增 export 的代码:// throw new Error('This module must be shimmed by a specific renderer.'); export * from "./forks/ReactFiberHostConfig.dom"; 3.2.3 packages/shared/ReactSharedInternals.js # 注释掉 import 和 const 声明的代码,重新进行 import 引入:// import * as React from 'react'; // const ReactSharedInternals = // React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; import ReactSharedInternals from "../react/src/ReactSharedInternals"; export default ReactSharedInternals; 3.2.4 packages/react/index.js # 设置默认导出,源码中只有 export 的方式,若外部直接使用,需要用* as React这种格式导出成全局变量。React 源码中也有解释:// Export all exports so that they're available in tests. // We can't use export * from in Flow for some reason. 在 Flow 语法中,我们无法用 export * 的这种方式来导出所有方法。因此这里我们单独添加一个默认导出。// 在文件的最底部 import * as React from "./src/React"; export default React; 3.2.5 packages/react-dom/client.js # 同上面的 react 原因,这里我们修改下 ReactDOM:// 在文件的最底部 const ReactDOM = { createRoot, hydrateRoot }; export default ReactDOM; 4. debug-react 的修改 # cra 的脚手架也需要稍微修改下。配置修改对应的 commit:chore(config): update config to load react source 4.1 添加全局变量 # react 源码中有不少的全局变量,如__DEV__等,这里我们需要在config/env.js中添加上,否则会提示找不到这个全局变量。注意,我们回到了最外层的 debug-react 项目了,是修改的用yarn eject弹出的配置。我们在变量 stringified 中添加下述变量:// config/env.js const stringified = { "process.env": Object.keys(raw).reduce((env, key) => { env[key] = JSON.stringify(raw[key]); return env; }, {}), // 新增全局变量 __DEV__: true, __PROFILE__: true, __UMD__: true, __EXPERIMENTAL__: true, __VARIANT__: false, // 新增全局变量结束 }; 4.2 添加别名 alias # 修改 webpack 配置中的别名 alias,用于调整引入的 React, ReactDOM 的引用位置。修改的文件: config/webpack.config.js// config/webpack.config.js module.exports = function () { return { // 新增别名 resolve: { alias: { // Support React Native Web // https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/ "react-native": "react-native-web", // Allows for better profiling with ReactDevTools ...(isEnvProductionProfile && { "react-dom$": "react-dom/profiling", "scheduler/tracing": "scheduler/tracing-profiling", }), ...(modules.webpackAliases || {}), // 新增的 alias react: path.resolve(__dirname, "../src/react/packages/react"), "react-dom": path.resolve(__dirname, "../src/react/packages/react-dom"), shared: path.resolve(__dirname, "../src/react/packages/shared"), "react-reconciler": path.resolve( __dirname, "../src/react/packages/react-reconciler" ), scheduler: path.resolve(__dirname, "../src/react/packages/scheduler"), "react-devtools-scheduling-profiler": path.resolve( __dirname, "../src/react/packages/react-devtools-scheduling-profiler" ), "react-devtools-shared": path.resolve( __dirname, "../src/react/packages/react-devtools-shared" ), "react-devtools-timeline": path.resolve( __dirname, "../src/react/packages/react-devtools-timeline" ), // 新增的 alias 结束 }, }, }; }; 5. typescript 的加持 # 在 ts 的环境中,存在着一个type引入的问题,React 源码是用 flow 语法编写的,ts 的类型定义是单独编写的。若想在 create-react-app 脚手架中支持 typescript,会存在 React 类型推导不正确的问题,这里需要我们单独设置下。我自己这里,React 引用的 ts 类型的路径是:react/packages/react/src/__tests__/testDefinitions/React.d.ts。declare module "react" { export class Component { props: any; state: any; context: any; static name: string; constructor(props?, context?); setState(partial: any, callback?: any): void; forceUpdate(callback?: any): void; } export let PropTypes: any; export function createElement(tag: any, props?: any, ...children: any[]): any; export function createRef(): any; } 可以看到,React 里很多的方法都没有定义。因此我们需要把这里 module 的名字react改成其他的名字,然后再让 ts 自动寻找其他的 ts 类型定义。 6. 总结 # 到这里我们基本上就可以改造完毕了,启动项目就可以运行起来。$npm start 我们需要在 react 源码中调试或者输出一些 log 时,就可以直接修改了。
2024年10月20日
5 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之fiber等几个重要的数据结构
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 我们接下来的文章是进行 React 源码解析的,已默认您已很熟练使用 React,并阅读过React 的官方文档。我们在阅读 React 源码之前,先熟悉几个概念,这样读起来会顺畅很多。 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 编译的结构。同样的,我们在 React 中像下面这样写的效果是一样的:createElement('div', { onClick: handleClick }, createElement('p', null, 'hello world')); 但这种方式使用起来确实不方便。 2. createElement 是用来干嘛的 # 上面提到会将 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 转换后的 object 类似的数据,会在 render()方法中将其转为 fiber 结构。render()方法里具体怎样转换的,我们稍后再讲,这里我们只是看下 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; 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初始化出来的实例 */ this.stateNode = null; /** * 下面的return, child和sibling都是指针,用来指向到其他的fiber节点, * React会将jsx编译成的element结构,转为以fiber为节点的链表结构, * return: 指向到父级fiber节点; * child: 指向到该节点的第1个子节点; * sibling: 指向到该节点的下一个兄弟节点; * 如图所示:https://pic4.zhimg.com/80/v2-a825372d761879bd1639016e6db93947_1440w.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 链表。为什么要使用 fiber 链表?这里我们稍微了解下,后面会详细介绍 fiber 链表如何进行 diff 每个 fiber 节点的。 3.1 Stack Reconciler # 在 React 15.x 版本以及之前的版本,Reconciliation 算法采用了栈调和器( Stack Reconciler )来实现,但是这个时期的栈调和器存在一些缺陷:不能暂停渲染任务,不能切分任务,无法有效平衡组件更新渲染与动画相关任务的执行顺序,即不能划分任务的优先级(这样有可能导致重要任务卡顿、动画掉帧等问题)。Stack Reconciler 的实现。 3.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 方法可以返回多个元素(即可以返回数组); 支持异常边界处理异常; 4. 总结 # fiber 现在是整个 React 应用的基础,无论是整个结构树,还是优先级的调度,或者是 diff 对比等,都是以 fiber 为基础的。
2024年10月20日
4 阅读
0 评论
0 点赞
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日
3 阅读
0 评论
0 点赞
1
...
50
51
52
...
213