三数之和

leetcode 算法题

暴力求解

三层循环遍历数组把符合条件的数据放入数组之中,然后再去重,时间复杂度 nnn 再加去重时间

优化暴力求解

数组排序,然后遍历数组,把数据放入对象中去重,避免最后的去重操作,时间复杂度 nnn

双指针方法

数组进行排序,然后外层循环固定一个数,内层使用两个指针分别指向数组的左右两边,如果三数之和小于 0 则把左边指针右移,若果三数之和大于 0 则把右边指针左移,等于 0 时判断数组是否已经存在结果数组中,若存在则跳过,不存在放入数组并在 map 中标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var threeSum = function (nums) {
const sortNums = nums.sort((a, b) => a - b)
const len = sortNums.length
const result = []
const obj = {}
for (let i = 0; i < len - 2; i++) {
let left = i + 1
let right = len - 1
while (left < right) {
if (sortNums[i] + sortNums[left] + sortNums[right] > 0) {
right = right - 1
} else if (sortNums[i] + sortNums[left] + sortNums[right] < 0) {
left = left + 1
} else if (sortNums[i] + sortNums[left] + sortNums[right] === 0) {
if (!obj[`${sortNums[i]},${sortNums[left]},${sortNums[right]}`]) {
result.push([sortNums[i], sortNums[left], sortNums[right]])
obj[`${sortNums[i]},${sortNums[left]},${sortNums[right]}`] = true
}
left = left + 1
}
}
}
return result
}

antd中遇到的问题(一)

antd v4 版本中 Form.Item 子组件 value 不生效问题

背景

Form.Item 中使用 Input 输入框设置 value 时发现其不生效,查看使用文档也没有发现问题

猜测

Form.Item 对子组件的 value 属性做了劫持,导致手动设置的 value 被覆盖

代码验证

  1. 通过查找 return 语句,发现执行了 renderLayout 这个方法
  2. 查看 renderLayout 的具体实现子组件实际渲染的是方法的实际参数 baseChildren
  3. 查看 renderLayout 的调用发现在 !hasName && !isRenderProps && !dependencies 才会直接渲染子组件,其它情况会注入 value 等属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 先执行
if (!hasName && !isRenderProps && !dependencies) {
return renderLayout(children)
}

// 再执行
if (React.isValidElement(children)) {
childNode = (
<MemoInput
value={mergedControl[props.valuePropName || 'value']}
update={updateRef.current}
>
{React.cloneElement(children, childProps)}
</MemoInput>
)
}

修复

去除 Form.Item 中的 name 属性,验证 value 属性生效

mysql数据库操作(二)

express 中使用 TypeORM 连接 mysql 数据库

TypeORM 介绍

TypeORM 是一个采用 TypeScript 编写的用于 Node.js 的优秀 ORM 框架,支持使用 TypeScript 或 Javascript(ES5, ES6, ES7)开发。

express 中使用

安装

yarn add typeorm mysql

初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import express, { Request, Response } from 'express'
import { createConnection } from 'typeorm'

export default createConnection(config.db)
.then(() => {
const app = express()
app.listen(port, () => {
info(
`the server is start at port ${port}, listening on http://localhost:${port}`
)
})
})
.catch((e) => {
console.error('connection mysql error: ', e.message)
})

使用

1
2
3
4
import { getRepository } from 'typeorm'
async function getList() {
const [list, total] = await getRepository(Entity).findAndCount()
}

mysql数据库操作

创建用户

CREATE USER 'username'@'host' IDENTIFIED BY 'password';

  • host 代表可以登录地址,% 表示任意地址
  • 可选选项 WITH mysql_native_password 语句表示密码的存储方式

修改用户密码

ALTER USER 'username'@'host' IDENTIFIED WITH mysql_native_password BY 'password';

授权

grant 权限 privileges on dbName.数据表 to 'tools';

  • 权限包括增删查改,all 代表所有权限
  • 数据库和数据表可以用 * 表示通配

