状态管理工程化

状态管理是一个前端界老生常谈的话题了,所有前端框架的发展历程中都离不开状态管理的迭代与更替,对于react来说呢,整个状态管理的发展也随着react架构的变更和新特性的加入而不停的做调整,作为一个一起伴随react成长了快5年的开发者,经历过reflux、redux、mobx,以及其他redux衍生方案dva、mirror、rematch等等后,我觉得它们都不是我想要的状态管理的终极形态,所以为了打造一个和react结合得最优雅、使用起来最简单、运行起来最高效的状态管理方案,踏上了追梦旅途。 为何需要状态管理 为何需要在前端引用里引入状态管理,基本上大家都达成了共识,在此我总结为3点: 随着应用的规模越来越大,功能越来越复杂,组件的抽象粒度会越来越细,在视图中组合起来后层级也会越来越深,能够方便的跨组件共享状态成为迫切的需求。 状态也需要按模块切分,状态的变更逻辑背后其实就是我们的业务逻辑,将其抽离出来能够彻底解耦ui和业务,有利于逻辑复用,以及持续的维护和迭代。 状态如果能够被集中的管理起来,并合理的派发有利于组件按需更新,缩小渲染范围,从而提高渲染性能 已有状态管理方案现状 redux 遵循react不可变思路的状态管理方案,无论从git的star排名还是社区的繁荣度,首推的一定是redux这个react界状态管理一哥,约束使用唯一路径reducer纯函数去修改store的数据,从而达到整个应用的状态流转清晰、可追溯。 image.png mbox 遵循响应式的后期之秀mbox,提出了computed、reaction的概念,其官方的口号就是任何可以从应用程序状态派生的内容都应该派生出来,通过将原始的普通json对象转变为可观察对象,我们可以直接修改状态,mbox会自动驱动ui渲染更新,因其响应式的理念和vue很相近,在react里搭配mobx-react使用后,很多人戏称mobx是一个将react变成了类vue开发体验的状态管理方案。 image.png 当然因为mbox操作数据很方便,不满足大型应用里对状态流转路径清晰可追溯的诉求,为了约束用户的更新行为,配套出了一个mobx-state-tree,总而言之,mobx成为了响应式的代表。 其他 剩下的状态管理方案,主要有3类。 一类是不满足redux代码冗余啰嗦,接口不够友好等缺点,进而在redux之上做2次封装,典型的代表国外的有如rematch,国内有如dva、mirror等,我将它们称为redux衍生的家族作品,或者是解读了redux源码,整合自己的思路重新设计一个库,如final-state、retalk、hydux等,我将它们称为类redux作品。 一类是走响应式道路的方案,和mobx一样,劫持普通状态对象转变为可观察对象,如dob,我将它们称为类mobx作品。 剩下的就是利用react context api或者最新的hook特性,主打轻量,上手简单,概念少的方案,如unstated-next,reactn、smox、react-model等。 我心中的理想方案 上述相关的各种方案,都各自在一定程度上能满足我们的需求,但是对于追求完美的水瓶座程序猿,我觉得它们终究都不是我理想的方案,它们或小而美、或大而全,但还是不够强,不够友好,所以决定开始自研状态管理方案。 我知道小和 美、全、强本身是相冲突的,我能接受一定量的大,gzip后10kb到20kb都是我接受的范围,在此基础上,去逐步地实现美、全、强,以便达到以下目的,从而体现出和现有状态管理框架的差异性、优越性。 让新手使用的时候,无需了解新的特性api,无感知状态管理的存在,使其遁于无形之中,仅按照react的思路组织代码,就能享受到状态管理带来的福利。 让老手可以结合对状态管理的已有认知来使用新提供的特性api,还原各种社区公认的最佳实践,同时还能向上继续探索和提炼,挖掘状态管理带来的更多收益。 在react有了hook特性之后,让class组件和function组件都能够享有一致的思路、一致的api接入状态管理,不产生割裂感。 在保持以上3点的基础上,让用户能够使用更精简且更符合思维直觉的组织方式书写代码,同时还能够获得巨大的性能提升收益。 为了达成以上目标,立项concent,将其定义为一个可预测、零入侵、渐进式、高性能的增强型状态管理方案,期待能把他打磨成为一个真真实实让用户用起来感觉到美丽、全面、强大的框架。 说人话就是:理解起来够简单、代码写起来够优雅、工程架构起来够健壮、性能用起来够卓越…… ^_^ concent.png 可预测 react是一个基于pull based来做变化侦测的ui框架,对于用户来说,需要显式的调用setState来让react感知到状态变化,所以concent遵循react经典的不可变原则来体现可预测,不使用劫持对象将转变为可观察对象的方式来感知状态变化(要不然又成为了一个类mobx……), 也不使用时全局pub&sub的模式来驱动相关视图更新,同时还要配置各种reselect、redux-saga等中间件来解决计算缓存、异步action等等问题(如果这样,岂不是又迈向了一个redux全家桶轮子的不归路….. ) 吐槽一下:redux粗放的订阅粒度在组件越来越多,状态越来越复杂的时候,经常因为组件订阅了不需要的数据而造成冗余更新,而且各种手写mapXXXToYYY很烦啊有木有啊有木有,伤不起啊伤不起…… 零入侵 上面提到了期望新手仅按照react的思路组织代码,就能够享受到状态管理带来的福利,所以必然只能在setState之上做文章,其实我们可以把setState当做一个下达渲染指令重要入口(除此之外,还有forceUpdate)。 setState,下达更新指令 仔细看看上图,有没有发现有什么描述不太准确的地方,我们看看官方的setState函数签名描述: 代码语言:txt AI代码解释 setState<K extends keyof S>( state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null), callback?: () => void ): void; 通过签名描述,我们可以看出传递给setState的是一个部分状态(片段状态),实际上我们在调用setState也是经常这么做的,修改了谁就传递对应的stateKey和值。 ...

