胖蔡叨叨叨
你听我说

前端面试-输入URL到页面加载的全过程(二)

胖蔡阅读(90)

上篇文章已经介绍了,在URL加载时数据请求过程发生的一些数据的交互以及涉及面试中可能存在的面试点。本篇继续介绍“输入URL到页面加载的全过程”中数据渲染部分的内容。本篇文章也可以说是对于浏览器渲染流程的一个简易化介绍。

数据渲染

当数据请求返回服务端的相应结果(html文件)后,浏览器会根据返回的Html文件进行页面的渲染,从拿到数据到最终显示到页面上,浏览器也是做了大量的工作的,大致原理可参考下图。

4ffce04d92a4d6c-5

  • 浏览器解析HTML,构建DOM数
  • 解析CSS,构建Style Tree规则树
  • 通过DOM Tree 和 style Rules构建 Render Tree
  • 通过Render Tree进行界面节点计算布局
  • 绘制界面

大致的流程就像上面的图示和列出的渲染流程一样,接下来我们通过下面的几个问题从侧面来了解网页的渲染过程。

 

1、什么是进程,什么是线程?它们的区别又是什么?
套用较为官方的一个定义来先解释下进程和线程的定义。
进程:进程是指在系统中正在运行的一个应用程序,程序一旦运行就是进程。进程是系统进行资源分配的独立实体, 且每个进程拥有独立的地址空间。一个进程可以拥有多个线程,每个线程使用其所属进程的栈空间。进程是系统进行资源分配和调度的一个独立单位。

线程:线程是进程的一个实体,是进程的一条执行路径。线程是CPU独立运行和独立调度的基本单位。

线程和进程的区别主要体现在如下几个方面:

  • 进程的地址空间和资源分配相互独立,而同一进程的各线程间共享。
  • 进程间通信需要通过IPC(无名管道、有名管道、信号机制、信号灯、共享内存、消息队列、套接字socket),线程间可以通过全局变量,信号量,互斥锁就可以实现通信。
  • 线程上下文切换比进程上下文切换快得多。所以较为频繁切换上下文的可以使用线程实现。
  • 一个进程内有多个线程

2、Chrome访问一个网页启动了多少个进程?

  • 浏览器进程:负责控制浏览器除标签页外的界面,包括地址栏、书签、前进后退按钮等,以及负责与其他进程的协调工作,同时提供存储功能。
  • GPU进程:负责整个浏览器界面的渲染。Chrome刚开始发布的时候是没有GPU进程的,而使用GPU的初衷是为了实现3D CSS效果,只是后面网页、Chrome的UI界面都用GPU来绘制,这使GPU成为浏览器普遍的需求,最后Chrome在多进程架构上也引入了GPU进程。
  • 网络进程:负责发起和接受网络请求,以前是作为模块运行在浏览器进程里面的,后面才独立出来,成为一个单独的进程。
  • 插件进程:主要是负责插件的运行,因为插件可能崩溃,所以需要通过插件进程来隔离,以保证插件崩溃也不会对浏览器和页面造成影响。
  • 渲染进程:负责控制显示tab标签页内的所有内容,核心任务是将HTML、CSS、JS转为用户可以与之交互的网页,排版引擎 Blink 和 JS 引擎 V8 都是运行在该进程中,默认情况下Chrome会为每个Tab标签页创建一个渲染进程。

如上,这是浏览器中我们常见的进程。其中浏览器进程,GPU进程,网络进程是共享的,同一个站点共用一个渲染进程。其中最后网页的渲染就是发送给GPU实现页面的渲染的。

3、渲染进程中包含哪些线程?

  • GUI渲染线程:GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被冻结了。
  • JavaScript 引擎线程:这就是我们常说的js单线程,主要是为了实现JS文件的解析和运行,包括执行用户的交互操作、dom树修改、css样式更新等操作。需要注意的是若js执行过程会导致GUI渲染线程阻塞,它们不可同步执行。
  • 定时触发器线程:浏览器定时计数器(setTimeout、setInterval )并不是由JS引擎计数的, 因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
  • 异步Http请求线程:在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理。
  • 事件处理线程:事件处理线程管理着任务队列,异步任务的回调函数会放在其中,等 执行栈中的任务执行完毕后,读取其中任务执行。如鼠标点击、滑动、异步http请求等的回调处理。

4、如何构建渲染树?

4ffce04d92a4d6c-6

当我们生成 Dom Tree 和 CSSOM(style Rules又称样式规则),就需要将它两合并起来。合并过程需要注意几点:

  • render Tree 只包含可见的节点,如”display:none;”则不会在render Tree中显示
  • 从解析构建到生成Render Tree这个过程:构建DOM、构建CSS rules、构建Render Tree、布局。这几个过程并不是严格按照顺序执行的。渲染引擎会以最快的速度展示内容,所以第二阶段不会等到第一阶段结束才开始,而是在第一阶段有输出的时候就开始执行。其它阶段也是如此。由于浏览器会尝试尽快展示内容,所以内容有时会在样式还没有加载的时候展示出来。 这就是经常发生的FOCU(flash of unstyled content)或白屏问题。

5、如何理解回流与重绘?

先来简单的了解下有关回流与重绘的含义。
回流(reflow):我们前面的问题已经知道了当我们将DOM Tree和CSSOm(style Rules)结合生成Render Tree渲染树后,需要我们通过新的Render Tree计算对应节点的位置与大小实现布局。这个计算过程就叫回流。
重绘(repaint):通过上述回流阶段的计算实现布局并最终将对应的节点转换成屏幕上的实际像素,这个阶段就叫做重绘节点。也就是在屏幕上绘制图形,但是如背景颜色发生改变,那么会引起重绘,但是不会引发回流。

如下情况发生回流:

  • 页面首次加载
  • 浏览器尺寸变化
  • Dom结构改变,添加或者删除dom元素或者内部文本发生改变
  • 改变节点元素的几何信息:margin、padding、width、height、display:none、border、fontSize
  • 激活CSS伪类,如hover、focus等
  • 获取某些属性的值,浏览器会为了获得准确的值也会触发回流的过程
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()getComputedStyle()getBoundingClientRect()scrollTo()IE的 currentStyle

注意:当页面中元素样式的改变并不影响它在文档流中的位置时,如color、background-color、visibility等,浏览器会将新样式赋予给元素并重新绘制它,这个过程重绘而不回流。

如何减少回流和重绘:

  • 使得节点脱离文档流
  • 使用 transform 替代 top
  • 使用 visibility 替换 display: none ,因为前者只会引起重绘,后者会引发回流(改变了布局)
  • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  • 不要把节点的属性值放在一个循环里当成循环里的变量。
  • 动画实现的速度的选择,动画速度越快,回流次数越多,也可以选择使用 requestAnimationFrame
  • CSS 选择符从右往左匹配查找,避免节点层级过多
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点。比如对于 video 标签来说,浏览器会自动将该节点变为图层。
  • 批量修改样式
  • 减少对于某些属性的值(offsetWidth等),获取后可对其进行存储,避免多次频繁获取使用

6、async和defer是什么?为什么需要使用到它们?

4ffce04d92a4d6c-7

上图展示了三种不同script加载声明下,html与script解析加载顺序情况。其中绿线代表HTML解析、蓝线代表js下载、红线代表js执行。首先来介绍下dom解析、css解析、js解析运行在html加载中的执行顺序问题。

  1. 后端返回html文档
  2. html开始进行dom解析,从html标签开始进行解析
  3. 浏览器发现link标签,发出CSS文件的请求,服务器返回这个CSS文件
  4. 浏览器发现script标签(无 async 和defer),向服务器发起请求返回js文件,并运行js代码(这时候DOM停止解析),js包含对css的操作,这时候html暂停dom解析,开始下载所有的css文件,然后解析cssom,
  5. 执行完js文件后继续dom解析
  6. 合并dom和cssom生成render tree
  7. 布局绘制界面

结合图示发现,常规的script标签不仅仅会影响dom的解析也会影响cssom的解析【cssom原本和dom解析应是同步进行的】,那么如何防止js打乱html的解析过程呢,如下:

  • 将js放置在body底部
  • 使用defer或aysnc减少js对html解析的感染,defer目前应该是最贴近我们的要求的

