浏览器HTTP的缓存机制详解

2019
15/09

当我们的页面发起资源请求时,浏览器会通过缓存等手段来尽快响应,避免不必要的http消耗,所以我们经常见到:Memory Cache、Disk Cache、Push Cache,现在又多了一种ServiceWorker。我们来简单对比如下:

ServiceWorker

Service worker是一个注册在指定源和路径下的事件驱动worker。它采用JavaScript控制关联的页面或者网站,拦截并修改访问和资源请求,细粒度地缓存资源。Service Worker 可以使你的应用先访问本地缓存资源,包括js、css、png、json等多种静态资源。

当我们启用ServiceWorker时,浏览器就会显示我们的资源来自ServiceWorker。Service Worker的缓存有别与其他缓存机制,我们可以自由控制缓存、文件匹配、读取缓存,并且是持续性的。当ServiceWorker没有命中时才会去重新请求数据。

因为 Service Worker 中涉及到请求拦截,所以必须使用 HTTPS 传输协议来保障安全 它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

Service Worker 实现缓存功能的三个步骤:

    • 首先需要注册 Service Worker
    • 然后监听 install 事件后就可以缓存需要的文件
    • 最后在下次用户访问的时候,通过拦截请求的方式查询是否存在缓存,如果存在就直接读取缓存文件否则就去请求数据

特点:

  1. 独立于主JavaScript线程(这就意味着它的运行丝毫不会影响我们主进程的加载性能)
  2. 设计完全异步,大量使用Promise(因为通常Service Worker通常会等待响应后继续,Promise再合适不过了)
  3. 不能访问DOM,不能使用XHR和localStorage
  4. Service Worker只能由HTTPS承载(出于安全考虑)

详见:前端缓存之:Service Worker | 码农备忘录 (zuifengyun.com)

当 Service Worker 没有命中缓存的时候,我们需要去调用 fetch 函数获取数据。也就是说,如果我们没有在 Service Worker 命中缓存的话,会根据缓存查找优先级去查找数据。但是不管我们是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示我们是从 Service Worker 中获取的内容。

Memory Cache

Memory Cache为内存中的缓存,读取内存中的资源是非常快的。 是浏览器为了加快读取缓存速度而进行的自身的优化行为,不受开发者控制,也不受 HTTP 协议头的约束。内存读取虽然快且高效,但它是短暂的,当浏览器或者tab页面关闭,内存就会被释放了。而且内存占用过多会导致浏览器占用计算机过大内存。

  • 内存中的缓存:读取高效、持续时效短,会随着进程的释放而释放
  • 在缓存资源时并不关心返回资源的请求头 Cache-Control 的值是什么,同时资源的匹配也并非仅仅是对 URL 做判断,还可能会对 Content-Type、Cors 等其他特征做校验
  • 普通刷新 F5 操作,优先使用 Memory Cache

Disk Cache

Disk Cache 将资源存储在硬盘中,读取速度次于Memory Cache。

优点:可长期存储,可定义时效时间、容量大;缺点:读取速度慢。

根据http header 请求头触发的缓存策略来做缓存,包含强缓存和协商缓存。

  • 硬盘中的缓存:对比 Memory Cache 读取速度慢,但是容量大、缓存时效长
  • 它根据 HTTP Header 中的字段判断哪些资源需要被缓存、哪些资源可以不请求直接使用、哪些资源已经过期需要重新请求。即使在跨站点的情况下相同地址的资源一旦被硬盘缓存下来就不会再次去请求数据
  • 对于大文件来说大概率在硬盘中缓存,反之内存缓存优先;当前系统内存使用率高的情况下文件优先存储进硬盘

Push Cache

当以上三种缓存都没有命中时才会使用Push Cache。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂。这种缓存一般用不到。

我们通过控制台network,最直观的查看资源用的缓存方式。如图所示:

缓存过程

meta标签设置http-equiv

http-equiv属性可以被称作http响应头,可对整个页面进行缓存设置。

注意:设置meta标签只缓存页面内容,不会缓存页面引用的资源文件。

