Ari1c

Ari1c 查看完整档案

杭州编辑  |  填写毕业院校  |  填写所在公司/组织 github.com/Ari1c 编辑
编辑

bug

个人动态

Ari1c 收藏了文章 · 10月14日

异步Promise及Async/Await可能最完整入门攻略

此文只介绍Async/Await与Promise基础知识与实际用到注意的问题,将通过很多代码实例进行说明,两个实例代码是setDelaysetDelaySecond

tips:本文系原创转自我的博客异步Promise及Async/Await最完整入门攻略,欢迎前端大神交流,指出问题


一、为什么有Async/Await?

我们都知道已经有了Promise的解决方案了,为什么还要ES7提出新的Async/Await标准呢?

答案其实也显而易见:Promise虽然跳出了异步嵌套的怪圈,用链式表达更加清晰,但是我们也发现如果有大量的异步请求的时候,流程复杂的情况下,会发现充满了屏幕的then,看起来非常吃力,而ES7的Async/Await的出现就是为了解决这种复杂的情况。

首先,我们必须了解Promise

二、Promise简介

2.1 Promise实例

什么是Promise,很多人应该都知道基础概念?直接看下面的代码(全文的例子都是基于setDelaySecondsetDelay两个函数,请务必记住):

const setDelay = (millisecond) => {
  return new Promise((resolve, reject)=>{
      if (typeof millisecond != 'number') reject(new Error('参数必须是number类型'));
      setTimeout(()=> {
        resolve(`我延迟了${millisecond}毫秒后输出的`)
      }, millisecond)
  })
}

我们把一个Promise封装在一个函数里面同时返回了一个Promise,这样比较规范。

可以看到定义的Promise有两个参数,resolvereject

  • resolve:将异步的执行从pending(请求)变成了resolve(成功返回),是个函数执行返回。
  • reject:顾名思义“拒绝”,就是从请求变成了"失败",是个函数可以执行返回一个结果,但我们这里推荐大家返回一个错误new Error()
上述例子,你可以reject('返回一个字符串'),随便你返回,但是我们还是建议返回一个Error对象,这样更加清晰是“失败的”,这样更规范一点

2.2 Promise的then和catch

我们通过Promise的原型方法then拿到我们的返回值:

setDelay(3000)
.then((result)=>{
    console.log(result) // 输出“我延迟了2000毫秒后输出的”
})

输出下列的值:“我延迟了2000毫秒后输出的”。

如果出错呢?那就用catch捕获:

setDelay('我是字符串')
.then((result)=>{
    console.log(result) // 不进去了
})
.catch((err)=>{
    console.log(err) // 输出错误:“参数必须是number类型”
})

是不是很简单?好,现在我增加一点难度,如果多个Promise执行会是怎么样呢?

2.3 Promise相互依赖

我们在写一个Promise:

const setDelaySecond = (seconds) => {
  return new Promise((resolve, reject)=>{
      if (typeof seconds != 'number' || seconds > 10) reject(new Error('参数必须是number类型,并且小于等于10'));
      setTimeout(()=> {
        console.log(`先是setDelaySeconds函数输出,延迟了${seconds}秒,一共需要延迟${seconds+2}秒`)
        resolve(setDelay(2000)) // 这里依赖上一个Promise
      }, seconds * 1000)
  })
}

在下一个需要依赖的resolve去返回另一个Promise,会发生什么呢?我们执行一下:

setDelaySecond(3).then((result)=>{
  console.log(result)
}).catch((err)=>{
  console.log(err);
})

你会发现结果是先执行:“先是setDelaySeconds输出,延迟了2秒,一共需要延迟5秒”

再执行setDelayresolve“我延迟了2000毫秒后输出的”。的确做到了依次执行的目的。

有人说,我不想耦合性这么高,想先执行setDelay函数再执行setDelaySecond,但不想用上面那种写法,可以吗,答案是当然可以。

2.4 Promise链式写法

先改写一下setDelaySecond,拒绝依赖,降低耦合性

const setDelaySecond = (seconds) => {
  return new Promise((resolve, reject)=>{
      if (typeof seconds != 'number' || seconds > 10) reject(new Error('参数必须是number类型,并且小于等于10'));
      setTimeout(()=> {
        resolve(`我延迟了${seconds}秒后输出的,是第二个函数`)
      }, seconds * 1000)
  })
}

先执行setDelay在执行setDelaySecond,只需要在第一个then的结果中返回下一个Promise就可以一直链式写下去了,相当于依次执行

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(3)
})
.then((result)=>{
  console.log('我进行到第二步的');
  console.log(result);
}).catch((err)=>{
  console.log(err);
})

发现确实达到了可喜的链式(终于脱离异步嵌套苦海,哭),可以看到then的链式写法非常优美。

2.5 链式写法需要注意的地方

这里一定要提到一点:

then式链式写法的本质其实是一直往下传递返回一个新的Promise,也就是说then在下一步接收的是上一步返回的Promise,理解这个对于后面的细节非常重要!!

那么并不是这么简单,then的返回我们可以看出有2个参数(都是回调):

  • 第一个回调是resolve的回调,也就是第一个参数用得最多,拿到的是上一步的Promise成功resolve的值。
  • 第二个回调是reject的回调,用的不多,但是求求大家不要写错了,通常是拿到上一个的错误,那么这个错误处理和catch有什么区别和需要注意的地方呢?

我们修改上面的代码:

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(20)
})
.then((result)=>{
  console.log('我进行到第二步的');
  console.log(result);
}, (_err)=> {
  console.log('我出错啦,进到这里捕获错误,但是不经过catch了');
})
.then((result)=>{
  console.log('我还是继续执行的!!!!')
})
.catch((err)=>{
  console.log(err);
})

可以看到输出结果是:进到了then的第二个参数(reject)中去了,而且最重要的是!不再经过catch了。

那么我们把catch挪上去,写到then错误处理前:

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(20)
})
.catch((err)=>{ // 挪上去了
  console.log(err); // 这里catch到上一个返回Promise的错误
})
.then((result)=>{
  console.log('我进行到第二步的');
  console.log(result);
}, (_err)=> {
  console.log('我出错啦,但是由于catch在我前面,所以错误早就被捕获了,我这没有错误了');
})
.then((result)=>{
  console.log('我还是继续执行的!!!!')
})

可以看到先经过catch的捕获,后面就没错误了。

可以得出需要注意的:

  • catch写法是针对于整个链式写法的错误捕获的,而then第二个参数是针对于上一个返回Promise的。
  • 两者的优先级:就是看谁在链式写法的前面,在前面的先捕获到错误,后面就没有错误可以捕获了,链式前面的优先级大,而且两者都不是break, 可以继续执行后续操作不受影响。

2.5 链式写法的错误处理

上述已经写好了关于then里面三个回调中第二个回调(reject)会与catch冲突的问题,那么我们实际写的时候,参数捕获的方式基本写得少,catch的写法会用到更多。

既然有了很多的Promise,那么我需不需要写很多catch呢?

答案当然是:不需要!,哪有那么麻烦的写法,只需要在末尾catch一下就可以了,因为链式写法的错误处理具有“冒泡”特性,链式中任何一个环节出问题,都会被catch到,同时在某个环节后面的代码就不会执行了

既然说到这里,我们把catch移到第一个链式的返回里面会发生什么事呢?看下面代码:

setDelay('2000')
.then((result)=>{
  console.log('第一步完成了');
  console.log(result)
  return setDelaySecond(3)
})
.catch((err)=>{ // 这里移到第一个链式去,发现上面的不执行了,下面的继续执行
  console.log(err);
})
.then((result)=>{
  console.log('第二步完成了');
  console.log(result);
})

惊喜的发现,链式继续走下去了!!输出如下(undefined是因为上一个then没有返回一个Promise):

clipboard.png

重点来了!敲黑板!!链式中的catch并不是终点!!catch完如果还有then还会继续往下走!不信的话可以把第一个catch在最后面的那个例子后面再加几个then,你会发现并不会跳出链式执行

如果顺序执行setDelay,setDelay1,setDelaySecond,按照上述的逻辑,流程图可以概括如下:

clipboard.png

catch只是捕获错误的一个链式表达,并不是break!

所以,catch放的位置也很有讲究,一般放在一些重要的、必须catch的程序的最后。**这些重要的程序中间一旦出现错误,会马上跳过其他后续程序的操作直接执行到最近的catch代码块,但不影响catch后续的操作!!!!

到这就不得不体一个ES2018标准新引入的Promise的finally,表示在catch后必须肯定会默认执行的的操作。这里不多展开,细节可以参考:Promise的finally

2.5 Promise链式中间想返回自定义的值

其实很简单,用Promise的原型方法resolve即可:

setDelay(2000).then((result)=>{
  console.log('第一步完成了');
  console.log(result);
  let message = '这是我自己想处理的值'; 
  return Promise.resolve(message) // 这里返回我想在下一阶段处理的值
})
.then((result)=>{
  console.log('第二步完成了');
  console.log(result); // 这里拿到上一阶段的返回值
  //return Promise.resolve('这里可以继续返回')
})
.catch((err)=>{
  console.log(err);
})

2.7 如何跳出或停止Promise链式

