在前端的世界里,不仅要会使用,更要懂原理。第二弹。

手写express

采用express第三方包实现的话,app.js如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const express = require('express')
const app = express()

// 我们在这里模拟一些get、post方法
app.get('/', (req, res) => {
res.end('welcome')
})
app.get('/name', (req, res) => {
res.end('yyyyy')
})
app.get('/age', (req, res) => {
res.end('12')
})
app.post('/name', (req, res) => {
res.end('yes!')
})

app.listen(3000, () => {
console.log('3000');
})

接下来创建express.js,实现自己的express,明确文件要导出一个方法,这个方法需要产生app对象(因为上述代码是采用express()实现的),开始搭建基本骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const http = require('http')
const url = require('url')

function createApplication() {
let app = (req, res) => {}

app.listen = function() {
let server = http.createServer(app)
server.listen(...arguments)
}

return app
}

module.exports = createApplication

接下来,我们实现get请求,思路如下:

  1. 创建app.routes数组,里面存放每一个路由信息(包括method、path、回调函数handler)
  2. 通过app.get方法,向app.routes放入每个路由信息
  3. 拿到传入的mthod和path,遍历app.routes,找到对应的handler并执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const http = require('http')
const url = require('url')

function createApplication() {
let app = (req, res) => {
// 获取请求的方法
let m = req.method.toLowerCase()
let {
pathname
} = url.parse(req.url, true)

// 取出每一个layer
// 根据方法和路径匹配成功后执行对应的回调函数
for (let i = 0; i < app.routes.length; i++) {
let {
method,
path,
handler
} = app.routes[i]
if (m === method && pathname === path) {
handler(req, res)
return
}
}
res.end('cannot find')

}

app.routes = []

app.get = function(path, handler) {
let layer = {
method: 'get',
path,
handler
}
app.routes.push(layer)
}

app.listen = function() {
let server = http.createServer(app)
server.listen(...arguments)
}

return app
}

module.exports = createApplication

我们实现了get请求了,接下来要实现其他的请求方法,如post、put、delete等等,但是method如此之多,一个个写肯定不是最明智的选择,因此我们选择批量生产方法的形式,通过http.METHODS拿到所有的method,实现批量挂载:

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

function createApplication() {
let app = (req, res) => {
// 获取请求的方法
let m = req.method.toLowerCase()
let {
pathname
} = url.parse(req.url, true)

// 取出每一个layer
// 根据方法和路径匹配成功后执行对应的回调函数
for (let i = 0; i < app.routes.length; i++) {
let {
method,
path,
handler
} = app.routes[i]
if (m === method && pathname === path) {
handler(req, res)
return
}
}
res.end('cannot find')

}

app.routes = []

// 批量生产方法
http.METHODS.forEach(method => {
method = method.toLowerCase()
app[method] = function(path, handler) {
let layer = {
method,
path,
handler
}
app.routes.push(layer)
}
})

// console.log(http.METHODS);
// [ 'ACL',
// 'BIND',
// 'CHECKOUT',
// 'CONNECT',
// 'COPY',
// 'DELETE',
// 'GET',
// 'HEAD',
// 'LINK',
// 'LOCK',
// 'M-SEARCH',
// 'MERGE',
// 'MKACTIVITY',
// 'MKCALENDAR',
// 'MKCOL',
// 'MOVE',
// 'NOTIFY',
// 'OPTIONS',
// 'PATCH',
// 'POST',
// 'PROPFIND',
// 'PROPPATCH',
// 'PURGE',
// 'PUT',
// 'REBIND',
// 'REPORT',
// 'SEARCH',
// 'SOURCE',
// 'SUBSCRIBE',
// 'TRACE',
// 'UNBIND',
// 'UNLINK',
// 'UNLOCK',
// 'UNSUBSCRIBE' ]

// restful api

// app.get = function(path, handler) {
// let layer = {
// method: 'get',
// path,
// handler
// }
// app.routes.push(layer)
// }

app.listen = function() {
let server = http.createServer(app)
server.listen(...arguments)
}

return app
}

module.exports = createApplication