<!--设定网页缓存时效-->
<meta http-equiv="Cache-Control" content="max-age=7200" />
<!--设定网页的到期时间-->
<meta http-equiv="Expires" content="Mon, 20 Jul 2009 23:00:00 GMT" />

用法:一般用于静态HTML页面缓存。

缺点:浏览器支持情况不同。expires支持较好。Cache-Control不一定管用。

强缓存和协商缓存

浏览器的缓存分为强缓存协商缓存,当客户端请求某个资源的时候,获取缓存的流程如下:

  1. 浏览器第一次打开一个网页获取资源后,根据返回的header(响应头)信息来告诉如何缓存资源。响应头携带(Last-Modified)内容最后修改时间。
  2. 下次请求,先根据这个资源的http header(响应头)判断它是否命中强缓存cache-controlexpires信息),如果命中,则直接从本地缓存中获取资源(包括缓存header信息),未命中则向服务器请求资源。
  3. 当强缓存没有命中时,客户端会发送请求到服务器,服务器通过第一次请求返回的请求头字段信息If-Modified-SinceEtag/If-None-Match)验证这个资源是否命中协商缓存,这个过程成为http再验证,如果命中,服务器直接返回请求(返回新的响应头信息更新缓存中的对应header信息)而不返回资源,告诉客户端从缓存中获取,客户端收到返回后就直接从客户端缓存获取资源。协商缓存返回的状态码一般为304。
  4. 强缓存和协商缓存的共同之处在于:如果命中缓存,服务器不会返回资源;区别是:强缓存不发送请求到服务器(不与服务器通信),但是协商缓存会发送请求到服务器
  5. 当协商缓存没有命中时,服务器会返回资源给客户端。
  6. 当ctrl+F5强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存
  7. 当F5刷新页面时,跳过强缓存但会检查协商缓存。

浏览器第一次请求

浏览器后续请求

强制刷新ctrl + f5 ,请求头header带有Cache-control:no-cache(为了兼容,还带了 Pragma: no-cache),同时不带有if-not-match / If-Modified-Since,所以请求服务器协商缓存会被当做失效,返回200和最新内容。

强缓存相关header字段

强缓存可以通过设置两种响应头实现:Expires 和 Cache-Control。

  1. Expires策略:数据缓存到期时间,在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。Expires设置失效时间,精确到时分秒。不过Expires 是HTTP 1.0的东西,现在浏览器均默认使用HTTP 1.1,所以它的作用基本忽略。
  2. Cache-control策略(重点):Cache-Control与Expires的作用一致,都是指明当前资源的有效期,控制浏览器是否直接从浏览器缓存取数据还是重新发请求到服务器取数据。同时设置的话,其优先级高于Expires。可以指定一个 max-age 字段,表示缓存的内容将在一定时间后失效。比如:Cache-Control:max-age=300 时,则代表在这个请求正确返回的5分钟内再次加载资源,就会命中强缓存。

http协议头Cache-Control

值可以是public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age,各个消息中的指令含义如下:

  1. Public指示响应可被任何缓存区缓存。
  2. Private指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当前用户的部分响应消息,此响应消息对于其他用户的请求无效。
  3. no-cache指示请求或响应消息不能缓存
  4. no-store用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存。
  5. max-age指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应。
  6. min-fresh指示客户机可以接收响应时间小于当前时间加上指定时间的响应。
  7. max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。

协商缓存相关的header字段

协商缓存可以通过设置两种响应头实现:Last-Modified 和 ETag 。

Last-Modifued/If-Modified-Since和Etag/If-None-Match这两组搭档都是成对出现的,即第一次请求的响应头带上某个字段(Last-Modifued或者Etag),则后续请求会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modifued或者Etag字段,则请求头也不会有对应字段。