不同于一般的functionbreak的方式,如果你是这样的操作:func().then().then().then().catch()的方式,你想在第一个then就跳出链式,后面的不想执行了,不同于一般的break;return null;return false等操作,可以说,如何停止Promise链,是一大难点,是整个Promise最复杂的地方。

1.用链式的思维想,我们拒绝掉某一链,那么不就是相当于直接跳到了catch模块吗?

我们是不是可以直接“拒绝“掉达到停止的目的?

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(1)
})
.then((result)=>{
  console.log('我进行到第二步的');
  console.log(result);
  console.log('我主动跳出循环了');
  return Promise.reject('跳出循环的信息') // 这里返回一个reject,主动跳出循环了
})
.then((result)=>{
  console.log('我不执行');
})
.catch((mes)=>{
  console.dir(mes)
  console.log('我跳出了');
})

但是很容易看到缺点:有时候你并不确定是因为错误跳出的,还是主动跳出的,所以我们可以加一个标志位:

return Promise.reject({
    isNotErrorExpection: true // 返回的地方加一个标志位,判断是否是错误类型,如果不是,那么说明可以是主动跳出循环的
}) 

或者根据上述的代码判断catch的地方输出的类型是不是属于错误对象的,是的话说明是错误,不是的话说明是主动跳出的,你可以自己选择(这就是为什么要统一错误reject的时候输出new Error('错误信息')的原因,规范!)

当然你也可以直接抛出一个错误跳出:

throw new Error('错误信息') // 直接跳出,那就不能用判断是否为错误对象的方法进行判断了

2.那有时候我们有这个需求:catch是放在中间(不是末尾),而同时我们又不想执行catch后面的代码,也就是链式的绝对中止,应该怎么办?

我们看这段代码:

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(1)
})
.then((result)=>{
  console.log('我进行到第二步的');
  console.log(result);
  console.log('我主动跳出循环了');
  return Promise.reject('跳出循环的信息') // 这里直接调用Promise原型方法返回一个reject,主动跳出循环了
})
.then((result)=>{
  console.log('我不执行');
})
.catch((mes)=>{
  console.dir(mes)
  console.log('我跳出了');
})
.then((res)=>{
    console.log('我不想执行,但是却执行了'); // 问题在这,上述的终止方法治标不治本。
})

这时候最后一步then还是执行了,整条链都其实没有本质上的跳出,那应该怎么办呢?

敲黑板!!重点来了!我们看Promise/A+规范可以知道:

A promise must be in one of three states: pending, fulfilled, or rejected.

Promise其实是有三种状态的:pending,resolve,rejected,那么我们一直在讨论resolve和rejected这2个状态,是不是忽视了pending这个状态呢?pending状态顾名思义就是请求中的状态,成功请求就是resolve,失败就是reject,其实他就是个中间过渡状态。

而我们上面讨论过了,then的下一层级其实得到的是上一层级返回的Promise对象,也就是说原Promise对象与新对象状态保持一致。那么重点来了,如果你想在这一层级进行终止,是不是直接让它永远都pending下去,那么后续的操作不就没了吗?是不是就达到这个目的了??觉得有疑问的可以参考Promise/A+规范。

我们直接看代码:

setDelay(2000)
.then((result)=>{
  console.log(result)
  console.log('我进行到第一步的');
  return setDelaySecond(1)
})
.then((result)=>{
  console.log(result);
  console.log('我主动跳出循环了');
  // return Promise.reject('跳出循环的信息')
  // 重点在这
  return new Promise(()=>{console.log('后续的不会执行')}) // 这里返回的一个新的Promise,没有resolve和reject,那么会一直处于pending状态,因为没返回啊,那么这种状态就一直保持着,中断了这个Promise
})
.then((result)=>{
  console.log('我不执行');
})
.catch((mes)=>{
  console.dir(mes)
  console.log('我跳出了');
})
.then((res)=>{
  console.log('我也不会执行')
})

这样就解决了上述,错误跳出而导致无法完全终止Promise链的问题。

但是!随之而来也有一个问题,那就是可能会导致潜在的内存泄漏,因为我们知道这个一直处于pending状态下的Promise会一直处于被挂起的状态,而我们具体不知道浏览器的机制细节也不清楚,一般的网页没有关系,但大量的复杂的这种pending状态势必会导致内存泄漏,具体的没有测试过,后续可能会跟进测试(nodeJS或webapp里面不推荐这样),而我通过查询也难以找到答案,这篇文章可以推荐看一下:从如何停掉 Promise 链说起。可能对你有帮助在此种情况下如何做。

当然一般情况下是不会存在泄漏,只是有这种风险,无法取消Promise一直是它的痛点。而上述两个奇妙的取消方法要具体情形具体使用。

2.8 Promise.all

其实这几个方法就简单了,就是一个简写串联所有你需要的Promise执行,具体可以参照阮一峰的ES6Promise.all教程

我这上一个代码例子

Promise.all([setDelay(1000), setDelaySecond(1)]).then(result=>{
  console.log(result);
})
.catch(err=>{
  console.log(err);
})
// 输出["我延迟了1000毫秒后输出的", "我延迟了1秒后输出的,注意单位是秒"]

输出的是一个数组,相当于把all方法里面的Promise并行执行,注意是并行。
相当于两个Promise同时开始执行,同时返回值,并不是先执行第一个再执行第二个,如果你想串行执行,请参考我后面写的循环Promise循环串行(第4.2小节)

然后把resolve的值保存在数组中输出。类似的还有Promise.race这里就不多赘述了。

三、Async/await介绍

3.1 基于Promise的Async/await

什么是async/await呢?可以总结为一句话:async/await是一对好基友,缺一不可,他们的出生是为Promise服务的。可以说async/await是Promise的爸爸,进化版。为什么这么说呢?且听我细细道来。

为什么要有async/await存在呢?

前文已经说过了,为了解决大量复杂不易读的Promise异步的问题,才出现的改良版。

这两个基友必须同时出现,缺一不可,那么先说一下Async

async function process() {
}

上面可以看出,async必须声明的是一个function,不要去声明别的,要是那样await就不理你了(报错)。

这样声明也是错的!

const async demo =  function () {} // 错误

必须紧跟着function。接下来说一下它的兄弟await

上面说到必须是个函数(function),那么await就必须是在这个async声明的函数内部使用,否则就会报错。

就算你这样写,也是错的。

let data = 'data'
demo  = async function () {
    const test = function () {
        await data
    }
}

必须是直系(作用域链不能隔代),这样会报错:Uncaught SyntaxError: await is only valid in async function

讲完了基本规范,我们接下去说一下他们的本质。

3.2 async的本质

敲黑板!!!很重要!async声明的函数的返回本质上是一个Promise

什么意思呢?就是说你只要声明了这个函数是async,那么内部不管你怎么处理,它的返回肯定是个Promise。

看下列例子:

(async function () {
    return '我是Promise'
})()
// 返回是Promise
//Promise {<resolved>: "我是Promise"}

你会发现返回是这个:Promise {<resolved>: "我是Promise"}

自动解析成Promise.resolve('我是Promise');

等同于:

(async function () {
    return Promise.resolve('我是Promise');
})()

所以你想像一般function的返回那样,拿到返回值,原来的思维要改改了!你可以这样拿到返回值:

const demo = async function () {
    return Promise.resolve('我是Promise');
    // 等同于 return '我是Promise'
    // 等同于 return new Promise((resolve,reject)=>{ resolve('我是Promise') })
}
demo.then(result=>{
    console.log(result) // 这里拿到返回值
})

上述三种写法都行,要看注释细节都写在里面了!!像对待Promise一样去对待async的返回值!!!

好的接下去我们看await的干嘛用的.

3.3 await的本质与例子

await的本质是可以提供等同于”同步效果“的等待异步返回能力的语法糖

这一句咋一看很别扭,好的不急,我们从例子开始看:

const demo = async ()=>{
    let result = await new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve('我延迟了一秒')
      }, 1000)
    });
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
}
// demo的返回当做Promise
demo().then(result=>{
  console.log('输出',result);
})

await顾名思义就是等待一会,只要await声明的函数还没有返回,那么下面的程序是不会去执行的!!!。这就是字面意义的等待一会(等待返回再去执行)。

那么你到这测试一下,你会发现输出是这个:输出 undefined。这是为什么呢?这也是我想强调的一个地方!!!

你在demo函数里面都没声明返回,哪来的then?所以正确写法是这样:

const demo = async ()=>{
    let result = await new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve('我延迟了一秒')
      }, 1000)
    });
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
    return result;
}
// demo的返回当做Promise
demo().then(result=>{
  console.log('输出',result); // 输出 我延迟了一秒
})

我推荐的写法是带上then,规范一点,当然你没有返回也是没问题的,demo会照常执行。下面这种写法是不带返回值的写法:

const demo = async ()=>{
    let result = await new Promise((resolve, reject) => {
      setTimeout(()=>{
        resolve('我延迟了一秒')
      }, 1000)
    });
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
}
demo();

所以可以发现,只要你用await声明的异步返回,是必须“等待”到有返回值的时候,代码才继续执行下去。

那事实是这样吗?你可以跑一下这段代码:

const demo = async ()=>{
    let result = await setTimeout(()=>{
      console.log('我延迟了一秒');
    }, 1000)
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
    return result
}
demo().then(result=>{
  console.log('输出',result);
})

