原理第六篇

  

前端

SPA

三大框架解决了jquery开发的问题

通过虚拟DOM显示,使前端不用再操纵 DOM,数据驱动,通过数据的改变直接改变 DOM

渐进式框架的概念

前端框架特别多,那么为什么要有框架呢?我个人的看法是,框架的存在是为了帮助我们应对复杂度。当我们需要解决一些前端上工程问题的时候,这些问题会有不同的复杂度。如果你用太简陋的工具应对非常复杂的需求,就会极大地影响你的生产力。所以,框架本身是帮我们把一些重复的并且已经受过验证的模式,抽象到一个已经帮你设计好的API封装当中,帮助我们去应对这些复杂的问题。

但是,框架本身也会带来复杂度。相信大家在调研各种框架或学习各种框架时,会遇到学习曲线问题——有些框架会让人一时不知如何上手。

工具的复杂度是可以理解为是我们为了处理问题内在复杂度所做的投资。为什么叫投资?那是因为如果投的太少,就起不到规模的效应,不会有合理的回报。这就像创业公司拿风投,投多少是很重要的问题。如果要解决的问题本身是非常复杂的,那么你用一个过于简陋的工具应付它,就会遇到工具太弱而使得生产力受影响的问题。

反之,是如果所要解决的问题并不复杂,但你却用了很复杂的框架,那么就相当于杀鸡用牛刀,会遇到工具复杂度所带来的副作用,不仅会失去工具本身所带来优势,还会增加各种问题,例如培训成本、上手成本,以及实际开发效率等。

vue的设计理念包含声明式渲染,组件系统、客户端路由、大规模状态管理和构建工具五大类,但是你并不需要一上手就把所有东西全用上,因为没有必要。无论从学习角度,还是实际情况,这都是可选的。声明式渲染和组建系统是Vue的核心库所包含内容,而客户端路由、状态管理、构建工具都有专门解决方案。这些解决方案相互独立,你可以在核心的基础上任意选用其他的部件,不一定要全部整合在一起。

spa、seo与ssr

Spa是single page application,在传统的网站中,不同的页面之间的切换都是直接从服务器加载一整个新的页面,而在SPA这个模型中,是通过动态地重写页面的部分与用户交互,而避免了过多的数据交换,响应速度自然相对更高。

Spa的优点:

  • 页面之间的切换非常快
  • 一定程度上减少了后端服务器的压力(不用管页面逻辑和渲染)
  • 后端程序只需要提供API,完全不用管客户端到底是Web界面还是手机等

Seo是Search Engine Optimization,搜索引擎优化,SEO是一种通过了解搜索引擎的运作规则(如何抓取网站页面,如何索引以及如何根据特定的关键字展现搜索结果排序等)来调整网站,以提高该网站在搜索引擎中某些关键词的搜索结果排名。

目前而言,部分搜索引擎如Google、bing等,它们的爬虫虽然已经支持执行JS甚至是通过AJAX获取数据了,但是对于异步数据的支持也还不足(也可能是搜索引擎提供商觉得没必要),Vue SSR中是这样说的:如果你的应用程序初始展示 loading 菊花图,然后通过 Ajax 获取内容,抓取工具并不会等待异步完成后再行抓取页面内容。

SPA应用中,通常通过AJAX获取数据,而这里就难以保证我们的页面能被搜索引擎正常收录到。并且有一些搜索引擎不支持执行JS和通过AJAX获取数据,那就更不用提SEO了。

对于有些网站而言,SEO显得至关重要,此时传统的spa与seo冲突

ssr是Server-Side Rendering(服务器端渲染)的缩写,在普通的SPA中,一般是将框架及网站页面代码发送到浏览器,然后在浏览器中生成和操作DOM(这里也是第一次访问SPA网站在同等带宽及网络延迟下比传统的在后端生成HTML发送到浏览器要更慢的主要原因),但其实也可以将SPA应用打包到服务器上,在服务器上渲染出HTML,发送到浏览器,这样的HTML页面还不具备交互能力,所以还需要与SPA框架配合,在浏览器上“混合”成可交互的应用程序。所以,只要能合理地运用SSR技术,不仅能一定程度上解决首屏慢的问题,还能获得更好的SEO。

