异步和多线程基础

2019
25/05

学习目录:

  • 1 进程-线程-多线程,同步和异步
  • 2 委托启动异步调用
  • 3 多线程特点:不卡主线程、速度快、无序性
  • 4 异步的回调和状态参数
  • 5 异步等待三种方式
  • 6 异步返回值

一、进程-线程-多线程,同步和异步

1.什么是进程?

当一个程序开始运行时,它就是一个进程,进程包括运行中的程序和程序所使用到的内存和系统资源。

而一个进程又是由多个线程所组成的。

2.什么是线程?

线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,即不同的线程可以执行同样的函数。

3.什么是多线程?

多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务。

4.多线程的好处:

可以提高CPU的利用率。在多线程程序中,一个线程必须等待的时候,CPU可以运行其它的线程而不是等待,这样就大大提高了程序的效率。

5.多线程的不利方面:

线程也是程序,所以线程需要占用内存,线程越多占用内存也越多; 

多线程需要协调和管理,所以需要CPU时间跟踪线程; 

线程之间对共享资源的访问会相互影响,必须解决竞用共享资源的问题;

线程太多会导致控制太复杂,最终可能造成很多Bug;

6.何时使用多线程?

 多线程程序一般被用来在后台执行耗时的任务。主线程保持运行,并且工作线程做它的后台工作。对于Windows Forms程序来说,如果主线程试图执行冗长的操作,键盘和鼠标的操作会变的迟钝,程序也会失去响应。由于这个原因,应该在工作线程中运行一个耗时任务时添加一个工作线程,即使在主线程上有一个有好的提示“处理中…”,以防止工作无法继续。这就避免了程序出现由操作系统提示的“没有相应”,来诱使用户强制结束程序的进程而导致错误。模式对话框还允许实现“取消”功能,允许继续接收事件,而实际的任务已被工作线程完成。BackgroundWorker恰好可以辅助完成这一功能。

在没有用户界面的程序里,比如说Windows Service,多线程在当一个任务有潜在的耗时,因为它在等待另台电脑的响应(比如一个应用服务器,数据库服务器,或者一个客户端)的实现特别有意义。用工作线程完成任务意味着主线程可以立即做其它的事情。

另一个多线程的用途是在方法中完成一个复杂的计算工作。这个方法会在多核的电脑上运行的更快,如果工作量被多个线程分开的话(使用Environment.ProcessorCount属性来侦测处理芯片的数量)。

一个C#程序称为多线程的可以通过2种方式:明确地创建和运行多线程,或者使用.NET framework的暗中使用了多线程的特性——比如BackgroundWorker类, 线程池,threading timer,远程服务器,或Web Services或ASP.NET程序。在后面的情况,人们别无选择,必须使用多线程;一个单线程的ASP.NET web server不是太酷,即使有这样的事情;幸运的是,应用服务器中多线程是相当普遍的;唯一值得关心的是提供适当锁机制的静态变量问题。

7、何时不要使用多线程?

多线程也同样会带来缺点,最大的问题是它使程序变的过于复杂,拥有多线程本身并不复杂,复杂是的线程的交互作用,这带来了无论是否交互是否是有意的,都会带来较长的开发周期,以及带来间歇性和非重复性的bugs。因此,要么多线程的交互设计简单一些,要么就根本不使用多线程。除非你有强烈的重写和调试欲望。

当用户频繁地分配和切换线程时,多线程会带来增加资源和CPU的开销。在某些情况下,太多的I/O操作是非常棘手的,当只有一个或两个工作线程要比有众多的线程在相同时间执行任务块的多。稍后我们将实现生产者/耗费者队列,它提供了上述功能。

前端算法:JavaScript实现字符串全排列

2019
25/05

我个人认为前端工程师笔试题中,算法题一般不难,也就这个难度,甚至比这还要简单。这是我在笔试过程中遇到的一个题~下面分享一下解题思路。

大体结构:定义一个方法,传入str变量,返回一个数组,包含所有排列:

function fun(str){ 
   var result = []; 
   return result;
}

主要逻辑:肯定是需要递归的~先将第一个字母取出,然后将剩下的字符串全排列。将这个字母,依次插入到每个排列所有缝隙。 如:abc进行全排列,取出a,得到全排列bc和cb,先向bc插,可以得到abc,bac,bca;再向cb插,得到acb,cab,cba;

if(str.length == 1 || str.length == 0 ){
   result.push(str);
   return result;
}else{
   var one = str.substr(0,1);
   var left = str.substr(1);
   var leftResult = fun(left);
   for(i=0;i<leftResult.length;i++){
      for(j=0;j<leftResult[i].length+1;j++){//加1的目的是让字符one也可以插入到最后一个位置
         result.push(leftResult[i].slice(0,j) + one + leftResult[i].slice(j));
      }
   }
}

这样就能实现字符串的全排列啦~

思路有了,可以用数组的迭代方法来实现:

const anagrams = str => {
   if (str.length <= 2) return str.length === 2 ? [str, str[1] + str[0]] : [str];
   return str.split('').reduce((acc, letter, i) =>acc.concat(anagrams(str.slice(0, i) + str.slice(i + 1)).map(val => letter + val)), [])
};

Markdown语法帮助(HBuilderX)

2019
22/05

markdown – 更简洁、更高效

=============================================

强烈建议开发者认真阅读本文档,掌握md及HBuilderX对md的强大支持。
如果没有点右键设置自动换行,可按Alt+滚轮横向滚动查看。

很多人只把markdown用于网络文章发表,这糟蹋了markdown

markdown不止是HTML的简化版,更重要的是txt的升级版、word的轻量版、笔记的最佳载体。

作为一种简单的格式标记语言,不同于txt的无格式,不同于HTML的复杂标记,也不同于word的鼠标调整样式。markdown通过简单的几个字符键入,就可以快捷的定义文档的样式。

比如在行首敲一个#,就把这行定义为了1级标题,并且在HBuilderX里有直观完善的着色,这样无需发布为web页面,可直接当word用。

掌握markdown,你可以完全抛弃txt和笔记软件的编辑器,并且在大多数场景下替代掉复杂臃肿的word。享受简洁之美、享受效率提升。

HBuilderX,可以被称为最强大的markdown书写工具了。

下面的示例列举了markdown语法及对应的HBuilderX使用技巧:

开始前,可以先按下文档结构图的快捷键Alt+w(Mac是Ctrl+w),浏览本文的大纲。

标题语法

markdown的标题是行首以#号开头,空格分割的,不同级别的标题,在HX里着色也不同。如下:

# 标题1 
## 标题2 
### 标题3 
#### 标题4 
##### 标题5 
###### 标题6