总结上面的可以发现:defer模式文档的解析不会停止,其他线程将下载脚本,待到文档解析完成,脚本才会执行。async模式文档的解析不会停止,其他线程将下载脚本,脚本下载完成后开始执行脚本,脚本执行的过程中文档将停止解析,直到脚本执行完毕。

如下有几个关于defer和aysnc的特点需要了解:

  • defer和aysnc只适合外联脚本
  • 多个defer脚本,浏览器会根据顺序下载和执行,但是多个async,它的下载和执行都是异步无序的。
  • defer脚本会在DOMContentLoaded和load事件之前执行
  • async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序

(更多…)

前端面试-输入URL到页面加载的全过程(一)

胖蔡阅读(97)

感兴趣的欢迎关注公众号“胖蔡话前端”,了解更多前端面试题。可扫描右侧二维码关注。

当面试官问我们:“输入一个URL到前端页面加载整个过程发生了什么”?我们该怎么去回答这个问题呢?

这个问题算是一个比较高频出现的前端面试题,因为问题本身涉及的知识点比较多,而且对于不同面试官希望听到的答案可能也不是完全相同。就我之前公司的面试经历而言,我【前端开发】可能比较关心候选人对于页面的加载过程的了解以及页面渲染的生命周期和加载顺序,而我们的架构师比较关心的则是这个过程中出现的数据交换过程的了解。那么,URL加载的全过程究竟做了哪些工作呢?还请大家继续看下去。

这个问题其实我们可以分为两个部分去了解即:数据请求和数据渲染。

数据请求

数据请求阶段主要是实现了客服端请求获取服务端数据的功能,具体流程步骤如下:

  • DNS解析
  • 建立连接
  • 发起请求
  • 返回数据

     

当然,上面是比较常规的步骤,要是考虑缓存因素的话,在DNS解析前还有一个本地缓存获取,在返回数据前也有一个服务器缓存获取【协商缓存】。本篇暂不考虑缓存情况。就上述过程来,我们来简单谈谈可能涉及哪些知识点,在这之前,先上一个网络层的模型供大家观摩观摩,问题主要集中在这块内容里。

4ffce04d92a4d6c

带着网络模块图我们来考虑考虑会有哪些问题需要我们去了解。

1、 简单介绍下DNS解析的过程

4ffce04d92a4d6c-1

 

整个过程大致如上图,客户机会通过访问本地的dns服务器然后逐级向上查询找到目标机器并返回给客户机,本地dns也会堆积进行记录。

2、URL由哪几部分组成?

这个不太常问,但需要了解。完整的URL组成如下

4ffce04d92a4d6c-4

  • 协议名
  • 域名
  • 端口
  • 路径
  • 查询条件
  • 锚点

     

3、简单介绍下建立连接的过程

或者是问“TCP三次握手的过程是什么?什么是TCP的四次回收?”,这个问题的核心就是需要对于TCP的三次握手和四次挥手过程熟练掌握。

4ffce04d92a4d6c-2

TCP三次握手

①第一次握手,客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN©。此时客户端处于 SYN_SEND 状态。

首部的同步位SYN=1,初始序号seq=x,SYN=1的报文段不能携带数据,但要消耗掉一个序号。

②第二次握手,服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN(s)。同时会把客户端的 ISN + 1 作为ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_RCVD 的状态。

在确认报文段中SYN=1,ACK=1,确认号ack=x+1,初始序号seq=y

③客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接。

4ffce04d92a4d6c

TCP四次挥手

由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭。这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了。但是在这个TCP连接上仍然能够发送数据,直到另一方向也发送了FIN。

①第一次挥手:客户端发送FIN报文,并指定seq=u(u之前的数据已经全部发送,并且数据发到u就可以截止了,就不再有数据了),用来关闭客户端到服务端的数据传送。客户端进入FIN_WAIT_1状态。

②第二次挥手:服务端收到FIN报文,发送seq=v和确认序号ack=u+1给客户端,服务端进入CLOSE_WAIT状态。

③第三次挥手:服务端发送FIN报文,seq=w和确认序号ack=u+1,用于关闭服务端到客户端的数据传输。服务端状态变成LAST_ACK状态。

④第四次挥手:客户端收到FIN报文,客户端进入TIME_WAIT状态,然后发送一个ACK给服务端,确认序号ack=w+1,服务端进入CLOSED状态。

4、http和https的区别,简单介绍下HTTPS的流程

这个在问到Http协议的时候可能会问到,一般会问“除了http协议你还知道其他哪些协议?”,大多数人都会说到https协议,这时候问题就来了,当然有时候即使你不问,面试官也会主动问你熟不熟悉https协议。这里简单介绍下https的加密过程。

4ffce04d92a4d6c-3

 

过程大致如图所示,一般不需要回答的特别细节,只需要知道几个重点知识就行:

  • https使用的是ssl证书进行加密
  • https加密方式为对称加密
  • ssl证书是由三方机构颁发的

如果问得比较深的话,或许还会问到其他加密算法,这个就得靠平时的积累了。

简单了解Vue3中的proxy

胖蔡阅读(112)

Vue3使用的proxy是什么?

众做周知Vue2的双向绑定原理是利用了ES5的一个APIObject.defineProperty()对数据进行劫持,结合发布订阅模式的方式来实现的。 而Vue3 中则是使用了ES6的Proxy 对数据代理。

Vue2 的Object.defineProperty()

  1. 定义: Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。 例子:
	const object1 = {};
	Object.defineProperty(object1, 'property1', {
	  value: 42,
	  writable: false
	});

	object1.property1 = 77;
	console.log(object1.property1);  //打印出来的一人是42
  1. 语法:
	Object.defineProperty(obj, prop, descriptor)
	// obj 要定义属性的对象
	// 要定义或修改的属性的名称或 Symbol
	//descriptor 要定义或修改的属性描述符

返回值:被传递给函数的对象。

  1. 如何实现响应式的? 通过defineProperty 两个属性,get及set
  • get 属性的getter函数,当访问改属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承管理,this不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。
  • set 属性的setter函数,当属性值被修改时,会调用此函数。改方法接受一个参数(也就是被赋的新值),会传入赋值时的this对象。默认undefined.

简单的例子: 监听person 的 namep变化

	let person ={}
	let name = '张小张'
	Object.defineProperty(person,'namep',{
		// 默认是不可枚举的(for in打印不出来) 可以使用:enumerable:true
		// 默认不可修改,可:wirtable:true
		//默认不可删除 可:configurable:true
		get(){
			console.log('触发了get的方法');
			return name
		},
		set(val){
			console.log('触发了set的方法');
			name = val
		}
	})
	// 读取对象的属性
	console.log(person.namep);   // 触发了get的方法  张小张
	
	// 修改name,再次读取
	name = '王小王'
	console.log(person.namep);   // 触发了get的方法  王小王
	
	// 修改对象
	person.namep = '李小里'
	console.log(person.namep);   // 触发了set的方法 触发了get的方法 李小里

如果监听多个属性的变化,这里就要用到Object.keys(obj)进行遍历等,以及深度监听时又要借助递归来实现。 而在监听数组方面

	let arr = [1,2,3,4,5]
	let obj = {}
	
	Object.defineProperty(obj,'arr',{
		get(){
			console.log('get');
			return arr
		},
		set(val){
			console.log('set',val);
			arr = val
		}
	})
	
	console.log(obj.arr); // get  [1,2,3,4,5]
	obj.arr = [2,3,4]   // set [2,3,4] 
	obj.arr.push(7)  // get 但是监听不到下面的了

由此看出:数组的一些方法,例如push等,get是监听不到的。 通过索引访问或者修改数组中已经存在的元素,是可以触达get和set,但是对于通过push、unshift增加的元素,会增加索引,这种情况无法监听,通过pop或shift删除元素,会更新索引,也会触发。 在vue2 中,是通过重写Array原型上的方法解决这个问题。 但是其实依然会有很多问题,类似给对象新增一个属性时,也需要手动监听,所以在vue2中给数组或者对象新增属性时,使用this.$set 或者vue.Set()