刷新权限

FLUSH PRIVILEGES;

  • 数据权限并不会自动更新,需要手动执行命令才会刷新

查看列表数据库

show databases;

查看数据表的结构

desc 数据库.数据表;

浏览器缓存

缓存分类

  • 共享缓存:存储的响应能够被多个用户使用。
  • 私有缓存:只能用于单独用户。

缓存方式

  • 浏览器与代理缓存
  • 网关缓存
  • CDN
  • 反向代理缓存和负载均衡器

操作的目标

常见的 HTTP 缓存只能存储 GET 响应,对于其他类型的响应则无能为力。缓存的关键主要包括 request method 和目标 URI(一般只有 GET 请求才会被缓存)。

缓存控制

没有缓存

缓存中不得存储任何关于客户端请求和服务端响应的内容。每次由客户端发起的请求都会下载完整的响应内容。

1
Cache-Control: no-store

缓存但重新验证

每次有请求发出时,缓存会将此请求发到服务器,服务器端会验证请求中所描述的缓存是否过期,若未过期,则缓存才使用本地缓存副本。

1
Cache-Control: no-cache

私有和公共缓存

  • “public” 指令表示该响应可以被任何中间人(译者注:比如中间代理、CDN 等)缓存。
  • “private” 则表示该响应是专用于某单个用户的,中间人不能缓存此响应,该响应只能应用于浏览器私有缓存中。
1
2
Cache-Control: private
Cache-Control: public

过期

过期机制中,最重要的指令是 "max-age=<seconds>",表示资源能够被缓存(保持新鲜)的最大时间。相对 Expires 而言,max-age 是距离请求发起的时间的秒数。针对应用中那些不会改变的文件,通常可以手动设置一定的时长以保证缓存有效,例如图片、cssjs 等静态资源。

1
Cache-Control: max-age=31536000

s-maxage仅适用于共享缓存(CDN),优先级高于 max-ageExpires

1
Cache-Control: s-maxage=3600

验证方式

当使用了 “must-revalidate” 指令,那就意味着缓存在考虑使用一个陈旧的资源时,必须先验证它的状态,已过期的缓存将不被使用。

1
Cache-Control: must-revalidate

Pragma 头

Pragma 是 HTTP/1.0 标准中定义的一个 header 属性,请求中包含 Pragma 的效果跟在头信息中定义 Cache-Control: no-cache 相同,但是 HTTP 的响应头没有明确定义这个属性,所以它不能拿来完全替代 HTTP/1.1 中定义的 Cache-control 头。通常定义 Pragma 以向后兼容基于 HTTP/1.0 的客户端。

新鲜度

理论上来讲,当一个资源被缓存存储后,该资源应该可以被永久存储在缓存中。

缓存驱逐

由于缓存只有有限的空间用于存储资源副本,所以缓存会定期地将一些副本删除。当服务器上面的资源进行了更新,那么缓存中的对应资源也应该被更新,由于 HTTP 是 C/S 模式的协议,服务器更新一个资源时,不可能直接通知客户端更新缓存,所以双方必须为该资源约定一个过期时间,在该过期时间之前,该资源(缓存副本)就是新鲜的,当过了过期时间后,该资源(缓存副本)则变为陈旧的。驱逐算法用于将陈旧的资源(缓存副本)替换为新鲜的,注意,一个陈旧的资源(缓存副本)是不会直接被清除或忽略的,当客户端发起一个请求时,缓存检索到已有一个对应的陈旧资源(缓存副本),则缓存会先将此请求附加一个 If-None-Match 头,然后发给目标服务器,以此来检查该资源副本是否是依然还是算新鲜的,若服务器返回了 304 (Not Modified)(该响应不会有带有实体信息),则表示此资源副本是新鲜的,这样一来,可以节省一些带宽。若服务器通过 If-None-Match 或 If-Modified-Since 判断后发现已过期,那么会带有该资源的实体内容返回。
缓存驱逐过程