标题使用技巧:

  1. Emmet快速输入:敲h2+Tab即可生成二级标题【同HTML里的emmet写法,不止标题,HX里所有可对应tag的markdown语法均支持emmet写法】。仅行首生效
  2. 智能双击:双击#号可选中整个标题段落
  3. 智能回车:行尾回车或行中Ctrl+Enter强制换行后会自动在下一行补#。而连续2次回车后将自动补的#去掉。(体验同word)
  4. 回车后再次按Tab可递进一层标题,再按Tab切换列表符
  5. 在# 后回车,可上插一个空标题行【同word】,或任意位置按Ctrl+Shift+Enter也可以上插空标题行
  6. 折叠:
    • 点标题前的-号可折叠该标题段落,快捷键是Alt+-(展开折叠是Alt+=
    • 多层折叠时折叠或展开子节点,快捷键是Alt+Shift+-=
    • 全文全部折叠或展开,快捷键是Ctrl+Alt+Shift+-=
    • 折叠其他区域,快捷键是Alt+Shift+o。这对长文档管理非常有用,可以专注于当前章节
    • 可以在菜单-跳转-折叠中随时找到这些功能

列表

markdown的列表支持有序列表、无序列表以及特殊的任务列表。

同样也是在行前加一个特殊符号,并空格后再跟列表文字内容。

有序列表

有序列表就是有顺序的列表,依靠行前的数字标记顺序。

1. 有序列表1 【设置或取消有序列表符的快捷键:`Ctrl+Alt+1`,可选中多行批量设置序号;支持多光标批量设置列表符,即按Ctrl+鼠标左键添加多光标】
2. 有序列表2 【列表后回车会自动补序号】 
3. 有序列表3 【智能双击:双击前面的数字,可重新对数字排序,修正序号错误,并选中有序列表段落(左边的4是故意写错让你体验的)】

无序列表

无序列表就是列表不排序,无序列表因书写随意而被更广泛的使用。
无序列表有3种前缀,HX里分别用于表示1级列表、2级列表、3级列表。

- 无序列表1 【快捷键:`Ctrl+Alt+-`;智能双击:双击-号可选中整段无序列表;再次按Tab会更换二级列表符】
   * 无序列表2 
   * Emmet:`li后敲Tab`可生成*号列表符,行首生效
   * 快捷键:`Ctrl+Alt+8`【8即*对应的数字】,支持多光标批量设置列表符,即按`Ctrl+`鼠标左键添加多光标
   * 智能双击:双击`*`号可选中整段无序列表
   * 智能回车:行尾回车或行中`Ctrl+Enter`强制换行后会自动续列表;连续按回车会清除列表符;再次按Tab会更换列表符;在列表符后回车或行尾`Shift+回车`,上一行留出列表符
   * *号常用于二级列表,列表符后继续Tab,可切换列表符
      + 无序列表3 【快捷键:`Ctrl+Alt+=`;常用于三级列表;其他同上】

任务列表

任务列表非常实用,管理待办已办非常便利。

[ ] 任务列表-未完成任务 【快捷键:`Ctrl+Alt+[`】
[x] 任务列表-已完成任务 【快捷键:`Ctrl+Alt+]`】
  1. 智能双击:双击方括号内可切换勾选状态,把任务标记为完成或未完成;双击方括号右侧可选中任务列表段落
  2.  智能回车:回车后自动补任务列表前缀符号;连续按回车清除前缀符号;在列表符后回车或行尾Shift+回车,上一行留出列表符

以上三种列表,均支持批量修改列表符,有如下方式建议依次学习尝试:

  1. 选中多行,按快捷键Ctrl+Alt+“1”或“-”或“[”或“]”,批量设置列表符
  2. 如果需要跳行设置有序或无序列表,通过Ctrl+鼠标左键点中目标多行(可不连续),产生多光标,然后按快捷键Ctrl+Alt+“1”或“-”或“[”或“]”,可跳行设置列表符,尤其是有序列表,数字也会跳行加1
  3. 按Alt+鼠标选中行首那列(列选择),这样每行行首都有光标,然后再键入或删除列表符即可批量操作
  4. 选中多行,按快捷键Ctrl+Shift+\(其实就是Ctrl+|),可以在每行行首添加一个光标

引用列表

> 引用1
> 引用2

快捷键:Ctrl+Alt+Shift+.

智能双击:双击>号可选中整段引用列表

智能回车:行尾回车或行中Ctrl+Enter强制换行后会自动续列表;连续按回车会清除列表符;在列表符后回车或行尾Shift+回车,上一行留出列表符

文字样式语法

**加粗** 【快捷键:Ctrl+B,支持多光标;Emmet:b后敲Tab】
__加粗2__

MARKDOWN_HASHa32704103cb5e608e494b0db9746e34eMARKDOWNHASH【Emmet:i后敲Tab;前后包围:选中文字按Ctrl+\是在选区两侧添加光标,可以继续输入】

*倾斜*

~~删除线~~

``` 单行代码 ```

包围插入:先选中文字内容,然后按_*~`等符号,会自动在2侧加包围

智能双击:双击语法区前面的定义符号,选中包含定义符的整段文字

去包围:选中整段文字后,按Ctrl+Shift+],可去除2侧包围符号

引号括号虽然不属于markdown语法,但也支持相同的包围、选择、去包围操作。

引号括号智能双击选择时略特殊的是:双击引号括号内侧,选中引号括号里的内容(不含引号括号);按下Alt+双击引号括号内侧,则选中包含符号的整段文字

HBuilderX还支持以下对2侧文本高效处理的手段

  1. 选中文字按Ctrl+\是在选区两侧添加光标,可以继续输入~~,会在2侧同时输入
  2. 向2侧扩大选择:【Win:Alt+Shit+→ 、Mac:Ctrl++Shit+→】;由2侧向内减少选择:【Win:Alt+Shit+←、Mac:Ctrl++Shit+←
[链接文字](http://dcloud.io)
  1. Emmet:a后敲Tab
  2. 打开链接:Alt+鼠标单击;如果是本地文件,可通过Shift+Alt+单击,在另一分栏打开文件
  3. 智能粘贴:粘贴URL会自动变成超链接格式;粘贴本地文件进来也会自动创建引用链接
  4. 智能双击:双击语法区开头,即[左侧,选中包含定义符的整段文字
![图片描述文字](logo.jpg)
  1. Emmet:img后敲Tab
  2. 智能粘贴:粘贴剪切板里的图形时会自动保存为本md文档的附件;删除文档中的图片语法,保存md文档时会自动删除对应的图片附件;粘贴图片文件时自动变成链接引用格式;
  3. 悬浮预览:鼠标移到图片语法上,本地图片会自动显示出来
  4. 智能双击:双击语法区开头,即!左侧,选中包含定义符的整段文字

表格

|       |       |       |
|-- |-- |-- |
|       |       |       |
|       |       |       |
  1. Emmet:table3*3后敲Tab,表示生成3行3列的表格,行首生效
  2. md表格对齐是传统md的痛点,HBuilderX按下Ctrl+K可以自动整理表格格式(暂未兼容不同缩放模式和字体的情况)
  3. 支持从excel、wps、word、number的表格中复制粘贴表格进来(不支持合并单元格和单元格换行)

分割线

------------- 【Emmet:hr后敲Tab】
*************
=============

代码区

``` javascript
  var a = document
```

Emmet:code后敲Tab,行首生效
智能双击:双击语法区开头,即!左侧,选中包含定义符的整段文字

注释

<!--注释-->

快捷键:Ctrl+/
智能双击:双击注释首尾的定义符,选中整段注释

其他emmet快捷输入

day后敲Tab,当前日期。注意day需在行首或前面有空格
time后敲Tab,当前时间。注意time需在行首或前面有空格

文档结构图

文章很长时,word里有文档结构图,HBuilderX也有。
菜单视图-文档结构图,快捷键Alt+W(mac是ctrl+W),轻松管理长文档

运行、预览和打印PDF

对md文件点工具栏或菜单里的浏览器运行,可以使用外部浏览器预览此md文件,会自动渲染为HTML。

点右上角的预览【快捷键Alt+p】,可在HBuilderX右侧预览该md文档的HTML渲染结果。

在浏览器中点打印,选择打印到PDF,可将md输出为PDF格式。(注意在打印选项里去掉页眉页脚)

其他常用但你可能不知道的快捷操作技巧

  • Ctrl+鼠标左键添加多光标,然后敲字或粘贴,可批量处理。Ctrl+鼠标左键拖选,可选中多个选区。
  • Ctrl+鼠标右键删除多光标
  • 不选内容按Ctrl+C或X可复制或剪切整行
  • 选中2个选区后,按Ctrl+Shift+X,可互换选区内容。如无选区,只是2个光标,则互换2行
  • Ctrl+上下键可上下移动行
  • Ctrl+Insert可重复插入当前行,如果有选中内容,可重复插入选中内容
  • Ctrl+Shift+K可合并多行(是格式化Ctrl+K的反操作)
  • 删除
    • 按Ctrl+D可删除选中行,支持多光标
    • Shift+Del删除到行尾
    • Shift+Backspace删除到行首
  • 选择
    • Ctrl+E选相同词(mac是Command+D),连续按可选中多词进一步操作,比替换更方便
    • Ctrl+L可连选多行,Ctrl+Shift+L也是选择行,但不选行首尾的空白字符
    • Ctrl+=可逐级放大选区
    • 双击标题、列表符可选中相应段落
    • 双击英文引号、括号内侧,可选中内部内容
    • 双击缩进符,可选中同缩进段落
    • 双击连字符比如-或_,可选中相连的词,比如双击这里试试,uni-app
  • 查找
    • Ctrl+P查找文件
    • Ctrl+Alt+F可在当前目录的所有文档中搜索指定关键字(mac是Command+Shift+f)
    • 选中文字按F3,查找下一个,Shift+F3找上一个
  • 云同步:HBuilderX+markdown用于云同步笔记的技巧,请参考http://ask.dcloud.net.cn/article/13097

都学会了吗?
markdown语法其实很简单,认真学半小时就能掌握。
HBuilderX的极客操作则需要不停反复练习,熟练掌握这些技巧,你将成为高效极客!

简述关系型数据库和非关系型数据库

2019
21/05

当前数据库分为关系型数据库和非关系型数据库

关系型数据库

关系型数据库:指采用了关系模型来组织数据的数据库。

关系模型指的就是二维表格模型,而一个关系型数据库就是由二维表及其之间的联系所组成的一个数据组织。

关系模型中常用的概念

  • 关系:一张二维表,每个关系都具有一个关系名,也就是表名
  • 元组:二维表中的一行,在数据库中被称为记录
  • 属性:二维表中的一列,在数据库中被称为字段
  • 域:属性的取值范围,也就是数据库中某一列的取值限制
  • 关键字:一组可以唯一标识元组的属性,数据库中常称为主键,由一个或多个列组成
  • 关系模式:指对关系的描述。其格式为:关系名(属性1,属性2, … … ,属性N),在数据库中成为表结构

关系型数据库的优点:

1.容易理解:二维表结构是非常贴近逻辑世界的一个概念,关系模型相对网状、层次等其他模型来说更容易理解

2.使用方便:通用的SQL语言使得操作关系型数据库非常方便

3.易于维护:丰富的完整性(实体完整性、参照完整性和用户定义的完整性)大大减低了数据冗余和数据不一致的概率

关系型数据库存在的问题

1.网站的用户并发性非常高,往往达到每秒上万次读写请求,对于传统关系型数据库来说,硬盘I/O是一个很大的瓶颈

2.网站每天产生的数据量是巨大的,对于关系型数据库来说,在一张包含海量数据的表中查询,效率是非常低的

3.在基于web的结构当中,数据库是最难进行横向扩展的,当一个应用系统的用户量和访问量与日俱增的时候,数据库却没有办法像web server和app server那样简单的通过添加更多的硬件和服务节点来扩展性能和负载能力。当需要对数据库系统进行升级和扩展时,往往需要停机维护和数据迁移。

4.性能欠佳:在关系型数据库中,导致性能欠佳的最主要原因是多表的关联查询,以及复杂的数据分析类型的复杂SQL报表查询。为了保证数据库的ACID特性,必须尽量按照其要求的范式进行设计,关系型数据库中的表都是存储一个格式化的数据结构。

数据库事务必须具备ACID特性,ACID分别是Atomic原子性,Consistency一致性,
Isolation隔离性,Durability持久性。

当今十大主流的关系型数据库

OracleMicrosoft SQL ServerMySQLPostgreSQLDB2Microsoft AccessSQLiteTeradataMariaDB(MySQL的一个分支),SAP

非关系型数据库

非关系型数据库:指非关系型的,分布式的,且一般不保证遵循ACID原则的数据存储系统。

非关系型数据库结构

非关系型数据库以键值对存储,且结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,不局限于固定的结构,可以减少一些时间和空间的开销。

优点

1.用户可以根据需要去添加自己需要的字段,为了获取用户的不同信息,不像关系型数据库中,要对多表进行关联查询。仅需要根据id取出相应的value就可以完成查询。

2.适用于SNS(Social Networking Services)中,例如facebook,微博。系统的升级,功能的增加,往往意味着数据结构巨大变动,这一点关系型数据库难以应付,需要新的结构化数据存储。由于不可能用一种数据结构化存储应付所有的新的需求,因此,非关系型数据库严格上不是一种数据库,应该是一种数据结构化存储方法的集合。

不足:

只适合存储一些较为简单的数据,对于需要进行较复杂查询的数据,关系型数据库显的更为合适。不适合持久存储海量数据

非关系型数据库的分类

非关系型数据库都是针对某些特定的应用需求出现的,因此,对于该类应用,具有极高的性能。依据结构化方法以及应用场合的不同,主要分为以下几类:

面向高性能并发读写的key-value数据库:

key-value数据库的主要特点是具有极高的并发读写性能

Key-value数据库是一种以键值对存储数据的一种数据库,类似Java中的map。可以将整个数据库理解为一个大的map,每个键都会对应一个唯一的值。

主流代表为RedisAmazon DynamoDBMemcachedMicrosoft Azure Cosmos DBHazelcast

面向海量数据访问的面向文档数据库:

这类数据库的主要特点是在海量的数据中可以快速的查询数据

文档存储通常使用内部表示法,可以直接在应用程序中处理,主要是JSON。JSON文档也可以作为纯文本存储在键值存储或关系数据库系统中。

主流代表为MongoDBAmazon DynamoDBCouchbaseMicrosoft Azure Cosmos DBCouchDB

面向搜索数据内容的搜索引擎:

搜索引擎是专门用于搜索数据内容的NoSQL数据库管理系统。

主要是用于对海量数据进行近实时的处理和分析处理,可用于机器学习和数据挖掘

主流代表为ElasticsearchSplunkSolrMarkLogicSphinx

面向可扩展性的分布式数据库:

这类数据库的主要特点是具有很强的可拓展性

普通的关系型数据库都是以行为单位来存储数据的,擅长以行为单位的读入处理,比如特定条件数据的获取。因此,关系型数据库也被成为面向行的数据库。相反,面向列的数据库是以列为单位来存储数据的,擅长以列为单位读入数据。

这类数据库想解决的问题就是传统数据库存在可扩展性上的缺陷,这类数据库可以适应数据量的增加以及数据结构的变化,将数据存储在记录中,能够容纳大量动态列。由于列名和记录键不是固定的,并且由于记录可能有数十亿列,因此可扩展性存储可以看作是二维键值存储。

主流代表为CassandraHBaseMicrosoft Azure Cosmos DBDatastax EnterpriseAccumulo

CAP理论

NoSQL的基本需求就是支持分布式存储,严格一致性与可用性需要互相取舍

CAP理论:一个分布式系统不可能同时满足C(一致性)、A(可用性)、P(分区容错性)三个基本需求,并且最多只能满足其中的两项。对于一个分布式系统来说,分区容错是基本需求,否则不能称之为分布式系统,因此需要在C和A之间寻求平衡

C(Consistency)一致性

一致性是指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致。与ACID的C完全不同

A(Availability)可用性

可用性是指服务一直可用,而且是正常响应时间。

P(Partition tolerance)分区容错性

分区容错性是指分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。

关系型与非关系型数据库的比较

1.成本:Nosql数据库简单易部署,基本都是开源软件,不需要像使用Oracle那样花费大量成本购买使用,相比关系型数据库价格便宜。

2.查询速度:Nosql数据库将数据存储于缓存之中,而且不需要经过SQL层的解析,关系型数据库将数据存储在硬盘中,自然查询速度远不及Nosql数据库。

3.存储数据的格式:Nosql的存储格式是key,value形式、文档形式、图片形式等等,所以可以存储基础类型以及对象或者是集合等各种格式,而数据库则只支持基础类型。

4.扩展性:关系型数据库有类似join这样的多表查询机制的限制导致扩展很艰难。Nosql基于键值对,数据之间没有耦合性,所以非常容易水平扩展。

5.持久存储:Nosql不使用于持久存储,海量数据的持久存储,还是需要关系型数据库

6.数据一致性:非关系型数据库一般强调的是数据最终一致性,不像关系型数据库一样强调数据的强一致性,从非关系型数据库中读到的有可能还是处于一个中间态的数据,Nosql不提供对事务的处理。

作者:意识流丶
来源:简书

ES6常用方法总结—Promise

2019
20/05

1.Promise用法

Promisethen方法会返回一个新的Promise

let p = function(){
    return new Promise((resolve, reject) => {
        try {
            setTimeout(()=>{
               return resolve()
            },500)
        } catch (error) {
            return reject(error)
        }
    })
}

async function do(){
    let res = await p()
    console.log(res)
}

p().then((res)=>{
    console.log(res)
},(err)=>{
    console.log(err)
})

p().then((res)=>{
    console.log(res)
}).catch((err)=>{
    console.log(err)
})

p().then((res)=>{
    console.log(res)
    return p()
}).then((res)=>{
    console.log(res)
}).catch((err)=>{
    console.log(err)
})

2.Promise.all用法(ES11)

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

另外Promise.allSettled看起来像是对Promise.all的一种补充,缓解了使用Promise.all碰到reject的痛点问题。

一句话概括Promise.allSettledPromise.all的最大不同:Promise.allSettled会执行所有的Promise。而Promise.all只要遇到第一个reject的结果,整体就会reject

Promise.allSettled执行后返回的是对象数组,每个对象中包含了各Promise的执行状态(fulfilledrejected)和结果值。

Promise.allSettled()Promise.all()用来做批量异步处理。如果说每一个异步都需要得到结果,就用allSettled()。如果说每一个异步都需要成功,才能往下进行,就用all ()。

let p1 = new Promise((resolve, reject) => {
  resolve('成功了')
})

let p2 = new Promise((resolve, reject) => {
  resolve('success')
})

let p3 = Promse.reject('失败')

Promise.all([p1, p2]).then((result) => {
  console.log(result)               //['成功了', 'success']
}).catch((error) => {
  console.log(error)
})

Promise.all([p1,p3,p2]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)      // 失败了,打出 '失败'
})

//---------------------

async function fn6(){
	let [a,b,c] = await Promise.all([
		await '成功了a',
		new Error('失败了b'), 
		await '成功了c',
	])
	return [a,b,c]
}
fn6().then(res=>{
	console.log('fn6-res',res)   //fn6-res [ '成功了a', Error: 失败了b, '成功了c' ]
})

//------------------------------

async function foo(){
  return Promise.allSettled([
    await new Promise((resolve) => setTimeout(() => resolve("1"), 2000)),
    await new Promise((resolve, reject) => setTimeout(() => resolve("2")))
  ]);
}

foo().then((data) => {
    console.log(data);  //[{status: 'fulfilled', value: '1'},{status: 'fulfilled', value: '2'}]
})

3.promise原理

promise的核心原理其实就是发布订阅模式,通过两个队列来缓存成功的回调(onResolve)和失败的回调(onReject)。实则也是通过回调函数方式实现异步。

4.promise特点

  1. Promise状态不受外部影响。Promise有三个状态:pending进行中、fulfilled已成功、rejected已失败。只有异步操作的结果才可以决定当前是哪个状态,其他任何操作都无法改变状态。(这也就是promise许诺的由来)
  2. 一旦状态改变,就不会再变。Promise对象状态改变只有两种可能:从pending改到fulfilled或者从pending改到rejected,只要这两种情况发生,状态就凝固了不会再变。这时候就称为定型resolved

5.原生实现Promise(手写Promise)

使用发布订阅模式+状态切换原理实现。由于原型方法诸如then、all

function MyPromise(execution) {
	// 静态属性(常量)
	MyPromise.PENDING = 'pending'
	MyPromise.FULFILLED = 'fulfilled'
	MyPromise.REJECTED = 'rejected'

	this.status = MyPromise.PENDING // 状态
	this.reason = null // 失败原因
	this.value = null // 成功返回值
	// 收集依赖缓存(函数)
	this.resolveDeps = []
	this.rejectDeps = []
	// 以上的遍历也可设为私有变量,再添加get/set方法,这里为简化没做

	// this缓存
	var self = this

	// 成功回调
	function resolve(res) {
		// 判断状态
		if (self.status === MyPromise.PENDING) {
			self.value = res
			self.status = MyPromise.FULFILLED

			// 这种方式可以遍历数组且执行后可以清空数组,简化清空数组的步骤
			while (self.resolveDeps.length) {
				// 发布给所有订阅
				self.resolveDeps.shift()(res)
			}
		}
	}

	// 失败回调
	function reject(err) {
		// 判断状态
		if (self.status === MyPromise.PENDING) {
			self.reason = err
			self.status = MyPromise.REJECTED
			// 发布给订阅
			while (self.rejectDeps.length) {
				self.rejectDeps.shift()(err)
			}
		}
	}


	// 判断参数传入是否是函数
	if (typeof execution === 'function') {

		// 捕获错误
		try {
			// 执行传入的执行器
			execution(resolve, reject)

		} catch (err) {
			reject(err)
		}

	} else {
		throw 'MyPromise 的参数必须为函数。'
	}
}


MyPromise.prototype.then = function(onResolved, onRejected) {
	if ((onRejected && typeof onRejected !== 'function') || typeof onResolved !== 'function') {
		throw 'then 方法的参数必须为函数。'
	}

	// then方法链式 返回新promise
	return new MyPromise((resolve, reject) => {

		const resolvedCb = (val) => {
			let nextVal = onResolved(val);

			if (nextVal instanceof MyPromise) { // promise对象

				nextVal.then(resolve, reject)

			} else { // 普通值

				resolve(nextVal);

			}
		}

		const rejectedCb = (reason) => {
			let nextVal = onRejected(reason);

			if (nextVal instanceof MyPromise) { // promise对象

				nextVal.then(resolve, reject)

			} else { // 普通值

				resolve(nextVal)

			}
		}

		// 执行器是异步操作时,status还是pending,
		// 所以此时并不知道是成功还是失败回调,那么这里就需要把这两个回调
		// 存储起来
		this.resolveDeps.push(resolvedCb)
		this.rejectDeps.push(rejectedCb)
	})
}

MyPromise.prototype.catch = function(onRejected) {
	if (typeof onRejected === 'function') {
		// 收集依赖
		this.addRejectDeps(onRejected)
	} else {
		throw 'catch 方法的参数必须为函数。'
	}
}

MyPromise.prototype.finally = function() {}

MyPromise.prototype.all = function() {}

MyPromise.prototype.allSettled = function() {}

// 测试
var demo = function() {
	return new MyPromise((resolve, reject) => {
		setTimeout(() => {
			resolve(123435)
		}, 1000)
	})
}
demo().then((res) => {
	console.log('=========res', res)
	return new MyPromise((resolve, reject) => {
		setTimeout(() => {
			resolve('erersadad')
		}, 1000)
	})
}).then((res) => {
	console.log('=========res2', res)
})

6.promise和async/await、generator区别

Async/await 是Javascript编写异步程序的新方法。以往的异步方法无外乎回调函数和Promise。但是Async/await建立于Promise之上。

async/await:

  1. async/await 是写异步代码的新方式,以前的方法有回调函数和Promise。
  2. async/await 是基于Promise实现的,它不能用于普通的回调函数。
  3. 在主体函数之前使用了async关键字,在函数体内,使用了await关键字。
  4. wait关键字只能出现在用async声明的函数体内。
  5. 当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
  6. async/await 与Promise一样,是非阻塞的。
  7. async/await 使得异步代码看起来像同步代码。

promise和async的区别:

  1. async/await 更加语义化,更简洁
  2. async/await可以使用try{}catch(){}捕获错误
  3. 深层嵌套可以使用promise.then方法进行链式请求,也可以使用promise.all方法。但最简洁的方式是使用async
  4. async 的优越性就是把每次异步返回的结果从 then 中拿到最外层的方法中,不需要链式调用

async 和 Generator区别:

  1. async是generator函数的语法糖。
  2. generator 函数是将函数分步骤阻塞 ,只有主动调用 next() 才能进行下一步
var gen1 = function* () {
    var f1 = yield 1;
    var f2 = yield 2;
    console.log(f1);
    console.log(f2);
};

var aaa = gen1() //undefined

aaa.next() //{value: 1, done: false} //执行f1

aaa.next() //{value: 2, done: false} //执行f2

aaa.next() 
//undefined
//undefined
//{value: undefined, done: true} 最终执行完毕

async对Generator函数做了以下4点改变:

  1. 内置执行器: async函数自带执行器,简单的说async函数就相当于自执行的Generator函数
  2. 更好的语义: async表示函数里有异步操作,await表示紧跟再后面的表达式需要等待结果
  3. 更广的适用性: yield命令后,只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值
  4. 返回值是Promise对象,可以使用then方法指定下一步操作

7.Promise如何转async/await(异步转同步)

new Promiose()async

async function getdata (params) {
  return 'hllo Nodejs'
}
//等价于
function getdata () {
   return Promise.resolve('hllo Nodejs')
}

// async 返回 reject
async function getdata2 (params) {
  return Promise.reject(1234)
}
// 或者
async function aaa (){
  throw 1234
}
// 使用then执行async
getdata2().catch(a=>console.log(a))  //1234

Promiose.then()await

async function fooAsync () {
  const data = await 1
  console.log(data)
}
//等价于
function fooAsync () {
  return Promise.resolve(1).then((data) => console.log(data))
}

await 后面可以是Promise或任意表达式,async函数返回的也是Promise

async function foo1() {
    const result1 = await new Promise((resolve) =>  resolve('1'))
    const result2 = await new Promise((resolve) => resolve('2'))
    return result1 + result2
}

async function foo2() {
    const result = await foo1()
}

async返回一个数组

内部是使用Promise.all()方法来执行,返回可迭代的数组,全部成功才返回成功,一个失败所有的都失败。

async function foo() {
    const result1 = await new Promise((resolve) => {
        setTimeout(() => resolve({ name: "selfsummer" }), 2000);
    });
    const result2 = await new Promise((resolve, reject) => setTimeout(() => resolve({ name: "自夏" })));
    return [result1, result2];
}

foo().then((data) => {
    Array.isArray(data)    // true
    console.log(data)     //[ { name: 'selfsummer' }, { name: '自夏' } ]
}).catch((err) => {
    console.log(err);
});

8.promise和setTimeout区别

Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行

setTimeout不会阻碍后续代码执行,会放到任务队列中。可以理解为一个异步函数。但论其原理实则不是严格意义上的异步,而只是推迟处理。

Promise中的异步操作(promise.then)是微任务。而setTimeout属于宏任务。微任务要优先于宏任务执行。

js事件循环机制总结

9.promise是异步还是同步

promise本身是同步的,其then方法是异步的(promise.then属于异步微任务,then中的方法,必须等到所有的同步任务执行完才执行)。promise是无法取消的,一旦新建就会立即执行。如果不设置回调函数,promise内部的错误是无法反应到外部的。当处于“pending”状态时,无法得知目前进展到哪个阶段(刚刚开始还是即将完成)。

异步的三种实现方式

  1. 回调函数(回调函数不一定是异步 , 但异步一定有回调函数)
  2. 事件
  3. promise 对象

10.Promise在resolve和reject执行之后,后续代码还会执行吗

会的。因为resolve和reject本身是回调函数,无法中断后续代码。需要使用return去中断。所以一般使用默认加return较妥。

function p() {
      return new Promise((resolve, reject) => {
        console.log(222)
        setTimeout(() => {
          console.log('async in p')
          reject('err in p')
          console.log('content after p reject')
        }, 200)
      })
}
function p1() {
      return new Promise((resolve, reject) => {
        console.log(333)
        setTimeout(() => {
          console.log('async in p1')
          resolve('resolev in p1')
          console.log('content after p1 resolve')
        }, 250)
      })
}
function p2() {
      return new Promise((resoleve, reject) => {
        console.log(444)
        setTimeout(() => {
          console.log('async in p2')
          return reject(new Error('p2error'))
          console.log('content after p2 return')
        }, 300)
      })
}

10.Promise的resolve和reject能否用return和throw代替

return无法取代resolve,因为return无法改变 <pending> 状态为<fulfilled>,在底层,需要执行resolve回调函数,才能改变状态。

throw可以取代reject,因为在底层,会有try/catch的错误捕获,如若出错,会将状态置为<rejected>

当然,在async函数中可以使用return和throw实现。

Web前端开发与iOS/Android终端开发的异同

2019
18/05

语言

前端和终端作为面向用户端的程序,有个共同特点:需要依赖用户机器的运行环境,所以开发语言基本上是没有选择的,不像后台想用什么就用什么,iOS只能用Objective-C,前端只能javascript,当然iOS还可以用RubyMotion,前端还能用GWT/CoffieScript,但不是主流,用的人很少,真正用了也会多出很多麻烦。

这两者有个有意思的对比:变量/方法命名的风格正好相反。苹果一直鼓吹用户体验,写代码也不例外,程序命名都是用英文全称并且要多详细有多详细,力求看变量和方法名就能知道是干嘛的,例如application:didFinishLaunchingWithOptions:。而js因为每次都要从网络下载,要力求减少代码体积,所以变量方法名是尽量用缩写,实际上有代码压缩工具,无论变量名写多长最终上线的效果是一样的,但大家也都习惯了用短的命名,例如上述objc的application:didFinishLaunchingWithOptions:方法在js里习惯的命名是:$()

objc与js都是动态语言,使用起来还蛮像,但objc是编译型,速度快,很多错误也能在编译过程中被发现,js是解释型,性能依赖于解释引擎,即使在强劲的v8引擎下性能也赶不上编译型语言,语言太动态,变量完全没有类型,写起来爽,debug起来稍微费点劲。一直感觉js轻巧灵活放荡不羁充满各种奇技淫巧,objc中规中矩没c++ java那么严肃也没有js那么灵活。

线程

前端开发几乎不需要线程这个概念,浏览器实现上页面HTML和CSS解析渲染可能与js不在同一个线程,但所有js代码只执行在一条线程上,不会并发执行,也就不需要考虑各种并发编程的问题。在新的JS特性中可以创建worker任务,这样的任务是可以另起一条线程并行执行的,但由于并不是所有浏览器都支持,不同线程传递数据各个标准定的还不一样,使用场景也少,似乎没有大规模用起来。对于数据库操作/发送网络请求这样的任务是在不同于js代码执行线程的,不过这些都由浏览器管理,前端无需关心也无法影响这些线程,只需接收事件回调,不需要处理任何并发问题。

终端开发需要大量使用多线程,iOS有一条主线程,UI渲染都在这个线程,其他耗时长的逻辑或者数据库IO/网络请求都需要自己另开线程执行,否则会占用主线程的时间,导致界面无法响应用户交互事件,或者渲染慢导致滚动卡顿。程序逻辑分布在多个线程里跑,需要处理好各种代码并发执行可能带来的数据不一致/时序错乱之类的问题,并发也导致有些bug难以排查,一不留神就掉坑,需要适当用一些队列/锁保证程序的执行顺序。iOS提供了一套多线程管理的方法GCD,已经把线程和队列封装得非常简单易用功能强大,比其他端或后台是好很多了,但还是会花大量功夫在处理多线程问题上。

存储

终端开发需要大量的数据存储逻辑,手机APP不像浏览器,用户打开浏览器必定是连着网,但打开一个APP时很可能是离线,也很可能处于网络状况极差的移动GPRS,所以必须把之前请求回来的数据保存好。保存数据后又需要与服务端最新的数据同步,如果全量同步数据量太大,耗流量速度也慢,于是需要增量同步,需要与服务端一起制定实现增量数据返回的方案,需要处理好客户端与服务端数据一致性的问题。当数据存储量大结构复杂时,还需要利用好有限的内存做cache,优化各类存储查询性能。

前端在桌面端很少需要存储,除非是Single Page App,不存储自然就不需要数据更新的一系列工作,数据都是从后台取出拼接后直接显示到页面上,即使像微博有可以在页面内不断加载更多数据,数据也只存在于内存,不会持久化存储,因为桌面端网速稳定,不计流量,所有数据可以直接从后端拿取,客户端没必要再做一套存储。移动端那些做得很像原生APP的Web应用就跟终端开发一样了,数据同样保存到SQLite,存储逻辑以及要处理的问题都差不多。

框架

在第三方框架上Web前端和iOS开发完全相反,Web原生弱小又十分开放,让大量第三方框架和类库可以施展拳脚,而iOS原生强大又十分封闭,导致第三方框架没有多少生存空间。

浏览器一开始只为内容型的网页而设计,js也只是这个网页上能加点小特效的脚本语言,在Web应用时代跟不上发展,需要很多第三方库和框架辅助,再加上前端开发是完全开放的领域,导致库和框架百花齐放多如牛毛,在初期多数库的作用集中在封装dom操作,大家不断重复造dom操作基础库的轮子,在一段时间百家争鸣后独尊jQuery,在有使用库的网站中90%以上使用jq,几乎成了个标准基础库。后期大家已经不再重复造这个基础库的轮子了,多了一些代码组织和前端架构的框架,例如一些帮助项目模块化的框架require.js,MVC框架backbone/angular.js等。

iOS开发苹果已提供了完整的开发框架cocoa,而这框架在每一代系统中都在升级优化和添砖加瓦,开发模式也已经定型,第三方框架没有多少生存空间,大量流行的开源项目是一些通用组件和库,像网络请求库AFNetworking,数据库操作库FMDB。而一些大的框架像beeFramework/ReactiveCocoa较难流行起来。

兼容

前端开发需要兼容大——量的浏览器,桌面的chrome,safari,ie6-ie10,firefox,以及各种套壳猎豹360等浏览器,移动端iOS/Android各自的浏览器,以及无限的不同的屏幕尺寸。看起来挺可怕,实际上也没那么难搞,只是拿出来吓唬下人。桌面端chrome/safari以及各种套壳的极速模式用的都是Webkit,差异很小,firefox也大体遵从标准实现,与Webkit差别不大,旧的ie6/7就需要特别照顾,不过很多网站都不支持ie6了,移动端更是一家亲,全是Webkit,除了新特性上的支持程度不一,其他差异不大。对于不同的屏幕尺寸,高端点的会用响应式布局,针对不同屏幕尺寸自适应到不同布局,一般点的桌面端定死宽度,移动端拉伸自适应宽度就搞定。

终端开发也需要兼容各种不同的系统版本和手机尺寸,Android不用说,iOS也有3.5/4/4.7/5.5/9.7英寸这些尺寸,不过兼容起来跟Web一样挺容易,就是自适应宽度,iOS的UIKit把这些都处理好了,还有autolayoutsizeClass等高级特性可用,在尺寸上并不用花太多功夫。系统版本上iOS7为分水岭,iOS7前后版本UI上差异比较大,需要做一些功夫兼容,不过iOS用户更新换代很快,预计再过一两年iOS7以下用户就可以忽略了。

性能

终端和前端都是面向用户的,性能优化目的都是尽快呈现内容,以及让程序在用户操作下流畅运行。终端主要关注的是存储/渲染性能。当一个APP存储数据量大,数据关系复杂时,数据查询很容易成为性能瓶颈,需要不断优化数据存取的效率,规划数据IO线程,设计内存cache,利用好终端设备有限的内存,渲染上避免重复渲染,尽可能复用视图,寻找最高效的渲染方案。

前端关注页面加载速度,由于Web页面的结构/样式/程序/资源图片都是实时请求的,要让页面更快呈现内容,就要优化这些请求,让这些资源以最快速度加载下来,包括合并图片/合并代码减少请求数,压缩代码,并行请求,根据版本号缓存代码请求,gzip压缩,模块/图片懒加载等。此外跟终端一样也关注渲染性能,遵从一些规则避免页面reflow,避免使用CSS阴影这样耗性能的特效,用CSS3动画代替js等。

编译

终端开发需要编译的过程,把程序编译成机器语言,再与各种库链接后生成平台对应的可执行文件,最后由操作系统调度执行。在iOS终端开发中编译和链接的规则苹果已经在xcode这个开发工具上封装好,一般开发可以不用关心,但有深层需求时还是需要跟编译打很多交道,例如用编译前端Clang自定义静态代码检测规则,写编译脚本做自动化编译和持续集成,打包生成静态库,根据链接后的可执行文件的组成优化APP体积等。

前端开发的程序则不需要编译过程,只需要把代码扔给浏览器,浏览器边解析代码边执行。虽然js/css代码写完无需做任何事情浏览器就可以解析执行,但为了上面说的性能优化,前端代码上线前会对所有代码和资源文件进行处理,这些处理包括:压缩合并js/css,合并css sprite图,处理模块依赖,处理代码资源版本号,处理资源定位等。这个过程很像传统程序的编译,把给人看的代码优化处理成给机器看的,并解决一些依赖关系,可以算是前端的编译过程。像grunt.js/fis这些工具可以帮助完成这个编译过程,通常前端编译跟上线部署结合在一起,作为上线系统的一部分。

安全

前端和终端的安全性问题上虽然不需要像后端考虑得那么多,但还是有些需要注意。在请求的安全上,终端和前端都一样,用户向后端发送的请求都需要经过层层路由,不知道在哪里就被截获篡改或回放了,于是需要做一些措施防御这些情况,最常见的就是身份验证,多是采用会过期的token形式代替用户名密码,防止被抓包后黑客可以永远登陆这个账号。数据安全要求高的会用加密传输,或者使用https,另外还需要看情况处理一些DNS劫持,运营商广告植入等问题。

其他安全问题终端很少考虑,在未越狱的iOS机器上系统已经帮忙保证了整个APP运行环境的安全,而在越狱的机器下恶意程序拥有root权限可以做任何事情,APP也难以防范。前端方面浏览器的特性使前端开发有几个安全隐患,一是Web页面上任意位置都可以动态插入js代码,浏览器会无区别地执行这些代码,二是身份验证信息都统一保存在cookie里,三是页面上可以随意通过iframe嵌入其他网站的页面。造成XSS、CSRF、cookie劫持这些攻击手段,所以前端写代码时都需要考虑还这些安全问题,做好相应的防范,最简单和重要的防范就是对所有用户输入输出的内容做完整的过滤,避免页面内被嵌入恶意代码。

交互/开发

最后说下对这两个领域在交互和开发上的个人感触。以前在做Web前端时,感觉Web让人机交互倒退了十年,交互都是硬邦邦的点击—啪一下出来结果,滚动是一格格地刷新,很多人当时在鼓吹html5可以做出多么炫的效果时,实际上FLASH在十年前就可以做出来了,还比最现代的浏览器更流畅。iPhone流行后,人机交互终于恢复了应有的水平,体验上比Web流畅太多,指尖交互/流畅的动画/便捷的滑动手势/无限制的实现,主流终于恢复或超越了十年前Flash的水平。

但人机交互提升了,开发方式却大倒退,Web的开发方式非常先进,用户用到的都是最新版本,发现bug可以马上上线秒修复,特别适用于互联网环境下的快速迭代,而终端APP不行,撇开iPhone的审核不说,Android也无法做到保证用户用的是最新的程序,用的都是传统的客户端更新的方式,bug的修复版无法及时给到用户,无法一天上线几十次,需要维护很多旧版本,开发方式倒退回Web时代以前。这都是因为移动网络不稳定以及流量有限造成的,移动端无法像桌面端浏览器那样完全依赖网络,所以在移动网络稳定流量免费之前,开发方式都不会有多大变化。

另外并不看好HTML5,网络上说它可以取代APP说了三四年,到现在也没什么战绩,我看不到它的优势,原生APP可以获得更多的系统资源,更流畅的人机交互体验,HTML5在这方面永远比不上,而它在移动端网络和流量的限制下也无法发挥Web的开发优势,所以它不会成为主流,只适合做一些轻量的小东西。

细说后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)

2019
18/05

前端与后端渲染方式的发展大致经历了这样几个阶段:后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)。

1. 后端模板渲染

前端与后端最初的渲染方式是后端模板渲染,就是由后端使用模板引擎渲染好 html 后,返回给前端,前端再用 js 去操作 dom 或者渲染其他动态的部分。

这个过程大致分成以下几个步骤:

说明:

  1. 前端请求一个地址 url
  2. 后端接收到这个请求,然后根据请求信息,从数据库或者其他地方获取相应的数据
  3. 使用模板引擎(如 java>jspphp>smarty)将这些数据渲染成 html
  4. 将 html 文本返回给前端

在这个过程中,前端的 html 代码需要嵌入到后端代码中(如 javaphp),并且在很多情况下,前端源代码和后端源代码是在一个工程里的。

所以,不难看出,这种方式的有这样的几个不足:

  1. 前后端杂揉在一起,不方便本地开发、本地模拟调试,也不方便自动化测试
  2. 前端被约束在后端开发的模式中,不能充分使用前端的构建生态,开发效率低下
  3. 项目难以管理和维护,也可能会有前后端职责不清的问题

尽管如此,但因为这种方式是最早出现的方式,并且这种渲染方式有一个好处,就是前端能够快速呈现服务器端渲染好的页面,而不用等客户端渲染,这能够提供很好的用户体验与 SEO 友好,所以当下很多比较早的网站或者需要快速响应的展示性网站仍然是使用这种方式。

2. 客户端渲染

随着前端工程化与前后端分离的发展,以及前端组件化技术的出现,如 react、vue 等,客户端渲染已经慢慢变成了主要的开发方式了。

与后端模板渲染刚好相反,客户端渲染的页面渲染都是在客户端进行,后端不负责任何的渲染,只管数据交互。

这个过程大致分成以下几个步骤:

CSR

说明:

  1. 前端请求一个地址 url
  2. 后端接收到这个请求,然后把相应的 html 文件直接返回给前端
  3. 前端解析 js 后,然后通过 ajax 向后台获取相应的数据
  4. 然后由 js 将这些数据渲染成页面

这样一来,前端与后端将完全解耦,数据使用全 ajax 的方式进行交互,如此便可前后端分离了。

其实,不难看出,客户端渲染与前后端分离有很大的好处:

  1. 前端独立出来,可以充分使用前端生态的强大功能
  2. 更好的管理代码,更有效率的开发、调试、测试
  3. 前后端代码解耦之后,能更好的扩展、重构

所以,客户端渲染与前后端分离现在已经是主流的开发方式了。

但这种方式也有一些不足:

  1. 首屏加载缓慢,因为要等 js 加载完毕后,才能进行渲染
  2. SEO 不友好,因为 html 中几乎没有可用的信息

3. node 中间层

为了解决客户端渲染的不足,便出现了 node 中间层的理念。

传统的 B/S 架构中,是 浏览器->后端服务器->浏览器,上文所讲的都是这种架构。

而加入了 node 中间层之后,就变成 浏览器->node->后端服务器->node->浏览器。

这个过程大致分成以下几个步骤:

SSR

说明:

  1. 前端请求一个地址 url
  2. node 层接收到这个请求,然后根据请求信息,向后端服务器发起请求,获取数据
  3. 后端服务器接收到请求,然后根据请求信息,从数据库或者其他地方获取相应的数据,返回给 node 层
  4. node 层根据这些数据渲染好首屏 html
  5. node 层将 html 文本返回给前端

一个典型的 node 中间层应用就是后端提供数据、node 层渲染模板、前端动态渲染。

这个过程中,node 层由前端开发人员掌控,页面中哪些页面在服务器上就渲染好,哪些页面在客户端渲染,由前端开发人员决定。

这样做,达到了以下的目的:

  1. 保留后端模板渲染、首屏快速响应、SEO 友好
  2. 保留前端后分离、客户端渲染的功能(首屏服务器端渲染、其他客户端渲染)

但这种方式也有一些不足:

  1. 增加了一个中间层,应用性能有所降低
  2. 增加了架构的复杂度、不稳定性,降低应用的安全性
  3. 对开发人员要求高了很多

4. 服务器端渲染(ssr)

大部分情况下,服务器端渲染(ssr)与 node 中间层是同一个概念。

服务器端渲染(ssr)一般特指,在上文讲到的 node 中间层基础上,加上前端组件化技术在服务器上的渲染,特别是 react 和 vue。

react、vue、angular 等框架的出现,让前端组件化技术深入人心,但在一些需要首屏快速加载与 SEO 友好的页面就陷入了两难的境地了。

因为前端组件化技术天生就是给客户端渲染用的,而在服务器端需要被渲染成 html 文本,这确实不是一件很容易的事,所以服务器端渲染(ssr)就是为了解决这个问题。

好在社区一直在不断的探索中,让前端组件化能够在服务器端渲染,比如 next.js、nuxt.js、razzle、react-server、beidou 等。

一般这些框架都会有一些目录结构、书写方式、组件集成、项目构建的要求,自定义属性可能不是很强。

以 next.js 为例,整个应用中是没有 html 文件的,所有的响应 html 都是 node 动态渲染的,包括里面的元信息、 css、js 路径等。渲染过程中, next.js 会根据路由,将首页所有的组件渲染成 html,余下的页面保留原生组件的格式,在客户端渲染。

5. SSR优缺点

1)优点:

  • 更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
  • 首屏加载更快:SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;

2)缺点:

  • 更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
  • 更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源,因此如果你预料在高流量环境下使用,请准备相应的服务器负载,并明智地采用缓存策略。

6. spa & ssr选择

  1. 不需要首屏快速加载、SEO 友好的,用全客户端渲染。
  2. 需要首屏快速加载、SEO 友好的,如果用了如 react、 vue 等组件化技术,将不得不用 node 中间层与服务器端渲染。
  3. 如果技术团队不支持,不建议在需要首屏快速加载、SEO 友好的地方使用如 react、 vue 等组件化技术。
  4. 前后端分离之后也可以做后端模板渲染,这样前端的调试可以搭配 handlebars、ejs 等模板引擎进行本地调试,而后端的调试则需要到测试机了。

Vue 服务端渲染简介和实践

2019
18/05

SSR , Server Side Render的简称, 服务端渲染. 首先服务端渲染并不神秘, 在 ajax 兴起之前, 所有 web 应用都是服务端渲染, 服务器直接返回 html 文本给浏览器, 用户操作比如在 A 页面注册提交表单, 跳转到B 页面, 服务器需要返回两个页面. 这样的弊端显而易见, 加大了服务器的消耗, 随着 JavaScript 的发展, ajax 技术的出现, 客户端的操作通过请求接口的形式与服务器交互, 服务器不用返回整个页面, 而只是数据. 后来出现了后端模版, 比如 jsp, cshtml

<table>
    <c:forEach var="data" items="${datas}" varStatus="loop">
    <tr>
        <td>${loop.index + 1}</td>
        <td>${data.time}</td>
        <td>${data.msg}</td>
    </tr>
    </c:forEach>
</table>

用户在首次进入页面的时候, 通过服务端渲染给出 html, 用户操作使用 ajax 与服务端交互, 动静混合的形式.

后来随着 JavaScript 的发展, 前端模版和近年SPA 框架的发展, 呈现页面完全静态化, 动态内容交给前端(Javscript), 服务器只提供数据(一般以 json 的形式). 用户看到页面, 大致上需要如下过程(忽略 cdn 等)

1.浏览器加载所有静态资源(html,css,js等)–> 2.js 发起请求获取数据 –> 3.渲染页面 –> 呈现用户

好处是前后端完全分离(开发部署), 各司其职, 同时也节约服务器资源(只有数据交互).
此时用户所获取的 html 只是如下的片段:

<!DOCTYPE html><html class=has-full>
<head><meta charset=utf-8>
<title>人事管理系统</title>
<link href=/static/css/app.0c7e1e58d27be30db979adc44f7cd4eb.css rel=stylesheet>
</head>
<body><div id=app></div>
<script type=text/javascript src=/static/js/manifest.ca2797291add890279b8.js></script>
<script type=text/javascript src=/static/js/vendor.ee32e29412ede428634a.js></script>
</body>
</html>

其中2, 3 步骤是最耗费时间的, 因为获取数据受到用户网络, 服务器带宽等条件的显示, 而且可以通过业务数据再次加载一些静态资源. 随着业务的复杂, 打包处理 bundle 逐渐增大, 用户看到页面的时间(首屏), 即内容到达时间(time-to-content)将延长, 降低用户体验, 对电商网站流量转换率影响比较明显.

ssr 所做的事情

借用 react ssr 的两张图说明问题( vue 的 ssr 和 react 同理)

SSR服务端渲染:
SSR
CSR客户端渲染:
CSR
最大的差异是, 服务端直接返回的 渲染完毕html 页面, 获取业务数据, 填充业务组件都在服务端完成, 用户能够更快的看到页面内容, 同时也有利于爬虫抓取(SEO).

但是 ssr 也不是万能的, 需要 node 服务器, 很耗费性能, 需要做好缓存和优化, 相当于空间换时间. 全站 ssr 明显不可取, 现在流行较多的是 首屏 ssr ,甚至 首屏部分 ssr

参考资料
前后端渲染之争
Vue 全站服务器渲染 SSR 实践

Nuxt

有上述可知, ssr 应该有两个代码入口, 服务端和客户端, 通过 webpack 打包之后为别为 server-bundle 和 client-bundle, 页面第一次呈现, 通过 server-bundle , 获取业务数据, 填充数据, 渲染组件, 发送 html 给浏览器, 之后用户操作通过 client-bundle, 依旧是在浏览器范围内.

从零开始配置vue ssr 是比较困难的, 幸好有 nuxt api.

nuxt预设了 vue 服务端渲染的一些配置, 约定大于配置,

pages 路由, vuex 模块划分

Quick start

vue init nuxt-community/starter-template my-project
# 安装依赖
yarn install
# 开发模式运行
yarn run dev
# build 生成环境
yarn run build
# 运行已 build 的代码
yarn run start

目录结构
layouts, middleware, pages, static, store 目录必须存在

配置文件nuxt.config.js,
尽可能罗列了 nuxt.config.js可配置项和默认值

module.exports = {
  cache: {},
  css: [
    // 加载一个 node.js 模块
    //  'hover.css/css/hover-min.css',
    //  // 同样加载一个 node.js 模块,不过我们定义所需的预处理器
    //  { src: 'bulma', lang: 'sass' },
    //  // 项目中的 CSS 文件
    //  '~assets/css/main.css',
    //  // 项目中的 Sass 文件
    //  { src: '~assets/css/main.scss', lang: 'scss' } // 指定 scss 而非 sass
  ],

  // 默认 true
  dev: process.env.NODE_ENV !== 'production',

  // 创建环境变量
  env: {},

  // 配置 Nuxt.js 应用生成静态站点的具体方式。
  genetate: {
    dir: '',
    minify: '',
    routes: [],
  },

  /*
    * vue-meta
    * Headers of the page
    */
  head: {
    title: 'ssr-vue',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'Nuxt.js project' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Customize the progress bar color
  */
  loading: { color: '#3B8070' },
  /*
  ** Build configuration
  */
  build: {
    /*
    ** Run ESLint on save
    */
    extend (config, { isDev, isClient }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  },

  performance: {
    gzip: false,
    prefetch: true
  },

  // 引入 Vue.use 的插件
  plugins: [],

  // 默认当前路径
  rootDir: process.cwd(),

  router: {
    base: '',
    mode: 'history',
    linkActiveClass: 'nuxt-link-active',
    scrollBehavior: (to, from, savedPosition) => {
      // savedPosition 只有在 popstate 导航(如按浏览器的返回按钮)时可以获取。
      if (savedPosition) {
        return savedPosition
      } else {
        let position = {}
        // 目标页面子组件少于两个
        if (to.matched.length < 2) {
          // 滚动至页面顶部
          position = { x: 0, y: 0 }
        }
        else if (to.matched.some((r) => r.components.default.options.scrollToTop)) {
          // 如果目标页面子组件中存在配置了scrollToTop为true
          position = { x: 0, y: 0 }
        }
        // 如果目标页面的url有锚点,  则滚动至锚点所在的位置
        if (to.hash) {
          position = { selector: to.hash }
        }
        return position
      }
    },
    // default
    middleware: 'user-agent',
    // 扩展路由
    extendRoutes: () => {},

    // 默认同 rootDir
    srcDir: this.rootDir,

    transition: {
      name: 'page',
      mode: 'out-in'
    },
    watchers: {
      chokidar: {}, // 文件监控
      webpack: {
        aggregateTimeout: 300,
        poll: 1000
      }
    }
  }
}

pages 路由

路由, 约定大于配置, 支持动态, 嵌套, 动态嵌套路由, 过渡效果和中间件,
通过文件夹目录名称, 组件名称, 生成路由配置,
默认的 transitionName 为 page, 可在 assets 中添加全局的过渡效果

路由中间件:
在匹配页面之前执行;
nuxt.config.js –> 执行middleware –> 匹配布局 –> 匹配页面

路由中间件

视图

模版

默认的 html 模版: 应用根目录下的 app.html 文件, 没有改文件, 则采用默认的模版

页面

页面就是我们最熟悉的.vue文件, 单文件组件, 但是 nuxt 有一些不同的地方, 混入了 asyncData, fetch, head 三个方法, 还有 指定 layout, transition, scrollToTop, validate, middleware配置项

asyncData 和 fetch都是获取数据的方法, 不同的是, asyncData是请求接口的数据, fetch 是用于填充store 数据, 不会设置组件的数据, 两者都在页面加载之前调用

代码build 之后有服务端和客户端两个入口, build 之后对应为 client.js 和 server.js,
asyncData,和 fetch 第一次在服务端执行, 第二次切换页面后在浏览器执行

head 方法相关使用方法, 可参考 vue-meta
nuxt.config.js 默认定义了全局的 mota 标签

页面相关 API

异步数据

通过 asyncData 获取异步数据, 第一个参数为上下文对象 context, 推荐使用 promise 或者 async/await

asyncData
context 对象

资源文件

项目中编写 js 文件和普通项目一样. 通过 webpack 处理, 对于一些不需要 webpack loader 处理的静态资源文件, 必须放在项目根目录下的static文件夹中, 项目中直接使用/引用相关资源,
需要 webpack 处理的 静态文件,

可以覆盖 nuxt.config.js 中build 字段中 loaders 中 url-loader 或者 file-loader的 默认配置, 进行自定义设置

loaders 配置

[
  {
    test: /\.(png|jpe?g|gif|svg)$/,
    loader: 'url-loader',
    query: {
      limit: 1000, // 1KO
      name: 'img/[name].[hash:7].[ext]'
    }
  },
  {
    test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
    loader: 'url-loader',
    query: {
      limit: 1000, // 1 KO
      name: 'fonts/[name].[hash:7].[ext]'
    }
  }
]

npm 模块 & 插件

服务端渲染, node 直接返回 html 给客户端, 所以 npm模块和插件应在 整个应用 实例化之前运行, 并且必须支持 ssr(服务端获取不到 window 等对象), 组件的生命周期只有beforeCreate 和created在client 和 server 均调用. 其余钩子函数只在 client 调用.

node_modules 中安装的模块在组件中可以直接使用

import someModules from 'some-module'

避免多个组件引用同一个模块重复打包的问题, 需要在 nuxt.config.js 中配置 vendor(路径和 plugin一致), 尽量将第三方模块打包至单独的文件中去

module.exports = {
    build: {
        vendor: ['path/to/your/modules'],
    },
    plugins: ['path/to/your/modules']
}

可以区分server 端插件和 client 端plugin, ssr 为 true 则只在服务端使用, 为 false 则反之

plugins: [{src: 'path/to/your/modules', ssr: false}]

ssr 部署及 pm2 的使用

使用 nuxt 官方模版新建的项目, 可以运行yarn build 命令进行构建, build 模板为项目根路径下的 .nuxt 文件夹, 其中 client.js 为客户端入口, server.js 为服务端入口, 通过命令nuxt start 启动 build 之后的代码.

通过简单命令启动在生成环境并不是好办法, 因此我们需要工具, 推荐pm2;

简单来说, PM2是node编写的, 进程管理工具,可以利用它来简化很多应用管理的繁琐任务,如性能监控、自动重启、负载均衡等。

安装之类的不在赘述, 具体参考文档 pm2

简单示例

在项目根目录新建 start.sh, 内容如下

#! /bin/bash
nuxt build
nuxt start

同样在项目根目录新建pm2.config.js, 内容如下

module.exports = {
  apps: [
    {
      name: 'test',
      script: './start.sh',
      env: {
        NODE_ENV: 'development'
      },
      env_production: {
        NODE_ENV: 'production'
      }
    }
  ]
}

参数设置可参考pm2 文档, 这只是个简单示例, 正式环境需要设置集群模式等
运行

pm2 start /path/to/pm2.config.js

即可开启 ssr 服务, 正式环境需要 nginx 代理到80或者443端口

作者:ethan_you
来源:简书

js知识总结—ES6篇(二)

2019
16/05

1.Fetch替代Ajax

Fetch并不是ES6的语法(应该是ES7),而是未来用于替代XMLHttpRequest的API, 它是W3C的正式标准。

Fetch API提供了一个fetch()方法,它被定义在BOM的window对象(全局方法)中。 该方法返回的是一个Promise对象。

fetch 规范与 jQuery.ajax() 不同:

  1. 当接收到一个代表错误的 HTTP 状态码时,从 fetch() 返回的 Promise 不会被标记为 reject, 即使响应的 HTTP 状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。
  2. fetch() 可以接受跨域 cookies;你也可以使用 fetch() 建立起跨域会话。跨域网站的Set-Cookie 头部字段将会被无视。
  3. fetch 不会发送 cookies。除非你使用了same-origin 的初始化选项。
  4. fetch发送post请求的时候发送2次,第一次状态码是204,第二次才成功原因,因为你用fetch的post请求的时候,导致fetch 第一次发送了一个Options请求,询问服务器是否支持修改的请求头,如果服务器支持,则在第二次中发送真正的请求。(跨域)

使用方法:

fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

// 传参
var url = 'https://example.com/profile';
var data = {username: 'example'};

fetch(url, {
  method: 'POST', // or 'PUT'
  body: JSON.stringify(data), // data can be `string` or {object}!
  headers: new Headers({
    'Content-Type': 'application/json'
  })
}).then(res => res.json())
.catch(error => console.error('Error:', error))
.then(response => console.log('Success:', response));

2.介绍一下Symbol

Symbol是ES6新属性(不是构造函数,不能使用new关键字),代表用给定名称作为唯一标识,这种类型的值可以这样创建:let id = Symbol("id")

Symbol可以确保值的唯一,即使采用相同的名称,也会产生不同的值。

  1. Symbol一般用于对象的key。let id = Symbol("id"); let obj = {[id]:1}; 
  2. 获取obj对象的所有Symbol:Object.getOwnPropertySymbols(obj)
  3. 获取含Symbol的对象的所有key不能使用Object.keys(obj),而是Reflect.ownKeys(obj)
  4. Reflect是一个内置的对象,它提供拦截 JS 操作的方法。这些方法与proxy handlers的方法相同

3.Map数据结构

js的对象,本质上是键值对的集合,但是传统上只能用字符串当作键。

ES6提供了Map数据结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串(优势),各种类型的值(包括对象)都可以当作键。

let m=new Map();
let o={p:'key'};
m.set(o,'aaa');
m.get(o);    // 'aaa'

作为构造函数,Map也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。

let map=new Map([
    ['name','张三'],
    ['age',18]
])
map.size    // 2
map.get("name") //"张三"

4.Map与对象比较,其优势

  1. Object的键只能是字符串,Map的键可以是任意类型的值(包括对象)。所以Map是一种更完善的Hash结构实现。
  2. 我们可以轻易得改变或删除Object的属性或方法,安全性较低。但Map能够避免这个问题。Map提供了set和delete方法来重设或删除属性。
  3. Object使用for.in遍历会将其原型链上的属性和方法同时遍历。我们需要通过obj.hasOwnProperty来筛选当前的属性是在自己,还是在原型链上的​。或者使用Object.keys(obj)方法取出键数组再进行遍历。而Map提供了forEach​/keys​/values等方法。

5.Map 与 WeakMap 的区别

Map 和 WeakMap 是两种数据结构,可用于操纵键和值之间的关系。WeakMap(弱引用的Map)也用于生成键值对的集合。

WeakMap可以使用set方法添加成员,也可以用get方法获取值。也可以接收一个数组作为构造函数的参数。

const wm1 = new WeakMap()
const key = {foo:1}
wm1.set(key,2)
wm1.get(key)  //2

// 接受复合数组为参数
const key2 = [1,2,3]
const wm2 = new WeakMap([
    [key,'foo'],
    [key2,'bar']
])
wm2.get(key2)  //bar

区别:

  1. WeakMap只接受对象(null除外、Symbol除外)作为键名,不接受其他类型的值作为键名。而Map键名可以是任意类型。
  2. WeakMap键名所指向的对象不计入垃圾回收机制。(键名所指向的对象被删除,该键值对就会自动清除。该键名指向的对象所占内存会被清除)
  3. WeakMap弱引用只针对键名而不是键值。键值依然正常引用。(键值引用的对象被删除,该键值对依然存在,该值引用的对象所占内存依然不被回收)

作用:

  1. 之所以键只接受对象,就是为了应对键所对应的对象可能会在将来消失的场景
  2. 有助于防止内存泄漏。

WeakMap设计目的在于,有时候我们想在某个对象上面放一些数据,但是会形成对这个对象的引用,一旦不在需要这个对象,我们除删除这个对象之外,也必须删除这个引用,否则垃圾回收机制就不会释放这个对象所占的内存,如果忘了删,就会造成内存泄漏。

var aaa = {a:1}
var bbb = [aaa,'sadad']
// 当不在需要aaa时
aaa = null
// 但bbb对aaa的引用依然存在

WeakMap键名所引用的对象都是弱引用,即垃圾回收机制(CG)不该将该引用考虑在内,因此,只要引用的对象及其他引用都删除,CG就会释放该对象的内存。也就是说,一旦不需要,WeakMap中键名对象和所对应的键值对就会消失(亲测不会立即清除,而是在下次CG回收时清除),不需要手动删除。

6.for…in、for…of、forEach()有什么区别

for…in

  1. 遍历的是数据结构中的key(遍历对象返回的对象key值,遍历数组返回的数组的索引)
  2. 不仅可以遍历对象构造器上的key,还会遍历对象原型上的可枚举key
  3. 也可以用来遍历数组,但是他是为遍历对象属性而构建的,所以不建议遍历数组。(而且他会遍历原型上可枚举的属性或方法,所以在原型上若有自定义的可枚举的方法也会被遍历出来导致出错,比如自定义:Array.prototype.a = xxx
  4. 不支持遍历MapSet
  5. 不支持breakcontinuereturn关键字
  6. 遍历是随机的
  7. 更适合用来遍历对象

将原型上可枚举的key设为不可枚举方式:

function aaa(){
  this.a=1
}
aaa.prototype.b = 2

for(let key in new aaa()){
  console.log(key)
}

// 结果 a b

// 设为不可枚举
Object.defineProperty(aaa.prototype, 'b',
  { enumerable: false }
)

for(let key in new aaa()){
  console.log(key)
}

// 结果 a

for…of

  1. 遍历的是数据结构中的value,且只能遍历可迭代的对象,或者说类数组结构,如Array, Map, Set, String, arguments, NodeList(Dom元素集合)等
  2. ES6新引入的特性
  3. 支持SetMap对象类型
  4. 不能循环普通的对象(会直接报错,因为普通对象不是类数组结构),需要通过和Object.keys()搭配
  5. 它可以与breakcontinuereturn关键字配合使用
  6. 遍历是有序的
// 遍历Map
var bbb = new Map([['a',1],['b',2]])

for(let val of bbb){ console.log(val) }

// ['a', 1]
// ['b', 2]

forEach()

  1. 用来遍历数组,不能遍历普通对象
  2. 不能使用breakcontinue中断循环,也不能使用 return 语句返回到外层函数
  3. 可以遍历value和引索

7.在js中使用ES6的模块化语法ES Module

script标签中使用type="module"属性可以原生实现ES Module

 // 方法 1 : 引入module.js,然后在script标签里面调用
  <script type="module">
    import test from './module.js';
    console.log(test())
  </script>
 
  // 方法 2 : 直接引入index.js,使用src引入
  <script type="module" src="./index.js"></script>

8.includes方法及其与indexOf区别

ES6之includes方法详解及其与indexOf区别

js知识总结—ES6篇(一)

2019
15/05

1.列举ES6的一些新特性

  1. 默认参数
  2. 模板字符串
  3. 解构赋值
  4. 增强的对象字面量
  5.  箭头函数
  6. Promises 异步
  7. generator和async/await
  8. 块作用域 和let和const
  9. Class 类
  10. Modules 模块

2.let ,const,var及其区别

JS代码在执行前会进行预解析。预解析会进行变量提升。

var 声明的变量会发生提升(提升到当前作用域顶部)。虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量,这种情况就叫做提升,并且提升的是声明。

注意:是申明的提升,赋值并不会提升。赋值和申明并不同时发生。只有先声明才能使用这个变量,后申明的值前面无法直接使用。

(这里注意undefined的写法,避免手写代码时写错)

console.log(a) // undefined,如果后面没有申明变量a,这里就不是undefined而是直接报错:is not defined
var a = 1
// 同理如下
var a1 = a2+1
var a2 = 10
console.log(a1) // NaN
//----------------------------------------
var a = 10
var a
console.log(a) // 10
//原因:var会忽略同一个变量声明,在一个作用域中,如果已经有一个变量使用var声明了,那JS会忽略后续的同一个变量声明。
var a = 'hello world';
var a = undefined;
console.log(a) //undefined
// var a = undefined虽然作用等同于var a 但由于赋值的覆盖,所以会打印undefined
//----------------------------------------
console.log(a) // ƒ a() {} 这就是为什么函数可以放到下面,在上面依然能调用的原因
function a() {}
var a = 1 // 函数会被提升,且优先于变量
//-----------------------------------------
let a = 1
let a = 2 //error:Identifier 'a' has already been declared
// let 无法重复声明变量

let和const 声明的变量不会提升。

  1. let 和 const 在全局作用域下声明变量,变量并不会被挂载到 window 上,而 var 会被挂载到window对象中。
  2. let 和 const 在声明 变量 之前如果使用了这个变量,就会出现报错,是因为存在暂时性死区。(let实际也会进行提升,只不过因为暂时性死区导致了并不能在声明前使用。)
  3. let和const无法重复声明,而var可以重复申明,且var后声明的值会覆盖前值(同名函数也会覆盖之前的函数)。如果后申明的重复变量未赋值,会被忽略。
  4. var没有块级作用域,只有函数作用域。而let和const有块级作用域
  5. const申明了变量后必须赋值。不赋值会报错。(const a;是错误的)
  6. const定义的常量不能被修改。
console.log(a) // Uncaught ReferenceError: a is not defined
let a // 这里将提升限制了,所以提示a未申明

函数作用域:

function fn(){
   var aaa = 1
}
fn()
console.log(aaa) // 报错,未定义

经典面试题:

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

// 第一个打印 undefined 是由于
// 函数申明也会被提升,且优先于变量
// 函数作用域中的变量的申明也会提升到函数最顶部
// 函数作用域中变量为局部变量,作用域链优先取局部值,局部没有才会找外层
// 上述代码等同于下:

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

面试题2

function fn(){
    console.log(a); // undefind
    a = 2
    console.log(a) // 2
}
fn() // 在这里执行时候,a只是提升申明,还没有赋值
var a = 1;

//----------------------------

function fn(){
    console.log(a); // 1
    a = 2
    console.log(a) // 2
}
var a = 1;
fn() // 在这里执行,a已经被赋值

块级作用域:

js有全局作用域、函数作用域。在ES6之后新增块级作用域。也就是`{}`中的作用域。if和for语句是典型的块级作用域。var在函数作用域中不会垮函数访问,而可以垮块级访问。也就是说var只承认函数作用域。
for(var i=0;i<3;i++){
    console.log("a:",i)
}
console.log("b:",i)
// a: 0
// a: 1
// a: 2
// b: 3

for(let j=0;j<3;j++){
    console.log("a:",j)
}
console.log("b:",j)
// a: 0
// a: 1
// a: 2
// j is not defined

当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到当前的函数(只读),而不会指向全局的变量。但在全局中,访问不到该函数。

var foo = 1
(function foo() {
    foo = 10
    console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }

console.log(foo) //1

小结:

函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部,而申明的值不会提升

var 存在提升,我们能在声明之前使用。let、const 因为暂时性死区的原因,不能在声明前使用

var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会

let 和 const 作用基本一致,但是后者声明的变量不能再次赋值

var只能提升变量或函数的申明,无法提升申明时的赋值。

var会忽略同一个变量声明,在一个作用域中,如果已经有一个变量使用var声明了,那JS会忽略后续的同一个变量声明。但是赋值还是继续有效。

更多容易出错的题目:

关于变量提升和作用域相关的题目

3.为什么要使用模块化?都有哪几种方式,有什么特点?

前端模块化时将复杂的js文件分为多个独立的模块。用于重用和可维护。这样会引来模块之间相互依赖的问题。所以有了commonJS规范,AMD,CMD等规范,以及打包工具webpack,gulp等。

使用模块化可以给我们带来以下好处

  1. 解决命名冲突和作用域污染
  2. 提供可复用性
  3. 提高代码可维护性

在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污 染全局作用域的问题。

早期的模块化是AMD 和 CMD技术。但现在基本不用了。像以前的requireJS 是AMD方 法。Sea.js是CMD技术。

AMD,异步模块定义(Asynchronous Module Definition),它是依赖前置 (依赖必须一开始就写好)会先尽早地执行(依赖)模块 。换句话说,所有的require都被提前执行(require 可以是全局或局部 )。

AMD规范只定义了一个函数 define,它是全局变量。用法:

defind(id, dependencies, factory)

CMD(Common Module Definition)更贴近 CommonJS Modules/1.1 和 Node Modules 规范,一个模块就是一个文件;它推崇依赖就近,想什么时候 require就什么时候加载,实现了懒加载(延迟执行 ) ;它也没有全局 require, 每个API都简单纯粹 。

在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

define(factory);

define(function(require, exports, module) {
    // 模块代码
});

AMD与CMD的比较

  • AMD:依赖前置,预执行(异步加载:依赖先执行)。CMD:依赖就近,懒(延迟)执行(运行到需加载,根据顺序执行)
// CMD
define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething();
    // 省略1万行
    var b = require('./b') // 依赖可以就近书写
    b.doSomething();
})

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
    a.doSomething();
    // 省略1万行
    b.doSomething();
})

CommonJS 最早是 Node 在使用,目前也仍然广泛使用。基本语法:

module.exports = {} // 导出全部
exports.a = {} // 部分导出
var a = require(“./a.js”) // 引入

ES Module 是最新的原生实现的模块化方式。与 CommonJS 有以下几个区别:

CommonJS 支持动态路径导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案。

CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响。

CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化。

ES Module 会编译成 require/exports 来执行的。

// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function()

4.Proxy 可以实现什么功能

Vue3.0 通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式(双向绑定)。

let p = new Proxy(target, handler) //target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。

与defineProperty 区别:

  1. Proxy 无需递归为每个属性添加代理,一次即可完成所有父子元素的监听,性能更好
  2. 在vue中Object.defineProperty有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,缺陷是浏览器兼容性不好。

优势:

  • 可以劫持整个对象
  • 可以监听对象动态属性的添加
  • 可以监听到数组的变化,包括数组的索引和数组length属性
  • 可以监听删除属性
  • 操作时不是对原对象操作,new Proxy 返回⼀个新对象(它不直接操作对象,而是代理模式,通过对象的代理对象进行操作)。

5.map, filter, reduce 各自有什么作用?

map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的 数组中。map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组。

filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组。 作用:过滤出需要的元素。filter 的回调函数也接受三个参数,用处也相同。

reduce 可以将数组中的元素通过回调函数最终转换为一个值。如计算数组元素的和:

const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)

reduce 接受两个参数,分别是回调函数和初始值。

首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入

回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数

在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入

所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6

6.箭头函数和普通函数区别

语法不同:语法简洁,箭头函数省去了function关键字,采用箭头=>来定义函数。函数的参数放在=>前面的括号中,函数体跟在=>后的花括号中。

参数写法不同:

① 如果箭头函数没有参数,直接写一个空括号即可。
② 如果箭头函数的参数只有一个,也可以省去包裹参数的括号。
③ 如果箭头函数有多个参数,将参数依次用逗号(,)分隔,包裹在括号中即可。

函数体不同:

①如果箭头函数的函数体只有一句代码,就是简单返回某个变量或者返回一个简单的JS表达式,可以省去函数体的大括号{ }和return关键字。

let f = val => val;// 等同于let f = function (val) { return val };
let sum = (num1, num2) => num1 + num2;// 等同于let sum = function(num1, num2) {
return num1 + num2;};

②如果箭头函数的函数体只有一句代码,就是返回一个对象,可以像下面这样写:

// 用小括号包裹要返回的对象,不报错
let getTempItem = id => ({ id: id, name: "Temp" });

③如果箭头函数的函数体只有一条语句并且不需要返回值(最常见是调用一个函数),可以给这条语句前面加一个void关键字,代表无返回值。

let fn = () => void doesNotReturn();

箭头函数最常见的用处就是简化回调函数。

[1,2,3].map(x => x * x);// map箭头函数写法
var result = [2, 5, 1, 4, 3].sort((a, b) => a - b);// 排序箭头函数写法

箭头函数不会创建自己的this

没有自己的this,它只会从自己的作用域链的上一层继承this。它会捕获自己在定义时(注 意,是定义时,不是调用时)所处的外层执行环境的this,并继承这个this值。所以,箭头函数 中this的指向在它被定义的时候就已经确定了,之后永远不会改变。

所以在使用时,普通函数需要用到外部的this时要将this赋值给另一个变量传进去,而箭头 函数不需要。直接可以再函数内部使用外部的this。常见使用场景为vue的方法。

普通函数简单调用时this指向函数本身。注意是在调用时才会确定this指向。

普通函数作为对象的方法调用时,this指向它所属的对象。

var id = 'GLOBAL';var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'

箭头函数继承而来的this指向永远不变,永远是定义时外层环境this

.call()/.apply()/.bind()无法改变箭头函数中this的指向

.call()/.apply()/.bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this 定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向,虽 然这么做代码不会报错。

由于this指向不是自身,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new调用时会报错。

箭头函数没有自己的arguments,在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。可以在箭头函数中使用rest参数(三点运算符)代替arguments对象。

Var a = (...values)=> {
  console.log(values[0]) // 2
  console.log(values[1]) // 5
  console.log(values[2]) // 8
}


箭头函数不会创建this,不能作为构造函数,不能使用new关键字。另外箭头函数没有原型prototype