ssr的优点:

  • 更快的响应时间,不用等待所有的JS都下载完成,浏览器便能显示比较完整的页面了。这个个人深有体会,我的博客最开始仅仅使用了Vue.js,而没有做服务端渲染,加之服务器不在大陆,第一次输入地址到看到完整的页面几乎是过了4、5秒,有时候还更长。
  • 更好的Seo,我们可以将SEO的关键信息直接在后台就渲染成HTML,而保证搜索引擎的爬虫都能爬取到关键数据。

ssr的缺点:

  • 相对于仅仅需要提供静态文件的服务器,SSR中使用的渲染程序自然会占用更多的CPU和内存资源
  • API可能无法正常使用,比如windowdocmentalert等,如果使用的话需要对运行的环境加以判断
  • 开发调试会有一些麻烦,因为涉及了浏览器及服务器,对于SPA的一些组件的生命周期的管理会变得复杂
  • 可能会由于某些因素导致服务器端渲染的结果与浏览器端的结果不一致。

虚拟dom与diff算法的原理

virtual DOM是对DOM的抽象,本质上是JavaScript对象,这个对象就是更加轻量级的对DOM的描述.

为什么需要虚拟dom

前端性能优化的一个秘诀就是尽可能少地操作DOM,不仅仅是DOM相对较慢,更因为频繁变动DOM会造成浏览器的回流或者重回,这些都是性能的杀手,因此我们需要这一层抽象,在patch过程中尽可能地一次性将差异更新到DOM中,这样保证了DOM不会出现性能很差的情况.

现代前端框架的一个基本要求就是无须手动操作DOM,一方面是因为手动操作DOM无法保证程序性能,多人协作的项目中如果review不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM操作可以大大提高开发效率.

Virtual DOM最初的目的,就是更好的跨平台,比如Node.js就没有DOM,如果想实现SSR(服务端渲染),那么一个方式就是借助Virtual DOM,因为Virtual DOM本身是JavaScript对象.

虚拟dom的优点

1.保证性能下限,在不需要手动优化的情况下依然提供过得去的性能

框架的虚拟DOM适配上层api可能的操作,但是比起粗暴的DOM操作会好很多。因为js的运行引擎和渲染引擎在浏览器中是两个线程。使用js去操作DOM时,本质上是JS引擎和渲染之间进行跨线程通信,每一次操作DOM都会跨线程通信,同时渲染进程和JS线程会相互阻塞。操作的次数多了就会产生性能问题

2.跨平台

虚拟DOM本质上是javaScript对象,而DOM与平台强相关。虚拟DOM最大的优势就在于抽象了原本的渲染过程,实现了跨平台的能力。而不局限于浏览器的DOM,也可以是安卓和IOS的原生组件,可以是近期很火的小程序,也可以是各种GUI

虚拟DOM是js对象,包含了tag、props、children三个属性

<div id="app">
  <p class="text">hello world</p>
</div>
//对于虚拟dom树
{
  tag: 'div',
	props: {
		id: 'app'
	},
	children: [
		{
			tag: 'p',
			props: {
				className; 'text'
			},
			children: [
				'hello world'
			]
		}
	]
}

虚拟DOM提升性能在于DOM发生变化时,通过diff算法比对js原生对象,计算出需要变更的dom,让只对变化的dom进行操作,而不是更新整个视图。

diff算法

diff算法是用来对比差异的算法,其他系统如有 linux命令 diff(我们dos命令中执行diff 两个文件可以比较出两个文件的不同)、git命令git diff、可视化diff(github、gitlab...)等各种实现。

vdom使用diff算法是为了找出需要更新的节点。vdom使用diff算法来比对两个虚拟dom的差异,以最小的代价比对2颗树的差异,在前一个颗树的基础上生成最小操作树,但是这个算法的时间复杂度为n的三次方=O(nnn),当树的节点较多时,这个算法的时间代价会导致算法几乎无法工作。

diff的算法的工作原则:

1.同级节点的比较,不能跨级

2.先序深度优先遍历、广度优先遍历

React的diff算法

策略1treediff:web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。

React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较如4.3.1;先序深度循环遍历(如4.3.2);这种改进方案大幅度的降低了算法复杂度。 当进行跨层级的移动操作,React并不是简单的进行移动,而是进行了删除和创建的操作,会影响到React性能。