到此,已实现全部请求方法的挂载。但还存在很多的问题,比如:

  1. 尚不支持params、query、request body等传参
  2. 未添加安全机制
  3. 其他express api均未实现
  4. ……

手写koa

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const Koa = require('./my-koa/application')
const app = new Koa()

app.use((ctx, next) => {
console.log(ctx.req.url);

// 原生
console.log(ctx.req.url); //ctx.req = req
console.log(ctx.request.req.url); // ctx.request.req = req

console.log(ctx.request.url); //ctx.request是koa自己封装的属性
console.log(ctx.url); //用ctx.url来代替ctx.request.url属性,简化写法
})
app.listen(3000, () => {
console.log('3000');
})

注意,我们需要先理清ctx.req | ctx.request.req | ctx.request | ctx 的关系。打印结果如下:

1
2
3
4
5
6
7
// 这两组总是一样
console.log(ctx.req.url); //ctx.req = req
console.log(ctx.request.req.url); // ctx.request.req = req

// 这两组总是一样
console.log(ctx.request.url); //ctx.request是koa自己封装的属性
console.log(ctx.url); //用ctx.url来代替ctx.request.url属性,简化写法

由此可以判断,我们实现时应该写

1
2
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res

加下来,我们实现自己的koa框架:

首先创建一个文件夹,新建application.js文件作为入口文件,再分别创建context.jsrequest.jsresponse.js文件。

==context.js==

1
2
let context = {}
module.exports = context

==request.js==

1
2
let request = {}
module.exports = request

==response.js==

1
2
let response = {}
module.exports = response

==application.js==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
const http = require('http')
const context = require('./context')
const request = require('./request')
const response = require('./response')

class Koa {
constructor() {
this.callbacksFn
this.context = context
this.request = request
this.response = response
}
use(cb) {
this.callbacksFn = cb
}
createContext(req, res) {
// 希望ctx可以拿到context的属性,但是不修改context
let ctx = Object.create(this.context)
ctx.request = Object.create(this.request)
ctx.response = Object.create(this.response)
ctx.req = ctx.request.req = req
ctx.res = ctx.response.res = res

return ctx
}
handlerRequest(req, res) {
let ctx = this.createContext(req, res)
this.callbacksFn(ctx) //这里传入的是ctx
}
listen() {
let server = http.createServer(this.handlerRequest.bind(this))
server.listen(...arguments)
}
}

module.exports = Koa

我们通过app.js检验一下:

1
2
3
4
5
6
console.log(ctx.req.url); //ctx.req = req
console.log(ctx.request.req.url); // ctx.request.req = req

// 这里仍然还是undefined
console.log(ctx.request.url); //ctx.request是koa自己封装的属性
console.log(ctx.url); //用ctx.url来代替ctx.request.url属性,简化写法

因为ctx.request.urlctx.url输出的还是undefined,因为没有实现,下面开始实现这一部分,核心源码是使用代理来实现:

==request.js==

1
2
3
4
5
6
7
8
9
10
11
12
const url = require('url')

let request = {
get url() {
return this.req.url
},
get path() {
return url.parse(this.req.url).pathname
}
}

module.exports = request

==response.js==

1
2
3
4
5
6
7
8
9
10
let response = {
set body(value) {
this.res.statusCode = 200 //只要调了ctx.body='xxx',就设置状态码为200
this._body = value
},
get body() {
return this._body
}
}
module.exports = response

==context.js==(实现代理功能)

实现:

  • ctx.url=ctx.request.url——取ctx.url是取的ctx.request.url
  • ctx.path=ctx.request.path——取ctx.path是取的ctx.request.path
  • ctx.body=ctx.request.body——取ctx.body是取的ctx.response.body,设置ctx.body是设置的ctx.response.body
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let ctx = {}

// 自定义获取器,代理属性
function defineGetter(property, name) {
// 取 ctx.url 取的是 ctx.request.url
// __defineGetter__是原生方法,也可以使用Object.defineProperty实现
ctx.__defineGetter__(name, function() {
return this[property][name]
})
}

function defineSetter(property, name, value) {
ctx.__defineSetter__(name, function(value) {
this[property][name] = value
})
}

