Samon

Samon 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 github.com/xianshannan/blog/issues 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

Samon 赞了文章 · 7月3日

在 Node 服务中发生 OOM 时,如何监控内存?

本文章已备份在 github 上 山月的博客 欢迎 star

刚开始,先抛出一个问题:

你知道你们生产环境的 Node 服务平时占用内存多少吗?或者说是多少量级?

山月在面试 Node 候选人时,这个问题足够筛掉一半的自称Node精通者,不过没有回答上来,我往往会再补充一个问题,以免漏掉优秀的无线上经验的候选人:

如何知道某个进程消耗多少内存?

当使用 Node 在生产环境作为服务器语言时,并发量过大或者代码问题造成 OOM (out of memory) 或者 CPU 满载这些都是服务器中常见的问题,此时通过监控 CPU 及内存,再结合日志及 Release 就很容易发现问题。

本章将介绍如何监控本地环境及生产环境的内存变化

一个 Node 应用实例

所以,如何动态监控一个 Node 进程的内存变化呢?

以下是一个 Node Server 的示例,并且是一个有内存泄漏问题的示例,并且是山月在生产环境定位了很久的问题的精简版。

那次内存泄漏问题中,导致单个容器中的内存从原先的 400M 暴涨到 700M,在 800M 的容器资源限制下偶尔会发生 OOM,导致重启。一时没有定位到问题 (发现问题过迟,半个月前的时序数据已被吞没,于是未定位到 Release),于是把资源限制上调到 1000M。后发现是由 ctx.request 挂载了数据库某个大字段而致
const Koa = require('koa')
const app = new Koa()

function getData () {
  return Array.from(Array(1000)).map(x => 10086)
}

app.use(async (ctx, next) => {
  ctx.data = getData()
  await next()
})

app.use(ctx => {
  ctx.body = 'hello, world'
})

app.listen(3200, () => console.log('Port: 3200'))

进程内存监控

一些问题需要在本地及测试环境得到及时扼杀,来避免在生产环境造成更大的影响。那么了解在本地如何监控内存就至关重要。

pidstatsysstat 系列 linux 性能调试工具的一个包,竟然用它来调试 linux 的性能问题,包括内存,网络,IO,CPU 等。

这不仅试用与 node,而且适用于一切进程,包括 pythonjava 以及 go

# -r: 指输出内存指标
# -p: 指定 pid
# 1: 每一秒输出一次
# 100: 输出100次
$ pidstat -r -p pid 1 100

而在使用 pidstat 之前,需要先找到进程的 pid

如何找到 Node 进程的 pid

node 中可以通过 process.pid 来找到进程的 pid

> process.pid
16425

虽然通过写代码可以找到 pid,但是具有侵入性,不太实用。那如何通过非侵入的手段找到 pid 呢?有两种办法

  1. 通过多余的参数结合 ps 定位进程
  2. 通过端口号结合 lsof 定位进程
$ node index.js shanyue

# 第一种方法:通过多余的参数快速定位 pid
$ ps -ef | grep shanyue
root     31796 23839  1 16:38 pts/5    00:00:00 node index.js shanyue

# 第二种方法:通过端口号定位 pid
lsof -i:3200
COMMAND   PID USER   FD   TYPE    DEVICE SIZE/OFF NODE NAME
node    31796 root   20u  IPv6 235987334      0t0  TCP *:tick-port (LISTEN)

使用 pidstat 监控内存

从以上代码中可以知道,node 服务的 pid 为 31796,为了可以观察到内存的动态变化,再施加一个压力测试

$ ab -c 10000 -n 1000000 http://localhost:3200/
# -r: 指输出内存指标
# -p: 指定 pid
# 1: 每一秒输出一次
# 100: 输出100次
$ pidstat -r -p 31796 1 100
Linux 3.10.0-957.21.3.el7.x86_64 (shuifeng)     2020年07月02日  _x86_64_        (2 CPU)

             UID       PID  minflt/s  majflt/s     VSZ    RSS   %MEM  Command
19时20分39秒     0     11401      0.00      0.00  566768  19800   0.12  node
19时20分40秒     0     11401      0.00      0.00  566768  19800   0.12  node
19时20分41秒     0     11401   9667.00      0.00  579024  37792   0.23  node
19时20分42秒     0     11401  11311.00      0.00  600716  59988   0.37  node
19时20分43秒     0     11401   5417.82      0.00  611420  70900   0.44  node
19时20分44秒     0     11401   3901.00      0.00  627292  85928   0.53  node
19时20分45秒     0     11401   1560.00      0.00  621660  81208   0.50  node
19时20分46秒     0     11401   2390.00      0.00  623964  83696   0.51  node
19时20分47秒     0     11401   1764.00      0.00  625500  85204   0.52  node

对于输出指标的含义如下

  • RSS: Resident Set Size,常驻内存集,可理解为内存,这就是我们需要监控的内存指标
  • VSZ: virtual size,虚拟内存

从输出可以看出,当施加了压力测试后,内存由 19M 涨到了 85M。

使用 top 监控内存

pidstat 是属于 sysstat 下的 linux 性能工具,但在 mac 中,如何定位内存的变化?

此时可以使用 top/htop

$ htop -p 31796

使用 htop 监控内存

生产环境内存监控

由于目前生产环境大都部署在 k8s因此生产环境对于某个应用的内存监控本质上是 k8s 对于某个 workload/deployment 的内存监控,关于内存监控 metric 的数据流向大致如下:

k8s -> metric server -> prometheus -> grafana

架构图如下:

以上图片取自以下文章

最终能够在 grafana 中收集到某一应用的内存监控实时图:

由于本部分设计内容过多,我将在以下的章节中进行介绍

这不仅仅适用于 node 服务,而且适用于一切 k8s 上的 workload

总结

本章介绍了关于 Node 服务的内存在本地环境及生产环境的监控

  1. 本地使用 htop/top 或者 pidstat 监控进程内存
  2. 生产环境使用 k8s/metric-server/prometheus/grafana 监控 node 整个应用的内存

当监控到某一服务发生内存泄漏后,如何解决问题?因此接下来的文章将会讲到

  1. 生产环境是如何监控整个应用的内存的
  2. 当生产环境发生 OOM 后,如何快速定位
  3. 真实生产环境若干 OOM 的示例定位
本文由博客一文多发平台 OpenWrite 发布!
查看原文

赞 16 收藏 7 评论 0

Samon 发布了文章 · 2019-11-02

你真的理解了比较运算符吗?

平常我们都是不建议在代码上编写一些比较难理解的代码,例如 x == y'A' > 'B' 。这篇文章或许不能给你带来什么大的帮助,但是却可以让你了解一些你可能没接触到的知识点。

由于有些参考资料来源于 ECMA 规范,所以感兴趣的可能需要先看《读懂 ECMAScript 规格》这篇文章,当然也可以忽略。

类型之间的转换表

首先我们需要先了解基本的类型转换规则。

粗体需要特别留意的,可能跟你想象中的不一样。

原始值转换为数字转换为字符串转换为布尔值
false0"false"false
true1"true"true
00"0"false
11"1"true
"0"0"0"true
"000"0"000"true
"1"1"1"true
NaNNaN"NaN"false
InfinityInfinity"Infinity"true
-Infinity-Infinity"-Infinity"true
""0""false
"20"20"20"true
"Runoob"NaN"Runoob"true
[ ]0""true
[20]20"20"true
[10,20]NaN"10,20"true
["Runoob"]NaN"Runoob"true
["Runoob","Google"]NaN"Runoob,Google"true
function(){}NaN"function(){}"true
{ }NaN"[object Object]"true
null0"null"false
undefinedNaN"undefined"false

这里根据上面的表格列举些例子:

  • 数字转字符串

    这个最常用了,这个也很好理解。

    String(123)

    或者

    const a = 123;
    a.toString();
  • 将字符串转换为数字

    Number("3.14")    // 返回 3.14
    Number(" ")       // 返回 0
    Number("")        // 返回 0
    Number("99 88")   // 返回 NaN
  • 字符串转布尔值

    Boolean('test')  // 返回 true
    Boolean('0')  // 返回 false
    Boolean('000')  // 返回 true

== 比较运算符

规则来源于 ECMA 相关规范 Abstract Equality Comparison

== 等同运算符的两边的类型不一样的时候,会有类型自动转换规则。

相同的类型可以直接比较(相当于 === 比较),无需自动转换,不同类型有下面几种自动转换规则(x == y),规则优先级自上而下:

  1. 如果 x 是 null,y 是 undefined,返回 true

    null == undefined
  2. 如果 x 是 undefined,y 是 null,返回 true

    undefined == null
  3. 如果 x 是 Number,y 是 String,将 y 转化成 Number,然后再比较

    0 == '0' // true
    0 == '1' // false
  4. 如果 x 是 String,y 是 Number,将 x 转化成 Number,然后再比较

    '0' == 0 // true
    '1' == 0 // false
  5. 如果 x 是 Boolean,那么将 x 转化成 Number,然后再比较

    true == 0 // false
    true == 1 // true
    true == 2 // false
    true == 'test' // false
    false == 0 // true
    false == 1 // false
    false == 2 // false
    false == 'test' // false
  6. 如果 y 是 Boolean,那么将 y 转化成 Number,然后再比较

    0 == true // false
    1 == true // true
    2 == true // false
    'test' == true // false
    0 == false // true
    1 == false // false
    2 == false // false
    'test' == false // false
  7. 如果 x 是 String 或者 Number,y 是 Object,那么将 y 转化成基本类型,再进行比较

    const a = {}
    1 == a    // false
    '1' == a  // false
  8. 如果 x 是 Object,y 是 String 或者 Number,将 x 转化成基本类型,再进行比较

    const a = {}
    a == 1    // false
    a == '1'  // false
  9. 其他情况均返回 false

    const a = {}
    a == null
    a == undefined
    0 == null
    '2' == null
    false  === null

即使我们搞懂了 == 的规则,还是建议使用 === 这种严格的运算符来替代 ==

> 或者 < 比较运算符

规则来源于 ECMA 相关规范 Abstract Relational Comparison

x < y 的规则步骤如下(规则优先级自上而下):

  1. x 和 y 需要转换为原始数据类型(ToPrimitive)

    var px = ToPrimitive(x)
    var py = ToPrimitive(y)
    // 下面会沿用这两个变量的

    除开原始的数据类型 undefined、null、boolean、number、string、 symbol,其他的都属于对象,所以可以理解为这个 ToPrimitive 只对对象有作用。(还有个特殊的 NaN,不需要转换,NaN 可以理解为一种特殊的 number,typeof NaN === 'number')。

    如果 x 或者 y 是对象,需要做转换处理,由于这里涉及的比较深,这里还是简单的说一下,知道有这回事就好。

    var a = {}
    a < 'f' // true
    a < 'F' // false
    // a 会转变为 [object Object]
    // 相当于运行了 a.valueOf().toString()

    为什么不直接 a.toString() 呢,看下下面的例子你就懂了(会首先运行 valueOf,如果返回的是对象则再运行 toString,否则直接返回 valueOf 的返回值)

    var d = new Date(1572594637602)
    d < 1572594637603 // true
    d < 1572594637601 // false
    // d 会转变为 1572594637602 (当前时间转变的成的毫秒时间戳)
    // 相当于运行了 a.valueOf()

    如果重写了 valueOf 方法,那么预期结果就不一样了

    var d = {}
    // 这里重写定义了valueOf
    d.valueOf = () => 1
    d < 2 // true
    d < 0 // false
    // d 会转变为 1
    // 相当于运行了 a.valueOf()

    更多的例子

    var a = {}
    a < 1 // false,相当于,Number('[object Object]') < 1
    a < 'a' // true,相当于 '[object Object]' < 'a'
    a < '[' // false,相当于 '[object Object]' < '['
    var b = function(){}
    b < 'g' // true,相当于 'function(){}' < 'g'
    b < 'e' // false,相当于 'function(){}' < 'e'
  2. 如果 px 和 py 都是字符串

    • 如果 py 是 px 的前缀,返回 false

      'test' < 'te'
    • 如果 px 是 py 的前缀,返回 true

      'test' < 'test1'
    • 如果 px 不是 py 的前缀,而且 py 也不是 px 的前缀

      那么需要从 px 和 py 的最小索引(假设是 k)对应的字符的 UTF-16 代码单元值 进行对比。

      假设 m = px.charCodeAt(k),n = py.charCodeAt(k),那么如果 m < n,返回 true,否则返回 false。

      'A' < 'B' // true
      // 相当于 'A'.charCodeAt(0) < 'B'.charCodeAt(0)

      更加复杂点的例子

      'ABC' < 'ABD' // true
      // 相当于 
      // var a = 'ABC'.charCodeAt(0) < 'ABD'.charCodeAt(0) // false
      // var b = 'ABC'.charCodeAt(1) < 'ABD'.charCodeAt(1) // false
      // var c = 'ABC'.charCodeAt(2) < 'ABD'.charCodeAt(2) // true
      // a || b || c
  3. 其他情况 px 和 py 一律转为数字类型进行比较

    var nx = Number(px)
    var ny = Number(py)

    例子

    '2' < 3 // true
    '2' < 1 // false

x > y 的道理一样,这里就不多说了。

参考文章

查看原文

赞 9 收藏 8 评论 0

Samon 发布了文章 · 2019-09-30

如何写一个让面试官满意的 Promise?

Promise 的实现没那么简单,也没想象中的那么难,200 行代码以内就可以实现一个可替代原生的 Promise。

Promise 已经是前端不可缺少的 API,现在已经是无处不在。你确定已经很了解 Promise 吗?如果不是很了解,那么应该了解 Promise 的实现原理。如果你觉得你自己挺了解的,那么你自己实现过 Promise 吗?

无论如何,了解 Promise 的实现方式,对于提升我们的前端技能都有一定的帮助。

下面的代码都是使用 ES6 语法实现的,不兼容 ES5,在最新的谷歌浏览器上运行没问题。

如果你想先直接看效果,可以看文章最后的完整版,也可以看 github,github 上包括了单元测试。

Promise 的由来

作为前端,一般最早接触 Promise 的概念是在 jquery 的1.5版本发布的 deferred objects。但是前端最早引入 P romise 的概念的却不是 jquery,而是dojo,而且 Promise 之所以叫 Promise 也是因为 dojo。Promises/A 标准的撰写者 KrisZyp 于 2009 年在 google 的 CommonJS 讨论组发了一个贴子,讨论了Promise API的设计思路。他声称想将这类 API 命名为 future,但是 dojo 已经实现的 deferred 机制中用到了 Promise这个术语,所以还是继续使用 Promise为此机制命名。