缓存的先后顺序

对于含有特定头信息的请求,会去计算缓存寿命。

  • max-age:Cache-control: max-age=N 的头,相应的缓存的寿命就是 N。
  • expires:通过比较 Expires 的值和头里面 Date 属性的值来判断是否缓存还有效。
  • Last-Modified: 缓存的寿命就等于头里面 Date 的值减去 Last-Modified 的值除以 10(注:根据 rfc2626 其实也就是乘以 10%)
1
expirationTime = responseTime + freshnessLifetime - currentAge
注:responseTime 表示浏览器接收到此响应的那个时间点。

缓存验证

用户点击刷新按钮时会开始缓存验证。

  • 如果缓存的响应头信息里含有”Cache-control: must-revalidate”的定义,在浏览的过程中也会触发缓存验证。
  • 在浏览器偏好设置里设置 Advanced->Cache 为强制验证缓存也能达到相同的效果。

当缓存的文档过期后,需要进行缓存验证或者重新获取资源。只有在服务器返回强校验器或者弱校验器时才会进行验证。

ETags

  • 作为缓存的一种强校验器,ETag 响应头是一个对用户代理(User Agent, 下面简称 UA)不透明(译者注:UA 无需理解,只需要按规定使用即可)的值。对于像浏览器这样的 HTTP UA,不知道 ETag 代表什么,不能预测它的值是多少。如果资源请求的响应头里含有 ETag, 客户端可以在后续的请求的头中带上 If-None-Match 头来验证缓存。
  • Last-Modified 响应头可以作为一种弱校验器。说它弱是因为它只能精确到一秒。如果响应头里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。

当向服务端发起缓存校验的请求时,服务端会返回 200 ok 表示返回正常的结果或者 304 Not Modified(不返回 body)表示浏览器可以使用本地缓存文件。304 的响应头也可以同时更新缓存文档的过期时间。

浏览器刷新

  • 地址栏输入地址回车:走正常的缓存流程
  • 刷新页面:让强缓存过期,走协商缓存逻辑
  • 强制刷新页面: 让强缓存和协商缓存都过期,不走缓存逻辑

CDN

内容分发网络,分为推和拉模式。

  • 推模式:有更新时会强制推送到网络的各个节点
  • 拉模式:有请求时会判断当前节点是否有数据且数据是否过期,然后再向源站请求资源

时间计算

拉模式下,所有节点的缓存都设置 5 分钟,浏览器可能拉到前 10 分钟到当前时间的资源(假设当前节点到源节点之间没有中间节点),原因如下

  • 浏览器到当前节点数据缓存 5 分钟
  • 当前节点到源数据可能缓存 5 分钟

babel编译过程

编译步骤

Babel 编译文件的主要处理步骤依次是:解析(parse)、转换(transform)、生成(generate)。

  • 解析:解析步骤主要是接受源代码并输出抽象语法树(AST)。此步骤主要由@babel/parser(原 Babylon)负责解析和理解 js 代码,输出对应的 AST。
  • 转换:转换步骤主要是接受 AST,并对其进行遍历,在此过程中会进行分析和修改 AST,这也是 Babel 插件主要工作的地方。此步骤主要用到@babel/traverse 和@babel/types 两个包。
  • 生成:生成步骤主要是将(经过一系列转换之后的)AST 再转换为正常的字符串代码。此步骤主要由@babel/generator 深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。
注:AST 其实是一个每个节点包含特定属性的对象树

实现 babel 的编译过程

  • 使用 @babel/parser 把代码格式化为 AST
  • 使用 @babel/traverse@babel/types 实现 AST 树的遍历及对树的特定节点的操作
  • 使用 @babel/generator 生成目标代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const type = require('@babel/types')
const generate = require('@babel/generator').default

const code = `
function test(n){
return n + 1
}
`
const ast = parser.parse(code, { sourceType: 'module' }) // 把代码格式化为 AST 树