defineGetter('request', 'url')
defineGetter('request', 'path')
defineGetter('response', 'body')
defineSetter('response', 'body')

module.exports = ctx

修改==application.js==

1
2
3
4
5
6
7
8
9
10
11
handlerRequest(req, res) {
res.statusCode = 404 //默认页面找不到
let ctx = this.createContext(req, res)
this.callbacksFn(ctx) //这里传入的是ctx,当回调函数执行后,ctx.body值就会发生变化
let body = ctx.body
if (typeof body === 'undefined') {
res.end('Not Found!')
} else if (typeof body === 'string') {
res.end(body)
}
}

完成后,通过app.js进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const Koa = require('./my-koa/application')
const app = new Koa()

app.use((ctx, next) => {
console.log('group 1 ============================');
console.log(ctx.req.url);
console.log('====================================');

console.log('group 2 ============================');
// 原生
console.log(ctx.req.url); //ctx.req = req
console.log(ctx.request.req.url); // ctx.request.req = req
// koa封装的
console.log(ctx.request.url); //ctx.request是koa自己封装的属性
console.log(ctx.url); //用ctx.url来代替ctx.request.url属性,简化写法
console.log('====================================');

console.log('group 3 ============================');
// koa封装的
console.log(ctx.request.path);
console.log(ctx.path);
console.log('====================================');

ctx.body = 'hello'

})
app.listen(3000, () => {
console.log('3000');
})


output:
group 1 ============================
/name
====================================
group 2 ============================
/name
/name
/name
/name
====================================
group 3 ============================
/name
/name
====================================

打开http://localhost:3000,显示'hello',上述代码没问题了。接下来我们实现多个中间件的功能,核心原理(洋葱模型):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function app() {}
app.middlewares = []
app.use = function(cb) {
app.middlewares.push(cb)
}
app.use((ctx, next) => {
console.log('mid 1-1');
next()
console.log('mid 1-2');
})
app.use((ctx, next) => {
console.log('mid 2-1');
next()
console.log('mid 2-2');
})
app.use((ctx, next) => {
console.log('mid 3-1');
next()
console.log('mid 3-2');
})

function dispatch(index) {
if (index === app.middlewares.length) return
let mid = app.middlewares[index]
mid({}, () => dispatch(index + 1))
}

dispatch(0)

----------------------------

output:
mid 1-1
mid 2-1
mid 3-1
mid 3-2
mid 2-2
mid 1-2

接下来,实现异步,采用async、await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function app() {}
app.middlewares = []
app.use = function(cb) {
app.middlewares.push(cb)
}

// koa可以使用async await
let log = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('ok');
resolve()
}, 1000);
})
}

app.use((ctx, next) => {
console.log('mid 1-1');
next()
console.log('mid 1-2');
})
app.use(async(ctx, next) => {
console.log('mid 2-1');
await log()
next()
console.log('mid 2-2');
})
app.use((ctx, next) => {
console.log('mid 3-1');
next()
console.log('mid 3-2');
})

function dispatch(index) {
if (index === app.middlewares.length) return
let mid = app.middlewares[index]
mid({}, () => dispatch(index + 1))
}

dispatch(0)


------------------------------
output:
mid 1-1
mid 2-1
mid 1-2
ok
mid 3-1
mid 3-2
mid 2-2

想想为什么会出现这个结果?

最后通过这个原理,修改原代码:

==application.js==(在以上实现的代码基础上修改,只展示新增部分)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Koa {
constructor() {
//新增
this.middlewares = []
}
//修改
use(cb) {
this.middlewares.push(cb)
}
//新增
compose(ctx, middlewares) {
function dispatch(index) {
// 处理越界
if (index === middlewares.length) return
let mid = middlewares[index]
return Promise.resolve(mid({}, () => dispatch(index + 1))) // 转化为promise
}
return dispatch(0)
}
//修改
handlerRequest(req, res) {
res.statusCode = 404 //默认页面找不到
let ctx = this.createContext(req, res)

// this.callbacksFn(ctx) //这里传入的是ctx,当回调函数执行后,ctx.body值就会发生变化
let composedMiddlewares = this.compose(ctx,this.middlewares)
composedMiddlewares.then(() => {
let body = ctx.body
if (typeof body === 'undefined') {
res.end('Not Found!')
} else if (typeof body === 'string') {
res.end(body)
}
})
}
}