更新日志

2019-10-12

  • 删除了 this._callbcaQueue,使用 this._nextCallback 替换

    由于链式返回的都是一个新的 Promise,所以回调其实只有一个,无需数组处理

部分术语

  • executor

    new Promise( function(resolve, reject) {...} /* executor */  );

    executor 是带有 resolvereject 两个参数的函数 。

  • onFulfilled

    p.then(onFulfilled, onRejected);

    当 Promise 变成接受状态(fulfillment)时,该参数作为 then 回调函数被调用。

  • onRejected
    当Promise变成拒绝状态(rejection )时,该参数作为回调函数被调用。

    p.then(onFulfilled, onRejected);
    p.catch(onRejected);

Promise 实现原理

最好先看看 Promises/A+规范,这里是个人总结的代码实现的基本原理。

Promise 的三种状态

  • pending
  • fulfilled
  • rejected

Promise 对象 pending 状可能会变为 fulfilled rejected,但是不可以逆转状态。

then、catch 的回调方法只有在非 pending 状态才能执行。

Promise 生命周期

为了更好理解,本人总结了 Promise 的生命周期,生命周期分为两种情况,而且生命周期是不可逆的。

pending -> fulfilld

pending -> rejected

executor、then、catch、finally 的执行都是有各自新的生命周期,即各自独立 Promise 环境。链式返回的下一个 Promise 的结果来源于上一个 Promise 的结果。

链式原理

如何保持 .then.catch.finally 的链式调用呢?

其实每个链式调用的方法返回一个新的 Promise 实例(其实这也是 Promises/A+ 规范之一,这个也是实现 Promise 的关键之处)就可以解决这个问题,同时保证了每个链式方式的 Promise 的初始状态为 pending 状态,每个 then、catch、finally 都有自身的 Promise 生命周期。

Promise.prototype.then = function(onFulfilled,onReject) {
  return new Promise(() => {
    // 这里处理后续的逻辑即可
  })
}

但是需要考虑中途断链的情况,断链后继续使用链式的话,Promise 的状态可能是非 pending 状态。

这一点刚接触的时候,是没那么容易搞懂的。

默认一开始就使用 new Promise(...).then(...) 进行链式调用, then、catch 等的回调函数都是处于 pending 状态,回调函数会加入异步列队等待执行。而断链的时候,可能过了几秒后才重新链式调用,那么 pending 状态就有可能变为了 fulfilled 或者 rejected 状态,需要立即执行,而不需要等待pending 状态变化后才执行。

不断链的例子如下:

new Promise(...).then(...)

断链的例子如下:

const a = new Promise(...)
// 这中途可能过去了几秒,pending 状态就有可能变为了 fulfilled 或者 rejected 状态
a.then(...)

所以需要考虑这两种情况。

异步列队

这个需要了解宏任务和微任务,但是,不是所有浏览器 JavaScript API 都提供微任务这一类的方法。

所以这里先使用 setTimeout 代替,主要的目的就是要等待 pending 状态切换为其他的状态(即 executor 的执行),才会执行后续链式的回调操作。

虽然异步 resolve 或者 reject 的时候,使用同步方式也可以实现。但是原生的 Promise 所有 then 等回调函数都是在异步列队中执行的,即使是同步 resolve 或者 reject。

new Promise(resolve=>{
  resolve('这是同步 resolve')
})
new Promise(resolve=>{
  setTimeout(()=>{
      resolve('这是异步 resolve')
  })
})

这里注意一下,后面逐步说明的例子中,前面一些代码实现是没考虑异步处理的情况的,后面涉及到异步 resolve 或者 reject 的场景才加上去的。

第一步定义好结构

这里为了跟原生的 Promise 做区别,加了个前缀,改为 NPromise。

定义状态

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected

定义 Promise 实例方法

class NPromise {
  constructor(executor) {}
  then(onFulfilled, onRejected) {}
  catch(onRejected) {}
  finally(onFinally) {}
}

定义拓展方法

NPromise.resolve = function(value){}
NPromise.reject = function(resaon){}
NPromise.all = function(values){}
NPromise.race = function(values){}

简单的 Promise

第一个简单 Promsie 不考虑异步 resolve 的等情况,这一步只是用法像,then 的回调也不是异步执行的

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

/**
 * @param {Function} executor
 * executor是带有 resolve 和 reject 两个参数的函数
 * Promise 构造函数执行时立即调用 executor 函数
 */
class NPromise {
  constructor(executor) {
    // 这里有些属性变量是可以不定义的,不过提前定义一下,提高可读性

    // 初始化状态为 pending
    this._status = PENDING
    // 传递给 then 的 onFulfilled 参数
    this._nextValue = undefined
    // 错误原因
    this._error = undefined
    executor(this._onFulfilled, this._onRejected)
  }

  /**
   * 操作成功
   * @param {Any} value 操作成功传递的值
   */
  _onFulfilled = (value) => {
    if (this._status === PENDING) {
      this._status = FULFILLED
      this._nextValue = value
      this._error = undefined
    }
  }

  /**
   * 操作失败
   * @param {Any} reason 操作失败传递的值
   */
  _onRejected = (reason) => {
    if (this._status === PENDING) {
      this._status = REJECTED
      this._error = reason
      this._nextValue = undefined
    }
  }

  then(onFulfilled, onRejected) {
    return new NPromise((resolve, reject) => {
      if (this._status === FULFILLED) {
        if (onFulfilled) {
          const _value = onFulfilled
            ? onFulfilled(this._nextValue)
            : this._nextValue
          // 如果 onFulfilled 有定义则运行 onFulfilled 返回结果
          // 否则跳过,这里下一个 Promise 都是返回 fulfilled 状态
          resolve(_value)
        }
      }

      if (this._status === REJECTED) {
        if (onRejected) {
          resolve(onRejected(this._error))
        } else {
          // 没有直接跳过,下一个 Promise 继续返回 rejected 状态
          reject(this._error)
        }
      }
    })
  }

  catch(onRejected) {
    // catch 其实就是 then 的无 fulfilled 处理
    return this.then(null, onRejected)
  }

  finally(onFinally) {
    // 这个后面实现
  }
}

测试例子(setTimeout 只是为了提供独立的执行环境)

setTimeout(() => {
  new NPromise((resolve) => {
    console.log('resolved:')
    resolve(1)
  })
    .then((value) => {
      console.log(value)
      return 2
    })
    .then((value) => {
      console.log(value)
    })
})

setTimeout(() => {
  new NPromise((_, reject) => {
    console.log('rejected:')
    reject('err')
  })
    .then((value) => {
      // 这里不会运行
      console.log(value)
      return 2
    })
    .catch((err) => {
      console.log(err)
    })
})
// 输出
// resolved:
// 1
// 2
// rejected:
// err

考虑捕获错误

还是先不考虑异步 resolve 的等情况,这一步也只是用法像,then 的回调也不是异步执行的

相对于上一步的改动点:

  • executor 的运行需要 try catch

    then、catch、finally 都是要经过 executor 执行的,所以只需要 try catch executor 即可。

  • 没有使用 promiseInstance.catch 捕获错误则直接打印错误信息

    不是 throw,而是 console.error,原生的也是这样的,所以直接 try catch 不到 Promise 的错误。

  • then 的回调函数不是函数类型则跳过

    这不属于错误捕获处理范围,这里只是顺带提一下改动点。

代码实现

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function isFunction(fn) {
  return typeof fn === 'function'
}

/**
 * @param {Function} executor
 * executor是带有 resolve 和 reject 两个参数的函数
 * Promise 构造函数执行时立即调用 executor 函数
 */
class NPromise {
  constructor(executor) {
    // 这里有些属性变量是可以不定义的,不过提前定义一下,提高可读性

    try {
      // 初始化状态为 pending
      this._status = PENDING
      // 传递给 then 的 onFulfilled 参数
      this._nextValue = undefined
      // 错误原因
      this._error = undefined
      executor(this._onFulfilled, this._onRejected)
    } catch (err) {
      this._onRejected(err)
    }
  }

  /**
   * 如果没有 .catch 错误,则在最后抛出错误
   */
  _throwErrorIfNotCatch() {
    setTimeout(() => {
      // setTimeout 是必须的,等待执行完毕,最后检测 this._error 是否还定义
      if (this._error !== undefined) {
        // 发生错误后没用 catch 那么需要直接提示
        console.error('Uncaught (in promise)', this._error)
      }
    })
  }

  /**
   * 操作成功
   * @param {Any} value 操作成功传递的值
   */
  _onFulfilled = (value) => {
    if (this._status === PENDING) {
      this._status = FULFILLED
      this._nextValue = value
      this._error = undefined
      this._throwErrorIfNotCatch()
    }
  }

  /**
   * 操作失败
   * @param {Any} reason 操作失败传递的值
   */
  _onRejected = (reason) => {
    if (this._status === PENDING) {
      this._status = REJECTED
      this._error = reason
      this._nextValue = undefined
      this._throwErrorIfNotCatch()
    }
  }

  then(onFulfilled, onRejected) {
    return new NPromise((resolve, reject) => {
      const handle = (reason) => {
        function handleResolve(value) {
          const _value = isFunction(onFulfilled) ? onFulfilled(value) : value
          resolve(_value)
        }

        function handleReject(err) {
          if (isFunction(onRejected)) {
            resolve(onRejected(err))
          } else {
            reject(err)
          }
        }

        if (this._status === FULFILLED) {
          return handleResolve(this._nextValue)
        }

        if (this._status === REJECTED) {
          return handleReject(reason)
        }
      }
      handle(this._error)
      // error 已经传递到下一个 NPromise 了,需要重置,否则会打印多个相同错误
      // 配合 this._throwErrorIfNotCatch 一起使用,
      // 保证执行到最后才抛出错误,如果没有 catch
      this._error = undefined
    })
  }

  catch(onRejected) {
    // catch 其实就是 then 的无 fulfilled 处理
    return this.then(null, onRejected)
  }

  finally(onFinally) {
    // 这个后面实现
  }
}

测试例子(setTimeout 只是为了提供独立的执行环境)

setTimeout(() => {
  new NPromise((resolve) => {
    console.log('executor 报错:')
    const a = 2
    a = 3
    resolve(1)
  }).catch((value) => {
    console.log(value)
    return 2
  })
})

setTimeout(() => {
  new NPromise((resolve) => {
    resolve()
  })
    .then(() => {
      const b = 3
      b = 4
      return 2
    })
    .catch((err) => {
      console.log('then 回调函数报错:')
      console.log(err)
    })
})

setTimeout(() => {
  new NPromise((resolve) => {
    console.log('直接打印了错误信息,红色的:')
    resolve()
  }).then(() => {
    throw Error('test')
    return 2
  })
})


// executor 报错:
//  TypeError: Assignment to constant variable.
//     at <anonymous>:97:7
//     at new NPromise (<anonymous>:21:7)
//     at <anonymous>:94:3
//  then 回调函数报错: TypeError: Assignment to constant variable.
//     at <anonymous>:111:9
//     at <anonymous>:59:17
//     at new Promise (<anonymous>)
//     at NPromise.then (<anonymous>:54:12)
//     at <anonymous>:109:6
// 直接打印了错误信息,红色的:
// Uncaught (in promise) Error: test
//     at <anonymous>:148:11
//     at handleResolve (<anonymous>:76:52)
//     at handle (<anonymous>:89:18)
//     at <anonymous>:96:7
//     at new NPromise (<anonymous>:25:7)
//     at NPromise.then (<anonymous>:73:12)
//     at <anonymous>:147:6

考虑 resolved 的值是 Promise 类型

还是先不考虑异步 resolve 的等情况,这一步也只是用法像,then 的回调也不是异步执行的

相对于上一步的改动点:

  • 新增 isPromise 判断方法
  • then 方法中处理 this._nextValue 是 Promise 的情况

代码实现

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function isFunction(fn) {
  return typeof fn === 'function'
}

function isPromise(value) {
  return (
    value &&
    isFunction(value.then) &&
    isFunction(value.catch) &&
    isFunction(value.finally)
    // 上面判断也可以用一句代码代替,不过原生的试了下,不是用下面的方式
    // value instanceof NPromise
  )
}

/**
 * @param {Function} executor
 * executor是带有 resolve 和 reject 两个参数的函数
 * Promise 构造函数执行时立即调用 executor 函数
 */
class NPromise {
  constructor(executor) {
    // 这里有些属性变量是可以不定义的,不过提前定义一下,提高可读性

    try {
      // 初始化状态为 pending
      this._status = PENDING
      // 传递给 then 的 onFulfilled 参数
      this._nextValue = undefined
      // 错误原因
      this._error = undefined
      executor(this._onFulfilled, this._onRejected)
    } catch (err) {
      this._onRejected(err)
    }
  }

  /**
   * 如果没有 .catch 错误,则在最后抛出错误
   */
  _throwErrorIfNotCatch() {
    setTimeout(() => {
      // setTimeout 是必须的,等待执行完毕,最后检测 this._error 是否还定义
      if (this._error !== undefined) {
        // 发生错误后没用 catch 那么需要直接提示
        console.error('Uncaught (in promise)', this._error)
      }
    })
  }

  /**
   * 操作成功
   * @param {Any} value 操作成功传递的值
   */
  _onFulfilled = (value) => {
    if (this._status === PENDING) {
      this._status = FULFILLED
      this._nextValue = value
      this._error = undefined
      this._throwErrorIfNotCatch()
    }
  }

  /**
   * 操作失败
   * @param {Any} reason 操作失败传递的值
   */
  _onRejected = (reason) => {
    if (this._status === PENDING) {
      this._status = REJECTED
      this._error = reason
      this._nextValue = undefined
      this._throwErrorIfNotCatch()
    }
  }

  then(onFulfilled, onRejected) {
    return new NPromise((resolve, reject) => {
      const handle = (reason) => {
        function handleResolve(value) {
          const _value = isFunction(onFulfilled) ? onFulfilled(value) : value
          resolve(_value)
        }

        function handleReject(err) {
          if (isFunction(onRejected)) {
            resolve(onRejected(err))
          } else {
            reject(err)
          }
        }

        if (this._status === FULFILLED) {
          if (isPromise(this._nextValue)) {
            return this._nextValue.then(handleResolve, handleReject)
          } else {
            return handleResolve(this._nextValue)
          }
        }

        if (this._status === REJECTED) {
          return handleReject(reason)
        }
      }
      handle(this._error)
      // error 已经传递到下一个 NPromise 了,需要重置,否则会打印多个相同错误
      // 配合 this._throwErrorIfNotCatch 一起使用,
      // 保证执行到最后才抛出错误,如果没有 catch
      this._error = undefined
    })
  }