Vue3的Proxy

Es6的proxy正好可以解决以上的问题。

  1. 定义: Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。 其实就是在对目标对象的操作之前提供了拦截,可以对外界的操作进行过滤和改写,修改某些操作的默认行为,这样我们可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象,达到预期的目的。
  2. 语法:
	const p = new Proxy(target, handler)
	//`target` 要使用  `Proxy`  包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

	// `handler` 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理  `p`  的行为。

简单的例子:

	// 定义一个需要代理的对象
	let person = {
		name:'张小张',
		age:18
	}
	
	//定义handler 对象
	let handler = {
		get(target,key){   // target就是代理的对象,key 是属性值
			return key in target?target[key]:'没有这个属性'
		},
		set(target,key,val){   // val 是新值
			target[key] = val
			return true
		}
	}
	
	let proxyObj = new Proxy(person,handler)
	
	console.log(proxyObj.name)   // 张小张
	
	console.log(proxyObj.sex) // 没有这个属性
	
	proxyObj.age = 19
	console.log(proxyObj.age) //19

这里注意一下:set()方法必须返回一个布尔值,返回 true 代表属性设置成功,- 在严格模式下,如果 set() 方法返回 false,那么会抛出一个异常。

  1. Reflect:是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers 的方法相同。 为操作对象而提供的新API,将Object对象的属于语言内部的方法放到Reflect对象上,即从Reflect对象上拿Object对象内部方法。 如果出错将返回false。 所以上面的例子改一下:
	// 定义一个需要代理的对象
	let person = {
		name:'张小张',
		age:18
	}
	
	//定义handler 对象
	let handler = {
		get(target,key){   // target就是代理的对象,key 是属性值
			console.log(Reflect.get(target, key));
			return Reflect.get(target, key)?Reflect.get(target, key):'没有这个属性'
		},
		set(target,key,val){   // val 是新值
			Reflect.set(target,key,val)
			return true
		}
	}
	
	let proxyObj = new Proxy(person,handler)
	
	console.log(proxyObj.name)   // 张小张  张小张 
	
	console.log(proxyObj.sex) // undefined 没有这个属性
	
	proxyObj.age =22 
	console.log(proxyObj.age) //22

