GithubHelp home page GithubHelp logo

careteenl / blog Goto Github PK

View Code? Open in Web Editor NEW
65.0 65.0 6.0 238.13 MB

✍️无他术,唯勤读书而多为之,自工。

Home Page: https://careteenl.github.io/blog

License: MIT License

JavaScript 70.93% HTML 21.27% Shell 0.58% CSS 6.20% TypeScript 1.02%
blog css data-structures-algorithms dva fe html http-server interview javascript koa nodejs react ui-components vue webpack

blog's Introduction

🐌First Imitate, 🚀Then Create.

visitor

blog's People

Contributors

careteenl avatar dependabot[bot] avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

聊聊编码那些事,顺带实现base64

目录

前言

日常工作中,频繁的使用base64取代小图标,以便减少HTTP请求进而达到性能优化的目的。基于此来聊聊编码的发展、为什么需要base64以及如何实现base64。此文章首发于聊聊编码那些事,顺带实现base64转载请注明来源。

进制间的转换

对任意进制的数进行任意进制转换

Number.prototype.toString(radix)

将任意进制数转换为十进制数

parseInt(string, radix)

几道关于parseInt的面试题

说到parseInt,不得不提到一个很有意思的面试题

// 会输出什么?
[1, 2, 3].map(parseInt)
// => 1, NaN, NaN

map方法第一个参数为函数,函数有三个参数,array.map((item, index, array) => { ... })

实际上相当于

function fn (item, index) {
  return parseInt(item, index)
}
[1, 2, 3].map(fn)
// parseInt迭代过程相当于如下
// parseInt(1, 0) => 1
// parseInt(2, 1) => NaN
// parseInt(3, 2) => NaN

再来看一个类似的面试题

// 会输出什么?
'1 2 3'.replace(/\d/g, parseInt)
// => 1, NaN, 3

replace方法第二个参数若是一个函数,函数会有若干个参数。第一个为匹配模式的字符串;第二个为与模式中子表达式匹配的字符串,可以有零个或多个这样的参数。

实际上相当于如下

function fn (...args) {
  // 只会取前两个参数
  return parseInt(args[0], args[1])
}
'1 2 3'.replace(/\d/g, fn)
// parseInt迭代过程相当于如下
// parseInt('1', 0) => 1
// parseInt('2', 2) => NaN
// parseInt('3', 4) => 3

其实在mdn中对parseInt/map/replace已经讲解的很详细,期望大家在工作之余不要太过浮躁,别做伸手党,静下心来啃一下文档并多做实践,很多面试题自然会迎刃而解。

编码发展历史

ASCII

GBK2312

GBK

GB18030/DBCS

Unicode

UTF-8

现在的标准,有如下特点

  • UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式
  • UTF-8就是每次以8个位为单位传输数据
  • 而UTF-16就是每次 16 个位
  • UTF-8 最大的一个特点,就是它是一种变长的编码方式
  • Unicode 一个中文字符占 2 个字节,而 UTF-8 一个中文字符占 3 个字节
  • UTF-8 是 Unicode 的实现方式之一

base64编码

为什么需要base64

在开发时,经常会有一些小图标图片,每一个图片都会有一次HTTP请求,由于浏览器对同一个域名的并发数量有限制,所以我们应该尽可能减少HTTP请求个数。

本文主要讲解编码相关,那就只讲解从编码入手如何去减少HTTP请求。

在计算机内部,任何信息最终都是使用一系列二进制存储,图片也不例外。

而且在img标签的src属性后跟上一个base64字符,如果该字符有效,那么会正常显示图片。

如何实现base64

以下涉及的所有代码均在仓库中,感兴趣的可以自取。

读取buffer转为json对象

首先准备一个2.txt文件。

冯兰兰啊我说今晚月色这么美,你说是的。

case.js代码

const fs = require('mz/fs')
const path = require('path')

// 读取成buffer对象
async function read2JSON () {
   let ret = await fs.readFile(path.resolve(__dirname, '2.txt'))
   console.log(ret.toJSON())
   return ret.toJSON()
}
read2JSON()
// => { type: 'Buffer', data: [ 229, 134, 175, 229... ] }

上面的依赖mz/fs已经将fs都包装成promise,所以我们能写的更像同步。

readFile函数如果第二个参数没有指定会读取成一个buffer流,是由一个个16进制数组合在一起的。

buffer.toJSON可以将一个buffer流转为一个json对象,十六进制也会被转十进制。如上输出所示。

将10进制转为2进制

十进制转为二进制可以通过Number.toString(2)方法

// 将10进制转为2进制
async function data2b () {
  let data = await read2JSON()
  let ret = []
  data.data.forEach(item => {
    ret.push(item.toString(2))
  })
  console.log(ret)
  return ret
}
data2b()
// => [ '11100101', '10000110', '10101111', '11100101'...]

将2进制拼一起3*8然后分隔成4*6

一个汉字在UTF-8规范中由三个字节组成,一个字节由8个二进制物理位构成。所以一个汉字实际占用内存3*8base64中我们实际需要6个物理位表示一个字节即2**6,所以做重新分割4*6

async function split () {
  let data = await data2b()
  let dataStr = data.join('')
  let ret = []

  let splitUnit = 6
  let flag = 0
  while (flag < dataStr.length) {
    ret.push(dataStr.substr(flag, splitUnit))
    flag = flag + splitUnit
  }
  console.log(ret)
  return ret
}
split()
// => [ '111001', '011000', '011010', '101111'...]

然后将2进制转成10进制

二进制转为十进制可以通过parseInt(string, 2)方法

async function data20 () {
  let data = await split()
  let ret = data.map(item => {
    return parseInt(item, 2)
  })
  console.log(ret)
  return ret
}
data20()
// => [ 57, 24, 26, 47, 57, 24, 22, 48, 57, 24, 22, 48 ]

base64码

base64中的64实际上是根据2**6所来,表示则由大写字母、小写字母、数字、+/构成。

const lowerCases = 'abcdefghijklmnopqrstuvwxyz'
const numbers = '0123456789'
const base64lib = `${lowerCases.toUpperCase()}${lowerCases}${numbers}+/`
console.log(base64lib)
// => ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

取到每一个base64码

然后我们则可以取到每一个base64

async function main () {
  let data = await data20()
  let ret = []
  data.forEach(item => {
    ret.push(base64lib[item])
  })
  console.log(ret.join(''))
  return ret.join()
}

main()
// => 5Yav5YWw5YWw5ZWK5oiR6K+05LuK5pma5pyI6Imy6L+Z5LmI576O77yM5L2g6K+05piv55qE44CC

我们可以前往base64在线转码解码进行验证。

encoding-base64-decode

简化代码

对以上思路进行代码简化

const CHARTS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
function transfer (str) {
  let buf = Buffer.from(str)
  let result = ''
  for(let b of buf){
    result += b.toString(2)
  }
  return result.match(/(\d{6})/g).map(val => parseInt(val, 2)).map(val => CHARTS[val]).join('')
}
let fl = transfer('冯')
console.log(fl) // => 5Yav

小结

如上我们可以实现将中文转为base64,同理我们也可以转换图片。

async function read2JSON () {
  // let ret = await fs.readFile(path.resolve(__dirname, '2.txt'))
  // 读取图片
  let ret = await fs.readFile(path.resolve(__dirname, '../assets/encoding-base64-example.png'))
  console.log(ret.toJSON())
  return ret.toJSON()
}

特别的: 由于将2进制拼一起3*8然后分隔成4*6,原来存储一个汉字需要三个字节,现在需要四个字节存储,所以转换为base64后会比之前大3/1

下面笑脸图片则是由img的src属性展示的(github似乎将base64过滤了,并没有展示),不过本文并没有实现图片转base64,因为其逻辑较为复杂,但是本文讲解了大致思路,感兴趣的可再做深究。

encoding-base64-example

encoding-base64-code

2020疫情期间求职的挑战

来自汤君健老师的建议

我们都知道,求职向来不是件容易的事。而疫情期间找工作、换工作,更是有不少新挑战。比如远程面试,怎么能发挥出优势?经济形势不理想,加入小公司会不会有风险?

挑战1

今年很多公司都转成线上面试了,可是我隔着屏幕和面试官谈话,找不到和一个真人谈话的那种默契,也没法隔空“解读”对方的肢体语言。这种情况下怎么调整状态,让自己临场发挥得更好呢?

关于远程面试,我想重点给你分享的,是我们得到老师在做直播时候的一个秘密。

不知道你有没有看过我在得到上的直播,对着屏幕,吐沫星子横飞讲得特别陶醉。

其实,背后的秘密是,我并不是临场发挥想到什么说什么,其实在正式直播之前,我至少已经彩排过不下5次。

而且在直播的时候,我也不是真的对着屏幕摄像头在那里说,其实我是有准备一份逐字稿放在边上提醒自己的。

其实对于校园招聘,面试官问来问去也就那么10来个常用问题,比如,你的自我介绍,你在大学期间做过最有成就感的一件事,为什么选择我们公司等等。

我建议你,静下心来花一整天的时间,把所有这些常见问题的答案都写下来,下次视频面试的时候,把这个逐字稿摆在电脑摄像头下方。面试官又不知道,你是照着念还是看着摄像头在说。

你可能觉得,我是在教你打小抄,但其实并不是这样。如果我是你的面试官,即使我发现你来面试的时候,提前做了很多准备,甚至带着这样一份准备好的稿子,我也会觉得你是做了功课的,反而会有加分。如果你是应聘一些外向型岗位,例如销售、商务拓展、营销等等,甚至你还可以像直播卖货一样,做一块白板,把你的一些成功时刻照片、案例关键字等等提前写上去。

然后就是反复练习,把练习的答案录下来。等你练到第5遍,第6遍,甚至第10遍的时候,基本上你就可以瞟一眼逐字稿里的关键字,就能做出回答。

挑战2

