bLanK's blog

攻无不克,战无不胜

vue2和vue3都重写了数组方法

vue2的defineProperty

1
2
3
4
5
6
7
8
9
10
11
12
13
function defineReactive(obj, key) {
let _val = obj[key];
Object.defineProperty(obj, key, {
get() {
console.log('get')
return _val;
},
set(newVal) {
console.log('set')
_val = newVal;
}
})
}

defineProperty利用闭包, 对_val进行set和get,称为数据劫持。

  • defineProperty的缺陷

由此可见defineProperty的响应式粒度是key级别的。也就是说,已知的属性才有办法劫持其getter和setter,所以其根本没有办法对未知的key劫持做响应。

  • 为什么vue2不对数组进行劫持?而是重写数组方法?
阅读全文 »

jsx 本质

再js里写html => 编译为 ReactDom对象,

使用React.creatElement() 同样可以创建ReactDom对象

jsx里的模板解析

  • String, Number: 直接渲染
  • Boolean: 不渲染
  • null, undefined, function: 不渲染
  • Object:只能渲染ReactDom 对象
  • Array: 每一项都单独渲染
  • 表达式: 会运行
阅读全文 »

包管理

npm

pnpm、npm、yarn 包管理工具『优劣对比』及『环境迁移』 - 掘金 (juejin.cn)

v2 早期递归依赖,导致重复安装问题

v3 扁平化解决递归依赖, 扁平化依赖算法耗时长,下载需要更长时间

  • 因为如果两个模块依赖同一个模块但版本不同,npm v3会尝试将其中一个版本的模块安装在顶层,另一个版本的模块安装在依赖它的模块下面

v5 package-lock.json解决扁平化耗时长问题:

  1. 锁定包精确版本
  2. 锁定下载地址
  3. 锁定包直接依赖关系

package-lock.json 的作用

  1. 记录项目所有依赖的精确版本号
  2. 提供生成确切的依赖树,可视化
  3. 优化依赖安装过程,跳过重复依赖
  4. 防止依赖意外更新

pnpm

  1. 采用了硬链接来直接指向依赖磁盘地址,避免重复安装依赖
  2. 默认支持monorepo 多项目管理
  3. 替代nvm pnpm env use --global [version]

什么是回流重绘

HTML中每个元素都可视为一个盒子,在浏览器解析时

  • 回流:当元素的尺寸、位置或某些属性发生变化时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来。这个过程就叫做回流。例如,修改元素的宽度、高度、或者添加或删除可见的DOM元素,都会触发回流。
  • 重绘:当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

回流必将引起重绘,而重绘不一定会引起回流

回流触发时机

回流一般在于元素的几何信息(大小、位置)发生变化时触发。

  1. 添加或删除节点
  2. 元素大小或位置改变
  3. 页面初次渲染
  4. 浏览器窗口尺寸变化(因为回流根据视口来计算的)

除此之外一些获取几何信息的属性也会触发回流

offsetTop offsetLeft offsetWidth offsetHeight
scroll-
client-
getBoundingClientRect() getComputedStyle()

重绘触发时机

  1. 颜色、阴影的修改
  2. 通过visibility、opacity 显示和隐藏
  3. 文本对齐方向

修改文本排列方向是会回流的,修改对齐方向不回流

浏览器优化

由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列

当你获取布局信息的操作的时候,会强制队列刷新,包括前面讲到的offsetTop等方法都会返回最新的数据

因此浏览器不得不清空队列,触发回流重绘来返回正确的值

