高阶函数
在函数的执行前后添加自定义行为
// 什么是高阶函数? 1.一个函数返回一个函数 2.一个函数可以接收一个参数是函数
// 利用高阶函数可以处理哪些问题 1) 扩展方法
function say(args) { // 我们需要对say方法进行扩展,但是不能修改源代码
console.log('say', args)
}
Function.prototype.before = function (cb) {
return (...args) => { // newSay
cb();
this(...args); // 扩展原来的函数
}
}
let newSay = say.before(() => {
console.log('beforeSay')
})
newSay('hello');
// 我们可以通过高阶函数来实现参数的保留
// 判断一个变量的类型有: typeof只能判断基础类型 instanceof 判断实例类型 constuctor可以看当前实例是由谁构造出来的
// Object.prototype.toString.call
function isType(typing) {
return (val) => {
return Object.prototype.toString.call(val) === `[object ${typing}]`
}
}
// 利用高阶函数保留参数变量 -> (函数柯里化, 函数的反柯里化)
let isString = isType('String'); // 比较就是函数声明的作用域和执行的作用域是不一样的,这时候就会导致闭包
console.log(isString('hello'));
console.log(isString(123));
// 函数柯里化 就是将对个参转化成一次传入一个参数
// 异步编程问题 主要有一个并发处理的问题
after方法封装
// 前端常见的就是 同时发送多个请求,最终拿到多个请求的返回值 来进行渲染页面
const fs = require('fs'); //node中自带核心模块
const path = require('path');
// js在执行的时候会有一个事件环的机制 默认先执行当前的上下文
// let school = {}
// function done(){
// if(Object.keys(school).length === 2){
// console.log(school)
// }
// }
// 你不知道的javascript 开发者的理解
function after(times,callback){ // 暂存times 同时返回一个新的函数
const obj = {}
return function(value,key){ // done
obj[key] = value;
if(--times === 0){
callback(obj);
}
}
}
const done = after(2,(obj)=>{ // 调用done两次后再去执行对应的回调
console.log(obj);
})
fs.readFile(path.resolve(__dirname,'name.txt'),'utf8',function(err,data){
done(data,'name')
})
fs.readFile(path.resolve(__dirname,'age.txt'),'utf8',function(err,data){
done(data,'age')
})
// 将name和age放到同一个对象中
发布/订阅
const fs = require('fs');
const path = require('path');
let events = {
_events:[], // 如果events中存放的是promise compose
on(cb){
this._events.push(cb);
},
emit(...args){
this._events.forEach(cb=>cb(...args))
}
};
// 所谓的订阅就是把事情 存到一个列表中,每次发布将列表中的函数依次执行
events.on(function(key){ // 每次发布都执行以下此函数
console.log('读取完毕了一次',key)
})
let school = {}
events.on(function(key,data){ // 每次发布都执行以下此函数
school[key] = data;
if(Object.keys(school).length === 2){
console.log(school);
}
})
fs.readFile(path.resolve(__dirname, 'name.txt'), 'utf8', function (err, data) {
events.emit('name',data)
})
fs.readFile(path.resolve(__dirname, 'age.txt'), 'utf8', function (err, data) {
events.emit('age',data)
})
// 观察者模式和发布订阅模式的区别
// 发布和订阅之间没有耦合关系, 什么时候发布是取决于自己的
// 观察者模式 观察者,被观察者。 如果被观察者发生了变化,会主动通知观察者去更新 (收集:被观察者要收集观察者)
// 观察者模式是包含发布订阅的
观察者模式
// 被观察者类
class Subject {
constructor(name) {
this.name = name;
this.observers = [];
this.state = '开心'
}
attach(o) {
this.observers.push(o); // 被观察者会收集所有的观察者
}
setState(newState) {
this.state = newState;
this.observers.forEach(o => o.update(this)); // 通知所有的观察者我发生了变化
}
}
// 观察者类
class Observer {
constructor(name) {
this.name = name
}
update(s) {
console.log(`我是${this.name}:当前宝宝的状态`, s.state)
}
}
const baby = new Subject('小宝宝')
const m = new Observer('妈妈');
const f = new Observer('爸爸')
baby.attach(m);
baby.attach(f);
baby.setState('不开心')
setTimeout(()=>{
baby.setState('开心了')
},1000)
// 我家有个小宝宝 自己的状态就是是否开心, 我和我媳妇要监控小宝宝的状态, 稍后小宝宝不开心了 会通知我们
promise
使用及规范:
// Promise 默认是一个类 用的时候需要new ,而且创建的实例上都有一个then方法. 在new过程中需要传入一个执行器
// 1) promise中有一个value属性用来描述成功的原因 reason是一个失败的原因
// 2) promise中如果出现异常也会执行失败的逻辑
// 3) promise有三个状态 pending 既不成功也不失败 fulfilled 成功态 rejected失败态
// 4)当状态是pending的时候 可以转化成成功或者失败,否则不能去改变状态
// 5) executor会立刻执行,并且传入两个参数resolve, reject
const Promise = require('./my-promise/promise')
const p1 = new Promise((resolve, reject) => {
setTimeout(()=>{
reject('成功')
},1000);
});
p1.then((value) => { // then里面要有两个参数 onFulfilled, onRejected
console.log('成功',value)
}, (reason) => {
console.log('失败',reason)
})
p1.then((value) => { // then里面要有两个参数 onFulfilled, onRejected
console.log('成功',value)
}, (reason) => {
console.log('失败',reason)
})
// 实现promise的链式调用
有一点需要注意,在链式调用的过程中,无论前一个then返回的promise走到了成功回调还是失败回调,接下来的promise总是走成功回调:
new Promise((resolve, reject) => {
setTimeout(() => {
reject(10)
}, 1000)
}).then(res => {
console.log('第1轮', 'go succ callback,', res)
return 'succ'
}, res => {
console.log('第1轮', 'go fail callback,', res)
return 'fail'
}).then(res => {
console.log('第2轮', 'go succ callback,', res)
return 'succ'
}, res => {
console.log('第2轮', 'go fail callback,', res)
return 'fail'
}).then(res => {
console.log('第3轮', 'go succ callback,', res)
return 'succ'
}, res => {
console.log('第3轮', 'go fail callback,', res)
return 'fail'
})
执行结果为:
第1轮 go fail callback, 10
第2轮 go succ callback, fail
第3轮 go succ callback, succ
从执行结果中可以看到,最开始new出来的那个Promise在setTimeout里面执行了reject,所以这个promise就变成了rejected状态,进而执行了失败回调,因此console打印出了
第1轮 go fail callback, 10
但后面的promise依然执行了成功回调
当然,如果在回调中throw出来了Error,无论是从成功回调中throw的,还是从失败回调中throw的,都会走到失败回调,例如:
new Promise((resolve, reject) => {
setTimeout(() => {
reject(10)
}, 1000)
}).then(res => {
console.log('第1轮', 'go succ callback', res)
return 'succ'
}, res => {
console.log('第1轮', 'go fail callback', res)
return 'fail'
}).then(res => {
console.log('第2轮', 'go succ callback', res)
throw new Error('aaa');
return 'succ'
}, res => {
console.log('第2轮', 'go fail callback', res)
return 'fail'
}).then(res => {
console.log('第3轮', 'go succ callback', res)
return 'succ'
}, res => {
console.log('第3轮', 'go fail callback', res)
return 'fail'
})
就会在控制台打出如下信息:
第1轮 go fail callback 10
第2轮 go succ callback fail
第3轮 go fail callback Error: aaa
at sn (1.html:21:13)
但是,如果在then链条中,没有传失败回调的话,需要将reject传递到之后的promise的失败回调中,例如:
new Promise((resolve, reject) => {
setTimeout(() => {
reject(10)
}, 1000)
}).then(res => {
console.log('第1轮', 'go succ callback', res)
return 'succ'
}).then(res => {
console.log('第2轮', 'go succ callback', res)
return 'succ'
}, res => {
console.log('第2轮', 'go fail callback', res)
return 'fail'
}).then(res => {
console.log('第3轮', 'go succ callback', res)
return 'succ'
}, res => {
console.log('第3轮', 'go fail callback', res)
return 'fail'
})
打印出的结果为:
135****8960
第2轮 go fail callback 10
第3轮 go succ callback fail
new出的Promise在调用then添加回调的时候没有绑定失败回调,因此new Promise里reject后,就会传导到下一个promise的then绑定的失败回调中去,所以输出了
第2轮 go fail callback 10
基本实现:
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
class Promise {
constructor(executor) {
this.status = PENDING;// promise的默认状态
this.value = undefined; // 成功的值和失败的原因
this.reason = undefined;
this.onResolvedCallbacks = []; // 这里存放所有成功的回调
this.onRejectedCallbacks = []; // 所有失败的回调
const resolve = (value) => { // 更改状态的方法 resolve
if(this.status == PENDING){
this.value = value;
this.status = FULFILLED
this.onResolvedCallbacks.forEach(cb=>cb(this.value))
}
}
const reject = (reason) => { // 更改状态的方法 reject
if(this.status === PENDING){
this.reason = reason;
this.status = REJECTED
this.onRejectedCallbacks.forEach(cb=>cb(this.reason))
}
}
try{
executor(resolve, reject); // executor就是执行器立刻执行,出错就调用reject
}catch(e){
reject(e);
}
}
then(onFulfilled, onRejected){ // 调用then的时候会判断是成功还是失败
if(this.status === FULFILLED){
onFulfilled(this.value);
}
if(this.status === REJECTED){
onRejected(this.reason)
}
if(this.status == PENDING){
// 发布订阅 有可能调用then的时候没成功也没失败,我就将回调存起来,稍后根据用户调用的方法在进行执行
this.onResolvedCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected)
}
}
}
module.exports = Promise;
支持链式调用:
1.promise中then传入的方法 返回的如果不是promise那么会将这个结果传递给下一次then的成功中去
2.如果then传入的方法在执行的时候出错了 会执行下一次then的失败
3.如果then传入的方法在执行返回的是一个promise,那么会根据promise的状态来决定走下一次then的成功还是失败.成功的值和失败的原因会以这个promise的结果为准
什么情况会走失败?
1) 抛出异常
2) 如果返回的是一个失败的promise会走失败
其他情况全部走成功
使用案例:
const fs = require('fs');
const path = require('path');
// 希望解决回调地狱的问题,恶魔金字塔。 就采用promise来解决这个问题
function readFile(url,encoding){
return new Promise((resolve,reject)=>{
fs.readFile(url,encoding,function(err,data){
if(err) return reject(err);
resolve(data)
})
})
}
// 1.promise中then传入的方法 返回的如果不是promise那么会将这个结果传递给下一次then的成功中去
// 2.如果then传入的方法在执行的时候出错了 会执行下一次then的失败
// 3.如果then传入的方法在执行返回的是一个promise,那么会根据promise的状态来决定走下一次then的成功还是失败.成功的值和失败的原因会以这个promise的结果为准
// 什么情况会走失败?1) 抛出异常 2) 如果返回的是一个失败的promise会走失败
// 其他情况全部走成功
readFile(path.resolve(__dirname,'file.txt'),'utf8').then(data=>{
return readFile(path.resolve(__dirname,data+1),'utf8')
}).then((data)=>{
console.log(data,'s');
},(err=>{
console.log(err,'e')
})).then(()=>{
console.log('成功')
},()=>{
console.log('失败')
})
实现:
then需要返回一个promise,由于传入Promise构造函数的回调是会立即执行的,所以我们可以把then里面的发布、订阅这些操作放到返回的这个promise的初始化回调函数中,这样我们就可以很方便的将订阅的函数的返回值给下一个promise传过去:
then(onFulfilled, onRejected){
let p1 = new Promise((resolve, reject) => {
// x是一个普通值 则将这个值直接传入到resolve函数中即可
if(this.status === FULFILLED){
let x = onFulfilled(this.value);
resolve(x);
}
if(this.status === REJECTED){
let x = onRejected(this.reason);
resolve(x);
}
if(this.status === PENDING){
this.onResolvedCallbacks.push(() => {
let x = onFulfilled(this.value);
resolve(x);
});
this.onRejectedCallbacks.push(() => {
let x = onRejected(this.reason);
resolve(x);
})
}
})
}
这样对于订阅了有异步操作的方法,如:
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('123')
}, 1000)
}).then((data) => {
return data
})
p1.then((data) => {
console.log(data, '000')
}, err => {
console.log(err, 'err');
});
为表述方便,此处将含有定时器的Promise命名为p0
上述Promise实现的思路就是,将p0的发布和p1的订阅放在一个回调中执行,这也是在then方法中自定义push进去的方法的用意所在,这样即可保证链式调用的衔接
通过then添加的,无论是成功还是失败的回调,只要出现异常,该then方法返回的promise就要走失败的一系列回调
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('123')
}, 1000)
}).then((data) => {
throw new Error('fail')
})
p1.then((data) => {
console.log(data, '000')
}, err => {
console.log(err, 'err');
});
上面案例中,console应该打印出err
因此我们需要在回调执行时,加上catch
then(onFulfilled, onRejected){
let p1 = new Promise((resolve, reject) => {
// x是一个普通值 则将这个值直接传入到resolve函数中即可
if(this.status === FULFILLED){
try {
let x = onFulfilled(this.value);
resolve(x);
} catch (e) {
reject(e);
}
}
if(this.status === REJECTED){
try {
let x = onRejected(this.reason);
resolve(x);
} catch (e) {
reject(e);
}
}
if(this.status === PENDING){
this.onResolvedCallbacks.push(() => {
try {
let x = onFulfilled(this.value);
resolve(x);
} catch (e) {
reject(e);
}
});
this.onRejectedCallbacks.push(() => {
try {
let x = onRejected(this.reason);
resolve(x);
} catch (e) {
reject(e);
}
})
}
})
}
在then中订阅的方法,除了可以返回一个普通的值之外,还可以返回一个promise实例
这时,我们需要单独封装一个方法来对resolve的执行进行一些处理,这个处理主要包含将p1的resolve的时机,延迟到返回值x这个promise对象的resolve之后(或者说将p1的resolve作为一个方法订阅到x的回调列表中去),为此我们要在p1 new的构造函数中使用p1,如果不做其他处理,这种操作是不被允许的,所以我们需要包装一层定时器
then(onFulfilled, onRejected) { // 调用then的时候会判断是成功还是失败
// 可以不停的then下去
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v
onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e }
// mutationObserver
let p1 = new Promise((resolve, reject) => {
// x是一个普通值 则将这个值直接传入到resolve函数中即可
if (this.status === FULFILLED) {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(p1, x, resolve, reject)
} catch (e) {
reject(e);
}
})
}
if (this.status === REJECTED) {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(p1, x, resolve, reject)
} catch (e) {
reject(e);
}
})
}
if (this.status == PENDING) {
// 发布订阅 有可能调用then的时候没成功也没失败,我就将回调存起来,稍后根据用户调用的方法在进行执行
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value);
resolvePromise(p1, x, resolve, reject)
} catch (e) {
reject(e);
}
})
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(p1, x, resolve, reject)
} catch (e) {
reject(e);
}
})
})
}
});
return p1; // 这里没有返回this
}
function resolvePromise(promise, x, resolve, reject) { // x可能是别人家的promise
// 用x的值来决定promise走 resolve还是reject
// 核心就是用x 来处理promise是成功还是失败
// 我们要考虑不同人写的promise可以互相兼容,所以这里要按照规范来实现,保证promise直接可以互相调用
// 判断x 是不是一个promise 是promise就采用他的状态,如果解析后还是promise会递归解析
if (promise == x) {
return reject(new TypeError(`TypeError: Chaining cycle detected for promise #<myPromise> `));
}
// 判断x 是不是一个promise, 如果不是promise,则直接用这个值将promise变成成功态即可
if ((typeof x === 'object' && x !== null) || typeof x === 'function') {
let called = false;
try {
let then = x.then; // 这个x可能是通过defineProperty定义的then
if (typeof then === 'function') { // 这已经最小判断
// x.then((y)=>{},r=>{}) // x.then 会再去取then
// 这个then方法可能是别人家的promise, 没有处理同时调用成功和失败方法
then.call(x, y => { // 如果x是一个promise就用他的状态来决定 走成功还是失败
if (called) return;
called = true
resolvePromise(promise, y, resolve, reject); //递归解析y的值
}, r => { // 一旦失败了 就不在解析失败的结果了
if (called) return;
called = true
reject(r)
})
} else {
// {} / function 没有then方法 依旧是普通值 {then:123}
resolve(x)
}
} catch (e) {
if (called) return;
called = true
reject(e);
}
} else {
// 不是对象和函数 普通值
resolve(x)
}
}
测试我们自己写的promise是否符合promise a+规范:
npm install promises-aplus-test -g 全局安装都是在命令行中使用
执行如下命令:promises-aplus-test 测试的文件名
比较老的一些库中,有deferred这样一个属性:
Promise.deferred = function () {
let dfd = {};
dfd.promise = new Promise((resolve, reject) => {
dfd.resolve = resolve;
dfd.reject = reject;
});
return dfd
}
使用案例:
const Promise1 = require('./my-promise/promise');
function readFile(url, encoding) {
// 延迟对象的使用场景
let dfd = Promise1.deferred();
fs.readFile(path.resolve(__dirname, url), encoding, function (err, data) {
if (err) return dfd.reject(err);
dfd.resolve(data);
})
return dfd.promise
}
readFile('file.txt', 'utf8').then(data => {
console.log(data)
})
我们经常通过Promise.resolve('xxx')来直接得到一个执行过resolve的Promise,可以添加如下静态方法来实现
class Promise {
//...
// 类本身调用的叫静态方法
static resolve(value) {
return new Promise((resolve, reject) => {
resolve(value)
})
}
static reject(value) {
// 默认创建一个失败的promise
return new Promise((resolve, reject) => {
reject(value)
})
}
如果只希望订阅一个失败回调,可以通过catch简化订阅的方式,而不必通过then传两个参数,第一个放一个null在那里占位
catch(errCallback) {
return this.then(null, errCallback)
}
Promise.all
static all = function (promises) {
let result = [];
let times = 0;
return new Promise((resolve, reject) => {
function processResult(data, index) {
result[index] = data; // 映射结果
if (++times == promises.length) {
resolve(result);
}
}
for (let i = 0; i < promises.length; i++) {
let promise = promises[i];
Promise.resolve(promise).then((data) => {
processResult(data, i);
}, reject)
}
})
}
注意:第7行不能写成result.length === promises.length这样来比较,
原因:假设有3个promise,result初始为空数组,如果第2个promise先resolve了,则会执行result[2] = xxx,然后result数组就会成为
[empty x 2, xxx]
但它的length是3
还有,在第13行中,调用Promise.resolve的时候直接传入了一个Promise类型的值:promise,沿着Promise.resolve这个静态方法走,这接下来会走到我们的构造函数中的resolve中去,我们需要对resolve的形参类型做一下兼容:
class Promise {
constructor(executor) {
this.status = PENDING;// promise的默认状态
this.value = undefined; // 成功的值和失败的原因
this.reason = undefined;
this.onResolvedCallbacks = []; // 这里存放所有成功的回调
this.onRejectedCallbacks = []; // 所有失败的回调
const resolve = (value) => { // 更改状态的方法 resolve
if (value instanceof Promise) { // 这个不属于规范
return value.then(resolve, reject)
}
if (this.status == PENDING) {
this.value = value;
this.status = FULFILLED
this.onResolvedCallbacks.forEach(cb => cb(this.value))
}
}
上述代码中,第10行value instanceof Promise这个条件将会是true,然后我们将resolve和reject再订阅给value,接下来就会走到13行的条件里了
node中util.promisify将回调类api转换为promise
let util = require('util'); // 工具方法
let readFile = util.promisify(fs.readFile); // 仅仅针对node中的api 因为node中的回调方法 都有err和data
使用方式:
Promise.all([readFile(path.resolve(__dirname, 'name.txt'), 'utf8'), readFile(path.resolve(__dirname, 'age.txt'), 'utf8'), '123']).then(data => {
console.log(data)
})
可以猜测到,promisify的实现方式,用到了高阶函数
function promisify(fn) { // fs.readFile
return function (...args) { // readFile
return new Promise((resolve, reject) => {
fn(...args, function (err, data) {
if (err) return reject(err);
resolve(data)
})
})
}
}
将fs下所有方法都转成promise:
let fsPromises = promisifyAll(fs);
function promisifyAll(obj) {
let result = {}
for (let key in obj) { // 判断值是不是一个函数,是函数就转化成promise
result[key] = typeof obj[key] === 'function' ? promisify(obj[key]) : obj[key]
}
return result;
}
Promise.all([fsPromises.readFile(path.resolve(__dirname, 'name.txt'), 'utf8'), fsPromises.readFile(path.resolve(__dirname, 'age.txt'), 'utf8'), '123']).then(data => {
console.log(data)
})
race方法:
赛跑就是以第一个结果为基准, 取最先的结果为准,其他的代码还会执行,只是不采用结果了
Promise.race = function (promises) {
return new Promise(resolve, reject) {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(resolve, reject)
}
}
}
race的使用案例:我们可能会有一个超时逻辑,超过1s 就不采用成功的结果了
假设某个promise做加载工作,耗时2s返回了结果:
let abort
let p = new Promise((resolve, reject) => {
abort = reject;
setTimeout(() => {
resolve('data~~~');
}, 2000);
});
setTimeout(() => {
abort('超时了'); // 这里只是借助了promise一旦失败就不能成功了,但是promise原有的逻辑还是会执行
}, 1000)
但是这样一来就会出现abort这样的全局变量,所以这个逻辑需要封装一下:
用户自己的promise是p,经过wrapPromise对p包装后,再将p重新赋值,给p添加了abort方法,这个abort指向内部promise的reject
看userPromise和internalPromise哪个先有结果:
如果先调用了超时,则internalPromise先有结果,这个结果是失败,userPromise就不再执行
如果我们将setTimeout的时间改成3000,则userPromise先有结果,这个结果是成功,userPromise的resolve被执行
let p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('data~~~');
}, 2000);
});
setTimeout(() => {
p.abort('超时了'); // 这里只是借助了promise一旦失败就不能成功了,但是promise原有的逻辑还是会执行
}, 1000)
// 我们可能会有一个超时逻辑,超过1s 就不采用成功的结果了
function wrapPromise(userPromise) {
// race 有任何一个失败了就失败 [自己造一个promise,userPromise]
let abort;
let internalPromise = new Promise((resolve, reject) => {
abort = reject;
})
let racePromise = Promise.race([internalPromise,userPromise]);
racePromise.abort = abort;
return racePromise
}
p = wrapPromise(p);
p.then(data => {
console.log(data);
}).catch(err => {
console.log(err);
});
Promise.allSettled方法,无论成功还是失败,都将结果返回
Promise.allSettled([
fsPromises.readFile(path.resolve(__dirname, 'file.txt'), 'utf8'),
fsPromises.readFile(path.resolve(__dirname, 'file1.txt'))]).then(data => {
console.log(data)
})
file1.txt这个文件不存在,但then的data参数中依然会打印出所有promise的结果
Promise.allSettled = function (promises) {
let result = [];
let times = 0;
return new Promise((resolve, reject) => {
function processResult(data, index, status) {
result[index] = { status, value: data }; // 映射结果
if (++times == promises.length) {
resolve(result);
}
}
for (let i = 0; i < promises.length; i++) {
let promise = promises[i];
Promise.resolve(promise).then((data) => {
processResult(data, i, 'fulfilled');
}, (err) => {
processResult(err, i, 'rejected');
})
}
})
}
finally:
// finnaly 无论成功和失败都执行的方法
Promise.prototype.finally = function (finallyCallback) {
return this.then((data) => {
return Promise.resolve(finallyCallback()).then(() => data);
}, (err) => {
return Promise.resolve(finallyCallback()).then(() => { throw err })
})
}
路径相关
let path = require('path');
//从当前路径出发,得到一个绝对路径
//C:\aproject\zhufengwebpack202011\10.hmr9000\doc\b
console.log(path.resolve('b'));
//解析模块路径
//c:\aproject\zhufengwebpack202011\10.hmr9000\node_modules\webpack\lib\index.js
console.log(require.resolve('webpack'));
path只是做拼接,不管这个文件是不是真正存在
而resolve会检查模块是不是存在