我最怕面试官问我,你未来5年到10年有什么计划?我刚毕业,根本想象不出来,在一家公司我会不会做5年或者10年。坦白说,我还是希望两到三年就换一次工作,因为我想到不同公司甚至不同行业闯闯。我该怎么回答这类问题呢?

如果面试官想了解,你的学习能力如何,他不会直接问你,同学你的学习能力好吗?你肯定会回答好,对不对?就像你去问卖西瓜的西瓜甜不甜。这种问题就问得太明显了。

在问你对未来510年有什么规划的时候,其实他并不是真的在乎你未来510年有什么样的规划。因为这是一个伪命题,他是没有办法去验证的。

通常来说,面试官问这个问题,他更想通过这个问题来判断以下三个方面:

  • 第一,你是一个做事情有计划性、目的性、以终为始的人,还是一个“走一步算一步”,工作没有计划,生活没有安排的人?
  • 第二,你是不是一个有进取心的人,希望自己不断进步?
  • 第三,你对所要从事的职业和行业,有没有进行过一定的了解?做过功课?能不能说出你的洞察?
    所以,我建议你,回答问题的立足点,回到你应聘的职业或者行业中,谈谈你对这个职业,或者所在行业的理解。

比如,如果你是应聘人力资源专员这个岗位,面试官如果能听到你为什么选择人力资源作为自己第1份工作的开始?5~10年之后,你希望成为一个怎样的人力资源员工?在这个成长的过程中,你希望应聘的这个公司,能够为你提供怎样的支持和帮助?等等,对你的面试过程,会加分不少。

如果是十五年前的汤老师遇到这个问题,我会这么来回答:在大学担任学生会勤工俭学部长的两年时间里,我逐渐发现,自己在组织团队、调动部员积极性、培训部员这些工作上,有浓厚的兴趣,而且取得了一定的成绩;而我自己也特别愿意成为一个“帮助他人成功”的人。

所以,我大学四年时间,刻意找了两份人力资源相关的实习工作。在实习的过程中,我意识到,人力资源的实际工作远比书本上说的要复杂和多样。我希望自己能够在5~10年的时间里,把人力资源的六个模块都历练一遍,成为一名人力资源专家。当然,仅凭自己的兴趣爱好,是很难有所作为的,我希望加入一家成熟的公司,帮助自己快速成长。

在和我的一位师兄交流的时候,他向我推荐了贵公司的人力资源专员这个岗位;他去年通过校园招聘成为贵公司的一名培训生。从他口中我了解到,贵司秉承“人才是公司资产”的价值观,重视人力资源建设,并且有成体系的人力资源人才培养方案。所以,我也非常期待能够在贵司的帮助下,实现我人力资源专家的发展目标。

你看,这种回答既能够体现出你对自己有明确的规划,同时也能展现出你是一个有进取心的候选人,最后,无形中也流露出了你对目标公司还有这个职业的熟悉,你是做了功课来的。

挑战3

老师,我好不容易找到了一份工作,可是因为疫情限制没办法现场入职,很长一段时间,我都是在家里和新同事们通过微信、视频沟通。在这样的情况下,我有什么方法可以融入到整个公司的大环境里呢?
首先,你得接受一个事实,那就是未来越来越多的工作会转移到线上。

在线上,处理和同事的关系,进行分工合作,将不可避免地成为未来工作的主流。所以你在职业一开始就能提前接受到这样的历练,其实是非常难得的。

好,回到问题本身。第一步,我会建议你寻求你的老板的帮助,请他列出和你工作最紧密的10位同事的名单。

然后,由他把你介绍给这10位同事,请你老板向他们发出一个邀请,哪位同事半个小时到一个小时的时间,给你做一些工作内容方面的介绍。

第二步,一开始和这些同事挨个地一对一谈话。

一方面,请他们向你介绍工作上和他们合作的流程,另一方面,你可以通过问一些有针对性的问题,让他们看出你对公司的融入。记住,千万不要问那些大而空的问题,比如咱们公司的文化是怎么样的啊?你在公司工作得开不开心啊?等等。

我给你几个我觉得特别有帮助的问题吧:

  • 在我之前这个位置的同事,你会如何评价他?你觉得他做得好的地方有哪些?还可以提升的地方有什么?
  • 我在这个位置上,有可能在哪些方面会摔大跟头?你能不能提前帮我指出来?
  • 如果我们合作了,一年之后,你评价我的工作做得非常好,可能会是因为什么原因?同样如果我做得不好,又会是因为什么?
  • 你在刚加入公司的时候,有没有不适应的阶段?当时你是怎么过来的?还有谁给了你帮助?
  • 我现在想快速地提升,你有什么建议可以给到我吗?

好,和大家聊完之后,就到第三步了。我建议你从这10位同事中,挑选12位,你聊下来最投缘的。邀请他们做你的小师傅,也就是很多外企常说的buddy。每周约他1020分钟通个电话,聊聊你的近况。
当然在这个过程中,还有一个重要的角色,就是你的老板。建议你跟你的老板,保持每天20分钟的沟通时间。你主动向他汇报一下你的工作,探讨一下明天的工作计划。

新员工特别容易犯的一个错误,就是害怕打搅老板,总以为向老板提问会显得自己很笨。这么想是大错特错的,对于你的直接经理而言,他更在乎你能不能快速成长融入公司。

所以不要害怕打搅他,主动去约他的时间,向他汇报你的成长。

这里,我非常推荐新员工写成长周报。

你可以尝试写一下,你这一周学到了哪些东西?有哪些事情做得好?有哪些事情搞砸了?如果重给你一次机会的话,你会怎样去做?然后,请老板看看,并给出建议。

挑战4

汤老师,我本来毕业计划是想要去小公司、创业公司工作,不过在现在的经济环境下,我又担心小企业不稳定。保险起见,今年春招是不是应该优先选择大企业呢?

我的建议是,如果有可能,尽量在刚毕业的时候,能够加入大公司,如果难度比较大,那么至少也应该是小公司的核心部门。

  • 首先,我们都知道,船小好调头,船大抗风浪。显然,大公司的优点就是在于它的抗风险能力更强。
    而且对于刚刚走出校门的你来说,校园招聘也是非常重要的给你一个“好出身”的机会。因为小公司你随时可以加入,而很多大企业基本上只进行校园招聘,然后人才自己培养。

  • 第二,一个大公司对你简历的加持作用是非常大的。等你将来要跳槽的时候,人力资源是很难有耐心去了解你工作过的那些小公司的,他更多是依靠看到耳熟能详的大名字进行判断。

  • 第三,在大公司里可以学到很多标准化、规范化的做法,要比你自己在小公司瞎摸索,高效得多。就拿销售这份工作来说,在宝洁早都已经总结出了一套完整的客户管理流程,从第一次拜访的话术,到谈判技巧,公司都会安排专门的培训,可以帮你少走很多弯路。

当然这并不代表说大公司就一定比小公司好。很多行业隐性冠军的企业,规模不大,但是有非常强的竞争力和“江湖地位”,同样也是很好的应聘目标。

不过,如果要加入小企业的话,我也建议尽量加入核心部门,以及不花钱的公司赚钱的部门。因为当公司要进行业务调整甚至裁员的时候,往往就会先从边缘部门和花钱的部门开始。

比如,同样是市场部的策划岗,一家生产制造型企业,显然不会把它作为核心部门,它的核心岗位是质量管理,而且这个岗位是在替公司“花钱”,往往就是第一波调整的对象;而对于一家4A公司来说,策划岗就是他们的核心,再怎么缩减成本,也不会拿这个岗位开刀。而且,你作为刚出校门的学生,工资比较低,公司也更愿意在这个时候培养你。

异步发展流程-手摸手带你实现一个Promise

异步发展流程-手摸手带你实现一个promise

篇幅较长,但重点为以下几点,可直接前往感兴趣的话题,各取所需。

所有涉及的例子均有完整代码存放在仓库,感兴趣的同学可直接clone在本地运行。

本文主要简单探讨下异步的前世今生,并手摸手带你实现一个promise

由于JavaScript单线程的特性,我们需要异步编程解决阻塞问题。

异步编程问题

我们每天的工作中都可能会用到以下函数做一些异步操作

  • setTimeout
  • onClick
  • ajax

如何解决异步问题

解决异步问题现有的方式如下

  • 回调函数
  • promise
  • generator 恶心
  • aync+await

下面将逐一介绍各种方式如何解决异步问题

回调函数

首先介绍一下高阶函数,即一个函数的参数是函数或者函数返回值为函数,此函数称做高阶函数。

lodash-after函数

再来看一个例子,常使用lodash的同学应该熟悉的一个方法_.after(n, fn),作用是fn函数在调用n次以后才会执行。

let fn = after(3, () => {
  console.log('该执行了')
})
fn()
fn()
fn() // => 该执行了

那如何实现一个after函数呢,其实主要是利用闭包和计数的**:

const after = (times = 1, cb = _defaultCb) => {
  return function () {
    if (--times === 0) {
      cb()
    }
  }
}

const _defaultCb = () => {}

其中cb作为函数参数传入after函数,即是高阶函数的一个应用。

after函数例子地址

⬆️回到顶部

Node读取文件

现在有一个场景,读取两个文件内容,赋值给一个对象,并打印。

./static下新建了两个文件name.txt,age.txt,期望读取文件内容并赋值给一个对象,然后打印。

const fs = require('fs')

let schoolInfo = {}

fs.readFile('./static/name.txt', 'utf8', (err,data) => {
  schoolInfo['name'] = data
})
fs.readFile('./static/age.txt', 'utf8', (err, data) => {
  schoolInfo['age'] = data
})
console.log(schoolInfo) // {}

由于读取文件的过程是异步的,所以通过这种方式是无法满足预期的。

并且异步操作存在以下三个问题

  • 1、异步没法捕获错误
  • 2、异步编程中,可能存在回调地狱
  • 3、多个异步操作,在同一时间内,如何同步异步的结果?

回调地狱大家应该非常熟悉了。

const fs = require('fs')