你会发现,输出是这样的:

我由于上面的程序还没执行完,先不执行“等待一会”
输出 1
我延迟了一秒

奇怪,并没有await啊?setTimeout是异步啊,问题在哪?问题就在于setTimeout这是个异步,但是不是Promise!起不到“等待一会”的作用。

所以更准确的说法应该是用await声明的Promise异步返回,必须“等待”到有返回值的时候,代码才继续执行下去。

请记住await是在等待一个Promise的异步返回

当然这种等待的效果只存在于“异步”的情况,await可以用于声明一般情况下的传值吗?

事实是当然可以:

const demo = async ()=>{
    let message = '我是声明值'
    let result = await message;
    console.log(result); 
    console.log('我由于上面的程序还没执行完,先不执行“等待一会”');
    return result
}
demo().then(result=>{
  console.log('输出',result);
})

输出:

我是声明值
我由于上面的程序还没执行完,先不执行“等待一会”
输出 我是声明值

这里只要注意一点:then的执行总是最后的。

3.4 async/await 优势实战

现在我们看一下实战:

const setDelay = (millisecond) => {
  return new Promise((resolve, reject)=>{
      if (typeof millisecond != 'number') reject(new Error('参数必须是number类型'));
      setTimeout(()=> {
        resolve(`我延迟了${millisecond}毫秒后输出的`)
      }, millisecond)
  })
}
const setDelaySecond = (seconds) => {
  return new Promise((resolve, reject)=>{
      if (typeof seconds != 'number' || seconds > 10) reject(new Error('参数必须是number类型,并且小于等于10'));
      setTimeout(()=> {
        resolve(`我延迟了${seconds}秒后输出的,注意单位是秒`)
      }, seconds * 1000)
  })
}

比如上面两个延时函数(写在上面),比如我想先延时1秒,在延迟2秒,再延时1秒,最后输出“完成”,这个过程,如果用then的写法,大概是这样(嵌套地狱写法出门右拐不送):

setDelay(1000)
.then(result=>{
    console.log(result);
    return setDelaySecond(2)
})
.then(result=>{
    console.log(result);
    return setDelay(1000)
})
.then(result=>{
    console.log(result);
    console.log('完成')
})
.catch(err=>{
    console.log(err);
})

咋一看是不是挺繁琐的?如果逻辑多了估计看得更累,现在我们来试一下async/await

(async ()=>{
  const result = await setDelay(1000);
  console.log(result);
  console.log(await setDelaySecond(2));
  console.log(await setDelay(1000));
  console.log('完成了');
})()

看!是不是没有冗余的长长的链式代码,语义化也非常清楚,非常舒服,那么你看到这里,一定还发现了,上面的catch我们是不是没有在async中实现?接下去我们就分析一下async/await如何处理错误?

3.5 async/await错误处理

因为async函数返回的是一个Promise,所以我们可以在外面catch住错误。

const demo = async ()=>{
  const result = await setDelay(1000);
  console.log(result);
  console.log(await setDelaySecond(2));
  console.log(await setDelay(1000));
  console.log('完成了');
}
demo().catch(err=>{
    console.log(err);
})

在async函数的catch中捕获错误,当做一个Pormise处理,同时你不想用这种方法,可以使用try...catch语句:

(async ()=>{
  try{
    const result = await setDelay(1000);
    console.log(result);
    console.log(await setDelaySecond(2));
    console.log(await setDelay(1000));
    console.log('完成了');
  } catch (e) {
    console.log(e); // 这里捕获错误
  }
})()

当然这时候你就不需要在外面catch了。

通常我们的try...catch数量不会太多,几个最多了,如果太多了,说明你的代码肯定需要重构了,一定没有写得非常好。还有一点就是try...catch通常只用在需要的时候,有时候不需要catch错误的地方就可以不写。

有人会问了,我try...catch好像只能包裹代码块,如果我需要拆分开分别处理,不想因为一个的错误就整个process都crash掉了,那么难道我要写一堆try...catch吗?我就是别扭,我就是不想写try...catch怎嘛办?下面有一种很好的解决方案,仅供参考:

我们知道await后面跟着的肯定是一个Promise那是不是可以这样写?

(async ()=>{
  const result = await setDelay(1000).catch(err=>{
      console.log(err)
  });
  console.log(result);
  const result1 = await setDelaySecond(12).catch(err=>{
      console.log(err)
  })
  console.log(result1);
  console.log(await setDelay(1000));
  console.log('完成了');
})()

这样输出:

我延迟了1000毫秒后输出的
Error: 参数必须是number类型,并且小于等于10
    at Promise (test4.html:19)
    at new Promise (<anonymous>)
    at setDelaySecond (test4.html:18)
    at test4.html:56
undefined
我延迟了1000毫秒后输出的
完成了

是不是就算有错误,也不会影响后续的操作,是不是很棒?当然不是,你说这代码也忒丑了吧,乱七八糟的,写得别扭await又跟着catch。那么我们可以改进一下,封装一下提取错误的代码函数:

// to function
function to(promise) {
   return promise.then(data => {
      return [null, data];
   })
   .catch(err => [err]); // es6的返回写法
}

返回的是一个数组,第一个是错误,第二个是异步结果,使用如下:

(async ()=>{
   // es6的写法,返回一个数组(你可以改回es5的写法觉得不习惯的话),第一个是错误信息,第二个是then的异步返回数据,这里要注意一下重复变量声明可能导致问题(这里举例是全局,如果用let,const,请换变量名)。
  [err, result] = await to(setDelay(1000)) 
   // 如果err存在就是有错,不想继续执行就抛出错误
  if (err) throw new Error('出现错误,同时我不想执行了');
  console.log(result);
  [err, result1] = await to(setDelaySecond(12))
   // 还想执行就不要抛出错误
  if (err) console.log('出现错误,同时我想继续执行', err);
  console.log(result1);
  console.log(await setDelay(1000));
  console.log('完成了');
})()

3.6 async/await的中断(终止程序)

首先我们要明确的是,Promise本身是无法中止的Promise本身只是一个状态机,存储三个状态(pending,resolved,rejected),一旦发出请求了,必须闭环,无法取消,之前处于pending状态只是一个挂起请求的状态,并不是取消,一般不会让这种情况发生,只是用来临时中止链式的进行。

中断(终止)的本质在链式中只是挂起,并不是本质的取消Promise请求,那样是做不到的,Promise也没有cancel的状态。

不同于Promise的链式写法,写在async/await中想要中断程序就很简单了,因为语义化非常明显,其实就和一般的function写法一样,想要中断的时候,直接return一个值就行,null,空,false都是可以的。看例子:

let count = 6;
const demo = async ()=>{
  const result = await setDelay(1000);
  console.log(result);
  const result1 = await setDelaySecond(count);
  console.log(result1);
  if (count > 5) {
      return '我退出了,下面的不进行了';
    // return; 
    // return false; // 这些写法都可以
    // return null;
  }
  console.log(await setDelay(1000));
  console.log('完成了');
};
demo().then(result=>{
  console.log(result);
})
.catch(err=>{
  console.log(err);
})

实质就是直接return返回了一个Promise,相当于return Promise.resolve('我退出了下面不进行了'),当然你也可以返回一个“拒绝”:return Promise.reject(new Error('拒绝'))那么就会进到错误信息里去。

async函数实质就是返回一个Promise!

四、实战中异步需要注意的地方

我们经常会使用上述两种写法,也可能混用,有时候会遇到一些情况,这边举例子说明:

4.1 Promise获取数据(串行)之then写法注意

并行的不用多说,很简单,直接循环发出请求就可以或者用Promise.all。如果我们需要串行循环一个请求,那么应该怎么做呢?

我们需要实现一个依次分别延迟1秒输出值,一共5秒的程序,首先是Promise的循环,这个循环就相对来说比较麻烦:

我们经常会犯的错误!就是不重视函数名与函数执行对程序的影响

先不说循环,我们先举一个错误的例子,现在有一个延迟函数

const setDelay = (millisecond) => {
  return new Promise((resolve, reject)=>{
      if (typeof millisecond != 'number') reject(new Error('参数必须是number类型'));
      setTimeout(()=> {
        resolve(`我延迟了${millisecond}毫秒后输出的`)
      }, millisecond)
  })
}

我们想做到:“循环串行执行延迟一秒的Promise函数”,期望的结果应该是:隔一秒输出我延迟了1000毫秒后输出的,一共经过循环3次。我们想当然地写出下列的链式写法:

arr = [setDelay(1000), setDelay(1000), setDelay(1000)]
arr[0]
.then(result=>{
  console.log(result)
  return arr[1]
})
.then(result=>{
  console.log(result)
  return arr[2]
})
.then(result=>{
  console.log(result)
})

但是很不幸,你发现输出是并行的!!!也就是说一秒钟一次性输出了3个值!。那么这是什么情况呢?其实很简单。。。就是你把setDelay(1000)这个直接添加到数组的时候,其实就已经执行了,注意你的执行语句(1000)

这其实是基础,是语言的特性,很多粗心的人(或者是没有好好学习JS的人)会以为这样就把函数添加到数组里面了,殊不知函数已经执行过一次了。