Last-Modified/If-Modified-Since要配合Cache-Control使用。

  1. Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。
  2. If-Modified-Since:当资源过期时(浏览器判断Cache-Control标识的max-age过期),发现响应头具有Last-Modified声明,则再次像服务器请求时带上头if-modified-since,表示请求时间。服务器收到请求后发现有if-modified-since则与被请求资源的最后修改时间进行对比(Last-Modified),若最后修改时间较新(大),说明资源又被改过,则返回最新资源,HTTP 200 OK;若最后修改时间较旧(小),说明资源无新修改,响应HTTP 304 走缓存。

Etag/If-None-Match也要配合Cache-Control使用

  1. Etag:服务器响应时,告诉浏览器当前资源在服务器的唯一标识(生成规则由服务器决定)。Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。
  2. If-None-Match:当资源过期时,浏览器发现响应头里有Etag,则再次像服务器请求时带上请求头if-none-match(值是Etag的值)。服务器收到请求进行比对,决定返回200或304
    为什么既有Last-Modified还有Etag(两者为什么并存,有什么好处)

使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:

  1. 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
  2. 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
  3. 某些服务器不能精确的得到文件的最后修改时间。

这时,利用Etag能够更加准确的控制缓存,因为Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符。Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。

协商缓存生效,返回304和Not Modified:

协商缓存失效,返回200和请求结果:

用户的行为对缓存的影响

强缓存

  • 不会向服务器发送请求,直接从缓存中读取资源,可以设置两种 HTTP Header 来实现:Expires、Cache-Control

Expires

  • 缓存过期时间用来指定资源的过期时间,是服务器端的具体的时间点,结合 last-modified 来使用
  • 是 HTTP/1 的产物,受限于本地时间,如果本地时间修改可能会造成缓存失效

Cache-Control

  • 可以在请求头或响应头中来设置,多个指令配合使用达到多个目的
  • 是 HTTP/1.1 的产物,如果和 Expires同时存在,那么它的优先级要高,所以 Expires 的存在成为了一种兼容性的写法

协商缓存

  • 强缓存的依据来自于缓存是否过期,而不关心服务端文件是否已经更新,这可能会导致加载的文件不是服务端最新的内容,此时我们就需要用到协商缓存
  • 协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发送请求,由服务器根据缓存标识决定是否使用缓存的过程,主要分两种情况
    • 协商缓存生效返回 304 和 Not Modified
    • 协商缓存失效返回 200 和请求的结果
  • 协商缓存可以通过设置两种 HTTP Header 来实现:Last-Modified 和 ETag

Last-Modified

  • 浏览器在第一次返回资源时,在响应头中添加 Last-Modified 的 header,值是这个资源在服务器上的最后的修改时间浏览器接收后缓存文件和 header
  • 浏览器下一次请求这个资源,浏览器检测到有 Last-Modified 这个 header,于是添加 If-Modified-Since 这个 header,值就是 Last-Modified 中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比,如果没有变化,返回 304 和空的响应体,直接从缓存读取,如果 If-Modified-Since 的时间小于服务器中这个资源的最后修改时间,说明文件有更新,于是返回新的资源文件和 200
  • Last-Modified 的弊端:
    • 如果本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改,服务端不能命中缓存导致发送相同的资源
    • 因为 Last-Modified 只能以秒计时,如果在不可感知的时间内修改完成文件,那么服务端会认为资源还是命中了,不会返回正确的资源

既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?所以在 HTTP / 1.1 出现了 ETag 和If-None-Match

ETag

  • ETag 是服务器响应请求时,返回当前资源文件的一个唯一标识,只要资源有变化 ETag 就会重新生成
  • 浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到 request header 里的 If-None-Match 里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现 ETag 匹配不上,那么直接以常规 GET 200 回包形式将新的资源(当然也包括了新的 ETag)发给客户端;如果 ETag 是一致的,则直接返回 304 知会客户端直接使用本地缓存即可。
  • Last-Modified 和 ETag 的区别:
    • 精度上 ETag 优于 Last-Modified
    • 性能上 ETag 逊于 Last-Modified
    • 优先级上服务器优先考虑 ETag