let schoolInfo = {}
fs.readFile('./static/name.txt', 'utf8', (err,data) => {
  schoolInfo['name'] = data
  fs.readFile('./static/age.txt', 'utf8', (err, data) => {
    schoolInfo['age'] = data
  })  
})

本例地址

并且两个文件读取时间是累加,不是并行的,如果文件很多并且很大,那等待时间将非常久,所以并不推荐。

这里针对第三个问题多个异步操作,在同一时间内,如何同步异步的结果?,可以采用发布订阅的方式解决

// 一个简易的方法订阅对象
let dep = {
  arr: [],
  emit () {
    this.arr.forEach((fn) => {
      fn()
    })
  },
  on (fn) {
    this.arr.push(fn)
  }
}

不了解发布订阅模式的请移步我的另一篇博客

通过以下操作即可达到预期

let schoolInfo = {}
const fs = require('fs')

// 一个简易的方法订阅对象
let dep = {
  arr: [],
  emit () {
    this.arr.forEach((fn) => {
      fn()
    })
  },
  on (fn) {
    this.arr.push(fn)
  }
}

// 订阅
dep.on(() => {
  // ��只有读取了两个文件的内容并赋值以后才会打印
  if (Object.keys(schoolInfo).length === 2){
    console.log(schoolInfo)
  }
})

// 读取触发
fs.readFile('./static/name.txt', 'utf8', (err, data) => {
  schoolInfo['name'] = data
  dep.emit()
})
fs.readFile('./static/age.txt', 'utf8', (err, data) => {
  schoolInfo['age'] = data
  dep.emit()
})

在每次读取文件时触发打印事件,事件中进行判断只有两次读取都完成的情况下才会打印。

以上方法看似解决了上面提到的第三个问题多个异步操作,在同一时间内,同步异步的结果,但是随着需求的变动,需要再读取一个address文件,就需作如下变动:

...
// 订阅
dep.on(() => {
  // ��只有读取了两个文件的内容并赋值以后才会打印
  if (Object.keys(schoolInfo).length === 3){ // 2改为3
    console.log(schoolInfo)
  }
})
...
// 新增一项adress
fs.readFile('./static/adress.txt', 'utf8', (err, data) => {
  schoolInfo['adress'] = data 
  dep.emit()
})

再新增多项的话,代码的扩展性就非常差了。

下面将将介绍如何实现一个promise然后解决上面提到的问题

node读取文件代码地址

⬆️回到顶部

为什么要用promise

那么接下来介绍promise的出现所解决的问题

  • 回调地狱,如果多个异步请求,有连带关系,回调嵌套
  • 多个异步实现并发的话,会出现无法同步异步的返回结果
  • 错误处理不方便

promise用法

  • 不跟你多BB

手摸手带你撸一个promise

首先需要提到promise/A+规范,我们自己编写的promise是需要一个标准的。可以根据此标准一步一步来。

需要三个状态

const PENDING = 'pending' // 等待态
const FULFILLED = 'fulfilled' // 成功态
const REJECTED = 'rejected' // 失败态
  • 当状态为pending
    • 可能转换为fulfilledrejected
  • 当状态为fulfilledrejected
    • 不能转为其他状态
    • 必须有一个valuereason且不能改变

面试点:promise的三个状态之间的关系?

⬆️回到顶部

then方法

更详细请移步文档,这里说几个重点

  • 处理executor函数中代码异常的情况
  • 处理executor函数中代码为异步的情况
  • 处理then的多次调用
  • 处理then的链式调用

处理executor函数中代码异常的情况

executortry-catch即可

class Promise {
  constructor(executor) {
    let self = this
    self.status = PENDING
    self.value = undefined
    self.reason = undefined    

    const resolve = (value) => {
      if(self.status === PENDING){
        self.value = value
        self.status = FULFILLED
      }      
    }

    const reject = (reason) => {
      if(self.status === PENDING){
        self.reason = reason
        self.status = REJECTED
      }      
    }

    try{
      executor(resolve, reject) // 如果执行这个executor执行时候抛出异常 应该走下一个then的失败
    }catch(e){
      reject(e)// 出错了 reason就是错误
    }    
  }

  then (onFulfilled, onRejected) {
    if (this.status === FULFILLED){
      onFulfilled(this.value)
    }
    if (this.status === REJECTED){
      onRejected(this.reason)
    }    
  }
}

如下使用

let Promise = require('./3.1.promise.js')

let p = new Promise((resolve,reject) => {
  resolve('xx')
})

p.then((data) => {
  console.log('p success',data)
}, (err) => {
  console.log(err)
})
// => p success xx

简易版1.0.0地址以及测试用例地址

虽然实现了一个很简易的promise,但还存在很多问题,比如下面

let Promise = require('./3.1.promise.js')
let p2 = new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve('xxx')
  }, 1000)
})

p2.then((data) => {
  console.log('p2 success',data)
}, (err) => {
  console.log(err)
})
// => p success xx
//    p second success xx

对于异步的代码是不会处理的

⬆️回到顶部

处理executor函数中代码为异步的情况

使用发布订阅模式的**处理

class Promise {
  constructor(executor) {
    let self = this
    self.status = PENDING
    self.value = undefined
    self.reason = undefined
    self.onResolvedCallbacks = [] // 新增 一个数组存放成功处理
    self.onRejectedCallbacks = [] // 新增 一个数组存放失败处理

    const resolve = (value) => {
      if(self.status === PENDING){
        self.value = value
        self.status = FULFILLED
        self.onResolvedCallbacks.forEach((fn) => { // 新增 触发时遍历所有
          fn()
        })     
      }      
    }

    const reject = (reason) => {
      if(self.status === PENDING){
        self.reason = reason
        self.status = REJECTED
        self.onRejectedCallbacks.forEach((fn) => { // 新增 触发时遍历所有
          fn()
        })        
      }      
    }

    try{
      executor(resolve, reject) // 如果执行这个executor执行时候抛出异常 应该走下一个then的失败
    }catch(e){
      reject(e)// 出错了 reason就是错误
    }    
  }

  then (onFulfilled, onRejected) {
    if (this.status === FULFILLED){
      onFulfilled(this.value)
    }
    if (this.status === REJECTED){
      onRejected(this.reason)
    }    
    if( this.status === PENDING){ // 新增 处理异步
      // 默认当前 new Promise  executor中是有异步的
      this.onResolvedCallbacks.push(() => {
        onFulfilled(this.value)
      });
      this.onRejectedCallbacks.push(() => {
        onRejected(this.reason)
      })
    }    
  }
}

使用

let Promise = require('./3.2.promise.js')

let p = new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve('xxx')
  }, 1000)
})

p.then((data) => {
  console.log('p success',data)
}, (err) => {
  console.log(err)
})

p.then((data) => {
  console.log('p second success',data)
}, (err) => {
  console.log(err)
})
// 一秒以后打印
// => p success xxx
//    p second success xxx

简易版1.0.1地址以及测试用例地址

⬆️回到顶部

处理then的链式调用

jQuery的链式调用一个套路,不过在这儿需要返回一个新的promise而不是当前,因为成功态和失败态是不能转为其他状态的

class Promise {
  constructor(executor) {
    let self = this
    self.status = PENDING
    self.value = undefined
    self.reason = undefined
    self.onResolvedCallbacks = []
    self.onRejectedCallbacks = []  

    const resolve = (value) => {
      if(self.status === PENDING){
        self.value = value
        self.status = FULFILLED
        self.onResolvedCallbacks.forEach((fn) => {
          fn()
        })     
      }      
    }

    const reject = (reason) => {
      if(self.status === PENDING){
        self.reason = reason
        self.status = REJECTED
        self.onRejectedCallbacks.forEach((fn) => {
          fn()
        })        
      }      
    }

    try{
      executor(resolve, reject) // 如果执行这个executor执行时候抛出异常 应该走下一个then的失败
    }catch(e){
      reject(e)// 出错了 reason就是错误
    }    
  }

  then (onFulfilled, onRejected) {
    let self = this
    let promise2 // 这个promise2 就是我们每次调用then后返回的新的promise
    // 实现链式调用主要的靠的就是这个promise
    promise2 = new Promise((resolve, reject) => {
      if (self.status === FULFILLED) {
        setTimeout(() => {
          try {
            // 这个返回值是成功函数的执行结果
            let x = onFulfilled(self.value)
            // 判断promise2 和 x 也是then函数返回的结果和promise2的关系 如果x 是普通值 那就让promise2成功 如果 是一个失败的promise那就让promise2 失败
            self._resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }

      if (self.status === REJECTED) {
        setTimeout(() => {
          try {
            // 这个返回值是失败函数的执行结果
            let x = onRejected(self.reason)
            // 判断promise2 和 x 也是then函数返回的结果和promise2的关系 如果x 是普通值 那就让promise2成功 如果 是一个失败的promise那就让promise2 失败
            self._resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }   
      
      if (self.status === PENDING) {
        // 默认当前 new Promise  executor中是有异步的
        self.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(self.value)
              self._resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)          
        });
        self.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(self.reason)
              self._resolvePromise(promise2, x, resolve, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)          
        })
      }       
    })
    return promise2
  }

  // 内部核心方法 处理 成功或者失败执行的返回值 和promise2的关系
  _resolvePromise (promise2, x, resolve, reject) {
    // 这个处理函数 需要处理的逻辑韩式很复杂的
    // 有可能这个x 是一个promise  但是这个promise并不是我自己的
    resolve(x) // 目前只做一个简单处理
  }
}

使用

let Promise = require('./3.3.promise.js')

let p = new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve('xxx')
  }, 1000)
})

// promise 中 每次调用then 都应该返回一个新的promise 
// promise的实例只能成功或者失败  不能既成功又失败
p.then((data) => {
  console.log('p success', data)
}, (err) => {
  console.log(err)
}).then((data) => {
  console.log('success then', data)
}, (err) => {
  console.log(err)
})
// 一秒以后打印
// => p success xxx
//    success then undefined

简易版1.0.2地址以及测试用例地址

面试点:如何实现promise的链式调用?