那么这样导致的后果是什么呢?也就是说数组里面保存的每个Promise状态都是resolve完成的状态了,那么你后面链式调用直接return arr[1]其实没有去请求,只是立即返回了一个resolve的状态。所以你会发现程序是相当于并行的,没有依次顺序调用。

那么解决方案是什么呢?直接函数名存储函数的方式(不执行Promise)来达到目的

我们这样改一下程序:

arr = [setDelay, setDelay, setDelay]
arr[0](1000)
.then(result=>{
  console.log(result)
  return arr[1](1000)
})
.then(result=>{
  console.log(result)
  return arr[2](1000)
})
.then(result=>{
  console.log(result)
})

上述相当于把Promise预先存储在一个数组中,在你需要调用的时候,再去执行。当然你也可以用闭包的方式存储起来,需要调用的时候再执行。

4.2 Promise循环获取数据(串行)之for循环

上述写法是不优雅的,次数一多就GG了,为什么要提一下上面的then,其实就是为了后面的for循环做铺垫。

上面的程序根据规律改写一下:

arr = [setDelay, setDelay, setDelay]
var temp
temp = arr[0](1000)
for (let i = 1; i <= arr.length; i++) {
    if (i == arr.length) {
      temp.then(result=>{
        console.log('完成了');
      })
      break;
    }
    temp = temp.then((result)=>{
        console.log(result);
        return arr[i-1](1000)
    });
}

错误处理可以在for循环中套入try...catch,或者在你每个循环点进行.then().catch()、都是可行的。如果你想提取成公共方法,可以再改写一下,利用递归的方式:

首先你需要闭包你的Promise程序

function timeout(millisecond) {
  return ()=> {
    return setDelay(millisecond);
  }
}

如果不闭包会导致什么后果呢?不闭包的话,你传入的参数值后,你的Promise会马上执行,导致状态改变,如果用闭包实现的话,你的Promise会一直保存着,等到你需要调用的时候再使用。而且最大的优点是可以预先传入你需要的参数

改写数组:

arr = [timeout(2000), timeout(1000), timeout(1000)]

提取方法,Promise数组作为参数传入:

const syncPromise = function (arr) {
  const _syncLoop = function (count) {
    if (count === arr.length - 1) { // 是最后一个就直接return
      return arr[count]()
    }
    return arr[count]().then((result)=>{
      console.log(result);
      return _syncLoop(count+1) // 递归调用数组下标
    });
  }
  return _syncLoop(0);
}

使用:

syncPromise(arr).then(result=>{
  console.log(result);
  console.log('完成了');
})
// 或者 添加到Promise类中方法
Promise.syncAll = function syncAll(){
  return syncPromise
}// 以后可以直接使用
Promise.syncAll(arr).then(result=>{
  console.log(result);
  console.log('完成了');
})

还有大神总结了一个reduce的写法,其实就是一个迭代数组的过程:

const p = arr.reduce((total, current)=>{
    return total.then((result)=>{
        console.log(result);
        return current()
    })
}, Promise.resolve('程序开始'))
p.then((result)=>{
    console.log('结束了', result);
})

都是可行的,在Promise的循环领域。

4.3 async/await循环获取数据(串行)之for循环

现在就来介绍一下牛逼的async/await实战,上述的代码你是不是要看吐了,的确,我也觉得好麻烦啊,那么如果用async/await能有什么改进吗?这就是它出现的意义:

模拟上述代码的循环:

(async ()=>{
    arr = [timeout(2000), timeout(1000), timeout(1000)]
    for (var i=0; i < arr.length; i++) {
        result = await arr[i]();
        console.log(result);
    }
})()

。。。这就完了?是的。。。就完了,是不是特别方便!!!!语义化也非常明显!!这里为了保持与上面风格一致,没有加入错误处理,所以实战的时候记得加入你的try...catch语句来捕获错误。

四、后记

一直想总结一下Promiseasync/await,很多地方可能总结得不够,已经尽力扩大篇幅了,后续有新的知识点和总结点可能会更新(未完待续),但是入门这个基本够用了。

我们常说什么async/await的出现淘汰了Promise,可以说是大错特错,恰恰相反,正因为有了Promise,才有了改良版的async/await,从上面分析就可以看出,两者是相辅相成的,缺一不可。

想学好async/await必须先精通Promise,两者密不可分,有不同意见和改进的欢迎指导!

前端小白,大家互相交流,peace!

查看原文

Ari1c 收藏了文章 · 5月11日

JavaScript:利用递归实现对象深拷贝

先来普及一下深拷贝和浅拷贝的区别
浅拷贝:就是简单的复制,用等号即可完成

let a = {a: 1}
let b = a

这就完成了一个浅拷贝
但是当修改对象b的时候,我们发现对象a的值也被改变了

b.a = 10
console.log(a.a) => 10

这是因为浅拷贝只复制了指向对象的指针,新旧对象共用同一块内存,修改某一个对象的同时也会把另一个都一并修改了

深拷贝:跟浅拷贝最简单明了的区别就是修改拷贝的对象,不会改变源对象
利用Object.assign可以对只有一层的对象实现深拷贝,如下:

let a = {a: 1,b: 2,c: 3}
let b = Object.assign({}, a)
b.b = 100
console.log(a.b) => 2

可以看出来这样是完全可以做到对只有一层的对象实现深拷贝的
但是如果对象里面的元素还是对象的话就没作用了

let a = {a: 1,b: 2,c: 3, d: {a: 1}}
let b = Object.assign({}, a)
b.d.a = 100
console.log(a.d.a) => 100

对于这种比较复杂的对象,我们就可以利用递归的方式实现真正的对象深拷贝了

function deepClone (sourceObj, targetObj) {
    let cloneObj = targetObj || {}
    if(!sourceObj || typeof sourceObj !== "object" || sourceObj.length === undefined){
        return sourceObj
    }
    if(sourceObj instanceof Array){
        cloneObj = sourceObj.concat()
    } else {
        for(let i in sourceObj){
            if (typeof sourceObj[i] === 'object') {
                cloneObj[i] = deepClone(sourceObj[i], {})
            } else {
                cloneObj[i] = sourceObj[i]
            }
        }
    }
    return cloneObj
}

简单的几行代码就可以轻松实现对象的深拷贝

简单的测试代码,如下:
let sourceObj = {
  a: 1,
  b: {
    a: 1
  },
  c: {
    a: 1,
    b: {
      a: 1
    }
  },
  d: function() {
    console.log('hello world')
  },
  e: [1, 2, 3]
}
let targetObj = deepClone(sourceObj, {})
targetObj.c.b.a = 9
console.log(sourceObj) => { a: 1,  b: { a: 1 },  c: { a: 1, b: { a: 1 } },  d: [Function: d],  e: [ 1, 2, 3 ] }
console.log(targetObj) => { a: 1,  b: { a: 1 },  c: { a: 1, b: { a: 9 } },  d: [Function: d],  e: [ 1, 2, 3 ] }

另外介绍两个用来做深拷贝的库

**jquery**
使用方法:
let targetObj = $.extent(true,{},sourceObj)
**lodash函数库**
使用方法:
npm install lodash
**es5写法**
let lodash = require('lodash')
**es6写法**
import lodash from 'lodash'

let targetOj = lodash.cloneDeep(sourceObj)

各位看官觉得有什么地方不对的请多多指教。

查看原文

Ari1c 赞了文章 · 2019-10-10

【JS 口袋书】第 3 章:JavaScript 函数

阿里云最近在做活动,低至2折,有兴趣可以看看:
https://promotion.aliyun.com/...


函数是什么

函数是完成某个特定功能的一组语句。如没有函数,完成任务可能需要五行、十行、甚至更多的代码。这时我们就可以把完成特定功能的代码块放到一个函数里,直接调用这个函数,就省重复输入大量代码的麻烦。

函数可以概括为:一次封装,四处使用。

函数的定义

函数的定义方式通常有三种:函数声明方式、函数表达式、 使用Function构造函数 。

函数声明方式

语法:

function 函数名(参数1,参数2,...){  
    //要执行的语句  
}  

例:

// 声明
function sum(num1, num2) {
  return num1 + num2;
}

// 调用
sum(1, 2)  // 3

函数表达式

语法:

var fn = function(参数1,参数2,...){  
    //要执行的语句  
};

例:

// 声明
var sum = function(num1,num2){  
  return num1+num2;  
};
// 调用
sum(1, 2) // 3

使用Function构造函数

Function构造函数可以接收任意数量的参数,最后一个参数为函数体,其他的参数则枚举出新函数的参数。其语法为:

new Function("参数1","参数2",...,"参数n","函数体"); 

例:

// 声明
var sum = new Function("num1","num2","return num1+num2");  
// 调用
sum(1, 2) // 3

三种定义方式的区别

三种方式的区别,可以从作用域、效率以及加载顺序来区分。

从作用域上来说,函数声明式和函数表达式使用的是局部变量,而 Function()构造函数却是全局变量,如下所示:

var name = '我是全局变量 name';

// 声明式
function a () {
  var name = '我是函数a中的name';
  return name;
}
console.log(a()); // 打印: "我是函数a中的name"

// 表达式
var b = function() {
  var name = '我是函数b中的name';
  return name; // 打印: "我是函数b中的name"
}
console.log(b())

