首页
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
如何给 create-react-app 添加构建时间
我们在通过流水线构建成功并发布后,想看下页面是否已更新。最简单的方式就是查看静态资源的 hash 值是否更新,但这种方式很不直观,还得记住之前的 hash 是什么。我打算修改下 create-create-app 的配置,来给页面头部添加一个构建时间的标签: 这种插入 meta 标签的方式,既不影响页面的功能,也能给开发者直观的构建时间。下面的这两种方式,都是基于 webpack 的 HtmlWebpackPlugin 插件实现的,只是实现方式上稍微有点差别。这个插件能让我们在 html 文件中读取到构建中的一些配置。这里我们主要用到了 meta 属性,表示会在 header 头中添加对应的 meta 标签:new HtmlWebpackPlugin({ meta: { buildTime: new Date().toLocaleString(), }, }); 关于该插件的其他配置,可以参考该文档:html-webpack-plugin#options。 1. 解压所有的配置 # 我们不推荐使用npm run eject来暴露所有的配置,因为解压的过程是不可逆的,解压后的配置就再也压缩不回去了;同时解压后,相关的配置无法跟着官方版本进行升级。但若您的项目已经解压开了,可以参考该方式。文件 config/webpack.config.js,然后查找new HtmlWebpackPlugin(,添加上 meta 属性(若已经有 meta 字段,直接新增即可):new HtmlWebpackPlugin({ meta: { buildTime: new Date().toLocaleString(), }, }); 添加的代码位置:在 build 后的 html 文件里,就会自动有该字段了。 如果您还想配置其他不是在 meta 中使用的属性,虽然 options 中并没有说明自定义属性,但您确实可以自定义任意属性。new HtmlWebpackPlugin({ buildIp: "127.0.0.1", }); 然后在 public/index.html 中使用: 2. 使用 craco 配置 # 我们可以使用 craco 在无需 eject 的前提下来修改配置。这里我们依然是修改 HtmlWebpackPlugin 的配置,craco.config.js:const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { webpack: { configure: (config) => { // plugins里有一堆的插件,这里我们只修改 HtmlWebpackPlugin config.plugins.forEach((plugin) => { if (plugin instanceof HtmlWebpackPlugin) { // add build time in head meta plugin.userOptions.meta = plugin.userOptions.meta || {}; // 避免meta属性可能不存在 plugin.userOptions.meta.buildTime = new Date().toLocaleString(); } }); return config; }, }, }; 然后在构建后的 index.html 文件里,就能看到 name 为 buildTime 的 meta 了。
2024年10月20日
4 阅读
0 评论
0 点赞
2024-10-20
敏捷开发的双周迭代模式
针对海量的需求,产品经理或项目经理没有相应的规划,需求是提了,那什么时候做?什么时候上线?哪些需求先做?这都是个问题。比如下面的某需求列表的截图:虽然对这些需求标记了优先级,但没有规划出具体的研发周期。 存在的问题 # 当公司或团队规模较小时,工作随意而快捷:客户早上提了需求,产品经理在黑板上写写画画,不必写正规的 PRD,直接和研发人员沟通就行;不必做回归测试,产品经理简单试试功能,晚上请研发工程师操作,产品就能上线了;不需要运维部署,速度快,效率高。这种现象在初创团队和创业公司中很常见,业务开展初期,问题多,想法多,市场变化快,没有条条框框的约束,拼的就是速度!随着公司规模变大,团队人数增多,作坊式工作方法不再适用:缺少流程制度,会让多人协作变得混乱失控;没有统一优先级裁定,会让跨部门项目引发争执;没有需求管理规范,会让业务部门抱怨产品研发部门配合不力,产品研发部门抱怨业务部门不靠谱,团队关系剑拔弩张。 需求太多:怎么排优先级,哪些需求要先做? 规划凌乱,没有目标:接收过来的需求太碎,不知道产品的目标和规划是什么,容易让一线的产品、开发等角色陷入漫无目的的状态;就是一直在工作、在开发,但对产品的长期规划不清楚、不明朗; 需求太大,开发周期太长:长时间的研发周期,容易让产品团队和研发团队疲惫,同时较长时间内看不到产出; 上线时间不明确:这里跟第 1 点是类似的,这么多需求,是全都做完上线?还是做完高优先级的就上线?还是说什么时候做完,什么时候上线? 需求饥饿问题:有些比较低优的需求,常常会被高优的需求插队,导致这些低优需求一直排不上、无限被延期;某天忽然想起这个需求了,然后又手忙脚乱地进行开发; 针对需求太大、开发周期太长的问题(第 3 点),稍微再展开说下。有的决策者认为开发一个需求也是开发,同时开发多个需求也是开发;虽然「可能」最终的上线时间是差不多的。但实际上这两种开发模式是完全不一样的,所谓“一鼓作气、再而衰、三而竭”,更长的研发周期,意味着研发同学投入的时间更长。一是容易让研发同学疲惫;二是让后续阶段的同学等待时间更长;三是更长时间内看不到产出;四是研发功能太多,每次都做出个庞然大物出来,再进行测试和上线,容易导致研发和测试同学忽略很多细节(非有意,只是事儿太多了),产生更多问题。比如一个需求的上线周期是两周(开发+测试+上线),但多个需求糅合到一起开发的周期可能是三周或者四周,甚至更长时间。两周上线一次的节奏,老板和用户更能及早地体验到新功能;但对于四周的上线周期,老板和用户则需要等待更长的时间,老板们甚至觉得这 1 个多月都在干啥了。这种情况下,如何才能保证产品研发团队高效运转?如何保证软件产品能够按计划交付?如何让用户满意?要解决这些问题,就必须从作坊式的生产模式转变为标准化、专业化的生产模式。优秀的项目管理是互联网公司在复杂环境下保证软件开发按计划推进、落地的关键,也是保障规模团队的产品研发效率和质量的核心要素。 双周迭代 # 过去版本发布没有控制节奏,有需求、缺陷完工了就安排上线,可能极端的情况是一天发几次;有时候连着做好多需求,研发好几个月再上线。上线时间不规律双周迭代,顾名思义,是以双周(两个星期)为周期,完成一个迭代周期的产品开发流程,不拖泥带水,不拉长开发周期。在双周迭代的模式里,在研发人员还在开发当前迭代的功能时,产品经理就规划好下一个迭代的需求,工作交替进行,这样能保证设计工作和开发工作无缝衔接,在一个合理的时间周期内快速实现软件产品的升级迭代。它是软件开发中最常见的软件敏捷开发方式。不过迭代的周期也不必一定要固定成双周的时间,得看团队和产品迭代的节奏,也可以实行单周迭代或月迭代。 适合什么场景 # 「双周迭代」适合什么项目和团队呢?主要是用于长期迭代,并且需求和问题比较频繁的项目。短期项目或者较长周期才有需求的,不适合迭代模式,比如一个极端的例子,公司官网几年才需要一次更新的这种,就不适合。尤其是 APP 的发布迭代,尤为明显。因为 APP 的发版比较麻烦,任何小问题的修改和调整,都得需要发版,而发版还得需要各种 APP 商店的审核,上线周期尤为漫长。比如我们就固定双周的周四封版发版,能在这个版本内开发完毕且测试完毕的,就放在这个版本迭代中;否则就放到下个版本迭代中。若自己作为乙方,本身就比较弱势,在规划迭代中的需求时,请给自己留出足够的时间,来应该甲方的各种要求。在双周迭代模式中,产品经理和研发人员能够更加聚焦当前周期内的任务,而不会被已经上线和未排期的任务产生干扰。并辅以「故事墙」、「甘特图」等功能,可以只看当前迭代周期内的任务完成情况。 如何执行 # 假如我们要执行双周迭代的制度。应该如何执行?双周迭代的开发周期是:单周开发、半周测试、后半周上线,如第 1 周开发,第 2 周的 1-3 天测试,周 4 上线。当然,每个需求的开发难度都不一样,有的可能 1 天就开发好了,有的可能需要 5-6 天的工作日;同时,也并一定当前迭代所有的需求都一起放到第二周的周 4 上线(理想情况下是一起上线)。具体问题、具体分析。但总得来说,本迭代周期内的所有需求和问题,尽量都要上线。如何执行: 产品拆分需求,将需求拆分到「单周开发、双周上线」的粒度,如果不行,就继续拆分; 按照需求的轻重缓急,将需求进行排序; 有个需求池,将拆分好的需求放到需求池中,等待评估排期; 在下一个迭代周期来临之前,产品和技术一起研讨,每双周或单周评估一次,评估下一迭代或下下迭代中,要排期开发的需求; 在进行排期时,一定要预留出部分的时间,来处理突发事件或技术需求,如甲方的个别紧急需求、线上重大 bug 等。还有一些开发自己的需求,比如一些性能优化、数据迁移、服务器部署等等。 可能存在的问题 # 前期在刚执行双周迭代模式的时候,肯定多多少少存在一些问题,比如 单个需求拆分不合理,导致开发时间过长,本次迭代内无法上线; 都是紧急需求,都想放到当前迭代中,导致该次迭代中分配的需求太多,做不完; 当前迭代的需求太少; 临上线前发现某需求有问题,是强行上线,还是下个迭代再上线; 不过几次迭代后,大家也都有经验了,在拆分需求、评估需求工期、分配任务时,就会好很多。虽然可能会有些问题,但我们总得要迈出这一步。 双周迭代真的这么神奇吗? # 有些人可能有疑惑,就区区一个「迭代」,真的能解决这些问题吗?其实任何方案只是计划或规划,最终还是要看团队的执行效果。要是所有人都不当回事儿,还是按照自己的节奏走;或者已经排在这个迭代中的需求,要么不想做,要么做的一堆 bug。那要是保持这些心态,什么开发模式都是扯。在没有规划的开发团队里。研发人员不知道产品经理都排了哪些需求,总不能每次来一个需求就扔到群里;研发人员对负责的产品的长期规划,就没有很好地认识。同理,产品经历也不知道研发团队什么时候能做这些需求,什么时候能排上期(毕竟之前已经往群里扔了好多需求了)。产品经理应当知道所有需求的轻重缓急,应当对产品有个未来的规划。如果所有需求都是紧急需求,那就相当于所有需求都不是紧急需求。其实无论是「双周迭代」,还是「单周迭代」,抑或是其他开发模式,都是为了需求可控、有计划。项目管理的核心目标,是平衡好软件产品交付中的成本、时间和质量三者之间的关系。在现实中,这三者往往不可能兼得,但我们总是希望通过各种科学的管理手段,尽量确保软件项目能够在可控的成本范围之内,符合质量的按期交付。 项目管理软件 # 我目前接触使用的几款软件,在敏捷迭代开发模式中,表现的都好挺不错。当然,还有很多其他的项目管理软件,只是本人并没有深入地了解和进行体会。 tapd # 企业微信中内置一个「tapd」的应用,tapd 是腾讯高效协作平台,凝聚腾讯多年研发协作经验。提供看板、文档、迭代计划&跟踪、产品需求规划、缺陷跟踪管理等丰富功能,帮助团队可视化工作进展、沉淀分享项目知识、提升团队协作效率。若公司本身就是在使用企业微信作为协作沟通软件,那 tapd 就更方便了。tapd 除了他的功能比较齐全而且免费外,也在企业微信的生态体系中,默认使用企业微信的账号体系,无需注册,并且在需求单、bug 单发生变更时,能够及时给相关人员发送通知。这里多一嘴,我建议各个公司,如果有条件的话,一定要更换成企业级的沟通协作软件,比如企业微信、钉钉、飞书等。因为这些企业 IM 不只是单纯的聊天沟通,他更是一个完整的协作平台。参见文章建立以企业 IM 为中心的沟通协作模式。企业微信的「tapd」应用默认是专业版,完全免费。关于迭代功能的使用,目前已足够使用。但还是缺少一个甘特图的功能,这个得需要升级到企业版才能使用。各版本功能的对比:https://www.tapd.cn/version。在 tapd 中还可以调整和增加工作流,使所有流程都清晰可控。如下图,我自己又新增了「测试中」和「待上线」两个流程。 ones # ones 是多种功能的集合,项目管理仅是众多功能中的一项。若团队中使用 ones 来做项目管理,其他功能模块也都能在这个生态中联动起来。同时,项目管理中的需求还可以跟 wiki 进行关联:像各种维度的数据的统计,它也都是有的,这里就不一一截图介绍了。也有各种插件可以来对接钉钉、飞书、企业微信等通知功能。
2024年10月20日
4 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之 useReducer 的原理
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 在浏览该文章前,请一定要先看文章React18 源码解析之 useState 的原理。不过若您看完关于 useState() 的文章后,其实 useReducer 的源码也就不用讲了。 1. useReducer() 的用法 # 我们先来看下 useReducer 的用法。useState() 实际上相当于 useReducer() 的简化版(或者定制版)。我们已经提前约定好了 dispatch()(即 setState())内部的功能。但若执行的操作比较复杂,useState() 无法满足我们的需要时,可以通过 useReducer() 来可以自定义 reducer,来实现相对复杂的状态变更。然后传给 useReducer()的第 1 个参数(第 2 个参数是 state 的初始值)。useReducer() 的 hook 接收两个参数: reducer: 执行 dispatch() 时的具体操作,该回调方法有两个参数,第 1 个是当前的状态,第 2 个是 dispatch()传入的数据;返回值即为要更新的状态; initialState: 状态的初始值; 先来看一个 useReducer() 的简单的例子:// 设置初始值 const initialState = { count: 0 }; // 自定义一个 reducer function reducer(state, action) { switch (action.type) { case 'increment': { return { count: state.count + 1 }; } case 'decrement': { return { count: state.count - 1 }; } default: { return state; } } } function Counter() { const [state, dispatch] = useReducer(reducer, initialState); return ( Count: {state.count} dispatch({ type: 'decrement' })}>- dispatch({ type: 'increment' })}>+ ); } useReducer 的返回值是一个数组,数组中包含两个元素: state:当前的状态值。 dispatch:一个用于触发状态更新的函数。当 dispatch 被调用时,会触发 reducer 函数,并传递当前状态和 action 作为参数。 按照习惯,我们一般约定,action 参数中通常有两个属性: type: 当前操作的类型; payload: 传入的数据,但不是必须的; 与 useState 相比,useReducer 的优点在于它可以管理更加复杂的状态,并且状态更新更加可控、可预测。同时,若多个 state 的变化过程一样的,还可以共用 reducer。 2. mountReducer() 的源码解析 # mountReducer()跟 mountState() 的代码几乎一样:可以看到 mountState() 中是指定了 reducer 的,而 mountReducer() 是开发者自行传入的。该 hook 的挂载过程,可以参考:hook 的初始挂载 - React18 源码解析之 useState 的原理。(偷个懒) 3. updateReducer() 的源码解析 # updateReducer()在 React 内部被调用过程中,若 useState() 的操作,则会指定 reducer 为 basicStateReducer。而若是 useReducer() 的操作,则会接收开发者传入的 reducer。该方法的源码,可以参考:updateReducer - React18 源码解析之 useState 的原理。 4. 总结 # useState() 和 useReducer() 中大部分的代码都是复用的,因此跟之前的文章内容比较重复。
2024年10月20日
5 阅读
0 评论
0 点赞
2024-10-20
React18 源码解析之 useContext 的原理
我们解析的源码是 React18.1.0 版本,请注意版本号。React 源码学习的 GitHub 仓库地址:https://github.com/wenzi0github/react。 useContext() 的这个 hook,有的同学可能使用的不太多,他的作用主要提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。更简要地的说,就是方便我们在不同的组件之间传递数据,就是类似于 redux、mobx,或者 vue 中的 vuex 等。当数据更新时,所有使用到该数据的组件都会自动更新。 1. useContext() 的使用 # 我们先来看下他的简易用法,主要是有 3 步: 在全局使用 createContext(initialValue) 创建并初始化一个 context,如名字叫 ThemeContext; 在限定范围内使用 ,它接收一个 value 属性,可将数据向下传递给消费组件。当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染; 使用上层传过来的 value,有两种方式: useContext(ThemeContext) 和 ; 1.1 全局创建 Context # 我们在全局,使用 createContext() 来创建一个 Context,这里有很多地方都要用到这个 Context,因此我们将其单独提取出来并导出。// store.js import { useContext } from 'react'; const CountContext = createContext(0); // 这个初始值可以是任意值,不过一般是在value不传入其他值时才会用到 export default CountContext; 创建出来的 Context,有两个属性:Provider 和 Consumer,我们从单词的字面意思就能了解到这两者的含义: Provider: 生产者,用于提供更新的数据; Consumer: 消费者,用来使用数据; 1.2 限定范围内监听 # 可以放置任意我们要使用的地方,不一定非得放在全局。当然如果全局都有需要的话,那就放在最顶层。在顶层使用 useState() 来存储和更新数据。更复杂一些的更新操作,可以使用 useReducer() 来自定义更新操作。这里我们在样例中仅使用 useState() 来进行数据的更新。// App.js import CountContext from './store'; function App() { const [count, setCount] = useState(1); // 顶层存储数据 return ( setValue(count + 1)}>click me ); } 使用 来限定范围,并将数据传给 value 属性。所有要使用到 value 属性中数据的组件,都应定义在 Provider 中间。value 可以接收任意类型的值,这里我们仅仅传入一个了 number 类型的,也可以传入更复杂的 object 类型的,甚至若还存在内部更新数据的需求,也可以将更新方法传进去,如:function App() { const [count, setCount] = useState(1); // 顶层存储数据 // 将 count 和 setCount 都传递进去 return ; } 但若这样直接传入的话,会存在一个频繁刷新的问题,稍后我们会展开讨论。下面 1.3 的例子,我们均以直接传入一个 count 为例。 1.3 使用或消费数据 # 数据已经在最顶层定义并传入进去,我们在需要使用 Provider 中的 value 数据时,这里有两种方式: 使用 ,它的 children 是一个函数,value 为该函数的参数,返回值即 jsx; 使用 hook useContext(Counttext),返回值即 value; 这两种方法获取到的 value,就是 的 value 属性的值,若 value 是一个复杂的结构,那还得自己摘选出自己需要的数据。这两种方法,我们一一来实现下。 1.3.1 使用 Consumer 来获取 value # 我们在需要用 value 的地方,用 标签将其包裹,然后 children 定义为一个函数即可。import CountContext from './store'; // 通过 Consumer 来获取响应数据 function CountConsumer() { return ( {value => ( count1: {value} count2: {value} )} {value => count3: {value}} ); } 若组件中有多个地方使用到 value,一种方法是将其都放到 的 children 里,再一种是可以定义多个 。 1.3.2 使用 useContext 来获取 value # 还有一种是通过 useContext() 的 hook 来获取到 value。import { useContext } from 'react'; import CountContext from './store'; // 通过 useContext() 来获取响应数据 function CountUseContext() { const value = useContext(CountContext); return count: {value}; } 这两种方法没什么优劣之分,凭个人的使用习惯即可。到这里,我们就实现了一个简单的全局状态管理。 2. 源码分析 # 在大致了解了 createContext() 和 useContext() 的用法后,我们来从源码的角度来分析下他们的原理。 2.1 createContext # createContext 源码定义在 react/src/ReactContext.js 位置。它返回一个 context 对象,提供了 Provider 和 Consumer 两个组件属性,_currentValue 会保存 context.value 值。export function createContext(defaultValue: T): ReactContext { const context: ReactContext = { $$typeof: REACT_CONTEXT_TYPE, // 将初始值给到 _currentValue _currentValue: defaultValue, _currentValue2: defaultValue, _threadCount: 0, // These are circular Provider: (null: any), Consumer: (null: any), // Add these to use same hidden class in VM as ServerContext _defaultValue: (null: any), _globalName: (null: any), }; context.Provider = { $$typeof: REACT_PROVIDER_TYPE, _context: context, }; context.Consumer = context; return context; } 从源码中可以看到,通过 Provider 和 Consumer 两个组件属性,都有 $$typeof 属性,即可以作为节点使用。 2.2 Provider 中的 value 更新时如何让消费组件进行重渲染? # 当 Provider 中的 value 属性的值发生变化时,如何让内部使用到该 value 值的组件进行重新渲染?组件的渲染与更新,是从 beginWork() 开始的,对 beginWork() 函数不太熟悉的同学,可以查看文章 React18 源码解析之 beginWork 的操作。这里我们主要聚焦在 Provider 类型上:function beginWork() { switch (workInProgress.tag) { // Provider 类型的,执行 updateContextProvider() case ContextProvider: return updateContextProvider(current, workInProgress, renderLanes); } } 接下来看下 updateContextProvider() 的执行逻辑:function updateContextProvider(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) { const providerType: ReactProviderType = workInProgress.type; const context: ReactContext = providerType._context; const newProps = workInProgress.pendingProps; const oldProps = workInProgress.memoizedProps; const newValue = newProps.value; /** * 目前context中存储的值存放到另一个栈中, * 然后再将 newValue 存储到 context._currentValue 上 * 目前这里用不到这个逻辑 */ pushProvider(workInProgress, context, newValue); const oldValue = oldProps.value; /** * 通过 Object.is() 来比较前后两个value是否发生了变化,若是 * 复杂类型的结构,每次比较时都会认为产生了更新。 * 1. 若 value 没有变化,且子节点也没有更新,则可以提前结束判断; * 2. 若 value 产生了变化,则查找该节点内所有的消费组件,然后将其标记为可更新 */ if (is(oldValue, newValue)) { // No change. Bailout early if children are the same. if (oldProps.children === newProps.children && !hasLegacyContextChanged()) { return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes); } } else { // The context value changed. Search for matching consumers and schedule // them to update. /** * 若 value 产生了变化,则查找所有使用 useContext() 的消费组件,将其标记为可更新; * 消费组件主要有两种, 和 使用 useContext() 的组件; * 每次执行到该组件时,都会重新执行,不用进行标记; * 而使用 useContext() 的组件,可能使用了多个 context,则需要判断该组件中使用 * 了这各产生更新的 context ,若能匹配上,则将该组件标记为可更新; * 这里只匹配使用了 useContext() 的 hook 的组件; */ propagateContextChange(workInProgress, context, renderLanes); } const newChildren = newProps.children; /** * 渲染该fiber节点的子节点, * 关于该方法的详细解读,可以参考下面的文章 * https://www.xiabingbao.com/post/react/reconcile-children-fiber-riezuz.html */ reconcileChildren(current, workInProgress, newChildren, renderLanes); return workInProgress.child; } 的子组件中,用的组件用了一个或者多个 context,怎么判断哪些子组件需要更新呢?流程就走到了propagateContextChange(workInProgress, context, renderLanes) 中的 propagateContextChange_eager(workInProgress, context, renderLanes)。// packages/react-reconciler/src/ReactFiberNewContext.old.js /** * 查找当前 子组件中,所有用到了 context 的组件,并将其标记为待更新 */ function propagateContextChange_eager(workInProgress: Fiber, context: ReactContext, renderLanes: Lanes): void { let fiber = workInProgress.child; if (fiber !== null) { // Set the return pointer of the child to the work-in-progress fiber. fiber.return = workInProgress; } while (fiber !== null) { let nextFiber; /** * 每次调用 useContext(context) 时,都会将使用的 context,放到 fiber 节点的 dependencies 属性上。 * 同样的,若该 fiber 节点有 dependencies 属性,则必然至少挂载了一个 context,然后我们在这个链表上查 * 找对比传过来的 context,若能找得到,则将该组件标记为待更新; * * dependencies 中的 context 如何挂载的,我们在后面的2.3小节会讲解到。 */ const list = fiber.dependencies; if (list !== null) { nextFiber = fiber.child; /** * 从 context 链表的第1个开始匹配,匹配到了则标记 */ let dependency = list.firstContext; while (dependency !== null) { // Check if the context matches. /** * 从第1个 context 开始查找,若能匹配上 */ if (dependency.context === context) { // Match! Schedule an update on this fiber. if (fiber.tag === ClassComponent) { // Schedule a force update on the work-in-progress. /** * 若这是 class 组件,则设置为强制更新 */ const lane = pickArbitraryLane(renderLanes); const update = createUpdate(NoTimestamp, lane); update.tag = ForceUpdate; // TODO: Because we don't have a work-in-progress, this will add the // update to the current fiber, too, which means it will persist even if // this render is thrown away. Since it's a race condition, not sure it's // worth fixing. // Inlined `enqueueUpdate` to remove interleaved update check const updateQueue = fiber.updateQueue; if (updateQueue === null) { // Only occurs if the fiber has been unmounted. } else { const sharedQueue: SharedQueue = (updateQueue: any).shared; const pending = sharedQueue.pending; if (pending === null) { // This is the first update. Create a circular list. update.next = update; } else { update.next = pending.next; pending.next = update; } sharedQueue.pending = update; } } // 设置该 fiber 的更新优先级 fiber.lanes = mergeLanes(fiber.lanes, renderLanes); const alternate = fiber.alternate; if (alternate !== null) { alternate.lanes = mergeLanes(alternate.lanes, renderLanes); } // 将该 fiber 节点及所有的父级节点标记为待更新 scheduleContextWorkOnParentPath(fiber.return, renderLanes, workInProgress); // Mark the updated lanes on the list, too. list.lanes = mergeLanes(list.lanes, renderLanes); // Since we already found a match, we can stop traversing the // dependency list. break; } // 一直在单链表中查找 context,直到找到或者到结尾 dependency = dependency.next; } } else if (fiber.tag === ContextProvider) { // Don't scan deeper if this is a matching provider /** * 若这里也是 节点,并且跟刚才的 context 所在的 Provider 是同一个组件, * 则停止寻找。因为消费组件使用到的 context 的值,是距离它最近的那个 ; * 当前 节点内的所有组件,是依赖当前的节点,而不是更外层的,因此更外层 * 的 查找到这里,即可查找。 * 若不是相同的 context ,则可以继续查找。 */ nextFiber = fiber.type === workInProgress.type ? null : fiber.child; } else if (fiber.tag === DehydratedFragment) { // If a dehydrated suspense boundary is in this subtree, we don't know // if it will have any context consumers in it. The best we can do is // mark it as having updates. // 这里主要是同构支出渲染的方式中出现,暂时不考虑 } else { // Traverse down. /** * 若当前节点没有使用任何的 useContext(),则继续查找 */ nextFiber = fiber.child; } /** * fiber节点的遍历顺序,先子节点,然后兄弟节点,最后回到父级节点 */ if (nextFiber !== null) { // Set the return pointer of the child to the work-in-progress fiber. nextFiber.return = fiber; } else { // No child. Traverse to next sibling. nextFiber = fiber; while (nextFiber !== null) { if (nextFiber === workInProgress) { // We're back to the root of this subtree. Exit. // 已经遍历完当前 workInProgress 下所有的子节点,直接退出 nextFiber = null; break; } const sibling = nextFiber.sibling; if (sibling !== null) { // Set the return pointer of the sibling to the work-in-progress fiber. sibling.return = nextFiber.return; nextFiber = sibling; break; } // No more siblings. Traverse up. nextFiber = nextFiber.return; } } fiber = nextFiber; } } 整个的流程比较长,我们再稍微总结梳理下: 所在的组件,被 useState 或者 useReducer,或者外层的属性等,进行渲染更新时,Provider 内就会对比新旧的 value 是否相同,若不相同,则查找该组件内所有使用到该 context 的子组件进行更新; 以先子节点、次之是兄弟节点、最后是父级的顺序,查找那些使用到了 context 的组件,将其标记为待更新;某些组件可能使用了多个的 context,这里只查找某个具体的 context; 上面的更新标记,只是对使用了 useContext() 的 hook 或者类组件进行标记。但 类型的组件并没有在这里处理。 2.2 Consumer # 消费或者使用 Provider 中的 value,一般是有两种方式,其中一种就是 组件,children 是一个方法,接受到最新的 value,然后返回 ReactCode。 组件中并没有对新旧 value 进行判断对比等,每次都会执行。/** * 组件的渲染 */ function updateContextConsumer(current: Fiber | null, workInProgress: Fiber, renderLanes: Lanes) { let context: ReactContext = workInProgress.type; const newProps = workInProgress.pendingProps; const render = newProps.children; prepareToReadContext(workInProgress, renderLanes); // 解析到这里时,读取 context 的最新值 const newValue = readContext(context); if (enableSchedulingProfiler) { markComponentRenderStarted(workInProgress); } let newChildren; /** * 每次都直接传入最新的value,然后执行 */ newChildren = render(newValue); if (enableSchedulingProfiler) { markComponentRenderStopped(); } // React DevTools reads this flag. workInProgress.flags |= PerformedWork; reconcileChildren(current, workInProgress, newChildren, renderLanes); return workInProgress.child; } 不过用 的开发者确实不太多了,很多就直接用下面的 hook 的写法了。 2.3 useContext() 的原理 # 大部分 hook 都会根据 mount 阶段和 update 阶段,分成两个 hook 来执行,而 useContext() 这个 hook,两个阶段内部使用的都是同一个 hook: readContext()。有些同学可能会有疑惑,若 mount 阶段和 update 阶段使用的是同一个方法,会不会造成 hook 的多次挂载?其实并不会,React 内部会在每次执行完该函数,在 commit 阶段,会将挂载的所有 useContext() 的 hook 进行清空。export function readContext(context: ReactContext): T { const value = isPrimaryRenderer ? context._currentValue : context._currentValue2; if (lastFullyObservedContext === context) { // Nothing to do. We already observe everything in this context. } else { // 根据 context 和 value 创建出一个链表的节点 const contextItem = { context: ((context: any): ReactContext), memoizedValue: value, next: null, }; /** * 将节点挂载到 dependencies 中的 firstContext 的链表上, * 1. 若之前链表为空,说明这是第1个节点,直接放到 dependencies 上; * 2. 若链表上已经有节点了,直接在节点的后面进行拼接; * 这样多次执行 useContext(context) 后,就会在 firstContext 上形成链表, * 那在决定是否要将该组件标记为更新时,也是在 firstContext 链表上查找对应的 context。 */ if (lastContextDependency === null) { // This is the first dependency for this component. Create a new list. lastContextDependency = contextItem; currentlyRenderingFiber.dependencies = { lanes: NoLanes, firstContext: contextItem, }; if (enableLazyContextPropagation) { currentlyRenderingFiber.flags |= NeedsPropagation; } } else { // Append a new context item. lastContextDependency = lastContextDependency.next = contextItem; } } // 返回最新的值 return value; } 3. 总结 # 到这里,我们对源码的分析,基本已经结束了。对于一些小型项目的状态维护,createContext + useContext 其实就能满足我们大致的需求。若是一些大型项目或者对性能要求比较高的,就要使用到成熟的状态管理工具了,如 redux、mobx、recoil 等。
2024年10月20日
3 阅读
0 评论
0 点赞
2024-10-20
uniapp 中 checkbox 中的 checked 不生效的方案
我在使用 uniapp 开发小程序的过程中,遇到了小小的问题。当我勾选 checkbox 后,我会去进行一些校验,若校验没有通过,需要再取消这次的勾选。如下: 选中 export default { data() { return { checked: false, }; }, methods: { checkboxChange(event) { if (Array.isArray(event.detail.value) && event.detail.value.length) { // 选中的操作 checkOpenId().then((isValid) => { if (!isValid) { // 不生效 this.checked = false; } }); } else { this.checked = false; } }, }, }; 但我不管怎么设置他的checked属性都不管用。复选框组件(checkbox 组件) 的问题,因为复选框组件没有 @change 事件,checkbox-group 组件拥有 @change 事件,事件返回的结果是当前复选框组中已经勾选的值,但并未同步修改 checked 属性。该组件是直接切换的组件状态,然后触发的 change 事件,并没有同步更新 checked 属性所绑定的值。所以开关组件默认 checked 属性为 true 的时候,点击组件本身会将组件的状态切换为关,但是 checked 属性的并没有改变。解决方案就是在 @change 的事件中,首先将 checked 设置为 true,然后再想设置成 false 的时候,就可以生效了。export default { data() { return { checked: false, }; }, methods: { checkboxChange(event) { if (Array.isArray(event.detail.value) && event.detail.value.length) { // 选中的操作 this.checked = true; // 先将其设置true checkOpenId().then((isValid) => { if (!isValid) { this.checked = false; } }); } else { this.checked = false; } }, }, };
2024年10月20日
3 阅读
0 评论
0 点赞
1
...
56
57
58
...
213