  catch(onRejected) {
    // catch 其实就是 then 的无 fulfilled 处理
    return this.then(null, onRejected)
  }

  finally(onFinally) {
    // 这个后面实现
  }
}

测试代码


setTimeout(() => {
  new NPromise((resolve) => {
    resolve(
      new NPromise((_resolve) => {
        _resolve(1)
      })
    )
  }).then((value) => {
    console.log(value)
  })
})

setTimeout(() => {
  new NPromise((resolve) => {
    resolve(
      new NPromise((_, _reject) => {
        _reject('err')
      })
    )
  }).catch((err) => {
    console.log(err)
  })
})

考虑异步的情况

异步 resolve 或者 reject,相对会复杂点,回调需要等待 pending 状态变为其他状态后才执行。

原生 JavaScript 自带异步列队,我们可以利用这一点,这里先使用 setTimeout 代替,所以这个 Promise 的优先级跟 setTimeout 是同一个等级的(原生的是 Promise 优先于 setTimeout 执行)。

相对于上一步的改动点:

  • this._onFulfilled 加上了异步处理,即加入所谓的异步列队
  • this._onRejected 加上了异步处理,即加入所谓的异步列队
  • 新增 this._nextCallback

    由于链式返回的都是一个新的 Promise,所以下一步的回调其实只有一个,只有 Promise 的状态为 pending 的时候,this._nextCallback 才会有值(非 pending 的时候回调已经立即执行),同时等待 pending 状态改变后才会执行。

    this._nextCallback 都只会在当前 Promise 生命周期中执行一次。

  • then 方法中需要根据 Promise 状态进行区分处理

    如果非 pending 状态,那么立即执行回调函数(如果没回调函数,跳过)。

    如果是 pending 状态 ,那么加入异步列队(通过 this._nextCallback 传递回调),等待 Promise 状态为非 pending 状态才执行。

  • then 方法的回调加上了 try catch 捕获错误处理
  • 实现了 finally 方法

代码实现

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function isFunction(fn) {
  return typeof fn === 'function'
}

function isPromise(value) {
  return (
    value &&
    isFunction(value.then) &&
    isFunction(value.catch) &&
    isFunction(value.finally)
    // 上面判断也可以用一句代码代替,不过原生的试了下,不是用下面的方式
    // value instanceof NPromise
  )
}

/**
 * @param {Function} executor
 * executor是带有 resolve 和 reject 两个参数的函数
 * Promise 构造函数执行时立即调用 executor 函数
 */
class NPromise {
  constructor(executor) {
    if (!isFunction(executor)) {
      throw new TypeError('Expected the executor to be a function.')
    }
    try {
      // 初始化状态为 PENDING
      this._status = PENDING
      // fullfilled 的值,也是 then 和 catch 的 return 值,都是当前执行的临时值
      this._nextValue = undefined
      // 当前捕捉的错误信息
      this._error = undefined
      // then、catch、finally 的异步回调列队,会依次执行
      // 改动点:由于 then、catch 都是返回新的 Promise 所以无需列队执行,只需要 nextCallback
      // this._callbacQueue = []
      // 异步 resolve 或者 reject 的处理需要等待 pending 状态转变后才执行下一步的回调
      this._nextCallback = undefined
      executor(this._onFulfilled, this._onRejected)
    } catch (err) {
      this._onRejected(err)
    }
  }

  /**
   * 如果没有 .catch 错误,则在最后抛出错误
   */
  _throwErrorIfNotCatch() {
    setTimeout(() => {
      // setTimeout 是必须的,等待执行完毕,最后检测 this._error 是否还定义
      if (this._error !== undefined) {
        // 发生错误后没用 catch 那么需要直接提示
        console.error('Uncaught (in promise)', this._error)
      }
    })
  }

  /**
   * 操作成功
   * @param {Any} value 操作成功传递的值
   */
  _onFulfilled = value => {
    setTimeout(() => {
      if (this._status === PENDING) {
        this._status = FULFILLED
        this._nextValue = value
        this._error = undefined
        this._nextCallback && this._nextCallback()
        this._throwErrorIfNotCatch()
      }
    })
  }

  /**
   * 操作失败
   * @param {Any} reason 操作失败传递的值
   */
  _onRejected = reason => {
    setTimeout(() => {
      if (this._status === PENDING) {
        this._status = REJECTED
        this._error = reason
        this._nextValue = undefined
        this._nextCallback && this._nextCallback()
        this._throwErrorIfNotCatch()
      }
    })
  }

  then(onFulfilled, onRejected) {
    return new NPromise((resolve, reject) => {
      const handle = reason => {
        try {
          function handleResolve(value) {
            const _value = isFunction(onFulfilled) ? onFulfilled(value) : value
            resolve(_value)
          }

          function handleReject(err) {
            if (isFunction(onRejected)) {
              resolve(onRejected(err))
            } else {
              reject(err)
            }
          }

          if (this._status === FULFILLED) {
            if (isPromise(this._nextValue)) {
              return this._nextValue.then(handleResolve, handleReject)
            } else {
              return handleResolve(this._nextValue)
            }
          }

          if (this._status === REJECTED) {
            return handleReject(reason)
          }
        } catch (err) {
          reject(err)
        }
      }

      const nextCallback = () => {
        handle(this._error)
        // error 已经传递到下一个 NPromise 了,需要重置,否则会抛出多个相同错误
        // 配合 this._throwErrorIfNotCatch 一起使用,
        // 保证执行到最后才抛出错误,如果没有 catch
        this._error = undefined
      }

      if (this._status === PENDING) {
        // 默认不断链的情况下,then 回电函数 Promise 的状态为 pending 状态(一定的)
        // 如 new NPromise(...).then(...) 是没断链的
        // 下面是断链的
        // var a = NPromise(...); a.then(...)
        // 当然断链了也可能是 pending 状态
        this._nextCallback = nextCallback
      } else {
        // 断链的情况下,then 回调函数 Promise 的状态为非 pending 状态的场景下
        // 如 var a = NPromise(...);
        // 过了几秒后 a.then(...) 继续链式调用就可能是非 pending 的状态了
        nextCallback() // 需要立即执行
      }
    })
  }

  catch(onRejected) {
    return this.then(null, onRejected)
  }

  finally(onFinally) {
    return this.then(
      () => {
        onFinally()
        return this._nextValue
      },
      () => {
        onFinally()
        // 错误需要抛出,下一个 Promise 才会捕获到
        throw this._error
      }
    )
  }
}

测试代码

new NPromise((resolve) => {
  setTimeout(() => {
    resolve(1)
  }, 1000)
})
  .then((value) => {
    console.log(value)
    return new NPromise((resolve) => {
      setTimeout(() => {
        resolve(2)
      }, 1000)
    })
  })
  .then((value) => {
    console.log(value)
  })

拓展方法

拓展方法相对难的应该是 Promise.all,其他的都挺简单的。

NPromise.resolve = function(value) {
  return new NPromise(resolve => {
    resolve(value)
  })
}

NPromise.reject = function(reason) {
  return new NPromise((_, reject) => {
    reject(reason)
  })
}

NPromise.all = function(values) {
  return new NPromise((resolve, reject) => {
    let ret = {}
    let isError = false
    values.forEach((p, index) => {
      if (isError) {
        return
      }
      NPromise.resolve(p)
        .then(value => {
          ret[index] = value
          const result = Object.values(ret)
          if (values.length === result.length) {
            resolve(result)
          }
        })
        .catch(err => {
          isError = true
          reject(err)
        })
    })
  })
}

NPromise.race = function(values) {
  return new NPromise(function(resolve, reject) {
    values.forEach(function(value) {
      NPromise.resolve(value).then(resolve, reject)
    })
  })
}

最终版

也可以看 github,上面有单元测试。

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function isFunction(fn) {
  return typeof fn === 'function'
}

function isPromise(value) {
  return (
    value &&
    isFunction(value.then) &&
    isFunction(value.catch) &&
    isFunction(value.finally)
    // 上面判断也可以用一句代码代替,不过原生的试了下,不是用下面的方式
    // value instanceof NPromise
  )
}

/**
 * @param {Function} executor
 * executor是带有 resolve 和 reject 两个参数的函数
 * Promise 构造函数执行时立即调用 executor 函数
 */
class NPromise {
  constructor(executor) {
    if (!isFunction(executor)) {
      throw new TypeError('Expected the executor to be a function.')
    }
    try {
      // 初始化状态为 PENDING
      this._status = PENDING
      // fullfilled 的值,也是 then 和 catch 的 return 值,都是当前执行的临时值
      this._nextValue = undefined
      // 当前捕捉的错误信息
      this._error = undefined
      // then、catch、finally 的异步回调列队,会依次执行
      // 改动点:由于 then、catch 都是返回新的 Promise 所以无需列队执行,只需要 nextCallback
      // this._callbacQueue = []
      // 异步 resolve 或者 reject 的处理需要等待 pending 状态转变后才执行下一步的回调
      this._nextCallback = undefined
      executor(this._onFulfilled, this._onRejected)
    } catch (err) {
      this._onRejected(err)
    }
  }

  /**
   * 如果没有 .catch 错误,则在最后抛出错误
   */
  _throwErrorIfNotCatch() {
    setTimeout(() => {
      // setTimeout 是必须的,等待执行完毕,最后检测 this._error 是否还定义
      if (this._error !== undefined) {
        // 发生错误后没用 catch 那么需要直接提示
        console.error('Uncaught (in promise)', this._error)
      }
    })
  }

  /**
   * 操作成功
   * @param {Any} value 操作成功传递的值
   */
  _onFulfilled = value => {
    setTimeout(() => {
      if (this._status === PENDING) {
        this._status = FULFILLED
        this._nextValue = value
        this._error = undefined
        this._nextCallback && this._nextCallback()
        this._throwErrorIfNotCatch()
      }
    })
  }

  /**
   * 操作失败
   * @param {Any} reason 操作失败传递的值
   */
  _onRejected = reason => {
    setTimeout(() => {
      if (this._status === PENDING) {
        this._status = REJECTED
        this._error = reason
        this._nextValue = undefined
        this._nextCallback && this._nextCallback()
        this._throwErrorIfNotCatch()
      }
    })
  }

  then(onFulfilled, onRejected) {
    return new NPromise((resolve, reject) => {
      const handle = reason => {
        try {
          function handleResolve(value) {
            const _value = isFunction(onFulfilled) ? onFulfilled(value) : value
            resolve(_value)
          }

          function handleReject(err) {
            if (isFunction(onRejected)) {
              resolve(onRejected(err))
            } else {
              reject(err)
            }
          }

          if (this._status === FULFILLED) {
            if (isPromise(this._nextValue)) {
              return this._nextValue.then(handleResolve, handleReject)
            } else {
              return handleResolve(this._nextValue)
            }
          }

          if (this._status === REJECTED) {
            return handleReject(reason)
          }
        } catch (err) {
          reject(err)
        }
      }

      const nextCallback = () => {
        handle(this._error)
        // error 已经传递到下一个 NPromise 了,需要重置,否则会抛出多个相同错误
        // 配合 this._throwErrorIfNotCatch 一起使用,
        // 保证执行到最后才抛出错误,如果没有 catch
        this._error = undefined
      }

      if (this._status === PENDING) {
        // 默认不断链的情况下,then 回电函数 Promise 的状态为 pending 状态(一定的)
        // 如 new NPromise(...).then(...) 是没断链的
        // 下面是断链的
        // var a = NPromise(...); a.then(...)
        // 当然断链了也可能是 pending 状态
        this._nextCallback = nextCallback
      } else {
        // 断链的情况下,then 回调函数 Promise 的状态为非 pending 状态的场景下
        // 如 var a = NPromise(...);
        // 过了几秒后 a.then(...) 继续链式调用就可能是非 pending 的状态了
        nextCallback() // 需要立即执行
      }
    })
  }

  catch(onRejected) {
    return this.then(null, onRejected)
  }

  finally(onFinally) {
    return this.then(
      () => {
        onFinally()
        return this._nextValue
      },
      () => {
        onFinally()
        // 错误需要抛出,下一个 Promise 才会捕获到
        throw this._error
      }
    )
  }
}

NPromise.resolve = function(value) {
  return new NPromise(resolve => {
    resolve(value)
  })
}

NPromise.reject = function(reason) {
  return new NPromise((_, reject) => {
    reject(reason)
  })
}

NPromise.all = function(values) {
  return new NPromise((resolve, reject) => {
    let ret = {}
    let isError = false
    values.forEach((p, index) => {
      if (isError) {
        return
      }
      NPromise.resolve(p)
        .then(value => {
          ret[index] = value
          const result = Object.values(ret)
          if (values.length === result.length) {
            resolve(result)
          }
        })
        .catch(err => {
          isError = true
          reject(err)
        })
    })
  })
}

NPromise.race = function(values) {
  return new NPromise(function(resolve, reject) {
    values.forEach(function(value) {
      NPromise.resolve(value).then(resolve, reject)
    })
  })
}

总结

实现 Promise 比较关键的点在于状态的切换,然后链式的处理(返回新的 Promise,核心点)。最主要的逻辑还是 then 方法的处理(核心),理解了 then 方法里面的逻辑,那么就了解了大部分了。

经过一些测试,除了下面两点之外:

  • 使用了 setTimeout 的宏任务列队外替代微任务
  • 拓展方法 Promise.all 和 Promise.race 只考虑数组,不考虑迭代器。

NPromise 单独用法和效果上基本 100% 跟原生的一致。

如果你不相信,看看 github 上的单元测试,同时你试试下面的代码:

注意 NPromise 和 Promise 的。

new NPromise((resolve) => {
  resolve(Promise.resolve(2))
}).then((value) => {
  console.log(value)
})

或者

new Promise((resolve) => {
  resolve(NPromise.resolve(2))
}).then((value) => {
  console.log(value)
})

上面的结果都是返回正常的。

参考文章

查看原文

赞 28 收藏 24 评论 0

Samon 发布了文章 · 2019-09-05

如何写一个让面试官满意的 Generator 执行器?