缓存机制

  • 强制缓存优先于协商缓存进行,若强制缓存生效则直接使用缓存,若不生效则进行协商缓存,协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回 200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回 304,继续使用缓存。
  • 如果什么缓存策略都没设置,那么浏览器会怎么处理?对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。
  • 浏览器缓存分为强缓存和协商缓存,两者有两个比较明显的区别:
    • 如果浏览器命中强缓存,则不需要给服务器发请求;而协商缓存最终由服务器来决定是否使用缓存,即客户端与服务器之间存在一次通信。
      在 chrome 中强缓存(虽然没有发出真实的 http 请求)的请求状态码返回是 200 (from cache);
    • 而协商缓存如果命中走缓存的话,请求的状态码是 304 (not modified)。 不同浏览器的策略不同,在 Fire Fox中,from cache 状态码是 304.

实际应用场景

  • 频繁变动的资源:Cache-Control: no-cache
  • 不常变化的资源:Cache-Control: max-age=31536000,一年的有效期,比如 jQuery 类库基本不换化

用户行为对浏览器缓存的影响

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。

前端算法: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)), [])
};

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实现。

细说后端模板渲染、客户端渲染、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

纯CSS+HTML制作三角形和圆|实心+空心

2019
11/05

我们的网页因为 CSS 而呈现千变万化的风格。这一看似简单的样式语言在使用中非常灵活,只要你发挥创意就能实现很多比人想象不到的效果。特别是随着 CSS3 的广泛使用,更多新奇的 CSS 作品涌现出来。

本文是流行的三角形和圆的绘制方法。

<style type="text/css">
    .clearfix{zoom:1;} /* clearfix for IE */
    .clearfix:after{content:"";display:block;clear:both;visibility:hidden;}
    .triangle-solid>div,.triangle-hollow>div{float: left;margin-right: 50px;}
    .top{
        position: relative;
        width: 0;
        height: 0;
        border-left: 50px solid transparent;
        border-right: 50px solid transparent;
        border-bottom: 50px solid #000;
    }
    .bottom{
        position: relative;
        width: 0;
        height: 0;
        border-left: 50px solid transparent;
        border-right: 50px solid transparent;
        border-top: 50px solid #000;
    }
    .left{
        position: relative;
        width: 0;
        height: 0;
        border-top: 25px solid transparent;
        border-right: 25px solid #000;
        border-bottom: 25px solid transparent;
    }
    .right{
        position: relative;
        width: 0;
        height: 0;
        border-top: 25px solid transparent;
        border-bottom: 25px solid transparent;
        border-left: 25px solid #000;
    }
    .triangle-hollow>.top:after{
        content: '';
        position: absolute;
        border-left: 50px solid transparent;
        border-right: 50px solid transparent;
        border-bottom: 50px solid #fff;
        left: -50px;
        top: 5px;
    }
    .triangle-hollow>.bottom:after{
        content: '';
        position: absolute;
        border-left: 50px solid transparent;
        border-right: 50px solid transparent;
        border-top: 50px solid #fff;
        left: -50px;
        top: -55px;
    }
    .triangle-hollow>.left:after{
        content: '';
        position: absolute;
        border-top: 25px solid transparent;
        border-right: 25px solid #fff;
        border-bottom: 25px solid transparent;
        left: 5px;
        top: -25px;
    }
    .triangle-hollow>.right:after{
        content: '';
        position: absolute;
        border-top: 25px solid transparent;
        border-bottom: 25px solid transparent;
        border-left: 25px solid #fff;
        left: -30px;
        top: -25px;
    }
    .round{
        width: 50px;
        height: 50px;
        background: #000;
        border-radius: 50%;
        -moz-border-radius: 50%;
        -webkit-border-radius: 50%;
    }
    .round-hollow{
        position: relative;
    }
    .round-hollow:after{
        position: absolute;
        content: '';
        width: 46px;
        height: 46px;
        background: #fff;
        border-radius: 50%;
        -moz-border-radius: 50%;
        -webkit-border-radius: 50%;
        top:2px;
        left:2px;
    }