如何减少回流重绘

  1. 通过改变元素的 class 类名设定元素的样式:这样做可以一次性地应用多个样式,而不需要逐个修改样式属性。这样可以减少浏览器重新计算样式和布局的次数,从而减少回流和重绘。
  2. 避免设置多项内联样式:内联样式会直接修改元素的样式,每修改一次都可能触发回流和重绘。使用class类名可以一次性地应用多个样式,减少回流和重绘的次数。
  3. 应用元素的动画,使用 position 属性的 fixed 值或 absolute 值:这样可以使元素脱离文档流,其位置的改变不会影响到其他元素的布局,从而减少回流。但是,这可能会增加重绘的次数,因为元素的位置改变可能会改变其可见性。
  4. 避免使用 table 布局:table 中每个元素的大小以及内容的改动,都会导致整个 table 的重新计算,从而触发回流。使用其他布局方式,如 flex 或 grid,可以减少回流的次数。
  5. 对于那些复杂的动画,对其设置 position: fixed/absolute:这样可以使元素脱离文档流,其位置的改变不会影响到其他元素的布局,从而减少回流。但是,这可能会增加重绘的次数,因为元素的位置改变可能会改变其可见性。
  6. 使用css3硬件加速:硬件加速可以让一些复杂的动画(如 transform、opacity、filters)在 GPU 而不是 CPU 上运行,这可以避免触发回流和重绘,从而提高性能。
  7. 避免使用 CSS 的 JavaScript 表达式:CSS 的 JavaScript 表达式会在每次页面渲染时都被重新计算,这可能会触发大量的回流和重绘。避免使用它们可以减少回流和重绘的次数。

HTTPS = HTTP + SSL/TLS

  1. http 明文传输,易被劫持,SEO降权
  2. TLS 传输层安全协议,是SSL的升级方案
阅读全文 »

0. 当前工程化痛点

主流webpack需要把整个项目编译好后再交给dev server,因此大型项目devserver启动时间很长,HMR也需要很长时间。
因此,Vite应运而生

Webpack

当我们使用webpack启动项目时,webpack会根据我们配置文件中的入口文件,

  1. 分析出项目项目所有依赖关系,
  2. 然后打包成一个文件(bundle.js),
  3. 交给浏览器去加载渲染。

在Webpack中,构建过程大概分为这么几个阶段初始化Init、构建Make、生成Seal

  • 初始化阶段: 修整配置参数,创建 Compiler、Compilation 等基础对象,并初始化插件及若干内置工厂、工具类,并最终根据 entry 配置,找到所有入口模块
  • 构建阶段: 从entry找到入口,调用loader编译模块,遍历 AST 找出模块依赖的模块,之后递归遍历所有依赖块,构建出 模块依赖关系图 (dependency graph)
  • 输出阶段: 根据output的配置,将模块拆解成不同的chunk对象,经过一系列优化,再将代码翻译成产物

描述得比较简单, 实际完成这些过程是比较复杂的

HMR

每次修改一个模块,webpack要遍历整个依赖图来找出所有依赖于该模块的其他模块。再进行局部的重新编译。
三个阶段:

  1. 找出过期的模块
  2. 从缓存中删除过期的模块
  3. 把新模块添加到modules中

webpack4以前是通过jsonp来拉取更新模块的,webpack4以及以后通过ws通信+ajax请求

devServer

原理是通过在内存中创建虚拟文件系统来提供开发服务器功能。它监听文件变化并通过 WebSocket 与浏览器通信,以实现HMR,提供高效的开发环境。

  1. 启动服务器;在本地启动http服务器
  2. 读入内存;把项目文件读入内存
  3. 编译构建;所有编译都在内存中完成,无需写入磁盘
  4. 请求转发;对于静态资源直接从内存中提供给客户端
  5. HMR;通过http升级websocket。

优化打包速度

  1. 禁止大图片转base64
  2. 利用webpack-bound-analyze分析
  3. CDN引入公共库,减少webpack打包量
  4. 开启缓存,以便在后续构建中重复使用之前的结果 cache: true
  5. 开启多线程,利用thread-loader

Vite

底层
基于esbuild和rollup,利用浏览器对ESM的支持

  • esbuild: 预构建。把不同模块规范统一编译为ESM
  • rollup:在build时打包成生产环境代码

ESM
ESM === ES module, 大部分浏览器都支持<script type='module'>
<script type="module">中,浏览器遇到内部的import引用时,会自动发起http请求,去加载对应的模块。

ESM 支持编译时就能确定模块的依赖关系