如代码中只是简单处理_resolvePromise方法

⬆️回到顶部

完善_resolvePromise

再移步规范文档处理_resolvePromise

需要考虑以下几种情况

_resolvePromise (promise2, x, resolve, reject)

  • x为一个普通值
  • x为promise2时会导致循环调用
  • x为一个对象或者函数
    • x为一个promise

考虑以上进行完善

  // 内部核心方法 处理 成功或者失败执行的返回值 和promise2的关系
  _resolvePromise (promise2, x, resolve, reject) {
    // 这个处理函数 需要处理的逻辑韩式很复杂的
    // 有可能这个x 是一个promise  但是这个promise并不是我自己的
    if (promise2 === x) {
      return reject(new TypeError('Chaining cycle detected for promise '))
    }
    // 不单单需要考虑自己 还要考虑 有可能是别人的promise
    let called // 文档要求 一旦成功了 不能调用失败
    if ((x !== null && typeof x === 'object') || typeof x === 'function') {
      // 这样只能说 x 可能是一个promise
      try {
        // x = {then:function(){}}
        let then = x.then // 取then方法
        if (typeof then === 'function') {
          then.call(x, y => { // resolve(new Promise)
            if (called) return
            called = true
            resolvePromise(promise2, y, resolve, reject) //  递归检查promise
          }, reason => {
            if (called) return
            called = true
            reject(reason)
          })
        } else { // then方法不存在
          resolve(x); // 普通值
        }
      } catch (e) { // 如果取then方法出错了,就走失败
        if (called) return
        called = true
        reject(e)
      }
    } else { // 普通值
      resolve(x)
    }
  }

使用

let Promise = require('./3.4.promise.js')

// 普通返回值
let p = new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve('xxx')
  }, 1000)
})
p.then((data) => {
  console.log(`p success ${data}`)
  return 'first result'
}, (err) => {
  console.log(err)
}).then((data) => {
  console.log(`p success then ${data}`)
}, (err) => {
  console.log(`p error ${err}`)
})
// 一秒以后打印
// => p success xxx
//    p success then first result

// 抛错
let p2 = new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve('p2 xxx')
  }, 1000)
})
p2.then((data) => {
  throw new Error('just happy')
}, (err) => {
  console.log(err)
}).then((data) => {
  console.log(`p2 success then ${data}`)
}, (err) => {
  console.log(`p2 error ${err}`)
})
// 一秒以后打印
// => p2 error Error: just happy

// promise
let p3 = new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve('p3 xxx')
  }, 1000)
})
p3.then((data) => {
  return new Promise((resolve, reject) => {
    resolve('p3 data')
  }).then(data => {
    return data
  })
}, (err) => {
  console.log(err)
}).then((data) => {
  console.log(`p3 success then ${data}`)
}, (err) => {
  console.log(`p3 error ${err}`)
})

// 循环引用 - 例子待改
let p4 = new Promise((resolve,reject) => {
  let circleP = new Promise((resolve, reject) => {
    resolve(circleP)
  })  
  return circleP
})
p4.then((data) => {
  console.log(data)
})

简易版1.0.3地址以及测试用例地址

面试点:如何判断并解决promise循环引用的问题?

⬆️回到顶部

以上一个符合Promise/A+规范的promise基本完成

那怎么验证自己写的promise是否正确呢?

追加以下deferred方法以供检查

