JavaScript进阶
1.1.1 三大内容
高阶函数 & 函数抽象:
重点
1.重点关注javasctipt的原始类型和引用类型,思考此设计影响的浅拷贝/深拷贝,可变性/不可变性概念。
2.理解纯函数、高阶函数、函数复用的相关知识、DRY、提升代码复用率。
应用
1.Shallow Copy => React 性能优化
2.Compose => Redux Middleware
异步编程模式:
重点
1.思考和理解EventLoop中个操作的执行顺序,特别是宏队列和微任务队列的执行顺序。
2.理解处理异步操作的演变历史,以及Promise、Async/Await 的原理。
应用
1.MircoTasks => Vue.$nextTick()
2.单线程 => Vue 依赖跟踪
javascript 设计模式:
重点
1.理解各个设计模式的使用场景。
2.对相似的设计模式(如观察者模式、订阅发布模式、中介者模式)能理解其异同点,打好基础,使后面学习框架的时候更加轻松自然。
应用
1.订阅发布模式 => Vuex
2.中间件模式 => Koa
2.函数
2.1 javaScript 内存管理
#2.1.1 js 内存机制
#🍅 内存空间:栈内存(stack)、堆内存(heap)
- 栈内存:所有原始数据类型都存储在栈内存中,如果删除一个栈原始数据,遵循先进后出;如下图:a 最先进栈,最后出栈。
- 堆内存:引用数据类型会在堆内存中开辟一个空间,并且会有一个十六进制的内存地址,在栈内存中声明的变量的值就是十六进制的内存地址。
函数也是引用数据类型,我们定一个函数的时候,会在堆内存中开辟空间,会以字符串的形式存储到堆内存中去,如下图:
function fn() {
var i = 10
var j = 10
console.log(i + j)
}
// 我们直接打印fn会出现一段字符串
console.log(fn)
// 打印结果
/*
f fn() {
var i=10;
var j=10;
console.log(i+j)
}
*/
// 加上括号才执行里面的代码
fn() // 20
#2.1.2 垃圾回收
#🍅 概念:(我们平时创建所有的数据类型都需要内存)
所谓的垃圾回收就是找出那些不再继续使用的变量,然后释放出其所占用的内存,垃圾回收会按照固定的时间间隔周期性的执行这一操作。
#🍅 javaScript 使用的垃圾回收机制来自动管理内存,垃圾回收是把双刃剑;垃圾回收是不可见的
- 优势:可以大幅简化程序的内存管理代码,降低程序员的负担,减少因长时间运转而带来的内存泄漏问题。
- 不足:程序员无法掌控内存,javascript 没有暴露任何关于内存的 api,无法强迫进行垃圾回收,无法干预内存管理。
#🍅 垃圾回收的方式
-
引用计数(reference counting)
跟踪记录每个值被引用的次数,如果一个值引用次数是 0,就表示这个值不再用到了,因此可以将这块内存释放
原理:每次引用加 1,被释放减 1,当这个值的引用次数变成 0 时,就将其内存空间释放。
let obj = { a: 10 } // 引用+1
let obj1 = { a: 10 } // 引用+1
obj = {} //引用减1
obj1 = null //引用为0
引用计数的 bug:循环引用
// ie8较早的浏览器,现在浏览器不会出现这个问题
function Fn() {
var objA = { a: 10 }
var objB = { b: 10 }
objA.c = objB
objB.c = objA
}
- 标记清除(现代浏览采用标记清除的方式)
#🍅 概念:
标记清除指的是当变量进入环境时,这个变量标记为“进入环境”;而当变量离开环境时,则将其标记为“离开环境”,最后垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间(所谓的环境就是执行环境)
#🍅 全局执行环境
- 最外围的执行环境
- 根据宿主环境的不同表示的执行环境的对象也不一样,在浏览器中全局执行环境被认为是 window 对象
- 全局变量和函数都是作为 window 对象的属性和方法创建的
- 某个执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数定义也随之销毁(全局执行环境只有当关闭网页的时候才会被销毁)
#🍅 环境栈(局部)
- 每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境,ECMAScript 程序中的执行流正是由这个方便的机制控制着
function foo (){
var a = 10 // 被标记进入执行环境
var b = ‘hello’ // 被标记进入执行环境
}
foo() //执行完毕,a 和 b 被标记离开执行环境,内存被回收
#2.1.3 V8 内存管理机制
#🍅 V8 引擎限制内存的原因
- V8 最初为浏览器设计,不太可能遇到大量内存的使用场景(表层原因)
- 防止因为垃圾回收所导致的线程暂停执行的时间过长(深层原因,按照官方的说法以 1.5G 的垃圾回收为例,v8 做一次小的垃圾回收需要 50 毫秒以上,做一次非增量的垃圾回收需要 1 秒以上,这里的时间是指 javascript 线程暂停执行的时间,这是不可接受的, v8 直接限制了内存的大小,如果说在 node.js 中操作大内存的对象,可以通过去修改设置去完成,或者是避开这种限制,1.7g 是在 v8 引擎方面做的限制,我们可以使用 buffer 对象,而 buffer 对象的内存分配是在 c++层面进行的,c++的内存不受 v8 的限制)
#🍅 V8 回收策略
- v8 采用可一种分代回收的策略,将内存分为两个生代;新生代和老生代
- v8 分别对新生代和老生代使用不同的回收算法来提升垃圾回收效率
#🍅 新生代垃圾回收
from 和 to 组成一个Semispace(半空间)当我们分配对象时,先在 from 对象中进行分配,当垃圾回收运行时先检查 from 中的对象,当obj2需要回收时将其留在 from 空间,而ob1分配到 to 空间,然后进行反转,将 from 空间和 to 空间进行互换,进行垃圾 回收时,将 to 空间的内存进行释放,简而言之 from 空间存放不被释放的对象,to 空间存放被释放的对象,当垃圾回收时将 to 空间的对象全部进行回收
#🍅 新生代对象的晋升(新生代中用来存放,生命较短的对象,老生代存放生命较长的对象)
- 在新生代垃圾回收的过程中,当一个对象经过多次复制后依然存活,它将会被认为是生命周期较长的对象,随后会被移动到老生代中,采取新的算法进行管理
- 在 From 空间和 To 空间进行反转的过程中,如果 To 空间中的使用量已经超过了 25%,那么就将 From 中的对象直接晋升到老生代内存空间中
#🍅 老生代垃圾回收(有 2 种回收方法)
- 老生代内存空间是一个连续的结构
- 标记清除(Mark Sweep) Mark Sweep 是将需要被回收的对象进行标记,在垃圾回收运行时直接释放相应的地址空间,红色的区域就是需要被回收的
- 标记合并(Mark Compact) Mark Compact 将存活的对象移动到一边,将需要被回收的对象移动到另一边,然后对需要被回收的对象区域进行整体的垃圾回收
2.2 如何保证你的代码质量
#2.2.2 单元测试 (写一段代码去验证另一段代码,检测的对象可以是样式、功能、组件等)
#🍅 概念
- 测试一种验证我们的代码是否可以按预期工作的方法
- 单元测试是对软件中的最小可测试单元进行检测和验证
#🍅 前端单元测试的意义
- 检测出潜在的 bug
- 快速反馈功能输出,验证代码是否达到预期
- 保证代码重构的安全性
- 方便协作开发
#🍅 单元测试代码
- 案例 1
let add = (a, b) => a + b //被测试的方法
let result = add(1, 2)
// 写的测试的代码
let expect = 4
if (result !== expect) {
throw new Error(`1+2应该等于${expect},但结果却是${result}`)
}
// 最后输出:Uncaught Error: 1+2应该等于4,但结果却是3
- 案例 2
//被测试的方法
let add = (a, b) => a + b
// 写的测试的代码
let expect = res => {
return {
toBe: actual => {
if (res !== actual) {
throw new Error(`预期值和实际值不符`)
}
}
}
}
// expect(add(1,2)).toBe(4)
let test = (desc, fn) => {
try {
fn()
console.log(`${desc}通过`)
} catch (err) {
console.log(`${desc}没有通过`)
}
}
test('加法测试', () => {
expect(add(1, 2)).toBe(3)
})
// 最后输出:加法测试通过
#2.2.1 jest 的基础使用(facebook 的一套测试 javascript 的框架)
#🍅 安装
- 安装 node
- yarn add -D jest
- 查看是否安装成功 npm ls jest
#🍅 jest 的基础使用
- 创建一个文件夹,然后 npm init -y,然后下载 jest:yarn add -D jest
- 在文件夹下创建 math.js,这个文件是写被测试的代码;如下:
let add = (a, b) => a + b
module.exports = {
add
}
- 在文件夹下创建 math.test.js,这个文件写测试代码;如下。
const { add } = require('./math')
test('加法测试', () => {
expect(add(1, 2)).toBe(3)
})
- 配置 package.json 里的 script 脚本
"scripts": {
"test": "jest"
}
- 执行 npm test,测试成功会出现以下信息
PASS ./math.test.js
✓ 加法测试 (3ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 0.98s
Ran all test suites.
2.3 提高代码的可靠性
#2.3.1 函数式编程
含义:函数式编程是一种编程范式,是一种构建计算机程序结构和元素的风格,它把计算看作是对数据函数的评估,避免了状态的变化和数据的可变。
将我们的程序分解为一些更可复用,更可靠且更易于理解的部分,然后在将他们组合起来,形成一个更易推理的程序整体。
- 案例 1:对一个数组每项加+1
// 初级程序员
let arr = [1, 2, 3, 4]
let newArr = []
for (var i = 0; i < arr.length; i++) {
newArr.push(arr[i] + 1)
}
console.log(newArr) //[2, 3, 4, 5]
// 函数式编程
let arr = [1, 2, 3, 4]
let newArr = (arr, fn) => {
let res = []
for (var i = 0; i < arr.length; i++) {
res.push(fn(arr[i]))
}
return res
}
let add = item => item + 1 //每项加1
let multi = item => item * 5 //每项乘5
let sum = newArr(arr, add)
let product = newArr(arr, multi)
console.log(sum, product) // [2, 3, 4, 5] [5, 10, 15, 20]
#2.3.2 纯函数
含义:如果函数的调用参数相同,则永远返回相同的结果。它不依赖于程序执行期间函数外部任何状态或数据的变化,必须只依赖于其输入的参数(相同的输入,必须得到相同的输出)。
// 纯函数
const calculatePrice=(price,discount)=> price * discount
let price = calculatePrice(200,0,8)
console.log(price)
// 不纯函数
const calculatePrice=(price,discount)=>{
const dt= new Date().toISOString()
console.log(`${dt}:${something}`)
return something
}
foo('hello')
#2.3.3 函数副作用
-
当调用函数时,除了返回函数值外,还对注调用函数产生附加的影响
-
例如修改全局变量(函数外的变量)或修改参数
//函数外a被改变,这就是函数的副作用 let a = 5 let foo = () => (a = a * 10) foo() console.log(a) // 50 let arr = [1, 2, 3, 4, 5, 6] arr.slice(1, 3) //纯函数,返回[2,3],原数组不改变 arr.splice(1, 3) // 非纯函数,返回[2,3,4],原数组被改变 arr.pop() // 非纯函数,返回6,原数组改变//通过依赖注入,对函数进行改进,所谓的依赖注入就是把不纯的部分作为参数传入,把不纯的代码提取出来;远离父函数;同时这么做不是为了消除副作用 //主要是为了控制不确定性 const foo = (d, log, something) => { const dt = d.toISOString() return log(`${dt}:${something}}`) } const something = '你好' const d = new Date() const log = console.log.bind(console) foo(d, log, something)#2.3.4 函数副作用可变性和不可变性
- 可变性是指一个变量创建以后可以任意修改
- 不可变性指一个变量,一旦被创建,就永远不会发生改变,不可变性是函数式编程的核心概念
// javascript中的对象都是引用类型,可变性使程序具有不确定性,调用函数foo后,我们的对象就发生了改变;这就是可变性,js中没有原生的不可变性 let data = { count: 1 } let foo = data => { data.count = 3 } console.log(data.count) // 1 foo(data) console.log(data.count) // 3 // 改进后使我们的数据具有不可变性 let data = { count: 1 } let foo = data => { let lily = JSON.parse(JSON.stringify(data)) // let lily= {...data} 使用扩展运算符去做拷贝,只能拷贝第一层 lily.count = 3 } console.log(data.count) // 1 foo(data) console.log(data.count) // 1
2.4 compose 函数 pipe 函数
#2.4.1 compose 函数
#🍅 含义:
- 将需要嵌套执行的函数平铺
- 嵌套执行指的是一个函数的返回值将作为另一个函数的参数
#🍅 作用:
实现函数式编程中的 Pointfree,使我们专注于转换而不是数据(Pointfree 不使用所有处理的值,只合成运算过程,即我们所指的无参数分割)
#🍅 案例
计算一个数加 10 在乘以 10
// 一般会这么做
let calculate => x => (x+10) * 10
console.log(calculate(10))
// 用compose函数实现
let add = x => x + 10
let multiply = y => y * 10
console.log(multiply(add(10)))
let compose = function() {
let args = [].slice.call(arguments)
return function(x) {
return args.reduceRight(function(total, current) {
//从右往左执行args里的函数
console.log(total, current)
return current(total)
}, x)
}
}
let calculate = compose(multiply, add)
console.log(calculate, calculate(10)) // 200
// 用es6实现
const compose = (...args) => x => args.reduceRight((res, cb) => cb(res), x)
#2.4.2 pipe 函数
pipe 函数 compose 类似,只不过从左往右执行
2.5 高阶函数
#🍅 含义:
- 高阶函数是对其他函数进行操作的函数,可以将它们作为参数或返回它们
- 简单来说,高阶函数是一个函数,它接收函数作为参数或将函数作为输出返回
#🍅 map/reduce/filter
// 用redece做累加
let arr = [1, 2, 3, 4, 5]
let sum = arr.reduce((pre, cur) => {
return pre + cur
}, 10)
console.log(sum) //20
// 用redece做去重
let arr = [1, 2, 3, 4, 5, 3, 3, 4]
let newArr = arr.reduce((pre, cur) => {
pre.indexOf(cur) === -1 && pre.push(cur)
return pre
}, [])
console.log(newArr) //[1, 2, 3, 4, 5]
#🍅 flat
let arr = [[1, 2, 3, [23, 3, [1, 2]]]]
let arr1 = arr.flat(Infinity) // 多维转一维数组
let arr2 = arr.flat(2) // // 多维转二维数组,默认值是1
console.log(arr1, arr2) // [1, 2, 3, 23, 3, 1, 2] [1, 2, 3, 23, 3, Array(2)]
#🍅 高阶函数的意义
- 参数为函数的高阶函数
// 参数为函数的高阶函数
function foo(f) {
// 判断是否为函数
if (typeof f === 'function') {
f()
}
}
foo(function() {})
- 返回值为函数的高阶函数
// 回值为函数的高阶函数
function foo (f){
rerutn function(){}
}
foo()
- 高阶函数的实际作用
let callback = value => {
console.log(value)
}
let foo = (value, fn) => {
if (typeof fn === 'function') {
fn(value)
}
}
foo('hello', callback)
2.6 常用函数
#2.6.1 memozition(缓存函数)
含义:缓存函数是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据
let add = (a, b) => a + b
// 假设memoize函数可以实现缓存
let calculate = memoize(add)
calculate(10, 20) // 30
calculate(10, 20) // 相同的参数,第二次调用是,从缓存中取出数据,而并非重新计算一次
#🍅 实现原理:把参数和对应的结果数据存到一个对象中去,调用时,判断参数对应的数据是否存在,存在就返回对应的结果数据
// 缓存函数
let memoize = function(func) {
let cache = {}
return function(key) {
if (!cache[key] || (typeof cache[key] === 'number' && !!cache[key])) {
cache[key] = func.apply(this, arguments)
}
return cache[key]
}
}
/*
*hasher也是个函数,是为了计算key,如果传入了hasher,就用hasher函数计算key;
否则就用memoize函数传入的第一个参数,接着就去判断如果这个key没有被求值过,就去执行,
最后我们将这个对象返回
*/
var memoize = function(func, hasher) {
var memoize = function(key) {
var cache = memoize.cache
var address = '' + (hasher ? hasher.apply(this, arguments) : key)
if (!cache[address] || (typeof cache[key] === 'number' && !!cache[key])) {
cache[address] = func.apply(this, arguments)
}
return cache[address]
}
memoize.cache = {}
return memoize
}
// 缓存函数可以是fei bo
#🍅 案例:求斐波那且数列
// 不用memoize的情况下,会执行453次
var count = 0
var fibonacci = function(n) {
count++
return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2)
}
for (var i = 0; i <= 10; i++) {
fibonacci(i) //453
}
console.log(count)
// 用memoize的情况下,会执行12次
var memoize = function(func, hasher) {
var memoize = function(key) {
var cache = memoize.cache
var address = '' + (hasher ? hasher.apply(this, arguments) : key)
if (!cache[address] || (typeof cache[key] === 'number' && !!cache[key])) {
cache[address] = func.apply(this, arguments)
}
return cache[address]
}
memoize.cache = {}
return memoize
}
fibonacci = memoize(fibonacci)
for (var i = 0; i <= 10; i++) {
fibonacci(i) //453 12
}
//缓存函数能应付大量重复计算,或者大量依赖之前的结果的运算场景
console.log(count)
#2.6.2 curry(柯里化函数)
含义:在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一些列使用一个参数的函数技术(把接受多个参数的函数转换成几个单一参数的函数)
// 没有柯里化的函数
function girl(name,age,single) {
return `${name}${age}${single}`
}
girl('张三',180,'单身')
// 柯里化的函数
function girl(name) {
return function (age){
return function (single){
return `${name}${age}${single}`
}
}
}
girl('张三')(180)('单身')
#🍅 案例 1:检测字符串中是否有空格
// 封装函数去检测
let matching = (reg, str) => reg.test(str)
matching(/\s+/g, 'hello world') // true
matching(/\s+/g, 'abcdefg') // false
// 柯里化
let curry = reg => {
return str => {
return reg.test(str)
}
}
let hasSpace = curry(/\s+/g)
hasSpace('hello word') // true
hasSpace('abcdefg') // false
#🍅 案例 2:获取数组对象中的 age 的属性值
let persons = [
{ name: 'zs', age: 21 },
{ name: 'ls', age: 22 }
]
// 不柯里化
let getage = persons.map(item => {
return item.age
})
// 用loadsh的curry 来实现
const _ = require('loadsh')
let getProp = _.curry((key, obj) => {
return obj[key]
})
person.map(getProp('age'))
#🍅 柯里化这个概念实现本身就难,平时写代码很难用到,关键理解其思想
#2.6.3 偏函数
#🍅 比较:
- 柯里化是将一个多参数函数转换成多个单参数的函数,也就是将一个 n 元函数转换成 n 个一元函数
- 偏函数则固定一个函数的一个或多个参数,也就是将一个 n 元函数转换成一个 n-x 元的函数
- 柯里化:f(a,b,c)=f(a)(b)(c)
- 偏函数:f(a,b,c)=f(a,b)(c)
/*
用bind函数实现偏函数,bind的另一个用法使一个函数拥有预设的初始参数,将这些参数写在bind的第一个参数后,
当绑定函数调用时,会插入目标函数的初始位置,调用函数传入的参数会跟在bind传入的后面
*/
let add = (x, y) => x + y
let rst = add.bind(null, 1)
rst(2) //3
2.7 防抖和节流
#🍅 为什么防抖和节流?
我们使用窗口的 resize,scorll,mousemove,mousehover;输入框等校验时,如果事件处理函数调用无限制,会加剧浏览器的负担,尤其是执行了操作 DOM 的函数,那不仅造成计算资源的浪费,还会降低程序运行速度,甚至造成浏览的奔溃,影响用户体验。
#🍅 区别
- 防抖:就是触发多次事件,最后一次执行事件处理函数
- 节流:隔一段时间执行一次事件处理函数
#2.7.1 函数防抖(debounce)
含义:当持续触发事件时,一定时间段内没有触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时,‘函数防抖’的关键在于,在一个动作发生一定时间之后,才会执行特定的事件
#🍅 案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<style>
* {
margin: 0;
padding: 0;
}
#content {
width: 200px;
height: 200px;
line-height: 200px;
background-color: #ccc;
margin: 0 auto;
font-size: 60px;
text-align: center;
color: #000;
cursor: pointer;
}
</style>
</head>
<body>
<div id="content"></div>
<script>
/*
连续onmousemove在最后一次触发changeNum函数,
多余的处理函数的都会被clearTimeout掉
*/
let num=1
let oDiv= document.getElementById('content')
let changeNum=function () {
oDiv.innerHTML=num++
}
let deBounce = function (fn,delay){
let timer=null
return function (...args) {
if(timer) clearTimeout(timer)
timer = setTimeout(()=>{
fn(...args)
},delay)
}
}
oDiv.onmousemove=deBounce(changeNum,500)
// or
let _deBounce = deBounce(changeNum,500)
oDiv.onmousemove=function(){
_deBounce()
}
</script>
</body>
</html>
#🍅 underscore 库 debounce 源码
_.debounce = function(func, wait, immediate) {
var timeout, result
var later = function(context, args) {
timeout = null
if (args) result = func.apply(context, args)
}
var debounced = restArguments(function(args) {
if (timeout) clearTimeout(timeout)
if (immediate) {
var callNow = !timeout
timeout = setTimeout(later, wait)
if (callNow) result = func.apply(this, args)
} else {
timeout = _.delay(later, wait, this, args)
}
return result
})
debounced.cancel = function() {
clearTimeout(timeout)
timeout = null
}
return debounced
}
#2.7.2 函数节流(throttle)
含义:当持续触发事件时,保证一定时间段内只调用一次事件处理函数
#🍅 案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body></body>
<button>点击</button>
<script>
/*
* 连续点击只会1000执行一次btnClick函数
*/
let obutton = document.getElementsByTagName('button')[0]
// 如果用箭头函数,箭头函数没有arguments,也不能通过apply改变this指向
function btnClick() {
console.log('我响应了')
}
/*
方法1: 定时器方式实现
缺点:第一次触发事件不会立即执行fn,需要等delay间隔过后才会执行
*/
let throttle = (fn, delay) => {
let flag = false
return function(...args) {
if (flag) return
flag = true
setTimeout(() => {
fn(...args)
flag = false
}, delay)
}
}
/*
方法2:时间戳方式实现
缺点:最后一次触发回调与前一次的触发回调的时间差小于delay,则最后一次触发事件不会执行回调
*/
let throttle = (fn, delay) => {
let _start = Date.now()
return function(...args) {
let _now = Date.now(),
that = this
if (_now - _start > delay) {
fn.apply(that, args)
start = Date.now()
}
}
}
// 方法3:时间戳与定时器结合
let throttle = (fn, delay) => {
let _start = Date.now()
return function(...args) {
let _now = Date.now(),
that = this,
remainTime = delay - (_now - _start)
if (remainTime <= 0) {
fn.apply(that, args)
} else {
setTimeout(() => {
fn.apply(that, args)
}, remainTime)
}
}
}
/*
方法4:requestAnimationFrame实现
优点:由系统决定回调函数的执行机制,60Hz的刷新频率,每次刷新都会执行一次回调函数,不
会引起丢帧和卡顿
缺点:1.有兼容性问题2.时间间隔有系统决定
*/
let throttle = (fn, delay) => {
let flag
return function(...args) {
if (!flag) {
requestAnimationFrame(function() {
fn.apply(that, args)
flag = false
})
}
flag = true
}
}
obutton.onclick = throttle(btnClick, 1000)
</script>
</html>
#🍅 underscore 库 throttle 源码
_.throttle = function(func, wait, options) {
var timeout, context, args, result
var previous = 0
if (!options) options = {}
var later = function() {
previous = options.leading === false ? 0 : _.now()
timeout = null
result = func.apply(context, args)
if (!timeout) context = args = null
}
var throttled = function() {
var now = _.now()
if (!previous && options.leading === false) previous = now
var remaining = wait - (now - previous)
context = this
args = arguments
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
result = func.apply(context, args)
if (!timeout) context = args = null
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining)
}
return result
}
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = context = args = null
}
return throttled
}
#2.7.3 防抖使用场景
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<input type="text" />
<!-- 防抖场景 -->
<script>
// 防抖函数
let deBounce = (fn, delay) => {
let timer = null
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn(...args)
}, delay)
}
}
let oInput = document.getElementsByTagName('input')[0]
// 模拟请求
let ajax = message => {
let json = { message }
console.log(JSON.stringify(json))
}
let doAjax = deBounce(ajax, 200)
// 键盘弹起执行
oInput.addEventListener('keyup', e => {
doAjax(e.target.value)
})
</script>
</body>
</html>
2.8 深拷贝和浅拷贝
#2.8.1 深拷贝&浅拷贝
对于原始数据类型,并没有深浅拷贝的区别,深浅拷贝都是对于引用数据类型而言,如果我们要赋值对象的所有属性都是引用类型可以用浅拷贝
#🍅 浅拷贝:只复制一层对象,当对象的属性是引用类型时,实质复制的是其引用,当引用值发生改变时,也会跟着改变
#🍅 深拷贝:深拷贝是另外申请了一块内存,内容和原来一样,更改原对象,拷贝对象不会发生改变
#2.8.2 浅拷贝实现
#🍅 for in 遍历实现
let shallCopy => obj=>{
let rst={}
for(let key in obj){
//只复制本身的属性(非继承过来的属性)枚举属性
if(obj.hasOwnProperty(key)){
rst[key]=obj[key]
}
}
return rst
}
let start ={
name:'古力娜扎',
age:'22',
friend:{
name:'邓超'
}
}
let copyStart=shallCopy(start)
copyStart.name="热巴"
copyStart.friend.name='黄渤'
// 拷贝的第一层层如果是引用类型,拷贝的其实是一个指针,所以拷贝对象改变会影响原对象
console.log(start.name,opyStart.friend.name) //古力娜扎 黄渤
#🍅 Object.assign(target,source) (适用于对象)
可以把 n 个源对象拷贝到目标对象中去(拷贝的是可枚举属性)
let start = {
name: '古力娜扎',
age: '22',
friend: {
name: '邓超'
}
}
let returnedTarget = Object.assign({}, start)
#🍅 扩展运算符...
let start = { name: '刘亦菲' }
let newStart = { ...start }
newStart.name = '迪丽热巴'
console.log(start.name) // 刘亦菲
#🍅 slice(适用于数组)
let a = [1, 2, 3, 4]
let b = a.slice()
b[0] = 9
console.log(a) //[1,2,3,4]
#2.8.3 深拷贝实现
#🍅 JSON.parse(JSON.string(obj))
let obj = {
name: '小明',
dog: ['小花', '旺财']
}
let obj1 = JSON.parse(JSON.stringify(obj))
obj1.name = '小华'
obj1.dog[0] = '小白'
console.log(obj) // {name: "小明", dog: ['小花', '旺财']}
// 原数组并没有改变,说明实现了深拷贝
let richGirl = [
{
name: '开心',
car: ['宝马', '奔驰', '保时捷'],
deive: function() {},
age: undefined
}
]
let richBoy = JSON.parse(JSON.stringify(richGirl))
console.log(richBoy)
/*
1. 当属性值为undefined或函数,则序列化的结果会把函数或 undefined丢失
2. 对象中存在循环引用的情况也无法正确实现深拷贝
3. Symbol,不能被JSON序列化
4. RegExp、Error对象,JSON序列化的结果将只得到空对象
5. 会丢失对象原型
*/
#🍅 递归实现深拷贝
let deepClone = obj => {
let newObj = Array.isArray(obj) ? [] : {}
if (obj && typeof obj === 'object') {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] && typeof obj[key] === 'object') {
newObj[key] = deepClone(obj[key])
} else {
// 如果不是对象直接拷贝
newObj[key] = obj[key]
}
}
}
}
return newObj
}
let richGirl = {
name: '开心',
car: ['宝马', '奔驰', '保时捷'],
deive: function() {},
age: undefined
}
let richBoy = deepClone(richGirl)
richBoy.deive = '渣男开大G'
richBoy.name = '小明'
richBoy.car = ['哈罗单车', '膜拜']
richBoy.age = 20
console.log(richGirl)
console.log(richBoy)
#2.8.4 第三方库实现拷贝
#🍅 lodash
//cloneDeep: 深拷贝 clone:浅拷贝,此例子介绍深拷贝
const _ = require('lodash') //全部引入
const cloneDeep = require('lodash/cloneDeep') //引入单个方法,用的方法少建议用这种方式引入
let obj = {
name: '开心',
car: ['宝马', '奔驰', '保时捷'],
deive: function() {},
age: undefined
}
const newObj = cloneDeep(obj)
newObj.name = '不开心'
newObj.car[0] = '自行车'
console.log(obj, newObj) // 原对象不会改变
3.1 理解异步
#3.1.1 同步与异步
#🍅 先看 2 段代码
//代码1
const test = () => {
let t = +new Date()
while (true) {
if (+new Date() - t >= 2000) {
break
}
}
}
console.log(1)
test()
console.log(2)
console.log(3)
// 执行结果 1 2 3
// 代码2
console.log(1)
setTimeout(() => {
console.log(2)
}, 2000)
console.log(3)
// 执行结果 1 3 2
代码 1 都是同步任务,代码 2 有异步任务 setTimeout;所有执行结果不同
同步:主线程上排队执行的任务,只有前面的任务执行完,才能执行后面的任务
异步:是指不进入主线程,而是进入任务队列的任务,只有任务队列通知主线程,某个 任务可以执行了,该任务才会进入主线程执行
#🍅 单线程的 js 怎么实现异步?
如果按照 js 是单线程的,上面的代码 2 应该是 123 才符合单线程的表现;现在为什么是 132 呢,那异步怎么实现的呢?
#🍅 学习异步之前先了解一下什么是进程?什么是线程?
打个比方:假如 cpu 是一个工厂,进程就是一个车间;线程就是工人,一个进程有多个线程,它们之间资源共享,也就内存空间共享。
linux 下 查看进程:
ps (process status) 列出系统中当前运行进程的快照
top (table of processes) 动态实时查看进程
kill 9 进程 pid 杀死进程
#🍅 回答单线程的 js 怎么实现异步?
通过浏览器的内核多线程实现异步
#3.1.2 javaScript 单线程
#🍅 浏览器是一个多进程的架构
我们打开一个浏览器就会启动以下进程,我们所要关心的是渲染进程,渲染进程是浏览器的核心进程
#🍅 渲染进程下的多线程
GUI 线程:负责渲染页面,解析 html、css;构建 DOM 树和渲染树
js 引擎线程: js 引擎线程负责解析和执行 js 程序,我们经常听到的 chrome 的 v8 引擎就是跑在 js 引擎线程上的,js 引擎线程只有一个,所有说 js 的单线程语言的原因,那其实语言没有单线程多线程之说,因为解释这个语言的是 的线程是单线程;js 引擎线程与 gui 线程互斥,当浏览器执行 javaScript 程序的时候,GUI 渲染线层会保存在一个队列当中;直到 js 程序执行完成,才会接着执行;如果 js 的执行时间过长,会影响页面的渲染不连贯,所有我们要尽量控制 js 的大小
定时触发线程:为什么 setTimeout 不阻塞后面程序的运行,那其实 setTimeout 不是由 js 引擎线程完成的,是由定时器触发线程完成的,所以它们可以是同时进行的,那么定时器触发线程在这定时任务完成之后会通知事件触发线程往任务队列里添加事件
事件触发线程:将满足触发条件的事件放入任务队列,一些异步的事件会放到异步队列中
异步 HTTP 请求线程:用与处理 ajax 请求的,当请求完成时如果有回调函数就通知事件触发线程往任务队列中添加任务
#🍅 异步场景
- 定时器
- 网络请求
- 事件绑定
- ES6 Promise
#3.1.3 定时器
#🍅 定时器的执行过程
代码在执行栈中执行,然后 1=>2=>3=>4
#🍅 定时器示例
执行过程解释:
console.log(1)先入栈执行,执行完出栈- 遇到
setTimeout调用 setTimeout 这个 webapi,通知定时触发线程定时 2 秒钟 console.log(3)入栈执行,执行完出栈- 栈中已经空了,去检查任务队列,此时还为到 2 秒钟,任务队列中还没有任务;这是一个循环检查的过程,等到 2 秒钟后 事件触发线程往任务队列中添加了定时器的事件,这时候再去检查的时候已经有了定时器的异步任务,我们取出这个任务放到执行栈中执行,这时候打印出了 2
#🍅 定时器会带来的问题
定时任务可能不能按时执行
const test = () => {
let t = +new Date()
while (true) {
if (+new Date() - t >= 5000) {
break
}
}
}
setTimeout(() => {
console.log(2)
}, 2000)
test()
// 等到5秒钟后才打印出了2
为什么呢?
因为 test 是会耗时 5 秒钟的同步任务,异步任务只能等待同步任务执行完之后才能执行,也就是说只能等 5 秒钟后才能检查的任务队列里的任务。
- 定时器嵌套 5 次之后最小间隔不能低于 4ms
#🍅 定时器的应用场景
- 防抖节流
- 到计时
- 动画 (有丢帧问题)
#3.2 Event Loop 机制
以前 js 是在浏览器环境中运行,由于 chrome 对 v8 做了开源;所以 js 有机会在服务端运行;浏览器和 node 都是 js 的运行环境,它们相当于是一个宿主,宿主能提供一个能力能帮助 js 实现 Event Loop
#🍅 js 单线程问题
所有任务都在一个线程上完成,一旦遇到大量任务或遇到一个耗时的任务,网页就可能出现假死,也无法响应用户的行为
#🍅 Event Loop 是什么
Event Loop 是一个程序结构,用于等待和发送信息的事件。 简单说就是在程序中设置 2 个线程,一个负责程序本身的运行,称为“主线程”;另一个负责主线程和其他进程(主要是各种 I/O 操作)的通信 被称为“Event Loop 线程”(也可以翻译为消息线层)
js 就是采用了这种机制,来解决单线程带来的问题。
3.2.1 浏览器的 Event Loop
#🍅 异步实现
- 宏观:浏览器多线程(从宏观来看是多线程实现了异步)
- 微观:Event Loop,事件循环(Event Loop 翻译是事件循环,是实现异步的一种机制)
#🍅 先看一个例子
console.log(1)
setTimeout(function() {
console.log(2)
}, 0)
Promise.resolve().then(function() {
console.log(3)
})
console.log(4)
// 1 4 3 2
1 和 4 是同步任务肯定是最先执行,现在要看异步任务,现在要看的是
promise的回调为什么在定时器前面执行,那为什么promise后放入,为什么先执行呢?那是因为 Event Loop 的机制是有微任务的说法的。
#🍅 宏任务(普通任务)和微任务
宏任务(task) :
- script:script 整体代码
- setImmediate:node 的一个方法
- setTimeout 和 setInterval
- requestAnimationFrame
- I/O
- UI rendering
......
微任务(microtask) :
- Object.observe:监听对象变化的一个方法
- MutationObserver:可以监听 Dom 结构变化的一个 api
- postMessgae:window 对象通信的一个方法
- Promise.then catch finally
#🍅 Event Loop 的运行过程
线程都有自己的数据存储空间,上图可以看见
堆和栈,堆的空间比较大,所以存储一些对象;栈的空间比较小, 所以存储一些基础数据类型、对象的引用、函数的调用;函数调用就入栈,执行完函数体里的代码就自动从栈中移除这个函数,这就是我们所说的调用栈; 栈是一个先进后出的数据结构,当最里面的函数出栈的时候,这个栈就空了;当我们调用时候会调用一些异步函数, 这个异步函数会找他们的异步处理模块,这个异步模块包括定时器、promise、ajax 等,异步处理模块会找它们各自 对应的线程,线程向任务队列中添加事件,看我们的蓝色箭头,表示在任务队列中添加事件,橘色的箭头是从任务队列中取事件,取出这个事件去执行对应的回调函数;
有 3 个点要注意
- 我们整个大的 script 的执行是全局任务也是一个宏任务的范畴,
- 当宏任务执行完,会去执行所有的微任务,
- 微任务全部执行完在去执行下一个宏任务,那什么时候去执行一个微任务呢,是等调用栈为空的时候, 调用栈不为空的时候,任务队列的微任务一直等待;微任务执行完又去取任务队列里的宏任务,去依次 执行宏任务,执行宏任务的时候就要检查当前有没有微任务,如果有微任务就去执行完所有微任务,然后 再去执行后续的宏任务
#🍅 代码示例 1
执行步骤:
- 大的 script 是个宏任务,检查任务队列是否为空,当前不为空,然后执行 1 和 8 行代码;那么打印出了 1 和 4
- 执行完 1 和 8 行代码后去检查微任务队列,微任务队列不为空,执行 Promise 的回调,此时打印出了 3
- 执行完 Promise 的回调后,在检查微任务队列,现在微任务队列为空,进行重新渲染一便
- 在去检查任务队列,现在任务队列中有了定时器的事件,又打印出了 2
注意点:
- 一个 Event Loop 有一个或多个 task queue(任务队列)
- 每个个 Event Loop 有一个 microtask queue(微任务队列)
- requestAnimationFrame 不在任务队列也不在为任务队列,是在渲染阶段执行的
- 任务需要多次事件循环才能执行完,微任务是一次性执行完的
- 主程序和和 settimeout 都是宏任务,一个 promise 是微任务,第一个宏任务(主程序)执行完,执行全部的微任务(一个 promise),再执行下一个宏任务(settimeout)
#🍅 代码示例 2
console.log('start')
setTimeout(() => {
console.log('setTimeout')
new Promise(resolve => {
console.log('promise inner1')
resolve()
}).then(() => {
console.log('promise then1')
})
}, 0)
new Promise(resolve => {
console.log('promise inner2') //同步执行的
resolve()
}).then(() => {
console.log('promise then2')
})
// 打应结果
/*
start
promise inner2
promise then2
setTimeout
promise inner1
promise then1
*/
#🍅 代码示例 3
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
return Promise.resolve().then(_ => {
console.log('async2 promise')
})
}
console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)
async1()
new Promise(function(resolve) {
console.log('promise1')
resolve()
}).then(function() {
console.log('promise2')
})
/*
start
async1 start
promise1
async2 promise
promise2
async1 end
setTimeout
*/
执行步骤:
- 先执行主线程的同步任务(也是宏任务), 先执行
start然后遇到了 setTimeout,把它放到下一次宏任务中执行,我们叫它宏 2;然后调用 async1()函数,执行了async1 start;又调用了 async2 函数执行 Promise.resolve().then;由于 then 的回调函数是微任务,就把它放到微任务队列中,我们叫它微 1;遇见 await 是等待的意思,需要把第一轮的微任务执行完,在执行 await 下面的内容,我们在执行 new Promise(),打印了promise1,又调用了 resolve()改变了 promise 状态,这个 then 的回调我们叫它微 2;第一轮宏任务执行完毕。 - 第一轮宏热任务执行完毕后,我们检查微任务队列中的微任务,把它全部执行完,就打印了
async2 promise、promise2 - 第一轮微任务执行完,就执行 await 后面的内容
async1 end - await 后面的内容执行完后又执行宏任务
setTimeout
#🍅 代码示例 4
setTimeout(() => {
console.log(1)
new Promise(r => {
r()
}).then(res => {
console.log(2)
Promise.resolve().then(() => {
console.log(3)
})
})
})
setTimeout(() => {
console.log(4)
new Promise(r => {
r(1)
}).then(res => {
console.log(5)
})
})
- 先执行 script 代码,遇到第一个 setTimeout,放到宏任务队列中我们叫它宏 1,遇到第二个 setTimeout,放到宏任务队列中我们叫它宏 2。
- 检查微任务队列中有无任务,有就全部执行,本次宏任务下没有产生微任务,没有就拿出宏 1 进行执行,执行宏 1 打印
1;然后遇到 promise 就把 then 的回调放到微任务队列;宏 1 执行完以后,就去检查微任务队列中有任务么,有就全部执行,所以我们就执行了 2,在 then 的回调中又产生了微任务,所以我们又执行了 3,记住一点,要把本次宏任务下所产生的微任务全部执行完才会执行下一个宏任务,记住是产生的,没有产生的不会执行。 - 一轮宏任务执行完以后,再去执行下一个宏任务,就会去执行宏 2,就打印了 4,在检查有没有产生了微任务,如果微任务队列中有微任务就全部执行,有微任务所以打印了 5。
#3.2.2 node.js 的 Event Loop
#🍅 node.js 架构图
有 3 层组成
- 第一层:node-core 是 node.js api 的核心库。
- 第二层:包装和暴露 libuv 和 js 的其他低级功能。
- 第三层:v8 和 libuv 这个库属于第三层的东西,v8 引擎是 chrome 开源的 js 引擎,也是 js 运行服务端的基础,libuv 是第三方的库,是 nodejs 异步编程的基础,是 node 底层的 IO 引擎,是 c 语言编写的事件驱动的库;负责 node api 的执行,它会将不通的任务分配给不同的线程,从而形成了 Event Loop 事件循环;它以异步的方式将任务的执行结果返回给 v8 引擎;那我们说 node 是非阻塞 IO 单线程,实现这个非阻塞的原因就在与 libuv 这个库,node.js 的 Event Loop 都是这个库实现的。
#🍅 node.js 的 Event Loop
执行的几个阶段
- timers 阶段:执行 timers 的回调,也是执行 setTimeout 和 setInterval 的回调
- pending IO callbacks:系统操作的回调
- idle,pepare:内部使用
- poll:等待新的 I/O 事件进来
- check:执行 setImmediate 回调
- close callbacks:内部使用
只需关注 1、4、5 阶段
每个阶段都有一个 callbacks 的先进先出的队列需要执行,当 event loop 运行到一个指定阶段时,该阶段的 fifo 队列将会被执行,当队列 callback 执行完或者执行的 callbacks 数量超过该阶段的上限时,event loop 会转入下一个阶段。
#🍅 poll 阶段
Poll 阶段主要有 2 个功能:
-
计算应该被 block 多久(等待 IO 的操作)
-
处理 poll 队列的事件
- . 先判断 poll 队列是否空或受到限制,否的话执行 poll 队列的 callback;循环执行,直到空为止;或者到了限制。
- . 如果 poll 队列是空而且受到限制,检查
setImmedidate有没有设置 callback,如果有设置就进入 check 阶段;如果没有设置 callback,就等待 callback 加入 poll 队列,如果这时候有 callback 加入到 poll 队列,那么这时又进入的 poll 队列;在 poll 阶段空闲的时候,Event loop 会检查 timer(定时器)是否到时间;假如到时间了,又 🈶️ 对应的 callback,那么它就会进去 timer 阶段;如果没到时间还是等待 callback 加入 poll 队列。
#🍅 案例 1
const fs = require('fs')
function someAsyncOperation(callback) {
// nodejs 一般没有加sync都是异步的
fs.readFile(__dirname, callback)
}
const timeoutScheduled = Date.now()
setTimeout(() => {
const delay = Date.now() - timeoutScheduled
// 计算多延迟多少毫秒打印这一句
console.log(`${delay}ms have passed since I was scheduled`)
}, 100)
someAsyncOperation(() => {
const startCallback = Date.now()
while (Date.now() - startCallback < 200) {
// do nothing
}
})
// 打印结果 202ms have passed since I was scheduled
- 读文件进入
poll阶段,然后进入会回调,读文件一般会需要几毫秒,在回调了我们使用了 while 循环; 延迟了 200 毫秒 - 执行完 poll 队列,现在是空闲状态,检查有没有到时间的定时器;然后有 setTimeout,就执行了 setTimeout 的回调
#🍅 案例 2
const fs = require('fs')
fs.readFile(__filename, _ => {
setTimeout(_ => {
console.log('setTimeout')
}, 0)
setImmediate(_ => {
console.log('setImmediate')
})
})
/*
打印结果 setImmediate
setTimeout
*/
可以根据poll阶段的图来看,check 阶段比 timer 阶段先执行,执行到读取文件的回调时,先看是否设置了 setImmediate 的回调,如果有就进去 check 阶段,在等待 callbakc 加入 poll 队列空闲的时候才会检测是否有 timer;所以setImmediate比setTimeout先执行
#🍅 process.nextTick()
process进程的意思,process.nextTick()是一个异步的 node API,但不属于 event loop 的阶段,它的作用是当调用这个方法的时候,event loop 会先停下来,先执行这个方法的回调
const fs = require('fs')
fs.readFile(__filename, _ => {
setTimeout(_ => {
console.log('setTimeout')
}, 0)
setImmediate(_ => {
console.log('setImmediate')
process.nextTick(_ => {
console.log('nextTick2')
})
})
process.nextTick(_ => {
console.log('nextTick1')
})
})
/*
打印结果 nextTick1
setImmediate
nextTick2
setTimeout
*/
- 首先
fs.readFile的回调加入 poll 队列,进入 poll 阶段,poll 阶段会看setimmediate有没有设置 callback,如果没有process.nextTick(),其实先打印setimmediate的回调,但是遇到process.nextTick()会暂停 event loop,先打印其回调。 - 打印
process.nextTick()回调之后,会进入setimmediate的 check 阶段,然后打印 setImmediate,然后又遇到process.nextTick()先停下来,又打印了 nextTick2。 - 在检测有没有到时的定时器,然后进入 timer 阶段,打印了 setTimeout。
#🍅 案例 3
console.log(1)
setTimeout(() => {
console.log(2)
process.nextTick(() => {
console.log(3)
})
new Promise(resolve => {
console.log(4)
resolve()
}).then(() => {
console.log(5)
})
})
new Promise(resolve => {
console.log(7)
resolve()
}).then(() => {
console.log(8)
})
process.nextTick(() => {
console.log(6)
})
setTimeout(() => {
console.log(9)
process.nextTick(() => {
console.log(10)
})
new Promise(resolve => {
console.log(11)
resolve()
}).then(() => {
console.log(12)
})
})
process.nextTick 优先于其他微任务的执行
#setTimeout 对比 setImmediate
- setTimeout(fn, 0)在 Timers 阶段执行,并且是在 poll 阶段进行判断是否达到指定的 timer 时间才会执行
- setImmediate(fn)在 Check 阶段执行
两者的执行顺序要根据当前的执行环境才能确定:
如果两者都在主模块(main module)调用,那么执行先后取决于进程性能,顺序随机 如果两者都不在主模块调用,即在一个 I/O Circle 中调用,那么 setImmediate 的回调永远先执行,因为会先到 Check 阶段
#setImmediate 对比 process.nextTick
- setImmediate(fn)的回调任务会插入到宏队列 Check Queue 中
- process.nextTick(fn)的回调任务会插入到微队列 Next Tick Queue 中
- process.nextTick(fn)调用深度有限制,上限是 1000,而 setImmedaite 则没有
3.3 异步编程方法-发布/订阅
#3.3.1 理解发布/订阅
#🍅 异步编程的几种方式
#🍅 回调的形式实现请求
function ajax(url, callback) {
// 实现省略
}
ajax('./test1.json', function(data) {
console.log(data)
ajax('./test2.json', function(data) {
console.log(data)
ajax('./test3.json', function(data) {
console.log(data)
})
})
})
#🍅 发布订阅的形式
// 发布订阅应用
function ajax(url, callback) {
// 实现省略
}
const pbb = new PubSub()
ajax('./test1.json', function(data) {
pbb.publish('test1Success', data)
})
pbb.subscribe('test1Success', function(data) {
console.log(data)
ajax('./test2.json', function(data) {
pbb.publish('test2Success', data)
})
})
pbb.subscribe('test2Success', function(data) {
console.log(data)
ajax('./test3.json', function(data) {
pbb.publish('test3Success', data)
})
})
pbb.subscribe('test2Success', function(data) {
console.log(data)
})
- 我们通过
pbb.publish这个方法发布test1Success这个事件 - 然后去订阅这个事件
#🍅 发布和订阅图例
#3.3.2 实现事件发布/订阅
#🍅 发布和订阅类的实现
class PubSub {
constructor() {
this.events = {}
}
// 发出一个事件
publish(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(cb => {
cb.apply(this, data)
})
}
}
// 订阅一个事件
subscribe(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName].push(callback)
} else {
this.events[eventName] = [callback]
}
}
// 取消一个事件
unSubcribe(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(
cb => cb !== callback
)
}
}
}
#🍅 发布和订阅的优缺点
优点
- 松耦合
- 灵活(多次去订阅一个事件) 缺点
- 无法确保消息被触发或者触发几次
发布订阅是 promise 之前的一个主流的解决请求高耦合的方案
#3.3.3 node.js 的发布/订阅
3.4 深入理解 promise
因为 es6 的 promise 是按照 A+规范来写的,如果我们想要理解 promise 源码,需要先看 A+规范
#3.4.1 promise A+规范
#🍅 术语
- promise 一个有 then 方法的对象或函数,其行为符合本规范
- thenable 一个定义了 then 方法的对象或函数
- value(值) 任何 javaScript 的合法值(包括 undefined,thenable 或 promise)
- exception(异常) throw 语句抛出的值
- reason(拒绝原因) 表示 promise 被拒绝原因的值
#🍅 promise 的状态
pending:等待,可以转换成 fulfilled 或 rejected 状态
fulfilled:完成,拥有一个不可变的终值
rejected:拒绝,拥有一个不可变的据因
一个 promise 的状态被改变了,就不能在改变了
#🍅 promise 的 then 方法
一个 promise 必须提供一个 then 方法以访问最终值 value 和 reason
promise 的 then 方法接受两个参数
promise.then(onFulfilled, onRejected)
1\
-
then 方法的参数
- 两个函数参数,都是可选参数
- onFulfilled 在 promise 完成后被调用,onRejected 在 promise 被拒绝执行后调用;onFulfilled 和 onRejected 如果不是函数,其必须被忽略
-
then 方法的调用:可以调用多次
-
then 方法的返回值:promise
1. onFulfilled 和 onRejected 都是可选参数(参数可选)
- onFulfilled 不是一个函数,则被忽略
- onRejected 不是一个函数,则被忽略
2. 如果 onFulfilled 是一个函数(onFulfilled 特性)
- 它必须在 promise fulfilled 后调用,且 promise 的 value 为其第一个参数
- 它不能在 promise fulfilled 前调用
- 其调用次数不可超过一次
3. 如果 onRejected 是一个函数(onRejected 特性)
- 它必须在 promise rejected 后调用,且 promise 的 reason 为其第一个参数
- 它不能在 promise rejected 前调用
- 其调用次数不可超过一次
4. onFulfilled 和 onRejected 只有在执行环境堆栈仅包含平台代码时才可被调用(调用时机)
5. onFulfilled 和 onRejected 必须被作为函数调用(即没有 this 值) (调用要求)
6. then 方法可以被同一个 promise 调用多次(多次调用)
- 当 promise 成功执行时,所有 onFulfilled 需按照其注册顺序依次回调
- 当 promise 被拒绝执行时,所有的 onRejected 需按照其注册顺序依次回调
7. then 方法必须返回一个 promise 对象(返回)
promise2 = promise1.then(onFulfilled, onRejected)
1\
then 方法必须返回一个 promise,它实现了链式调用,它的返回值必须有 then 方法,所以它返回的是一个 promise; 既然 then 方法返回一个 promise,那么这个返回的 promise 的值是怎么确定的呢?假如我们返回的 promsie 是 promise2 那规范中分了 3 种情况;我们根据这 3 种情况来确定 promsie2 的值和状态是什么?
返回的 primise2 的值和状态是怎样确定的?A+规范分了 3 种情况
- onFulfilled 不是函数,promise1 的状态是 fulfilled
state:fulfilled value:同 promise1
- onRejected 不是函数,promise1 的状态是 rejected
state:rejected reason:同 promise1
- onFullfilled 或者 onRejected,return x(onFullfilled 或者 onRejected 有一个返回值,这个返回值是 x,这个时候规范定义了一个解析过程)
#🍅 promise 解析过程
- 抽象模型 resolve(promise,x)
- 如果 promise 和 x 指向相同的值 如果他们指向相同的值,就形成了循环引用;所以就 return resolve(promise,new TypeError('cant be the same'))
- 如果 x 是一个 promsie,状态有 3 种
- 如果 x 是一个对象或一个函数
- 如果 x 不是对象也不是函数
function resolve(promise, x) {
// 如果promise和x指向相同的值
if (x === promise) {
return reject(promise, new TypeError('cant be the same'))
}
//如果x是一个promsie
if (isPromise(x)) {
if (x.state === 'pending') {
return x.then(
() => {
resolve(promise, x.value)
},
() => {
reject(promise, x.value)
}
)
}
if (x.state === 'fulfilled') {
return fulfill(promise, x.value)
}
if (x.state === 'rejected') {
return reject(promise, v.value)
}
// 如果x是一个对象或一个函数
} else if (isObject(x) || isFuction(x)) {
let then
try {
then = x.then
} catch (e) {
return reject(promise, e)
}
if (isFunction(then)) {
let isCalled = false
try {
then.call(
x,
function reslovePromise(y) {
if (isCalled) {
return
}
isCalled = true
resolve(promise, y)
},
function rejectPromise(r) {
if (isCalled) {
return
}
isCalled = true
reject(promise, r)
}
)
} catch (e) {
if (!isCalled) {
return reject(promise, e)
}
}
} else {
return fulfill(promise, x)
}
// 如果x不是对象也不是函数
} else {
return fulfill(promise, x)
}
}
#🍅 案例
const promise = Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
// 或
const promise1 = Promise.resolve(1)
const promise2 = promise1.then(2)
const promise3 = promise2.then(Promise.resolve(3))
const promise4 = promise3.then(console.log)
// 1
Promise1 是 resolved 状态,它的 value 是 1,then 方法不函数的参数会忽略掉;promise2 的状态也是 resolved 状态;Value 也是 1; 第三步的参数还是会忽略掉,promise3 的状态也是 resolved 状态;Value 也是 1;第四步 console.log 是个函数,所以会打印出 1.
#3.4.2 ES6 Promise API
#🍅 Promise 构造函数
new Promise( function(resolve,reject){
// resolve(value)
// reject(reson)
})
// 函数作为参数
resolve函数将promise的状态从pending变成resolved(fulfilled)
reject函数将promise状态从pending变成rejected
#🍅 Promise 的静态方法
| 方法 | 说明 |
|---|---|
| Promise.resolve(param) | 等同于 new Promise(function (resolve.,reject){resolve(param)}) |
| Promise.reject(reason) | 等同于 new Promise(function (resolve.,reject){reject(reason)}) |
| Promise.all([p1,...,pn]) | 输入一组 promise 返回一个新的 promise,全部 promise 都是 fulfilled 结果才是 fulfilled 状态;如果有一个失败,结果 promise 就是失败 |
| Promise.allSettled([p1,...,pn]) | 输入一组 promise 返回一个新的 promise,所有的 promise 状态改变后,结果 promise 变成 fulfilled |
| Promise.race([p1,...,pn]) | 输入一组 promise 返回一个新的 promise,结果 promise 的状态跟随第一个变化的 promsie 状态,最先返回 promise 是成功的,结果 promise 就是成功,否则就是失败 |
#🍅 Promise 的实例方法
| 方法 | 说明 |
|---|---|
| promise.then(onFulfilled,onRejected) | promise 状态改变之后的回调,返回新的 promise 对想 |
| Promise.catch(reason) | 同 promise.then(null,onRejected),promise 状态为 rejected 回调 |
| Promise.finally(function(reason){ }) | 不管 promise 的状态如何都会执行 |
then 和 catch 都会返回一个新的 promise,链式调用的时候 catch 会冒泡到最后一层
#3.4.3 promise 实践
3 秒后亮一次红灯,再过 2 秒亮一次绿灯,在过 1 秒亮一次黄灯,用 promise 实现多次交替亮灯的效果
function light(color, second) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(color)
resolve()
}, second * 1000)
})
}
let list = [
{
color: 'red',
time: 3
},
{
color: 'green',
time: 2
},
{
color: 'yellew',
time: 1
}
]
function orderLights(list) {
let promise = Promise.resolve()
list.forEach(item => {
promise = promise.then(function() {
return light(item.color, item.time)
})
})
promise.then(function() {
orderLights(list)
})
}
orderLights(list)
3.5 Generator 函数及其异步的应用
#3.5.1 Generator 函数
Generator 函数可以直接生成迭代器,也是 es6 异步编程的解决方案
- es6 异步编程解决方案
- 声明:通过 function *声明
- 返回值:符合可迭代协议和迭代器协议的生成器对象
- 在执行时能暂停,又能从暂停出继续执行
生成器对象原型上有 3 个方法:1.next(param); 2.return(param) 3.throw(param)
#🍅 先看 2 个概念:迭代器 vs 生成器
-
迭代器
- 有 next 方法,执行返回结果对象 结果对象包含:1.value 2.done
用 es5 自己写一个迭代器,让大家看的更清楚
function createIterator(item) {
var i = 0
return {
next: function() {
var done = i >= item.length
var value = !done ? item[i++] : undefined
return {
done: done,
value: value
}
}
}
}
var iterator = createIterator([1, 2, 3])
console.log(iterator.next()) // { done: false, value: 1 }
console.log(iterator.next()) // { done: false, value: 2 }
console.log(iterator.next()) // { done: false, value: 3 }
console.log(iterator.next()) // { done: true, value: undefined }
#🍅 迭代协议
- 可迭代协议
-
[Symblo.iterator]属性
-
内置可迭代对象
- String Array Map Set 等
- 迭代器协议
-
next 方法
- done
- value
#🍅 yield 关键字
-
只能出现在 Generator 函数
-
用来暂停和恢复生成器函数
-
next 执行
- 遇 yield 暂停,将紧跟 yield 表达式的值作为返回的对象的 value
- 没有 yield,一直执行到 return,将 return 的值作为返回的对象的 value
- 没有 return,将 undefined 作为返回的对象的 value
-
next 参数
- next 方法可以作为一个参数,该参数会被当作一个 yield 表达式的返回值
案例 1
function* createIterator() {
let first = yield 1
let second = yield first + 2
yield second + 3
}
let iterator = createIterator()
iterator.next() // {value:1,done:false}
iterator.next(4) // {value:6,done:false}
iterator.next(5) // {value:8,done:false}
iterator.next() // {value:undefined,done:true}
运行流程:
- 第一次 next 遇到 yield 会把 yield 后面跟的表达式的值作为返回对象的 value,这个表达式是 1,所以 value 是 1
- 第二个 next 执行的时候,上一次 next 执行的时候 yield 1 返回了,但是 first 的值还未赋值;因为我们执行 yield 的时候就停了,停了之后到第二个 next 执行的时候会才会从 first 这个值开始执行,next 传入了参数 4 会把 第一次执行的 yield 值改变,所以这个时候 fisrt 是 4,那么 first+2 是 6,这时候还没执行完 done 是 false,value 是 6
- 第三次执行 next,传入了 5,那么 second 是 5,所以 value 是 8
- 第四次执行 next,前一次执行完 next 后,看着代码已经执行完了,然而相当于后面还有 return undefined
#🍅 yield* 生成器函数/可迭代对象
- 委托其他可迭代对象
- 作用:复用生成器
案例 2
function* generator1 (){
yield 1
yield 2
}
function* gennerator2 (){
yield 100
yield* generator1()
yield 200
}
let g2=generator2()
g2.next() {value:100,done:false}
g2.next() {value:1,done:false}
g2.next() {value:2,done:false}
g2.next() {value:200,done:false}
g2.next() {value:undefined,done:ture}
#🍅 return(param)
- 给定 param 值终结遍历器,param 可缺省
案例 3
function* createIterator (){
yield 1
yield 2
yield 3
}
let iterator=createIterator()
iterator.next(); {value:1,done:false}
iterator.return(); {value:undefined,done:false}
iterator.next();{value:undefined,done:false}
#🍅 thorow(param)
- 让生成器对象内部抛出错误 案例 4
function* createIterator() {
let first = yield 1
let second
try {
second = yield first + 2
} catch (e) {
second = 6
}
yield seond + 3
}
let iterator = createIterator()
iterator.next() // {value:1,done:false}
iterator.next(10) // {value:12,done:false}
iterator.throw(new Error('error')) // {value:9,done:false} 遇到yield才会暂停
iterator.next() //{value:undefined,done:true}
#🍅 协程
- 一个线程存在多个协层,但同时只能执行一个
- Genrator 函数的协层在 ES6 的实现
- Yield 挂器 x 协程(交给其他协程),next 唤醒 x 协程
#🍅 Generator 函数的应用
回调函数的写法
function readFilesByCallback() {
const fs = require('fs')
const files = [
'/Users/kitty/testgenerator/1.json',
'/Users/kitty/testgenerator/2.json',
'/Users/kitty/testgenerator/3.json'
]
fs.readFile(files[0], function(err, data) {
console.log(data.toString())
fs.readFile(files[1], function(err, data) {
console.log(data.toString())
fs.readFile(files[2], function(err, data) {
console.log(data.toString())
})
})
})
}
// 调用
readFilesByCallback()
generator 函数的写法
function* readFilesByGenerator() {
const fs = require('fs')
const files = [ '/Users/kitty/testgenerator/1.json', '/Users/kitty/testgenerator/2.json', '/Users/kitty/testgenerator/3.json' ]
let fileStr = ''
function readFile(filename) {
fs.readFile(filename, function(err, data) {
console.log(data.toString())
f.next(data.toString())
})
}
yield readFile(files[0])
yield readFile(files[1])
yield readFile(files[2])
}
// 调用
const f = readFilesByGenerator()
f.next()
缺点:需要在 readFile 函数内部调用生成器 f,不是很优雅,thunk 能把这个耦合能解开来,不用在函数内部调用函数外部的变量
3.5.2 Thunk 函数
-
求职策略 传值调用,传名调用 sum(x+1,x+2)
- 传值调用就是在计算 sum 之前先计算 x+1 和 x+2 的值,这 2 个值有了才传入 sum 函数里面计算
- 传名调用是等函数内部用到 x+1 和 x+2 的时候在计算
-
thunk 函数是传名调用的实现方式之一
-
可以实现自动执行 Generator 函数
const fs = require('fs')
const Thunk = function(fn) {
return function(...args) {
return function(callback) {
return fn.call(this, ...args, callback)
}
}
}
const readFileThunk = Thunk(fs.readFile)
function run(fn) {
var gen = fn()
function next(err, data) {
var result = gen.next(data)
if (result.done) return
result.value(next)
}
next()
}
const g = function*() {
const s1 = yield readFileThunk('/Users/kitty/testgenerator/1.json')
console.log(s1.toString())
const s2 = yield readFileThunk('/Users/kitty/testgenerator/2.json')
console.log(s2.toString())
const s3 = yield readFileThunk('/Users/kitty/testgenerator/3.json')
console.log(s3.toString())
}
run(g)
3.6 深入理解 async/await
#3.6.1 async 函数
#🍅 async
-
一个语法糖 是异步操作更简单
-
返回值 返回值是一个 promise 对象
- return 的值是 promise resolved 时候的 value
- Throw 的值是 Promise rejected 时候的 reason
async function test() {
return 1
}
const p = test()
console.log(p) // 打印出一个promise,状态是resolved,value是1
p.then(function(data) {
console.log(data) //1
})
async function test() {
throw new Error('error')
}
const p = test()
console.log(p) // 打印出一个promise,状态是rejected,value是error
p.then(function(data) {
console.log(data) //打印出的promise的reason 是error
})
可以看出 async 函数的返回值是一个 promise
#🍅 await
- 只能出现在 async 函数内部或最外层
- 等待一个 promise 对象的值
- await 的 promise 的状态为 rejected,后续执行中断
await 可以 await promise 和非 promsie,如果非 primse,例如:await 1 就返回 1
await 为等待 promise 的状态是 resolved 的情况
async function async1() {
console.log('async1 start')
await async2() // await为等待promise的状态,然后把值拿到
console.log('async1 end')
}
async function async2() {
return Promsie.resolve().then(_ => {
console.log('async2 promise')
})
}
async1()
/*
打印结果
async1 start
async2 promise
async1 end
*/
await 为等待 promise 的状态是 rejected 的情况
async function f() {
await Promise.reject('error')
//后续代码不会执行
console.log(1)
await 100
}
// 解决方案1
async function f() {
await Promise.reject('error').catch(err => {
// 异常处理
})
console.log(1)
await 100
}
// 解决方案2
async function f() {
try {
await Promise.reject('error')
} catch (e) {
// 异常处理
} finally {
}
console.log(1)
await 100
}
#🍅 async 函数实现原理
实现原理:Generator+自动执行器
async 函数是 Generator 和 Promise 的语法糖
3.6.2 应用
#🍅 用 async 函数方案读取文件
async function readFilesByAsync() {
const fs = require('fs')
const files = [
'/Users/kitty/testgenerator/1.json',
'/Users/kitty/testgenerator/2.json',
'/Users/kitty/testgenerator/3.json'
]
const readFile = function(src) {
return new Promise((resolve, reject) => {
fs.readFile(src, (err, data) => {
if (err) reject(err)
resolve(data)
})
})
}
const str0 = await readFile(files[0])
console.log(str0.toString())
const str1 = await readFile(files[1])
console.log(str1.toString())
const str2 = await readFile(files[2])
console.log(str2.toString())
}
3.7 手写 Promise
#3.7.1 先实现整体结构
定义一个模块
// 我们用es5的自执行函数定义模块,如果用AMD规范的需要编译,用自执行函数方便我们一会调用测试
;(function(window) {
function Promise() {}
window.Promise = Promise
})(window)
基本结构
;(function(window) {
// executor执行器,就是我们new Promise((resolve,reject)=>) 传过来的函数,它是同步执行的
function Promise(executor) {}
/**
* then方法指定了成功的和失败的回调函数,如果指定的不是函数,会忽略该值
* 返回一个新的promise对象,该promsie的结果onResolved和onRejected决定,状态由上个Promise决定
*/
Promise.prototype.then = function(onResolved, onRejected) {}
/**
* 传入失败回调
* 返回一个新的Promise,由于已经捕获错误了,会返回一个成功的Promise
*/
Promise.prototype.catch = function(OnRejected) {}
/**
* 返回一个指定结果成功的promise
*/
Promise.resolve = function(value) {}
/**
* 返回一个指定reason失败的promise
*/
Promise.reject = function(reason) {}
/**
* 返回一个新Promsie
* 所有的promise成功才成功,有一个失败就失败
*/
Promise.all = function(promises) {}
/**
* 返回一个新Promsie
* 传入的数组中第一个返回的Promise成功就成功,如果不成功就失败(第一个promise不是你传入的第一个,比如请求接口,最新拿到结果的是第一个)
*/
Promise.race = function(promises) {}
window.Promise = Promise
})(window)
3.7.2 实现 Promise 内部的 resolve 和 reject
当我们new promise((resolve,reject)=>{})时会传入一个回调函数,我们这里叫 executor(执行器),promise 拿到这个方法以后, 调用这个 executor 方法并传入 resolve 和 reject 方法,让用户控制 promise 是成功还是失败;调用 resolve 是成功,调用 reject 是失败。
;(function(window) {
// 常量定义3promise的三个状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
// executor执行器,就是我们new Promise((resolve,reject)=>) 传过来的函数,它是同步执行的
function Promise(executor) {
// 存一下this,因为代码中调用resolve时,在全局下调用的,此时resolve里面this是window
// 关于this指向问题,就是谁调用就指向谁,当然也可以用箭头函数处理这个问题
const self = this
self.status = PENDING
self.state = undefined //存传的值
self.callbackQueues = [] // 存回调队列
// 让promise成功的函数
function resolve(value) {
if (self.status !== PENDING) return
self.status = FULFILLED
self.state = value
/*
这里会让人感到疑惑?下面是干什么的?
onResolved是then方法的第一个参数,onRejected是第二个参数
其实promise用了发布订阅的设计模式,promise把then方法的OnResolved和OnRejected方法存到一个数组里
不懂没关系,可以看下面的我分析的代码执行步骤
*/
if (self.callbackQueues.length > 0) {
self.callbackQueues.map(item => {
setTimeout(() => {
item.onResolved(value)
})
})
}
}
// 让promsie失败的函数
function reject(reason) {
// 如果不是pending状态,就没必要往下了,因为promise的状态一旦改变就无法在更改
if (self.status !== PENDING) return
self.status = REJECTED
self.state = reason
if (self.callbackQueues.length > 0) {
self.callbackQueues.map(item => {
setTimeout(() => {
item.onRejected(value)
})
})
}
}
// 捕获executor函数里意外错误,如果错误改变状态
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
/**
* then方法指定了成功的和失败的回调函数
* 返回一个新的promise对象
*/
Promise.prototype.then = function(onResolved, onRejected) {
const seft = this
seft.callbackQueues.push({
onResolved,
onRejected
})
}
window.Promise = Promise
})(window)
#分析一下代码执行步骤
<script src="./Promise.js"></script>
<script>
new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
})
}).then(res => {
console.log(res)
})
</script>
new Promise()会传入一个回调函数到构造函数 Promise 中,然后执行 Promise 里的代码。- 开始就是
const self=this的执行,这些不是重点,重点是执行executor(resolve,reject),并传入 resolve 和 reject 函数。 - 开始执行
executor函数,函数里执行了setTimeout,我们知道setTimeout是异步执行的,接下来会执行then方法。 - 执行 then 方法并传入了
onResolved函数,then 方法里把传入的onResolvedpush 到了callbackQueues数组里。 - 同步的代码执行完了,开始执行异步任务了,显然是指行 setTimeout 里 resovle 方法。
- 在 resolve 方法里开始判断不是
pending状态就退出,然后就把状态改成fulfilled,把传过来的值存到state里;然后执行callbackQueues里的函数,callbackQueues 里存的 then 方法传回调函数,调用里面的回调把state值传进去。
上面代码只是简单的实现了then方法,接下来我们具体实现
#3.7.3 实现 Promise 原型上的 then 方法
看到这么多代码不要慌张,我会拆分详细讲解,then 方法是 Promise 的重点,其他方法都 then 方法有关系
我们要先明确 then 方法实现了什么?
- 返回一个新的 Promsie
- 新的 promise 的值由上一个 Promsie 的 onResolved 和 onRejected 的结果决定
Promise.prototype.then=function(onResolved,onRejected){
const self= this
/*
我们为什么把判断写到promise里面?
因为我们需要根据上一个Promsie的状态去改变当前这个返回的promise的状态
上一个promsie的状态可以根据seft.status拿到,我们要改变返回的这个promise的状态,
就是调用resolve或reject,我们只有写在promise里面才到调用这两个函数
*/
return new Promise((resolve,reject)=>{
// 我们调用then方法的时候 ,promise可能是以下三种状
// 如果是pending状态,那么说明Promsie内部的resolve还没执行,因为如果执行了,resolve函数会改变状态的
// 由于resolve函数还未执行,我们也拿不到传过来的值,先把回调函数放到callbackQueues数组中
if(seft.status===PENDING){
seft.callbackQueues.push({
onResolved,
onRejected
})
}else if(seft.status===FULFILLED){
// 用self.state 拿到当前promsie state的值,把值传递给使用者传入的第一个回调函数
onResolved(self.state)
else {
// 用self.state 拿到当前promsie state的值,把值传递给使用者传入的第二个回调函数
onRejected(self.state)
}
})
}
上面我们实现了,返回一个 promsie,并调用了传递过来的 onResolved 和 onRejected 函数,接下来我们改变这个返回 promise 的状态
Promise.prototype.then=function(onResolved,onRejected){
const self= this
return new Promise((resolve,reject)=>{
if(seft.status===PENDING){
seft.callbackQueues.push({
onResolved,
onRejected
})
// 当前Promise fulfilled状态时
}else if(seft.status===FULFILLED){
/*
const p = new Promise((resolve,reject)=>{resolve(1)})
p.then((res)=>{
return 2
or
return new Promise((resolve,reject)=>{resolve(2)})
})
*/
// 我们调用onResolved拿到函数的返回值,这个返回值,也有可能是一个promise
const result= onResolved(self.state)
if(result instance Promise){
result.then(res=>{ // 调用then方法拿到值
resolve(res) // 改变返回的这个promise状态为fulfilled,并传入了值
})
} else {
resolve(result) // 改变返回的这个promise状态为fulfilled,并传入了值
}
// 当前Promise 为rejected状态时,下面的实现方法跟上面基本一样
// 为什么我们下面也调用resolve,因为onRejected这个函数中已经捕获了错误
// 一旦有onRejected函数捕获了错误,错误就不再往下传递,让下一个promise成功!
else {
const result= onRejected(self.state)
if(result instance Promise){
result.then(res=>{ // 调用then方法拿到值
resolve(res) // 改变返回的这个promise状态为fulfilled,并传入了值
})
} else {
resolve(result) // 改变返回的这个promise状态为fulfilled,并传入了值
}
}
})
}
把相同的代码封装成一个函数 handle,我们知道 promsie 的 then 的回调函数是异步的,所以 setTimeout 模拟 then 方法的异步问题
Promise.prototype.then=function(onResolved,onRejected){
const self= this
return new Promise((resolve,reject)=>{
// 把相同的代码封装起来,并用try catch捕获错误
/*
像这种情况,使用者如果抛出错误,直接让下个promise也就是当前返回的promise状态为失败
then(res=>{
throw '我抛出错误'
})
*/
function handle (callback){
try {
const result= callback(self.state)
if(result instance Promise){
result.then(res=>{
resolve(res)
})
} else {
resolve(result)
}
}catch(reason){
reject(reason)
}
}
// 当前Promise pending状态时
if(seft.status===PENDING){
seft.callbackQueues.push({
onResolved,
onRejected
})
// 当前Promise fulfilled状态时
}else if(seft.status===FULFILLED){
setTimeout(()=>{
handle(onResolved)
})
// 当前Promise 为rejected状态时
else {
setTimeout(()=>{
handle(onRejected)
})
}
})
}
then 方法已经实现的差不多了,但是 promise 为 pending 状态时,我们没有去改变返回那个 promise 的状态,改变状态需要调用 resolve 或 reject 然而我们并没有调用
Promise.prototype.then=function(onResolved,onRejected){
// 如果传入的不是函数,就用默认函数,并把上一个promse的值往下传递
const onResolved=typeof onResolved ==='fucntion' ? onResolved :(value)=>value
// 如果传入的不是函数,就给默认函数,并抛出错误,让返回的这个promsie为失败状态
const onResolved=typeof onRejected ==='fucntion' ? onRejected :(reason)=>{throw reason}
const self= this
return new Promise((resolve,reject)=>{
function handle (callback){
/*省略*/
}
// 当前Promise pending状态时
if(seft.status===PENDING){
seft.callbackQueues.push({
onResolved()=>{
handle(onResolved)
},
onRejected()=>{
handle(onRejected)
}
})
}else if(seft.status===FULFILLED){// 当前Promise fulfilled状态时
/*省略*/
else { // 当前Promise 为rejected状态时
/*省略*/
}
})
}
then 完整代码
/**
* then方法指定了成功的和失败的回调函数
* 返回一个新的promise对象,它实现了链式调用
* 返回的promise的结果由onResolved和onRejected决定
*/
Promise.prototype.then = function(onResolved, onRejected) {
onResolved = typeof onResolved === 'function' ? onResolved : value => value
onRejected =
typeof onRejected === 'function'
? onRejected
: reason => {
throw reason
}
const seft = this
return new Promise((resolve, reject) => {
function handle(callback) {
try {
const result = callback(seft.state)
if (result instanceof Promise) {
result.then(
res => {
resolve(res)
},
err => {
reject(err)
}
)
} else {
resolve(result)
}
} catch (err) {
reject(err)
}
}
// 当是Promise状态为pending时候,将onResolved和onRejeactd存到数组中callbackQueues
if (seft.status === PENDING) {
seft.callbackQueues.push({
onResolved(value) {
handle(onResolved)
},
onRejected(reason) {
handle(onRejected)
}
})
} else if (seft.status === FULFILLED) {
setTimeout(() => {
handle(onResolved)
})
} else {
setTimeout(() => {
handle(onRejected)
})
}
})
}
3.7.4 实现 promsie 原型的 catch 方法
/**
* 传入失败回调
* 返回一个新的Promise
*/
// 第一个参数不传,then里面会有默认参数,传入OnRejected回调函数
// then 方法里会调用OnRejected并传入拒绝的理由
Promise.prototype.catch = function(OnRejected) {
return this.then(undefined, OnRejected)
}
#3.7.5 Promise.resolve
/**
* Promise函数对象的resove方法
* 返回一个指定结果成功的promise
*/
// 这个简单 让返回的promise成功就行
Promise.resolve = function(value) {
return new Promise((resolve, reject) => {
if (value instanceof Promise) {
value.then(resolve, reject)
} else {
resolve(value)
}
})
}
#3.7.6 Promise.reject
/**
* Promise函数对象的reject方法
* 返回一个指定reason失败的promise
*/
// 这个简单 让返回的promise失败就行
Promise.reject = function(reason) {
return new Promise((resove, reject) => {
reject(reason)
})
}
#3.7.7 Promise.all
/**
* 所有成功才成功,有一个失败就失败
* 返回一个的Promise,这个promise的结果由传过来的数组决定,一个失败就是失败
*/
// 这个也不难,循环传入的数组,把成功的promise的返回的值放到values中
// 只有当values和promises相同时,说明全部成功,这时候返回一个成功的数组,有一个失败就失败
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
let values = []
promises.map(item => {
if (item instanceof Promise) {
item.then(res => {
values.push(res)
}, reject)
} else {
// 为了正确的放入values,所以也让其异步
setTimeout(() => {
values.push(item)
})
}
})
// 这里用setTiemeout是因上面的then方法是异步的,让下面的代码也异步,才能拿到最终的values数组
setTimeout(() => {
if (values.length === promises.length) {
resolve(values)
}
})
})
}
#3.7.8 Promise.race
/**
* 第一个成功就成功,如果不成功就失败(就是最先拿到谁的值,就成功)
* 返回一个Promsie
*/
// 这个简单,只要发现一个promsie成功了,就让返回的promsie成功
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
promises.map(item => {
if (item instanceof Promise) {
item.then(resolve, reject)
} else {
resolve(item)
}
})
})
}
#3.7.9 总结
我们要实现 promsie 应该先看 A+规范,总结一下重点:
- promise 用了发布订阅的设计模式。
- 重点在 then 方法,它实现了返回一个新的 promise,这个 promsie 的状态由上一个 promsie 的状态所决定。
- 调用 resolve 和 reject 去改变 promise 的状态
3.8 Web Workers 的多线程机制
#3.8.1 Web Workers 介绍
-
web Workers
- 一个 Web API-> 浏览器能力 -> 提供一个 js 可以运行的环境
- Web 应用程序可以在独立于主线层的后台线程中,运行一个脚本操作
- 关键点:性能考虑
web Workers 主要处理一些耗时的任务,比如一家公司老板忙不来,肯定会招一些人,这些会承担一些公司的脏活累活,占用时间比较多的活,老板只做一些核心的事情。所以 web worker 是来分担主线程的负担的,所以 web worker 的意义在于一些耗时的任务从主线层剥离出来,让 worker 做这些耗时的处理; 那么主线程专注于页面的渲染和交互,那么我们访问的页面的性能会更好。
#worker 的主要方法
- onerror、onmessage、onmessageerror、postMessage、importScripts、close
#🍅 worker 线程和主线程的通信
我们知道 worker 是一个线程,那么主线程指派任务给 worker 线程是怎样通信的呢?就设计到 2 个线程之间的通信。
主线程有一个postMessage方法用来通知 worker 线程任务,worker 有一个onMessage的方法用来接收主线程的任务,接收到主线程的消息之后就去工作,工作完之后 worker 也有一个postMessage方法用来通知主线程干完了;主线程也有一个onMessage方法,能获取到 worker 的工作已经做完了,以及结果是什么。
#3.8.2 实战一个 web worker 的案例
这个案例是我们把耗时的斐波那契数列放到 worker 里计算
- index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./main.js"></script>
</body>
</html>
- main.js
// new Worker 之后,worker进行工作
const worker = new Worker('worker.js')
worker.onmessage = function(e) {
console.log('拿到worker通知的数据', e)
worker.postMessage('message收到了')
}
- worker.js
worker 线程去计算斐波那契数列
function fibonacci(n) {
if (n === 1 || n == 2) {
return 1
}
return fibonacci(n - 2) + fibonacci(n - 1)
}
// 告诉主线程
postMessage(fibonacci(40))
// 收到主线程的消息
onmessage = function(e) {
console.log('好的,拿到了就好', e)
}
#当你点开 index.html 会出现报错
这个报错是什么原因呢?
因为我们用 worker 是限制的,不是本地跑一个 html 就行,new Worker("worker.js")里的参数不能通过本地路径去取的,不支持 file 协议;需要起一个静态资源服务,我们可以用browser-sync。
npm install -g browser-sync # 下载
-browser-sync start --server # 启动,注意启动服务要在index.html这个文件夹
xw 启动完之后,访问http://localhost:3000/index.html
源代码地址:Senior-FrontEnd/examples/jsadvanced/3.8/
#3.8.3 worker 的能力是受限制的
#预习资料
| 预习资料名称 | 链接 | 备注 |
|---|---|---|
| Blob 对象用法 | 阅读地址 | 嵌入式 webworkers 会用到 Blob |
| JavaScript 中的 Blob 对象 | 阅读地址 | 掘金上比较基础的一篇文章 |
-
与主线程脚本同源
在主线程下运行的 worker 必须与主线程同源
-
与主线程上下文不同
- 在 worker 下无法操作 dom,可以操作 Navigator、location 等,就是不会引起页面混乱的可以用。
- 不能执行 alert 等
-
不能读取本地文件
- 所有加载的文件来源于网络,不来来源于 file 协议。
可以在 worker.js 下打印 this 看一下它的全局对象
#嵌入式 worker
我们刚刚了解到,new Worker(woker路径)里的路径不能去从本地读取的,但是我们现在很多都是模块的写法,很多都会用到require.js,那么有没有简单的方法在项目中使用 workers 呢?看下面案例。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- type="javascript/worker" 写上这个类型,里面的脚本是不会执行的 -->
<script type="javascript/worker" id="worker">
function fibonacci (n) {
if(n===1 || n ==2) {
return 1
}
return fibonacci(n-2) + fibonacci(n-1)
}
postMessage(fibonacci(40))
</script>
<script>
// 拿到worker里的代码字符串
var workerScript = document.querySelector('#worker').textContent
// Blob :二进制大对象
var blob = new Blob([workerScript], { type: 'text/javascript' })
// blob:null/9d8594c9-1783-46f9-8001-c6112af6a15a 可以在浏览器中访问,可以看见worker里的代码
var worker = new Worker(window.URL.createObjectURL(blob))
worker.onmessage = function(e) {
console.log('拿到worker通知的数据', e)
worker.postMessage('message收到了')
}
</script>
</body>
</html>
#webworkify
如果按照上面的写法,在 2 个 script 标签里面写,显得不是很优雅;于是前辈给我们封装了webworkify,它也是利用blob、createObjectURL。
#3.8.4 Web Worker 的使用场景
-
解决的痛点
- js 执行复杂运算时阻塞了页面渲染
-
使用场景
- 复杂运算
- 渲染优化(canvas 有个离线的 api 结合 worker)
- 流媒体数据处理
#哪些项目中用到了 Web worker?
- flv.js 用 h5 的标签去播放 flv 格式的视频,用于直播、录播,它去解码 http-flv 格式的视频,通过 Media SourceExtensions,解码的过程就是在 workers 里执行的。
了解 SharedWorker 和 ServiceWOrker(pwa 的基础,可以做页面的缓存优化)
#3.9 Service Workers
#3.9.1 初识 Service Workers
Service Worker(以下简称 sw)是基于 WEB Worker 而来的。
Service Workers 的本质充当 WEB 应用程序、浏览器与网络(可用时)之间的代理服务器,这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作,更新来自服务器的资源,它还提供入口推送通知和访问后台同步 API。
service worker 的特点 重要
- 网站必须使用 HTTPS,除了本地开发环境(localhost)
- 运行于浏览器,可以控制打开的作用域范围下所有的页面请求,可拦截请求和返回,缓存文件;sw 可以通过 fetch 这个 api,来拦截网络和处理网络请求,再配合 cacheStorage 来实现 web 页面的缓存管理以及与前端 postMessage 通信
- 单独的作用域范围,单独的运行环境和执行环境
- 不能操作页面 DOMM,可以通过是事件机制来处理
- 完全异步,同步 API(如 XHR 和 localStorage)不能再 service work 中使用,sw 大量使用 promise
- 一旦被 install,就永远存在,除非被 uninstall 或者 dev 模式手动删除
- 响应推送
- service worker 是事件驱动的 worker,生命周期与页面无关。 关联页面未关闭时,它也可以退出,没有关联页面时,它也可以启动。
#🍅 service worker 的生命周期 重要
注册 -> 安装 -> 激活 -> 废弃
-
installing(安装中)
这个状态发生在 service worker 注册之后,表示开始安装,同时会进入 service Worker 的 install 事件中,触发 install 的事件回调指定一些静态资源进行离线缓存
-
install(安装后)
安装完成,进入了 waiting 状态,等待其他 Service worker 被关闭,所以当前脚本尚未激活,处于等待中;可以通过 self.skipWaiting()跳过等待
-
activating(激活中)
等待激活,在这个状态下没有被其他的 Servie Worker 控制的客户端,允许当前 worker 完成安装,并且清除了了其他的 worker 以及关联缓存的旧缓存资源(在 acitive 的事件回调中,可以调用 self.clients.claim())
-
activated(激活后)
在这个状态会处理 actived 事件回调,并且提供处理功能性事件:fetch(请求)、sync(后台同步)、push(推送)
-
redundant(废弃)
表示一个 Service Worker 的生命周期结束
#service worker 的优势
- 支持离线访问
- 加载速度快
- 离线状态下的可用性
就算已经关闭了页面,它也能帮助我们继续发送代理的请求
#service worker 的安全策略
由于 service worker 功能强大,可以修改任何通过它的请求,因此需要对其进行一定的安全限制
- 使用 https 或者本地的 localhost 才能使用 Service Worker
- Service Worker 都有一个有限的控制范围,这个范围通过放置 Service Worker 的 js 文件的目录决定的,也就是 Service Worker 所在目录以及所有的子目录。
也可以通过注册 Service Worker 的时候传入一个scope选项,用来覆盖默认的作用域,但是只能将作用域的范围缩小,不能将它扩大。
navigator.serviceWorker.register('serviceworker.js', { scope: '/' })
#3.9.1 小试牛刀
- 新建 index.html
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document1</title>
</head>
<body>
<div>test1</div>
<script src="./sw.js"></script>
<script>
window.onload = function() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('./sw.js', { scope: '/' })
.then(registration => {
console.log(
'ServiceWorker 注册成功!作用域为: ',
registration.scope
)
})
.catch(err => {
console.log('ServiceWorker 注册失败: ', err)
})
}
}
</script>
</body>
</html>
- 新建 sw.js
self.addEventListener('install', function(event) {
event.waitUntil(
caches.open('cache').then(cache => {
// 缓存index.html文件
return cache.add('./index.html')
})
)
})
self.addEventListener('fetch', function(event) {
event.respondWith(
fetch(event.request).catch(() => {
// 匹配返回缓存资源
return caches.match('./index.html')
})
)
})
- 启动一个服务
browser-sync start --server # 在3.9文件夹下
- 关闭服务
关闭刚刚启动的服务,发现照样能访问资源,只不过控制台会出现一行报错sw.js:1 Uncaught SyntaxError: Unexpected token '<'
查看自己的浏览器上有哪些网站用了 service worker
浏览器访问:chrome://serviceworker-internals/
#总结
这章我们学习了异步编程的解决方法
回调->发布订阅->promise->Generator->async/await