// Function构造函数
function c() {
  var name = '我是函数c中的name';
  return new Function('return name')
}
console.log(c()()) // 打印:"我是全局变量 name",因为Function()返回的是全局变量 name,而不是函数体内的局部变量。

从执行效率上来说Function()构造函数的效率要低于其它两种方式,尤其是在循环体中,因为构造函数每执行一次都要重新编译,并且生成新的函数对象。

来个例子:

var start = new Date().getTime()

for(var i = 0; i < 10000000; i++) {
  var fn = new Function('a', 'b', 'return a + b')
  fn(i, i+1)
}
var end = new Date().getTime();
console.log(`使用Function构造函数方式所需要的时间为:${(end - start)/1000}s`)
// 使用Function构造函数方式所需要的时间为:8.646s

start = new Date().getTime();
var fn = function(a, b) {
  return a + b;
}
for(var i = 0; i < 10000000; i++) {
  fn(i, i+1)
}
end = new Date().getTime();
console.log(`使用表达式的时间为:${(end - start)/1000}s`)
// 使用表达式的时间为:0.012s

由此可见,在循环体中,使用表达式的执行效率比使用 Function()构造函数快了很多很多。所以在 Web 开发中,为了加快网页加载速度,提高用户体验,我们不建议选择 Function ()构造函数方式来定义函数。

最后是加载顺序,function 方式(即函数声明式)是在 JavaScript 编译的时候就加载到作用域中,而其他两种方式则是在代码执行的时候加载,如果在定义之前调用它,则会返回 undefined

console.log(typeof f) // function
console.log(typeof c) // undefined
console.log(typeof d) // undefined

function f () {
  return 'JS 深入浅出'
}
var c = function () {
  return 'JS 深入浅出'
}
console.log(typeof c) // function
var d = new Function('return "JS 深入浅出"')
console.log(typeof d) // function

函数的参数和返回值

函数的参数-arguments

JavaScript 中的函数定义并未指定函数形参的类型,函数调用也未对传入的实参值做任何类型检查。实际上,JavaScript 函数调用甚至不检查传入形参的个数。

function sum(a) {
  return a + 1;
}
console.log(sum(1)); // 2
console.log(sum('1')); // 11
console.log(add()); // NaN
console.log(add(1, 2)); // 2

当实参比形参个数要多时,剩下的实参没有办法直接获得,需要使用即将提到的arguments对象。

JavaScript中的参数在内部用一个数组表示。函数接收到的始终都是这个数组,而不关心数组中包含哪些参数。在函数体内可以通过arguments对象来访问这个参数数组,从而获取传递给函数的每一个参数。arguments对象并不是Array的实例,它是一个类数组对象,可以使用方括号语法访问它的每一个元素。

function sum (x) {
  console.log(arguments[0], arguments[1], arguments[2]); // 1 2 3
}

sum(1, 2, 3)

arguments对象的length属性显示实参的个数,函数的length属性显示形参的个数。

function sum(x, y) {
  console.log(arguments.length); // 3
  return x + 1;
}
sum(1, 2, 3)
console.log(sum.length) // 2

函数的参数-arguments

JavaScript 中的函数定义并未指定函数形参的类型,函数调用也未对传入的实参值做任何类型检查。实际上,JavaScript 函数调用甚至不检查传入形参的个数。

function sum(a) {
  return a + 1;
}
console.log(sum(1)); // 2
console.log(sum('1')); // 11
console.log(add()); // NaN
console.log(add(1, 2)); // 2

函数的参数-同名参数

在非严格模式下,函数中可以出现同名形参,且只能访问最后出现的该名称的形参。

function sum(x, x, x) {
  return x;
}
console.log(sum(1, 2, 3)) // 3

而在严格模式下,出现同名形参会抛出语法错误。

function sum(x, x, x) {
  'use strict';
  return x;
}
console.log(sum(1, 2, 3)) // SyntaxError: Duplicate parameter name not allowed in this context

函数的参数-参数个数

当实参比函数声明指定的形参个数要少,剩下的形参都将设置为undefined值。

function sum(x, y) {
  console.log(x, y);
}
sum(1); // 1 undefined

函数的返回值

所有函数都有返回值,没有return语句时,默认返回内容为undefined

function sum1 (x, y) {
  var total = x + y
} 
console.log(sum1()) // undefined

function sum2 (x, y) {
  return x + y
}
console.log(sum2(1, 2)) // 3

如果函数调用时在前面加上了new前缀,且返回值不是一个对象,则返回this(该新对象)。

function Book () {
  this.bookName = 'JS 深入浅出'
}

var book = new Book();
console.log(book); // Book { bookName: 'JS 深入浅出' }
console.log(book.constructor); // [Function: Book]

如果返回值是一个对象,则返回该对象。

function Book () {
  return {bookName: JS 深入浅出}
}

var book = new Book();
console.log(book); // { bookName: 'JS 深入浅出' }
console.log(book.constructor); // [Function: Book]

函数的调用方式

JS 一共有4种调用模式:函数调用、方法调用、构造器调用和间接调用。

函数调用

当一个函数并非一个对象的属性时,那么它就是被当做一个函数来调用的。对于普通的函数调用来说,函数的返回值就是调用表达式的值

function sum (x, y) {
  return x + y;
}
var total = sum(1, 2);
console.log(total); // 3

使用函数调用模式调用函数时,非严格模式下,this被绑定到全局对象;在严格模式下,thisundefined

// 非严格模式
function whatIsThis1() {
  console.log(this);
}
whatIsThis1(); // window

// 严格模式
function whatIsThis2() {
  'use strict';
  console.log(this);
}
whatIsThis2(); // undefined

方法调用

当一个函数被保存为对象的一个属性时,称为方法,当一个方法被调用时,this被绑定到该对象。

function printValue(){
  console.log(this.value);  
}
var value=1;
var myObject = {value:2};
myObject.m = printValue;
//作为函数调用
printValue();
//作为方法调用
myObject.m();

咱们注意到,当调用printValue时,this绑定的是全局对象(window),打印全局变量value1。但是当调用myObject.m()时,this绑定的是方法m所属的对象Object,所以打印的值为Object.value,即2

构造函数调用

如果函数或者方法调用之前带有关键字new,它就构成构造函数调用。

function fn(){
    this.a = 1;
};
var obj = new fn();
console.log(obj.a);//1

参数处理:一般情况构造器参数处理和函数调用模式一致。但如果构造函数没用形参,JavaScript构造函数调用语法是允许省略实参列表和圆括号的。

如:下面两行代码是等价的。

var o = new Object();
var o = new Object;

函数的调用上下文为新创建的对象。

function Book(bookName){
  this.bookName = bookName;
}
var bookName = 'JS 深入浅出';
var book = new Book('ES6 深入浅出');
console.log(bookName);// JS 深入浅出
console.log(book.bookName);// ES6 深入浅出
Book('新版JS 深入浅出');
console.log(bookName); // 新版JS 深入浅出
console.log(book.bookName);// ES6 深入浅出

1.第一次调用Book()函数是作为构造函数调用的,此时调用上下文this被绑定到新创建的对象,即 book。所以全局变量bookName值不变,而book新增一个属性bookName,值为'ES6 深入浅出'

2.第二次调用Book()函数是作为普通函数调用的,此时调用上下为this被绑定到全局对象,在浏览器中为window。所以全局对象的bookNam值改变为' 新版JS 深入浅出',而book的属性值不变。

间接调用

JS 中函数也是对象,函数对象也可以包含方法,call()apply()方法可以用来间接地调用函数。

这两个方法都允许显式指定调用所需的this值,也就是说,任何函数可以作为任何对象的方法来调用,哪怕这个函数不是那个对象的方法。两个方法都可以指定调用的实参。call()方法使用它自有的实参列表作为函数的实参,apply()方法则要求以数组的形式传入参数。

var obj = {};
function sum(x,y){
    return x+y;
}
console.log(sum.call(obj,1,2));//3
console.log(sum.apply(obj,[1,2]));//3

词法(静态)作用域与动态作用域

作用域

通常来说,一段程序代码中所用到的名字并不总是有效/可用的,而限定这个名字的可用性的代码范围就是这个名字的作用域。

词法作用域

词法作用域,也叫静态作用域,它的作用域是指在词法分析阶段就确定了,不会改变。而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

来个例子,如下代码所示:

var blobal1 = 1;
function fn1 (param1) {
  var local1 = 'local1';
  var local2 = 'local2';
  
  function fn2(param2) {
    var local2 = 'inner local2';
    console.log(local1)
    console.log(local2)
  }

  function fn3() {
    var local2 = 'fn3 local2';
    fn2(local2)
  }
  fn3()
}

fn1()

当浏览器看到这样的代码,不会马上去执行,它会先生成一个抽象语法树。上述代码生成的抽象语法树大概是这样的:

clipboard.png

执行fn1函数,fn1中调用 fn3(),从fn3函数内部查找是否有局部变量 local1,如果没有,就根据抽象树,查找上面一层的代码,也就是 local1 等于 'local1' ,所以结果会打印 'local1'

同样的方法查找是否有局部变量 local2,发现当前作用域内有local2变量,所以结果会打印 'inner local2

思考

有如下的代码:

var a = 1;
function fn() {
  console.log(a)
}