虽然现在使用 async 函数 就可以替代 Generator 执行器了,不过了解下 Generator 执行器的原理还是挺有必要的。

如果你不了解 Generator,那么你需要看这里

例子都可以在 Console 中运行的(谷歌版本 76.0.3809.100),都是以最新浏览器支持的 JavaScript 特性来编写的,不考虑兼容性。

术语区分

Generator 是 Generator function 运行后返回的对象。

执行器的原理

原理一

有 Generator next 函数的特性,next 函数运行后会返回如下结构:

{ value: 'world', done: false }

或者

{ value: undefined, done: true }

那么我们可以使用递归运行 next 函数,如果 done 为 true 则停止 next 函数的运行。

原理二

yield 表达式本身没有返回值,或者说总是返回 undefined。

next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。

由于 next 方法的参数表示上一个 yield 表达式的返回值,所以在第一次使用 next 方法时,传递参数是无效的。

function* test() {
  const a = yield "a"
  console.log(a)
  return true
}
const gen = test()
// 第一次 next() 会卡在 yield a 中,next 返回 {value: "a", done: false}
// 第一个 next() 函数的参数是无效的,无需传递
const nextOne = gen.next() 
// 传递参数 nextOne.value
// test 函数中的 console.log 输出 'a'
// 第二次的 next() 返回值为 {value: true, done: true}
gen.next(nextOne.value)

原理三

gen.throw(exception) 可以抛出异常,并恢复生成器的执行(前提需要 try catch yield 语句),返回带有 done 及 value 两个属性的对象。而且该异常可以通过 try...catch 块进行捕获。那么我们可以通过 gen.throw 抛出 Promise 错误,这样就可以使用 try catch 拦截 Promise 的错误了。

function* gen() {
  let a
  try {
    a = yield 42
  } catch (e) {
    console.log('Error caught!')
  }
  console.log(a)
  yield 33
}

var g = gen()
g.next() // { value: 42, done: false }
g.throw(new Error('Something went wrong')) // "Error caught!"

简单的执行器

简单的执行器代码如下:

/**
 * generator 执行器
 * @param {Function} func generator 函数
 * @return {Promise} 返回一个 Promise 对象
 */
function generatorExecuter(func) {
  const gen = func()

  function recursion(prevValue) {
    const next = gen.next(prevValue)
    const value = next.value
    const done = next.done
    if (done) {
      return Promise.resolve(value)
    } else {
      return recursion(value)
    }
  }
  return recursion()
}

代码并不复杂,最简单的执行器就出来了。如果你是一步一步的看文章过来的,都理解了原理,那么这些代码也很好理解。

考虑 yield 的类型是 Promise

上面的代码执行如下的 Generator 函数是不正确的:

function* test() {
  const a = yield Promise.resolve('a')
  console.log(a)
  return true
}
generatorExecuter(test)

运行上面的代码后,test 函数 console.log 输出的不是 a,而是 Promise {<resolved>: "a"}

那么我们代码需要这样处理:

/**
 * generator 执行器
 * @param {GeneratorFunction} generatorFunc generator 函数
 * @return {Promise} 返回一个 Promise 对象
 */
function generatorExecuter(generatorFunc) {
  return new Promise((resolve) => {
    const generator = generatorFunc()
    // 触发第一次执行下面定义的 next 函数
    onFullfilled()
    
    /**
     * Promise 成功处理的回调函数
     * 回调函数会执行 generator.next()
     * @param {Any} value yield 表达式的返回值
     */
    function onFullfilled(value) {
      let result
      // next() 会一步一步的执行 generator 函数体的代码,
      result = generator.next(value)
      next(result)
    }
    
    function next(result) {
      const value = result.value
      const done = result.done

      if (done) {
        return resolve(value)
      } else {
        return Promise.resolve(value).then(onFullfilled)
      }
    }
  })
}

这样就再运行 generatorExecuter(test) 就没问题了。

考虑 try catch 捕获 yield 错误

这里用到了上面提到的原理三,如果不清楚可以回去看看。

如下代码,使用上面的执行器是无法 try catch 到 yield 错误的:

function* test() {
  try {
    const a = yield Promise.reject('error')
  }catch (err) {
    console.log('发生错误了:', err)
  }
  return true
}
generatorExecuter(test)

运行上面代码后,会报错 Uncaught (in promise) error,而不是拦截后输出 发生错误了: error

要改成这样才行(使用 gen.throw ):

/**
 * generator 执行器
 * @param {GeneratorFunction} generatorFunc generator 函数
 * @return {Promise} 返回一个 Promise 对象
 */
function generatorExecuter(generatorFunc) {
  return new Promise((resolve, reject) => {
    const generator = generatorFunc()
    // 触发第一次执行下面定义的 next 函数
    onFullfilled()
    /**
     * Promise 成功处理的回调函数
     * 回调函数会执行 generator.next()
     * @param {Any} value yield 表达式的返回值
     */
    function onFullfilled(value) {
      let result
      // next() 会一步一步的执行 generator 函数体的代码,
      // 如果报错,我们需要拦截,然后 reject
      try {
        // yield 表达式本身没有返回值,或者说总是返回 undefined。
        // generator.next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。
        // 由于 generator.next 方法的参数表示上一个 yield 表达式的返回值,
        // 所以在第一次使用 generator.next 方法时,传递参数是无效的。
        result = generator.next(value)
      } catch (error) {
        return reject(error)
      }
      next(result)
    }
    /**
     * Promise 失败处理的回调函数
     * 回调函数会执行 generator.throw() ,这样可以 try catch 拦截 yield xxx 的错误
     * @param {Any} reason 失败原因
     */
    function onRejected(reason) {
      let result
      try {
        // 这里 try catch 是捕获恢复生成器执行的时候,代码发生错误的情况
        // gen.throw() 方法用来向生成器抛出异常,并恢复生成器的执行,
        // 返回带有 done 及 value 两个属性的对象。
        result = generator.throw(reason)
      } catch (error) {
        return reject(error)
      }
      // gen.throw() 的错误被捕获后,可以继续执行下去,否则终止后续的运行
      // 这可以达到同步效果
      //(不是上面的 try catch,是使用者 try catch yield 语句)
      next(result)
    }

    function next(result) {
      const value = result.value
      const done = result.done

      if (done) {
        return resolve(value)
      } else {
        return Promise.resolve(value).then(onFullfilled, onRejected)
      }
    }
  })
}

考虑 yield 的其他类型

考虑 yield 的其他类型,如 generator 函数,可以把这些类型适配为 Promise 就很好处理了

上面的代码执行如下的 Generator 函数是不正确的:

function* aFun() {
  return 'a'
}

function* test() {
  const a = yield aFun
  console.log(a)
  return true
}
generatorExecuter(test)

运行上面的代码后,test 函数 console.log 输出的不是 a,而是输出下面的字符串:

ƒ* aFun() {
  return 'a'
}

那么我们代码需要这样处理:

/**
 * generator 执行器
 * @param {GeneratorFunction | Generator} generatorFunc Generator 函数或者 Generator
 * @return {Promise} 返回一个 Promise 对象
 */
function generatorExecuter(generatorFunc) {
  if (!isGernerator(generatorFunc) && !isGerneratorFunction(generatorFunc)) {
    throw new TypeError(
      'Expected the generatorFunc to be a GeneratorFunction or a Generator.'
    )
  }

  let generator = generatorFunc
  if (isGerneratorFunction(generatorFunc)) {
    generator = generatorFunc()
  }

  return new Promise((resolve, reject) => {
    // 触发第一次执行下面定义的 next 函数
    onFullfilled()
    /**
     * Promise 成功处理的回调函数
     * 回调函数会执行 generator.next()
     * @param {Any} value yield 表达式的返回值
     */
    function onFullfilled(value) {
      let result
      // next() 会一步一步的执行 generator 函数体的代码,
      // 如果报错,我们需要拦截,然后 reject
      try {
        // yield 表达式本身没有返回值,或者说总是返回 undefined。
        // generator.next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。
        // 由于 generator.next 方法的参数表示上一个 yield 表达式的返回值,
        // 所以在第一次使用 generator.next 方法时,传递参数是无效的。
        result = generator.next(value)
      } catch (error) {
        return reject(error)
      }
      next(result)
    }
    /**
     * Promise 失败处理的回调函数
     * 回调函数会执行 generator.throw() ,这样可以 try catch 拦截 yield xxx 的错误
     * @param {Any} reason 失败原因
     */
    function onRejected(reason) {
      let result
      try {
        // 这里 try catch 是捕获恢复生成器执行的时候,代码发生错误的情况
        // gen.throw() 方法用来向生成器抛出异常,并恢复生成器的执行,
        // 返回带有 done 及 value 两个属性的对象。
        result = generator.throw(reason)
      } catch (error) {
        return reject(error)
      }
      // gen.throw() 的错误被捕获后,可以继续执行下去,否则终止后续的运行
      // 这可以达到同步效果
      //(不是上面的 try catch,是使用者 try catch yield 语句)
      next(result)
    }

    function next(result) {
      const value = toPromise(result.value)
      const done = result.done

      if (done) {
        return resolve(value)
      } else {
        return value.then(onFullfilled, onRejected)
      }
    }
  })
}

/**
 * 考虑 yield 的其他类型,如 generator 函数
 * 可以把这些类型适配为 Promise 就很好处理了
 */
function toPromise(value) {
  if (isGerneratorFunction(value) || isGernerator(value)) {
    // generatorExecuter 返回 Promise
    return generatorExecuter(value)
  } else {
    // 字符串、对象、数组、Promise 等都转成 Promise
    return Promise.resolve(value)
  }
}

/**
 * 是否是 generator 函数
 */
function isGerneratorFunction(target) {
  if (
    Object.prototype.toString.apply(target) === '[object GeneratorFunction]'
  ) {
    return true
  } else {
    return false
  }
}

/**
 * 是否是 generator
 */
function isGernerator(target) {
  if (Object.prototype.toString.apply(target) === '[object Generator]') {
    return true
  } else {
    return false
  }
}

这样就再运行 generatorExecuter(test) 就没问题了。

最终版

Generator 执行器没想的那么难,花点时间就可以吃透了。

/**
 * generator 执行器
 * @param {GeneratorFunction | Generator} generatorFunc Generator 函数或者 Generator
 * @return {Promise} 返回一个 Promise 对象
 */
function generatorExecuter(generatorFunc) {
  if (!isGernerator(generatorFunc) && !isGerneratorFunction(generatorFunc)) {
    throw new TypeError(
      'Expected the generatorFunc to be a GeneratorFunction or a Generator.'
    )
  }

  let generator = generatorFunc
  if (isGerneratorFunction(generatorFunc)) {
    generator = generatorFunc()
  }

  return new Promise((resolve, reject) => {
    // 触发第一次执行下面定义的 next 函数
    onFullfilled()
    /**
     * Promise 成功处理的回调函数
     * 回调函数会执行 generator.next()
     * @param {Any} value yield 表达式的返回值
     */
    function onFullfilled(value) {
      let result
      // next() 会一步一步的执行 generator 函数体的代码,
      // 如果报错,我们需要拦截,然后 reject
      try {
        // yield 表达式本身没有返回值,或者说总是返回 undefined。
        // generator.next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。
        // 由于 generator.next 方法的参数表示上一个 yield 表达式的返回值,
        // 所以在第一次使用 generator.next 方法时,传递参数是无效的。
        result = generator.next(value)
      } catch (error) {
        return reject(error)
      }
      next(result)
    }
    /**
     * Promise 失败处理的回调函数
     * 回调函数会执行 generator.throw() ,这样可以 try catch 拦截 yield xxx 的错误
     * @param {Any} reason 失败原因
     */
    function onRejected(reason) {
      let result
      try {
        // 这里 try catch 是捕获恢复生成器执行的时候,代码发生错误的情况
        // gen.throw() 方法用来向生成器抛出异常,并恢复生成器的执行,
        // 返回带有 done 及 value 两个属性的对象。
        result = generator.throw(reason)
      } catch (error) {
        return reject(error)
      }
      // gen.throw() 的错误被捕获后,可以继续执行下去,否则终止后续的运行
      // 这可以达到同步效果
      //(不是上面的 try catch,是使用者 try catch yield 语句)
      next(result)
    }

    function next(result) {
      const value = toPromise(result.value)
      const done = result.done

      if (done) {
        return resolve(value)
      } else {
        return value.then(onFullfilled, onRejected)
      }
    }
  })
}

/**
 * 考虑 yield 的其他类型,如 generator 函数
 * 可以把这些类型适配为 Promise 就很好处理了
 */
function toPromise(value) {
  if (isGerneratorFunction(value) || isGernerator(value)) {
    // generatorExecuter 返回 Promise
    return generatorExecuter(value)
  } else {
    // 字符串、对象、数组、Promise 等都转成 Promise
    return Promise.resolve(value)
  }
}

/**
 * 是否是 generator 函数
 */
function isGerneratorFunction(target) {
  if (
    Object.prototype.toString.apply(target) === '[object GeneratorFunction]'
  ) {
    return true
  } else {
    return false
  }
}

/**
 * 是否是 generator
 */
function isGernerator(target) {
  if (Object.prototype.toString.apply(target) === '[object Generator]') {
    return true
  } else {
    return false
  }
}

运行例子如下,直接在谷歌 console 运行即可:

// 例子
function* one() {
  return 'one'
}

function* two() {
  return yield 'two'
}

function* three() {
  return Promise.resolve('three')
}

function* four() {
  return yield Promise.resolve('four')
}

function* five() {
  const a = yield new Promise(resolve => {
    setTimeout(() => {
      resolve('waiting five over')
    }, 1000)
  })
  console.log(a)
  return 'five'
}

function* err() {
  const a = 2
  a = 3
  return a
}

function* all() {
  const a = yield one()
  console.log(a)
  const b = yield two()
  console.log(b)
  const c = yield three()
  console.log(c)
  const d = yield four()
  console.log(d)
  const e = yield five()
  console.log(e)
  try {
    yield err()
  } catch (err) {
    console.log('发生了错误', err)
  }
  return 'over'
}

generatorExecuter(all).then(value => {
  console.log(value)
})

// 或者
// generatorExecuter(all()).then(value => {
//   console.log(value)
// })
查看原文

赞 11 收藏 8 评论 0

Samon 发布了文章 · 2019-08-30

深入了解 JavaScript 内存泄露