August 7, 2025

代码分割

什么是代码分割 代码分割是一种将代码分割成多个小块的方式,然后按需加载或并行加载所需的块的技术。代码分割可以用于减少应用程序的初始加载时间或将代码切割成可按需加载的块,从而减少应用程序所需的总体代码量。 为什么要使用代码分割 在大型应用程序中,将所有代码放在一个文件中会导致应用程序加载时间过长,影响用户体验。为了解决这个问题,我们需要将代码分割成小块,然后按需加载或并行加载所需的块。 代码分割的方式 1. 动态导入 import import()是 JavaScript 中的一个动态导入语法,它允许在运行时异步加载模块。它返回一个 Promise,该 Promise 在模块加载完成后被解析为一个包含导出内容的模块对象。 使用 import()的语法如下: import(modulePath) .then((module) => { // 使用导入的模块 }) .catch((error) => { // 处理错误 }); 在这个语法中,modulePath 是一个字符串,用于指定要加载的模块路径。它可以是相对路径或绝对路径,也可以是一个包名。 当调用 import(modulePath)时,它会返回一个 Promise。这个 Promise 会在模块加载完成后被解析为一个包含导出内容的模块对象。你可以使用.then()方法来处理解析后的模块对象,并在其中使用导入的模块。 如果模块加载失败,Promise 会被拒绝,并触发.catch()方法中的错误处理逻辑。 注意事项 import()只能在模块的顶层作用域中使用,不能在函数内部或条件语句中使用。这是因为 import()是静态解析的,它在代码加载时就会执行,而不是在运行时。 另外,import()可以与其他语法结合使用,例如动态模块路径和对象解构。 动态模块路径: const modulePath = "./myModule"; import(modulePath) .then((module) => { // 使用导入的模块 }) .catch((error) => { // 处理错误 }); 在这个示例中,modulePath 是一个变量,它的值在运行时确定。这样可以根据需要动态加载不同的模块。 对象解构: import("./myModule") .then(({ myFunction, myVariable }) => { // 使用导入的函数和变量 myFunction(); console.log(myVariable); }) .catch((error) => { // 处理错误 }); 2. React.lazy React.lazy 是 React 16.6 版本引入的一个特性,它可以让你以动态的方式进行代码拆分(code splitting)。通过 React.lazy,你可以延迟加载(lazy load)一个组件,只有在需要时才会加载该组件,从而提高应用程序的性能。 React.lazy 的用法如下: ...

December 17, 2024

实体字符(转义字符)

HTML 的实体字符(HTML Entities)是用于显示某些特殊字符或符号的编码形式。 HTML 中某些符号具有特殊含义,例如 <, >, & 等字符不能直接在网页内容中使用,所以需要使用实体字符来表示它们。 常见的 HTML 实体字符 空格和控制字符 &nbsp;:空格字符 (non-breaking space) &nbsp; <!-- 表示一个空格 --> &lt;:小于号 < &lt;div&gt; <!-- 表示 <div> --> &gt;:大于号 > &gt;/div&gt; <!-- 表示 </div> --> &amp;:与符号 & &amp; <!-- 表示 & --> &quot;:双引号 " &quot;Hello&quot; <!-- 表示 "Hello" --> &apos;:单引号 ' &apos;Hello&apos; <!-- 表示 'Hello' --> 数学符号 &plus;:加号 + &minus;:减号 − &times;:乘号 × &divide;:除号 ÷ 货币符号 &dollar;:美元符号 $ &euro;:欧元符号 € &pound;:英镑符号 £ &yen;:日元符号 ¥ 其他常见符号 &copy;:版权符号 © &reg;:注册商标符号 ® &trade;:商标符号 ™ &sect;:章节符号 § &deg;:度数符号 ° 希腊字母 &alpha;:α &beta;:β &gamma;:γ &delta;:δ 为什么需要实体字符? 避免 HTML 解析冲突:例如 <, >, & 等符号在 HTML 中有特定的用途,用实体字符可以避免冲突。 显示特殊符号:例如版权符号、商标符号或货币符号等,不能直接在键盘上输入,使用实体字符可以确保正确显示。 常见考点 1. HTML 实体字符的定义与作用 什么是 HTML 实体字符?为什么需要使用它们? 实体字符用于在 HTML 中显示特殊符号(如 <, >, &, " 等)或不可见字符(如空格 、换行符等),避免与 HTML 语法冲突。 2. 常见的 HTML 实体字符 &lt;:小于号 < &gt;:大于号 > &amp;:与符号 & &quot;:双引号 " &apos;:单引号 ' &nbsp;:不间断空格 3. Unicode 和 ASCII 编码的实体字符 HTML 支持通过 &#xNNNN;(十六进制)或 &#NNNN;(十进制)表示 Unicode 字符。例如: &#x2665; 或 &#9829; 表示 ♥ 考察点可能涉及通过实体字符显示 Unicode 表情符号、语言字符等。 4. 转义字符 在 HTML 中如何使用实体字符进行转义? 为什么需要转义符号(如 <, >, &)?特别是在 HTML 元素内嵌 JavaScript 或样式时需要避免歧义。 安全性方面的考量,尤其是防止 XSS(跨站脚本攻击)。 5. 空白字符 &nbsp;(不间断空格):防止自动换行或保留多个空格。 &ensp; 和 **&emsp;**:半角和全角空格符号。 实体空白符在排版和布局中的应用(如对齐、占位)。 6. 其他常见实体字符 货币符号:如 &yen;(¥)、&euro;(€)。 数学符号:如 &plusmn;(±)、&times;(×)、&divide;(÷)。 特殊符号:如 &copy;(©)、&reg;(®)、&trade;(™)。 7. HTML5 实体字符扩展 HTML5 中新增的一些实体字符,如: **&le;**(≤)和 &ge;(≥) &bull;(•)用于列表符号 &hellip;(…)省略号 8. 实体字符与浏览器兼容性 浏览器如何处理不支持的实体字符? 考察点可能涉及浏览器对不同编码的支持,以及如何确保兼容性。 9. HTML 实体字符与国际化 如何通过实体字符支持多语言文本? 考察 Unicode 与实体字符在国际化中的应用,如如何使用实体字符表示非 ASCII 字符。 10. HTML 实体字符在输入表单中的应用 在表单中,如何防止特殊字符输入导致 HTML 注入攻击? 使用实体字符保护表单数据的完整性和安全性。

October 15, 2024

单位

说起CSS单位,我们最熟悉的可能就是像素单位(px),它是一个绝对单位,也就是说一个10px的文字,放在哪里都是一样大的。单位可以影响颜色、距离、尺寸等一系列的属性。CSS中单位的形式有很多种,下面就分别来看看这些单位。 1. 相对单位 相对单位就是相对于另一个长度的长度。CSS中的相对单位主要分为两大类: 字体相对单位,他们都是根据font-size来进行计算的。常见的字体相对单位有:em、rem、ex、ch; 视窗相对单位,他们都是根据视窗大小来决定的。常见的视窗相对单位有vw、vh、vmax、vmin。 下面就来看看这些常见的CSS单位。 (1)em 和 rem em是最常见的相对长度单位,适合基于特定的字号进行排版。根据CSS的规定,1em 等于元素的font-size属性的值。 em 是相对于父元素的字体大小进行计算的。如果当前对行内文本的字体尺寸未进行显示设置,则相对于浏览器的默认字体尺寸。当DOM元素嵌套加深时,并且同时给很多层级显式的设置了font-size的值的单位是em,那么就需要层层计算,复杂度会很高。 当然,上面的这个说法是不严谨 的。来看一个例子: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style> .parent { width: 300px; height: 300px; font-size: 20px; } .child { border: 1em solid ; } </style> </head> <body> <div class="parent"> <div class="child"> 子元素 </div> </div> </body> </html> 这里给父元素设置了字体大小为20px,然后给子元素的border宽度设置为1em,这时,子元素的border值为20px,确实是相对于父元素的字体大小设置的: 那如果我们给子元素的字体设置为30px: .child { font-size: 30px; border: 1em solid ; } 这时可以看到,子元素的边框宽度就是30px,它是相对自己大小进行计算的: 所以,可以得出结论:如果自身元素是没有设置字体大小的,那么就会根据其父元素的字体大小作为参照去计算,如果元素本身已经设置了字体,那么就会基于自身的字体大小进行计算。 em单位除了可以作用于 font-size之外,还可以运用于其他使用长度的属性,比如border-width、width、height、margin、padding、text-shadow等。 所以,em的使用还是比较复杂的,它可能会继承任意一级父元素的字体大小。需要谨慎使用。 rem相对于em就简单了很多,它是根据页面的根元素(根元素)的字体大小来计算的。来对上面的例子进行修改: .child { font-size: 30px; border: 1rem solid ; } html { font-size: 25px; } 效果如下,可以看到,边框的长度变成了25px,它是根据根元素html的字体大小计算的: ...

October 10, 2024

mixin-混入

1. 什么是Mixin? 想要使用一个事物或者工具,我们首要先了解它是什么,这样我们才好对症下药。 其实Mixin不是Vue专属的,可以说它是一种思想,也可以说它就是混入的意思,在很多开发框架中都实现了Mixin(混入),我们这里主要讲解的是Vue中的Mixin。 老规矩,先看官方文档。 官方解释: 混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。 官方的解释通常都是晦涩难懂的,因为要专业和准确嘛! 我们可以用我们自己容易理解的话来说说Vue中的Mixin是什么。 民间解释: 将组件的公共逻辑或者配置提取出来,哪个组件需要用到时,直接将提取的这部分混入到组件内部即可。这样既可以减少代码冗余度,也可以让后期维护起来更加容易。 这里需要注意的是:提取的是逻辑或配置,而不是HTML代码和CSS代码。其实大家也可以换一种想法,mixin就是组件中的组件,Vue组件化让我们的代码复用性更高,那么组件与组件之间还有重复部分,我们使用Mixin在抽离一遍。 2. Mixin和Vuex的区别? 上面一点说Mixin就是一个抽离公共部分的作用。在Vue中,Vuex状态管理似乎也是做的这一件事,它也是将组件之间可能共享的数据抽离出来。两者看似一样,实则还是有细微的区别,区别如下: Vuex公共状态管理,如果在一个组件中更改了Vuex中的某个数据,那么其它所有引用了Vuex中该数据的组件也会跟着变化。 Mixin中的数据和方法都是独立的,组件之间使用后是互相不影响的。 3. 如何使用? 我们了解了Mixin的概念,那么如何使用它呢?这才是我们的重点。 3.1 准备工作 接下来我们的mixin就放在Vue2.x的脚手架项目中演示。 利用Vue-cli初始化一个最简单的项目: 3.1 mixin定义 定义mixin也非常简单,它就是一个对象而已,只不过这个对象里面可以包含Vue组件中的一些常见配置,如data、methods、created等等。 在我们的项目src目录下新建mixin文件夹,然后新建index.js文件,该文件存放我们的mixin代码。 代码如下: // src/mixin/index.js export const mixins = { data() { return {}; }, computed: {}, created() {}, mounted() {}, methods: {}, }; 可以看到我们的mixin非常的简单,主要包含了一个Vue组件的常见的逻辑结构。 接下来让我们在mixin中简单的写点东西,代码如下: export const mixins = { data() { return { msg: "我是小猪课堂", }; }, computed: {}, created() { console.log("我是mixin中的created生命周期函数"); }, mounted() { console.log("我是mixin中的mounted生命周期函数"); }, methods: { clickMe() { console.log("我是mixin中的点击事件"); }, }, }; 3.2 局部混入 我们的公共mixin定义好后,最重要就是如何使用它。根据不同的业务场景,我们可以分为两种:局部混入和全局混入。顾名思义,局部混入和组件的按需加载有点类似,就是需要用到mixin中的代码时,我们再在组件章引入它。全局混入的话,则代表我在项目的任何组件中都可以使用mixin。 ...

October 5, 2024

defineProperty 与 proxy

前言 我们或多或少都听过“数据绑定”这个词,“数据绑定”的关键在于监听数据的变化,可是对于这样一个对象:var obj = {value: 1},我们该怎么知道 obj 发生了改变呢? definePropety ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。 语法 Object.defineProperty(obj, prop, descriptor) 参数 obj: 要在其上定义属性的对象。 prop: 要定义或修改的属性的名称。 descriptor: 将被定义或修改的属性的描述符。 举个例子: var obj = {}; Object.defineProperty(obj, "num", { value : 1, writable : true, enumerable : true, configurable : true }); // 对象 obj 拥有属性 num,值为 1 虽然我们可以直接添加属性和值,但是使用这种方式,我们能进行更多的配置。 函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符。 两者均具有以下两种键值: configurable 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,也能够被删除。默认为 false。 enumerable 当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。 数据描述符同时具有以下可选键值: value 该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。 writable 当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false。 存取描述符同时具有以下可选键值: get 一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。 set 一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。 值得注意的是: ...

October 2, 2024

事件循环

单线程的 JavaScript 同步与异步 JavaScript 在设计之初是作为浏览器脚本语言,主要用于与用户进行页面交互和操纵 DOM。因此,为避免由不可预测的用户操作可能带来的复杂的并发问题,JavaScript 只能设计成单线程的,这也是这门语言的核心特征之一。 JS 的宿主环境通过仅提供一个线程运行 JS 来保证 JS 代码的单线程运行。 单线程意味着 JS 引擎在同一时间只能做一件事情,即同步地执行代码。但在代码执行过程中不可避免地会遇到一些无法立即执行的任务,例如: 计时器到达时间后要执行的任务 — setInterval、setTimeout 网络请求完成后要执行的任务 — XMLHttpRequest、Fetch 监听到用户操作后要执行的任务 — addEventListener … … … 如果让执行 JS 的线程去处理这些任务,就会导致该线程处于长期阻塞的状态,而宿主环境中的 JS 执行线程往往还承担着极其重要的工作。例如,在浏览器中,如果执行 JS 的线程长期阻塞,就会导致浏览器卡死! 为避免上述情况发生,JS 的宿主环境使用了异步的方式来处理这种无法立即执行的任务。 当遇到异步任务时,宿主环境会将其交给其他线程处理,执行 JS 的线程则会立即结束当前任务转而去执行后续代码。 事件循环 事件循环是宿主环境处理 JS 异步操作,让其能够非阻塞式运行的机制。 不同宿主环境对事件循环的实现方式有所不同,不过在核心机制上大同小异。 接下来,本文将详细描述 JavaScript 在浏览器和 Node.js 这两个宿主环境中的事件循环机制。 浏览器事件循环 浅谈浏览器 聊浏览器的事件循环之前,我们先说说浏览器本身。 现代浏览器是一个多进程多线程的应用程序,内部工作极其复杂,其程度直逼操作系统。它拥有数个功能模块,为避免单个模块崩溃牵连其他模块,导致连锁反应,使浏览器彻底崩溃。浏览器在启动时,会开启多个进程,把不同的功能模块放在不同的进程里。 浏览器进程众多,其中比较重要的有: 📚 浏览器进程 浏览器进程是浏览器的主进程,无论打开多少浏览器窗口,它仅有一个。 它主要负责浏览器界面显示、用户交互和进程管理。 这里说的界面和交互,不是视窗内的网站界面。而是指浏览器本身自带的部分,如导航栏、书签栏、刷新按钮等。 刚打开浏览器的时候只有一个浏览器进程,其他进程都是它创建的。 📚 网络进程 网络进程主要负责处理网站的数据请求和响应,通常情况下,它与渲染进程的交互最为密切。每当网站需要进行资源请求,渲染进程就会将任务交给网络进程处理,网络进程取得响应结果后再返回给渲染进程。 网络进程内部会开启多个线程,以实现多网络请求的异步化处理。 📚 渲染进程 渲染进程负责控制和显示视窗部分(网站页面)的所有内容,主要是解析 HTML、CSS、JS 和其他资源,并生成渲染树、执行布局和绘制等操作。 ...

October 1, 2024

react 状态提升

在开始介绍状态提升之前,我想先介绍一下 Prop Drilling(属性钻取)的概念。 什么是Prop Drilling ? 那么,什么是状态提升呢?我们都知道 React 中的prop drilling(属性钻取)。prop drilling意味着从父组件向子组件传递一些 props。通过这种方式,我们可以在不同的组件之间共享数据或函数。可以使用 prop 传递任何类型的数据。 在上面的示例代码中,你可以看到,Home 组件具有一个名为 name 的状态,并且她与子组件共用 name 变量。在 React 中,只能将数据从父组件传递到子组件。 什么是 State Lifting 一般来说,状态提升允许你将数据从子组件移动到父组件,与Props Drilling不同,你可以在父组件使用子组件的状态值。这是不是很疯狂? 在 React 生态中,我们知道数据只能从父组件流向子组件,但现在将学习如何让数据反过来流动。 但是,为什么要这么做? 我们可以在父组件中定义状态并将其以 prop 的方式传递给子组件,但是这么做有可能会导致性能损失和代码复杂度提升。在我之前的博客中有提到:保持状态本地化很重要。因为当状态更新时,组件以及该组件的子组件都会重新渲染。 此外对于代码复杂性,对于如 Modal 这样的组件,选择在子组件中声明状态而不是在父组件中声明状态,将能够使代码更易于阅读。使用这种方法,你不必在每个父组件中声明状态。 通常对应用程序中的大多数组件来说,这是不必要的。但是,对于某些类型的组件,特别是可重用组件,会有这种需求。下面描述一下最通用的场景。 多说无用,上代码: 例如,我们有一个常见的 Modal 组件,我们想要在 Modal 中保留可见性状态。当一些组件想要使用这个 Modal 时,通常会定义自己的可见状态,但是通过 State Lifting,这个步骤将不再必要。 在 上面的 Modal 组件中,做了如下操作: 首先,使用forwardRef执行 ref 转发,ref 转发是一种自动将 ref 通过组件传递给子组件的技术。 使用 useState 声明状态以控制Modal组件的展示或隐藏。 使用 React 内置的 useImperativeHandle hook 将属性或者方法暴露出去,这里我们添加了一些数据和函数供父组件使用。现在父组件可以直接调用子组件的show、hide、isVisble这些方法和属性。 ...

December 17, 2024

BFC

BFC 是前端面试中经常会被问到的一个知识点,今天就来带大家深入了解下什么是 BFC 以及它的作用。 文档流 在介绍BFC之前,需要先给大家介绍一下文档流。 我们常说的文档流其实分为定位流、浮动流、普通流三种。 绝对定位(Absolute positioning) 如果元素的属性 position 为 absolute 或 fixed,它就是一个绝对定位元素。 在绝对定位布局中,元素会整体脱离普通流,因此绝对定位元素不会对其兄弟元素造成影响,而元素具体的位置由绝对定位的坐标决定。 它的定位相对于它的包含块,相关CSS属性:top、bottom、left、right; 对于 position: absolute,元素定位将相对于上级元素中最近的一个relative、fixed、absolute,如果没有则相对于body; 对于 position:fixed,正常来说是相对于浏览器窗口定位的,但是当元素祖先的 transform 属性非 none 时,会相对于该祖先进行定位。 浮动 (float) 在浮动布局中,元素首先按照普通流的位置出现,然后根据浮动的方向尽可能的向左边或右边偏移,其效果与印刷排版中的文本环绕相似。 普通流 (normal flow) 普通流其实就是指BFC中的FC。FC(Formatting Context),直译过来是格式化上下文,它是页面中的一块渲染区域,有一套渲染规则,决定了其子元素如何布局,以及和其他元素之间的关系和作用。 在普通流中,元素按照其在 HTML 中的先后位置至上而下布局,在这个过程中,行内元素水平排列,直到当行被占满然后换行。块级元素则会被渲染为完整的一个新行。 除非另外指定,否则所有元素默认都是普通流定位,也可以说,普通流中元素的位置由该元素在 HTML 文档中的位置决定。 BFC 概念 先看下MDN上关于BFC的定义: 块格式化上下文(Block Formatting Context,BFC) 是Web页面的可视CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。 具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素不会在布局上影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。 通俗一点来讲,可以把 BFC 理解为一个封闭的大箱子,箱子内部的元素无论如何翻江倒海,都不会影响到外部。 除了 BFC,还有: IFC(行级格式化上下文)- inline 内联 GFC(网格布局格式化上下文)- display: grid FFC(自适应格式化上下文)- display: flex或display: inline-flex 注意:同一个元素不能同时存在于两个 BFC 中。 ...

November 9, 2024

离线存储

HTML5 离线存储是指通过 Web Storage API 提供的一种在客户端本地存储数据的机制,主要有两种方式:localStorage 和 sessionStorage。与传统的 Cookie 相比,Web Storage 提供了更大容量、灵活性,并且数据不会随着 HTTP 请求自动发送到服务器,这可以优化性能和安全性。此外,HTML5 还支持 离线应用缓存(Application Cache) 和 IndexedDB,用于更加复杂的数据存储需求和离线应用。 工作原理 HTML5 的离线存储主要通过以下几个机制来实现数据存储和访问: localStorage:一种持久性存储机制,数据存储在用户的浏览器中,即使关闭浏览器或重新加载页面,数据也不会丢失,除非明确被删除。其特点是跨页面、跨会话存储数据。 sessionStorage:会话级别的存储机制,只在当前页面的会话期间有效。当会话结束或浏览器窗口关闭时,数据会自动删除。其特点是只能在当前窗口或标签页中存储数据,不能跨窗口或标签页共享。 Application Cache(已被 Service Worker 替代):允许开发者将网站的某些资源缓存到本地,使得用户即使在离线的情况下也能访问特定页面。虽然该机制已被弃用,但它曾经是实现离线访问的核心。 IndexedDB:一种更为复杂的浏览器内置数据库,支持存储结构化数据,如对象和文件。它适用于更大规模的数据存储需求,并且允许通过索引进行高效的查询和数据操作。 使用方式 1. localStorage 特点:持久性存储,浏览器关闭后数据依然存在,除非手动清除。 使用方式: // 存储数据 localStorage.setItem('key', 'value'); // 获取数据 let value = localStorage.getItem('key'); // 删除数据 localStorage.removeItem('key'); // 清空所有 localStorage localStorage.clear(); 2. sessionStorage 特点:仅在当前会话期间有效,浏览器关闭或页面重新加载后数据会被清除。 使用方式: // 存储数据 sessionStorage.setItem('sessionKey', 'sessionValue'); // 获取数据 let sessionValue = sessionStorage.getItem('sessionKey'); // 删除数据 sessionStorage.removeItem('sessionKey'); // 清空所有 sessionStorage sessionStorage.clear(); 3. IndexedDB 特点:适合存储大量的结构化数据,支持异步 API 和事务。 使用方式: IndexedDB 的使用较为复杂,通常涉及打开数据库、创建事务、存储和检索数据等操作。一个简单的示例如下: let request = indexedDB.open('myDatabase', 1); request.onupgradeneeded = function(event) { let db = event.target.result; let objectStore = db.createObjectStore('myObjectStore', { keyPath: 'id' }); }; request.onsuccess = function(event) { let db = event.target.result; let transaction = db.transaction(['myObjectStore'], 'readwrite'); let objectStore = transaction.objectStore('myObjectStore'); objectStore.add({ id: 1, name: 'John Doe' }); }; 4. Application Cache(弃用) 特点:通过 manifest 文件指定需要缓存的资源,允许在离线时访问应用。 使用方式: 在 HTML 文档中指定 manifest 文件: <html manifest="app.manifest"> 然后在 app.manifest 文件中列出需要缓存的资源: ...

October 16, 2024