</style>
<body>
    <h2>实心三角</h2>
    <div class="triangle-solid clearfix">
        <div class="top"></div>
        <div class="bottom"></div>
        <div class="left"></div>
        <div class="right"></div>
    </div>
    <h2>空心三角</h2>
    <div class="triangle-hollow clearfix">
        <div class="top"></div>
        <div class="bottom"></div>
        <div class="left"></div>
        <div class="right"></div>
    </div>
    <h2>实心圆</h2>
    <div class="round"></div>
    <h2>空心圆</h2>
    <div class="round round-hollow"></div>
</body>

纯CSS+HTML制作饼图和条形图

2019
11/05

使用html+css制作的图案或图形化内容响应速度最快,如下贴出我在开发过程中使用CSS+HTML制作的饼图和条形图,有需要的可以拿去。

CSS制作饼图

<div class="pie" style="animation-delay: -0.05s;"><span>5%</span></div>
<style type="text/css">
    @keyframes spin{
        to{transform: rotate(.5turn);}
    }
    @keyframes bg{
        50% {background: blue;}
    }
    .pie{
        position: relative;
        width: 200px; 
        height: 200px; 
        border-radius: 50%; 
        background: red;
        background-image:linear-gradient(to right, transparent 50%, blue 0);
    }
    .pie>span{
        position: absolute;
        line-height: 200px;
        width: 100%;
        color:#fff;
        text-align: center;
        font-size: 25px;
    }
    .pie::before{
        position: absolute;
        content: '';
        display: block;
        height: 100%;
        width: 50%;
        left: 50%;
        top: 0;
        border-radius: 0 100% 100% 0/50%;
        background-color: inherit;
        transform-origin: left;
        animation: spin .5s linear infinite,bg 1s step-end infinite;
        animation-play-state: paused;
        animation-delay: inherit;
    }
</style>

CSS制作条形图

<div class="barChart">
    <div class="x">
        <span>张三</span>
        <span>李四</span>
    </div>
    <div class="y">
        <div><span>0</span></div>
        <div><span>10</span></div>
        <div><span>20</span></div>
        <div><span>30</span></div>
        <div><span>40</span></div>
        <div><span>50</span></div>
    </div>
    <div class="bars">
        <div class="bar" style="height:150px;"><span>50</span></div>
        <div class="bar" style="height:90px;"><span>30</span></div>
    </div>
</div>
<style type="text/css">
    .barChart{
        position: relative;
        width: 80%;
        height: 400px;
    }
    .barChart>.x{
        bottom: 0;
        position: absolute;
        width: 100%;
        display: flex;
        flex-direction: row;
        align-items:flex-end;
        justify-content: space-around;
    }
    .barChart>.x>span{
        transform-origin:left top; 
        transform: rotate(45deg);
        width: 0;
        white-space: nowrap;
    }
    .barChart>.y{
        position: absolute;
        bottom: 30px;
        width: 100%;
        display: flex;
        flex-direction: column-reverse;
        align-items: flex-start;
        justify-content: flex-start;
    }
    .barChart>.y>div{
        border-bottom: 1px solid #00af16;
        width: 100%;
        height: 29px;
    }
    .barChart>.y>div>span{
        display: inline-block;
        transform: translateY(20px);
        margin-left: -40px;
        width: 30px;
        background: white;
        text-align: right;
    }
    .bars{
        position: absolute;
        bottom: 31px;
        width: 100%;
        display: flex;
        flex-direction: row;
        align-items: flex-end;
        justify-content: space-around;
    }
    .bars>.bar{
        width: 5%;
        background-image: linear-gradient(to right, #b8f123 0%, #b8f123 47%, #79a602 50%, #79a602 100%);
        display: inline-block;
    }
    .bars>.bar>span{
        display: block;
        text-align: center;
        margin-top: -22px;
    }
</style>

js知识总结—理论知识篇(二)

2019
15/03

1.js事件循环机制

js是一门单线程语言,但却能优雅地处理异步程序,在于js的事件循环机制。

浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等
其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 、构造 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程。

js执行线程:

  1. 主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。
  2. 工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求、定时器等异步事件。

任务队列( Event Queue )

所有的任务可以分为同步任务和异步任务,同步任务一般会直接进入到主线程中立即执行;而异步任务会通过任务队列的机制(先进先出的机制)来进行协调。如图:

事件循环:主线程内的任务先执行完毕,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。

宏任务和微任务:任务包含两种,宏任务(Macro Task)和微任务(Micro Task)。微任务要优先于宏任务执行。故主线程任务执行完毕后会先读取任务队列中的微任务,将微任务按照先进先出的原则全部执行且清空后,再去读取任务队列中的宏任务。。。若没有宏任务,则进行下一次事件循环(Next Tick)。在事件循环中,每进行一次循环操作称为tick。(如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。)

宏任务主要包含:script( 整体代码)、setTimeoutsetInterval、I/O、UI 交互事件、setImmediate(IE、Node.js )、requestAnimationFrame

宏任务的优先级: 主代码块 > setImmediate > MessageChannel > requestAnimationFrame > setTimeout / setInterval

微任务主要包含PromiseMutaionObserver(IOS)、process.nextTick(Node.js )

微任务的优先级: process.nextTick > Promise > MutationObserver

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

//输出的顺序是:script start, script end, promise1, promise2, setTimeout

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

console.log('script start');

setTimeout(function() {
  console.log('timeout1');
}, 10);

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})