这篇文章是针对浏览器的 JavaScript 脚本,Node.js 大同小异,这里不涉及到 Node.js 的场景。当然 Node.js 作为服务端语言,必然更关注内存泄漏的问题。

用户一般不会在一个 Web 页面停留比较久,即使有一点内存泄漏,重载页面内存也会跟着释放。而且浏览器也有自动回收内存的机制,所以我们前端其实并没有像 C、C++ 这类语言一样,特别关注内存泄漏的问题。

但是如果我们对内存泄漏没有什么概念,有时候还是有可能因为内存泄漏,导致页面卡顿。了解内存泄漏,如何避免内存泄漏,也是我们提升前端技能的必经之路。

俗话说好记忆不如烂笔头,所以本人就总结了一些内存泄漏相关的知识,避免一些低级的内存泄漏问题。

什么是内存?

在硬件级别上,计算机内存由大量触发器组成。每个触发器包含几个晶体管,能够存储一个位。单个触发器可以通过唯一标识符寻址,因此我们可以读取和覆盖它们。因此,从概念上讲,我们可以把我们的整个计算机内存看作是一个巨大的位数组,我们可以读和写。

这么底层的概念,了解下就好,绝大多数数情况下,JavaScript 语言作为你们高级语言,无需我们使用二进制进直接进行读和写。

内存生命周期

内存也是有生命周期的,不管什么程序语言,一般可以按顺序分为三个周期:

  • 分配期

    分配所需要的内存

  • 使用期

    使用分配到的内存(读、写)

  • 释放期

    不需要时将其释放和归还

内存分配 -> 内存使用 -> 内存释放。

什么是内存泄漏?

计算机科学中,内存泄漏指由于疏忽或错误造成程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。

如果内存不需要时,没有经过生命周期的释放期,那么就存在内存泄漏

内存泄漏简单理解:无用的内存还在占用,得不到释放和归还。比较严重时,无用的内存会持续递增,从而导致整个系统卡顿,甚至崩溃。

JavaScript 内存管理机制

像 C 语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()free()。相反,JavaScript是在创建变量(对象,字符串等)时自动进行了分配内存,并且在不使用它们时“自动”释放。 释放的过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者错误的感觉他们可以不关心内存管理。

JavaScript 内存管理机制和内存的生命周期是一一对应的。首先需要分配内存,然后使用内存,最后释放内存

其中 JavaScript 语言不需要程序员手动分配内存,绝大部分情况下也不需要手动释放内存,对 JavaScript 程序员来说通常就是使用内存(即使用变量、函数、对象等)。

内存分配

JavaScript 定义变量就会自动分配内存的。我们只需了解 JavaScript 的内存是自动分配的就足够了

看下内存自动分配的例子:

// 给数值变量分配内存
let number = 123; 
// 给字符串分配内存
const string = "xianshannan"; 
// 给对象及其包含的值分配内存
const object = {
  a: 1,
  b: null
}; 
// 给数组及其包含的值分配内存(就像对象一样)
const array = [1, null, "abra"]; 
// 给函数(可调用的对象)分配内存
function func(a){
  return a;
} 

内存使用

使用值的过程实际上是对分配内存进行读取与写入的操作。读取与写入可能是写入一个变量或者一个对象的属性值,甚至传递函数的参数。

根据上面的内存自动分配例子,我们继续内存使用的例子:

// 写入内存
number = 234;
// 读取 number 和 func 的内存,写入 func 参数内存
func(number);

内存回收

前端界一般称垃圾内存回收GC(Garbage Collection,即垃圾回收)。

内存泄漏一般都是发生在这一步,JavaScript 的内存回收机制虽然能回收绝大部分的垃圾内存,但是还是存在回收不了的情况,如果存在这些情况,需要我们手动清理内存。

以前一些老版本的浏览器的 JavaScript 回收机制没那么完善,经常出现一些 bug 的内存泄漏,不过现在的浏览器基本都没这些问题了,已过时的知识这里就不做深究了。

这里了解下现在的 JavaScript 的垃圾内存的两种回收方式,熟悉下这两种算法可以帮助我们理解一些内存泄漏的场景。

引用计数垃圾收集

这是最初级的垃圾收集算法。此算法把“对象是否不再需要”简化定义为“对象有没有其他对象引用到它”。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

看下下面的例子,“这个对象”的内存被回收了吗?

// “这个对象”分配给 a 变量
var a = {
  a: 1,
  b: 2,
}
// b 引用“这个对象”
var b = a; 
// 现在,“这个对象”的原始引用 a 被 b 替换了
a = 1;

当前执行环境中,“这个对象”内存还没有被回收的,需要手动释放“这个对象”的内存(当然是还没离开执行环境的情况下),例如:

b = null;
// 或者 b = 1,反正替换“这个对象”就行了

这样引用的"这个对象"的内存就被回收了。

ES6 把引用有区分为强引用弱引用,这个目前只有再 Set 和 Map 中才有。

强引用才会有引用计数叠加,只有引用计数为 0 的对象的内存才会被回收,所以一般需要手动回收内存(手动回收的前提在于标记清除法还没执行,还处于当前执行环境)。

弱引用没有触发引用计数叠加,只要引用计数为 0,弱引用就会自动消失,无需手动回收内存。

标记清除法

当变量进入执行环境时标记为“进入环境”,当变量离开执行环境时则标记为“离开环境”,被标记为“进入环境”的变量是不能被回收的,因为它们正在被使用,而标记为“离开环境”的变量则可以被回收

环境可以理解为我们的作用域,但是全局作用域的变量只会在页面关闭才会销毁。

// 假设这里是全局变量
// b 被标记进入环境
var b = 2;
function test() {
  var a = 1;
  // 函数执行时,a 被标记进入环境
  return a + b;
}
// 函数执行结束,a 被标记离开环境,被回收
// 但是 b 就没有被标记离开环境
test();

JavaScript 内存泄漏的一些场景

JavaScript 的内存回收机制虽然能回收绝大部分的垃圾内存,但是还是存在回收不了的情况。程序员要让浏览器内存泄漏,浏览器也是管不了的。

下面有些例子是在执行环境中,没离开当前执行环境,还没触发标记清除法。所以你需要读懂上面 JavaScript 的内存回收机制,才能更好理解下面的场景。

意外的全局变量

// 在全局作用域下定义
function count(number) {
  // basicCount 相当于 window.basicCount = 2;
  basicCount = 2;
  return basicCount + number;
}

不过在 eslint 帮助下,这种场景现在基本没人会犯了,eslint 会直接报错,了解下就好。

被遗忘的计时器

无用的计时器忘记清理是新手最容易犯的错误之一。

就拿一个 vue 组件来做例子。

<template>
  <div></div>
</template>

<script>
export default {
  methods: {
    refresh() {
      // 获取一些数据
    },
  },
  mounted() {
    setInterval(function() {
      // 轮询获取数据
      this.refresh()
    }, 2000)
  },
}
</script>

上面的组件销毁的时候,setInterval 还是在运行的,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候清除计时器,如下:

<template>
  <div></div>
</template>

<script>
export default {
  methods: {
    refresh() {
      // 获取一些数据
    },
  },
  mounted() {
    this.refreshInterval = setInterval(function() {
      // 轮询获取数据
      this.refresh()
    }, 2000)
  },
  beforeDestroy() {
    clearInterval(this.refreshInterval)
  },
}
</script>

被遗忘的事件监听器

无用的事件监听器忘记清理是新手最容易犯的错误之一。

还是继续使用 vue 组件做例子。

<template>
  <div></div>
</template>

<script>
export default {
  mounted() {
    window.addEventListener('resize', () => {
      // 这里做一些操作
    })
  },
}
</script>

上面的组件销毁的时候,resize 事件还是在监听中,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候移除相关的事件,如下:

<template>
  <div></div>
</template>

<script>
export default {
  mounted() {
    this.resizeEventCallback = () => {
      // 这里做一些操作
    }
    window.addEventListener('resize', this.resizeEventCallback)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.resizeEventCallback)
  },
}
</script>

被遗忘的 ES6 Set 成员

如果对 Set 不熟悉,可以看这里

如下是有内存泄漏的(成员是引用类型的,即对象):

let map = new Set();
let value = { test: 22};
map.add(value);

value= null;

需要改成这样,才没内存泄漏:

let map = new Set();
let value = { test: 22};
map.add(value);

map.delete(value);
value = null;

有个更便捷的方式,使用 WeakSet,WeakSet 的成员是弱引用,内存回收不会考虑到这个引用是否存在。

let map = new WeakSet();
let value = { test: 22};
map.add(value);

value = null;

被遗忘的 ES6 Map 键名

如果对 Map 不熟悉,可以看这里

如下是有内存泄漏的(键值是引用类型的,即对象):

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);
key = null;

需要改成这样,才没内存泄漏:

let map = new Map();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);

map.delete(key);
key = null;

有个更便捷的方式,使用 WeakMap,WeakMap 的键名是弱引用,内存回收不会考虑到这个引用是否存在。

let map = new WeakMap();
let key = new Array(5 * 1024 * 1024);
map.set(key, 1);

key = null;

被遗忘的订阅发布事件监听器

这个跟上面的被遗忘的事件监听器的道理是一样的。

假设订阅发布事件有三个方法 emitonoff 三个方法。

还是继续使用 vue 组件做例子。

<template>
  <div @click="onClick"></div>
</template>

<script>
import customEvent from 'event'

export default {
  methods: {
    onClick() {
      customEvent.emit('test', { type: 'click' })
    },
  },
  mounted() {
    customEvent.on('test', data => {
      // 一些逻辑
      console.log(data)
    })
  },
}
</script>

上面的组件销毁的时候,自定义 test 事件还是在监听中,里面涉及到的内存都是没法回收的(浏览器会认为这是必须的内存,不是垃圾内存),需要在组件销毁的时候移除相关的事件,如下:

<template>
  <div @click="onClick"></div>
</template>

<script>
import customEvent from 'event'

export default {
  methods: {
    onClick() {
      customEvent.emit('test', { type: 'click' })
    },
  },
  mounted() {
    customEvent.on('test', data => {
      // 一些逻辑
      console.log(data)
    })
  },
  beforeDestroy() {
    customEvent.off('test')
  },
}
</script>

被遗忘的闭包

闭包是经常使用的,闭包能给我们带来很多便利。

首先看下这个代码:

function closure() {
  const name = 'xianshannan'
  return () => {
    return name
      .split('')
      .reverse()
      .join('')
  }
}
const reverseName = closure()
// 这里调用了 reverseName
reverseName();

上面有没有内存泄漏?

上面是没有内存泄漏的,因为name 变量是要用到的(非垃圾)。这也是从侧面反映了闭包的缺点,内存占用相对高,量多了会有性能影响。

但是改成这样就是有内存泄漏的:

function closure() {
  const name = 'xianshannan'
  return () => {
    return name
      .split('')
      .reverse()
      .join('')
  }
}
const reverseName = closure()

在当前执行环境未结束的情况下,严格来说,这样是有内存泄漏的,name 变量是被 closure 返回的函数调用了,但是返回的函数没被使用,这个场景下 name 就属于垃圾内存。name 不是必须的,但是还是占用了内存,也不可被回收。

当然这种也是极端情况,很少人会犯这种低级错误。这个例子可以让我们更清楚的认识内存泄漏。

脱离 DOM 的引用

每个页面上的 DOM 都是占用内存的,假设有一个页面 A 元素,我们获取到了 A 元素 DOM 对象,然后赋值到了一个变量(内存指向是一样的),然后移除了页面的 A 元素,如果这个变量由于其他原因没有被回收,那么就存在内存泄漏,如下面的例子:

class Test {
  constructor() {
    this.elements = {
      button: document.querySelector('#button'),
      div: document.querySelector('#div'),
      span: document.querySelector('#span'),
    }
  }
  removeButton() {
    document.body.removeChild(this.elements.button)
    // this.elements.button = null
  }
}

const a = new Test()
a.removeButton()

上面的例子 button 元素 虽然在页面上移除了,但是内存指向换为了 this.elements.button,内存占用还是存在的。所以上面的代码还需要这样写: this.elements.button = null,手动释放这个内存。

如何发现内存泄漏?

内存泄漏时,内存一般都是会周期性的增长,我们可以借助谷歌浏览器的开发者工具进行判别。

这里不进行详细的开发者工具使用说明,详细看谷歌开发者工具,不过谷歌浏览器是不断迭代更新的,有些文档落后了,界面长得不一样。

本人测试的谷歌版本为:版本 76.0.3809.100(正式版本) (64 位)

这里针对下面例子进行一步一步的排查和找到问题出现在哪里:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <div id="app">
      <button id="run">运行</button>
      <button id="stop">停止</button>
    </div>
    <script>
      const arr = []
      for (let i = 0; i < 200000; i++) {
        arr.push(i)
      }
      let newArr = []

      function run() {
        newArr = newArr.concat(arr)
      }

      let clearRun

      document.querySelector('#run').onclick = function() {
        clearRun = setInterval(() => {
          run()
        }, 1000)
      }

      document.querySelector('#stop').onclick = function() {
        clearInterval(clearRun)
      }
    </script>
  </body>
</html>

上面例子的代码可以直接运行的,怎么运行我就不多说了。

第一步:确定是否是内存泄漏问题

访问上面的代码页面,打开谷歌开发者工具,切换至 Performance 选项,勾选 Memory 选项。

在页面上点击运行按钮,然后在开发者工具上面点击左上角的录制按钮,10 秒后在页面上点击停止按钮,5 秒后停止内存录制。得到的内存走势如下:

clipboard.png

由上图可知,10 秒之前内存周期性增长,10 后点击了停止按钮,内存平稳,不再递增。

我们可以使用内存走势图判断当前页面是否有内存泄漏。经过测试上面的代码 20000 个数组项改为 20 个数组项,内存走势也一样能看出来。

第二步:查找内存泄漏出现的位置

上一步确认是内存泄漏问题后,我们继续利用谷歌开发者工具进行问题查找。

访问上面的代码页面,打开谷歌开发者工具,切换至 Memory 选项。页面上点击运行按钮,然后点击开发者工具左上角录制按钮,录制完成后继续点击录制,知道录制完三个为止。然后点击页面的停止按钮,再连续录制 3 次内存(不要清理之前的录制)。下图就是进行这些步骤后的截图:

clipboard.png

从这里也可以看出,点击运行按钮后,内存在不断递增。点击停止按钮后,内存就平稳了。虽然我们也可以使用这样的方式来判别是否存在内存泄漏,但是不够第一步的方法便捷,走势图也更直观。