// 基于Promise实现Deferred 也提供给`promises-aplus-tests`做检查
static deferred () {
  let dfd = {}
  dfd.promise = new Promise((resolve, reject) => {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd    
}

安装检查工具promises-aplus-tests

npm i -g promises-aplus-tests

执行检查

promises-aplus-tests your-promise.js

都是绿色表示检查通过

代码地址

⬆️回到顶部

promise周边

以上只是一个简易的promise,我们期望完善更多功能:

  • catch方法
  • 静态方法
  • finally方法
  • all方法
  • race方法

下面实现的地址在简易版1.1.1以及测试用例

catch方法

实现

// 用于promise方法链时 捕获前面onFulfilled/onRejected抛出的异常
catch (onRejected) {
  return this.then(null, onRejected)
}

使用

let Promise = require('./3.6.promise.js')

// catch
let p = new Promise((resolve,reject) => {
  setTimeout(() => {
    resolve('xxx')
  }, 1000)
})
p.then((data) => {
  console.log(`p success then ${data}`)
}).then((data) => {
  throw new Error('just happy')
}).catch(err => {
  console.log(`p ${err}`)
})
// => p success then xxx
//    p Error: just happy

⬆️回到顶部

静态方法

实现

static resolve (value) {
  return new Promise(resolve => {
    resolve(value)
  })
}

static reject (reason) {
  return new Promise((resolve, reject) => {
    reject(reason)
  })
}

使用

let Promise = require('./3.6.promise.js')

// static resolve reject
let p2 = Promise.resolve(100)
p2.then(data => {
  console.log(`p2 ${data}`)
})
let p3 = Promise.reject(999)
p3.then(data => {
  console.log(`p3 ${data}`)
}).catch(err => {
  console.log(`p3 err ${err}`)
})
// => p2 100
//    p3 err 999

⬆️回到顶部

finally方法

实现

// finally 也是then的一个简写
finally (cb) {
  // 无论成功还是失败 都要执行cb 并且把成功或者失败的值向下传递
  return this.then(data => {
    cb()
    return data
  }, err => {
    cb()
    throw err
  })
}

使用

let Promise = require('./3.6.promise.js')

// finally
let p4 = Promise.resolve(100)
p4.then(data => {
  throw new Error('error p4')
}).finally(data => {
  console.log(`p4 ahhh`)
}).catch(err => {
  console.log(`p4 err ${err}`)
})
// => p4 ahhh
//    p4 err Error: error p4

面试点:如何实现promise的finally方法?

⬆️回到顶部

all方法

实现

/**
 * @desc 当这个数组里的所有promise对象全部变为resolve状态的时候,才会resolve。
 * @param {Array<Promise>} promises promise对象组成的数组作为参数
 * @return 返回一个Promise实例
 */
static all (promises) {
  return new Promise((resolve, reject) => {
    let arr = []
    // 处理数据的方法
    let i = 0
    const processData = (index, data) => {
      arr[index] = data //数组的索引和长度的关系
      if (++i === promises.length){ // 当数组的长度 和promise的个数相等时 说明所有的promise都执行完成了
        resolve(arr)
      }
    }
    for (let i = 0; i < promises.length; i++){
      let promise = promises[i]
      if (typeof promise.then == 'function'){
        promise.then(function (data) {
          processData(i, data) // 把索引和数据 对应起来 方便使用
        }, reject)
      }else{
          processData(i,promise)
      }
    }
  })    
}

使用

let Promise = require('./3.6.promise.js')

// all & race
const fs = require('fs')
const path = require('path')
const resolvePath = (file) => {
  return path.resolve(__dirname, '../callback/static/', file)
}
const read = (file) => {
  return new Promise((resolve, reject) => {
    fs.readFile(resolvePath(file), 'utf8', (err, data) => {
      if(err) reject(err)
      resolve(data)
    })    
  })
}
// all
Promise.all([
  read('name.txt'),
  read('age.txt')
]).then(data => {
  console.log(`all ${data}`)
}).catch(err => {
  console.log(`all err ${err}`)
})
// => all Careteen,23

面试点:如何实现promise的all方法?

⬆️回到顶部

race方法

实现

/**
 * @desc 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理(取决于哪一个更快)
 * @param {Array<Promise>} promises 接收 promise对象组成的数组作为参数
 * @return 返回一个Promise实例
 */
static race (promises) {
  return new Promise((resolve, reject) => {
    promises.forEach((promise, index) => {
      promise.then(resolve, reject)
    })
  })    
}

使用

let Promise = require('./3.6.promise.js')

// all & race
const fs = require('fs')
const path = require('path')
const resolvePath = (file) => {
  return path.resolve(__dirname, '../callback/static/', file)
}
const read = (file) => {
  return new Promise((resolve, reject) => {
    fs.readFile(resolvePath(file), 'utf8', (err, data) => {
      if(err) reject(err)
      resolve(data)
    })    
  })
}
// race
Promise.race([
  read('name.txt'),
  read('age.txt')  
]).then(data => {
  console.log(`race ${data}`)
}).catch(err => {
  console.log(`race err ${err}`)
})
// => race Careteen/23 不一定 得看读取速度

⬆️回到顶部

generator用法

在理解generator之前先看一个例子。

实现一个可传任意个参数的加法函数

老司机们三下五除二就能得出求和函数

const sum = () => {
  return [...arguments].reduce((ret, item) => {
    return ret + item
  }, 0)
}
sum(1, 2, 3, 4) // => 10
sum(1, 2, 3, 4, 5) // => 15

以上代码地址

使用ES6的展开运算符...可枚举出所有参数,再用数组包裹,即可将一个类数组转换为一个数组。利用reduce实现累加,方可得出求和函数。

那展开运算符能否操作对象佯装的类数组呢?那就来试一试

let obj = {
  0: 1,
  1: 2,
  2: 3,
  length: 3
}
console.log([...obj]) // => TypeError: obj[Symbol.iterator] is not a function

以上代码地址

可得知对象是不能被迭代的,根据报错信息,我们再改进代码

let o = { 0: 1, 1: 2, 2: 3, length: 3, [Symbol.iterator]: function () {
  let currentIndex = 0
  let that = this
  return {
    next(){
      return {
        value: that[currentIndex++],
        done: currentIndex-1 === that.length
      }
    }
  }
}}
let arr = [...o] // [1, 2, 3]

再使用generator实现

let o = {0: 1, 1: 2, 2: 3, length: 3, [Symbol.iterator]: function* () {
    let index = 0
    while (index !== this.length) {
      yield this[index]
      index++
    }
  }
}
let arr = [...o] // [1, 2, 3]

以上代码地址

生成器可以实现生成迭代器,生成器函数就是在函数关键字中加个*再配合yield来使用,并且yield是有暂停功能的。

function * say() {
  yield 'node'
  yield 'react'
  yield 'vue'
}

那如何遍历迭代器呢?

let it = say ()
let flag = false
do{
  let {value, done} = it.next()
  console.log(value)
  flag = done
}while(!flag)
// => node
//    react
//    vue
//    undefined

以上代码地址

迭代器提供next方法,可得出迭代的value和是否已经迭代完成done,用一个循环即可遍历。

yield的返回值的使用场景

function * say() {
    let a = yield 'hello'
    console.log('a', a)
    let b = yield 'careteen'
    console.log('b', b)
    let c = yield 'lanlan'
    console.log(c)
}
let it = say()
it.next(100) // 第一次next传递参数 是无意义的
it.next(200)
it.next(300)
// => a 200
//    b 300

以上代码地址

generator执行流程大体如下图

generator-process

可看出第一次next传递参数是无意义的,所以输出结果为 a 200 b 300

以上均为同步的情况,接下来看下yield后面是异步的场景。

再通过一个例子来深入理解。

通过读取文件1.txt的内容为2.txt,再读取2.txt的内容为3.txt,最后读取3.txt中的内容Careteen

首先需要准备三个文件,放置在./static目录下,再准备读取文件的函数,

const fs = require('fs')
const path = require('path')

const resolvePath = (file) => {
  return path.resolve(__dirname, './static/', file)
}

function read (file) {
  return new Promise((resolve, reject) => {
    fs.readFile(resolvePath(file), 'utf8', (err, data) => {
      if (err) reject(err)
      resolve(data)
    })
  })
}

在使用generator实现读取函数

function * r() {
   let r1 = yield read('1.txt')
   let r2 = yield read(r1)
   let r3 = yield read(r2)
  return r3
}

期望的过程是通过读取文件1.txt的内容为2.txt,再读取2.txt的内容为3.txt,最后读取3.txt中的内容Careteen,进行返回。

首先我们能想到使用回调的方式解决,因为yield后面是一个promise

let it = r()
let {value,done} = it.next()
value.then((data) => { // data->2.txt
  let {value,done} = it.next(data)
  value.then((data) => {
    let { value, done } = it.next(data)
    value.then((data) => {
      console.log(data) // data-> 结果
    })
  })
})
// => Careteen

但这又会产生回调地狱,所以需要优化,我们需要一个迭代函数,通过递归可以实现

function co (it) {
  return new Promise((resolve, reject) => {
    // next方法  express koa  原理 都是这样的
    function next (data) { // 使用迭代函数来实现 异步操作按顺序执行
      let { value, done } = it.next(data)
      if(done){
        resolve(value)
      }else{
        value.then((data) => {
          next(data)
        },reject)
      }
    }
    next()
  })
}

使得异步可以按顺序来执行,最后看一下执行

co(r()).then((data) => {
  console.log(data)
})
// => Careteen

以上代码地址

非常完美的实现了,但是如果yield的后面是一个同步操作,没有then方法,在co方法中我们还需要特殊处理,也比较简单。

牛逼的TJ大神的CO库就对此做了很完善的处理,感兴趣的可前往仓库看看源码,只有200多行。

generator的应用:

如何实现generator

function * careteen() {
  yield 100
  yield 200
}

可查看babel编译后的结果

⬆️回到顶部

async-await

  • 写起来是同步的,语法糖很甜不腻。

  • bluebird

    • promisify
    • promisifyAll
  • async-await

    • 串行情况
    • 并行情况
  • async-await内部机制

    • 在babel中的编译结果,实质上就是generator+co
  • 例子

    • 三个小球滚动
      • 回调实现 回调地狱
      • promise实现 也不是很美观
      • generator实现 需要co库
      • async-await实现
async function careteen() {
  await 100
  await 200
  return 300
}
careteen.then(_ => {
  console.log(_)
})

通过babel编译后可看出实质上是通过generator+co的方式实现的。

⬆️回到顶部

解析URL字符串

解析字符串

前言

写一个程序parse,解析下面的queryString,返回一个对象

console.log(parse('a=1&b=2&c=3')) // => { a: '1', b: '2', c: '3' }
console.log(parse('a&b&c')) // => {}
console.log(parse('a[name][second]=careteen&a[company]=sohu&b=y')) // => { a: { name: { second: 'careteen' }, company: 'sohu' }, b: 'y' }
console.log(parse('color=Deep%20Blue')) // => { color: 'Deep Blue' }
console.log(parse('a[0]=1&a[1]=2')) // => { a: [ '1', '2' ] }

分析

首先要先了解url参数的规则

  • &或其他字符进行分割,且以=分割键值对
  • &=分割时可能没有值
  • =后面的值可能已经encodeURIComponent转码,需要解码
  • 可能会是一个多层级的对象a[name][second]=careteen&a[company]=sohu&b=y,需要按层级设置对象
  • 对象的键可能为一个数字a[0]=1&a[1]=2,此时应该处理返回成数组

实现

针对上述分析其规则,解析一个URL需要考虑诸多情况。

具体代码和测试用例实现

下面给出具体实现

/**
 * @desc 解析URL
 * @param {String} str 
 * @param {Object} options 
 * @param {String} options.delimiter // 分隔符
 */
const parse = (str, options = {}) => {
  let config = Object.assign({
    delimiter: '&'
  }, options)

  return str.split(config.delimiter).reduce((ret, cur) => {
    let [key, value] = cur.split('=')
    if (!value) return ret
    // ret[key] = value
    deepSet(ret, key, value)
    return ret
  }, {})
}

辅助函数

/**
 * @desc 辅助函数 深层级设置
 */
const deepSet = (ret, key, value) => {
  /* eslint-disable */
  let path = key.split(/[\[\]]/g).filter(item => !!item)
  // console.log(path)
  let i = 0
  for (; i < path.length - 1; i++) {
    if (ret[path[i]] === undefined) {
      if (path[i + 1].match(/^\d+$/)) {
        ret[path[i]] = []
      } else {
        ret[path[i]] = {}
      }
    }
    ret = ret[path[i]]
  }
  ret[path[i]] = decodeURIComponent(value)
  // console.log(ret)
}

测试用例

console.log(parse('a=1&b=2&c=3')) // => { a: '1', b: '2', c: '3' }
console.log(parse('a&b&c')) // => {}
console.log(parse('a[name][second]=careteen&a[company]=sohu&b=y')) // => { a: { name: { second: 'careteen' }, company: 'sohu' }, b: 'y' }
console.log(parse('color=Deep%20Blue')) // => { color: 'Deep Blue' }
console.log(parse('a[0]=1&a[1]=2')) // => { a: [ '1', '2' ] }

总结

解析字符串看似简单,实则考察诸多知识点

  • 使用reduce去简化流程
  • 考虑URL规则满足各种需求
  • 检验对正则的掌握
  • 深层级对象的设置需要使用循环去合理设置
  • 区分数组和对象两种场景
  • 别忘了解码

在vue和element-ui的table中实现分页复选

实现分页复选

背景

后台管理系统中,使用表格展示数据时,可能的需求是多项选择然后进行批量操作,也期望能翻页多选。

实现

paging-check-1
页面结构如下

<el-table
  class="table"
  :data="tableData"
  border
  style="width: 100%"
  @selection-change="handleSelectionChange"
  ref="asTable"
>
  <el-table-column
    width="50"
    type="selection"
    fixed="left"
  >
  </el-table-column>
  <el-table-column
    prop="id"
    label="ID"
  >
  </el-table-column>
  <el-table-column
    prop="name"
    label="名字"
  >
  </el-table-column>

  <el-table-column
    label="操作"
    width="100"
  >
    <template slot-scope="scope">
      <el-button type="primary" plain size="small" @click="handleEdit(scope.row)" >编辑</el-button>
    </template>
  </el-table-column>
</el-table>
<el-pagination
  background
  @current-change="handleCurrentChange"
  :current-page.sync="pagination.currentPage"
  :page-size="pagination.size"
  layout="total, prev, pager, next, jumper"
  :total="pagination.total"
  slot="pagination"
>
</el-pagination>

模拟数据实现分页

data () {
  return {
    tableData: [],
    multipleSelection: [],
    pagination: {
      currentPage: 1,
      size: 10,
      total: 1000
    }
  }
},
beforeMount () {
  this.fetchData()
},
methods: {
  fetchData () {
    this.tableData = []
    let start = (this.pagination.currentPage - 1) * this.pagination.size
    let end = this.pagination.currentPage * this.pagination.size
    setTimeout(_ => {
      for (let i = start; i < end; i++) {
        this.tableData.push({
          id: i,
          name: `王${i}兰`
        })
      }
    }
  },
  handleCurrentChange () {
    this.fetchData()
  },
  handleSelectionChange (val) {
    this.multipleSelection = val
  },
}

展示已选择项

<div class="result">
  已选:{{ allMultipleSelection }}
</div>
allMultipleSelection: [],

在复选事件中对所选项进行存储

主要思路就是:

  • 将当前页已选数据放入所有已选项
  • 将所有已选项数据中当前页没选择的项移除
handleSelectionChange (val) {
  this.multipleSelection = val
  // @tip 实现分页复选
  console.log(val, 'selection')
  setTimeout(_ => {
    this.resolveAllSelection()
  }, 50)
},
resolveAllSelection () {
  let currentPageData = this.tableData.map(item => item[this.uniqueKey]) // 当前页所有数据
  let currentPageSelected = this.multipleSelection.map(item => item[this.uniqueKey]) // 当前页已选数据
  let currentPageNotSelected = currentPageData.filter(item => !currentPageSelected.includes(item)) // 当前页未选数据
  // 将当前页已选数据放入所有已选项
  currentPageSelected.forEach(item => {
    if (!this.allMultipleSelection.includes(item)) {
      this.allMultipleSelection.push(item)
    }
  })
  // 将所有已选项数据中当前页没选择的项移除
  currentPageNotSelected.forEach(item => {
    let idx = this.allMultipleSelection.indexOf(item)
    if (idx > -1) {
      this.allMultipleSelection.splice(idx, 1)
    }
  })
  console.log(this.allMultipleSelection, 'all')
},

此时还需要在切换页面时将之间选择项进行重新选中,即遍历当前页所有数据如果存在于所有已选项中,则将其置为已选择。

fetchData () {
  // ...
  setTimeout(_ => {
    // ...
    // @tip 实现分页复选
    setTimeout(_ => {
      this.setSelectedRow()
    }, 50)
  }, 200)
},
setSelectedRow () {
  // 设置当前页已选项
  this.tableData.forEach(item => {
    if (this.allMultipleSelection.includes(item[this.uniqueKey])) {
      this.$refs.asTable.toggleRowSelection(item, true)
      console.log(item[this.uniqueKey], 'set')
    }
  })
},

paging-check-2
以上实现了分页复选功能。

所有代码存放在@careteen/lan-vue

查看DEMO

clone仓库并引入依赖

git clone [email protected]:careteenL/lan-vue.git
npm install
npm run serve

浏览器打开 http://localhost:8080/#/examples/pagingCheck 即可看到效果

事件循环详解

事件循环详解

Node系列-上一节为什么要使用Node

目录

事件循环机制

先了解下任务队列

  • 所有同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  • 主线程不断重复上面的第三步。

此时区分为浏览器的事件循环Node端的事件循环。下面将一一详解。

浏览器的事件循环

主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

eventloop-browser

上图过程是一个宏观的表述,实际上callback queue任务队列是分为macro task宏任务和micro task微任务两种。

在一个事件循环当中,执行的步骤如下:

  1. 首先将同步代码放入执行栈进行执行,若存在异步事件则将其的返回结果会被放到一个任务队列中。任务队列又分为宏任务队列和微任务队列。
  2. 当执行栈为空时,会优先查看微任务队列中是否有事件存在
  • 若存在,则依次执行队列事件中的对应回调,直到微任务队列为空,再进入下一步
  • 若不存在,跳往下一步
  1. 查看宏任务队列中是否有事件存在
  • 若存在,则将队列中的事件的对应回调放入执行栈执行
  • 若不存在,跳往下一步
  1. 若执行栈中又有异步代码,则放入下一个任务队列。如此反复循环前三个步骤

从以上我们得知重点是执行栈为空后优先处理微任务再处理宏任务。

浏览器中的宏任务和微任务有哪些

写过vue的同学应该熟悉一个方法this.$nextTick(() => { ... }),此时对于宏任务和微任务的分类我们不知所措,那就看看vue源码中是如何处理的。

next-tick.js传送门

其中定义的很清楚

  • 宏任务

    • setImmediate
    • MessageChannel
    • setTimeout
  • 微任务

    • promise的then方法
    • MutationObserver (vue2.0已经废弃使用此API)

setImmediate/setTimeout

setImmediate为IE特有的,我们可以在IE浏览器环境下做测试

setImmediate(() => {
  console.log('setImmediate')
})
setTimeout(() => {
  console.log('setTimeout')
}, 0) // 延迟设置为0,实际上是会有4ms的延迟。
// => setImmediate
//    setTimeout

例子代码地址

MessageChannel

H5的API,兼容性不怎么好,前往mdn查看使用

再做如下测试

let channel = new MessageChannel()
let port1 = channel.port1
let port2 = channel.port2
port1.postMessage('hello careteen ~')
port2.onmessage = function (e) {
  console.log(e.data)
}
// => hello careteen ~

例子代码地址

promise的then方法

Promise.resolve(100).then(data => {
  console.log(data)
})
// => 100

MutationObserver
这也是一个属于微任务的异步方法,前往mdn查看使用

此方法在vue1.0中有使用到,但是再2.0以后则废弃了。

下面简单介绍下使用,场景为为页面创建多个节点,当节点创建完成后告诉我们。

html

<div id="box"></div>

js

let observer = new MutationObserver(function() {
  console.log('dom 更新完了')
  console.log(box.children.length)
})
// 监听后代的变化
observer.observe(box, {
  childList: true
})
for (let i = 0; i < 5; i++) {
  box.appendChild(document.createElement('sapn'))
}
for (let i = 0; i < 10; i++) {
  box.appendChild(document.createElement('p'))
}

// => 当节点创建完成后
//    dom 更新完了
//    15

例子代码地址

面试题详解

case1:

Promise.resolve().then(() => {
  console.log('then1')
  setTimeout(() => {
    console.log('timer1')
  }, 0)
})
console.log('start')
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(() => {
    console.log('then2')
  })
}, 0)
// => start -> then1 -> timer2 -> then2 -> timer1

例子代码地址

根据上面的执行流程可得知结果。

  • 先执行同步代码start
  • promise的then方法里面放进微任务队列,
    • 然后执行同步代码then1
    • 将setTimeout放入宏任务队列
  • setTimeout的回调放入宏任务队列
  • 等到setTimeout2时间到了
    • 执行setTimeout2回调的同步代码timer2
    • promise的then方法里面放进微任务队列,然后执行then2
    • 等到setTimeout1的时间到了,执行里面同步代码

Node的事件循环

eventloop-node

  • V8引擎解析JavaScript脚本。
  • 解析后的代码,调用Node API。
  • libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
  • V8引擎再将结果返回给用户。

以上也只是宏观上的一个描述,和浏览器一样,异步事件方法的队列也细分了几个。前往Node官网可查看详细说明

下面摘自Node官网

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

从上面的模型中我们可以得知机制的执行顺序:

外部输入数据-->轮询阶段(poll)-->检查阶段(check)-->关闭事件回调阶段(close callback)-->定时器检测阶段(timer)-->I/O事件回调阶段(I/O callbacks)-->闲置阶段(idle, prepare)-->轮询阶段...

细分的几个异步队列:

  • timers阶段
    • setTimeout方法
    • setInterval方法
  • pending callbacks阶段
    • I/O读写操作。如fs.readFile()方法
  • idle, prepare阶段
    • 仅在内部使用,我们暂时不用关注
  • poll阶段
    • 检索新的I/O事件,执行与I/O相关的回调(除了关闭回调、计时器调度的回调和setimmediation()之外,几乎所有回调都是如此);节点将在适当的时候在这里阻塞。
  • check阶段
    • setImmediate()的回调会在这个阶段执行
  • close callbacks阶段
    • 例如socket.on('close', ...)这种close事件的回调

和浏览器的事件循环机制有所不同:

  • 首先会在poll阶段

    • 先查看poll queue中是否有事件
      • 若有,则按先进先出的顺序依次执行回调
      • 若无,则会检查是否有setImmediate的回调
        • 若有此回调,则会进入下面check阶段去执行这些回调
      • 于此同时,检查是否有到期的timer
        • 若有,则将这些到期的timer的回调按照调用顺序放到timer queue,然后循环进入timer阶段执行队列中的回调
      • setImmediatetimer的判断顺序不是固定的,受代码运行环境的影响
      • 如果setImmediatetimer的队列都是空的,那么循环会在poll阶段停留,直到有一个I/O事件返回,循环会进入I/O callback阶段并立即执行这个事件的回调
  • check阶段

    • 此阶段专门执行setImmediate的回调,当poll阶段进入空闲状态,并且setImmediate队列中有callback时,事件循环进入这个阶段
  • close阶段

    • 当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()方法发送出去
  • timer阶段

    • 这个阶段以先进先出的方式执行所有到期的timer加入timer队列里的callback,一个timer callback指得是一个通过setTimeout或者setInterval函数设置的回调函数
  • I/O callback阶段

    • 如上文所言,这个阶段主要执行大部分I/O事件的回调,包括一些为操作系统执行的回调。例如一个TCP连接生错误时,系统需要执行回调来获得这个错误的报告。
  • 如此反复循环

  • 特别的:process.nextTick()这个方法虽然没有在上面列入,但是却在每个阶段执行完毕准备进入下一个阶段时优先调用

    • 与执行poll queue的任务不同的是,这个操作在队列清空前是不会停止的。也就是说错误使用会导致node进入一个死循环,直到内存泄露

面试题详解

上面说了一大推,看起来很枯燥,那么下面来几个case深入理解

case1:

Promise.resolve().then(() => {
  console.log('then1')
  setTimeout(() => {
    console.log('timer1')
  }, 0)
})
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(() => {
    console.log('then2')
  })
}, 0)
// => then1 -> timer2 -> then2 -> timer1
// or then1 -> timer2 -> timer1 -> then2