目前已经完成了一个简易版koa,通过app.js即可进行测试!

手写Promise

参考只会用?一起来手写一个合乎规范的Promise
先看看promise的一个基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function task1() {
return new Promise(function(resolve, reject) {
console.log("task1");
})
}

function task2() {
return new Promise(function(resolve, reject) {
console.log("task2");
})
}

function task3() {
return new Promise(function(resolve, reject) {
console.log("task3");
})
}

// 调用函数
task1()
.then(task2())
.then(task3())

创建Promise实例时,我们传入了一个函数,函数的两个参数(resolve/reject)分别将Promise的状态变为成功态和失败态。首先搭建出基本骨架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

class Promise {
constructor(executor) {
this.state = PENDING
this.value = undefined
this.reason = undefined
}

resolve(value) {

}

reject(reason) {

}
}

Promise.prototype.then = (onFullFilled, onRejected) => {

}

module.exports = Promise

Promise实例中state保存它的状态,分为3种:等待态(pending)成功态(resolved)和失败态(rejected)。因为Promise也可以通过.then进行调用,因此在Promise的原型上绑定了then方法。

接下来分别实现:

  1. 当实例化Promise时,构造函数中就要马上调用传入的executor函数执行
  2. 完成resolve和reject方法,已经是成功态或是失败态不可再更新状态
  3. 实现原型上的then方法,完成Promise.prototype.then函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

class Promise {
constructor(executor) {
this.state = PENDING
this.value = undefined
this.reason = undefined
executor(this.resolve, this.reject)
}

resolve(value) {
if (this.state === PENDING) {
this.value = value
this.state = RESOLVED
}
}

reject(reason) {
if (this.state === PENDING) {
this.reason = reason
this.state = REJECTED
}
}
}

Promise.prototype.then = (onFullFilled, onRejected) => {
if (this.state === RESOLVED) {
if (typeof onFullFilled === 'function') {
onFullFilled(this.value)
}
}
if (this.state === REJECTED) {
if (typeof onRejected === 'function') {
onRejected(this.reason)
}
}
}

module.exports = Promise

目前已经完成了Promise的基本功能,接下来解决异步问题。因为此时的代码还不支持Promise种传入异步函数。
我们可以创建两个数组onFulfilledFunc、onRejectedFunc 分别存放成功的回调和失败的回调,当then方法执行时,若状态还在等待态(pending),将回调函数依次放入数组中,这样在resolve和reject方法中可以分别将数组中的回调函数依次执行(resolve中执行onFulfilledFunc的所有方法,reject中执行onRejectedFunc的所有方法),具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
const PENDING = 'pending'
const RESOLVED = 'resolved'
const REJECTED = 'rejected'

class Promise {
constructor(executor) {
this.state = PENDING
this.value = undefined
this.reason = undefined
this.onFulfilledFunc = []; //保存成功回调
this.onRejectedFunc = []; //保存失败回调
executor(this.resolve, this.reject)
}

resolve(value) {
if (this.state === PENDING) {
this.value = value
this.onFulfilledFunc.forEach(fn => fn(value))
this.state = RESOLVED
}
}

reject(reason) {
if (this.state === PENDING) {
this.reason = reason
this.onRejectedFunc.forEach(fn => fn(reason))
this.state = REJECTED
}
}
}

Promise.prototype.then = (onFullFilled, onRejected) => {
if (this.state === PENDING) {
if (typeof onFulfilled === 'function') {
this.onFulfilledFunc.push(onFulfilled); //保存回调
}
if (typeof onRejected === 'function') {
this.onRejectedFunc.push(onRejected); //保存回调
}
}
if (this.state === RESOLVED) {
if (typeof onFullFilled === 'function') {
onFullFilled(this.value)
}
}
if (this.state === REJECTED) {
if (typeof onRejected === 'function') {
onRejected(this.reason)
}
}
}

module.exports = Promise