console.log('script end');

//注意new Promise并不是异步,只是一个实例化对象,期回调先被执行。
//setTimeout遵循先进先出执行
//输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2

2.setTimeout(fn,100)中的100毫秒时如何权衡的

当我们执行到setTimeout定时器代码时候,会把代码放到一个定时触发器线程,然后开始计时。计时结束后,会将定时的回调函数插入任务队列。必须等到主线程中的代码及先进入队列的任务执行完,主线程才会去执行这个回调。所以有可能要等很久,所以没办法保障回调函数一定会来100毫秒后执行。

这里注意,并不是等主线程执行完毕才开始计时,而是执行到定时器代码块时候就开始计时了。影响回调函数执行时间的是主线程的执行时间。

定时触发器线程

浏览器定时计数器并不是由JS引擎计数的,,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确。因此通过单独线程来计时并触发定时是更为合理的方案。

3.js基本数据类型和引用数据类型的区别

  1. js基本数据类型有:Number、String 、Boolean、Null和Undefined;引用类型有:Object 、Array 、Function。
  2. 基本数据类型的值是不可变的,任何方法都无法改变一个基本类型的值,当这个变量重新赋值之后看起来变量的值是变了,但这里实际只是变量名指向变量的一个指针变了(改变的是指针的指向)。该变量是不会变的。引用类型时可以被改变的。一旦重新赋值,其值则被改变。
  3. 基本数据类型不可以添加属性和方法,但是引用类型可以。
  4. 基本数据类型的赋值都是简单赋值,如果从一个变量向另一个变量赋值基本类型的值,会在变量对象上创建一个新的值。然后把该值复制到为新变量分配的内存位置上。而引用类型的赋值是对象的引用。
  5. 基本数据类型的比较是值的比较,引用类型的比较是引用的比较。引用数据了类型比较的是内存地址是否相同。
  6. 基本数据类型是存放在栈区的,引用数据类型会同时保存在栈区和堆区。栈区保存的是一个地址。

4.一个单机网页游戏老是卡顿或崩溃,原因可能是什么

  1. 内存溢出问题:及时清理内存,将无用对象回收(垃圾回收机制)
  2. 资源文件过大:选择较小的图片或压缩图片
  3. 资源加载过慢:预加载资源,在游戏初始化之前就加载资源
  4. 动画绘制频率问题:每一帧的绘制间隔时间与显示器刷新频率保持一致。
  5. 单机游戏与网络无关。如果是网络游戏,ping值过高,请确认自己网络是否延迟过高。

5.函数式编程思想

定义:函数式编程(FP)是一种编程范式(其针对与面向对象编程范式OOP,函数式与面向对象冲突)。函数式编程是把运算过程尽量写成一系列嵌套的函数调用。 这里的函数式指的是数学函数,而非编程语言中的函数。

