docs: 文档更新

This commit is contained in:
ZiuChen 2023-02-05 15:07:58 +08:00
parent 31cf31267a
commit 5815566c30
18 changed files with 2720 additions and 9 deletions

View File

@ -1,4 +1,5 @@
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
import generateSideBar from './scripts/generateSideBar'
export default defineConfig({ export default defineConfig({
title: 'ZiuChen', title: 'ZiuChen',
@ -35,6 +36,10 @@ export default defineConfig({
{ text: 'JavaScript进阶', link: '/note/JavaScriptEnhanced' } { text: 'JavaScript进阶', link: '/note/JavaScriptEnhanced' }
] ]
}, },
{
text: '文章创作',
link: '/article/一文读懂伪类与伪元素'
},
{ {
text: '个人介绍', text: '个人介绍',
link: '/self/' link: '/self/'
@ -57,6 +62,11 @@ export default defineConfig({
{ text: '社区贡献', link: '/works/contribution' } { text: '社区贡献', link: '/works/contribution' }
] ]
}, },
{
text: '文章创作',
collapsible: true,
items: [...generateSideBar()]
},
{ {
text: '学习笔记', text: '学习笔记',
collapsible: true, collapsible: true,
@ -67,7 +77,15 @@ export default defineConfig({
] ]
} }
], ],
socialLinks: [{ icon: 'github', link: 'https://ziuchen.github.io/' }], socialLinks: [
{ icon: 'github', link: 'https://ziuchen.github.io/' },
{
icon: {
svg: '<svg width="36" height="28" viewBox="0 0 36 28" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M17.5875 6.77268L21.8232 3.40505L17.5875 0.00748237L17.5837 0L13.3555 3.39757L17.5837 6.76894L17.5875 6.77268ZM17.5863 17.3955H17.59L28.5161 8.77432L25.5526 6.39453L17.59 12.6808H17.5863L17.5825 12.6845L9.61993 6.40201L6.66016 8.78181L17.5825 17.3992L17.5863 17.3955ZM17.5828 23.2891L17.5865 23.2854L32.2133 11.7456L35.1768 14.1254L28.5238 19.3752L17.5865 28L0.284376 14.3574L0 14.1291L2.95977 11.7531L17.5828 23.2891Z" fill="#1E80FF"/></svg>'
},
link: 'https://juejin.cn/user/1887205216238477'
}
],
editLink: { editLink: {
pattern: 'https://github.com/ZiuChen/ZiuChen.github.io/edit/main/docs/:path', pattern: 'https://github.com/ZiuChen/ZiuChen.github.io/edit/main/docs/:path',
text: 'Edit this page on GitHub' text: 'Edit this page on GitHub'

View File

@ -0,0 +1,17 @@
import fs from 'fs'
import path from 'path'
export default function generateSideBar() {
const articles = fs.readdirSync(path.resolve(__dirname, '../../article'))
const sidebar = articles.map((article) => {
// 移除后缀 `.md`
const title = article.replace(/\.md$/, '')
return {
text: title,
link: `/article/${title}`
}
})
return sidebar
}

View File

@ -8,6 +8,11 @@
z-index: 21; z-index: 21;
} }
/* SideBar scrollbar */
.VPSidebar::-webkit-scrollbar {
display: none;
}
/* Color Variables */ /* Color Variables */
:root { :root {

View File

@ -0,0 +1,709 @@
# 【2023】青训营 - 前端练习题汇总解析
汇总了青训营官方账号每天发布的练习题,并且给出了答案、做了简单解析与知识扩充,有不足之处欢迎一起交流学习。
每天的选择题不同而编程题是一样的直接去Leetcode刷题即可。
## 选择题
### DAY 1
#### 题目描述
```text
选择题 1
下列哪些是 HTML5 的新特性?
A. 语义标签
B. Canvas 绘图
C. <audio>元素
D. 增强型表单
选择题 2
下面可以继承的属性有哪些?
A. font-size
B. background
C. color
D. cursor
```
#### 答案与解析
```
1. ABCD
2. ACD
```
#### HTML5新增内容
- 语义化标签
- `header` `nav` `section` `article` `aside` `footer`
- 使用语义化标签有利于SEO与无障碍
- `<video>``<audio>`标签
- 支持引入外部视频/音频资源
- 可以搭配`<source>`标签实现fallback
- `<canvas>`画布
- `<input> <form>`元素属性扩展
- `type`属性支持了更多的值 内置了更多的样式
- 全局新增属性`data-*`
- 在JavaScript中可以通过DOM元素引用的`.dataset`直接获取到元素上的`data-*`属性的值
- Vue的`Scoped Style`Github的贡献墙颜色都是基于`[data-*]`选择器
#### CSS属性可继承性
常见的CSS属性都支持继承不必特别记忆
- 如果一个属性具备继承性,那么在该元素上设置后,它的后代元素都可以继承这个属性
- 如果后代元素自己设置有该属性,则会优先使用自己的属性
- 需要注意的是:子元素从父元素继承来的`font-size: 2em;`继承的是计算值,而不是原始值
- 如果父元素设置了`font-size: 2em;`,此时子元素再设置`font-size: 2em;`达到的效果实际上是在父元素的基础上的`2em`,实质上是`font-size: 4em;`
#### 强制继承
如果某些属性不具备可继承性,我们希望子元素能够继承,则可以使用强制继承:
```css
.father {
border: 2px soild red;
}
.child {
border: inherit;
}
```
### DAY 2
#### 题目描述
```text
选择题 1
对于一条100M的宽带理论下载速度上限是多少
A. 12.5MB/s
B. 100MB/s
C. 10MB/s
D. 10Mb/s
选择题 2
关于 for of 和 for in 的描述,正确的是?
A. for in 可以循环普通对象
B. for of 可以循环普通对象
C. 都不可以循环数组
D. 都可以循环数组
```
#### 答案与解析
```
1. A
2. AD
```
#### 字节与比特
运营商的100M指的是100Mb单位是`bit`,而日常使用中的网速单位是`Byte`
换算公式是`1Byte = 8bit`,故`100 / 8 = 12.5MB/s`
#### for-in与for-of
- `for-in`可以遍历数组与对象 得到的是索引/key
- `for-of`可以遍历数组但不能遍历对象 遍历数组得到的是值
```js
const arr = ['abc', 'cba', 'nba']
const obj = {
name: 'Ziu',
age: 18
}
// for-in可以遍历数组
for (const i in arr) {
console.log(i) // 0 1 2
}
// for-of可以遍历数组
for (const i of arr) {
console.log(i) // abc cba nba
}
// for-in可以遍历对象的key值
for (const i in obj) {
console.log(i) // name age
}
// for-of不能遍历对象 会报错
for (const i of obj) {
console.log(i) // Error
}
```
### DAY 3
#### 题目描述
```text
选择题 1
关于事件冒泡描述正确的是?
A. 从目标元素向 document 冒泡
B. 从 document 向目标元素冒泡
C. 从 document 向目标元素冒泡,再从目标元素向 document 冒泡
D. 以上都不是
选择题 2
以下哪些 script 标签属性会使脚本有可能在 DOMContentLoaded 事件之后加载?
A. <script async>
B. <script defer>
C. <script type="module">
D. <script type="module" async>
```
#### 答案与解析
```
1. A
2. AD
```
#### 冒泡与捕获
事件冒泡:浏览器从触发事件的目标元素开始向上检查其祖先元素是否注册了此事件的监听器,如果其祖先元素注册了此事件监听回调,则触发回调。此操作会层层向上,直到`<html>`元素
[一文读懂事件冒泡与事件捕获](https://juejin.cn/post/7093018681621512222)
#### script的defer async属性
- defer 延迟加载
- 不会阻塞DOM Tree的构建过程
- 下载完成后 总会等待DOM Tree构建完成后执行
- 一定在`DOMContentLoaded`事件之前触发
- async 异步加载
- 不保证执行顺序 独立下载、独立运行
- 只要下载完成就立即执行 不保证在`DOMContentLoaded`之前或之后被执行
被标记为`type="module"``<script>`会被浏览器识别为ES6 Module当浏览器解析到此`<script>`时将异步下载此脚本,不会造成堵塞浏览器,其本质上与`defer`是一致的:
```html
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
```
当然,如果额外添加了`async`标记,此脚本将在被下载完成后中断渲染,立即执行
[Module 的加载实现](https://es6.ruanyifeng.com/#docs/module-loader)
### DAY 4
#### 题目描述
```text
选择题 1
以下哪些是 CSS 块级元素的特性?
A. 宽度默认由其中的内容决定
B. 高度默认由其中的内容决定
C. 可以被分拆到多行
D. 可以通过 height 属性指定高度
选择题 2
以下关于跨域说法错误的是?
A. http://example.com和https://example.com是相同的域名属于同源
B. 跨域资源共享规范中规定了除了 GET 之外的 HTTP 请求,或者搭配某些 MINE 类型的 POST 请求,浏览器都需要先发一个 OPTIONS 请求。
C. CSS 中通过 @font-face 使用字体也会有跨域问题
D. CookieLocalStorage 和 IndexedDB 都会受到同源策略的限制
```
#### 答案与解析
```
1. BD
2. A
```
#### 块级元素、行内级元素、行内块级元素
块级元素在BFC中布局行内级元素在IFC中布局二者具有不同的特性
常见的块级(block)元素:`div` `h1` `p`
- 每个块级元素都是独自占一行
- 元素的宽度如果不设置的话默认为父元素的宽度父元素宽度100%
- 高度、行高、外边距margin以及内边距padding都可以控制
- 多个块状元素从上至下排列
常见的行内级(inline-level)元素:`a` `span`
- 不能设置宽高,默认宽度就是它本身内容的宽度,不独占一行
- 高度、行高、外边距以及内边距都可以控制
- 但是之间会有空白缝隙,设置它上一级的 `font-size: 0;` 才能消除间隙
常见的行内块级(inline-block)元素:`img` `button` `input`
- 同时具有块级与行内级元素的特性
- 可以设置宽高,其默认宽高由其内容决定
#### 跨域资源共享 CORS
CORS是常用的跨域解决办法之一下面简单列出纲要详细解析请见 [跨域资源共享CORS](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS)
- 什么是CORS可以解决哪些场景下的跨域问题
- 满足哪些条件的请求可以被识别为`简单请求`
- 什么是预检请求?
- 附带身份凭证的跨域请求需要注意什么?
简单介绍一下跨域的内容:
- 跨域的概念
- 域名由`协议://域名:端口号`组成
- 三个部分有任一不同即被浏览器视为跨域
- 是浏览器的安全行为,跨域请求会被真实发出和处理,但响应会被浏览器拦截
- 跨域的影响
- 不同域之间的资源不能共享(`Cookie LocalStorage IndexedDB`等)
- 无法接触非同源网页的DOM和js对象
- 无法向非同源地址发送Ajax请求
- 跨域的解决方案
- JSONP
- CORS
- 代理服务器
[什么是跨域?出现原因及解决方法](https://segmentfault.com/a/1190000040485198)
### DAY 5
#### 题目描述
```text
选择题 1
下列哪些可以实现浏览器存储数据?
A. cookie
B. localStorage
C. session
D. sessionStorage
选择题 2
对以下代码说法正确的是?
let arr = [1,2,3,4,5];
let arr2 = [1, , 3];
A. 执行 arr.length = 3此时数组为 [1,2,3]
B. 执行 arr[10] = 11此时 arr.length 为 6
C. 执行 delete arr[2],此时 arr.length 为 4数组为 [1,2,4,5]
D. arr2.length 的长度为 2
```
#### 答案与解析
```
1. ABD
2. A
```
#### 解析Cookie LocalStorage SessionStorage异同
`Cookie LocalStorage SessionStorage`都是保存在浏览器内部的数据
`Session`一般是保存在服务器中,用来标识客户端会话信息。
| | Cookie | LocalStorage | SessionStorage |
| --- | --- | --- | --- |
| 大小 | 4Kb | 5MB | 5MB |
| 兼容 | H4/H5 | H5 | H5 |
| 访问 | 任何窗口 | 任何窗口 | 同一窗口 |
| 有效期 | 手动设置 | 无 | 窗口关闭 |
| 存储位置 | 浏览器和服务器 | 浏览器 | 浏览器 |
| 与请求一起发送 | 是 | 否 | 否 |
| 语法 | 复杂 | 简单 | 简单 |
- Cookie是由`?key1=value1;key2=value2`组成的,可以通过`encodeURIComponent()`来保证它不包含任何逗号、分号或空格(cookie值中禁止使用这些值).
- Cookie一般的字段有`path domain max-age expires secure`
- 不同的host之间的localStorage、sessionStorage对象是隔离的
#### 代码运行结果
首先定义`arr``arr2`两个数组:
```js
let arr = [1, 2, 3, 4, 5]
let arr2 = [1, , 3]
```
A选项
```js
arr.length = 3
console.log(arr) // [1, 2, 3]
```
B选项
```js
arr[10] = 11
console.log(arr.length) // 11
```
C选项
```js
// 当你删除一个数组元素时,数组的长度不受影响。即便你删除了数组的最后一个元素也是如此。
delete arr[2]
console.log(arr[2]) // undefined
console.log(arr) // [1, 2, <empty item>, 4, 5]
console.log(arr.length) // 5
```
D选项
```js
console.log(arr2.length) // 3
```
delete 操作符用于删除对象的某个属性;如果没有指向这个属性的引用,那它最终会被释放。
[MDN: delete操作符](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/delete)
### DAY 6
#### 题目描述
```text
选择题 1
在 css 选择器当中,优先级排序正确的是?
A. id选择器>标签选择器>类选择器
B. 标签选择器>类选择器>id选择器
C. 类选择器>标签选择器>id选择器
D. id选择器>类选择器>标签选择器
选择题 2
如以下代码所示,给 body 绑定两个事件后,调用 document.body.click() 输出的结果是?
document.body.addEventListener('click', () => {
Promise.resolve().then(() => console.log(1))
console.log(2);
}, false);
document.body.addEventListener('click', () => {
Promise.resolve().then(() => console.log(3))
console.log(4);
}, false);
A. 2, 4, 1, 3
B. 2, 1, 4, 3
C. 1, 2, 3, 4
D. 1, 3, 2, 4
```
#### 答案与解析
```
1. D
2. B
```
#### CSS选择器优先级
内联样式 > **ID选择器 > 类选择器 > 标签选择器** > 通配符
不同选择器权重值不同,权重值更高的属性值会覆盖更低的值
- `!important` 10000
- `内联样式` 1000
- `id选择器` 100
- `类选择器、属性选择器、伪类` 10
- `元素选择器、伪元素` 1
- `通配符` 0
#### 事件循环
需要了解浏览器的事件循环机制才能解出此题,以下是一些可能需要掌握的概念:
- JavaScript是单线程的
- 浏览器事件循环机制
- 主线程、宏任务队列、微任务队列、常见的宏任务与微任务
- 任务执行队列、函数执行栈
### DAY 7
#### 题目描述
```text
选择题 1
浮动会导致页面的非正常显示,以下几种清除浮动的方法,哪个是不推荐使用的?
A. 在浮动元素末尾添加一个空的标签例如 <div style=”clear:both”></div>
B. 通过设置父元素overflow值为hidden
C. 给父元素添加clearfix类
D. 父元素也设置浮动
选择题 2
以下代码的运行结果是?
var f = function () { console.log('1'); } function f() { console.log('2'); }
f()
A. undefined
B. 报错
C. 2
D. 1
```
#### 答案与解析
```
1. D
2. D
```
#### 清除浮动的方法及原理
什么是清除浮动?
当容器的高度为`auto`,且容器的内容中有浮动元素时,容器的高度不能自动伸长以适应内容的高度,这个时候就需要清除浮动,让容器的高度自动伸长以适应浮动元素。
清除浮动常用方法
- 使用带clear属性的空元素
- 在浮动元素后使用一个空元素如`<div class="clear"></div>`
- 并在CSS中赋予`.clear{ clear: both; }`属性即可清理浮动
- 使用CSS的overflow属性
- 给父元素赋予`overflow: visible;`创建一个新的BFC
- 浮动元素的父元素触发BFC形成独立的块级格式化上下文
- 浮动元素的父元素的高度值为autoheight的默认值即为auto
- BFC拥有自己的计算高度的方法与规则
- **如果有浮动元素,那么会增加高度以包括这些浮动元素的下边缘**
- [W3C 10.6.7 'Auto' heights for block formatting context roots](https://www.w3.org/TR/CSS2/visudet.html)
- 给浮动的元素的容器添加浮动
- 会导致父元素也跟着浮动 不常用 也不推荐
- 使用邻接元素处理
- 给浮动元素后面的兄弟元素添加`clear`属性
- 使用CSS的`::after`伪元素
- 给浮动元素的父元素末尾添加一个看不见的块元素Block element
- 给这个伪元素添加`clear`属性清理浮动
#### 变量提升
题目涉及JavaScript的变量提升知识详细解析见
[MDN 变量提升](https://developer.mozilla.org/zh-CN/docs/Glossary/Hoisting)
[彻底解决JS变量提升的面试题](https://segmentfault.com/a/1190000039288278)
### DAY 8
#### 题目描述
```text
选择题 1
下列说法正确的有哪些?
A. visibility:hidden 表示所占据的空间位置仍然存在,仅为视觉上的完全透明
B. display:none 不为被隐藏的对象保留其物理空间
C. visibility:hidden 与display:none 两者没有本质上的区别
D. visibility:hidden 回流与重绘
选择题 2
若主机甲与主机已已建立一条 TCP 链接最大段长MSS为 1KB往返时间RTT为 2 ms则在不出现拥塞的前提下拥塞窗口从 8KB 增长到 32KB 所需的最长时间是?
A. 4ms
B. 8ms
C. 24ms
D. 48ms
```
#### 答案与解析
```
1. AB(D)
2. D
```
#### 回流与重绘
- 重绘不一定引起回流,而回流一定会引起重绘,所以回流是一件很消耗性能的事情
- 什么情况下会触发回流?
- DOM 结构发生改变(添加新的节点或者移除节点)
- 改变了布局修改了width height padding font-size等值
- 窗口resize修改了窗口的尺寸等
- 调用getComputedStyle方法获取尺寸、位置信息
- 什么情况会触发重绘?
- 修改背景色、文字颜色、边框颜色、样式等
元素的几种不同显示状态:
- `display: none` 指的是元素完全不陈列出来不占据空间涉及到了DOM结构故产生reflow与repaint
- `visibility: hidden` 指的是元素不可见但存在保留空间不影响结构故只产生repaint但不可触发绑定事件
- `opacity: 0` 指的是元素不可见但存在保留空间不影响结构并且如果该元素已经绑定一些事件如click事件那么点击该区域也能触发点击事件的
#### 拥塞窗口调整
拥塞窗口调整有两种模式,一种是以指数增长的慢启动模式,另一种是以线性增长的拥塞避免模式
开始传输时是以慢启动模式调整拥塞窗口当窗口大小达到名为ssthresh的阈值时改用拥塞避免模式调整。
问“最长时间”应该就是指ssthresh非常小以至于一开始就是拥塞避免模式使得窗口调整的速度较慢每一个RTT会增加1个MSS这里是1KB
从8KB到32KB有24KB的调整空间就需要`24个RTT x 每个RTT的2ms`共计48ms
[WikiPedia: TCP拥塞控制](https://zh.wikipedia.org/zh-tw/TCP%E6%8B%A5%E5%A1%9E%E6%8E%A7%E5%88%B6)
### DAY 9
#### 题目描述
```text
选择题 1
以下对HTML标签的使用哪些是符合语义的
A. 使用 table 展示表格数据
B. 使用 span 表示按钮
C. 使用 article 展示文章内容
D. 使用 p 标签展示文章标题
选择题 2
包过滤防火墙对数据包的过滤依据不包括哪些?
A. 源IP地址
B. 源端口号
C. MAC 地址
D. 目的 IP 地址
```
#### 答案与解析
```
1. AC
2. C
```
#### HTML5语义化标签
解析略
#### 包过滤
包过滤是在IP层实现的包过滤根据数据包的源IP地址、目的IP地址、协议类型TCP包、UDP包、ICMP包、源端口、目的端口等包头信息及数据包传输方向等信息来判断是否允许数据包通过
## 编程题
### 题目 1
```
**题目**
给定一个十进制整数字符串,判断它是否是 4 的幂。
**示例 1**
输入:"16"输出true
**示例 2**
输入:"101"输出false
**示例 3**
输入:"70368744177664"输出true
**限定语言:** C、 C++、Java、Python、JavaScript V8
```
#### 题解
TypeScript
```ts
// LeetCode 342
function isPowerOfFour(n: number): boolean {
return n > 0 && (n & (n - 1)) === 0 && n % 3 === 1
}
```
### 题目 2
```
**题目**
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略空格、字母的大小写。
**示例**
输入:"A man, a plan, a canal: Panama"输出true
**限定语言:** C、 C++、Java、Python、JavaScript V8
```
#### 题解
TypeScript
```ts
// LeetCode 125
function isPalindrome(s: string): boolean {
let checkStr = ''
let revStr = ''
// 遍历原字符串
// 1.将原串转为仅剩数字与小写字母 2.生成反转串
for (let i = 0; i < s.length; i++) {
if (isAlnum(s[i])) {
checkStr += s[i].toLocaleLowerCase()
revStr = s[i].toLocaleLowerCase() + revStr
}
}
return checkStr === revStr
}
/**
* 检查字符串是否仅由数字与小写字母组成
* @param s 待检查字符串
*/
function isAlnum(s: string): boolean {
return /^[a-z0-9]+$/i.test(s)
}
```
### 题目 3
```
**题目**
给定一个字符串,找出该字符串中最长回文子串的长度。
**示例 1**
输入:"abc"输出0
**示例 2**
输入:"abcbe"输出3
**示例 3**
输入:"acdcecdcf"输出7
**限定语言:** C、 C++、Java、Python、JavaScript V8
```
#### 题解
TypeScript
```ts
// LeetCode 5
function longestPalindrome(s: string): string {
const len = s.length
if (len < 2) return s
let maxLength = 1
let begin = 0
for (let i = 0; i < len - 1; i++) {
// 头指针 从0遍历到len-1
for (let j = i + 1; j < len; j++) {
// 尾指针 从1遍历到len
if (j - i + 1 > maxLength && validPalindrome(s, i, j)) {
// 子串更长 且为回文字符串
maxLength = j - i + 1 // 更新长度
begin = i // 更新头指针位置
}
}
}
return s.substring(begin, begin + maxLength)
function validPalindrome(str: string, begin: number, end: number) {
while (begin < end) {
if (str[begin] !== str[end]) return false
begin++
end--
}
return true
}
}
```

View File

@ -0,0 +1,186 @@
# 一文读懂事件冒泡与事件捕获
## 💡 从例子入手
这是一个[简单的 Demo](https://mdn.github.io/learning-area/javascript/building-blocks/events/show-video-box.html),点击的 `Display video` 按钮后,将视频展示出来。
其中的视频 `<video>` 标签被 `<div>` 包裹,`<div>``<video>` 上都绑定了自己的 `click` 事件。
[代码片段](https://code.juejin.cn/pen/7092947625791455262)
我们的预期是:点击 `<video>` 时播放视频,点击 `<div>` 时隐藏视频,然而实际上你会发现,点击视频后,不仅视频虽然正常播放,但同时也被隐藏了。
点击子元素,父元素的事件也被触发,导致这种现象的原因正是:**浏览器的事件冒泡机制**。
## 🤔 什么是事件冒泡机制?事件捕获又是什么?
现代浏览器提供了两种事件处理阶段:**捕获阶段与冒泡阶段**
![bubbling-capturing.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/712c504b08f946088fc51dd5f8959020~tplv-k3u1fbpfcp-zoom-1.image)
> 在捕获阶段:
> - 浏览器检查元素的最外层祖先 `<html>` ,是否在捕获阶段中注册了一个 `onclick` 事件处理程序,如果是,则运行它。
> - 然后,它移动到 `<html>` 中单击元素的下一个祖先元素,执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素。
> 在冒泡阶段,与上述顺序相反:
> - 浏览器检查实际点击的元素是否在冒泡阶段中注册了一个 `onclick` 事件处理程序,如果是,则运行它
> - 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达 `<html>` 元素。
当一个事件被触发时,浏览器**先运行捕获阶段,后运行冒泡阶段**,并且在默认情况下,**所有事件处理程序都在冒泡阶段进行注册**。
针对上面提到的问题,我们可以知道:当 `<video>` 点击事件触发后,虽然我们没有主动触发 `<div>` 上绑定的点击事件,但由于冒泡机制,点击事件冒泡到了 `<div>` 上,并触发了绑定在其上的监听回调函数,将 `<video>` 标签隐藏。
### 📌 用例子验证结论
下面是一个用于验证上述结论的Demo
页面中包括由外向内的三个类名不同的div标签 `div1` `div2` `div3`,并为他们在捕获阶段/冒泡阶段分别绑定了不同的事件函数 `click``dblclick`
[代码片段](https://code.juejin.cn/pen/7092975347720781861)
当点击最内部的 `div3` 后,浏览器控制台输出:
```
> 捕获 click div1
> 捕获 click div2
> 捕获 click div3
> 冒泡 click div3
> 冒泡 click div2
> 冒泡 click div1
```
捕获阶段先执行,由外向内,冒泡阶段后执行,由内向外。 `dblclick` 事件并未被触发。
由此可知:
* 事件触发 => 捕获阶段 => 冒泡阶段
* 默认情况下,所有事件都在冒泡阶段被注册
* 捕获阶段,浏览器**由外层向内层**逐个元素检查事件函数,如有则执行它。
* 冒泡阶段,浏览器**由内层向外层**逐个元素检查事件函数,如有则执行它。
* 子元素一个事件触发后,只有**相同的事件会被捕获/冒泡检查**
****
在本例中,通过为 `addEventListener` 函数**指定第三个参数,从而在捕获阶段监听事件**
```js
target.addEventListener(type, listener, useCapture);
```
`useCapture``true` 时,事件监听回调函数将在捕获阶段被触发。
## 🧐 为什么有两个阶段?它们有什么用?
### 📌 历史渊源
> 在过去Netscape网景只使用事件捕获而Internet Explorer只使用事件冒泡。当W3C决定尝试规范这些行为并达成共识时他们最终得到了包括这两种情况捕捉和冒泡的系统最终被应用在现代浏览器中。
### 📌 事件代理 (Event delegation)
利用捕获/冒泡机制,我们可以实现事件代理,什么是事件代理?
试想一下,此时有一个包含大量列表项的无序列表,我们希望每一个 `<li>` 的点击事件都能被监听并且添加特定的处理函数,然而我们不可能为每一个 `<li>` 都添加一次事件监听函数,这样效率太低了。
```js
<ul>
<li>Li.</li>
<li>Li.</li>
<li>Li.</li>
<li>Li.</li>
<li>Li.</li>
<li>Li.</li>
<li>Li.</li>
</ul>
```
这时,我们可以为最外层的 `<ul>` 绑定一个 `click` 事件的监听函数,利用捕获/冒泡机制,就可以在事件对象的 `target` 属性中拿到对应的 `<li>`
```js
document.querySelector("ul").addEventListener("click", (e) => {
console.log(e.target); // > li (实际点击的元素)
console.log(e.currentTarget); // > ul (事件绑定的元素)
});
```
### 📌 事件对象中的`target``currentTarget`
在实际的使用中,你会发现事件对象中存在两个不同的属性:`target` `currentTarget`
它们有什么区别?和回调函数中的 `this` 的关系是怎样的?
复用上面验证捕获与冒泡顺序结论的例子,下面的代码片段验证了 `target``currentTarget` 的关系。
[代码片段](https://code.juejin.cn/pen/7092980795211513892)
当点击最内部的 `div3` 后,浏览器控制台输出:
```
> 捕获 target: div3 currentTarget: div1 this: div1
> 捕获 target: div3 currentTarget: div2 this: div2
> 捕获 target: div3 currentTarget: div3 this: div3
> 冒泡 target: div3 currentTarget: div3 this: div3
> 冒泡 target: div3 currentTarget: div2 this: div2
> 冒泡 target: div3 currentTarget: div1 this: div1
```
由此可知,事件对象中的 `target` 属性为实际触发事件的DOM元素`currentTarget` 指向注册事件监听时绑定的DOM元素。
需要注意的是,为了验证 `this` 指向,此处使用了 `function` 声明函数替代前例中的 `() => {}`,如果仍以箭头函数形式声明,则 `this` 始终指向 `Window` 对象。
## 🥳 如何阻止事件冒泡?
如你所见,大多数情况事件冒泡机制可以为我们带来便利,但是少数情况(如本文开头的例子)下,会影响预期的代码效果,我们应该如何阻止事件冒泡呢?
### 📌 .stopPropagation()
直接调用 `e.stopPropagation()` 阻止事件向上冒泡 触发其他回调
```js
document.querySelector(".div1")((e) => {
let e = e || window.event;
// some code ...
e.stopPropagation();
});
```
### 📌 e.target == e.currentTarget
当使用事件代理,给目标元素的父元素添加监听回调函数时添加判断
只有当**实际触发元素与回调绑定的元素相同**时,才触发相关逻辑
```js
document.querySelector(".div1")((e) => {
if (e.target == e.currentTarget) {
// some code ...
}
});
```
### 📌 return false
当回调内逻辑执行完毕后,直接 `return false` 可以中止事件向上冒泡
```js
document.querySelector(".div1")((e) => {
// some code ...
return false;
});
```
需要注意的是,`return false` 的方法不仅阻止了事件冒泡,而且阻止了默认事件。
> **默认事件**DOM元素的默认行为选中复选框是点击复选框的默认行为。下面这个例子说明了怎样阻止默认行为的发生
另一种阻止默认事件的方法是 `.preventDefault()`
```js
document.querySelector(".div1")((e) => {
// some code ...
e.preventDefault()
});
```
## 相关链接
[`事件冒泡及捕获`](https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Building_blocks/Events#%E4%BA%8B%E4%BB%B6%E5%86%92%E6%B3%A1%E5%8F%8A%E6%8D%95%E8%8E%B7)

View File

@ -0,0 +1,190 @@
# 一文读懂伪类与伪元素
## 🔰 什么是伪类?
伪类是添加到选择器的 **关键字** ,指定要选择的元素的特殊状态。
### 典型的伪类关键字
在大多数情况下,**伪类都与基础选择器搭配使用**,下述是伪类在一些典型场景下的应用。
#### `:hover`
指针在 `<button>` 上悬停,但没有激活它时,按钮颜色变为蓝色
```css
button:hover {
color: blue;
}
```
> **注意**: 在触摸屏上 `:hover` 基本不可用。不同的浏览器上`:hover` 伪类表现不同。网页开发人员不要让任何内容只能通过悬停才能展示出来,不然这些内容对于触摸屏使用者来说是很难或者说不可能看到。
#### `:not`
**`:not()`** 用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中,它也被称为*反选伪类**negation pseudo-class*)。
将所有不是`<p>`的元素颜色改为蓝色:
```css
body:not(p) {
color: blue;
}
/* 实测下述代码没有效果 */
:not(p) {
color: blue;
}
```
> **注意**: 在触摸屏上 `:hover` 基本不可用。不同的浏览器上`:hover` 伪类表现不同。网页开发人员不要让任何内容只能通过悬停才能展示出来,不然这些内容对于触摸屏使用者来说是很难或者说不可能看到。
#### `:first-child`
给所有 `<ul>` 下的第一个 `<li>` 应用不同的样式
```css
ul li {
color: blue;
}
ul li:first-child {
color: red;
font-weight: bold;
}
```
#### `:active`
`:active` 表示的是鼠标从按下到松开的时间,下述代码表示 `<a>` 在不同状态下的样式。
```css
a:link { color: blue; } /* 未访问链接 */
a:visited { color: purple; } /* 已访问链接 */
a:hover { background: yellow; } /* 用户鼠标悬停 */
a:active { color: red; } /* 激活链接 */
```
> **注意**: `:active` 赋予的样式可能会被后声明的其他链接相关的伪类覆盖。为保证样式生效,需要把 `:active` 的样式放在所有链接相关的样式之后。
这种链接伪类先后顺序被称为 *LVHA 顺序*`:link` > `:visited` > `:hover` > `:active`
### 单独使用的伪类关键字
我们常见的伪类关键字的特征是以单个冒号`:`开头,跟随在基础选择器后面。
**但是单独使用的伪类关键字也可以对页面产生效果,** 例如:
通过 `:focus` 伪类,可以让任何元素获得焦点后的颜色变为红色。
```css
:focus {
color: red;
}
```
更多伪类,见 [MDN - 标准伪类索引](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Pseudo-classes#%E6%A0%87%E5%87%86%E4%BC%AA%E7%B1%BB%E7%B4%A2%E5%BC%95)
![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d77fd1d23840415ca645e9429237b6dc~tplv-k3u1fbpfcp-watermark.image?)
## 🔰 什么是伪元素?
伪元素是一个**附加至选择器末的关键词**,允许你对被选择元素的特定部分修改样式。
与伪类相同,一个选择器中只能使用一个伪元素。但是,**伪元素必须紧跟在语句中的基础选择器之后**。
例如,下述代码可以给页面中每个 `<a>` 标签前添加一个😃表情。
```css
a::before {
content: "😃";
}
```
相比于伪类,伪元素的使用方式更加固定,其基本语法:
```css
selector::pseudo-element {
property: value;
}
```
> **注意**: 在书写伪元素时,你会见到单冒号 `:` 的写法但此为CSS2过时语法仅用于支持IE8大多情况请书写双冒号 `::` 来表示伪元素。
更多伪元素见 [MDN - 标准伪元素索引](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Pseudo-elements#%E6%A0%87%E5%87%86%E4%BC%AA%E5%85%83%E7%B4%A0%E7%B4%A2%E5%BC%95)
![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/17049cd3157f4a368c98d4fcd905c064~tplv-k3u1fbpfcp-watermark.image?)
## 📌 伪类与伪元素共同使用
下面一个案例中,同时用到了伪类和伪元素:
> 用CSS实现一个开关样式hover时触发滑块为正方形具体大小可随意如下图
![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a6e4b09bb9e64989b0561d48d10a2734~tplv-k3u1fbpfcp-watermark.image?)
> 尽量实现如下要求,可实现一部分:
> 1. 开关动作均有动画过度(滑块移位、背景色)
> 2. 只用一个dom元素实现
> 3. 开关的高度是固定的但宽度不固定即宽度为未知父元素的100%,宽度始终大于高度
### 题目解读
由于只能使用一个 `DOM` 元素,而要区分滑块和背景的不同状态,故使用伪元素 `::before``.box` 内部添加一个滑块,滑块采用 `inline-block` 方式展示,并且由 `transition` 属性指定过渡动画。
对于背景也采用了 `transition` 来指定过渡动画,并且二者都通过 `:hover` 触发。
![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1ac690e718be445083bc67b7bbcccb7e~tplv-k3u1fbpfcp-watermark.image?)
### 实现代码
`HTML`
```html
<div class="father">
<div class="box"></div>
</div>
```
`CSS`
```css
.box {
height: 50px;
width: 100%;
background-color: #c2d3e4;
transition: background-color 0.25s
}
.box::before {
content: "b";
color: white;
display: inline-block;
background-color: white;
position: relative;
top: 5px;
left: 5%;
height: 40px;
width: 35%;
transition: left 0.25s;
}
.box:hover {
background-color: #348fe4;
}
.box:hover::before {
left: 60%;
}
/* 父元素宽度任意 */
.father {
width: 100px;
}
```
### 相关链接
[代码片段](https://code.juejin.cn/pen/7088328950488760334)
[`CSS选择器`](https://developer.mozilla.org/zh-CN/docs/Learn/CSS/Building_blocks/Selectors)

View File

@ -0,0 +1,302 @@
# 一文读懂函数中this指向问题
## 函数中this指向
函数在调用时, Javascript会默认为this绑定一个值
```
// 定义一个函数
function foo() {
 console.log(this)
}
// 1. 直接调用
foo() // Window
// 2. 绑定对象调用
const obj = { name: 'ziu', aaa: foo }
obj.aaa() // obj
// 3. 通过call/apply调用
foo.call('Ziu') // String {'Ziu'}
```
this的绑定:
- 和定义的位置没有关系
- 和调用方式/调用位置有关系
- 是在运行时被绑定的
**this始终指向最后调用它的对象**
```
function foo() {
 console.log(this)
}
foo() // Window
const obj = {
 name: 'ziu',
 bar: function () {
   console.log(this)
}
}
obj.bar() // obj
const baz = obj.bar
baz() // Window
```
## 如何改变this的指向
### new 实例化一个函数
> new一个对象时发生了什么:
>
> 0. 创建一个空对象
> 0. 这个空对象会被执行prototype连接
> 0. 将this指向这个空对象
> 0. 执行函数体中的代码
> 0. 没有显式返回这个对象时 会默认返回这个对象
函数可以作为一个构造函数, 作为一个类, 可以通过new关键字将其实例化
```
function foo() {
 console.log(this)
 this.name = 'Ziu'
}
foo() // 直接调用的话 this为Window
new foo() // 通过new关键字调用 则this指向空对象
```
### 使用 call apply bind
在 JavaScript 中, 函数是对象。
JavaScript 函数有它的属性和方法。call() 和 apply() 是预定义的函数方法。
两个方法可用于调用函数,两个方法的第一个参数必须是对象本身
* * *
要将`foo`函数中的`this`指向`obj`,可以通过赋值的方式:
```
obj.foo = foo // 绑定
obj.foo() // 调用
```
但是也可以通过对函数调用call / apply实现
```
var obj = {
 name: 'Ziu'
}
function foo() {
 console.log(this)
}
foo.call(obj) // 将foo执行时的this显式绑定到了obj上 并调用foo
foo.call(123) // foo的this被绑定到了 Number { 123 } 上
foo.call("ziu") // 绑定到了 String { "ziu" } 上
```
#### 包装类对象
当我们直接使用类似:
```
"ZiuChen".length // String.length
```
的语句时,`JS`会为字符串 `ZiuChen` 包装一个对象,随后在这个对象上调用 `.length` 属性
#### call和apply的区别
- 相同点:第一个参数都是相同的,要求传入一个对象
- 在函数调用时会将this绑定到这个传入的对象上
- 不同点:后面的参数
- apply 传入的是一个数组
- call 传入的是参数列表
```
function foo(name, age, height) {
 console.log(this)
}
foo('Ziu', 18, 1.88)
foo.apply('targetThis', 'Ziu', 18, 1.88)
foo.call('targetThis', ['Ziu', 18, 1.88])
```
当我们需要给一个带参数的函数通过call/apply的方式绑定this时就需要使用到call/apply的第二个入参了。
#### 参数列表
当传入函数的参数有多个时,可以通过`...args`将参数合并到一个数组中去
```
function foo(...args) {
 console.log(args)
}
foo("Ziu", 18, 1.88) // ["Ziu", 18, 1.88]
```
#### bind绑定
如果我们希望:在每次调用`foo`时,都能将`obj`绑定到`foo``this`上,那么就需要用到`bind`
```
// 将obj绑定到foo上
const fun = foo.bind(obj)
// 在后续每次调用foo时, foo内的this都将指向obj
fun() // obj
fun() // obj
```
`bind()`方法将创建一个新的函数,当被调用时,将其`this`关键字
## 箭头函数
箭头函数是`ES6`新增的编写函数的方式,更简洁。
- 箭头函数不会绑定`this``argument`属性
- 箭头函数不能作为构造函数来使用(不能与`new`同用,会报错)
### 箭头函数中的this
在箭头函数中是没有`this`的:
```
const foo = () => {
 console.log(this)
}
foo() // window
console.log(this) // window
```
之所以找到了`Window`对象,是因为在调用`foo()`时,函数内部作用域并没有找到`this`,转而向上层作用域找`this`
因此找到了顶层的全局`this`,也即`Window`对象
### 箭头函数中this的查找规则
检查以下代码:
```
const obj = {
 name: "obj",
 foo: function () {
   const bar = () => {
     console.log(this)
  }
   return bar
}
}
const fn = obj.foo()
fn() // obj
```
代码执行完毕,控制台输出`this`值为`obj`对象,这是为什么?
箭头函数中没有`this`,故会向上层作用域寻找`this``bar`的上层作用域为函数`foo`,而函数`foo``this`由其调用决定
调用`foo`函数的为`obj`对象,故内部箭头函数中的`this`指向的是`obj`
检查以下代码:
```
const obj = {
 name: "obj",
 foo: () => {
   const bar = () => {
     console.log(this)
  }
   return bar
}
}
const fn = obj.foo()
fn() // Window
```
和上面的代码不同之处在于:`foo`也是由箭头函数定义的,`bar`向上找不到`foo``this`,故而继续向上,找到了全局`this`,也即`Window`对象
### 严格模式
- 在严格模式下,全局的`this`不是`Window`对象,而是`undefined`
- 在 JavaScript 严格模式(strict mode)下, 在调用函数时第一个参数会成为 this 的值, 即使该参数不是一个对象。
- 在 JavaScript 非严格模式(non-strict mode)下, 如果第一个参数的值是 null 或 undefined, 它将使用全局对象替代。
## this面试题
```
var name = 'window'
var person = {
 name: 'person',
 sayName: function () {
   console.log(this.name)
}
}
function sayName() {
 var sss = person.sayName
 sss() // 默认绑定: window
 person.sayName();  // 隐式绑定: person
(person.sayName)() // 隐式绑定: person, 本质与上一行代码相同
;(person.sayName = person.sayName)() // 间接调用: window
}
sayName()
```
```
var name = 'window'
var person1 = {
 name: 'person1',
 foo1: function () {
   console.log(this.name)
},
 foo2: () => console.log(this.name),
 foo3: function () {
   return function () {
     console.log(this.name)
  }
},
 foo4: function () {
   return () => console.log(this.name)
}
}
var person2 = {
 name: 'person2'
}
person1.foo1() // 隐式绑定: person1
person1.foo1.call(person2) // 显式绑定: person2
person1.foo2() // 上层作用域: window
person1.foo2.call(person2) // 上层作用域: window
person1.foo3()() // 默认绑定: window
person1.foo3.call(person2)() // 默认绑定: window
person1.foo3().call(person2) // 显式绑定: person2
person1.foo4()() // 隐式绑定: person1
person1.foo4.call(person2)() // 显式绑定: person2
person1.foo4().call(person2) // 隐式绑定: person1
```

View File

@ -0,0 +1,227 @@
# 从0实现一个年度报告
每到年底各大应用都会推出自己的年终总结报告,统计出用户一年来在应用内的行为展示给用户,供用户记录、分享。
今年掘金社区推出了自己的[2022掘友年度报告](https://zjsms.com/hbdA5jR)这次我们仿照这个报告从0开始自己实现一个年终总结报告页面
## 实现难点
### 1. 数据模拟
一般情况下是根据用户UID到后端去请求相关接口获得统计数据。
例如掘金的接口为`https://api.juejin.cn/event_api/v1/annual/annual_summary?aid=xxxxxx`
本次后端使用`NodeJS`实现了一个爬虫可以将用户数据统计完成后导出JSON格式的数据将此数据粘贴到前端页面的输入框即可生成自己的报告
### 2. 屏幕适配
可以观察到在PC端和在手机端访问年度报告展示的效果是不一样的。
本次考虑使用媒体查询来实现这个功能:
- 宽屏则展示背景,页面切换也使用背景中的上下切换按钮
- 小屏则隐藏背景,让内容填满屏幕,页面切换通过滑动事件监听
### 3. 动画效果
动画分为文本与背景元素的动画
- 背景元素的动画使用了SVG动画
- 动画中不动的部分直接使用`.png`图片
- 运动的部分使用SVG动画绘制如克里克的眼睛、尾巴
- 文本的动画使用了`CSS Animation`渐显的效果
- 不同段落之间通过`animation-delay`属性,彼此相差`1000ms`
背景动画容器的四个位置:`左上角` `右下角` `中间部分` `中间(悬浮气泡)`。不同位置的动画容器都采用绝对定位`position: absolute;`,辅以`z-index`实现层叠
囿于工期,本次的背景动画直接采用静态图片+`CSS Animation`实现上下浮动的效果
### 4. 音乐播放
通过`Audio`接口访问网络音乐链接,控制音乐相关功能
- 进入页面开始播放
- 离开页面暂停播放
- 支持点击按钮切换播放状态
## 用户数据
### 用户数据内容
```
- 用户名
- 注册时间 距今天数
- 创作相关
- 发布文章数
- 总阅读数 总赞数 总评论数
- 掘力值增长 超过人数百分比
- 最受欢迎的文章标题
- 社交相关
- 多少位掘友看过你的文章
- 点赞最多的掘友 评论最多的掘友
- 学习相关
- 阅读文章数 点赞数 评论数
- 总阅读字数
- 最关注的技术领域TOP3
- 沸点相关
- 发布沸点数 互动掘友数 点赞数 收赞数
- 互动最频繁的掘友
- 深夜阅读
- 最晚一次阅读时间
- 阅读的文章标题
- 早起阅读
- 最晚一次阅读时间
- 阅读的文章标题
- 最终总结
- 用户名
- 获得成就 饼图
```
### 数据模拟
根据不同页面分配不同的字段与数据我根据自己的报告模拟了以下JSON数据
```json
{"pages":[{"id":0,"data":{"userName":"Ziu","enterDay":1647619200000,"tillNow":291}},{"id":1,"data":{"publishArticle":187,"getReader":78959,"getLike":393,"getComment":13,"increment":3693,"rate":0.9773,"mostPopularArticle":"深入理解浏览器缓存原理"}},{"id":2,"data":{"growFriends":2861,"mostLike":"老边","mostComment":"重载新生"}},{"id":3,"data":{"mostGoodAt":["JavaScript","Vue3","TypeScript"]}},{"id":4,"data":{"readed":1397,"like":1347,"comment":1345,"words":"244万","mostFocus":["前端","JavaScript","Vue.js"]}},{"id":5,"data":{"oftenRead":[{"name":"seloven","followed":0},{"name":"掘金酱","followed":1},{"name":"CLICK克里克","followed":1},{"name":"稀土君","followed":1},{"name":"cc123nice","followed":0}]}},{"id":6,"data":{"publishPin":395,"interact":1283,"like":705,"getLike":374,"mostInteractWith":"狗哥哥"}},{"id":7,"data":{"mostLateTime":1665768240000,"publishArticle":"深入理解浏览器缓存原理"}},{"id":8,"data":{"mostEarlyTime":1661208660000,"publishArticle":"深入理解Proxy与Reflect"}},{"id":9,"data":{"nickName":"Ziu","ability":"学习力","analysis":"滴水石穿,读百篇尽显敏而好学","medal":["与你同行","笔耕不辍","博览群文","高才掘学","前排围观"]}}]}
```
## 编码中遇到的问题
### 音乐自动播放的问题
之前我希望不操作DOM而是通过`new Audio()`更优雅的实现音频播放。实际情况是:浏览器禁用了无用户操作的音频自动播放,要想实现自动播放有两种解决方法:
- HTML标签`<audio>`添加`autoplay`属性 后续播放/暂停都需要操作DOM
- 为界面添加监听器,监听到用户操作后手动触发`audio.play()`
为了保证功能实现的稳定性,我选择了前者。
```js
// useAudio.js
import { ref } from 'vue'
const defaultURL = 'https://xxxxxxxxxxxxxxxxxxxxxxxx.mp3'
export default function useAudio(url = defaultURL) {
const audio = new Audio(url)
audio.loop = true
const playStatus = ref(false)
const play = () => {
audio.play()
playStatus.value = true
}
const pause = () => {
audio.pause()
playStatus.value = false
}
return {
audio, play, pause, playStatus
}
}
```
### VNode调整样式的问题
项目使用的框架是Vue3部分页面使用到了JSX语法众所周知JSX语法定义的组件返回的是一个VNode树要想统一为树上元素添加渐变的样式则需要遍历他们。
需要遍历树上所有元素,并分别为他们添加不同值的`animation-delay`属性:
```js
const children = DOM.children
for (let i = 1; i < children.length; i++) {
const child = children[i]
child.props
? (child.props.style = { animationDelay: `${i * 1000}ms` })
: Object.assign(child, {
props: { style: { animationDelay: `${i * 1000}ms` } }
})
}
```
`style`属性并不是VNode的直接属性而放在`props`上的
### 监听`Animation`结束事件并更新响应式变量
如果当前页所有的动画都播放完毕则需要将播放完的事件通知给JavaScript并将状态更新到响应式的变量中
在前文遍历DOM树时为树中最后一个动画节点添加`animationend`监听回调,回调函数中执行`document.dispatchEvent(e)`,其中`e`是通过`const e = new CustomEvent('custom-animationend')`创建的自定义事件
回调触发时,在别处的`onMounted`回调内监听该自定义事件的触发,并更新响应式变量
### 切换页面支持触控滑动滚轮键盘
由于年终总结报告需要考虑到多种终端用户的体验,所以需要对翻页操作进行更多的优化
监听页面的触控滑动事件、滚轮滚动事件,并且匹配上翻页操作
```js
// 监听移动端滑动事件
let startY = 0
let endY = 0
document.addEventListener('touchstart', (e) => {
startY = e.touches[0].clientY
})
document.addEventListener('touchend', (e) => {
endY = e.changedTouches[0].clientY
if (endY - startY > 50) {
prevPage()
} else if (endY - startY < -50) {
nextPage()
}
})
// 监听鼠标滚轮事件
document.addEventListener('wheel', (e) => {
if (e.deltaY > 0) {
nextPage()
} else if (e.deltaY < 0) {
prevPage()
}
})
// 监听键盘事件
document.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp') {
prevPage()
} else if (e.key === 'ArrowDown') {
nextPage()
}
})
```
### 代码打包
由于最后需要将项目放到码上掘金平台运行,所以需要考虑静态资源的加载问题
这里我使用到了Vite提供的类似`file-loader`的功能可以将大小在指定阈值下的图片资源直接转为行内的DataURL配置选项是`config.build.assetsInlineLimit`,这样所有的图片资源都不必考虑外部引入的问题,直接内嵌进代码。
## 技术介绍
主界面使用的是Vue3的`SFC`,主要逻辑都在单文件组件中完成。通过`JSX`语法编写不同页面的内容,这样更方便我们为每个节点添加不同的动画。
`JSX`编写的组件通过全局注册后,在`SFC`中通过`<Component>`动态加载。
图片资源方面使用到了雪碧图部署后可以降低客户端发起HTTP请求频次提高性能
代码复用方面,样式代码都抽离为单个的`xxxx.less`文件,哪里用到了直接导入即可
使用到了`Pinia`状态管理库,将`switching` `pageId` `audioStatus`等全局状态放到其中管理非常方便,避免了`provide``inject`的繁琐
## 功能介绍
- 支持PC端/移动端展示不同样式
- PC端左侧有控制栏 支持控制音乐播放 切换页面
- 移动端右上角控制音乐 滑动切换页面
- 页面之间切换有动态效果 文字逐行展示
- 左上角、右下角会出现矢量动图
## Demo展示
[Demo(Vercel)](https://juejin-annual-report.vercel.app/)
[jcode](https://code.juejin.cn/pen/7184691373911015459)

View File

@ -0,0 +1,123 @@
# 彻底搞懂对象的数据属性描述符、存储属性描述符
## 属性描述符
```js
let obj = {
name: "ziu",
age: 18
}
Object.defineProperty(obj, "height", {
value: 1.88
})
console.log(obj) // { name: 'ziu', age: 18 }
```
`obj`对象的控制台输出中,并没有`defineProperty`新定义的`height`,这是因为不可枚举的(不可遍历的),在打印时并没有和其他属性一起输出。这个属性已经被添加到对象中,只不过不可见。
```js
console.log(obj.height) // 1.88
```
属性描述符是一个对象,根据功能不同,可以分为两类:**数据属性描述符**和**存储属性描述符**
### 数据属性描述符
* 数据属性描述符Data Properties
* `value`该属性对应的值,默认`undefined`
* `configurable`该属性描述符是否可被改变、是否可被删除,默认为`false`
* `enumerable`该属性是否可被枚举,默认为`false`
* `writable`该属性是否可以被写入新的值,默认为`false`
**没有用属性描述符定义的对象属性(直接使用`.`语法)**,也是具有以上属性描述符的特性的,`value`值为属性被赋值的值,其他`configurable``emumerable``writable`默认值都为`true`。**注意,属性描述符区全为小写。**
```js
let obj = {
name: "ziu",
age: 18
}
// 数据属性描述符
Object.defineProperty(obj, "address", {
value: "Beijing", // 该属性对应的值默认为undefine
configurable: false, // 该属性描述符是否可被改变、是否可被删除默认为false
enumerable: true, // 该属性是否可被枚举默认为false
writable: true // 该属性是否可以被写入新的值默认为false
})
// configurable
delete obj.address
obj.address = "Shanghai"
console.log(obj.address) // Beijing
// enumerable
console.log(obj)
for(let key in obj) {
console.log(key)
}
console.log(Object.keys(obj))
// writable
obj.address = "Tianjin"
console.log(obj.address)
```
### 存储属性描述符
* 存储属性描述符Accessor Properties
* `get`当访问该属性时,会调用此函数,默认为`undefined`
* `set`当属性值被修改时,会调用此函数,默认为`undefined`
注意,`get``set`描述符与`vaule``writable`描述符不共存。
| | `configurable` | `enumerable` | `value` | `writable` | `get` | `set` |
| ---------- | -------------- | ------------ | ---------- | ---------- | ---------- | ---------- |
| 数据描述符 | 可以 | 可以 | 可以 | 可以 | **不可以** | **不可以** |
| 存取描述符 | 可以 | 可以 | **不可以** | **不可以** | 可以 | 可以 |
```js
let obj = {
name: "ziu",
age: 18,
_address: "Beijing"
}
// Accessor Properties
Object.defineProperty(obj, "address", {
enumerable: true,
configurable: true,
// value: "Beijing",
// writable: true,
get: function() {
return this._address
},
set: function(val) {
this._address = val
}
})
console.log(obj.address) // 调用getter() Beijing
obj.address = "Shanghai" // 调用setter()
console.log(obj._address) // Shanghai
```
### 应用场景
1. 隐藏某个私有属性,希望直接被外界使用和赋值。(下划线开头的变量一般为私有属性)
2. 获取某个属性被访问或赋值的时刻,可以设置伴随被访问或被赋值时,执行其他函数。
```js
Object.defineProperty(obj, "address", {
get: function() {
bar()
return this._address
},
set: function(val) {
foo()
this._address = val
}
})
console.log(obj.address) // got address value once
obj.address = "Shanghai" // resetted address value once
function bar() {
console.log("got address value once")
}
function foo() {
console.log("resetted address value once")
}
```

View File

@ -0,0 +1,164 @@
# 深入Vue3源码看看Vue.use后究竟发生了什么
## 从全局注册组件库入手
如果我们自定义了几个自定义组件,当我们想在`.vue`文件中使用它们时,需要手动`import`导入组件并在`component`中注册:
```html
<script>
import CustomInput from '@/component/CustomInput.vue'
export default {
component: {
CustomInput
}
}
</script>
```
通过`Vue.use``ElementPlus`全局注册后,所有的组件都可以在`.vue``<template>`标签中直接使用,不需要再导入、注册。
```js
import ElementPlus from 'element-plus'
Vue.use(ElementPlus)
```
这个过程里`Vue.use`究竟为我们做了哪些事?
假设我此时有两个自定义组件`ZiuInput``ZiuButton`位于`@/module/ZiuUI/component`目录下,我希望能通过`Vue.use`达到像`ElementPlus`那样免导入注册就能直接使用的效果。
于是我在`ZiuUI`目录下创建了`index.js`,并在其中编写以下代码:
```js
// @/module/ZiuUI/index.js
import ZiuInput from './component/ziu-input.vue'
import ZiuButton from './component/ziu-button.vue'
const components = [ ZiuInput, ZiuButton ]
const ZiuUI = {
install(Vue) {
// 注册组件
components.forEach(component => {
Vue.component(component.name, component)
})
}
}
export default ZiuUI
```
当我们将`ZiuUI`这个对象传给`Vue.use()`时,`Vue`会自动调用其中的`install`方法,并将`Vue`实例传入其中,那么我们就可以在`install`方法中实现组件的全局注册。
```js
// @/main.js
import Vue from 'vue'
import App from './App'
import ZiuUI from './module/ZiuUI'
Vue.use(ZiuUI) // 将ZiuUI传入Vue.use()
...
```
## 深入源码
下载Vue3的源码阅读我们可以发现`use`相关的代码:
```ts
use(plugin: Plugin, ...options: any[]) {
// 组件已经被安装了 若是开发环境 则抛出警告
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)
}
// 组件未安装 且install方法为函数 那么执行安装 并调用install方法
// installedPlugins是一个Set 用于记录已经安装的组件
else if (plugin && isFunction(plugin.install)) {
installedPlugins.add(plugin)
plugin.install(app, ...options)
}
// 传入Vue.use本身就是一个函数 那么执行这个函数
else if (isFunction(plugin)) {
installedPlugins.add(plugin)
plugin(app, ...options)
}
// 如果当前为开发环境 且Vue.use未传参 则抛出警告
else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`
)
}
// 执行结束 返回App本身便于链式调用
return app
}
```
## 手动引入&注册组件
有时候,我们不希望全局注册一个组件库,导致整个项目体积变得巨大,而是希望能只引入某些用到的组件,但是又不想用到一个组件就需要手动的导入、注册。
除了使用组件库提供的自动导入插件,我们还可以手动实现一个“半自动导入组件”的功能。
编写一个`register-element.ts`文件,将所有我们项目中需要用到的组件都在此文件中引入并注册。
```ts
// register-element.ts
declare function require(moduleName: string): void
import type { App } from 'vue'
import 'element-plus/theme-chalk/base.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import 'element-plus/theme-chalk/el-loading.css'
import {
ElButton,
ElTabs,
ElTabPane
} from 'element-plus'
const components = [
ElButton,
ElTabs,
ElTabPane
]
export default function registerElement(app: App): void {
components.forEach((c) => {
const name = transferCamel(c.name)
// 引入组件样式 将驼峰改为-分隔命名
require(`element-plus/theme-chalk/${name}.css`)
// 注册组件
app.component(name, c)
})
}
function transferCamel(camel: string): string {
return camel
.replace(/([A-Z])/g, '-$1')
.toLowerCase()
.slice(1)
}
```
阅读完源码我们发现,如果为`Vue.use()`传入的是一个函数那么Vue会将app实例传入并调用这个函数。因此我们只需要在`main.ts`中在`App`实例上链式调用`.use`方法,并将`registerElement`函数传入那么Vue会自动将`app`实例传入并调用这个方法:
```ts
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import registerElement from './global/register-element.ts'
const app = createApp(App).use(registerElement)
app.mount('#app')
```
当有新的需要使用的组件时,只需要到`register-element.ts`文件中引入一次即可。
## 参考阅读
[Vue文档: App.use](https://vuejs.org/api/application.html#app-use)
[Vue文档: Plugins](https://vuejs.org/guide/reusability/plugins.html)

View File

@ -0,0 +1,279 @@
# 深入理解Proxy与Reflect
### 监听对象的操作
可以使用Proxy对象将原对象包裹此后的操作都对`proxy`进行,每次`get``set`被触发时都会自动执行相应代码
```js
const obj = {
name: 'ziu',
age: 18,
height: 1.88
}
const proxy = new Proxy(obj, {
get(target, key) {
console.log('get', key)
return target[key]
},
set(target, key, value) {
console.log('set', key, value)
target[key] = value
}
})
```
```js
const tmp = proxy.height // getter被触发
proxy.name = 'Ziu' // setter被触发
```
除此之外,在之前的版本中可以通过`Object.defineProperty`为对象中某个属性设置`getter``setter`函数,可以达到类似的效果
```js
for (const key of Object.keys(obj)) {
let value = obj[key]
Object.defineProperty(obj, key, {
get() {
console.log('get', value)
return value
},
set(newVal) {
console.log('set', key, newVal)
value = newVal
}
})
}
```
但是通过`Object.defineProperty`实现的监听存在问题:
- `Object.defineProperty`设计之初并不是为了监听一个对象中的所有属性的
- 如果要监听新增/删除属性,那么此时`Object.defineProperty`是无能为力的
### Proxy类基本使用
```JS
const proxy = new Proxy(target, handler)
```
即使不传入handler默认也会进行基本的代理操作
```js
const obj = {
name: 'ziu',
age: 18
}
const proxy = new Proxy(obj, {})
proxy.height = 1.88 // 添加新属性
proxy.name = 'Ziu' // 修改原属性
console.log(obj) // { name: 'Ziu', age: 18, height: 1.88 }
```
### 捕获器
常用的捕获器有`set``get`函数
```js
const proxy = new Proxy(obj, {
set: function (target, key, newVal) {
console.log(`监听: ${key} 设置 ${newVal}`)
target[key] = newVal
},
get: function (target, key) {
console.log(`监听: ${key} 获取`)
return target[key]
}
})
```
- set函数有四个参数
- target 目标对象(侦听的对象)
- property 即将被设置的属性key
- value 新属性值
- receiver 调用的代理对象
- get函数有三个参数
- target 目标对象(侦听的对象)
- property 被获取的属性key
- receiver 调用的代理对象
另外介绍两个捕获器:`has``deleteProperty`
```js
const proxy = new Proxy(obj, {
...
has: function (target, key) {
console.log(`监听: ${key} 判断`)
return key in target
},
deleteProperty: function (target, key) {
console.log(`监听: ${key} 删除 `)
return true
}
})
delete proxy.name // 监听: name 删除
console.log('age' in proxy) // 监听: age 判断
```
### Reflect
Reflect是ES6新增的一个API它本身是一个对象
- 提供了很多操作JavaScript对象的方法有点像Object中操作对象的方法
- 比如`Reflect.getPrototypeOf(target)`类似于`Object.getPrototypeOf()`
- 比如`Reflect.defineProperty(targetm propertyKey, attributes)`类似于`Object.defineProperty()`
如果我们又Object对象可以完成这些操作为什么还需要Reflect呢
- Object作为一个构造函数这些操作放到它身上并不合适
- 包含一些类似于 in delete的操作符
- 在ES6新增了Reflect让这些操作都集中到了Reflect对象上
- 在使用Proxy时可以做到不操作原对象
### 与Object的区别
删除对象上的某个属性
```js
const obj = {
name: 'ziu',
age: 18
}
// 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变
// 同时该属性也能从对应的对象上被删除。 默认为 false。
Object.defineProperty(obj, 'name', {
configurable: false
})
// 1. 旧方法 检查`delete obj.name`是否执行成功
// 结果: 需要额外编写检查代码且存在问题(严格模式下删除configurable为false的属性将报错)
delete obj.name
if (obj.name) {
console.log(false)
} else {
console.log(true)
}
// 2. Reflect
// 结果: 根据是否删除成功返回结果
if (Reflect.deleteProperty(obj, 'name')) {
console.log(true)
} else {
console.log(false)
}
```
### Reflect常见方法
其中的方法与Proxy的方法是一一对应的一共13个。其中的一些方法是Object对象中没有的
- `has` 判断一个对象是否存在某个属性,和 `in` 运算符功能完全相同
- `get` 获取对象身上某个属性的值,类似于`target[key]`
- `set` 将值分配给属性的函数返回一个Boolean如果更新成功则返回true
- `deleteProperty` 作为函数的 `delete` 操作符,相当于执行 `delete target[key]`
- ···
代理对象的目的不再直接操作原始对象一切读写操作由代理完成。我们先前在编写Proxy的代理代码时仍然有操作原对象的行为
```js
const proxy = new Proxy(obj, {
set: function (target, key, newVal) {
console.log(`监听: ${key} 设置 ${newVal}`)
target[key] = newVal // 直接操作原对象
},
})
```
这时我们可以让Reflect登场代替我们对原对象进行操作之前的代码可以修改
```js
const proxy = new Proxy(obj, {
set: function (target, key, newVal) {
console.log(`监听: ${key} 设置 ${newVal}`)
Reflect.set(target, key, newVal)
},
get: function (target, key) {
console.log(`监听: ${key} 获取`)
return Reflect.get(target, key)
},
has: function (target, key) {
console.log(`监听: ${key} 判断`)
return Reflect.has(target, key)
}
})
```
使用Reflect替代之前的对象操作有以下好处
- 代理对象的目的:不再直接操作原对象
- Reflect.set方法有返回Boolean值可以判断本次操作是否成功
- receiver就是外层的Proxy对象
针对好处三,做出如下解释。以下述代码为例,`set name(){}`函数中的`this`指向的是`obj`
```js
const obj = {
_name: 'ziu',
set name(newVal) {
console.log(`set name ${newVal}`)
console.log(this)
this._name = newVal
},
get name() {
console.log(`get name`)
console.log(this)
return this._name
}
}
console.log(obj.name)
obj.name = 'Ziu'
```
```js
const proxy = new Proxy(obj, {
set: function (target, key, newVal, receiver) {
console.log(`监听: ${key} 设置 ${newVal}`)
Reflect.set(target, key, newVal, receiver)
},
get: function (target, key, receiver) {
console.log(`监听: ${key} 获取`)
return Reflect.get(target, key, receiver)
}
})
```
我们使用Proxy代理并且使用Reflect操作对象时输出的`this`仍然为`obj`,需要注意的是,此处的`this`指向是默认指向原始对象`obj`,而如果业务需要改变`this`指向,此时可以为`Reflect.set()`的最后一个参数传入`receiver`
### Reflect.construct方法
以下两段代码的实现结果是一样的:
```js
function Person(name, age) {
this.name = name
this.age = age
}
function Student(name, age) {
Person.call(this, name, age) // 借用
}
const stu = new Student('ziu', 18)
console.log(stu)
```
```js
function Person(name, age) {
this.name = name
this.age = age
}
function Student(name, age) {
// Person.call(this, name, age) // 借用
}
const stu = new Reflect.construct(Person, ['ziu', 18], Student)
console.log(stu)
```

View File

@ -0,0 +1,167 @@
# 深入理解浏览器缓存机制
浏览器有两种缓存规则:强制缓存与协商缓存
1. 强制缓存:不会向服务器发送请求,直接从缓存中读取资源
2. 协商缓存向服务器发送请求服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存如果命中则返回304状态码并带上新的response header通知浏览器从缓存中读取资源
- 共同点:都是从客户端缓存中读取资源
- 不同点:强制缓存不会发请求,协商缓存会发请求
## 强制缓存
### 什么是强制缓存
浏览器在服务器发起真正请求前,先检查浏览器缓存:
- 如果命中缓存,且缓存未过期,那么直接使用缓存资源
- 如果未命中缓存,或缓存已过期失效,那么向服务器发出请求
### 强制缓存的规则
服务器通过向响应头添加`Expires``Cache-Control`字段来标识强制缓存的状态,浏览器会将这两个信息缓存到本地,后续有相同请求时,优先到浏览器缓存中检查资源是否到期。
其中`Cache-Control`优先级比`Expires`高,即:二者同时存在时,浏览器以`Cache-Control`为标准,检查缓存资源是否过期
### Expires与Cache-Control
#### Expires
`Expires`表示当前资源的失效时间它的值是一个HTTP-日期时间戳,例如:`Expires: Thu, 01 Dec 1994 16:00:00 GMT`
使用`Expires`存在一些弊端:
- 代表的是绝对时间,如果浏览器和服务器的时间不同步,会导致缓存目标时间存在偏差
- 如果服务端设置的日期格式不规范,那么等同于无缓存
- `Expires``HTTP/1.0`的字段,但是现在浏览器默认使用的是`HTTP/1.1`
在某些不支持HTTP1.1的环境下,`Expires`就会发挥用处
所以`Expires`其实是过时的产物,现阶段它的存在只是一种兼容性的写法
#### Cache-Control
如果在`Cache-Control`响应头设置了"max-age"或者"s-max-age"指令,那么`Expires`头会被忽略
设置`Cache-Control`的值有以下规则:
- 不区分大小写,但建议使用小写
- 多个指令以逗号分隔
- 具有可选参数,可以用令牌或者带引号的字符串语法
常用的指令:
- public所有内容都将被缓存即使是通常不可缓存的内容如POST请求
- private所有内容只有客户端可以缓存不能作为共享缓存即代理服务器不能缓存它这也是`Cache-Control`的默认取值
- no-cache客户端缓存内容但是是否使用缓存则需要经过协商缓存来验证决定
- no-store所有内容都不会被缓存即不使用强制缓存也不使用协商缓存即不使用任何缓存。
- max-age=xxx (xxx is numeric)缓存内容将在xxx秒后失效
举几个例子:
此次请求之后的600秒内如果浏览器再次发起请求那么直接使用缓存中的资源
```
Cache-Control: max-age=600
```
浏览器可以缓存资源,但每次使用缓存资源前都必须重新验证其有效性:
```
Cache-Control: no-cache
```
```
Cache-Control: max-age=0, must-revalidate
```
这意味着每次都会发起 HTTP 请求,但当缓存内容仍有效时可以跳过 HTTP 响应体的下载
## 协商缓存
当浏览器检查本地的**强制缓存**已经失效后,浏览器携带该资源的**协商缓存**标识向服务器发起请求,由服务器根据缓存标识决定是否继续使用本地缓存。
- 协商缓存生效服务器返回304通知浏览器继续使用本地缓存
- 协商缓存失效服务器返回200与最新的请求资源
### 协商缓存的规则
服务器与浏览器通过两两成对的请求头来控制协商缓存:
- `Etag` `If-None-Match`
- `Last-Modified` `If-Modified-Since`
其中,`Etag``Last-Modified`是由服务器设置的响应头的字段,`If-None-Match``If-Modified-Since`则是浏览器向服务器发送的请求头的字段
#### Etag与Last-Modified
`Etag`是上一次加载资源时服务器返回的ResponseHeader是对该资源的一种唯一标识只要资源有变化`Etag`就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的`Etag`值放到RequestHeader里的`If-None-Match`里,服务器接受到`If-None-Match`的值后,会拿来跟该资源文件的`Etag`值做比较,如果相同,则表示资源文件没有发生改变,命中协商缓存。
- `Etag`由服务器生成,标志当前资源的唯一标识,一般包含大小、修改时间等信息
- `If-None-Match`浏览器缓存到本地的`Etag`
HTTP协议并未规定`Etag`的内容是如何生成的,但一般包含大小、修改时间等信息
Node.js下生成`Etag`的示例:
```js
// 根据文件的fs.Stats信息计算出etag
const genEtag = (stat) => {
const fileLength = stat.size // 文件的大小
const fileLastModifiedTime = stat.mtime.getTime() // 文件的最后更改时间
// 数字都用16进制表示
return `${fileLength.toString(16)}-${fileLastModifiedTime.toString(16)}`
}
```
#### Last-Modified与If-Modified-Since
`Last-Modified`是该资源文件最后一次更改时间服务器会在ResponseHeader里返回同时浏览器会将这个值保存起来在下一次发送请求时放到RequestHeader里的`If-Modified-Since`里,服务器在接收到后也会做比对,如果相同则命中协商缓存。
- `Last-Modified`由服务器添加,标志资源文件上次被修改的时间
- `If-Modified-Since`浏览器缓存到本地的`Last-Modified`
`If-None-Match`的优先级要高于`If-Modified-Since`,即:如果浏览器同时存在
#### 两种协商缓存的区别
- 精确度上,`Etag`要优于`Last-Modified``Last-Modified`的时间单位是秒如果某个文件在1秒内改变了多次那么他们的`Last-Modified`其实并没有体现出来修改但是Etag每次都会改变确保了精度如果是负载均衡的服务器各个服务器生成的`Last-Modified`也有可能不一致。
- 性能上,`Etag`要逊于`Last-Modified`,毕竟`Last-Modified`只需要记录时间,而`Etag`需要服务器通过算法来计算出一个值。
- 优先级上,服务器校验优先使用`Etag`
## 内存缓存与硬盘缓存
当我们打开一个新网页服务器返回200将资源发送给浏览器浏览器做本地缓存
当我们刷新标签页,浏览器从内存缓存获得资源
当我们关闭标签页重新打开,浏览器从硬盘缓存获得资源
- **内存缓存**(MemoryCache):内存缓存具有两个特点,分别是快速读取和时效性
- 快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。
- 时效性:一旦该进程关闭,则该进程的内存则会清空。
- **硬盘缓存**(DiskCache)硬盘缓存则是直接将缓存写入硬盘文件中读取缓存需要对该缓存存放的硬盘文件进行I/O操作然后重新解析该缓存内容读取复杂速度比内存缓存慢。
在浏览器中浏览器会在js和图片等文件解析执行后直接存入内存缓存中那么当刷新页面时只需直接从内存缓存中读取(MemoryCache)而css文件则会存入硬盘文件中所以每次渲染页面都需要从硬盘读取缓存(DiskCache)。
## 用户对浏览器缓存的控制
- 地址栏访问,链接跳转是正常用户行为,将会触发浏览器缓存机制
- F5刷新浏览器会设置max-age=0跳过强缓存判断会进行协商缓存判断
- Ctrl+F5刷新跳过强缓存和协商缓存直接从服务器拉取资源
## 参考资料
[[稀土掘金] 彻底理解浏览器的缓存机制](https://juejin.cn/post/6844903593275817998)
[[微信公众号] 浏览器的缓存机制小结](https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651226262&idx=1&sn=2128db200b88479face67ed8e095757c&chksm=bd4959128a3ed0041b43a5683c75c4b88c7d35fac909a59c14b4e9fc11e8d408680b171d2706&scene=21#wechat_redirect)
[[微信公众号] 浏览器缓存机制剖析](https://mp.weixin.qq.com/s?__biz=MjM5MTA1MjAxMQ==&mid=2651226347&idx=1&sn=6dbccc54406f0b075671884b738b1e88&chksm=bd49596f8a3ed079f79cda4b90ac3cb3b1dbdb5bfb8aade962a16a323563bf26a0c75b0a5d7b&scene=21#wechat_redirect)
[[RFC-9111] Expires](https://httpwg.org/specs/rfc9111.html#field.expires)
[[MDN] Expires](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Expires)
[[MDN] Cache-Control](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cache-Control)
[[MDN] ETag](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/ETag)

View File

@ -0,0 +1,285 @@
# 深入理解浏览器运行原理
## 网页解析过程
输入域名 => DNS解析为IP => 目标服务器返回`index.html`
> DNSDomain Name System
## HTML解析过程
- 浏览器开始解析`index.html`文件,当遇到`<link>`则向服务器请求下载`.css`文件
- 遇到`<script>`标签则向服务器请求下载`.js`文件
<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0a90afa7c2534ae78f3eab9200a0095b~tplv-k3u1fbpfcp-watermark.image?" alt="浏览器解析HTML过程" width="70%" />
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/630cf2fd1d3748d6846fe7f2ab99a01b~tplv-k3u1fbpfcp-watermark.image?" alt="浏览器是和如何工作的" width="70%" />
[How browsers work](https://web.dev/howbrowserswork/)
## 生成CSS规则
在解析的过程中,如果遇到`<link>`元素那么会由浏览器负责下载对应的CSS文件
- 注意下载CSS文件不会影响到DOM解析
- 有单独一个线程对CSS文件进行下载与解析
浏览器下载完CSS文件后就会对CSS文件进行解析解析出对应的规则树
- 我们可以称之为CSSOMCSS Object ModelCSS对象模型
## 构建Render Tree
有了DOM Tree和CSSOM Tree之后就可以将二者结合构建Render Tree了
此时如果有某些元素的CSS属性`display: none;`那么这个元素就不会出现在Render Tree中
- 下载和解析CSS文件时不会阻塞DOM Tree的构建过程
- 但会阻塞Render Tree的构建过程因为需要对应的CSSOM Tree
## 布局和绘制(Layout & Paint)
第四步是在渲染树Render Tree上运行布局Layout以计算每个节点的几何体
- 渲染树会表示显示哪些节点以及其他的样式,但是不表示每个节点的尺寸、位置等信息
- 布局是确定呈现树中所有节点的宽度、高度和位置信息
第五步是将每个节点绘制Paint到屏幕上
- 在绘制阶段浏览器布局阶段计算的每个frame转为屏幕上实际的像素点
- 包括将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素
## 回流和重绘(Reflow & )
回流也可称为重排
理解回流Reflow
- 第一次确定节点的大小和位置称之为布局layout
- 之后对节点的大小、位置修改重新计算,称之为回流
什么情况下会引起回流?
- DOM 结构发生改变(添加新的节点或者移除节点)
- 改变了布局修改了width height padding font-size等值
- 窗口resize修改了窗口的尺寸等
- 调用getComputedStyle方法获取尺寸、位置信息
理解重绘Repaint
- 第一次渲染内容称之为绘制paint
- 之后的重新渲染称之为重绘
什么情况下会引起重绘?
- 修改背景色、文字颜色、边框颜色、样式等
**回流一定会引起重绘,所以回流是一件很消耗性能的事情**
- 开发中要尽量避免发生回流
- 修改样式尽量一次性修改完毕
- 例如通过cssText一次性设置样式或通过修改class的方式修改样式
- 尽量避免频繁的操作DOM
- 可以在一个DocumentFragment或者父元素中将要操作的DOM操作完成再一次性插入到DOM树中
- 尽量避免通过getComputedStyle获取元素尺寸、位置等信息
- 对某些元素使用position的absolute或fixed属性
- 并不是不会引起回流,而是开销相对较小,不会对其他元素产生影响
## 特殊解析: composite合成
在绘制的过程中,可以将布局后的元素绘制到多个合成图层中
- 这是浏览器的一种优化手段
- 将不同流生成的不同Layer进行合并
```
标准流 => LayouTree => RenderLayer
`position:fixed;` => RenderLayer
```
默认情况标准流中的内容都是被绘制在同一个图层Layer中的
而一些特殊的属性浏览器会创建一个新的合成层CompositingLayer并且新的图层可以利用GPU来加速绘制
- 每个合成层都是单独渲染的
- 单独渲染可以避免所有的动画都在同一层中渲染导致性能问题
- 在各自的层中渲染完成后,只需要将渲染结果更新回合成层即可
当元素具有哪些属性时,浏览器会为其创建新的合成层呢?
- 3D Transforms
- video canvas iframe
- opacity 动画转换时
- position: fixed
- will-change: 一个实验性的属性,提前告诉浏览器此元素可能发生哪些变化
- animation 或 transition设置了opacity、transform
### 案例1同一层渲染
```
.box1 {
 width: 100px;
 height: 100px;
 background-color: red;
}
.box2 {
 width: 100px;
 height: 100px;
 background-color: blue;
}
```
```
<body>
 <div class="box1"></div>
 <div class="box2"></div>
</body>
```
在开发者工具的图层工具中可以看到,两个元素`.box1``.box2`都是在一个层Document下渲染的
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/373fb3a4d5284e6c80c3ec519918e6e4~tplv-k3u1fbpfcp-watermark.image?" alt="image-20221122103111654.png" width="70%" />
### 案例2分层渲染
当我们为`.box2`添加上`position: fixed;`属性,这时`.box2`将在由浏览器创建出来的合成层,分层单独渲染
```
.box2 {
 width: 100px;
 height: 100px;
 background-color: blue;
 position: fixed;
}
```
<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6116d83ee8c041f586d627e549fdd5bf~tplv-k3u1fbpfcp-watermark.image?" alt="image-20221122103256116.png" width="70%" />
### 案例3transform 3D
为元素添加上`transform`属性时浏览器也会为对应元素创建一个合成层需要注意的是只有3D的变化浏览器才会创建
如果是`translateX``translateY`则不会
```
.box2 {
 width: 100px;
 height: 100px;
 background-color: blue;
 /* position: fixed; */
 transform: translateZ(10px);
}
```
<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/befb4826b079439f81c98b03586a36e5~tplv-k3u1fbpfcp-watermark.image?" alt="image-20221122103715428.png" width="70%" />
### 案例4transition+transform
当我们为元素添加上动画时,动画的中间执行过程的渲染会在新的图层上进行,但是中间动画渲染完成后,结果会回到原始图层上
```
.box2 {
 width: 100px;
 height: 100px;
 background-color: blue;
 transition: transform 0.5s ease;
}
.box2:hover {
 transform: translateY(10px);
}
```
- 这也是使用`transform`执行动画性能更高的原因,因为浏览器会为动画的执行过程单独创建一个合成层
- 如果是通过修改`top` `left`等定位属性实现的动画,是在原始的图层上渲染完成的。“牵一发则动全身”,动画过程中将导致整个渲染树回流与重绘,极大的影响性能
### 案例5transition+opacity
`transform`类似,使用`transition`过渡的`opacity`动画,浏览器也会为其创建一个合成层
```
.box2 {
 width: 100px;
 height: 100px;
 background-color: blue;
 opacity: 1;
 transition: opacity 0.5s ease;
}
.box2:hover {
 opacity: 0.2;
}
```
### 总结
分层确实可以提高性能但是它是以内存管理为代价的因此不应当作为Web性能优化策略的一部分过度使用
## 浏览器对script元素的处理
之前我们说到,在解析到`link`标签时浏览器会异步下载其中的css文件并在DOM树构建完成后将其与CSS Tree合成为RenderTree
但是当浏览器解析到`script`标签时,整个解析过程将被阻塞,当前`script`标签后面的DOM树将停止解析直到当前`script`代码被下载、解析、执行完毕才会继续解析HTML构建DOM树
为什么要这样做呢?
- 这是因为Javascript的作用之一就是操作DOM并且可以修改DOM
- 如果我们等到DOM树构建完成并且渲染出来了再去执行Javascript会造成回流和重绘严重影响页面性能
- 所以当浏览器构建DOM树遇到`script`标签时会优先下载和执行Javascript代码而后再继续构建DOM树
这也会带来新的问题,比如在现代的页面开发中:
- 脚本往往比HTML更“重”浏览器也需要花更多的时间去处理脚本
- 会造成页面的解析阻塞,在脚本下载、解析、执行完成之前,用户在界面上什么也看不到
为了解决这个问题,浏览器的`script`标签为我们提供了两个属性attribute`defer``async`
## defer属性
`defer` 即推迟,为`script`标签添加这个属性相当于告诉浏览器不要等待此脚本下载而是继续解析HTML构建DOM Tree
- 脚本将由浏览器进行下载但是不会阻塞DOM Tree的构建过程
- 如果脚本提前下载好了那么它会等待DOM Tree构建完成`DOMContentLoaded`**事件触发之前**先执行`defer`中的代码
```
<script>
 console.log('script enter')
 window.addEventListener('DOMContentLoaded', () => {
   console.log('DOMContentLoaded enter')
})
</script>
<script src="./defer.js" defer></script>
```
```
// defer.js
console.log('defer script enter')
```
上述代码在控制台的输出为:
```
script enter
defer script enter
DOMContentLoaded enter
```
- 多个带`defer`的脚本也是按照自上至下的顺序执行的
- 从某种角度来说,`defer`可以提高页面的性能,并且推荐放到`head`元素中
- 注意:`defer`仅适用于外部脚本,对于`script`标签内编写的默认`JS`代码会被忽略掉
## async属性
`async`属性也可以做到让脚本异步加载而不阻塞DOM树的构建它与`defer`的区别:
- 用`async`标记的脚本是**完全独立**的
- `async`脚本不能保证执行顺序,因为它是独立下载、独立运行,不会等待其他脚本
- 使用`async`标记的脚本不会保证它将在`DOMContentLoaded`之前或之后被执行
要使用`async`属性标记的`script`操作DOM必须在其中使用`DOMContentLoaded`监听器的回调函数在该事件触发DOM树构建完毕执行相应的回调函数

View File

@ -25,13 +25,14 @@ navbar: false
**首次安装需要设置“跟随主程序同时启动”** **首次安装需要设置“跟随主程序同时启动”**
- ✅ 监听剪贴板并持续将新内容更新到本地磁盘 数据读写**完全本地化** - ✅ 监听剪贴板并持续将新内容更新到本地磁盘 数据读写**完全本地化**
- ✅ 按下`Shift``空格`进入**多选模式** 连续选择多条内容合并复制 支持**跨标签**合并复制/粘贴
- ✅ 快速`收藏`/`转存`/`分词`/`复制`/`删除`/`打开文件&目标文件夹` - ✅ 快速`收藏`/`转存`/`分词`/`复制`/`删除`/`打开文件&目标文件夹`
- ✅ 功能按钮 定义**无限可能** `OCR识别` `百度搜索` `百度识图` `统计文本字数` `颜色管理` `识别图片中二维码` `上传到图床` `翻译` - ✅ 功能按钮 定义**无限可能** `OCR识别` `百度搜索` `百度识图` `统计文本字数` `颜色管理` `识别图片中二维码` `上传到图床` `翻译`
- ✅ `鼠标左键` 复制并粘贴 `鼠标右键` 仅复制 - ✅ `鼠标左键` 复制并粘贴 `鼠标右键` 仅复制
- ✅ 按下`Shift``空格`进入**多选模式** 连续选择多条内容合并复制 支持**跨标签**合并复制/粘贴
- ✅ 键盘 `↑` `↓` 选中历史记录,按下回车直接粘贴 - ✅ 键盘 `↑` `↓` 选中历史记录,按下回车直接粘贴
- ✅ 使用 `Ctrl/Alt+数字键` 快速粘贴指定条的内容 使用 `Tab` 键切换分类 - ✅ 键盘 `←` `→`切换分类 `Tab`键连续切换分类
- ✅ 插件内`按下任意键`自动聚焦搜索框 支持多个关键词**同时检索** - ✅ 使用 `Ctrl/Alt+数字键` 快速粘贴
- ✅ 插件内`输入任意字母或数字`自动聚焦搜索框 支持使用`空格`同时检索**多个关键词**
- ✅ **智慧分词** 快速拖选指定内容 **超级粘贴** 直接转存为文件 - ✅ **智慧分词** 快速拖选指定内容 **超级粘贴** 直接转存为文件
- ✅ 优雅的界面动效与交互 跟随系统的深色模式 - ✅ 优雅的界面动效与交互 跟随系统的深色模式
- ✅ 优秀的剪贴板监听性能 强大的自定义功能按钮 自搭建多端同步 ··· - ✅ 优秀的剪贴板监听性能 强大的自定义功能按钮 自搭建多端同步 ···

View File

@ -1,5 +1,11 @@
# 插件会员 # 插件会员
:::tip
超级剪贴板是一款 “随心所欲” 的软件。您可以永久免费使用它。
但是,如果您真的喜欢它,您可以付费支持它的发展。作为答谢,您将获得更多更方便的功能。这取决于您,如果您不想,也没关系。谢谢你,祝你有美好的一天!☀️
:::
## 会员权益 ## 会员权益
- **插件会员** - **插件会员**

View File

@ -5,6 +5,7 @@
"preview": "vitepress preview docs" "preview": "vitepress preview docs"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^18.11.19",
"vitepress": "1.0.0-alpha.43", "vitepress": "1.0.0-alpha.43",
"vue": "^3.2.45" "vue": "^3.2.45"
}, },

21
pnpm-lock.yaml generated
View File

@ -1,6 +1,7 @@
lockfileVersion: 5.4 lockfileVersion: 5.4
specifiers: specifiers:
'@types/node': ^18.11.19
medium-zoom: ^1.0.8 medium-zoom: ^1.0.8
vitepress: 1.0.0-alpha.43 vitepress: 1.0.0-alpha.43
vue: ^3.2.45 vue: ^3.2.45
@ -9,7 +10,8 @@ dependencies:
medium-zoom: registry.npmmirror.com/medium-zoom/1.0.8 medium-zoom: registry.npmmirror.com/medium-zoom/1.0.8
devDependencies: devDependencies:
vitepress: registry.npmmirror.com/vitepress/1.0.0-alpha.43 '@types/node': registry.npmmirror.com/@types/node/18.11.19
vitepress: registry.npmmirror.com/vitepress/1.0.0-alpha.43_@types+node@18.11.19
vue: registry.npmmirror.com/vue/3.2.45 vue: registry.npmmirror.com/vue/3.2.45
packages: packages:
@ -480,6 +482,12 @@ packages:
dev: true dev: true
optional: true optional: true
registry.npmmirror.com/@types/node/18.11.19:
resolution: {integrity: sha512-YUgMWAQBWLObABqrvx8qKO1enAvBUdjZOAWQ5grBAkp5LQv45jBvYKZ3oFS9iKRCQyFjqw6iuEa1vmFqtxYLZw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/node/-/node-18.11.19.tgz}
name: '@types/node'
version: 18.11.19
dev: true
registry.npmmirror.com/@types/web-bluetooth/0.0.16: registry.npmmirror.com/@types/web-bluetooth/0.0.16:
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz} resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz}
name: '@types/web-bluetooth' name: '@types/web-bluetooth'
@ -496,7 +504,7 @@ packages:
vite: ^4.0.0 vite: ^4.0.0
vue: ^3.2.25 vue: ^3.2.25
dependencies: dependencies:
vite: registry.npmmirror.com/vite/4.0.4 vite: registry.npmmirror.com/vite/4.0.4_@types+node@18.11.19
vue: registry.npmmirror.com/vue/3.2.45 vue: registry.npmmirror.com/vue/3.2.45
dev: true dev: true
@ -870,8 +878,9 @@ packages:
engines: {node: '>=4'} engines: {node: '>=4'}
dev: true dev: true
registry.npmmirror.com/vite/4.0.4: registry.npmmirror.com/vite/4.0.4_@types+node@18.11.19:
resolution: {integrity: sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/vite/-/vite-4.0.4.tgz} resolution: {integrity: sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/vite/-/vite-4.0.4.tgz}
id: registry.npmmirror.com/vite/4.0.4
name: vite name: vite
version: 4.0.4 version: 4.0.4
engines: {node: ^14.18.0 || >=16.0.0} engines: {node: ^14.18.0 || >=16.0.0}
@ -897,6 +906,7 @@ packages:
terser: terser:
optional: true optional: true
dependencies: dependencies:
'@types/node': registry.npmmirror.com/@types/node/18.11.19
esbuild: registry.npmmirror.com/esbuild/0.16.16 esbuild: registry.npmmirror.com/esbuild/0.16.16
postcss: registry.npmmirror.com/postcss/8.4.21 postcss: registry.npmmirror.com/postcss/8.4.21
resolve: registry.npmmirror.com/resolve/1.22.1 resolve: registry.npmmirror.com/resolve/1.22.1
@ -905,8 +915,9 @@ packages:
fsevents: registry.npmmirror.com/fsevents/2.3.2 fsevents: registry.npmmirror.com/fsevents/2.3.2
dev: true dev: true
registry.npmmirror.com/vitepress/1.0.0-alpha.43: registry.npmmirror.com/vitepress/1.0.0-alpha.43_@types+node@18.11.19:
resolution: {integrity: sha512-bguPWYojF371vIfTY8jGmKeFroRe6UVGmDp3+PjTnVq57wTlz9x88OKm6KKF4l/1D7GugkHex5X9wNcOdCmCiQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/vitepress/-/vitepress-1.0.0-alpha.43.tgz} resolution: {integrity: sha512-bguPWYojF371vIfTY8jGmKeFroRe6UVGmDp3+PjTnVq57wTlz9x88OKm6KKF4l/1D7GugkHex5X9wNcOdCmCiQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/vitepress/-/vitepress-1.0.0-alpha.43.tgz}
id: registry.npmmirror.com/vitepress/1.0.0-alpha.43
name: vitepress name: vitepress
version: 1.0.0-alpha.43 version: 1.0.0-alpha.43
hasBin: true hasBin: true
@ -918,7 +929,7 @@ packages:
'@vueuse/core': registry.npmmirror.com/@vueuse/core/9.12.0_vue@3.2.45 '@vueuse/core': registry.npmmirror.com/@vueuse/core/9.12.0_vue@3.2.45
body-scroll-lock: registry.npmmirror.com/body-scroll-lock/4.0.0-beta.0 body-scroll-lock: registry.npmmirror.com/body-scroll-lock/4.0.0-beta.0
shiki: registry.npmmirror.com/shiki/0.13.0 shiki: registry.npmmirror.com/shiki/0.13.0
vite: registry.npmmirror.com/vite/4.0.4 vite: registry.npmmirror.com/vite/4.0.4_@types+node@18.11.19
vue: registry.npmmirror.com/vue/3.2.45 vue: registry.npmmirror.com/vue/3.2.45
transitivePeerDependencies: transitivePeerDependencies:
- '@algolia/client-search' - '@algolia/client-search'

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"types": ["node"],
"skipLibCheck": true,
"noEmit": true,
"baseUrl": "."
},
"include": ["docs/.vitepress/**/*.ts"],
"exclude": ["node_modules", "dist"]
}