现在我们测试实现的Promise类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function task1() {
return new Promise(function(resolve, reject) {
console.log("task1");
})
}

function task2() {
return new Promise(function(resolve, reject) {
console.log("task2");
})
}

function task3() {
return new Promise(function(resolve, reject) {
console.log("task3");
})
}

// 调用函数
task1()
.then(task2())
.then(task3())

-----------------------
output:
task1
task2
task3

不过目前的Promise还存在一些问题:

  1. 尚不支持then链式调用
  2. 异常捕获
  3. all、race等方法实现

接下来实现链式调用和异常捕获:

  • 每个then方法都返回一个新的Promise对象(原理的核心)
  • 如果then方法中显示地返回了一个Promise对象就以此对象为准,返回它的结果
  • 如果then方法中返回的是一个普通值(如Number、String等)就使用此值包装成一个新的Promise对象返回。
  • 如果then方法中没有return语句,就视为返回一个用Undefined包装的Promise对象
  • 若then方法中出现异常,则调用失败态方法(reject)跳转到下一个then的onRejected
  • 如果then方法没有传入任何回调,则继续向下传递(值的传递特性)。

修改如下:

  1. 使MyPromise.prototype.then方法返回一个Promise
  2. 实现resolvePromise方法(核心)
  3. 重写MyPromise.prototype.then逻辑
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    const PENDING = 'pending'
    const RESOLVED = 'resolved'
    const REJECTED = 'rejected'

    class MyPromise {
    constructor(executor) {
    this.state = PENDING
    this.value = undefined
    this.reason = undefined
    this.onFulfilledFunc = []; //保存成功回调
    this.onRejectedFunc = []; //保存失败回调
    executor(this.resolve, this.reject)
    }
    resolve(value) {
    if (this.state === PENDING) {
    this.value = value
    this.onFulfilledFunc.forEach(fn => fn(value))
    this.state = RESOLVED
    }
    }
    reject(reason) {
    if (this.state === PENDING) {
    this.reason = reason
    this.onRejectedFunc.forEach(fn => fn(reason))
    this.state = REJECTED
    }
    }
    }

    /**
    * 解析then返回值与新Promise对象
    * @param {Object} promise2 新的Promise对象
    * @param {*} x 上一个then的返回值
    * @param {Function} resolve promise2的resolve
    * @param {Function} reject promise2的reject
    */
    function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
    reject(new TypeError('Promise发生了循环引用'));
    }
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    //可能是个对象或是函数
    try {
    let then = x.then;
    if (typeof then === 'function') {
    let y = then.call(x, (y) => {
    //递归调用,传入y若是Promise对象,继续循环
    resolvePromise(promise2, y, resolve, reject);
    }, (r) => {
    reject(r);
    });
    } else {
    resolve(x);
    }
    } catch (e) {
    reject(e);
    }
    } else {
    //是个普通值,最终结束递归
    resolve(x);
    }
    }

    MyPromise.prototype.then = (onFullfilled, onRejected) => {
    var promise2 = new Promise((resolve, reject) => {})
    var self = this
    if (this.state === PENDING) {
    promise2 = new Promise(function(resolve, reject) {
    if (typeof onFullFilled === 'function') {
    self.onRejectedFunc.push(function() {
    //x可能是一个promise,也可能是个普通值
    setTimeout(function() {
    try {
    let x = onFullfilled(self.value)
    resolvePromise(promise2, x, resolve, reject)
    } catch (err) {
    reject(err)
    }
    });
    })
    }
    if (typeof onRejected === 'function') {
    self.onRejectedFunc.push(function() {
    //x可能是一个promise,也可能是个普通值
    setTimeout(function() {
    try {
    let x = onRejected(self.reason)
    resolvePromise(promise2, x, resolve, reject)
    } catch (err) {
    reject(err)
    }
    });
    })
    }
    })
    }
    if (this.state === RESOLVED) {
    if (typeof onFullFilled === 'function') {
    promise2 = new Promise(function(resolve, reject) {
    //x可能是一个promise,也可能是个普通值
    setTimeout(function() {
    try {
    let x = infulfilled(self.value)
    onFullFilled(promise2, x, resolve, reject)
    } catch (err) {
    reject(err)
    }
    });
    })
    }
    }
    if (this.state === REJECTED) {
    if (typeof onRejected === 'function') {
    promise2 = new Promise(function(resolve, reject) {
    //x可能是一个promise,也可能是个普通值
    setTimeout(function() {
    try {
    let x = onRejected(self.reason)
    resolvePromise(promise2, x, resolve, reject)
    } catch (err) {
    reject(err)
    }
    });
    })
    }
    }
    return promise2
    }

    module.exports = MyPromise