例子代码地址

上面代码的结果有两种可能,then2执行后,timer1可能还没到时间也可能到时间了,因为setTimeout方法第二个参数如果设置为0,实际执行时会有4ms的延迟。

case2:

setTimeout(() => {
  console.log('timeout')
}, 0)
setImmediate(() => {
  console.log('setImmediate')
})
// => setImmediate -> timeout
// or timeout -> setImmediate

例子代码地址

多运行几次,运行结果是不一定的,这取决于运行代码的环境。运行和环境中的各种复杂情况会导致在同步队列里两个方法的顺序随机决定。

再来看一个例子case3:

let fs = require('fs')
fs.readFile('1.setImmediate.html', () => {
  setTimeout(() => {
    console.log('timeout')
  }, 0)
  setImmediate(() => {
    console.log('setImmediate')
  })
})
// => setImmediate -> timeout

例子代码地址

回顾上面提到的阶段,在I/O事件的回调中,setImmediate的回调永远优先于setTimeout的回调执行。所以返回结果的顺序是固定的。

友情链接

Node系列-下一节手摸手带你撸一个commonjs规范

如果项目中没有亮点怎么办

亮点可以写:

  • 假设你们公司开发小程序没有区分环境,你做了,还写了一个简单的脚手架,这虽然是很小的事但还是能体现你的价值。
  • 比如你们公司没用ESLint、你推动各个项目组都使用了,你使用了webpack打包优化,优化了打包速度等等。
  • 比如小程序、PC端、H5端等、线上问题排查定位困难,你引入了免费的sentry错误监控,来解决问题,保证线上稳定性。引入sentry其实挺简单的,通过这件事,你学习总结了错误监控原理,都是亮点。
  • 比如项目有登录功能,你研究后知道如何实现的,另外又自己总结了几种常见登录方案的优缺点。比如jwt和session等。
  • 比如你们可能每次都带薪打包编译项目,你引入了持续集成CI,都是体现你的价值。
  • 比如你们可能项目提交git commit message信息比较乱,你引入了工具校验message、生成changelog、使用了release-it等等,虽然比较容易, 但你有意识的去做了,也是亮点。
  • 根据业务场景和统计需求。引入了埋点(比如友盟)等,了解统计用户行为和浏览器等设备信息,帮助公司开发某些功能时做一些决策,而且研究了其原理等。