然后第二步的主要目的来了,记录 JavaScript 堆内存才是内存录制的主要目的,我们可以看到哪个堆占用的内存更高。

在刚才的录制中选择 Snapshot 3 ,然后按照 Shallow Size 进行逆序排序(不了解的可以看内存术语),如下:

clipboard.png

从内存记录中,发现 array 对象占用最大,展开后发现,第一个 object elements 占用最大,选择这个 object elements 后可以在下面看到 newArr 变量,然后点击 test:23,只要是高亮下划线的地方都可以进去看看 (测试页面是 test.html),可以跳转到 newArr 附近。

参考资料

查看原文

赞 29 收藏 24 评论 3

Samon 发布了文章 · 2019-08-28

前端优化之 Http 相关优化总结


学习和总结文章同步发布于 https://github.com/xianshanna...,有兴趣可以关注一下,一起学习和进步。

Http 优化方式是前端性能优化的重要部分,也是前端必备的知识点之一。

减少静态资源文件大小

这个是最根本的途径,假设真的有个 10 几兆以上的静态资源文件,不减少大小的情况下,即使优化做到了极致,用户体验也好不了哪里去。

如果整个网页就 2KB 大小的资源文件,不优化都很快。

代码层优化

  • 只打包用到的依赖包,目前 webpack tree shaking 功能已经自动处理了,还有尽量少使用第三方依赖包(当然看情况啦)。
  • 代码分割(code splitting),不同页面加载自己用到的代码,不加载其他页面的代码(其实也属于懒加载)。

传输层优化

Http 传承启用压缩传输方式。

一般我们开启 gzip,基本都能压缩 6 倍左右(一般都是文件越大,字符串相似率越大,压缩率越大)。

首先经过服务器压缩后,然后 Http 响应头 Content-Encoding 设置为相应的压缩方式,浏览器会自动解压的。

Content-Encoding: gzip

当然还有其他的压缩方式,如 compress、deflate 等等,目前使用最广的还是 gzip。

适当合并或者分散请求

合并请求或者分散请求需要看实际情况的。

合并请求

http 1.1 (包括 http1.1)之前的版本,浏览器存在同域名并发限制,谷歌目前是同域名并发现在为6 个请求,其他的浏览器或多或少,但也差不了多少。

如果是使用的是 http1.1 web 服务,那么我们首次加载的资源要基本保证在 4 个以内,所以静态资源请求数过多就要看情况进行合并请求了。

分散请求

Http2.0 没有同域名并发的问题,我们可以适当分散请求。当然如果在 Http1.1 一个资源文件过大,然后并发并没有达到限制,也可以拆分资源文件达到分散请求的目的。

使用预加载

预加载某些情况下可以大大提升加载速度进而提示用户体验。

预加载需要了解 preloadprefetch 的知识。

预加载 DNS

dns 解析也是需要时间的,特别在移动端的时候更明显,我们可以预解析 dns 减少不通域名 dns 解析时间。

    <link rel="dns-prefetch" href="//example.com">

其实还有个 preconnectpreconnect 不仅完成 DNS 预解析,同时还将进行 TCP 握手和建立传输层协议,但是浏览器有兼容性,目前用不上。

<link rel="preconnect" href="http://example.com">

预加载静态资源

使用 preload

通过 preload 一般是预加载当前页面要用到的图片、字体、js 脚本、css 文件等静态资源文件。

场景一

如果需要,你可以完全以脚本化的方式来执行这些预加载操作。例如,我们在这里创建一个HTMLLinkElement 实例,然后将他们附加到 DOM 上:

var preloadLink = document.createElement("link");
preloadLink.href = "myscript.js";
preloadLink.rel = "preload";
preloadLink.as = "script";
document.head.appendChild(preloadLink);

这意味着浏览器将预加载这个JavaScript文件,但并不实际执行它。

如果要对其加以执行,在需要的时候,你可以执行:

var preloadedScript = document.createElement("script");
preloadedScript.src = "myscript.js";
document.body.appendChild(preloadedScript);

当你需要预加载一个脚本,但需要推迟到需要的时候才令其执行时,这种方式会特别有用。

场景二

字体是要使用到的时候才会去加载字体的(如果字体是自定义的字体,会发起 Http 请求加载字体)。

由于这个特性,我们可以预加载字体,待使用到字体的时候,字体已经加载完毕,无需等待加载。

如下我们没有 preload 的时候,代码也是可以运行的,但是字体加载是需要等待页面 JS、CSS 资源加载完毕后,当前页面使用到字体才会去加载的:

<style>
  @font-face {
    font-family: Test-Number-Medium;
    src: url(./static/font/Test-Number-Medium.otf);
  }
</style>

我们加上:

<link rel="preload" href="./static/font/Test-Number-Medium.otf">

就可以提交加载,节省大部分甚至全部的字体加载时间,一般都是全部的时间,因为 JS 资源文件比字体大多了(并行下载,最长的资源加载时间,决定了最大加载时间)。

使用 prefech

prefetch 一般是预加载非当前页面的资源,prefetch 是一个低优先级的资源提示,允许浏览器在后台(空闲时)获取将来可能用得到的资源,并且将他们存储在浏览器的缓存中。当前页面加载完毕,才会开始下载 d带有 prefetch 标记的资源,然后当用户进入另外一个页面,已经 prefetched 的资源可以立刻从缓存中加载。

不过 prefech 的应用场景比较少。

<link rel="prefetch" href="/uploads/images/pic.png">

采用懒加载

图片懒加载

这种做法一般都是在用户滚动到响应位置(当然从用户体验式来说,需要提前一点加载),才会加载响应的图片,图片特别多的网上基本都会做这个优化(如视频网站)。

或者幻灯片查看图片的时候,用户即将查查下一张图片的时候再加载,而不是一次性加载全部的图片。

JS 懒加载

需要用到相关 JS 时,通过动态创建 <script> 标签进行 JS 文件懒加载,如 Webpack 的 code splitting

合理使用 defer 和 async

带有 deferasync 属性的 script 资源都会并行下载,而且不会影响页面的解析,从而达到了节省脚本下载时间

两种的不同的在于:

带有 defer 属性的资源会按照顺序在页面出现的属性,资源加载完后,会在 DOMContentLoaded事件调用之前依次执行。

带有 async 属性的资源则是下载完立即执行,可能在 DOMContentLoaded 事件之前或者之后执行,多个带有 async 属性的资源无执行顺序,谁先加载完成,谁先执行。

那么为什么可以节省下载时间?我们来对比一下。

defer 资源加载类似于把 <script> 放在 </body> 前,但是 defer 资源可以和 <head> 的资源并行下载,那么就可以节省部分甚至全部的下载时间(看 <head> 资源和 defer 资源大小)。所以我们何以适当的把 defer 资源放在 <head>,某些场景是可以提升一定的速度。

async 资源有点类似于 </body> 前的 script 资源加载完成后,动态创建 <script> 标签加载资源,但是却要等待页面 JS 文件执行之后才可以加载,而 async 资源无需等待即可提前加载,就可以节省一定的加载时间。所以比较适合加载如谷歌分析百度统计日志上报等类型的 js 资源,都是独立运作也不影响页面的辅助 js 资源。

利用缓存

缓存对于再次访问相同资源来说,是个极大的优化,缓存是 http 优化的必经之道。对于 css 和 js 这些静态资源文件,我们一般都是用强缓存(例如缓存30天),强缓存无需再次向服务请求静态资源。
但是强缓存如果使用不当,那么会对用户造成意想不到的 bug,如入口 html 文件就不能被强缓存了,否则版本更新后,用户在缓存期间是无法访问到新版的页面。

详细可以看下本人的另一篇缓存相关的文章,浏览器之HTTP缓存的那些事

使用 Http2.0

Http2.0 多路复用解决了多域名并发现在问题,可以节省资源总体的下载时间,还有请求头压缩和差异传输也会提高传输效率。

多路复用

HTTP1.1 持久连接解决了连接复用问题,但还是存在着一个问题,一个 TCP 无法并发处理请求:在一个 TCP 连接中,同一时间只能够发送一个请求,并且需要等响应完成才能够发送第二个请求。

HTTP2.0 使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比 HTTP1.1 大了好几个数量级。

当然 HTTP1.1 也可以多建立几个 TCP 连接,来支持处理更多并发的请求,但是创建TCP连接本身也是有开销的。

TCP 连接有一个预热和保护的过程,先检查数据是否传送成功,一旦成功过,则慢慢加大传输速度。因此对应瞬时并发的连接,服务器的响应就会变慢。所以最好能使用一个建立好的连接,并且这个连接可以支持瞬时并发的请求。

多路复用能带来哪些优化呢?

有多路复用特性,那么浏览器对同一域名的链接数的限制也是没必要的了(HTTP1.1 的谷歌对统一域名并发请求最多支持 6个 持久链接)。

那么我们可以根据实际情况进行资源拆分,从而节省下载时间,无并发请求限制的情况下,下载的时间是根据并行下载的最长时间来算的,无需等待上一个资源下载,才能进行另外一个资源的下载,在资源比较多的情况下,这将大大提升资源总体的下载速度。

以前的 CSS 的雪碧图 优化手段,在多路复用的特性下,已经是没必要的了。

多路复用还带来了,延迟低的优化,这也是速度提升的一方面。

二进制分帧

在应用层与传输层之间增加一个二进制分帧层,以此达到在不改动 HTTP 的语义,HTTP 方法、状态码、URI 及首部字段的情况下,突破 HTTP1.1 的性能限制,改进传输性能,实现低延迟和高吞吐量。

在二进制分帧层上,HTTP2.0 会将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码,其中HTTP1.x 的首部信息会被封装到 Headers 帧,而我们的 request body 则封装到 Data 帧里面。

首部字段压缩

HTTP2.0 使用 HPACK 算法对首部字段的数据进行压缩,这样数据体积小了,在网络上传输就会更快。

首部字段差异传输

HTTP2.0 规定了在客户端和服务器端会使用并且维护首部表来跟踪和存储之前发送的键值对,对于相同的头部,不必再通过请求发送,只需发送一次。

事实上,如果请求中不包含首部字段(例如对同一资源的轮询请求),此时服务器自动使用之前请求发送的首部字段,那么首部字段开销就是零字节。

如果首部发生变化了,那么只需要发送变化了数据在 Headers 帧里面,新增或修改的首部帧会被追加到首部表首部表在 HTTP2.0 的连接存在期内始终存在,由客户端和服务器共同渐进地更新。

使用 CDN

严格意义上,CDN 不算 Http 优化,前端也无法直接处理这个事情,这是运维的事。CDN 节点可以解决跨运营商和跨地域访问的问题,提升访问速度。

CDN的全称是 Content Delivery Network,即内容分发网络。CDN 是构建在网络之上的内容分发网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。CDN 的关键技术主要有内容存储和分发技术。—— 科学百科

CDN 有什么优势?

CDN 最大的优势在于提升用户资源访问速度,因此静态资源走 CDN 是一个很好的优化点。
分布式服务器,用户就近访问,CDN 节点可以解决跨运营商和跨地域访问的问题,同时分散源服务器访问压力。

还有一个而外的优点:
无 cookie 传输(其实这个不完全算是优势)。静态资源一般无需 cookie,静态资源放在不同域名可以减少一定程度的带宽和提升一定的访问速度,虽然单个请求不明显,但是量多了是会有质的区别的。

CDN 是如何分散源服务器的压力的?

CDN 的核心点有两个: 一个是缓存,一个是回源。

通过缓存和回源策略,达到分散源服务器的压力。首先将从根服务器请求来的资源按要求缓存。然后当有用户访问某个资源的时候,如果被解析到的那个 CDN 节点没有缓存响应的内容,或者是缓存已经到期,就会回源站去获取。

查看原文

赞 17 收藏 10 评论 0

Samon 发布了文章 · 2019-08-27

HTTPS 的基本原理

HTTPS = HTTP + TLS/SSL,简单理解 HTTPS 其实就是在 HTTP 上面加多了一层安全层。HTTP 可以是 Http2.0 也可以是 Http1.1,不过现在 Http2.0 是强制要求使用 Https 的。

HTTPS 基本原理

首先需要一个第三方认证机构(CA认证),确保公钥的合法性(即证书,不合法的证书浏览器会警告),然后利用非对称加密(公钥私钥)方式加密并传输共享密钥到服务器,可以确保共享密钥无法被拦截被获取到(共享密钥被公钥加密了,只有对应的私钥才能解密,服务器有私钥),最终的客户端和服务端 HTTP 传输就是使用共享秘钥加密进行通信。

HTTPS 流程图

首先,我们先看下HTTPS 的整个流程。

clipboard.png

HTTPS 是如何确保安全的?

  • 使用非对称密钥(即公钥私钥))和对称密钥)(即共享密钥)相结合

    通过公钥私钥的方式,避免了共享密钥发送途中被第三方拦截获取密钥的安全问题。

    通过公钥和私钥加密建立保护层(即 SSL 保护层),后续的 Http 请求就会使用共享密钥进行加密通信(共享的密钥已经被 SSL 保护起来了,外面无法拦截到),即所谓的安全层。

    所以建立了安全层后,即使 HTTP 报文被拦截到,也无法解密。

  • CA 认证

    由于公钥这个环节是公开的,存在被替换的风险,所以就有了第三方证书认证公司(CA认证),浏览器通过判断证书是否有效,发现网站是否值得信任。

    一般系统或者浏览器都会内置信任的根证书(这些 CA 组织都是非常可信的),浏览器可以根据这个根证书判断网站的证书是否合法。

    证书如果不合法,那么浏览器就会警告,不给用户访问证书不合法的网站,除非用户跳过这个警告。

查看原文

赞 12 收藏 7 评论 0

Samon 发布了文章 · 2019-08-26

订阅发布模式和观察者模式的区别

学习和总结文章同步发布于 https://github.com/xianshanna...,有兴趣可以关注一下,一起学习和进步。

首先我们需要了解两者的定义和实现的方式,才能更好的区分两者的不同点。

或许以前认为订阅发布模式是观察者模式的一种别称,但是发展至今,概念已经有了不少区别。

订阅发布模式

软件架构中,发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

或许你用过 eventemitter、node 的 events、Backbone 的 events 等等,这些都是前端早期,比较流行的数据流通信方式,即订阅发布模式