用这个例子和上面的例子作比较发现:Proxy代理的是整个对象,而不是对象的某个特定属性,不需要我们通过遍历来逐个进行数据绑定。 不光如此,对于上面遇到的数组方法、对象新增属性等都可以得到解决。

  1. Proxy 的拦截: 除了上面用到的set()和get(),Proxy支持的拦截操作一共有13种。具体信息可以查看MDN([https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy]
  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。
  1. Proxy 总结: Proxy相比Object.defineProperty,可以直接监听整个对象而非属性,也可以直接监听数组的变化,并且提供13中拦截方法。但是缺点是不兼容IE,存在浏览器兼容问题,也没有polyfill(指的是一个代码块。这个代码块向开发者提供了一种技术, 这种技术可以让浏览器提供原生支持,抹平不同浏览器对API兼容性的差异)。Object.defineProperty支持IE9。

探究React中setState的执行机制

胖蔡阅读(88)

相关的一些问题

  1. setState 是同步还是异步的,为什么有时可以里见到更新的结果而有时又不行?
  2. 1 钩子函数和React合成事件中的 setState

假如有两个组件

javascript
componentDidMount(){
  console.log('这是父组件componentDidMount');
}
render(){
  return (
    <div>
      <SetState1></SetState1>
      <SetState></SetState>
    </div>
  )
}

组件内部放入同样的代码,并在SetState1中的componentDidMount中添加一段同步延时代码,打印延时时间

javascript
componentWillUpdate(){
  console.log('componentWillUpdate');
}
componentDidUpdate(){
  console.log('componentDidUpdate');
}
componentDidMount(){
  console.log('SetState调用setState');
  this.setState({index:this.state.index +1});
  console.log('state',this.state.index);
  console.log('SetState调用setState');
  this.setState({index:this.state.index+1});
  console.log('state', this.state.index);
}

说明:

    1. 调用 setState 不会立即更新
    1. 所有组件使用的是同一套更新机制,当所有组件didmount后,父组件didmount,然后执行更新
    1. 更新时会把每个组件的更新合并,每个组件只会触发一次更新的生命周期。

1.2 异步函数和原生事件中的 setstate ?
在 setTimeout 中调用 setState(例子和在浏览器原生事件以及接口回调中执行效果相同)

javascript
componentDidMount(){
  setTimeout(()=>{console.log('调用setState');  
  this.setState({index:this.state.index+1})
  console.log('state',this.state.index);
  console.log('调用setState');
  this.setState({index:this.state.index +1})
  console.log('state',this.state.index);},0);
}

根据执行结果可以得出:

    1. 在父组件 didmount 后执行
    1. 调用 setState 同步更新
  1. 为什么有时连续两次 setState 只有一次生效
    分别执行以下代码:

    javascript
    componentDidMount(){
      this.setState({index:this.state.index +1},()=>{console.log(this.state.index);})
      this.setState({index:this.state.index +1},()=>{console.log(this.state.index);})
    }
    
    componentDidMount(){
      this.setState((preState)=>({index:preState.index+1}),()=>{
        console.log(this.state.index);
      })
      this.setState(preState => ({index:preState.index+1}),()=>{
        console.log(this.state.index);
      })
    }

    执行结果:

    javascript
    1
    1
    2
    2

    说明:

    1. 直接传递对象的 setstate 会被合并成一次
    1. 使用函数传递 state 不会被合并

setState执行过程

1. 流程图
  • partialState: setState传入的第一个参数,对象或函数
  • _pendingStateQueue:当前组件等待执行更新的 state队列
  • isBatchingUpdates:react用于标识当前是否处于批量更新状态,所有组件公用
  • dirtyComponent:当前所有处于待更新状态的组件队列
  • transcation:react的事务机制,在被事务调用的方法外包装n个 waper对象,并一次执行: waper.init、被调用方法、 waper.close
  • FLUSH_BATCHED_UPDATES:用于执行更新的 waper,只有一个 close方法
2. 执行过程

按照上面的流程图可以得到:

  • 1.将setState传入的 partialState参数存储在当前组件实例的state暂存队列中。
  • 2.判断当前React是否处于批量更新状态,如果是,将当前组件加入待更新的组件队列中。
  • 3.如果未处于批量更新状态,将批量更新状态标识设置为true,用事务再次调用前一步方法,保证当前组件加入到了待更新组件队列中。
  • 4.调用事务的 waper方法,遍历待更新组件队列依次执行更新。
  • 5.执行生命周期 componentWillReceiveProps。
  • 6.将组件的state暂存队列中的 state进行合并,获得最终要更新的state对象,并将队列置为空。
  • 7.执行生命周期 componentShouldUpdate,根据返回值判断是否要继续更新。
  • 8.执行生命周期 componentWillUpdate。
  • 9.执行真正的更新, render。
  • 10.执行生命周期 componentDidUpdate。

总结

1. 钩子函数和合成事件中:

在 react的生命周期和合成事件中, react仍然处于他的更新机制中,这时 isBranchUpdate为true。
按照上述过程,这时无论调用多少次 setState,都会不会执行更新,而是将要更新的 state存入 _pendingStateQueue,将要更新的组件存入 dirtyComponent。
当上一次更新机制执行完毕,以生命周期为例,所有组件,即最顶层组件 didmount后会将 isBranchUpdate设置为false。这时将执行之前累积的 setState。

2. 异步函数和原生事件中

由执行机制看, setState本身并不是异步的,而是如果在调用 setState时,如果 react正处于更新过程,当前更新会被暂存,等上一次更新执行后在执行,这个过程给人一种异步的假象。
在生命周期,根据JS的异步机制,会将异步函数先暂存,等所有同步代码执行完毕后在执行,这时上一次更新过程已经执行完毕, isBranchUpdate被设置为false,根据上面的流程,这时再调用 setState即可立即执行更新,拿到更新结果。

3. partialState合并机制

我们看下流程中 _processPendingState的代码,这个函数是用来合并 state暂存队列的,最后返回一个合并后的 state。

javascript
_processPendingState : function(props,context)
{
var inst = this._instance;

var queue = this._pendingStateQueue;
var replace = this._pendingReplaceState;

this._pendingReplaceState = false;
this._pendingStateQueue = null;

if(!queue)
{     
return inst.state;
}
if(replace && queue.length === 1)
{
  return queue[0];
}
    
var nextState = _assign({}, replace ? queue[0] : inst.state);    
for(var i = replace ? 1 : 0 ; i < queue.length;i++)
{
  var partial = queue[i];
  _assign(nextState, typeof partial === 'function' ? partial.call(inst,nextState,props,context) : partial);
}
return nextState;
}

我们只需要关注下面这段代码:

javascript
_assign(nextState,typeof partial === 'function' ? partial.call(inst,nextState,props,context) : partial);

如果传入的是对象,很明显会被合并成一次:

javascript
Object.assign(
  nextState,
  {index: state.index+1},
  {index: state.index+1},
)

如果传入的是函数,函数的参数preState是前一次合并后的结果,所以计算结果是准确的。

4. componentDidMount调用 setstate

在componentDidMount()中,你 可以立即调用setState()。它将会触发一次额外的渲染,但是它将在浏览器刷新屏幕之前发生。这保证了在此情况下即使render()将会调用两次,用户也不会看到中间状态。谨慎使用这一模式,因为它常导致性能问题。在大多数情况下,你可以 在constructor()中使用赋值初始状态来代替。然而,有些情况下必须这样,比如像模态框和工具提示框。这时,你需要先测量这些DOM节点,才能渲染依赖尺寸或者位置的某些东西。

以上是官方文档的说明,不推荐直接在 componentDidMount 直接调用 setState,由上面的分析: componentDidMount 本身处于一次更新中,我们又调用了一次 setState,就会在未来再进行一次 render,造成不必要的性能浪费,大多数情况可以设置初始值来搞定。

当然在 componentDidMount 我们可以调用接口,再回调中去修改 state,这是正确的做法。

当state初始值依赖dom属性时,在 componentDidMount 中 setState是无法避免的。

5. componentWillUpdate componentDidUpdate

这两个生命周期中不能调用 setState
由上面的流程图很容易发现,在它们里面调用 setState会造成死循环,导致程序崩溃。

6. 推荐使用方式

在调用 setState时使用函数传递 state值,在回调函数中获取最新更新后的 state

JS中的Map和Object的使用

胖蔡阅读(151)

在 JavaScript 中,普通对象和 ES6 的新对象 Map 都可以存储键值对,平时普通对象用的较多,现在着重了解一下Map

描述

Map 对象存有键值对,其中的键可以是任何数据类型。
Map 对象记得键的原始插入顺序。
Map 对象具有表示映射大小的属性。

创建

构造函数创建

javascript
new Map([iterable])

Iterable 可以是一个数组或者其他 iterable 对象,其元素为键值对(两个元素的数组,例如: [[ 1, ‘one’ ],[ 2, ‘two’ ]])。 每个键值对都会添加到新的 Map。null 会被当做 undefined。

方法和属性

javascript
let map = new Map()

打印map可得
87263a741d1708d

javascript
Map(0) {size: 0}
  [[Entries]]
  No properties
  size: 0
  [[Prototype]]: Map
  clear: ƒ clear()
  constructor: ƒ Map()
  delete: ƒ delete()
  entries: ƒ entries()
  forEach: ƒ forEach()
  get: ƒ ()
  has: ƒ has()
  keys: ƒ keys()
  set: ƒ ()
  size: 0
  values: ƒ values()
  Symbol(Symbol.iterator): ƒ entries()
  Symbol(Symbol.toStringTag): "Map"
  get size: ƒ size()
  [[Prototype]]: Object

size

访问属性,用于返回 一个Map 对象的成员数量。

javascript
let map = new Map([
  ['name','wawa'],
  ['age',18]
])
console.log(map.size);//2 

has()

返回一个bool值,用来表明map 中是否存在指定元素,返回布尔值

javascript
let map = new Map([
  ['name','wawa'],
  ['age',18]
])

console.log(map.has('name'));// true
console.log(map.has('hahahah')  //false

set()

为 Map 对象添加或更新一个指定了键(key)和值(value)的(新)键值对

javascript
let map = new Map([
  ['name','wawa'],
  ['age',18]
])
map.set('bar', 'foo');
console.log(map);  //Map(3) {'name' => 'wawa', 'age' => 18, 'bar' => 'foo'}

get()

返回某个 Map 对象中的一个指定元素。

javascript
let map = new Map([
  ['name','wawa'],
  ['age',18]
])
map.set('bar', 'foo');
console.log(map.get('name'));//wawa

keys()

返回一个引用的 Iterator 对象。它包含按照顺序插入 Map 对象中每个元素的key值

javascript
let map = new Map([
  ['name','wawa'],
  ['age',18]
])
map.set('bar', 'foo');
let keys = 	map.keys()
console.log(keys); //MapIterator {'name', 'age', 'bar'}

for(let k of keys){
  console.log(k);   // name  age bar
}

values()

返回一个新的Iterator对象。它包含按顺序插入Map对象中每个元素的value值。

javascript
let map = new Map([
  ['name','wawa'],
  ['age',18]
])
map.set('bar', 'foo');
let values = 	map.values()
console.log(values); //MapIterator {'wawa', 18, 'foo'}

for(let k of values){
  console.log(k);   
}

entries()

返回一个新的包含 [key, value] 对的 Iterator 对象,返回的迭代器的迭代顺序与 Map 对象的插入顺序相同。

javascript
let map = new Map([
  ['name','wawa'],
  ['age',18]
])
map.set('bar', 'foo');
let entries = map.entries()
console.log(entries); //MapIterator {'name' => 'wawa', 'age' => 18, 'bar' => 'foo'}
console.log(entries.next().value); //(2) ['name', 'wawa']
console.log(entries.next().value); //(2)(2) ['age', 18]
console.log(entries.next().value); //(2) (2) ['bar', 'foo'] 
  • next() 是Iterator 对象的方法

    clear()

    移除Map对象中的所有元素

    javascript
    let map = new Map([
      ['name','wawa'],
      ['age',18]
    ])
    map.set('bar', 'foo');
    
    map.clear()
    console.log(map.size);//0

    delete()

    移除 Map 对象中指定的元素,返回布尔值

    javascript
    let map = new Map([
      ['name','wawa'],
      ['age',18]
    ])
    map.set('bar', 'foo');
    
    
    console.log(map.delete('name'));//true
    console.log(map.delete('namess'));//false

    forEach()

    按照插入顺序依次对 Map 中每个键/值对执行一次给定的函数

    javascript
    let map = new Map([
      ['name','wawa'],
      ['age',18]
    ])
    map.set('bar', 'foo');
    
    map.forEach((value,key,map)=>{
      console.log(`value:${value},key:${key}`);
    })
    //value:wawa,key:name  value:18,key:age value:foo,key:bar

    for…of…

    Map可以使用for..of循环来实现迭代

    javascript
    let map = new Map([
      ['name','wawa'],
      ['age',18]
    ])
    map.set('bar', 'foo');
    
    for(let m of map){
      console.log(m);
    }
    //(2) ['name', 'wawa'] ['age', 18]  ['bar', 'foo']

    利用散布运算符…遍历集合

    javascript
    let map = new Map([
      ['name','wawa'],
      ['age',18]
    ])
    map.set('bar', 'foo');
    let keys = 	map.keys()
    console.log(keys); //MapIterator {'name', 'age', 'bar'}
    
    console.log(...keys); // 'name', 'age', 'bar'
    console.log(...map)  // (2) ['name', 'wawa']['age', 18] ['bar', 'foo']

    和普通对象的区别

    Objects 和 Maps 类似的是,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。
    不同的是:

    1. 默认值

    1. Map 默认情况不包含任何键。只包含显式插入的键。
    2. 一个 Object 有一个原型, 原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。

      2. 键类型

    3. 一个Object 的键必须是一个 String 或是Symbol
    4. 一个 Map的键可以是任意值,包括函数、对象或任意基本类型。

      3. 键顺序

    5. 一个 Object 的键是无序的
    6. Map 中的 key 是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。

      4. Size

    7. Object 的键值对个数只能手动计算
    8. Map 的键值对个数可以轻易地通过size 属性获取

      5. 迭代

    9. 迭代一个Object需要以某种方式获取它的键然后才能迭代
    10. 使用 for…of 语句或 Map.prototype.forEach 直接迭代 Map 的属性

      序列化和解析

    11. 普通对象支持 JSON 序列化
    12. Map 默认无法获取正确数据

      性能

    13. 在频繁增删键值对的场景下表现更好
    14. 在频繁添加和删除键值对的场景下未作出优化

迭代协议:

迭代协议具体分为两个协议:可迭代协议和 迭代器协议。
可迭代协议:允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for..of 结构中,哪些值可以被遍历到。一些内置类型同时是内置可迭代对象,并且有默认的迭代行为,比如 Array 或者 Map,而其他内置类型则不是(比如 Object))
要成为可迭代对象, 一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性:

属性
[Symbol.iterator] 一个无参数的函数,其返回值为一个符合迭代器协议的对象。

迭代器协议:定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值
只有实现了一个拥有以下语义(semantic)的 next() 方法,一个对象才能成为迭代器:

属性
next 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
done(boolean)
如果迭代器可以产生序列中的下一个值,则为 false。
如果迭代器已将序列迭代完毕,则为 true。
这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
next() 方法必须返回一个对象,
该对象应当有两个属性: done 和 value,如果返回了一个非对象值(比如 false 或 undefined),
则会抛出一个 TypeError 异常(”iterator.next() returned a non-object value”)。

Iterator 迭代器是一种接口,为不同的数据结构提供统一的访问机制,这个访问机制主要是遍历,我们知道,在数组、在对象、在类数组、在map、在set里面,都可以用for of或者扩展运算符来得到一个数组或者是遍历当前的数据结构,为什么能够遍历的原因就是因为存在这个Iterator 迭代器这个东西,所以我们可以用for of来提供一个统一的遍历,因此这个底层或者是Itrator它最初是为了for of而设计的。

为了给所有的数据结构有一个统一的遍历接口,统一的访问机制,因此就产生了迭代器。
迭代器的特点:

  • 按某种次序排列数据结构
  • 为for of提供一个遍历支持
  • 为不同的数据结构提供统一的访问机制(目的)

目前所有的内置可迭代对象如下:StringArrayTypedArrayMap 和 Set等,它们的原型对象都实现了 @@iterator 方法。对象(Object)之所以没有默认部署Iterator接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。
例如:由上面的学习,我们知道map的values() 返回的就是一个Iterator对象

javascript
let map = new Map([
      ['name','zz'],
      ['age',15]
    ])
  console.log(map.values().next()); // {value: 'zz', done: false}

展开语法… :
其内部实现也使用了同样的迭代协议

javascript
console.log(...'string') // s t r i n g

例如:String 是一个内置的可迭代对象

javascript
let str = 'string'
// 利用Symbol.iterator 属性获取一个迭代器对象
let it = str[Symbol.iterator]()
console.log(it); // StringIterator {}
console.log(it.next());  // {value: 's', done: false}

问题:obj is not iterable

javascript
let obj = {
    name:'张小张',
    age:18
  }
console.log([...obj]); // Uncaught TypeError: obj is not iterable

怎么做好利用展开值且不报错??
根据mdn迭代器的介绍:
给obj加上一个属性[Symbol.iterator],且返回next()

javascript
obj[Symbol.iterator] = function() {
    let nextIndex = 0;
    return {
        next: function () {
            let array = Object.values(obj)
            return nextIndex < array.length ? {
                value: array[nextIndex++],
                done: false
            } : {
                done: true
            };
        }
    };
}
			
console.log([...obj]) // ['张小张', 18]

Vue+Element项目的批量导入组件封装

胖蔡阅读(95)

Excel导入、Excel导出是一对好兄弟,经常成双成对出现。所以,在封装了Excel导出方法 —— 《用xlsx-style玩转Excel导出——拿走即用的Excel导出方法封装》后,今天我们来封装一下Excel导入。

Excel导入的封装和Excel导出的封装不太一样,Excel导出是纯js封装的,但是Excel导入的封装可能是组件级别的封装。

一、 安装依赖

在正式开始封装之前,我们需要先安装xlsx。这个插件,能够方面我们将导入的文件,处理成我们需要的数据。

js
npm install -S xlsx

安装后,在需要使用的地方引入即可。

js
import XLSX from 'xlsx'

二、上传文件

因所做的涉及到Excel批量导入的项目,都是基于Vue+Element的,所以直接选用了Element的el-upload组件实现文件上传功能,如下:

js
// BatchImport.vue

<template>

  <el-upload
  action="#"
  accept=".xls,.xlsx"
  :show-file-list="false"
  :auto-upload="true"
  :multiple="false"
  :http-request="handleBatchImport"
>
  <el-button size="small" type="primary">批量导入</el-button>
 </el-upload>

</template>

三、处理文件

文件上传后,我们获取到导入的Excel文件,然后进行如下处理:

  • 通过new FileReader()读取文件
  • 将读取结果通过XLSX.read()方法转换成工作簿对象
  • 处理工作簿对象,获取有效的目标数据
js
// BatchImport.vue

<script>
import XLSX from 'xlsx'
  export default { 
    name: 'BatchImport',
    methods: {
      handleBatchImport(file) {
        const that = this
        // 通过FileReader对象读取文件
        const fileReader = new FileReader();
        fileReader.onload = (event) => { 
          try {
            const { result } = event.target;
            // 以二进制流方式读取得到整份excel表格对象
            const workbook = XLSX.read(result, { type: "binary" });
            // 存储获取到的数据
            const excelData = {};
            // 遍历获取每张工作表 除去隐藏表
            const allSheets = workbook.Workbook.Sheets;
            allSheets.forEach( item => {
              const name = item.name;
              if (workbook.Sheets.hasOwnProperty(name) && item.Hidden === 0) {
                // 利用 sheet_to_json 方法将 excel 转成 json 数据
                const data = [].concat(
                  XLSX.utils.sheet_to_json(workbook.Sheets[name])
                ); 
                // 处理表格有效内容后面的空格行,获得有效数据  
                excelData[name] = that.handleEmptyRow(data)
              }
            })
            //将excelData暴露出去
            that.$emit('importExcel', excelData)
          } catch(e) {
            that.$message.error('文件上传错误!')
            that.$emit('importExcel', null)
            return false
          }
        }
        //以二进制方式打开文件
        fileReader.readAsBinaryString(file.file);
      },
    
      // 处理表格空行问题
      handleEmptyRow(data) {
        // 去除最后的空格行
        for (let i = data.length - 1; i >= 0; i--) {
          // 判断数组是否全为空
          const flag = Object.values(data[i]).find((item) => {
            return String(item).replace(/(^s*)|(s*$)/g, '').length
          })
          if (!flag) {
            data.splice(i, 1)
          } else {
            break
          }
        }
        return data
      },
    }
  }
</script>

四、处理数据

经过上述步骤,我们获取到目标数据,接下来,在父组件里对数据进行进一步处理和操作。

js
// Father.vue

<template>
  <batch-import @importExcel="importExcel"></batch-import>
</template>

<script>
import BatchImport from './BatchImport.vue'
  export default { 
    components: {
      BatchImport,
    },
    methods: {
      importExcel(excelData) {
        ...
    
      }
    }
  }
</script>

因数据具体如何处理,需要根据项目需求和后端要求而定,所以未将数据在BatchImport组件内进行处理,而是选择暴露出来在父组件内处理。

—— 本文结束,感谢您的

Chrome访问一个网页启动了多少个进程?

胖蔡阅读(101)

在目前的Chrome进程架构里,访问一个网站至少包含四个进程:一个浏览器进程、一个GPU进程、一个渲染进程和一个网络进程。除此之外还有包含多个插件进程组成Chrome的进程架构。

e01ed7fbfba87ce

 

浏览器进程

负责控制浏览器除标签页外的界面,包括地址栏、书签、前进后退按钮等,以及负责与其他进程的协调工作,同时提供存储功能。

 

GPU进程

负责整个浏览器界面的渲染。Chrome刚开始发布的时候是没有GPU进程的,而使用GPU的初衷是为了实现3D CSS效果,只是后面网页、Chrome的UI界面都用GPU来绘制,这使GPU成为浏览器普遍的需求,最后Chrome在多进程架构上也引入了GPU进程。

网络进程

负责发起和接受网络请求,以前是作为模块运行在浏览器进程里面的,后面才独立出来,成为一个单独的进程。

插件进程

主要是负责插件的运行,因为插件可能崩溃,所以需要通过插件进程来隔离,以保证插件崩溃也不会对浏览器和页面造成影响。

渲染进程

负责控制显示tab标签页内的所有内容,核心任务是将HTML、CSS、JS转为用户可以与之交互的网页,排版引擎 Blink 和 JS 引擎 V8 都是运行在该进程中,默认情况下Chrome会为每个Tab标签页创建一个渲染进程。

渲染进程中包含哪些线程

  • eca0b769b3f7ef1GUI渲染线程:GUI渲染线程负责渲染浏览器界面HTML元素,当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,也就是说被冻结了。
  • JavaScript 引擎线程:这就是我们常说的js单线程,主要是为了实现JS文件的解析和运行,包括执行用户的交互操作、dom树修改、css样式更新等操作。需要注意的是若js执行过程会导致GUI渲染线程阻塞,它们不可同步执行。
  • 定时触发器线程:浏览器定时计数器(setTimeout、setInterval )并不是由JS引擎计数的, 因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确, 因此通过单独线程来计时并触发定时是更为合理的方案。
  • 异步Http请求线程:在XMLHttpRequest在连接后是通过浏览器新开一个线程请求,将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件放到JS引擎的处理队列中等待处理。
  • 事件处理线程:事件处理线程管理着任务队列,异步任务的回调函数会放在其中,等 执行栈中的任务执行完毕后,读取其中任务执行。如鼠标点击、滑动、异步http请求等的回调处理。

 

用xlsx-style玩转Excel导出——拿走即用的Excel导出方法封装

胖蔡阅读(90)

《用xlsx-style玩转Excel导出——像手动操作Excel表格一样用JS操作Excel》一文中,我们详细介绍了xlsx-style插件相关的概念、属性和方法,今天,我们将利用xlsx-style插件封装出一个通用的、可自定义的Excel导出方法,以期能够满足日常Web项目的导出需求。

一、常见自定义需求

在正式开始封装之前,我们先梳理一下,我们Excel导出一般都有那些自定义需求。

  1. 导出的Excel文件名自定义
  2. 支持单sheet、多sheet导出
  3. sheet名自定义
  4. 表头内容自定义
  5. 表格内容自定义
  6. 表格样式自定义
  7. 支持合并单元格
  8. 支持冻结单元格

二、抽象导出方法名及参数

根据上述自定义需求,我们可以初步将Excel导出方法及参数定义如下:

/**
 * 自定义导出excel
 * @param { 导出的数据 } exportData
 * @param { 工作簿配置 } workBookConfig
 * @param { 导出文件名 } fileName
 */
 
 function exportExcel(exportData, workBookConfig = defaultWorkBook, fileName = '未命名') {
 
 }

三、参数介绍及处理

(一)exportData

参数exportData是一个对象数组,可以满足单sheet、多sheet的导出,同时可以配置每个sheet的表头、表格内容、样式等,格式如下:

[
  {
    header: [], // 表头
    dataSource: [], // 数据源
    workSheetConfig: {}, // 工作表配置
    cellConfig: {}, // 单元格配置
    sheetName: ''// sheet名
  }
]
  • header

header是一个对象数组,用来设置表头,格式如下:

[
   { title: '分站', dataIndex: 'subName', width: 140 }, 
   { title: '企业总数', dataIndex: 'enterpriseCount', width: 140 },
]

接收到header后,需将header内容处理成表格语言,具体如下:

const _header = header.map((item, i) =>
    Object.assign({}, {
      key: item.dataIndex,
      title: item.title,
      // 定位单元格
      position: getCharCol(i) + 1, 
      // 设置表头样式
      s: data.cellConfig && data.cellConfig.headerStyle ? data.cellConfig.headerStyle : defaultCellStyle.headerStyle,
    })
  ).reduce((prev, next) =>
    Object.assign({}, prev, {
      [next.position]: { v: next.title, key: next.key,s: next.s },
    }), {},
 )

header已经被处理成worksheet Object,而其中很重要一步,就是定位单元格,而我们Excel是用字母+数字的形式定位单元格的,所以我们可以通过如下代码定位单元格。

position: getCharCol(i) + 1

其中,getCharCol()方法是用来生成单元格对应的字母的,代码如下:

/**
 * 生成ASCll值 从A开始
 * @param {*} n
 */
function getCharCol(n) {
  if( n > 25) {
    let s = ''
    let m = 0
   while (n > 0) {
    m = n % 26 + 1
    s = String.fromCharCode(m + 64) + s
    n = (n - m) / 26
   }
    return s
  }
  return String.fromCharCode(65 + n) 
}

  • cellConfig

cellConfig是一个对象,用来配置单元格的样式,格式如下:

{
  headerStyle: { // 表头区域样式配置
    border: {},
    font: {}, 
    alignment: {}, 
    fill: {},
  },
  dataStyle: { // 内容样式配置
    border: {},
    font: {}, 
    alignment: {},
    fill: {}, 
  } 
}

  • dataSource

dataSource是一个对象数组,用来表示表格内容,格式如下:

[
   { subName:'合肥' }, 
   { enterpriseCount: '100' },
]

同样,接收到dataSource后,需将dataSource内容处理成表格语言,具体如下:

let _data = {};

 // 处理表格内容及样式
dataSource.forEach((item, i) => {
  header.forEach((obj, index) => {
    const key = getCharCol(index) + (i + 2);
    const key_t = obj.dataIndex;
    _data[key] = {
      v: item[key_t],
      s: cellConfig.dataStyle? cellConfig.dataStyle : defaultCellStyle.dataStyle
    }
  })
});


  • workSheetConfig

workSheetConfig是一个对象,用来配置工作表的合并单元格、冻结单元格等功能,格式如下:

{
  merges: [ // 合并单元格
    { s: { c: 1, r: 1 },e: { c: 3, r: 3 } }
  ],
  freeze:{ // 冻结单元格
    xSplit: '1',
    ySplit: '1',
    topLeftCell: 'B2',
    state: 'frozen',
  } 
}

(二)workBookConfig

workBookConfig是一个对象,用来配置工作簿的类型等,格式如下:

{
  bookType: 'xlsx', // 工作簿的类型
  bookSST: false,
  type: 'binary', // 输出数据类型
}

四、生成workbook object

const wb = {SheetNames: [], Sheets: {}}

exportData.forEach( (data, index) => { 

  const output = Object.assign({}, _headers, _data);
  const outputPos = Object.keys(output); 
  
  // 设置单元格宽度
  const colWidth = data.header.map( item => { wpx: item.width || 60 })
  
  const merges = data.workSheetConfig && data.workSheetConfig.merges

  const freeze = data.workSheetConfig && data.workSheetConfig.freeze
  
  // 处理sheet名
  wb.SheetNames[index] = data.sheetName ? data.sheetName : 'Sheet' + (index + 1)
  
  // 处理sheet数据
  wb.Sheets[wb.SheetNames[index]] = Object.assign({}, 
     output, //导出的内容
     {
        '!ref': `${outputPos[0]}:${outputPos[outputPos.length - 1]}`,
        '!cols': [...colWidth],
        '!merges': merges ? [...merges] : undefined,
        '!freeze': freeze ? [...freeze] : undefined,
    }
  )  

})

五、new Blob转换成二进制类型的对象

 const tmpDown = new Blob(
    [ s2ab( XLSX.write(wb, workBookConfig))],
    { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'}
 )
  
// 字符串转字符流---转化为二进制的数据流
function s2ab(s) {
  if (typeof ArrayBuffer !== 'undefined') {
    const buf = new ArrayBuffer(s.length);
    const view = new Uint8Array(buf);
    for (let i = 0; i !== s.length; ++i) view[i] = s.charCodeAt(i) & 0xff;
    return buf;
  } else {
    const buf = new Array(s.length);
    for (let i = 0; i !== s.length; ++i) buf[i] = s.charCodeAt(i) & 0xff;
    return buf;
  }
}

六、下载Excel

downExcel(tmpDown, `${fileName + '.'}${workBookConfig.bookType == 'biff2' ? 'xls' : workBookConfig.bookType}`)

function downExcel(obj, fileName) {
  const a_node = document.createElement('a');
  a_node.download = fileName;

  // 兼容ie
  if ('msSaveOrOpenBlob' in navigator) {
    window.navigator.msSaveOrOpenBlob(obj, fileName);
  } else {
    // 新的对象URL指向执行的File对象或者是Blob对象.
    a_node.href = URL.createObjectURL(obj);
  }
  a_node.click();
  setTimeout(() => {
    URL.revokeObjectURL(obj);
  }, 100);
}

七、默认配置

// 默认工作簿配置

const defaultWorkBook = {
  bookType: 'xlsx',
  bookSST: false,
  type: 'binary',
}

// 默认样式配置

const borderAll = {  //单元格外侧框线
    top: {
      style: 'thin',
    },
    bottom: {
      style: 'thin'
    },
    left: {
      style: 'thin'
    },
    right: {
      style: 'thin'
    }
  };

const defaultCellStyle = {
  headerStyle: { // 表头区域样式配置
    border: borderAll,
    font: { name: '宋体', sz: 11, italic: false, underline: false, bold: true }, 
    alignment: { vertical: 'center', horizontal: 'center'}, 
    fill: { fgColor: { rgb: 'FFFFFF' } },
  },
  dataStyle: { // 内容样式配置
    border: borderAll,
    font: { name: '宋体', sz: 11, italic: false, underline: false }, 
    alignment: { vertical: 'center', horizontal: 'left', wrapText: true},
    fill: { fgColor: { rgb: 'FFFFFF' } }, 
  } 
}

八、完整代码

import XLSX from 'xlsx-style'

// 默认工作簿配置

const defaultWorkBook = {
  bookType: 'xlsx',
  bookSST: false,
  type: 'binary',

}

// 默认样式配置

const borderAll = { 
  top: {
    style: 'thin',
  },
  bottom: {
    style: 'thin',
  },
  left: {
    style: 'thin',
  },
  right: {
    style: 'thin',
  },
}

const defaultCellStyle = {
  // 表头区域样式配置
  headerStyle: { 
    border: borderAll,
    font: { name: '宋体', sz: 11, italic: false, underline: false, bold: true },
    alignment: { vertical: 'center', horizontal: 'center' },
    fill: { fgColor: { rgb: 'FFFFFF' } },
  },
  // 内容区域样式配置
  dataStyle: { 
    border: borderAll,
    font: { name: '宋体', sz: 11, italic: false, underline: false },
    alignment: { vertical: 'center', horizontal: 'left', wrapText: true },
    fill: { fgColor: { rgb: 'FFFFFF' } },
  },
}

function exportExcel(exportData, workBookConfig = defaultWorkBook, fileName = '未命名') {

  if (!(exportData && exportData.length)) {
    return
  }
  
  // 定义工作簿对象
  const wb = { SheetNames: [], Sheets: {} }

  exportData.forEach((data, index) => {
  
    // 处理sheet表头
    const _header = data.header.map((item, i) =>
      Object.assign({}, {
        key: item.dataIndex,
        title: item.title,
        // 定位单元格
        position: getCharCol(i) + 1, 
        // 设置表头样式
        s: data.cellConfig && data.cellConfig.headerStyle ? data.cellConfig.headerStyle : defaultCellStyle.headerStyle,
      })
    ).reduce((prev, next) =>
      Object.assign({}, prev, {
        [next.position]: { v: next.title, key: next.key, s: next.s },
      }), {}
    )

    // 处理sheet内容
    const _data = {}
    data.dataSource.forEach((item, i) => {
      data.header.forEach((obj, index) => {
        const key = getCharCol(index) + (i + 2)
        const key_t = obj.dataIndex
        _data[key] = {
          v: item[key_t],
          s: data.cellConfig && data.cellConfig.dataStyle ? data.cellConfig.dataStyle : defaultCellStyle.dataStyle,
        }
      })
    })

    const output = Object.assign({}, _header, _data)
    const outputPos = Object.keys(output)


    // 设置单元格宽度
    const colWidth = data.header.map(item => { return { wpx: item.width || 80 } })

    const merges = data.workSheetConfig && data.workSheetConfig.merges

    const freeze = data.workSheetConfig && data.workSheetConfig.freeze

    // 处理sheet名
    
    wb.SheetNames[index] = data.sheetName ? data.sheetName : 'Sheet' + (index + 1)
    
    // 处理sheet数据
    
    wb.Sheets[wb.SheetNames[index]] = Object.assign({},
      output, // 导出的内容
      {
        '!ref': `${outputPos[0]}:${outputPos[outputPos.length - 1]}`,
        '!cols': [...colWidth],
        '!merges': merges ? [...merges] : undefined,
        '!freeze': freeze ? [...freeze] : undefined,
      }
    )
  })
  
  // 转成二进制对象
  const tmpDown = new Blob(
    [s2ab(XLSX.write(wb, workBookConfig))],
    { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
  )
  
  // 下载表格
  downExcel(tmpDown, `${fileName + '.'}${workBookConfig.bookType === 'biff2' ? 'xls' : workBookConfig.bookType}`)
}

/**
 * 生成ASCll值 从A开始
 * @param {*} n
 */
function getCharCol(n) {
  if (n > 25) {
    let s = ''
    let m = 0
    while (n > 0) {
      m = n % 26 + 1
      s = String.fromCharCode(m + 64) + s
      n = (n - m) / 26
    }
    return s
  }
  return String.fromCharCode(65 + n)
}

// 字符串转字符流---转化为二进制的数据流
function s2ab(s) {
  if (typeof ArrayBuffer !== 'undefined') {
    const buf = new ArrayBuffer(s.length)
    const view = new Uint8Array(buf)
    for (let i = 0; i !== s.length; ++i) { view[i] = s.charCodeAt(i) & 0xff }
    return buf
  } else {
    const buf = new Array(s.length)
    for (let i = 0; i !== s.length; ++i) { buf[i] = s.charCodeAt(i) & 0xff }
    return buf
  }
}

function downExcel(obj, fileName) {
  const a_node = document.createElement('a')
  a_node.download = fileName

  // 兼容ie
  if ('msSaveOrOpenBlob' in navigator) {
    window.navigator.msSaveOrOpenBlob(obj, fileName)
  } else {
    // 新的对象URL指向执行的File对象或者是Blob对象.
    a_node.href = URL.createObjectURL(obj)
  }
  a_node.click()
  setTimeout(() => {
    URL.revokeObjectURL(obj)
  }, 100)
}

export {
  exportExcel,
}

Vue项目中,路由都被缓存了,特定页面需要自定义刷新规则,怎么办?

胖蔡阅读(226)

最近开发的几个后台管理系统,都是基于vue-element-admin框架开发的。框架中,默认将所有页面进行了keep-alive缓存。但是在实际需求中,某些页面每次打开都需要刷新数据。这就出现了一个问题:

对于已被缓存的页面,如何进行数据刷新?

目前,在项目开发中,主要用到以下三种方式:

一、监听路由,刷新数据

js
watch: {
   $route: function (newVal) {
     if (/data-count\/firmCount/.test(newVal.path)) {
       this.init()
     }
   },
 },

init()方法中,将页面所有缓存的内容、操作的痕迹都进行了重置,让页面回到初始状态。也就是,每次打开目标页面,都如强制刷新一样,没有任何缓存的痕迹。

二、利用生命周期函数,刷新数据

通过keep-alive缓存后的页面,大部分的生命周期函数是不走的。比如:created,mounted等。能够走的生命周期函数就是:activated与deactivated。所以可以在activated生命周期函数中,进行数据刷新。

js
activated() {
    this.init()
},

init()方法的写法同上。

三、利用导航守卫清除缓存,刷新数据

js
beforeRouteLeave(to, from, next) {   //参数(马上去的页面,现在的页面,跳转)

    if(判断条件){
         to.meta.keepAlive = false  //将要去的那个页面的缓存清空

    }else{
       to.meta.keepAlive = true   //将要去的那个页面的缓存保留
    }
    next();
  },

相对来说,前两种方法比较简单粗暴,哪个页面需要就哪个页面使用,缺点就是每个页面需要重置的内容各不相同,处理比较繁琐。

而第三种方法,我们可以封装成一个公共的方法,在需要的页面引用即可。具体可以参考公司大佬的封装,如下:

js
//RouteMixin.js

export default {

  beforeRouteLeave(to, from, next) { // 离开路由之前执行的函数
    if (this.hasCached(from) && this.hasCached(to)) {
      const vnode = this.$vnode
      const key = vnode.key// 当前关闭的组件名
      if (key && vnode.parent) {
        const cache = vnode.parent.componentInstance.cache// 缓存的组件
        const keys = this.$vnode.parent.componentInstance.keys// 缓存的组件名
        if (keys) {
          keys.splice(0, keys.length)
        }
        if (cache) {
          for (const k in cache) {
            delete cache[k]
          }
        }
      }
    }
    if (this.hasCached(to)) {
      to.meta.keepAlive = true
    } else {
      to.meta.keepAlive = false
    }
    next()
  },
  methods: {
    hasCached(route) { // 根据需要定制缓存与否的规则
      const matched = route.matched
      const current = matched[matched.length - 1]
      const parent = current.parent
      if (parent && (parent.components.default.name === 'Layout' || parent.components.default.name === 'BlankLayout')) { 
        return true
      }
      return false
    },
  },

}

需求是千变万化的,这不,在vue-element-admin项目中,关于页面的缓存刷新,最近又遇到一个新问题:

对于已被缓存的页面,如何进行前进刷新后退不刷新?

也就是说,同一个页面,从上级页面或兄弟页面到达时,刷新,从下级页面返回时,不刷新。这应该怎么处理呢?
最终,进行一番搜索,解决如下:

在beforeRouteLeave中做标记,在activated中根据标记进行刷新与否的处理

js
beforeRouteLeave(to, from, next) {
   if (to.path === '/data-count/focusGroup') {//去下级页面时打标记
     this.$route.meta.isBack = true
   } else {
     this.$route.meta.isBack = false
   }
   next()
 },
 activated() {
   if (!this.$route.meta.isBack) {
     this.init()
   } 
 },

由此,又延申出一个问题:

从下级页面返回时,页面如何定位到历史位置?

处理过前进刷新后退不刷新的问题后,这个问题变得相对简单了,我们只需在上述代码的基础上,进行如下处理:

在beforeRouteLeave中记录scrollTop,在activated中根据记录的scrollTop进行位置处理

js
beforeRouteLeave(to, from, next) {
   if (to.path === '/data-count/focusGroup') {
     this.$route.meta.isBack = true
     this.scrollTop = this.$refs.proTable.$el.scrollTop || 0 // 新增的代码
   } else {
     this.$route.meta.isBack = false
   }
   next()
 },
 activated() {
   if (!this.$route.meta.isBack) {
     this.init()
   } else {
     this.$refs.proTable.$el.scrollTop = this.scrollTop // 新增的代码
   }
 },

前端开发之websocket

胖蔡阅读(562)

一、什么是WebSocket

  • WebSocket是一种通信协议,可在单个TCP连接上进行全双工通信。WebSocket使得客户端和服务器之间的数据交
    换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以建立持久性的连接,并进行双向数据传输。
  1. WebSocket是一种通信协议
  2. 区别于HTTP协议,HTTP协议只能实现客户端请求,服务端响应的这种单项通
    而WebSocket可以实现客户端与服务端的双向通讯,,最大也是最明显的区别就是可以做到服务端主动将消息推送给客户端。

二、为什么需WebSocket?

  • 因为 HTTP 协议有一个缺陷:通信只能由客户端发起。

由于http协议只能由客户端发起通信,如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用”轮询”:每隔一段时候,就发出一个询问,了解服务器有没有新的信息。最典型的场景就是聊天室。Websocket在解决这样的需求上非常方便。
如果不使用WebSocket与服务器实时交互,一般有两种方法。AJAX轮询和Long Polling长轮询。

  • 上边这两种方式都有个致命的弱点,开销太大,被动性。假设并发很高的话,这对服务端是个考验。
    而WebSocket一次握手,持久连接,以及主动推送的特点可以解决上边的问题,又不至于损耗性能。
  1. AJAX轮询
  • AJAX轮询也就是定时发送请求,也就是普通的客户端与服务端通信过程,只不过是无限循环发送,这样,可以保证服务端一旦有最新消息,就可以被客户端获取。
  1. Long Polling长轮询
  • Long Polling长轮询是客户端和浏览器保持一个长连接,等服务端有消息返回,断开。
    然后再重新连接,也是个循环的过程,无穷尽也。。。
    客户端发起一个Long Polling,服务端如果没有数据要返回的话,
    会hold住请求,等到有数据,就会返回给客户端。客户端又会再次发起一次Long Polling,再重复一次上面的过程。

三、WebSocket特点

  • 最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
    ![websocket](/yifu-study-front-share/images/websocket/WebSocket1.png)
  • 建立在 TCP 协议之上,服务器端的实现比较容易。
  • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。


729e0f4ce93a4c4

四、WebSocket的API

1、WebSocket构造函数: 新建WebSocket实例后,会创建客户端与服务端的WebSocket连接

jsx
let ws = new WebSocket(url)

2、 WebSocket实例的当前状态:

jsx
ws.readyState 
// 0: 'CONNECTING',表示正在连接
// 1: 'OPEN', 表示连接成功,可以通信了
// 2: 'CLOSING', 表示连接正在关闭
// 3: 'CLOSED', 表示连接已经关闭,或者打开连接失败。

3、WebSocket实例的事件:

jsx
ws.onopen = function () {
  // 指定连接成功后的回调函数
}         
ws.onclose = function () {
  // 指定连接关闭后的回调函数
}         
ws.onmessage = function () {
  // 指定收到服务器数据后的回调函数
}         
ws.onerror = function () {
  // 指定报错时的回调函数
}

4、WebSocket实例向后台推送消息的方法:

jsx
ws.send(message)  向服务器发送数据
ws.close()  关闭当前连接

五、WebSocket的解读

1、申请一个WebSocket对象,参数是需要连接的服务器端的地址,同http协议使用http://开头一样,WebSocket协议的URL使用ws://开头,另外安全的WebSocket协议使用wss://开头。

2、WebSocket对象一共支持四个消息 onopen, onmessage, onclose和onerror,
所有的操作都是采用消息的方式触发的,这样就不会阻塞UI,使得UI有更快的响应时间,得到更好的用户体验。
当Browser接收到WebSocketServer发送过来的数据时,就会触发onmessage消息,参数evt中包含server传输过来的数据;

jsx
ws.onmessage = function(evt) {
};

3、WebSocket与TCP、HTTP的关系WebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议,Web开发者调用的WebSocket的send函数在browser的实现中最终都是通过TCP的系统接口进行传输的。

4、WebSocket和Http协议一样都属于应用层的协议,那么他们之间有没有什么关系呢?答案是肯定的,WebSocket在建立握手连接时,数据是通过http协议传输的,但是在建立连接之后,真正的数据传输阶段是不需要http协议参与的

f39ca9160c4c531

六、WebSocket通讯详细解读

d29a5d8d9dd0705

从图可以明显的看到,分三个阶段:

  • 打开握手
  • 数据传递
  • 关闭握手