traverse(ast, {
// 遍历 AST 树并进行操作
enter(path) {
// 从根节点到叶子节点遍历过程中
console.log('enter', path.node.name)
if (type.isIdentifier(path.node, { name: 'n' })) {
// 把参数 n 转化成 x
path.replaceWith(type.identifier('x'))
}
},
exit(path) {
// 从叶子节点到根节点遍历过程中
console.log('exit', path.node.name)
},
})

const target = generate(ast) // 生成代码

console.log(target.code)
/*
function test(x) {
return x + 1;
}
*/

参考

ES6模块与CommonJS模块

ES6 模块与 CommonJS 模块的区别

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。(因为 CommonJS 加载的是一个对象(即 module.exports 属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。)
  • CommonJS 模块的 require()是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立模块依赖的解析阶段。

ES6 模块输出的是值的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// lib.mjs
let count = 1

setTimeout(() => {
count = count + 1
}, 1000)

export { count }

// index.mjs
import { count } from './lib.mjs'

console.log('count', count) // 1

setTimeout(() => {
console.log('timeout count', count) // 2
}, 1100)

count = 1000 // TypeError: Assignment to constant variable.

CommonJS 模块输出的是一个值的拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// lib.cjs
let count = 1

setTimeout(() => {
count = count + 1
}, 1000)

module.exports = { count }

// index.cjs
let { count } = require('./lib.cjs')

console.log('count', count) // 1

setTimeout(() => {
console.log('timeout count', count) // 1000
}, 1100)

count = 1000
  • 从 Node.js v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持
  • Node.js 要求 ES6 模块采用.mjs 后缀文件名
  • commonjs 模块采用.cjs 后缀文件名

循环引用

“循环加载”(circular dependency)指的是,a 脚本的执行依赖 b 脚本,而 b 脚本的执行又依赖 a 脚本。

CommonJS 模块的循环加载

CommonJS 模块的脚本代码在 require 的时候,就会全部执行,一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。

ES6 模块的循环加载

ES6 模块遇到模块加载命令 import 时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。

参考

除自身以外数组的乘积

leetcode 算法题

分析

  • 不要用除法:只能使用乘法
  • 复杂度为 n :只能遍历一次或者有限次
  • 结果:分为左右两部分,可以通过左边的积与右边的积相乘的方式得到结果

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @param {number[]} nums
* @return {number[]}
*/
var productExceptSelf = function (nums) {
const len = nums.length
const result = new Array(len).fill(1) // 初始化结果数组
let left = 1
let right = 1
for (let i = 0; i < len; i++) {
result[i] *= left // 除当前元素的左积
left *= nums[i] // 当前元素的左积

result[len - 1 - i] *= right // 除当前元素的右积
right *= nums[len - i - 1] // 当前元素的右积
}
return result
}

检测项目中的包版本变化

概述

由于项目中的某个依赖版本意外的升级导致项目中的部分功能不可使用

目的

每次 package.json 有更新时检测依赖版本是否有更新,小版本更新提示,大版本更新直接警告,避免依赖包的意外升级

准备

  • git 检测指定文件的内容变更
  • 正则等方式分割变更内容,提取出对应的信息
  • 在 node 脚本中运行上述命令
  • 在 commit 之前检测变更并提示

实现

提取内容变更

  • git diff 查看尚未暂存的文件更新了哪些部分
  • git diff filename 查看尚未暂存的某个文件更新了哪些
  • git diff –cached 查看已经暂存起来的文件和上次提交的版本之间的差异
  • git diff –cached filename 查看已经暂存起来的某个文件和上次提交的版本之间的差异
  • git diff commitHash commitHash 查看某两个版本之间的差异
  • git diff commitHash:filename commitHash:filename 查看某两个版本的某个文件之间的差异

node 脚本中运行命令

输出内容

1
2
+  "packageA": "~x.y.z",
- "packageB": "^x.y.z",

观察得出变化内容以 +- 开头,依赖包的格式固定,每行变更内容以换行符分割,所以先用换行符分割内容,再检查 +- 筛选出变化内容,最后用正则等方法分离出依赖包的名称和版本,最后通过前后版本的比较获取变化的包及包版本

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#! /usr/bin/env node
const childProcess = require('child_process')
const path = require('path')

const { logger, warn, info } = require('./logger')

function resolveRootPath(...args) {
return path.resolve(process.cwd(), ...args)
}

// 获取变化
function getDiff(isCached = false) {
try {
const result = childProcess.execSync(
`git diff ${isCached ? '--cached' : ''} ${resolveRootPath(
'./package.json'
)}`,
{ encoding: 'utf8' }
)
return result
} catch (e) {
return ''
}
}

// 序列化版本为对象
function serializeDiff(diffStr) {
const VERSION_REG = /\"\S+\"\s*:\s*\"(~|\-|\^)?(\d+)\.(\d+)\.(\d+)/
const arr = `${diffStr || ''}`
.split('\n')
.filter((str) => /^(\-|\+)\s+/.test(str))
.filter((str) => VERSION_REG.test(str))
const beforeArr = arr
.filter((str) => /^\-\s+/.test(str))
.map((str) => str.replace(/^\-\s+/, ''))
.map((str) => str.replace(/\,$/, ''))
const afterArr = arr
.filter((str) => /^\+\s+/.test(str))
.map((str) => str.replace(/^\+\s+/, ''))
.map((str) => str.replace(/\,$/, ''))

const beforeObj = beforeArr.reduce((prev, curr) => {
const [key, value] = curr.replace(/"/g, '').split(/\s*:\s*/)
prev[key] = value
return prev
}, {})
const afterObj = afterArr.reduce((prev, curr) => {
const [key, value] = curr.replace(/"/g, '').split(/\s*:\s*/)
prev[key] = value
return prev
}, {})
return [beforeObj, afterObj]
}

function getChange(before, after) {
const beforeKeys = Object.keys(before)
const afterKeys = Object.keys(after)

const deletePackage = beforeKeys
.filter((key) => !after[key])
.map((key) => `${key}: ${after[key]}`)
const addPackage = afterKeys
.filter((key) => !before[key])
.map((key) => `${key}: ${after[key]}`)
const updatePackage = beforeKeys
.filter((key) => after[key])
.map((key) => `${key}: ${before[key]} ===> ${after[key]}`)

const waringUpdatePackage = beforeKeys
.filter((key) => after[key])
.filter((key) => {
const beforeVision = Number(/(~|\-|\^)?(\d+)\./.exec(before[key])[2])
const afterVision = Number(/(~|\-|\^)?(\d+)\./.exec(after[key])[2])
return beforeVision !== afterVision
})
.map((key) => `${key}: ${before[key]} ===> ${after[key]}`)
const outList = [
{
label: `此次删除的依赖包有: \n ${deletePackage.join('\n')}`,
show: !!deletePackage.length,
log: warn,
},
{
label: `此次新增的依赖包有: \n ${addPackage.join('\n')}`,
show: !!addPackage.length,
log: info,
},
{
label: `此次更新的依赖包有: \n ${updatePackage.join('\n')}`,
show: !!updatePackage.length,
},
{
label: `此次大版本更新的依赖包有: \n ${waringUpdatePackage.join('\n')}`,
show: !!waringUpdatePackage.length,
log: warn,
},
].filter((item) => item.show)

if (outList.length) {
console.log('\n')
outList.forEach((item) => {
const { log = logger, label } = item
log(label)
})
console.log('\n')
}
}

const diffStr = getDiff(true)
const [before, after] = serializeDiff(diffStr)
getChange(before, after)

扩展

  • 放入 package.json 中每次提交确认变化
  • 配置提示方式和确认方法等