策略2component diff:拥有相同类型的两个组件将会生成相似的树形结构,拥有不同类型的两个组件将会生成不同树形结构。

这里指的是函数组件和类组件(如案例2.2.2和2.2.3),比较流程:

1、比较组件是否为同一类型;不是 则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。

2、是同一类型的组件: 按照原策略继续比较 virtual DOM tree 。

策略3elementdiff:对于同一层级的一组子节点,他们可以通过唯一key进行区分。

当节点处于同一层级时, React diff 提供了三种节点操作,我们可以给不同类型定义区分规则。

INSERT(插入)、MOVE(移动)和 REMOVE(删除)。

1、INSERT:表示新的虚拟dom的类型不在老集合(我们会生成一个针对老的虚拟dom的key-index的集合)里,说明这个节点时新的,需要对新节点进行插入操作。 2、MOVE:表示在老的集合中存在,我们这个时候要比较上一次保存的比较索引跟这个老的节点的本身索引比,且element是可更新的类型,这时候就需要做移动操作,可以复用以前的DOM节点 3、REMOVE:旧组件类型,在新集合里也有,但对应的element不同则不能直接复用和更新,需要执行删除操作,或者旧组件不在新集合里的,也需要执行删除操作

MVVM与MCV、MVP

MVC是model、view、controller,

MVVM是model、view、Viewmodel

MVP是model、view、presenter

MVVM优点:低耦合、可重用性、独立开发

区别:

controller改变为viewmodel,通过数据来显示图层而不是节点操作,解决了mvc中大量的dom操作使得页面渲染性能降低,加载速度变慢,影响用户体验

MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过 Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。

而 MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。 唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。这样开发者就不用处理接收事件和View更新的工作,框架已经帮你做好了。

vue与react区别

二者都是使用虚拟dom,虚拟dom比更改原生dom渲染更快

都鼓励组件化,功能分离

都有自己的构建工具,react使用create react app,vue使用vue-cli

vue使用类似于html的模板,react采用jsx语法,倾向于函数式编程

双向绑定与单向绑定

表单的双向绑定,说到底是 (value 的单向绑定 + onChange 事件侦听)的一个语法糖,你如果不想用 v-model,像 React 那样处理也是完全可以的。

在 React 应用中,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。当然,这可以通过shouldComponentUpdate这个生命周期方法来进行控制purerender,但Vue将此视为默认的优化。

vue中实现数据绑定靠的是数据劫持(Object.defineProperty())+发布-订阅模式。在 Vue 应用中,组件的依赖是在渲染过程中自动追踪的,所以系统能精确知晓哪个组件需要被重渲染。你可以理解为每一个组件都已经自动获得了shouldComponentUpdate,并且没有上述的子树问题限制。

vue中双向绑定是使用v-model实现的,单向绑定使用v-bind、v-on事件绑定或者插值形式,v-model是v-on和v-bind结合的语法糖

angular与vue相同

单项数据绑定的优缺点:

优点:所有的状态变化都可以被记录,跟踪,状态变化通过手动调用触发,源头易追溯

缺点:会有很多类似的样板代码,代码量会相应的上升

双向数据绑定的优缺点:

优点:在操作表单时方便简单,省略繁琐的绑定事件

缺点:属于暗箱操作,无法很好地追踪数据

双向数据流与单向数据流

组件间的数据传递,Vue 默认是单向的,和 React 一样。Angular相同

数据流与数据绑定是两个概念

vue和react视图更新机制

vue的订阅式机制决定了它不仅知道哪些数据发生了更新,也知道数据更新之后哪些组件的视图需要重新渲染。这是通过依赖收集实现的。vue的视图template会编译成render函数,在数据中定义了getter,每次调用各个组件的render函数时,通过getter就知道哪些组件的视图所依赖,下一次对这些数据赋值时,就是调用setter,相应的视图就能触发重渲染,而无关的组件则不调用render函数,节省了开销

react中则是通过setState触发视图更新。组件的props或者state变化时,会触发shouldComponentUpdate,也可以使用forceUpdate强制触发。

vue与reactdiff算法区别

相同点:

1.vue和react的diff算法,都是忽略跨级比较,只做同级比较,所以复杂度都为O(n),降低了算法复杂度。