思考 :如何实现Promise.all() Promise.race()方法?

封装ajax

搭建基本骨架:

1
2
3
4
5
6
7
8
9
10
var $ = (function() {
return {
ajax: function(opt) {
},
get: function(url, success) {
},
post: function(url, data, success) {
}
}
})();

这样的话,可以直接通过$.ajax()、$.get()、$.post()调用。接下来完善其中的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
var $ = (function() {
var o = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
if (!o) {
throw new Error('Your browser cannot support http')
}

function doAjax(opt) {
var opt = opt || {},
type = (opt.type || 'GET').toUpperCase(),
async = opt.async || true,
url = opt.url,
data = opt.data || null,
error = opt.error || function() {},
success = opt.success || function() {},
complete = opt.complete || function() {}

if (!url) {
throw new Error('no url')
}

o.open(type, url, async)
type === 'POST' && o.setRequestHeader('Content-type', 'application/x-www-form-urlencoded')
o.send(type === 'GET' ? null : formatDatas(data))
o.onreadystatechange = function() {
if (o.readyState === 4 && (o.status === 200 || status === 304)) {
success(JSON.parse(o.responseText))
} else {
error()
}
complete()
}
}

function formatDatas(obj) {
var str = ''
for (var key in obj) {
str += key + '=' + obj[key] + '&'
}
return str.replace(/&$/, '')
}

return {
ajax: function(opt) {
doAjax(opt)
},
get: function(url, success) {
doAjax({
type: 'GET',
url,
success
})
},
post: function(url, data, success) {
doAjax({
type: 'POST',
url,
data,
success
})
}
}
})();
// 测试
$.ajax({
url:'xxx',
type:'GET',
success:function(){}
})
$.get('xxx',fucntion(){})
$.post('xxx',{a=1,b=2},fucntion(){})

手写深拷贝

出自如何写出一个惊艳面试官的深拷贝?

浅拷贝:如果属性是值类型,拷贝的是值类型的值;如果属性是引用类型,拷贝的是引用类型的内存地址。其中一个对象改变了,另一个对象也会随之改变。

深拷贝:开辟一个新的区域存放新对象,且修改新对象不会影响原对象

对于深拷贝,我们可以简单地通过JSON.parse(JSON.stringify());这么一段简单地写法实现。但是它还存在着很大的缺陷,例如无法拷贝其他引用类型、拷贝函数、解决循环引用等情况。

1. 基础版本

  • 如果是值类型,无需继续拷贝,直接返回
  • 如果是引用类型,创建一个新对象,依次遍历将原对象上的属性拷贝到新对象上(采用递归实现)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function clone(target) {
    if (typeof target === 'object') {
    let cloneTarget = {};
    for (const key in target) {
    cloneTarget[key] = clone(target[key]);
    }
    return cloneTarget;
    } else {
    return target;
    }
    };

2. 考虑数组

  • 如果拷贝的数组,则不应该创建对象{},则是创建数组[],在前面加上判断即可。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function clone(target) {
    if (typeof target === 'object') {
    let cloneTarget = Array.isArray(target) ? [] : {};
    for (const key in target) {
    cloneTarget[key] = clone(target[key]);
    }
    return cloneTarget;
    } else {
    return target;
    }
    };

3. 解决循环引用

  • 以上的版本如果发生循环引用的话会发生栈内存溢出的情况
  • 这时,我们应该额外开辟一个存储空间,用来存放当前对象与拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中查找,如果有的话就直接返回,如果没有的话就继续拷贝
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function clone(target, map = new Map()) {
    if (typeof target === 'object') {
    let cloneTarget = Array.isArray(target) ? [] : {};
    if (map.get(target)) {
    return map.get(target);
    }
    map.set(target, cloneTarget);
    for (const key in target) {
    cloneTarget[key] = clone(target[key], map);
    }
    return cloneTarget;
    } else {
    return target;
    }
    };