开发环境

  1. 预构建。
    1. 针对node_modules中的第三方依赖,由esbuild把所有模块编译为ESM。
    2. 整合模块,对于具有许多内部模块的,整合成一个模块。避免发送太多http请求。
    3. 缓存。把预构建好的ESM存到node_modules/.vite下。
  2. 冷启动devServer。
  3. 按需编译。页面需要某些模块会发送http请求,此时vite再进行编译,编译后发给浏览器。
    此时编译是对src源码的编译,包括对ts、less、esm、图片、字体的处理
    1. 利用http2.0 支持并发请求。
    2. 利用http缓存,
      1. node_modules 使用强制缓存
      2. src下源码 使用协商缓存

注意:
按需编译,编译了:
typescript、vue、jsx、css预处理语言、静态资源等。实际上都转换为了浏览器可以解析的html/js/css,但是由于SouceMap方便开发者,我们在F12网络看到的资源依然是index.ts这样的后缀。

HMR

当修改一个文件时vite精确定位到该模块,使其失活。vite重新编译该模块后再发送给浏览器。浏览器利用动态导入import()来替换。

原理:利用websocket连接浏览器,建立连接后监听文件变化。服务器向浏览器注入代码用于处理ws消息(重新请求模块、刷新页面)

重新请求模块依然走ajax请求,而不是ws;目的是为了保证ws只用于通信上。

生产环境

基于rollup打包,rollup轻量、打包更快。

为什么vite比webpack快

从几方面回答:

  1. vite基于esbuild、rollup
    1. esbuild基于Go,善于利用多核cpu
    2. rollup轻量,打包更快
  2. vite的预构建、冷启动、按需编译
  3. HMR的区别
  4. vite内置功能多,使用成本低

思想

大部分算法可以看作对于二叉树的操作

二叉树解题的思维模式分两类:

1、是否可以通过遍历一遍二叉树得到答案 ?如果可以,用一个 traverse 函数配合外部变量来实现,这叫「遍历」的思维模式。

2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案 ?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。

无论使用哪种思维模式,你都需要思考:

如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做 ?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。

  • 比如快排是二叉树前序遍历
1
2
3
4
5
6
7
8
9
void sort(int[] nums, int lo, int hi) {
/****** 前序遍历位置 ******/
// 通过交换元素构建分界点 p
int p = partition(nums, lo, hi);
/************************/

sort(nums, lo, p - 1);
sort(nums, p + 1, hi);
}
  • 归并排序是二叉树后序遍历
1
2
3
4
5
6
7
8
9
10
11
12
void sort(int[] nums, int lo, int hi) {
int mid = (lo + hi) / 2;
// 排序 nums[lo..mid]
sort(nums, lo, mid);
// 排序 nums[mid+1..hi]
sort(nums, mid + 1, hi);

/****** 后序位置 ******/
// 合并 nums[lo..mid] 和 nums[mid+1..hi]
merge(nums, lo, mid, hi);
/*********************/
}

前中后序是遍历二叉树过程中处理每一个节点的三个特殊时间点

  1. 前序位置的代码在刚刚进入一个二叉树节点的时候执行;
  2. 后序位置的代码在将要离开一个二叉树节点的时候执行;
  3. 中序位置的代码在一个二叉树节点左子树都遍历完,即将开始遍历右子树的时候执行。
阅读全文 »

js中实现异步有三大方式: 回调 Promise generator

  • 回调

    • 优势:简单、语义清晰
    • 劣势:回调地狱
  • Promise

    • 优势: 链式调用解决回调地狱、Promise.all 可以处理多个异步
    • 劣势:需要适用Promise语法
  • Generator

    • 优势:利用yield编写可读性更强、可以恢复和暂停、是async/await的底层原理、
    • 劣势:不是专门解决异步的,需要搭配Promise、要手动编写生成器和迭代器使得编码更复杂

现在我们来详细聊聊generator实现异步

场景

我要读一个文件dir.txt 这是一部目录,里面记录了 Lzy.json , XXX.json …. 等等 用户文件。

然后我们要读取Lzy.json 的信息。

async/await 方式解决

1
2
3
4
5
6
7
async function read() {
const dir = await readFile('./txt/dir.txt', 'utf8');
const userInfo = await readFile(path.resolve('./txt', dir), 'utf-8');
return userInfo
}