纯函数:函数式编程允许我们编写的函数是纯函数。纯函数的特点:

  1. 如果给定相同的参数,则返回相同的结果(也称为确定性)。可以理解为返回结果不因除参数外的任何变量的改变而改变。
  2. 它不会引起任何副作用。比如修改全局对象或修改通过引用传递的参数。

函数式编程的特点:

  1. 数据和行为分离(OOP是把数据和行为打包)
  2. 不可修改性(immutability。OOP以及面向过程编程很多时候都会对内存状态进行修改,比如给变量或成员重新赋值、往一个集合里添加/删除数据等,FP不允许这么干)
  3. 一等公民是函数(函数可以赋值给变量,可以当参数传递,函数的返回值可以是函数。OOP里的一等公民只有对象)
  4. 闭包

函数式编程的优势:

  1. 并行:纯函数无需任何修改即可并行执行。
  2. 无副作用:不会修改全局变量和引用变量,不会干扰其他程序。
  3. 更容易测试:函数式程序的 bug 不依赖于与任何与其无关的代码,你遇到的问题就总是可以再现。我们可以使用不同的上下文对纯函数进行单元测试。
  4. 引用透明性:如果一个函数对于相同的输入始终产生相同的结果,那么它可以看作透明的。
  5. 不可变性:只要参数不变,返回值永远不变
  6. 高内聚低耦合:函数是一阶公民。函数可以作为参数或返回值。我们可以组合不同的函数来创建具有新行为的新函数。

6.JS函数柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

Currying有哪些好处

① 提高复用率

// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

② 提前确认

var on = function(element, event, handler) {
    if (document.addEventListener) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    }
}

// 提前确认调用哪个方法,不用每次调用都判断
var on = (function() {
    if (document.addEventListener) {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        };
    } else {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        };
    }
})();

③ 延迟运行

Function.prototype.bind = function (context) {
    var _this = this,args = Array.from(arguments).slice(1)
    return function() {
        return _this.apply(context, args)
    }
}

7.立即执行函数的作用

作用:

  1. 不必为函数命名,避免了污染全局变量
  2. 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
  3. 立即执行函数执行完后就立刻被销毁,避免常驻内存。(所以命名的立即执行函数外部无法访问,实际情况是已被销毁)
(function foo() {
    console.log(123);
})();
//已被销毁,所以访问不到。所以一般用匿名函数
foo();  // ReferenceError: foo is not defined

// 要访问自执行函数的执行结果
var num = (function (a, b) {
    return a + b;
})(1, 2);

//其实这种写法不用括号把函数包起来也是可以的
var num = function (a, b) {
    return a + b;
}(1, 2);
//在执行赋值操作的时候,上面的代码已经变成了一个表达式,而 JavaScript 解释器认为 函数表达式 是可以直接被执行的。

// 如果要再次访问自执行函数,需要将其作为返回值
// 这时就不能用匿名函数了
var num = (function aaa(a, b) {
    console.log(a+b)
    return aaa
})(1, 2);
num(3,4)
num(3,4)(5,6)

// 利用arguments.callee方法返回当前函数用于访问自执行函数
// 这样免得给匿名函数命名
// 但arguments相关的方法在严格模式下会报错
var num = (function(a, b) {
    console.log(a+b)
    return arguments.callee
})(1, 2);

使用场景:

  1. 立即执行函数执行完后就立刻被销毁,所以如果函数只被执行一次的时候就可以将其换成立即执行函数。
  2. 创建独立作用域,避免变量干扰。
  3. 用于初始化方法(数据初始化)。可避免内部变量污染全局。
  4. 可以用来封装一些插件、轮子、方法。

ES6中,块级作用域已经可以取代立即执行函数。可以将需要立即执行的代码放到花括号{}中。

8.定时器的最小间隔(延迟)时间

HTML5标准规定

  • setTimeout的最短时间间隔是4毫秒;
  • setInterval的最短间隔时间是10毫秒,也就是说,小于10毫秒的时间间隔会被调整到10毫秒