从字面意思来看,我们需要首先订阅,发布者发布消息后才会收到发布的消息。不过我们还需要一个中间者来协调,从事件角度来说,这个中间者就是事件中心,协调发布者和订阅者直接的消息通信。

完成订阅发布整个流程需要三个角色:

  • 发布者
  • 事件中心
  • 订阅者

以事件为例,简单流程如下:

发布者->事件中心<=>订阅者,订阅者需要向事件中心订阅指定的事件 -> 发布者向事件中心发布指定事件内容 -> 事件中心通知订阅者 -> 订阅者收到消息(可能是多个订阅者),到此完成了一次订阅发布的流程。

简单的代码实现如下:

class Event {
  constructor() {
    // 所有 eventType 监听器回调函数(数组)
    this.listeners = {}
  }
  /**
   * 订阅事件
   * @param {String} eventType 事件类型
   * @param {Function} listener 订阅后发布动作触发的回调函数,参数为发布的数据
   */
  on(eventType, listener) {
    if (!this.listeners[eventType]) {
      this.listeners[eventType] = []
    }
    this.listeners[eventType].push(listener)
  }
  /**
   * 发布事件
   * @param {String} eventType 事件类型
   * @param {Any} data 发布的内容
   */
  emit(eventType, data) {
    const callbacks = this.listeners[eventType]
    if (callbacks) {
      callbacks.forEach((c) => {
        c(data)
      })
    }
  }
}

const event = new Event()
event.on('open', (data) => {
  console.log(data)
})
event.emit('open', { open: true })

Event 可以理解为事件中心,提供了订阅和发布功能。

订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。

观察者模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

观察者模式我们可能比较熟悉的场景就是响应式数据,如 Vue 的响应式、Mbox 的响应式。

观察者模式有完成整个流程需要两个角色:

  • 目标
  • 观察者

简单流程如下:

目标<=>观察者,观察者观察目标(监听目标)-> 目标发生变化-> 目标主动通知观察者。

简单的代码实现如下:

/**
 * 观察监听一个对象成员的变化
 * @param {Object} obj 观察的对象
 * @param {String} targetVariable 观察的对象成员
 * @param {Function} callback 目标变化触发的回调
 */
function observer(obj, targetVariable, callback) {
  if (!obj.data) {
    obj.data = {}
  }
  Object.defineProperty(obj, targetVariable, {
    get() {
      return this.data[targetVariable]
    },
    set(val) {
      this.data[targetVariable] = val
      // 目标主动通知观察者
      callback && callback(val)
    },
  })
  if (obj.data[targetVariable]) {
    callback && callback(obj.data[targetVariable])
  }
}

可运行例子如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover"
    />
    <title></title>
  </head>
  <body>
    <div id="app">
      <div id="dom-one"></div>
      <br />
      <div id="dom-two"></div>
      <br />
      <button id="btn">改变</button>
    </div>
    <script>
      /**
       * 观察监听一个对象成员的变化
       * @param {Object} obj 观察的对象
       * @param {String} targetVariable 观察的对象成员
       * @param {Function} callback 目标变化触发的回调
       */
      function observer(obj, targetVariable, callback) {
        if (!obj.data) {
          obj.data = {}
        }
        Object.defineProperty(obj, targetVariable, {
          get() {
            return this.data[targetVariable]
          },
          set(val) {
            this.data[targetVariable] = val
            // 目标主动通知观察者
            callback && callback(val)
          },
        })
        if (obj.data[targetVariable]) {
          callback && callback(obj.data[targetVariable])
        }
      }

      const obj = {
        data: { description: '原始值' },
      }

      observer(obj, 'description', value => {
        document.querySelector('#dom-one').innerHTML = value
        document.querySelector('#dom-two').innerHTML = value
      })

      btn.onclick = () => {
        obj.description = '改变了'
      }
    </script>
  </body>
</html>

两者的区别在哪?

角色角度来看,订阅发布模式需要三种角色,发布者、事件中心和订阅者。二观察者模式需要两种角色,目标和观察者,无事件中心负责通信。

从耦合度上来看,订阅发布模式是一个事件中心调度模式,订阅者和发布者是没有直接关联的,通过事件中心进行关联,两者是解耦的。而观察者模式中目标和观察者是直接关联的,耦合在一起(有些观念说观察者是解耦,解耦的是业务代码,不是目标和观察者本身)。

两者的优缺点?

优缺点都是从前端角度来看的。

订阅发布模式优点

  • 灵活

    由于订阅发布模式的发布者和订阅者是解耦的,只要引入订阅发布模式的事件中心,无论在何处都可以发布订阅。同时订阅发布者相互之间不影响。

订阅发布模式在使用不当的情况下,容易造成数据流混乱,所以才有了 React 提出的单项数据流思想,就是为了解决数据流混乱的问题。

订阅发布模式缺点

  • 容易导致代码不好维护

    灵活是有点,同时也是缺点,使用不当就会造成数据流混乱,导致代码不好维护。

  • 性能消耗更大

    订阅发布模式需要维护事件列队,订阅的事件越多,内存消耗越大。

观察者模式优点

  • 响应式

    目标变化就会通知观察者,这是观察者最大的有点,也是因为这个优点,观察者模式在前端才会这么出名。

观察者模式缺点

  • 不灵活

    相比订阅发布模式,由于目标和观察者是耦合在一起的,所以观察者模式需要同时引入目标和观察者才能达到响应式的效果。而订阅发布模式只需要引入事件中心,订阅者和发布者可以不再一处。

参考文章

订阅发布模式和观察者模式真的不一样

查看原文

赞 22 收藏 16 评论 5

Samon 发布了文章 · 2019-08-19

JavaScript 的一些常用设计模式

学习和总结文章同步发布于 https://github.com/xianshanna...,有兴趣可以关注一下,一起学习和进步。
设计模式的定义:在面向对象软件设计过程中针对特定问题的简洁而优雅的解决方案

设计模式是前人解决某个特定场景下对而总结出来的一些解决方案。可能刚开始接触编程还没有什么经验的时候,会感觉设计模式没那么好理解,这个也很正常。有些简单的设计模式我们有时候用到,不过没意识到也是存在的。

学习设计模式,可以让我们在处理问题的时候提供更多更快的解决思路。

当然设计模式的应用也不是一时半会就会上手,很多情况下我们编写的业务逻辑都没用到设计模式或者本来就不需要特定的设计模式。

适配器模式

这个使我们常使用的设计模式,也算最简单的设计模式之一,好处在于可以保持原有接口的数据结构不变动。

适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。

例子

适配器模式很好理解,假设我们和后端定义了一个接口数据结构为(可以理解为旧接口):

[
  {
    "label": "选择一",
    "value": 0
  },
  {
    "label": "选择二",
    "value": 1
  }
]

但是后端后面因为其他原因,需要定义返回的结构为(可以理解为新接口):

[
  {
    "label": "选择一",
    "text": 0
  },
  {
    "label": "选择二",
    "text": 1
  }
]

然后我们前端的使用到后端接口有好几处,那么我可以把新的接口字段结构适配为老接口的,就不需要各处去修改字段,只要把源头的数据适配好就可以了。

当然上面的是非常简单的场景,也是经常用到的场景。或许你会认为后端处理不更好了,的确是这样更好,但是这个不是我们讨论的范围。

单例模式

单例模式,从字面意思也很好理解,就是实例化多次都只会有一个实例。

有些场景实例化一次,可以达到缓存效果,可以减少内存占用。还有些场景就是必须只能实例化一次,否则实例化多次会覆盖之前的实例,导致出现 bug(这种场景比较少见)。

例子

实现弹框的一种做法是先创建好弹框, 然后使之隐藏, 这样子的话会浪费部分不必要的 DOM 开销, 我们可以在需要弹框的时候再进行创建, 同时结合单例模式实现只有一个实例, 从而节省部分 DOM 开销。下列为登入框部分代码:

const createLoginLayer = function() {
  const div = document.createElement('div')
  div.innerHTML = '登入浮框'
  div.style.display = 'none'
  document.body.appendChild(div)
  return div
}

使单例模式和创建弹框代码解耦

const getSingle = function(fn) {
  const result
  return function() {
    return result || result = fn.apply(this, arguments)
  }
}
const createSingleLoginLayer = getSingle(createLoginLayer)

document.getElementById('loginBtn').onclick = function() {
  createSingleLoginLayer()
}

代理模式

代理模式的定义:为一个对象提供一个代用品或占位符,以便控制对它的访问。

代理对象拥有本体对象的一切功能的同时,可以拥有而外的功能。而且代理对象和本体对象具有一致的接口,对使用者友好。

虚拟代理

下面这段代码运用代理模式来实现图片预加载,可以看到通过代理模式巧妙地将创建图片与预加载逻辑分离,,并且在未来如果不需要预加载,只要改成请求本体代替请求代理对象就行。

const myImage = (function() {
  const imgNode = document.createElement('img')
  document.body.appendChild(imgNode)
  return {
    setSrc: function(src) {
      imgNode.src = src
    }
  }
})()

const proxyImage = (function() {
  const img = new Image()
  img.onload = function() { // http 图片加载完毕后才会执行
    myImage.setSrc(this.src)
  }
  return {
    setSrc: function(src) {
      myImage.setSrc('loading.jpg') // 本地 loading 图片
      img.src = src
    }
  }
})()

proxyImage.setSrc('http://loaded.jpg')

缓存代理

在原有的功能上加上结果缓存功能,就属于缓存代理。

原先有个功能是实现字符串反转(reverseString),那么在不改变 reverseString 的现有逻辑,我们可以使用缓存代理模式实现性能的优化,当然也可以在值改变的时候去处理下其他逻辑,如 Vue computed 的用法。

function reverseString(str) {
  return str
    .split('')
    .reverse()
    .join('')
}
const reverseStringProxy = (function() {
  const cached = {}
  return function(str) {
    if (cached[str]) {
      return cached[str]
    }
    cached[str] = reverseString(str)
    return cached[str]
  }
})()

订阅发布模式

软件架构中,发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者(如果有的话)存在。

或许你用过 eventemitter、node 的 events、Backbone 的 events 等等,这些都是前端早期,比较流行的数据流通信方式,即订阅发布模式

从字面意思来看,我们需要首先订阅,发布者发布消息后才会收到发布的消息。不过我们还需要一个中间者来协调,从事件角度来说,这个中间者就是事件中心,协调发布者和订阅者直接的消息通信。

完成订阅发布整个流程需要三个角色:

  • 发布者
  • 事件中心
  • 订阅者

    订阅者是可以多个的。

以事件为例,简单流程如下:

发布者->事件中心<=>订阅者,订阅者需要向事件中心订阅指定的事件 -> 发布者向事件中心发布指定事件内容 -> 事件中心通知订阅者 -> 订阅者收到消息(可能是多个订阅者),到此完成了一次订阅发布的流程。

简单的代码实现如下:

class Event {
  constructor() {
    // 所有 eventType 监听器回调函数(数组)
    this.listeners = {}
  }
  /**
   * 订阅事件
   * @param {String} eventType 事件类型
   * @param {Function} listener 订阅后发布动作触发的回调函数,参数为发布的数据
   */
  on(eventType, listener) {
    if (!this.listeners[eventType]) {
      this.listeners[eventType] = []
    }
    this.listeners[eventType].push(listener)
  }
  /**
   * 发布事件
   * @param {String} eventType 事件类型
   * @param {Any} data 发布的内容
   */
  emit(eventType, data) {
    const callbacks = this.listeners[eventType]
    if (callbacks) {
      callbacks.forEach((c) => {
        c(data)
      })
    }
  }
}

const event = new Event()
event.on('open', (data) => {
  console.log(data)
})
event.emit('open', { open: true })

Event 可以理解为事件中心,提供了订阅和发布功能。

订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。

观察者模式

观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新。

观察者模式我们可能比较熟悉的场景就是响应式数据,如 Vue 的响应式、Mbox 的响应式。

观察者模式有完成整个流程需要两个角色:

  • 目标
  • 观察者

简单流程如下:

目标<=>观察者,观察者观察目标(监听目标)-> 目标发生变化-> 目标主动通知观察者(可能是多个)。

简单的代码实现如下:

/**
 * 观察监听一个对象成员的变化
 * @param {Object} obj 观察的对象
 * @param {String} targetVariable 观察的对象成员
 * @param {Function} callback 目标变化触发的回调
 */
function observer(obj, targetVariable, callback) {
  if (!obj.data) {
    obj.data = {}
  }
  Object.defineProperty(obj, targetVariable, {
    get() {
      return this.data[targetVariable]
    },
    set(val) {
      this.data[targetVariable] = val
      // 目标主动通知观察者
      callback && callback(val)
    },
  })
  if (obj.data[targetVariable]) {
    callback && callback(obj.data[targetVariable])
  }
}

可运行例子如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width,initial-scale=1,maximum-scale=1,viewport-fit=cover"
    />
    <title></title>
  </head>
  <body>
    <div id="app">
      <div id="dom-one"></div>
      <br />
      <div id="dom-two"></div>
      <br />
      <button id="btn">改变</button>
    </div>
    <script>
      /**
       * 观察监听一个对象成员的变化
       * @param {Object} obj 观察的对象
       * @param {String} targetVariable 观察的对象成员
       * @param {Function} callback 目标变化触发的回调
       */
      function observer(obj, targetVariable, callback) {
        if (!obj.data) {
          obj.data = {}
        }
        Object.defineProperty(obj, targetVariable, {
          get() {
            return this.data[targetVariable]
          },
          set(val) {
            this.data[targetVariable] = val
            // 目标主动通知观察者
            callback && callback(val)
          },
        })
        if (obj.data[targetVariable]) {
          callback && callback(obj.data[targetVariable])
        }
      }

      const obj = {
        data: { description: '原始值' },
      }

      observer(obj, 'description', value => {
        document.querySelector('#dom-one').innerHTML = value
        document.querySelector('#dom-two').innerHTML = value
      })

      btn.onclick = () => {
        obj.description = '改变了'
      }
    </script>
  </body>
</html>

装饰者模式

装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。

ES6/7 的decorator 语法提案,就是装饰者模式。

例子

class A {
  getContent() {
    return '第一行内容'
  }
  render() {
    document.body.innerHTML = this.getContent()
  }
}

function decoratorOne(cla) {
  const prevGetContent = cla.prototype.getContent
  cla.prototype.getContent = function() {
    return `
      第一行之前的内容
      <br/>
      ${prevGetContent()}
    `
  }
  return cla
}