两个问题:

  1. 函数 fn 里面的变量 a, 是不是外面的变量 a
  2. 函数 fn 里面的变量 a的值, 是不是外面的变量 a的值。

对于第一个问题:

分析一个语法,就能确定函数 fn里面的 a 就是外面的 a

对于第二个问题:

函数 fn 里面的变量 a的值, 不一定是外面的变量 a的值,假设咱们这样做:

var a = 1;
function fn() {
  console.log(a)
}
a = 2
fn()

这时候当咱们执行 fn() 的时候,打印 a 的值为 2。所以如果没有看到最后,一开始咱们是不知道打印的 a 值到底是什么。

所以词法作用域只能确定变量所在位置,并不能确定变量的值。

调用栈(Call Stack)

什么是执行上下文

执行上下文就是当前JavaScript代码被解析和执行是所在环境的抽象概念,JavaScript中运行任何的代码都是在执行上下文中运行。

执行上下文的类型,主要有两类:

  • 全局执行上下文:这是默认的,最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。共有两个过程:1.创建有全局对象,在浏览器中这个全局对象就是window对象。2.将this指针指向这个全局对象。一个程序中只能存在一个执行上下文。
  • 函数执行上下文:每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。一个程序中可以存在多个函数执行上下文,这些函数执行上下文按照特定的顺序执行一系列步骤,后文具体讨论。

调用栈