2.都使用key比较是否是相同节点,都是为了尽可能复用节点

3.都是操作虚拟DOM,最小化操作真实DOM,提高性能

4.都不要使用index作为key

vue diff时调动patch函数,参数是vnode和oldVnode,分别代表新旧节点。

不同点:

1.vue比对节点,当节点元素类型相同,但是className不同,任务是不同类型元素,删除重建,而react会认为是同类型节点,只是修改节点属性

2.遍历原理:vue的列表比对,采用从两端到中间的比对方式,而react则采用从左到右依次比对的方式。当一个集合,只是把最后一个节点移动到了第一个,react会把前面的节点依次移动,而vue只会把最后一个节点移动到第一个。总体上,vue的对比方式更高效。

3.更新DOM逻辑:Vue基于snabbdom库,它有较好的速度和模块机制。vue diff使用双向链表,边对比边更新dom。react主要使用diff队列保存需要更新哪些dom,得到patch树,在统一操作批量更新DOM

react hook与vue3 Hook

react hook的限制:

  1. 不要在循环,条件或嵌套函数中调用 Hook
  2. 确保总是在你的 React 函数的最顶层调用他们。
  3. 遵守这条规则,你就能确保 Hook 在每一次渲染中都按照同样的顺序被调用。这让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确。

Vue3 hook的区别:

1.与 React Hooks 相同级别的逻辑组合功能,但有一些重要的区别。 与 React Hook 不同,setup 函数仅被调用一次,这在性能上比较占优。

2.对调用顺序没什么要求,每次渲染中不会反复调用 Hook 函数,产生的的 GC 压力较小。

3.不必考虑几乎总是需要 useCallback 的问题,以防止传递函数prop给子组件的引用变化,导致无必要的重新渲染。

4.React Hook 有臭名昭著的闭包陷阱问题(甚至成了一道热门面试题,omg),如果用户忘记传递正确的依赖项数组,useEffect 和 useMemo 可能会捕获过时的变量,这不受此问题的影响。 Vue 的自动依赖关系跟踪确保观察者和计算值始终正确无误。

5.不得不提一句,React Hook 里的「依赖」是需要你去手动声明的,而且官方提供了一个 eslint 插件,这个插件虽然大部分时候挺有用的,但是有时候也特别烦人,需要你手动加一行丑陋的注释去关闭它。

脚手架

cli表示command line interface,命令行界面,以命令行的形式安装和操作gulp或webpakc的工具

能够快速帮我生成新项目的目录模板(Node.js),减少重复工作进而提升我的开发效率和开发的舒适性(webpack)。脚手架是指通过输入简单指令帮助你快速搭建好一个基本环境的工具,

手动实现mvvm双向绑定

使用Object.defineProperty()方法

<input id="input"/>
const data = {};
const input = document.getElementById('input');
let temp
Object.defineProperty(data,'text',{
  set(value){
    input.value = value;
    this.value = value;
    temp = value
  }
  get() {
		return temp
	}
});

Object.defineProperty()方法默认无法支持数组的变化,vue2.0中通过技巧实现了8种数组方法的变化,分别是push、pop、shift、unshift、splice、sort、reverse。

使用proxy方法。proxy是defineproperty的升级版,直接劫持整个对象而非劫持某个属性,使得操作便利程度和底层功能都强于前者

vue3.0使用

优化seo、spa架构优化

  • 合理的title、description、keywords:
  • 语义化的html代码:语义化代码让搜索引擎容易理解网页
  • 重要内容html代码放在前面:搜索引擎抓取html顺序是从上到下,保证重要内容被抓取
  • 重要内容不用js输出:爬虫不会执行js获取内容
  • 少用frame:搜索引擎不会抓取iframe中的内容
  • 非装饰性图片必须加alt
  • 提高网站速度:网站速度上搜索引擎排序的重要指标
  • 友情链接:好的友情链接能够提高你的网站权重
  • 外链。高质量的外链会给你的网站提高源源不断的权重提升
  • 向各大搜索引擎提交未收录站点

react组件通信方式的使用场景、优缺点