read().then(res=>console.log(res))

generator方式解决

1
2
3
4
5
6
7
8
9
10
11
12
13
function* read() {
const dir = yield readFile('./txt/dir.txt', 'utf8');
console.log(dir)
const userInfo = yield readFile(path.resolve('./txt', dir), 'utf-8');
console.log(userInfo)
return userInfo
}
const it = read() // 获取生成器对象
const res1 = it.next();
res1.value.then(res => {
// 相当于dir.txt完成后把值传给read函数的dir变量, 并执行一步生成器
it.next(res);
})

这样只执行了第一步,而我们得到第一步的PromiseResult之后要继续递归的运行next(PromiseResult) 这样才能得到第二步。

所以我们需要一个执行器 co

1
2
let finalRes= co(read())
finalRes.then(res=> console.log(res)) // 一步到位,输出task3完成时的PromiseResult

实现 co

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function co(gen) {
return new Promise((resolve, reject) => {
next();
function next(context) { // context 就是上一步异步任务完成的PromiseResult
const { value, done } = gen.next(context);
if (done)
resolve(value)
else
// Promsie.resolve(value) 为了防止value是普通值。
Promise.resolve(value).then(val => next(val))
}

})
}

context 就是上一步yield后面的表达式完成时的PromiseResult

例如: const dir = yield readFile('./txt/dir.txt', 'utf8'); 就是把context 赋值给了dir

Worker

创建一个worker对象,可以在浏览器中创建一个新线程不阻塞UI线程渲染。

  • 必须遵守同源策略
  • 深拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<body>
<button id="btn">send msg</button>
<input type="text" id="msg">
<div><button id="terminate" style="color: red;">terminate</button></div>
<script>
const btn = document.querySelector('#btn');
const terminate = document.querySelector('#terminate')
const input = document.querySelector('#msg');
const worker = new Worker('./worker.js')
worker.onmessage = (e) => {
console.log('我接收到了',e.data);
}
btn.addEventListener('click', () => {
console.log('我发送了',input.value)
worker.postMessage(input.value);
})

terminate.addEventListener('click', () => {
worker.terminate()
})
</script>
</body>

worker.js

1
2
3
4
5
6
7
8
9
10
11
onmessage = function(e) {
console.log('另一个线程监听到了:', e.data)
postMessage('另一个线程加工了'+e.data)
}
onerror = function(e) {
console.log('onerror监听到:', e)
}
onmessageerror = function(e) {
console.log('onmessageerror监听到:',e)
}
// console.log(self);

通过new Worker(‘./worker.js’) 的worker.js 文件处于 DedicatedWorkerGlobalSpace作用域下,可以用 self 访问到这个全局对象。

所以以上三个方法就是对全局对象的事件处理函数的重写

Shared Worker

  • 必须遵守同源策略
  • 实现标签页共享
  • 深拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
const btn = document.querySelector('#btn');
const terminate = document.querySelector('#terminate')
const msg = document.querySelector('#msg');
const ul = document.querySelector('.recv')
const worker = new SharedWorker('./sharedWorker.js')
// worker.port.start();
btn.onclick = () => {
console.log('我发送了', msg.value);
worker.port.postMessage(msg.value);
}
worker.port.onmessage = function (e) {
console.log('我接收到了', e.data)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let counter = 0; // 计时器
const clients = [] //连接上的用户,注意 无论有几个标签页,e.ports 的长度始终都是1
self.onconnect = (e) => {
const port = e.ports[0];
//console.log(e.ports); //⭐这里的内容必须在 edge://inspect/#workers 的inspect 里的控制台查看
//port.postMessage('sharedworker已连接'); //不能发送e或者 ports 里的任何对象。因为他们is not transferred
console.log('sharedworker已连接')
clients.push(port);
port.onmessage = (e) => {
console.log('sharedworker接收到', e.data)
counter++;
clients.forEach(client => {
client.postMessage(counter)
})
}
};

注意

  1. 同一个name下,且同一个域名下才可以共享
  2. 想实现广播功能要自己保存每个连接(类似websocket)
{% if theme.CloudCalendar.enable %} {% endif %}