调用栈,具有LIFO(Last in, First out 后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。

当JavaScript引擎首次读取脚本时,会创建一个全局执行上下文并将其push到当前执行栈中。每当发生函数调用时,引擎都会为该函数创建一个新的执行上下文并push到当前执行栈的栈顶。

引擎会运行执行上下文在执行栈栈顶的函数,根据LIFO规则,当此函数运行完成后,其对应的执行上下文将会从执行栈中pop出,上下文控制权将转到当前执行栈的下一个执行上下文。

看看下面的代码:

var myOtherVar = 10;

function a() {
  console.log('myVar', myVar);
  b();
}

function b() {
  console.log('myOtherVar', myOtherVar);
  c();
}

function c() {
  console.log('Hello world!');
}

a();

var myVar = 5;

有几个点需要注意:

  • 变量声明的位置(一个在上,一个在下)
  • 函数a调用下面定义的函数b, 函数b调用函数c

当它被执行时你期望发生什么? 是否发生错误,因为ba之后声明或者一切正常? console.log 打印的变量又是怎么样?

以下是打印结果:

"myVar" undefined
"myOtherVar" 10
"Hello world!"

1. 变量和函数声明(创建阶段)

第一步是在内存中为所有变量和函数分配空间。 但请注意,除了undefined之外,尚未为变量分配值。 因此,myVar在被打印时的值是undefined,因为JS引擎从顶部开始逐行执行代码。

函数与变量不一样,函数可以一次声明和初始化,这意味着它们可以在任何地方被调用。

所以以上代码在创建阶段时,看起来像这样子:

var myOtherVar = undefined
var myVar = undefined

function a() {...}
function b() {...}
function c() {...}

这些都存在于JS创建的全局上下文中,因为它位于全局作用域中。

在全局上下文中,JS还添加了:

  • 全局对象(浏览器中是 window 对象,NodeJs 中是 global 对象)
  • this 指向全局对象

2. 执行

接下来,JS 引擎会逐行执行代码。

myOtherVar = 10在全局上下文中,myOtherVar被赋值为10

已经创建了所有函数,下一步是执行函数 a()

每次调用函数时,都会为该函数创建一个新的上下文(重复步骤1),并将其放入调用堆栈。

function a() {
  console.log('myVar', myVar)
  b()
}

如下步骤:

  • 创建新的函数上下文
  • a 函数里面没有声明变量和函数
  • 函数内部创建了 this 并指向全局对象(window)
  • 接着引用了外部变量 myVarmyVar 属于全局作用域的。
  • 接着调用函数 b 函数b的过程跟a一样,这里不做分析。

下面调用堆栈的执行示意图:

clipboard.png

  • 创建全局上下文,全局变量和函数。
  • 每个函数的调用,会创建一个上下文,外部环境的引用及 this。
  • 函数执行结束后会从堆栈中弹出,并且它的执行上下文被垃圾收集回收(闭包除外)。
  • 当调用堆栈为空时,它将从事件队列中获取事件。

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具Fundebug

交流

阿里云最近在做活动,低至2折,有兴趣可以看看:https://promotion.aliyun.com/...

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。

clipboard.png

每次整理文章,一般都到2点才睡觉,一周4次左右,挺苦的,还望支持,给点鼓励

查看原文

赞 14 收藏 12 评论 0

Ari1c 关注了专栏 · 2019-06-29

铁皮饭盒的前端小课堂

关于前端的一切的文章

关注 853

Ari1c 关注了问题 · 2019-05-27

如何从零开始开发一套JS SDK?

今天收到公司的一个任务,要开发一套多平台使用的前端JS SDK,最起码支持浏览器和手机的webview。

我本人也算是个全栈JS,写过Node,也写过前端。jQuery和React也玩的很溜。但是突然接到写SDK的工作,还是有点蒙圈。需求我也明白,说白了就是获取数据,处理一下,放出接口,让其他开发者使用。但是真开始写起来,还是感觉无从下手。

请问,该如何去写一套JS的SDK啊?如何组织代码?用什么样的工作流程?怎么测试?怎么打包发布(我们需要压缩代码,做一些混淆)?

求大神帮助解答,分享些经验或学习资料,不胜感激啊。

关注 6 回答 2

Ari1c 关注了专栏 · 2019-01-17

于梦中的前端成长日记

于梦中的前端成长日记——记录点滴

关注 119

Ari1c 赞了文章 · 2019-01-08

「前端面试题系列3」伪类与伪元素的区别及实战

图片描述

前言

这是前端面试系列的第3篇,前面的章节,你可以在这里找到:

面试前端候选人的时候,我经常会问这样一个有关CSS的问题:

你知道伪类与伪元素么,它们的分别是什么?
这时,能回答上来的很少。

换一种问法,你知道 :hover, :active, :focus, :visited么?
这时,基本都能回答上来,这不就是a标签的四种状态么。

嗯,ok。然后继续问,那么 ::before 和 ::after,听说过么?
这时,能听到的回答是,嗯,我看到过,偶尔会用。

伪类与伪元素,都有一个“伪”字,那它们有什么区别么?
这时,回应我的,是一片沉默。。。

从回答上来分析,虽然伪类和伪元素平时都有接触,但在概念上,都比较模糊。今天,我们就来说说伪类与伪元素的区别,以及使用场景。伪类,不是只有a标签的四种状态。伪元素,也不是只有 ::before 与 ::after。更多的伪类与伪元素,详见文末附录。

概念上的区别

从概念上来区分,大致有以下几点:

伪类,更多的定义的是状态。常见的伪类有 :hover,:active,:focus,:visited,:link,:not,:first-child,:last-child等等。

伪元素,不存在于DOM树中的虚拟元素,它们可以像正常的html元素一样定义css,但无法使用JavaScript获取。常见伪元素有 ::before,::after,::first-letter,::first-line等等。

CSS3明确规定了,伪类用一个冒号(:)来表示,而伪元素则用两个冒号(::)来表示。但目前因为兼容性的问题,它们的写法可以是一致的,都用一个冒号(:)就可以了,所以非常容易混淆。

实战场景——伪类

表单校验

表单的校验中,常会用到 :required:valid:invalid 这三个伪类。先来看看它们所代表的含义。

  • :required,指定具有 required属性 的表单元素
  • :valid,指定一个 匹配指定要求 的表单元素
  • :invalid,指定一个 不匹配指定要求 的表单元素

看下面这个例子:

<p>input中类型为email的校验</p>

<p>符合email校验规则</p>
<input type="email" required placeholder="请输入" value="24238477@qq.com" />
<br><br>

<p>不符合email校验规则</p>
<input type="email" required placeholder="请输入" value="lalala" />
<br><br>

<p>有required标识,但未填写</p>
<input type="email" required placeholder="请输入" value="" />
input {
    &:valid {
        border-color: green;
        box-shadow: inset 5px 0 0 green;
    }
    &:invalid {
        border-color: red;
        box-shadow: inset 5px 0 0 red;
    }
    &:required {
        border-color: red;
        box-shadow: inset 5px 0 0 red;
    }
}

效果如下:
图片描述

折叠面板

过去,要实现折叠面板的显示或隐藏,只能用JavaScript来搞定。但是现在,可以用伪类 :target 来实现。 :target 是文档的内部链接,即 URL 后面跟有锚名称 #,指向文档内某个具体的元素。

看下面这个例子:

<div class="t-collapse">
    <!-- 在url最后添加 #modal1,使得target生效 —>
    <a class="collapse-target" href="#modal1">target 1</a>
    <div class="collapse-body" id="modal1">
        <!-- 将url的#modal1 变为 #,使得target失效 —>
        <a class="collapse-close" href="#">target 1</a>
        <p>...</p>
    </div>
</div>
.t-collapse {
    >.collapse-body {
        display: none;
        &:target {
            display: block;
        }
    }
}

元素的index

当我们要指定一系列标签中的某个元素时,并不需要用JavaScript获取。可以用 :nth-child(n):nth-of-type(n) 来找到,并指定样式。但它们有一些小区别,需要注意。

首先,它们的n可以是大于零的数字,或者类似2n+1的表达式,再或者是 even / odd。

另外,还有2个区别:

  • :nth-of-type(n) 除了关注n之外,还需要关注最前面的类型,也就是标签。
  • :nth-child(n) 它关注的是:其父元素下的第n个孩子,与类型无关。

看下面这个例子,注意两者的差异:

<h1>这是标题</h1>
<p>第一个段落。</p>
<p>第二个段落。</p>
<p>第三个段落。</p>
<p>第四个段落。</p>
<p>第五个段落。</p>

图片描述

实战场景——伪元素

antd的彩蛋事件

还记得2018年圣诞节的“彩蛋事件”,在整个前端圈,轰动一时。因为按钮上的一朵云,导致不少前端er提前回家过年了。当时,彩蛋事件出现的第一时间,就吓得我赶快打开工程看了一眼,果然也中招了。为了保住饭碗,得赶紧把云朵去掉。

查看了生成的html,发现原来是 button 下藏了一个 ::before。所以,赶紧把样式覆盖掉,兼容代码如下:

.ant-btn {
    &::before {
        display: none !important;
    }
}

美化选中的文本

在网页中,默认的划词效果是,原字色保持不变,划过时的背景变为蓝底色。其实,这是可以用 ::selection 来进行美化的。看下面这个例子:

<p>Custom text selection color</p>
::selection {
    color: red;
    background-color: yellow;
}

效果如下:
图片描述

划过的部分美化为:红色的字体,并且底色变为了黄色。

总结

CSS也可以实现动态的交互,并非只有JavaScript才能实现。

书写的时候,要尊重规范。写伪类的时候用 :,而写伪元素的时候用 ::

兼容性的问题,交给postcss去做。本文并未涉及兼容性的写法,包括前缀问题,可以交给autoprefixer去做。

附录

CSS3中的伪类

:root 选择文档的根元素,等同于 html 元素
:empty 选择没有子元素的元素
:target 选取当前活动的目标元素
:not(selector) 选择除 selector 元素意外的元素
:enabled 选择可用的表单元素
:disabled 选择禁用的表单元素
:checked 选择被选中的表单元素
:nth-child(n) 匹配父元素下指定子元素,在所有子元素中排序第n
:nth-last-child(n) 匹配父元素下指定子元素,在所有子元素中排序第n,从后向前数
:nth-child(odd) 、 :nth-child(even) 、 :nth-child(3n+1)
:first-child 、 :last-child 、 :only-child
:nth-of-type(n) 匹配父元素下指定子元素,在同类子元素中排序第n
:nth-last-of-type(n) 匹配父元素下指定子元素,在同类子元素中排序第n,从后向前数
:nth-of-type(odd) 、 :nth-of-type(even) 、 :nth-of-type(3n+1)
:first-of-type 、 :last-of-type 、 :only-of-type

CSS3中的伪元素

::after 已选中元素的最后一个子元素
::before 已选中元素的第一个子元素
::first-letter 选中某个款级元素的第一行的第一个字母
::first-line 匹配某个块级元素的第一行
::selection 匹配用户划词时的高亮部分

PS:欢迎关注我的公众号 “超哥前端小栈”,交流更多的想法与技术。
1334261100-5c1de2b334a9e_articlex

查看原文

赞 55 收藏 42 评论 2

Ari1c 收藏了文章 · 2018-11-15

前端最实用书签(持续更新)

前言

一直混迹社区,突然发现自己收藏了不少好文但是管理起来有点混乱;
所以将前端主流技术做了一个书签整理,不求最多最全,但求最实用。

书签源码

书签导入浏览器效果截图
书签

书签源码请戳github地址
Chrome---设置---书签---导入书签就可在浏览器即可使用

1.ES6,ES7和ES8

2.vue

2.1 api

2.2 vue源码

2.3 插件

2.3 项目

2.4 vue-cli

2.5 vue高级

3.react

3.1 api

3.2 ui组件

3.3 项目

3.4 源码解析

4.微信小程序

4.1 api

4.2 插件

4.3 项目

电商项目1
小程序demo

5.Hybrid

5.1 通讯

5.2 适配和兼容

6.webpack

7.npm

npm配置

8.node.js

8.1 api

8.2 框架

8.3项目

9.高德地图

9.css

10.性能优化

11.安全

12.前端架构

13.算法

14.前端测试

15.常用工具

16.浏览器

17.大佬博客

18.面试

结语

后期会继续优化维护这些书签
为了便于知识归纳,所以将有些作者的文章标题修改了,谢谢优秀作者的文章分享!

查看原文

Ari1c 收藏了文章 · 2018-11-06

一个 PWA 的诞生

原文链接:https://ssshooter.com/2018-09...

PWA(Progressive Web Apps)虽然是网页应用,但是可以带来媲美原生的用户体验,其中包含离线可用,后台推送等功能。PWA 不是一个新名词,早在 2015 年已经提出这个思想,但是直到 iOS12 的发布,PWA 终于可以在 iOS 中添加到主屏,只有安卓和 iOS 都能使用 PWA 的基本功能,它才算是真的开始走近大众。想了解 PWA,可以看看百度的 LAVAS 官网,在国内网站中 LAVAS 官网会有比较完整的 PWA 资料。

本文可以提前让大家熟悉 PWA 搭建,文中提到的配置来自一个 Redesign 的 nipponcolors,最近才做好的,使用的各种库都是现在(2018.09.14)最新的,可用的,另外本文不讨论应用功能,单纯讲讲 PWA 的搭建。

仓库地址:https://github.com/ssshooter/...
网页地址:https://ssshooter.github.io/n...

搭建

使用 Vue-cli3

Vue-cli3 与 2 的区别挺大的,3 默认搭建工程并非像 2 一样拉取模板,而是自己选择需要的 feature 生成项目。

clipboard.png

要生成 PWA 项目请勾上 PWA Support

Vue-cli3 的一个关于 HMR 的已知问题

Issue 地址
假设大家都知道 HMR(热模块更新)是什么了,vue-cli3 的某些版本可能会有这么一个问题,HMR 无法使用,控制台就一直显示 waiting 连接,文件一改直接显示连接断开。解决方案有二:

  1. 如果你的依赖使用 cnpm 安装,尝试删掉 node_modules 后使用 npm 下载。
  2. vue.config.js 作以下配置

    chainWebpack: config => {
        config.resolve.symlinks(true)
        return config
    },

值得注意的配置

eslint 配置

在这里首先推荐一下尾逗号,加上尾逗号的好处只有一个,但真的十分重要,那就是 diff 会非常好看。然后 eslint-plugin-vue 插件是一些预设规则组合,分 base,essential,strongly-recommended,recommended 四级,请自由选择,没有最好的搭配,只要用得舒服就好,团队合作还是非常推荐定好 eslint 规则,使用尽量详细的同一套规则,在代码合并时感觉会非常爽快。

clipboard.png

设置 eslint 后建议配置开发服务器的 overlay 选项,在 eslint 报错时会覆盖在页面上,时刻提醒你写代码得有信条。

  devServer: {
    overlay: {
      warnings: true,
      errors: true,
    },
  },

资源优化

图片

vue-cli3 搭建的工程没有自带图片优化插件,所以请自行安装。有更好的图片压缩插件求推荐啦,这里使用的是 imagemin-webpack-plugin,基本配置如下:

var ImageminPlugin = require('imagemin-webpack-plugin').default
// Or if using ES2015:
// import ImageminPlugin from 'imagemin-webpack-plugin'

module.exports = {
  plugins: [
    // Make sure that the plugin is after any plugins that add images
    new ImageminPlugin({
      disable: process.env.NODE_ENV !== 'production', // Disable during development
      pngquant: {
        quality: '95-100'
      }
    })
  ]
}

字体

对于中文站点,字体问题可是个大问题,因为中文字体实在太太太太大了,随随便便一个都 10m 了,等待这东西下载真的給用户体验带来致命打击,但是!但是!有这么一个程序!

font-spider-plus

之前一直知道 font-spider,它的功能是获取使用到的字体,然后分析出使用了改字体的字符,最后把字符抽离出来。这大大减少了中文字体的体积,但是缺不能用于 js 渲染的网页,后来才找到 font-spider-plus,虽然要手动操作一下(最简单的方法:发布网页之后用 fsp 把用到的字爬出来),但是得到的优化真的很大哦!

响应式设计

PWA 给了我们一个很简单的跨平台方法,不只有移动端可以添加到首屏,PC 端也同样可以,所以响应式设计对 PWA 可以说是必须的。
PC 端的添加方法在 F12 里,至于更加方便的方法...好像是要改 flag 才能用,那么也谈不上多方便了,所以不介绍了,等到此项技术更加成熟,想必这个按钮就会出现在显眼的位置 ?

clipboard.png

PWA 专属问题

建议使用 LightHouse 给你的 PWA 评分,报告中还会带有修改建议,十分实用。

clipboard.png

manifest.json

PWA 添加至桌面的功能实现依赖于 manifest.json。这个文件结构很简单,填写对应字段即可,可在这里了解详情。

Service Worker 刷新

针对 PWA 功能来说,项目成功生成就已经配置好离线缓存功能,对 Service Worker 不熟悉的同学来说可以说很方便了。但是对于推送功能和 Service Worker 的更多细节仍然需要深入研究才能流畅使用这个新兴玩意。
(以下称 Service Worker 为 SW)
有一个问题特别值得注意,那就是 SW 的更新问题。SW 控制项目环境的缓存,但是 SW 更新后怎么刷新缓存就不那么容易理解了。SW 更新后,会进入 waiting 状态,旧的 SW 依旧正常运行,所以新的 SW 无法激活,你需要关闭整个浏览器才能把旧的 SW 关闭,再次打开网页就能看到新的 SW 运作了。
有一个方便一点的方法,配置

    workboxOptions: {
      skipWaiting: true,
      clientsClaim: true,
    },

之后,第一个选项如其字面意思,会跳过 Waiting 状态,而 clientsClaim 可以让 SW 进入 activated 状态后立即控制页面(但是实际上,SW 即使立刻控制页面并输出新数据,SPA 也要刷新才能渲染为最新版本,但这也总比关闭浏览器好多了)。
详细原理请查阅谷歌官方资料(需要梯子)。

最后,给你一个佛系刷新方法

Service Worker 的特殊之处除了由浏览器触发更新之外,还应用了特殊的缓存策略: 如果该文件已 24 小时没有更新,当 Update 触发时会强制更新。这意味着最坏情况下 Service Worker 会每天更新一次。

Preload

clipboard.png

这个是 Lighthouse 提醒我的。对于一些必要的资源,可以使用 Preload 预先下载(特别是字体或图片等资源),不必等使用时再下载,这又将会是几百毫秒的等待。更多信息可以参考 vue-cli 官网相关页面:https://cli.vuejs.org/zh/guid...

最后

完整配置地址
https://github.com/ssshooter/...
https://github.com/ssshooter/...
这是一个最最简单的 PWA,没有对 SW 进行深度配置,很多功能没有用上。Vue-cli3 搭建的项目使用 workbox-webpack-plugin,SW 默认是自己生成的,需要自己附加 SW 功能可以使用 InjectManifest 插件。最后提一下 Lavas App,可以把你的 PWA 包装成一个 apk,本质是快捷方式,方便了还没支持 PWA 的安卓系统。

如果有其他问题,可以在评论区讨论~

查看原文

Ari1c 收藏了文章 · 2018-11-06

一个 PWA 的诞生

原文链接:https://ssshooter.com/2018-09...

PWA(Progressive Web Apps)虽然是网页应用,但是可以带来媲美原生的用户体验,其中包含离线可用,后台推送等功能。PWA 不是一个新名词,早在 2015 年已经提出这个思想,但是直到 iOS12 的发布,PWA 终于可以在 iOS 中添加到主屏,只有安卓和 iOS 都能使用 PWA 的基本功能,它才算是真的开始走近大众。想了解 PWA,可以看看百度的 LAVAS 官网,在国内网站中 LAVAS 官网会有比较完整的 PWA 资料。

本文可以提前让大家熟悉 PWA 搭建,文中提到的配置来自一个 Redesign 的 nipponcolors,最近才做好的,使用的各种库都是现在(2018.09.14)最新的,可用的,另外本文不讨论应用功能,单纯讲讲 PWA 的搭建。

仓库地址:https://github.com/ssshooter/...
网页地址:https://ssshooter.github.io/n...

搭建

使用 Vue-cli3

Vue-cli3 与 2 的区别挺大的,3 默认搭建工程并非像 2 一样拉取模板,而是自己选择需要的 feature 生成项目。

clipboard.png

要生成 PWA 项目请勾上 PWA Support

Vue-cli3 的一个关于 HMR 的已知问题

Issue 地址
假设大家都知道 HMR(热模块更新)是什么了,vue-cli3 的某些版本可能会有这么一个问题,HMR 无法使用,控制台就一直显示 waiting 连接,文件一改直接显示连接断开。解决方案有二:

  1. 如果你的依赖使用 cnpm 安装,尝试删掉 node_modules 后使用 npm 下载。
  2. vue.config.js 作以下配置

    chainWebpack: config => {
        config.resolve.symlinks(true)
        return config
    },

值得注意的配置

eslint 配置

在这里首先推荐一下尾逗号,加上尾逗号的好处只有一个,但真的十分重要,那就是 diff 会非常好看。然后 eslint-plugin-vue 插件是一些预设规则组合,分 base,essential,strongly-recommended,recommended 四级,请自由选择,没有最好的搭配,只要用得舒服就好,团队合作还是非常推荐定好 eslint 规则,使用尽量详细的同一套规则,在代码合并时感觉会非常爽快。

clipboard.png

设置 eslint 后建议配置开发服务器的 overlay 选项,在 eslint 报错时会覆盖在页面上,时刻提醒你写代码得有信条。

  devServer: {
    overlay: {
      warnings: true,
      errors: true,
    },
  },

资源优化

图片

vue-cli3 搭建的工程没有自带图片优化插件,所以请自行安装。有更好的图片压缩插件求推荐啦,这里使用的是 imagemin-webpack-plugin,基本配置如下:

var ImageminPlugin = require('imagemin-webpack-plugin').default
// Or if using ES2015:
// import ImageminPlugin from 'imagemin-webpack-plugin'

module.exports = {
  plugins: [
    // Make sure that the plugin is after any plugins that add images
    new ImageminPlugin({
      disable: process.env.NODE_ENV !== 'production', // Disable during development
      pngquant: {
        quality: '95-100'
      }
    })
  ]
}

字体

对于中文站点,字体问题可是个大问题,因为中文字体实在太太太太大了,随随便便一个都 10m 了,等待这东西下载真的給用户体验带来致命打击,但是!但是!有这么一个程序!

font-spider-plus

之前一直知道 font-spider,它的功能是获取使用到的字体,然后分析出使用了改字体的字符,最后把字符抽离出来。这大大减少了中文字体的体积,但是缺不能用于 js 渲染的网页,后来才找到 font-spider-plus,虽然要手动操作一下(最简单的方法:发布网页之后用 fsp 把用到的字爬出来),但是得到的优化真的很大哦!

响应式设计

PWA 给了我们一个很简单的跨平台方法,不只有移动端可以添加到首屏,PC 端也同样可以,所以响应式设计对 PWA 可以说是必须的。
PC 端的添加方法在 F12 里,至于更加方便的方法...好像是要改 flag 才能用,那么也谈不上多方便了,所以不介绍了,等到此项技术更加成熟,想必这个按钮就会出现在显眼的位置 ?

clipboard.png

PWA 专属问题

建议使用 LightHouse 给你的 PWA 评分,报告中还会带有修改建议,十分实用。

clipboard.png

manifest.json

PWA 添加至桌面的功能实现依赖于 manifest.json。这个文件结构很简单,填写对应字段即可,可在这里了解详情。

Service Worker 刷新

针对 PWA 功能来说,项目成功生成就已经配置好离线缓存功能,对 Service Worker 不熟悉的同学来说可以说很方便了。但是对于推送功能和 Service Worker 的更多细节仍然需要深入研究才能流畅使用这个新兴玩意。
(以下称 Service Worker 为 SW)
有一个问题特别值得注意,那就是 SW 的更新问题。SW 控制项目环境的缓存,但是 SW 更新后怎么刷新缓存就不那么容易理解了。SW 更新后,会进入 waiting 状态,旧的 SW 依旧正常运行,所以新的 SW 无法激活,你需要关闭整个浏览器才能把旧的 SW 关闭,再次打开网页就能看到新的 SW 运作了。
有一个方便一点的方法,配置

    workboxOptions: {
      skipWaiting: true,
      clientsClaim: true,
    },

之后,第一个选项如其字面意思,会跳过 Waiting 状态,而 clientsClaim 可以让 SW 进入 activated 状态后立即控制页面(但是实际上,SW 即使立刻控制页面并输出新数据,SPA 也要刷新才能渲染为最新版本,但这也总比关闭浏览器好多了)。
详细原理请查阅谷歌官方资料(需要梯子)。

最后,给你一个佛系刷新方法

Service Worker 的特殊之处除了由浏览器触发更新之外,还应用了特殊的缓存策略: 如果该文件已 24 小时没有更新,当 Update 触发时会强制更新。这意味着最坏情况下 Service Worker 会每天更新一次。

Preload

clipboard.png

这个是 Lighthouse 提醒我的。对于一些必要的资源,可以使用 Preload 预先下载(特别是字体或图片等资源),不必等使用时再下载,这又将会是几百毫秒的等待。更多信息可以参考 vue-cli 官网相关页面:https://cli.vuejs.org/zh/guid...

最后

完整配置地址
https://github.com/ssshooter/...
https://github.com/ssshooter/...
这是一个最最简单的 PWA,没有对 SW 进行深度配置,很多功能没有用上。Vue-cli3 搭建的项目使用 workbox-webpack-plugin,SW 默认是自己生成的,需要自己附加 SW 功能可以使用 InjectManifest 插件。最后提一下 Lavas App,可以把你的 PWA 包装成一个 apk,本质是快捷方式,方便了还没支持 PWA 的安卓系统。

如果有其他问题,可以在评论区讨论~

查看原文

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-12-06
个人主页被 196 人浏览