在这里,我们也可以通过WeakMap来实现。如果我们创建了强引用的对象,我们只有手动设置为null才能被GC回收,如果是弱引用类型的话,GC会自动帮我们回收。

如果我们要拷贝的对象非常非常大时,使用Map会对内存造成很大的消耗,这时使用WeakMap可以解决这个问题。

4. 性能优化
当遍历数组时,直接使用forEach进行遍历,当遍历对象时,使用Object.keys取出所有的key进行遍历,然后在遍历时把forEach会调函数的value当作key使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function clone(target, map = new WeakMap()) {
if (typeof target === 'object') {
const isArray = Array.isArray(target);
let cloneTarget = isArray ? [] : {};

if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);

const keys = isArray ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone2(target[key], map);
});

return cloneTarget;
} else {
return target;
}
}

5. 考虑其他数据类型
首先,判断是否为引用类型,我们还需要考虑function和null两种特殊的数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
//判断是否为对象
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
if (!isObject(target)) {
return target;
}
//获取数据类型
function getType(target) {
return Object.prototype.toString.call(target);
}

下面我们抽离出一些常用的数据类型以便后面使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
//可以继续遍历的类型
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
//不可以继续遍历的类型
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';

可继续遍历的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function clone(target, map = new WeakMap()) {

// 克隆原始类型
if (!isObject(target)) {
return target;
}

// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
}

// 防止循环引用
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);

// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value,map));
});
return cloneTarget;
}

// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value,map));
});
return cloneTarget;
}

// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});

return cloneTarget;
}

不可继续遍历的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
default:
return null;
}
}
// clone Symbol
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}
// Clone Regexp
function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}
// Clone Function
const isFunc = typeof value == 'function'
if (isFunc || !cloneableTags[tag]) {
return object ? value : {}
}
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
console.log('普通函数');
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
console.log('匹配到函数体:', body[0]);
if (param) {
const paramArr = param[0].split(',');
console.log('匹配到参数:', paramArr);
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}

跨域实现原理

《非常全的跨域实现方案》

跨域方案

  1. jsonp
  2. cors
  3. postMessage
  4. document.domain
  5. window.name
  6. location.hash
  7. http-proxy
  8. nginx
  9. websocket

jsonp实现

  1. 只能发送get请求,不支持post/put/delete等
  2. 也不安全,存在xss攻击
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // 调用方法:
    jsonp({
    url: 'xxx',
    params: {
    wd: 'aa'
    },
    cb: 'show'
    }).then(data => {
    console.log(data);
    })


    function jsonp({
    url,
    params,
    cb
    }) {
    return new Promise((resolve, reject) => {
    window[cb] = function(data) {
    resolve(data)
    document.body.removeChild(script)
    }
    params = {
    ...params,
    cb
    }
    let arrs = []
    for (const key in params) {
    arrs.push(`${key}=${params[key]}`)
    }
    let script = document.createElement('script')
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
    })
    }

cors实现(最常用)——服务端设置

express示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const whiteList = ['http://localhost:3000']
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whiteList.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin) //允许哪个源可以访问我
res.setHeader('Access-Control-Allow-Headers', 'name') //允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Methods', 'POST') //允许哪个方法访问我
res.setHeader('Access-Control-Max-Age', 6) //预检的存活时间
res.setHeader('Access-Control-Allow-Credentials', true) //允许携带cookie
res.setHeader('Access-Control-Expose-Headers', 'name') //允许返回的头

// post/put请求前会发送options请求,试探作用,看服务器是否支持
if (req.method === 'OPTIONS') {
next()
}
}
next()
})

oauth原理

QQ互联

OAuth2.0原理与实现

具体流程

  1. 用户访问浏览器
  2. 授权页面,用户确认授权(生成一个code授权码)
  3. 回调url(授权码拼接到url请求得到token)
  4. 用户使用token访问
  5. 返回头像、昵称