总之就是发现问题,解决问题,总结问题,推广给他人。

技术各职级所需核心能力

对标阿里职级

P6

主要职责是交付产品,准确理解需求,形成产品设计方案,通过沟通和协调完整的把控产品,实现落地过程及时交付,最终实现用户价值。

P7

需要把控全局,清晰定义问题,对需求有深刻理解、判断和拆解。对技术的应用有较强认知,形成有创造性的方案。以影响力协同多团队,实现所主导产品线的规划落地并推进持续迭代。具备基本的成本和收益意识,从客户,合作方的角度思考产品价值和做出判断。保障产品的投入产出效益。主导产品线,实现用户体验和产品数据有效提升。

P8

深度洞察商业和技术趋势,结合对业务规划的理解,形成相比业界大多数同类产品更高效的和创新的全局规划和运营方案。在单个领域形成领先。在 BU 层面具备影响力,使各利益方有机结合,实现所主导多产品线的全局规划落地,并推进持续创新。理解产业,开展生态与竞争合作关系分析、判断,转化为产品承接,形成匹配商业节奏的产品节奏。主导多产品线,实现有效运营和增长。

P9

要能做到的是领先业界。具有成熟的系统架构和机制设计能力,形成跨领域的、业界领先的产品战略布局和全局规划。能实现规模化、产品情商(用户体验)优秀,跨领域复杂问题的定义能力优秀。在 BG 层面具备广泛影响力,通过配置资源,落地产品战略布局。预判产品和人情趋势,构建战略控制节奏,形成产品架构的前瞻布局。同时要保障 BU 目标达成,为 BG 业务和公司中长期经营业绩带来显著的影响。或者通过为客户创造价值和创新,为所负责产品,带来超越业界同类产品的影响力和商业结果。或者通过洞见新的用户需求和赛道机会,凭借产品专业能力和商业模式定义独立将产品带到领先业界的影响力。例如达到千万级 DAU。

达到P6+我需要做到甚至做到更多

  • 积极心态,有自驱力,努力把事情做的更好
  • 专业能力
    • 基本功扎实
    • 良好的规范,熟悉类库和周围生态
    • 能完成复杂的前端设计方案
    • 快速定位及解决疑难问题
    • 一定服务端的能力
  • 职业能力
    • 良好的沟通能力
    • 可以独立承担业务接口人
    • 可以承担跨部门PM能力
    • 理解业务优先级
  • 场景方案
    • 业务方案沉淀
    • 专项突破攻关
  • 推动落地能力
    • 抓住工作重点
    • 利用团队的力量获取成果
    • 影响其他同学技能提升
    • 坚定目标,不折不挠
  • 业务中
    • 主动承担更多的事情来赋能业务,持续打磨提升自己
    • 建设团队组件库
    • 工程脚手架建设
    • 结合业务所在公司做有深度的技术分享
    • 参与社区,反哺业务
    • 推动TS发展进程
    • 锻炼协作沟通能力
      ...

天道酬勤

前端面试-实现一个简版koa

目录

koa的使用

koa的使用非常简单,引入依赖后编写

const Koa = require('koa')
let app = new Koa()
app.use((ctx, next) => {
  console.log(ctx)
})
app.listen(4000)

然后在浏览器端打开http://127.0.0.1:4000即可访问

若没有指定返回body,koa默认处理成了Not Found

ctx

再进一步扩展代码,看看ctx上面有哪些东西

// ...
  console.log(ctx)
  console.log('native req ----') // node原生的req
  console.log(ctx.req.url)
  console.log(ctx.request.req.url)
  console.log('koa request ----') // koa封装了request
  console.log(ctx.url)
  console.log(ctx.request.url)
  // native req ----
  // /
  // /
  // koa request ----
  // /
  // /
// ...

以上代码存放在仓库,自取。

koa官网有说明在ctx挂载了一系列requestresponse的属性别名。

ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
// ctx.url 代理了 ctx.request.url

next

以下代码存放在仓库,自取。

使用next看看作用

const Koa = require('koa')
let app = new Koa()
app.use((ctx, next) => {
  console.log(1)
  next()
  console.log(2)
})
app.use((ctx, next) => {
  console.log(3)
  next()
  console.log(4)
})
app.use((ctx, next) => {
  console.log(5)
  next()
  console.log(6)
})
app.listen(4000)
// 1
// 3
// 5
// 6
// 4
// 2

从上面代码打印结果可以看出,next的作用就是做一个占位符。可以看成以下形式

app.use((ctx, next) => {
  console.log(1)
  app.use((ctx, next) => {
    console.log(3)
    app.use((ctx, next) => {
      console.log(5)
      next()
      console.log(6)
    })
    console.log(4)
  })
  console.log(2)
})

这即是洋葱模型。

如果某个中间件有异步代码呢?

const Koa = require('koa')
let app = new Koa()
// 异步函数
const logger = () => {
  return new Promise((resolve, reject) => {
    setTimeout(_ => {
      console.log('logger')
      resolve()
    }, 1000)
  })
}
app.use((ctx, next) => {
  console.log(1)
  next()
  console.log(2)
})
app.use(async (ctx, next) => {
  console.log(3)
  await logger()
  next()
  console.log(4)
})
app.use((ctx, next) => {
  console.log(5)
  next()
  console.log(6)
})
app.listen(4000)
// 1
// 3
// 2
// 等待1s
// logger
// 5
// 6
// 4

此时打印结果并不是我们预期的结果,我们期望的是1 -> 3 -> 1s logger -> 5-> 6-> 4 ->2

此时我们需要在next前面加一个await

// ...
app.use(async (ctx, next) => {
  console.log(1)
  await next()
  console.log(2)
})
// ...

简单阅读下koa源码

koa致力于成为一个更小、更富有表现力、更健壮的web开发框架。

其源码也是非常轻量且易读。

核心文件四个

  • application.js:简单封装http.createServer()并整合context.js
  • context.js:代理并整合request.jsresponse.js
  • request.js:基于原生req封装的更好用
  • response.js:基于原生res封装的更好用

开始撸源码

下面涉及到的代码存放到仓库中,需要的自取。

koa是用ES6实现的,主要是两个核心方法app.listen()app.use((ctx, next) =< { ... })

先来在application.js中实现app.listen()

const http = require('http')
class Koa {
  constructor () {
    // ...
  }  
  // 处理用户请求
  handleRequest (req, res) {
    // ...
  }  
  listen (...args) {
    let server = http.createServer(this.handleRequest.bind(this))
    server.listen(...args)
  }  
}
module.exports = Koa

ctx挂载了什么东西

从上面的简单使用ctx中可以看出

ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
ctx.xxx = ctx.request.xxx
ctx.yyy = ctx.response.yyy

我们需要以上几个对象,最终都代理到ctx对象上。

创建context.js/request.js/response.js三个文件

request.js内容

const url = require('url')
let request = {}
module.exports = request

response.js内容

let response = {}
module.exports = response

context.js内容

let context = {}

module.exports = context

application.js中引入上面三个文件并放到实例上

const context = require('./context')
const request = require('./request')
const response = require('./response')
class Koa extends Emitter{
  constructor () {
    super()
    // Object.create 切断原型链
    this.context = Object.create(context)
    this.request = Object.create(request)
    this.response = Object.create(response)
  }
}

由于不能直接用等号为其赋值,不然在修改变量属性时会直接篡改原始变量,因为对象引用了同一内存空间。

所以使用Object.create方法切断依赖,此方法相当于

function create (parentPrototype) {
  function F () {}
  F.prototype = parentPrototype
  return new F()
}

然后处理用户请求并在ctx上代理request / response

  // 创建上下文
  createContext (req, res) {
    let ctx = this.context
    // 请求
    ctx.request = this.request
    ctx.req = ctx.request.req = req
    // 响应
    ctx.response = this.response
    ctx.res = ctx.response.res = res
    return ctx
  }
  handleRequest (req, res) {
    let ctx = this.createContext(req, res)
    return ctx
  }

context.js中,使用__defineGetter__ / __defineSetter__实现代理,他是Object.defineProperty()方法的变种,可以单独设置get/set,不会覆盖设置。

let context = {}
// 定义获取器
function defineGetter (key, property) {
  context.__defineGetter__ (property, function () {
    return this[key][property]
  })
}
// 定义设置器
function defineSetter (key, property) {
  context.__defineSetter__ (property, function (val) {
    this[key][property] = val
  })
}
// 代理 request
defineGetter('request', 'path')
defineGetter('request', 'url')
defineGetter('request', 'query')
// 代理 response
defineGetter('response', 'body')
defineSetter('response', 'body')
module.exports = context

request.js中,使用ES5提供的属性访问器实现封装

const url = require('url')
let request = {
  get url () {
    return this.req.url // 此时的this为调用的对象 ctx.request
  },
  get path () {
    let { pathname } = url.parse(this.req.url)
    return pathname
  },
  get query () {
    let { query } = url.parse(this.req.url, true)
    return query
  }
  // ...更多待完善
}
module.exports = request

response.js中,使用ES5提供的属性访问器实现封装

let response = {
  set body (val) {
    this._body = val
  },
  get body () {
    return this._body // 此时的this为调用的对象 ctx.response
  }
  // ...更多待完善
}
module.exports = response