function decoratorTwo(cla) {
  const prevGetContent = cla.prototype.getContent
  cla.prototype.getContent = function() {
    return `
      ${prevGetContent()}
      <br/>
      第二行内容
    `
  }
  return cla
}

const B = decoratorOne(A)
const C = decoratorTwo(B)
new C().render()

策略模式

在策略模式(Strategy Pattern)中,一个行为或其算法可以在运行时更改。

假设我们的绩效分为 A、B、C、D 这四个等级,四个等级的奖励是不一样的,一般我们的代码是这样实现:

/**
 * 获取年终奖
 * @param {String} performanceType 绩效类型,
 * @return {Object} 年终奖,包括奖金和奖品
 */
function getYearEndBonus(performanceType) {
  const yearEndBonus = {
    // 奖金
    bonus: '',
    // 奖品
    prize: '',
  }
  switch (performanceType) {
    case 'A': {
      yearEndBonus = {
        bonus: 50000,
        prize: 'mac pro',
      }
      break
    }
    case 'B': {
      yearEndBonus = {
        bonus: 40000,
        prize: 'mac air',
      }
      break
    }
    case 'C': {
      yearEndBonus = {
        bonus: 20000,
        prize: 'iphone xr',
      }
      break
    }
    case 'D': {
      yearEndBonus = {
        bonus: 5000,
        prize: 'ipad mini',
      }
      break
    }
  }
  return yearEndBonus
}

使用策略模式可以这样:

/**
 * 获取年终奖
 * @param {String} strategyFn 绩效策略函数
 * @return {Object} 年终奖,包括奖金和奖品
 */
function getYearEndBonus(strategyFn) {
  if (!strategyFn) {
    return {}
  }
  return strategyFn()
}

const bonusStrategy = {
  A() {
    return {
      bonus: 50000,
      prize: 'mac pro',
    }
  },
  B() {
    return {
      bonus: 40000,
      prize: 'mac air',
    }
  },
  C() {
    return {
      bonus: 20000,
      prize: 'iphone xr',
    }
  },
  D() {
    return {
      bonus: 10000,
      prize: 'ipad mini',
    }
  },
}

const performanceLevel = 'A'
getYearEndBonus(bonusStrategy[performanceLevel])

这里每个函数就是一个策略,修改一个其中一个策略,并不会影响其他的策略,都可以单独使用。当然这只是个简单的范例,只为了说明。

策略模式比较明显的特性就是可以减少 if 语句或者 switch 语句。

职责链模式

顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为型模式。

在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。

例子

function order(options) {
  return {
    next: (callback) => callback(options),
  }
}

function order500(options) {
  const { orderType, pay } = options
  if (orderType === 1 && pay === true) {
    console.log('500 元定金预购, 得到 100 元优惠券')
    return {
      next: () => {},
    }
  } else {
    return {
      next: (callback) => callback(options),
    }
  }
}

function order200(options) {
  const { orderType, pay } = options
  if (orderType === 2 && pay === true) {
    console.log('200 元定金预购, 得到 50 元优惠券')
    return {
      next: () => {},
    }
  } else {
    return {
      next: (callback) => callback(options),
    }
  }
}

function orderCommon(options) {
  const { orderType, stock } = options
  if (orderType === 3 && stock > 0) {
    console.log('普通购买, 无优惠券')
    return {}
  } else {
    console.log('库存不够, 无法购买')
  }
}

order({
  orderType: 3,
  pay: true,
  stock: 500,
})
  .next(order500)
  .next(order200)
  .next(orderCommon)
// 打印出 “普通购买, 无优惠券”

上面的代码,对 order 相关的进行了解耦,order500order200orderCommon 等都是可以单独调用的。

参考文章

查看原文

赞 14 收藏 12 评论 0

Samon 发布了文章 · 2019-07-03

Svelte 前端框架探索

Svelte 的作者也是 rollup 的作者 Rich Harris,前端界的轮子哥。sevlte 项目首次提交于 2016 年 11 月 16 日,目前版本是 3.6.1(2019-06-27),v3 版本进行了大改动,跟之前的版本有很大的差别(v1、v2 版本 API 用法跟 vue 很像,v3 完全属于自己的风格)。

看下 2016-12-02,尤雨溪大神对此框架的评价(当然已经过时了,但是核心的思想还是一致的):

这个框架的 API 设计是从 Ractive 那边传承过来的(自然跟 Vue 也非常像),但这不是重点。Svelte 的核心思想在于『通过静态编译减少框架运行时的代码量』。举例来说,当前的框架无论是 React Angular 还是 Vue,不管你怎么编译,使用的时候必然需要『引入』框架本身,也就是所谓的运行时 (runtime)。但是用 Svelte 就不一样,一个 Svelte 组件编译了以后,所有需要的运行时代码都包含在里面了,除了引入这个组件本身,你不需要再额外引入一个所谓的框架运行时!

什么是 Svelte?

Svelte 跟 vue 和 react一样,是一个数据驱动组件框架。但是也有很大的不同,它是一个运行时框架,无需引入框架本身,同时也没用到虚拟 DOM(运行时框架特性决定了这个框架跟虚拟 DOM 无缘)。

Svelte runs at build time, converting your components into highly efficient imperative code that surgically updates the DOM. As a result, you're able to write ambitious applications with excellent performance characteristics.

虽然没使用到虚拟 DOM,但一样可以达到出色的性能,而且对开发者编写代码是十分便捷。

与 React 和 Vue 有和不同?

那么我们先看下 svelte 的因为意思:苗条的。苗条的框架正是作者的初始目的,苗条包括代码编写量、打包大小等等。

总结一下这个框架的优势,即作者开发新框架的目的。

  • 静态编译,无需引入框架自身

    一个 Svelte 组件是静态编译,所有需要的运行时代码都包含在里面了,除了引入这个组件本身,你感觉不到框架存在。

  • 编写更少代码

    svelte 模板提供一些简便的用法,在维护和编写上都变得更简单,代码量更少(维护的代码),这些模板会编译为最终的js 代码。

  • 只会打包使用到的代码

    即 tree shaking,这个概念本来也是作者首先提出来的,webpack 是参考了 rollup。

  • 无需虚拟 DOM 也可进行响应式数据驱动
  • 更便捷的响应式绑定

    既有响应式数据的优点,v3 版本也解决了 vue 数据绑定缺点,用起来十分方便。

简单用法对比

react hook

import React, { useState } from 'react';
export default () => {
  const [a, setA] = useState(1);
  const [b, setB] = useState(2);
  function handleChangeA(event) {
    setA(+event.target.value);
  }
  function handleChangeB(event) {
    setB(+event.target.value);
  }
  return (
    <div>
      <input type="number" value={a} onChange={handleChangeA}/>
      <input type="number" value={b} onChange={handleChangeB}/>
      <p>{a} + {b} = {a + b}</p>
    </div>
  );
};

vue

<template>
  <div>
    <input type="number" v-model.number="a">
    <input type="number" v-model.number="b">
    <p>{{a}} + {{b}} = {{a + b}}</p>
  </div>
</template>

<script>
  export default {
    data: function() {
      return {
        a: 1,
        b: 2
      };
    }
  };
</script>

svelte

<script>
  let a = 1;
  let b = 2;
</script>

<input type="number" bind:value={a} />
<input type="number" bind:value={b} />
<p>{a} + {b} = {a + b}</p>

都不用多说,一眼就看出来,svelte 简单多了。

为什么不使用虚拟 DOM?

在 react 和 vue 盛行的时代,你会听说虚拟 DOM 速度快,而且还可能被灌输一个概念,虚拟 DOM 的速度比真实 DOM 的速度要快。

所以如果你有这个想法,那么你肯定疑惑 svelte 没用到虚拟 DOM,它的速度为什么会快?

其实虚拟 DOM 并不是什么时候都快,看下粗糙的对比例子。

对比例子

这里并没有直接统计渲染的时间,通过很多条数据我们就可以感受出来他们直接的性能。特别是点击每条数据的时候,明显感觉出来(由于是在线上的例子,所以首次渲染速度不准确,主要看点击的响应速度)。

当然这仅仅是在 50000 条数据下的测试,对比一下框架所谓的速度,实际的情况下我们是不会一次性展示这么多数据的。所以在性能还可行的情况下,更多的选择是框架所带来的的便利,包括上手难度、维护难度、社区大小等等条件。

svelte

https://svelte.dev/repl/367a2...

<script>
  import { onMount } from "svelte";

  const  = [];
  for (let i = 0; i < 50000; i++) {
    list.push(i);
  }
  const beginTime = +new Date();
  let name = "world";
  let data = list;
  function click(index) {
    return () => {
      data[index] = "test";
    };
  }
  onMount(() => {
    const endTime = +new Date();
    console.log((endTime - beginTime) / 1000, 1);
  });
</script>

{#each data as d, i}
  <h1 on:click={click(i)}>
    <span>
      <span>
        <span>
          <span>Hello {name} {i} {d}!</span>
        </span>
      </span>
    </span>
  </h1>
{/each}

vue

http://jsrun.net/kFyKp/edit

<div id="component-demo" class="demo">
  <div v-for="(d, i) in list" @click="click(i)"> 
    <span>
      <span> 
        <span>
          <span>
            Hello {{name}} {{i}} {{d}}!
          </span>
        </span>
      </span>
    </span>
  </div>
</div>
const list  = []
for(let i = 0; i < 50000; i++) {
  list.push(i);
}
const beginTime = +new Date();
new Vue({
  el: '#component-demo',
  data: {
    list: list,
    name: 'Hello'
  },
  methods:{
    click(index){
      const list = new Array(50000);
      list[index] = 'test'
      this.list = list
    }
  },
  mounted(){
    const endTime = +new Date();
      console.log((endTime - beginTime) / 1000,1);
  }
})

react

http://jsrun.net/TFyKp/edit

const list  = []
for(let i = 0; i < 50000; i++) {
  list.push(i);
}
class App extends React.Component {
  constructor(props){
    super(props);
    this.state = {
      list
    } 
  }
  click(i) {
    return ()=>{
      list[i] = 'test'
      this.setState({
        list,
      })
    }
  }
  render() {
    return (
      <div>
        {this.state.list.map((v,k)=>{
          return(
            <h1 onClick={this.click(k)}>
              <span>
                <span>
                  <span>
                    <span>
                      Hello wolrd {k} {v}!
                    </span>
                  </span>
                </span>
              </span>
            </h1>
          )
        })}
      </div>
    )
  }
}

function render() {
  ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
}

render();

总结

首先虚拟 DOM 不是一个功能,它只是实现数据驱动的开发的手段,没有虚拟 DOM 我们也可以实现数据驱动的开发方式,svelte 正是做了这个事情。

单纯从上面的对比例子来看,svelte 的速度比虚拟 DOM 更快(不同框架虚拟 DOM 实现会有差别)。虽然没有进行更深层次的对比,但是如果认为虚拟 DOM 速度快的观点是不完全对的,应该说虚拟 DOM 可以构建大部分速度还可以的 Web 应用。

Svelte 有哪些好用的特性?

  • 完全兼容原生 html 用法

    编写代码是那么的自然,如下面就是一个组件。

    <script>
      const content = 'test';
    </script>
    <div>
      { test }
    </div>
  • 响应式也是那么的自然

    <script>
        let count = 0;
        function handleClick () {
            // calling this function will trigger an
            // update if the markup references `count`
            count = count + 1;
        }
    </script>
    <button on:click="handleClick">+1</button>
    <div>{ count }</div>
  • 表达式也可以是响应式的

    这个就牛逼了,更加的自然,这种特性只有静态编译才能做到,这个就是 svelte 目前独有的优势。

    <script>
        let numbers = [1, 2, 3, 4];
        function addNumber() {
            numbers.push(numbers.length + 1);
        }
        $: sum = numbers.reduce((t, n) => t + n, 0);
    </script>
    <p>{numbers.join(' + ')} = {sum}</p>
    <button on:click={addNumber}>Add a number</button>
  • 自动订阅的 svelte store

    这个其实就是订阅发布模式,不过 svelte 提供了自身特有的便捷的绑定方式(自动订阅),用起来是那么的自然,那么的爽。

    这种特性只有静态编译才能做到,这个就是 svelte 目前独有的优势。

    stores.js

    import { writable } from 'svelte/store';
    export const count = writable(0);

    A.svelte

    <script>
        import { count } from './stores.js';
    </script>
    <h1>The count is {$count}</h1>

    B.svelte

    <script>
        import { count } from './stores.js';
      function increment() {
            $count += 1;
        }
    </script>
    <button on:click={increment}>增加</button>
  • 所有组件都可以单独使用

    可以直接在 react、vue、angular 等框架中使用。

    // SvelteComponent.js 是已经编译后的组件
    import SvelteComponent from './SvelteComponent';
    
    const app = new SvelteComponent({
        target: document.body,
        props: {
            answer: 42
        }
    });

Svelte 有什么缺点?

svelte 是一个刚起步不久的前端框架,无论在维护人员还是社区上都是大大不如三大框架,这里列举一下本人认为的 svelte 存在的缺点。

  • props 是可变的

    当然这也是这个框架故意这样设计的,这样 props 也是可以响应式的。

    <script>
      export let title;
      title = '前缀' + title
    </script>
    <h1>
      { title }
    </h1>
  • props 目前无法验证类型

    <script>
      export let propOne;
      export let propTwo = 'defaultValue';
    </script>
  • 无法通过自定义组件本身直接访问原生 DOM

    需要利用 props 的双向绑定特性,这就可能导致深层次组件的需要层层传递 DOM 对象(是子父传递,不是父子传递)。

    App.svelte

    <script>
      export let customDOM;
    </script>
    <A bind:dom={customDOM} />
    <!--bind:this 是无用的,只有再原生才有用-->

    A.svelte

    <script>
      export let dom;
    </script>
    
    <div bind:this={dom}>
      test
    </div>
  • 只有组件才支持 svelte 的静态模板特性

    js 文件是不支持 sevelte 静态模板特性的,像下面这样是会报错的。

    import { count } from './stores.js';
    function increment() {
      $count += 1;
    }
  • 组件不支持 ts 用法

    找了一下,没找到可以支持 ts 的解决方案,如果有解决方案可以评论下。


学习和总结文章同步发布于 https://github.com/xianshanna...,有兴趣可以关注一下,一起学习和进步。
查看原文

赞 10 收藏 5 评论 10

认证与成就

  • 获得 298 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-08-26
个人主页被 1.3k 人浏览