1.props传递:适用于简单的父子组件、兄弟组件传值,不适用于处理复杂的数据流,需要层层传递,具体来说:

  • 1、中间作为桥梁的组件会引入很多不属于自己的属性
  • 2、短时间内,给开发者带来庞大的工作量和代码量。拉长时间看,整个项目的维护成本也变得昂贵

    2.利用发布-订阅模式实现通信:适用于任意组件间的通信。只要在同一个上下文里,监听事件的位置和触发事件的位置是不受限的,这个特性,恰好解决了跨层级通信的问题,适用任意组件间的通信。

缺点:不适合复杂庞大的项目,当组件之间关系复杂之后,就会形成一种网状的依赖结构,在这种结构下,暂且不说能不能理清它们之间的关系,光是可能出现的环形依赖所造成的死循环,就已经让开发者抓狂

3.redux:

4.context:React16下的Context API能够跨越层级,换句话说就是组件树内人人都可消费数据。其设计目的是为了共享那些对于一个组件树而言是“全局”的数据,类似主题、身份认证等

使用场景:组件树中有不同层级的组件需要访问同样的一批数据,即共享的数据 管理当前的 locale,theme,或者一些缓存数据

前端路由的痛点、优缺点

前端路由的出现,是要帮助我们在仅有一个页面的情况下,“记住”用户当前走到了哪一步。因此,它需要为SPA(单页面应用)中的各个视图配置一个唯一标识,这意味着用户前进、后退触发的内容,都会映射到不同的URL上去。此时即便刷新页面,内容也不会消失,因为当前的URL已经有能力识别到它所处的位置了。

SPA下的两大痛点:

  • 当用户刷新页面时,浏览器会默认根据当前URL对资源重新定位(回到最初的状态) - 这个动作不仅使得用户的前进后退操作无法被记录,而且它是不必要的,你想想:SPA作为单页面,本身也只会有一个资源与之对应
  • SPA对服务端而言,就是一个URL、一个资源。如何将其改变为“多个URL” 映射多个视图内容呢?

解决思路:

1.拦截用户的刷新操作,避免服务端盲目响应,返回不符合预期的资源内容,把刷新这个动作完全放到前端逻辑里消化掉

2.感知URL的变化。前端给URL做些不会影响其本身性质的微小处理,不会影响服务器对它的识别,只有前端能感知到,进而通过JS去生成不同的内容

hash路由和history路由的区别

1.hash路由在地址栏URL上有#,而history路由没有会好看一点

2.进行回车刷新操作,hash路由会加载到地址栏对应的页面,而history路由一般就404报错了(刷新是网络请求,没有后端准备时会报错)。

3.hash路由支持低版本的浏览器,而history路由是HTML5新增的API。

4.hash的特点在于它虽然出现在了URL中,但是不包括在http请求中,所以对于后端是没有一点影响的,所以改变hash不会重新加载页面,所以这也是单页面应用的必备。

5.history运用了浏览器的历史记录栈,之前有back,forward,go方法,之后在HTML5中新增了pushState()和replaceState()方法(需要特定浏览器的支持),它们提供了对历史记录进行修改的功能,不过在进行修改时,虽然改变了当前的URL,但是浏览器不会马上向后端发送请求。

浏览器

浏览器跨Tab通信

同源页面的方式很多:

父页面打开的子页面可以通过window.open过去句柄,通过postMessage完成通信

//parent.html
const childPage = window.open('child.html','child')

childPage.onload = () => {
  childPage.postMessage('hello',location.origin)
}

//child.html
window.onmessage = evt => {
       //evt.data
}

sharedWorker

//a.html
var sharedworker = new ShareWorker("worker.js")
sharedworker.port.start()
sharedworker.port.onmessage = evt => {
  //evt.data
}

//b.html
var sharedworker = new SharedWorker('worker.js')
sharedworker.port.start()
sharedworker.port.postMessage('hello')

//work.js
const ports = []
onconnect = e => {
  const port = e.ports[0]
  ports.push(port)
  port.onmessage = evt => {
    ports.filter(v => v!== port) 
    .forEach(p => p.postMessage(evt.data))
  }
}

BroadcastChannel

//a.html
const channel = new BroadcastChannel('tabs')

channel.onmessage = evt =>{
     //evt.data
}
//b.html
const channel = new BroadcastChannel('tabs')
channel.postMessage('hello')

Localstorage

//a.html
localStorage.setItem('message','hello')

//b.html
window.onstorage = evt => {
  //evt.key
}