以上实现了封装request/response并代理到ctx

ctx = {}
ctx.request = {}
ctx.response = {}
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res
ctx.xxx = ctx.request.xxx
ctx.yyy = ctx.response.yyy

next构建的洋葱模型

接下来实现koa中第二个方法app.use((ctx, next) =< { ... })

use中存放着一个个中间件,如cookie、session、static...等等一堆处理函数,并且以洋葱式的形式执行。

  constructor () {
    // ...
    // 存放中间件数组
    this.middlewares = []
  }
  // 使用中间件
  use (fn) {
    this.middlewares.push(fn)
  }

当处理用户请求时,期望执行所注册的一堆中间件

  // 组合中间件
  compose (middlewares, ctx) {
    function dispatch (index) {
      // 迭代终止条件 取完中间件
      // 然后返回成功的promise
      if (index === middlewares.length) return Promise.resolve()
      let middleware = middlewares[index]
      // 让第一个函数执行完,如果有异步的话,需要看看有没有await
      // 必须返回一个promise
      return Promise.resolve(middleware(ctx, () => dispatch(index + 1)))
    }
    return dispatch(0)
  }
  // 处理用户请求
  handleRequest (req, res) {
    let ctx = this.createContext(req, res)
    this.compose(this.middlewares, ctx)
    return ctx
  }

以上的dispatch迭代函数在很多地方都有运用,比如递归删除目录,也是koa的核心。

中间件含异步代码如何保证正确执行

返回的promise主要是为了处理中间件中含有异步代码的情况

在所有中间件执行完毕后,需要渲染页面

  // 处理用户请求
  handleRequest (req, res) {
    let ctx = this.createContext(req, res)
    res.statusCode = 404 // 默认404 当设置body再做修改
    let ret = this.compose(this.middlewares, ctx)
    ret.then(_ => {
      if (!ctx.body) { // 没设置body
        res.end(`Not Found`)
      } else if (ctx.body instanceof Stream) { // 流
        res.setHeader('Content-Type', 'text/html;charset=utf-8')
        ctx.body.pipe(res)
      } else if (typeof ctx.body === 'object') { // 对象
        res.setHeader('Content-Type', 'text/josn;charset=utf-8')
        res.end(JSON.stringify(ctx.body))
      } else { // 字符串
        res.setHeader('Content-Type', 'text/html;charset=utf-8')
        res.end(ctx.body)
      }
    })
    return ctx
  }

需要考虑多种情况做兼容。

解决多次调用next导致混乱问题

通过以上代码进行以下测试

执行结果将是

// 1 => 3 =>1s,logger => 4
//   => 3 =>1s,logger => 4  => 2

并不满足我们的预期

因为执行过程如下

在第 2 步中, 传入的 i 值为 1, 因为还是在第一个中间件函数内部, 但是 compose 内部的 index 已经是 2 了, 所以 i < 2, 所以报错了, 可知在一个中间件函数内部不允许多次调用 next 函数。

解决方法就是使用flag作为洋葱模型的记录已经运行的函数中间件的下标, 如果一个中间件里面运行两次 next, 那么 index 是会比 flag 小的。

  /**
   * 组合中间件
   * @param {Array<Function>} middlewares 
   * @param {context} ctx 
   */ 
  compose (middlewares, ctx) {
    let flag = -1
    function dispatch (index) {
      // 3)flag记录已经运行的中间件下标
      // 3.1)若一个中间件调用两次next那么index会小于flag
      // if (index <= flag) return Promise.reject(new Error('next() called multiple times'))
      flag = index
      // 2)迭代终止条件:取完中间件
      // 2.1)然后返回成功的promise
      if (index === middlewares.length) return Promise.resolve()
      // 1)让第一个函数执行完,如果有异步的话,需要看看有没有await
      // 1.1)必须返回一个promise
      let middleware = middlewares[index]
      return Promise.resolve(middleware(ctx, () => dispatch(index + 1)))
    }
    return dispatch(0)
  }

基于事件驱动去处理异常

如何处理在中间件中出现的异常呢?

Node是以事件驱动的,所以我们只需继承events模块即可

const Emitter = require('events')
class Koa extends Emitter{
  // ...
  // 处理用户请求
  handleRequest (req, res) {
    // ...
    let ret = this.compose(this.middlewares, ctx)
    ret.then(_ => {
      // ...
    }).catch(err => { // 处理程序异常
      this.emit('error', err)
    })
    return ctx
  }  
}

然后在上面做捕获异常,使用时如下就好

const Koa = require('./src/index')

let app = new Koa()

app.on('error', err => {
  console.log(err)
})

测试用例代码存放在仓库中,需要的自取。

总结

通过以上我们实现了一个简易的KOArequest/response.js文件还需扩展支持更多属性。

完整代码以及测试用例存放在@careteen/koa,感兴趣可前往调试。

工作学习效率低怎么办?怎样集中注意力?保持专注的10个方法

工作学习效率低怎么办?怎样集中注意力?保持专注的10个方法

无论是工作,还是学习,有的时候,我们难以保持专注,容易受到周围事物的干扰而打断,注意力涣散,效率低下,浪费了我们大量的时间和精力。

注意力集中,学习工作的效率高,别人花8个小时才能完成的工作,你可能4个小时就干完了,这就相当于你比其他人多了半天的时间。

注意力涣散,效率低下,别人8个小时能完成的工作,你却要花上10个小时,加班加点也就成为了常态。

工作学习效率低怎么办?怎样集中注意力?保持专注的10个方法。

为什么会失去注意力,无法专注

首先,我们先来思考两个问题:

为什么会失去注意力?

一种情况是: 做的事情太简单,大脑没有得到充分的利用,大部分的大脑"带宽"闲置下来,就导致了大脑走神的情况。

还有一种情况是: 做一些对自己来说太难的事情,或者说大脑处于一种疲劳的状态,无法有效地处理信息,从而无法集中注意力。

为什么我们在玩游戏的时候不容易走神?

因为我们对游戏感兴趣,游戏中有目标,有奖励,有惩罚,有及时反馈等等,下面的部分,我们来一一开始展开。

如何高度集中注意力

良好的工作环境,远离干扰源

想要提升效率,好环境的作用不言而喻。这其中包括了安静,无噪音的场景,明亮的光线,通风,干净,整洁的办公环境。

最好是有一个自己独立的,专门工作的小房间,不容易让周围走来走去的人群吸引你的注意力,不容易让自己受到周围环境的干扰。

远离对你工作和学习产生干扰的源头。关闭即时通讯软件,关闭社交媒体,关闭推送和提醒,减少干扰。

实际上,你浪费的不仅仅是看手机一刹那的时间,还有反复切换转入工作状态的时间。

另外,我们还需要学会对身边的一些干扰说“不”,学会延后处理一些不紧急不重要的事情。

调整自己的状态

深呼吸,自我暗示,2分钟冥想。

工作,学习前,我们可以通过深呼吸,放松全身,来调整我们身体的状态。

通过积极的自我暗示,来调节自己的情绪和心理状态。

通过冥想,明确目标,来加强自身的目标感,以及行动力。

确立目标,结果导向

学习和工作前,提前给自己制定好目标,制定需要完成的任务,所要达成的指标。

没有目标,我们就不知道需要做到什么程度,做事情没有标准,就容易懈怠,有了目标就有了动力,更容易让大脑保持专注。

这个执行的目标不需要太长远,要短期可以量化。比如这25分钟的时间,我需要完成什么工作,做到何种程度。

切换工作和学习的输入方式

换一种输入的方式,比如,把视觉输入转化为听觉输入。

当你上了一天班,感觉大脑和眼睛都比较累的时候,可以通过听的方式,语音朗读的方式来学习一些东西。

不同的人对不同感觉器官的敏感度不同,有些书,当你看不下去的时候,可能通过听的方式反而可以很好地理解。

另外,有时候,我们还可以通过大声朗读进行输入,换一种方式输入说不定的你的效率反而更高。

增加难度,加快速度,发挥潜能

在听课,学习一些视频课程的时候,对于一些你比较了解的内容,你可能会感到无聊,听着听着大脑就容易走神。因为这些你已经理解的内容,再也无法勾起你大脑的全部注意力。

这个时候,增加一定的难度,或者调用多重感官一起来学习,反而能够让大脑更好地集中注意力,起到提升效率的作用。

实际上,大部分人能够理解的速度远远超过讲的速度,听课学习,其实是比较慢的。讲话速度远远低于我们大脑思考的速度,没有足够的兴奋点,就容易走神。

因此,我们可以加快阅读的速度,或者说对于一些讲座和课程,加倍速度播放,这样,既集中了注意力,也让我们节省了时间。

专注力的训练

专注力就像肌肉,训练的多了,就会更加地强壮。平时,我们可以通过舒尔特表的训练来提升自己的注意力。

舒尔特表是一种NxN的表格,每个小方格里面都有随机的数字。

比如5x5表格,每个小方格里面就有1-25中的随机数字。我们需要做的是按照顺序把数字一个个地找出来,注意不要移动头部,眼睛集中在表格的中心区域。

当你的速度越来越快的时候,说明注意力的集中能力越来越好。后面我们可以通过增加方格的个数,来增加难度。

专注做好一件事,番茄工作法

大多数人总是高估自己,但是却没有多任务处理的能力,不断切换任务,不断消耗注意力。

单任务处理,做好一件事情,再做另一件事情。用好番茄工作法,每隔25分钟为一个周期,专注做好一件事情。

培养自己对当前事物的兴趣

培养良好的感觉,挖掘事情的好处,痛点,激发自己内心的欲望。

很多人一看书,注意力就涣散,容易走神,甚至想睡觉。就是因为对要做的事情没有产生足够的兴趣和欲望,自然就无法集中注意力。

做事情需要有激情,善于激发自己的欲望。学会把完成这件事跟自己的欲望,未来,目标联系起来。

良好的作息,劳逸结合

每隔一段时间休息一下,有助于放松大脑,缓解疲劳,太累了就无法集

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.