cookie

websocket

对于非同源页面,可以使用嵌入同源iframe作为桥,将非同源页面通信转为同源页面通信

由于iframe与父页面之间可以指定origin而忽略同源限制,因此在每个页面嵌入一个iframe(例如bridge.html),而这些iframe由于使用一个url,因此使用同源页面。

//Tab A
sendMessageToTab({
  type:'ASays',
  data:'hello world,B'
})

window.sendMessageToTab = function(data){
  document.querySelector('#J_bridge').contentWindow.postMessage(JSON.stringfy(data),'/');
}

//接收来自iframe的tab消息
window.addEventListener('message',function(e){
  let {data,source,origin} = e;
  if(!data)
      return;
  try{
    let info = JSON.parse(JSON.parse(data));
    if(info.type == 'BSays'){
       console.log('BSays',info);
    }
  }catch(e){
  }
})
//bridge.html
function message_broadcast(message){
   localStorage.setItem('message',JSON.stringfy(message));
   localStorage.removeItem('message')
}

window.addEventListener("storage",function(ev){
   if(ev.key == 'message'){
     window.parent.postMessage(ev.newValue,'*')
   }
})

window.addEventListener('message',function(e){
  let {data,source,origin} = e;
  //接收到父文档的消息后广播给其他同源页面
  message_broadcast(data);
})
//Tab B
window.addEventListener('message',function(e){
  let {data,source,origin} = e;
  if(!data)
      return;
  let info = JSON.parse(JSON.parse(data);
  if(info.type == "Asays"){
    docment.querySelector('#J_bridge').contentWindow.poseMessage(JSON.stringify({
    type:'Bsays',
    data: 'echo from B'
  }),'*');
  }
})

document.querySelector('button').addEventListener('click',function(){
  docment.querySelector('#J_bridge').contentWindow.poseMessage(JSON.stringify({
    type:'Bsays',
    data: 'I am B'
  }),'*');
})

浏览器缓存

浏览器缓存是浏览器对之前请求过的文件进行缓存,以便下一次访问时重复使用,节省带宽,提高访问速度,降低服务器压力。http缓存机制主要在http响应头中设定,响应头中相关字段为Expires、Cache-Control、Last-Modified、Etag。

简而言之,就是服务端告诉浏览器在约定的这个时间前,可以直接从缓存中获取资源(representations),而无需跑到服务器去获取。

浏览器缓存分为强缓存和协商缓存

如果是强缓存,浏览器不会向服务端发送任何请求,直接从本地缓存读取文件并返回status code200

协商缓存时,浏览器会向服务器发送请求,向服务器发送请求,服务器会根据这个请求的request header的一些参数来判断是否命中协商缓存,如果命中,则返回304状态码并带上新的response header通知浏览器从缓存中读取资源;

强缓存设置的header参数

Expires:过期时间,如果设置了时间,则浏览器会在设置的时间内直接读取缓存,不再请求

Cache-Control:当值设为max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的5分钟内再次加载资源,就会命中强缓存。

cache-control是http1.1的头字段,expires是http1.0的头字段,如果expires和cache-control同时存在,cache-control会覆盖expires,建议两个都写。

协商缓存设置header参数

Etag是属于HTTP 1.1属性,它是由服务器(Apache或者其他工具)生成返回给前端,用来帮助服务器控制Web端的缓存验证。 Apache中,ETag的值,默认是对文件的索引节(INode),大小(Size)和最后修改时间(MTime)进行Hash后得到的。

当资源过期时,浏览器发现响应头里有Etag,则再次像服务器请求时带上请求头if-none-match(值是Etag的值)。服务器收到请求进行比对,决定返回200或304。

Last-Modified:浏览器向服务器发送资源最后的修改时间

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

两种方式对比

Last-Modifed/If-Modified-Since的时间精度是秒,而Etag可以更精确。

Etag优先级是高于Last-Modifed的,所以服务器会优先验证Etag

Last-Modifed/If-Modified-Since是http1.0的头字段

性能优化

首批加载时间,srr服务端渲染,pwa缓存,懒加载(异步加载import函数,图片滚动渲染),预加载(dns缓存,preload),http请求合并。

浏览器进程与线程

浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程(也不一定,因为多个空白 tab 标签会合并成一个进程),浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种。

在 Chrome 多进程架构里,它包括了四个进程:

  • Browser进程(负责地址栏、书签栏、前进后退、网络请求、文件访问等)
  • Renderer进程(负责一个Tab内所有和网页渲染有关的所有事情,是最核心的进程
  • GPU进程(负责GPU相关的任务)
  • Plugin进程(负责Chrome插件相关的任务)

一个标签页就是一个进程,甚至一个扩展程序就是一个进程!在浏览器中打开一个网页就相当于新开了一个进程。

但是:在这里浏览器有自己的优化机制,有时候打开多个标签页,进程会合并,所以每一个标签页对应一个进程不是绝对的。

这样的多进程分配的好处是:

  • 如果一个页面挂了,不会影响其他页面,甚至影响到整个浏览器
  • 避免安装的三方插件等影响了浏览器全局
  • 多进程充分利用了多核的优势
  • 把插件,扩展程序等全部隔离,提高稳定性

当然,缺点很就明显了,内存消耗大,确实有点像空间换时间的意思。

渲染进程内将要工作的线程:

  • 主线程(Main thread):下载资源、执行js、计算样式、进行布局、绘制合成
  • 光栅线程(Raster thread)
  • 合成线程(Compositor thread)
  • 工作线程(Worker thread)

渲染过程中,其实就是这四个进程的互相配合,我们一起来看下吧。

  1. 当渲染过程接收提交消息用于导航和开始接收HTML数据,主线程开始解析文本串(HTML),使之成为一个 Document Object Model ,也就是 DOM
  2. 网站有用到图片,CSS 和JavaScript的话,这些东西需要从网络或者缓存中加载,主线程可以边请求,边预加载构建DOM。
  3. 当HTML解析器找到 <script> 标签后,将会暂停HTML解析,并且必须加载、解析和执行 JavaScript的代码。为什么?因为JavaScript 可以使用诸如 document.write() 更改整个DOM结构!所以开发人员在写代码的时候可以在<script> 标签上加 async 或者 defer 属性。然后浏览器将会异步加载并运行JavaScript,不会阻止解析。
  4. 主线程解析CSS样式,并把CSS样式一一对应到DOM节点上,注意,此时CSS页面还没有生效,只是样式和节点绑定了关系。
  5. 接下来CSS根据DOM节点,会生成类似于DOM结构的一个布局树,仅包含了页面上可见内容的信息,如果有 display: none 等,则该元素不属于布局树。如果有 p::before {content:"123"} 等伪类的存在,就算它不在DOM中,也会包含在布局树中。
  6. 在此绘制步骤中,主线程遍历布局树以创建绘制记录。绘画记录是绘画过程的注释,例如“先是背景,然后是文本,然后是矩形”,类似 canvas。 注意:在渲染的时候,每个步骤前面操作的结果都用于创建新数据,如果布局树发生了更改,文档受影响的部分就会重新绘制,也就是 重绘,开发过程中要尽量避免这一现象。
  7. 至此浏览器知道了:文档的结构,每个DOM元素的样式,页面的几何形状以及绘制的顺序。把这些东西换转为屏幕上像素我们称之为 光栅化。在现代浏览器中执行这一行为的过程,称为 合成(Compositing),就是把页面各个部分分成若干层,分别进行栅格化,然后合成器线程的单独线程中进行合成,一个层可以称之为一个 layer。
  8. 层分好了并确定了顺序之后,主线程就把这个信息提交给合成线程,然后合成器线程把每个图层栅格化,发送给栅格线程,栅格线程把它们存储在GPU内存内。
  9. 最终,合成线程将栅格化的块合成帧,并通过IPC传递给浏览器进程,显示在屏幕上。

https://developer.aliyun.com/article/946694

浏览器是事件驱动的(Event driven),浏览器中很多行为是异步(Asynchronized)的,会创建事件并放入执行队列中。浏览器中很多异步行为都是由浏览器新开一个线程去完成,一个浏览器内核进程中至少实现三个常驻线程:JS引擎线程、GUI渲染线程、事件触发线程

GUI 渲染线程:

  • 负责渲染页面,解析 HTML,CSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程。
  • 和 JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行。

事件触发线程:

  • 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理。

JS引擎线程:

  • 单线程工作,负责解析运行 JavaScript 脚本。
  • 和 GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞。

其他线程如定时器触发线程

  • 浏览器定时计数器并不是由 JS 引擎计数的,阻塞会导致计时不准确。
  • 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理。

异步http 请求线程:

  • http 请求的时候会开启一条请求线程,负责异步请求一类函数的线程,如promise、axios等。
  • 请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理。

浏览器事件循环

JavaScript 事件循环机制分为浏览器和 Node 事件循环机制,两者的实现技术不一样,浏览器 Event Loop 是 HTML 中定义的规范,Node Event Loop 是由 libuv 库实现。这里主要讲的是浏览器部分。

JavaScript引擎是一个专门处理JavaScript脚本的虚拟机,最出名的就是Chrome浏览器的V8引擎,JS引擎有两个组件:堆(内存分配)、栈(函数调用时会形成一个个栈帧)

在程序中设置两个线程:一个负责程序本身的运行,称为"主线程";另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为"Event Loop线程"(可以译为"事件循环线程"),Event Loop是由javascript宿主环境(像浏览器)来实现的。

事件循环的具体内容为:

函数先入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空;

在此期间WebAPIs完成这个事件,把回调函数放入CallbackQueue中等待;

当执行栈为空时,Event Loop把Callback Queue中的一个任务放入Stack中,回到第1步

WebAPIs是由C++实现的浏览器创建的线程,处理诸如DOM事件、http请求、定时器等异步事件;

一个event loop有一个或者多个task队列,只有一个microtask 队列。task任务源非常宽泛,比如ajax的onload,click事件,基本上我们经常绑定的各种事件都是task任务源,还有数据库操作,setTimeout、setInterval、setImmediate,I/O,渲染UI,mcirotask包括promise、process.nextTick、MutationObserver、Object.observe

对于宏任务和微任务,JavaScript 引擎执行时首先会把整个 script 代码当成一个宏任务执行,执行完成之后,再检测本次循环中是否寻在微任务,存在的话就依次从微任务的任务队列中读取执行完所有的微任务,再读取宏任务的任务队列中的任务执行,再执行所有的微任务,如此循环。JS 的执行顺序就是每次事件循环中的宏任务-微任务。

https://segmentfault.com/a/1190000015559210

浏览器内核

浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是JS引擎。渲染引擎在不同的浏览器中也不是都相同的。

主流浏览器:Firefox、Google Chrome、Safari、Windows Edge、Opera

国内浏览器:百度、搜狗、傲游、QQ、2345、360、UC、猎豹

浏览器的内核

Opera、Google Chrome:Webkit/Blink

Firfox:Gecko

Safari:webkit

Windows Edge:EdgeHTML/Trident

使用的Trident单核,如:2345、世界之窗;

使用Trident+Webkit/Blink双核,如:qq、UC、猎豹、360、百度;

使用Webkit/Blink单核,如:搜狗、遨游。

双核浏览器通过WebKit内核来访问一些不需要进行网上交易的网站,使用起来速度更快更方便;双核浏览器在进行支付系统或者是网上银行的访问时,则使用的是Trident内核。这就是双核浏览器的高速模式和兼容模式。

浏览器渲染html过程(超详细

  1. 解析HTML,生成DOM树
  2. 解析CSS,生成CSSOM树
  3. 将DOM树和CSSOM树结合,生成渲染树(Render Tree)

4.Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)

5.Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素

6.Display: 将像素发送给GPU,最后通过调用操作系统Native GUI的API绘制,展示在页面上。(这一步其实还有很多内容,比如会在GPU将多个合成层合并为同一个层,并展示在页面中。而css3硬件加速的原理则是新建合成层,这里我们不展开,之后有机会再写一篇博客来介绍)

https://347830076.github.io/myBlog/javascript/%E6%B5%8F%E8%A7%88%E5%99%A8%E6%B8%B2%E6%9F%93%E5%8E%9F%E7%90%86%E6%B5%81%E7%A8%8B.html#%E6%9E%84%E5%BB%BAdom%E8%AF%A6%E7%BB%86%E6%B5%81%E7%A8%8B

如果你觉得我的文章对你有帮助的话,希望可以推荐和交流一下。欢迎關注和 Star 本博客或者关注我的 Github