``python
]]>跟着代码随想录刷了一遍,很多细节还是似懂非懂,囫囵吞枣,故此要刷几遍。
借此机会也多做做笔记,好记性不如烂笔头~
数组是存放在连续内存空间上的相同类型数据的集合。通过下标索引的方式很方便获取到下标下对应的数据。
数组的元素是不能删的,只能覆盖。
]]>探讨一些经典和现代的设计模式的JavaScript实现。
没有最好的,只有最合适的
设计模式一共分为3大类23种
模式类型 | 设计模式 |
---|---|
创建型模式 | 单例模式、工厂模式、建造者模式 |
结构型模式 | 适配器模式、装饰器模式、代理模式 |
行为型模式 | 策略模式、观察者模式、发布订阅模式、职责链模式、中介者模式 |
一个类只有一个实例,并提供一个访问他的全局访问点
Singleton
:特定类,这是我们需要访问的类,访问者要拿到的是它的实例;
instance
:单例,是特定类的实例,特定类一般会提供 getInstance
方法来获取该单例;
getInstance
:获取单例的方法;
1 | class Singleton { |
Vuex 实现了一个全局的store用来存储应用的所有状态。这个store的实现就是单例模式的典型应用。
如果一个类实例化过程消耗资源比较多,可以使用单例避免性能浪费
需要公共状态,可以使用单例保证访问一致性。
工厂模式:根据不同的参数,返回不同类的实例。将对象的创建与对象的实现分离。实现复杂,但使用简单。直接使用工厂提供的方法即可
优点:
良好的封装,访问者无需了解创建过程,代码结构清晰。
扩展性良好,通过工厂方法隔离了用户和创建流程,符合开闭原则。
缺点:
给系统增加了抽象性,带来了额外的系统复杂度,不能滥用
document.createElement
创建 DOM
元素。这个方法采用的就是工厂模式,方法内部很复杂,但外部使用很简单。
对象创建比较复杂,访问者无需了解创建过程。
需要处理大量具有相同/类似属性的小对象。
用于解决兼容问题,接口/方法/数据不兼容,将其转换成访问者期望的格式进行使用。
场景特点:
有点原型链的味道
为一个对象提供一个代用品或占位符,以便控制对它的访问
定义一系列算法,根据输入的参数决定使用哪个算法。
场景:双十一满减活动。满200-20、满300-50、满500-100。这个需求,怎么写?
1 | // if-else:臃肿,难改动 |
优点:
if-else
进行策略选择,提高了维护性。缺点:
一个对象(称为subject)维持一系列依赖于它的对象(称为observer),将有关状态的任何变更自动通知给它们(观察者)。
优点:目标变化就会通知观察者,这是观察者模式最大的优点。
缺点: 目标和观察者是耦合在一起的,要实现观察者模式,必须同时引入被观察者和观察者才能达到响应式的效果。
假设B站用户就是观察者,B站up主是被观察者,有多个的B站用户关注了青春湖北这个up主,当这个up主更新视频时就会通知这些关注的B站用户。
基于一个主题,希望接收通知的对象(称为subscriber)通过自定义事件订阅主题,被激活事件的对象(称为publisher)通过发布主题事件的方式被通知。
微信会关注很多公众号,公众号有新文章发布时,就会有消息及时通知我们文章更新了。
这个时候公众号为发布者,用户为订阅者,用户将订阅公众号的事件注册到事件调度中心,当发布者发布新文章时,会发布事件至事件调度中心,调度中心会发消息告诉订阅者。
Vue
双向绑定通过数据劫持和发布-订阅模式实现
通过DefineProperty
劫持各个数据的setter
和getter
,并为每个数据添加一个订阅者列表,这个列表将会记录所有依赖这个数据的组件。
响应式数据相当于消息的发布者。
每个组件都对应一个Watcher
订阅者,当组件渲染函数执行时,会将本组件的Watcher
加入到所依赖的响应式数据的订阅者列表中。
这个过程叫做“依赖收集”。
当响应式数据发生变化时,会出setter
,setter
负责通知数据的订阅者列表中的Watcher
,Watcher
触发组件重新渲染来更新视图。
视图层相当于消息的订阅者。
观察者是经典软件设计模式
中的一种,但发布订阅只是软件架构中的一种消息范式
观察者模式 | 发布订阅 |
---|---|
2个角色 | 3个角色 |
重点是被观察者 | 重点是发布订阅中心 |
观察与被观察的关系是通过被观察者主动
建立的,被观察者
至少要有三个方法——添加观察者、移除观察者、通知观察者。
发布订阅基于一个中心来建立整个体系,其中发布者
和订阅者
不直接进行通信,而是发布者将要发布的消息交由中心管理,订阅者也是根据自己的情况,按需订阅中心中的消息。
发布订阅的实现内部利用了观察者模式
,但由于发布订阅中心
这一中间层的出现,对于生产方和消费方的通信管理变得更加的可管理和可拓展。
Array(3).fill(Array(3).fill(0)) === Array(3).fill(0).map((value) => Array(3).fill(0)) 为什么返回false?
刷Leecode77题中发现的有关push(array)和push([…array])的区别,记录一下。
题目:
给定两个整数 n
和 k
,返回范围 [1, n]
中所有可能的 k
个数的组合。
你可以按 任何顺序 返回答案。
答案:
1 | var combine = function(n, k) { |
我这个答案得出的结果是 [[],[],[],[]….]
在查看别人代码之后,发现了差异,是push(array)和push([…array])的区别。
解析
如何理解宏任务、微任务?
JS 引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环。
JavaScript代码执行顺序:
结论:在异步任务中:微任务先于宏任务
在事件循环中,每进行一次循环操作称为 tick,关键步骤如下:
(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)
浏览器为了能够使得JS内部(macro)task与DOM任务能够有序的执行,会在一个(macro)task执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:
(macro)task->渲染->(macro)task->…
宏任务具体包括:
1 | script(整体代码) |
new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的
在当前 task 执行结束后立即执行的任务。在当前task任务后,下一个task之前,在渲染之前。
在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕
具体包括:
1 | Promise.then |
案例
1 | setTimeout(_ => console.log(4)) |
所以,最终输出结果为:1 > 2 > 3 > 4
区别一:call、apply是立即执行、bind是返回一个新的函数
bind 返回的是一个新的函数,你必须调用它才会被执行
1 | obj.myFun.call(db); // 德玛年龄 99 |
区别二:
call 、bind 、 apply 这三个函数的第一个参数都是 this 的指向对象,第二个参数差别
call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔
apply 的所有参数都必须放在一个数组里面传进去
bind 除了返回是函数以外,它 的参数和 call 一样
1 | obj.myFun.call(db,'成都','上海'); // 德玛 年龄 99 来自 成都去往上海 |
背八股过程中,做一些精简记录,不定期更新。
比较全的前端框架知识 见以前的博文。
一个构建数据驱动的渐进式框架,关于 Vue 的优点,主要有响应式编程、组件化开发、虚拟 DOM
响应式编程:通过 MVVM 思想实现数据的双向绑定
组件化开发:将应用各模块拆分到各组件中,提高开发效率、方便重复使用…
虚拟 DOM:传统开发中,jQuery或原生DOM操作会导致浏览器不断渲染DOM树,性能开销大;虚拟 DOM 将各种操作放在虚拟节点中,计算完毕后才统一提交。
采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为:
Vue 3.0 中采用了 Proxy,抛弃了 Object.defineProperty 方法
判断当前 Reflect.get 的返回值是否为 Object,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。
从本质上来说,Virtual Dom是一个JavaScript对象,通过对象的方式来表示DOM结构。
将页面的状态抽象为JS对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。
通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能。
如果渲染使用真实DOM,由于真实DOM的各项操作会带来大量性能损耗,极大降低渲染效率。使用虚拟 dom ,主要为解决渲染效率的问题。
虚拟DOM保证性能下限,提供过得去的性能
Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。
当组件创建和更新时,vue 均会执行内部的 update 函数,该函数使用 render 函数生成的虚拟 dom 树,将新旧两树进行对比,找到差异点,最终更新到真实 dom
对比差异的过程叫 diff,vue 在内部通过一个叫 patch 的函数完成该过程
在对比时,vue 采用深度优先、同层比较的方式进行比对。
在判断两个节点是否相同时,vue 是通过虚拟节点的 key 和 tag来进行判断
现代前端框架有两种方式侦测变化,一种是 pull,一种是 push。
其代表为 React,我们可以回忆一下 React 是如何侦测到变化的。
我们通常会用 setState API 显式更新,然后 React 会进行一层层的 Virtual Dom Diff 操作找出差异,然后 Patch 到 DOM 上,React 从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的 Diff 操作查找「哪发生变化了」
Vue 的响应式系统则是 push 的代表,当 Vue 程序初始化的时候就会对数据 data 进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知,因此 Vue 是一开始就知道是「在哪发生变化了」
这又会产生一个问题,通常绑定一个数据就需要一个 Watcher,一但我们的绑定细粒度过高就会产生大量的 Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此 Vue 的设计是选择中等细粒度的方案,在组件级别进行 push 侦测的方式
通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行 Virtual Dom Diff 获取更加具体的差异,而 Virtual Dom Diff 则是 pull 操作,Vue 是 push + pull 结合的方式进行变化侦测的。
根本原因是 Vue 与 React 的变化侦测方式有所不同
React 是 pull 的方式侦测变化,当 React 知道发生变化后,会使用 Virtual Dom Diff 进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要用 shouldComponentUpdate 进行手动操作来减少 diff,从而提高程序整体的性能。
Vue 是 pull+push 的方式侦测变化的,在一开始就知道那个组件发生了变化,因此在 push 的阶段并不需要手动控制 diff,而组件内部采用的 diff 方式实际上是可以引入类似于 shouldComponentUpdate 相关生命周期的,但是**通常合理大小的组件不会有过量的 diff,手动优化的价值有限**,因此目前 Vue 并没有考虑引入 shouldComponentUpdate 这种手动优化的生命周期。
给 script
标签附上 setup
属性后,内部将不再通过 export default
抛出方式的语法。template
模板可以直接拿到 script
标签内声明的变量,并且支持响应式
对于 vue 来讲,生命周期就是一个 vue 实例从创建到销毁的过程。
通俗的说,hook 就是在程序运行中,在某个特定的位置,框架的开发者设计好了一个钩子来告诉我们当前程序已经运行到特定的位置了,会触发一个回调函数,并提供给我们,让我们可以在生命周期的特定阶段进行相关业务代码的编写
先看看生命周期的对照表:
Vue 2.0 | Vue3 | |
---|---|---|
beforeCreate | setup() | |
created | setup() | 创建data 和 method |
beforeMount | onBeforeMount | 组件挂载前执行的函数 |
mounted | onMounted | 组件挂载后的函数 |
beforeUpdate | onBeforeUpdate | 更新前 |
updated | onUpdated | 更新后 |
beforeDestroy | onBeforeUnmount | 卸载前 |
destroyed | onUnmounted | 卸载后 |
activated | onActivated | 被 keep-alive 缓存的组件激活时调用 |
deactivated | onDeactivated | 被 keep-alive 缓存的组件停用时调用 |
errorCaptured | onErrorCaptured | 捕获来自子孙组件的异常时激活钩子函数 |
setup
调用的时机是创建组件实例,然后初始化 props
,紧接着就是调用 setup
函数。在 beforeCreate
钩子之前被调用,所以 setup
内是拿不到 this
上下文的。
vue3中,除去 beforeCreate
和 created
之外,有 9 个旧的生命周期钩子,我们可以在 setup
方法中访问
reactive 接收一个 JS 对象 作为参数,返回一个该对象的proxy代理,允许多层嵌套
JS 对象为 对象、数组和 Map
、Set
这样的集合类型,而对 string
、number
和 boolean
这样的 原始类型 无效
不可以随意地“替换”一个响应式对象,不可以将响应式对象的属性赋值或解构
需要使用 scoped 属性 保证组件间不会样式污染,但是有时候需要做样式穿透,有这么几种方式:
ref 的作用是被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。其特点是:
常见的使用场景有:
header组件一般分为 左右中 三部分。中间为主标题,可以通过vue props方式做成可配置向外暴露,左右两侧可以通过 vue slot 插槽的方式对外暴露以实现多样化,同时也可以提供 default slot 默认插槽来统一页面风格。
可以,几种写法:
1 | 写法一: |
DOM 更新是一个异步操作,在数据更新后会首先被 set 钩子监听到,但是不会马上执行 DOM 更新,而是在下一轮循环中执行更新。
具体实现是vue 中实现了一个 queue 队列用于存放本次事件循环中的所有 watcher 更新,并且同一个 watcher 的更新只会被推入队列一次。在本轮事件循环的微任务执行结束后执行此更新,这就是 DOM 只会更新一次的原因。
在下一个的事件循环“tick”中,vue 刷新队列并执行实际 (已去重的) 工作。vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
插槽的作用是子组件提供了可替换模板,父组件可以更换模板的内容。
作用域插槽给了子组件将数据返给父组件的能力,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。本质是子组件可以通过插槽的位置绑定一些数据,让父组件插槽位置可以用这个数据。
vue2 中采用混入(mixin)技术。
vue3 中采用组合式函数,组合式函数(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染;为了性能考虑,Vue 会在本轮数据更新后,再去异步更新视图
异步渲染的原理
nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。
nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列。
有时候,可能遇到这样的情况,DOM1的数据发生了变化,而DOM2需要从DOM1中获取数据,那这时就会发现DOM2的视图并没有更新,这时就需要用到了nextTick
了。
由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick
中。
两种情况用到:
nextTick()
的回调函数中;nextTick()
的回调函数中。因为在created()钩子函数中,页面的DOM还未渲染,没办法操作DOM,所以必须将代码放在nextTick()
的回调函数中两者区别:
v-model 实际上是一个语法糖
1 | <input v-model="searchText"> |
适用于表单元素、组件
如何理解v-model用在组件上,自定义组件和父组件的交互如下:
searchText
变量传入custom-input 组件,使用的 prop 名为value
;input
的事件,父组件将接收到的值赋值给searchText
;在组件切换的时候,保存一些组件的状态防止多次渲染,可以使用 keep-alive 组件包裹需要保存的组件。
#
后面 hash 值的变化,不会导致浏览器向服务器发出请求,浏览器不发出请求,就不会刷新页面;
通过监听 hashchange 事件可以知道 hash 发生了哪些变化,然后根据 hash 变化来实现更新页面部分内容的操作。
history 模式的实现,主要是 HTML5 标准发布的两个 API,pushState 和 replaceState,这两个 API 可以 改变url但又不刷新页面的效果。这样就可以监听 url 变化来实现更新页面部分内容的操作。
两者区别:
延迟加载,即在需要的时候进行加载,随用随载
如果能把不同路由对应的组件分割成不同的代码块,当路由被访问的时候才加载对应组件,这样就更加高效了。
非懒加载:
1 | import List from "@/components/list.vue"; |
懒加载:
1 | const List = () => import("@/components/list.vue"); |
在前端技术早期,一个 url 对应一个页面,如果要从 A 页面切换到 B 页面,那么必然伴随着页面的刷新。这个体验并不好,不过在最初也是无奈之举——用户只有在刷新页面的情况下,才可以重新去请求数据。
Ajax 出现了,它允许人们在不刷新页面的情况下发起请求。与之共生的,还有不刷新页面即可更新页面内容这种需求。在这样的背景下,出现了 SPA(单页面应用)。
在 SPA 诞生之初,在内容切换前后,页面的 URL 都是一样的,这就有两个问题:
为了解决这个问题,前端路由出现了
前端路由可以帮助我们在仅有一个页面的情况下,“记住”用户当前走到了哪一步——为 SPA 中的各个视图匹配一个唯一标识。刷新页面,因为当前的 URL 可以标识出他所处的位置,因此内容也不会丢失。
如何实现呢:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
待续…
]]>现在也尝试着多做一些记录,以此篇记录倒数第二个寒假时光~
每年过年回来和高中同学一起吃饭,聊聊一年里新奇有趣的事,吐槽一下苦闷,不同行业之间交流见闻,是我每次回来都很期待的事情。
今天约了三五好友,湖边散步,聊天说地,晚上一起吃的火锅。
和老同学一起,总是格外的放松,难得的快乐
今天一到家又被家里的WiFi开始折磨。因为小区比较老了,之前光纤埋线只埋了一个端口,导致现在只有爸妈房间可以装路由。
家里空间比较大,墙壁比较厚,导致我房间一直处于信号荒漠区,晚上在家躺床玩手机还用不上wifi,被折磨了四年o_o ….
前两年在客厅装了个tplink的信号增强器,有效果,我姐的房间实现网络覆盖,but我的还在荒漠区(;´༎ຶД༎ຶ`)
前一阵看一篇文章介绍申请公网IP里提到 拨号方式中路由和桥接的不同,这次回家第一件事就是试试这个。
现在大多运营商都是光迁入户,然后通过光猫实现广电转换。现在光猫基本上都有两种模式,路由模式和桥接模式,两种模式的区别如下:
路由模式:使用光猫完成光电转换,并进行拨号,并路由给其他设备,光猫任务量比较大。
桥接模式:光猫只完成光电转换,使用下一级路由器进行拨号,并路由,光猫任务量轻。主要任务是由路由器完成。
因为运营商赠送的光猫一般情况下性能相对较弱,对于家中设备多的折腾一族来说,因为有大量的设备,端口转发等等,光猫基本上不能满足需求。所以大都是选择光猫桥接,路由器进行拨号,最好选一个性能强悍点的路由器。
光猫改桥接有两种方法:一种打客服电话,他们会后台同步,把配置改为桥接;但是我电话打过去了,湖北电信不肯提供这个服务,无奈;另一种就是拿到超级管理员密码,自行修改。关于这个我也打了电话,问他们超级管理员密码,依旧是说没有,反正没有帮上任何忙
尝试了网络上很多方法,得出的结论是大多数帖子都失效了,联通电信移动 又不傻,不会放任一些万能密码的,现在应该都是每台光猫独自的密码,看这种类似的教程还是要看新不看旧;
我是跟着这个视频成功的:电信光猫100%获取超级管理员密码,光猫改桥接,通过小翼管家返回密码_哔哩哔哩_bilibili
主要用到小翼管家 和 一个抓包软件,教程里是httpcanary,我用的是IOS系统,所以自己换成了stream。思路流程还是和视频里一样,这里就不赘述了。
这里额外想提一点:
很多时候,跟着网上的教程做一些配置,不能无脑全称跟,还是要有一些基础知识备着,这样才能在一些环节自己做替换。 比如这次的抓包软件 ,以前的我可能就是借台安卓机,非要一步步全程按照教程来,但最近自己正好在学http请求头相关的浏览器知识,所以知其所以然后就自己找了个 ios的抓包软件,软件里界面啥的都不一样,但是只要自己知道为什么做这一步,很多时候就可以举一反三了。
连接光猫的wifi,通过超级管理员密码进入光猫管理后台,具体设置指南可以看下面这篇文章,讲的也挺详细,但是有两点不要跟:
下图是我的配置截图
路由模式下,光猫既干光电转换,又做进行拨号,任务量比较大;改成桥接模式后光猫只完成光电转换,拨号就交给路由器。
GG的是 我又不知道家里的宽带密码,好在现在的服务越来越好了,直接网上可以查询修改,附上链接
进入路由器后台管理系统,我用的TP-LINK。在上网连接里 使用 宽带上网,使用账号密码,就搞定了。网络测速:测速网 - 专业测网速
搞完明显网络好了很多,上来的流程说来简单,但是也搞了我好几个小时(;´༎ຶД༎ຶ`)。但是这种成功DIY也是超级快乐的。
]]>XSS攻击指的是跨站脚本攻击(Cross-Site Scripting),是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。
XSS 的本质是因为网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
攻击者可以通过XSS攻击进行以下操作:
XSS 可以分为存储型、反射型和 DOM 型:
反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库⾥,反射型 XSS 的恶意代码存在 URL ⾥。
反射型 XSS 漏洞常⻅于通过 URL 传递参数的功能,如⽹站搜索、跳转等。 由于需要⽤户主动打开恶意的 URL 才能⽣效,攻击者往往会结合多种⼿段诱导⽤户点击。
DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执⾏恶意代码由浏览器端完成,属于前端JavaScript ⾃身的安全漏洞,⽽其他两种 XSS 都属于服务端的安全漏洞。
- CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。
- 通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式
CSRF 攻击指的是跨站请求伪造攻击。
CSRF攻击的过程是,用户在浏览器上登录了网站A,并且网站A返回了Cookie信息给浏览器,用户没有退出网站A;然后用户在同一浏览器上访问了网站B,网站B上有一些隐藏的链接或者表单,指向网站A的一些敏感操作;当用户点击这些链接或者表单时,浏览器会自动带上网站A的Cookie信息,发送请求到网站A,网站A认为这是用户的合法请求,从而执行了攻击者想要的操作
CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。
⽹络劫持分为两种:
(1)DNS劫持: (输⼊京东被强制跳转到淘宝这就属于dns劫持)
(2)HTTP劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于http明⽂传输,运营商会修改你的http响应内容(即加⼴告)
DNS劫持由于涉嫌违法,已经被监管起来,现在很少会有DNS劫持,⽽http劫持依然⾮常盛⾏,最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。
进程是资源分配的最小单位,线程是CPU调度的最小单位。
进程放在应用上来说就代表了一个程序;线程是进程中的更小单位,描述了执行一段指令所需的时间。
进程和线程之间的关系:
打开一个网页,最少需要四个进程:1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程。如果打开的页面有运行插件的话,还需要再加上 1 个插件进程。
虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:
GUI渲染线程,渲染浏览器页面
JS引擎线程,负责处理Javascript脚本程序,一个Tab页中无论什么时候都只有一个JS引擎线程在运行JS程序;
时间触发线程,用来控制事件循环;
定时器触发进程即setInterval与setTimeout所在线程;
异步http请求线程,XMLHttpRequest连接后通过浏览器新开一个线程请求;
检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将回调函数放入事件队列中,等待JS引擎空闲后执行;
GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
管道通信
管道就是操作系统在内核中开辟的一段缓冲区,进程1可以将需要交互的数据拷贝到这段缓冲区,进程2就可以读取了。
所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。
系统中的资源可以分为两类:
很多网站的资源后面都加了版本号,这样做的目的是:每次升级了 JS 或 CSS 文件后,为了防止浏览器进行缓存,强制改变版本号,客户端浏览器就会重新下载新的 JS 或 CSS 文件 ,以保证用户能够及时获得网站的最新更新。
Service Worker 运行在 JavaScript 主线程之外,虽然由于脱离了浏览器窗体无法直接访问 DOM,但是它可以完成离线缓存、消息推送、网络代理等功能。它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。当 Service Worker 没有命中缓存的时候,需要去调用 fetch
函数获取 数据。也就是说,如果没有在 Service Worker 命中缓存,会根据缓存查找优先级去查找数据。但是不管是从 Memory Cache 中还是从网络请求中获取的数据,浏览器都会显示是从 Service Worker 中获取的内容。
Memory Cache 就是内存缓存,它的效率最快,但是内存缓存虽然读取高效,可是缓存持续性很短,会随着进程的释放而释放。一旦我们关闭 Tab 页面,内存中的缓存也就被释放了。
内存缓存中有一块重要的缓存资源是preloader相关指令下载的资源,preloader的相关指令已经是页面优化的常见手段之一,它可以一边解析js/css文件,一边网络请求下一个资源。
Disk Cache 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。在所有浏览器缓存中,Disk Cache 覆盖面基本是最大的。它会根据 HTTP Header 中的字段判断哪些资源需要缓存,哪些资源可以不请求直接使用,哪些资源已经过期需要重新请求。并且即使在跨站点的情况下,相同地址的资源一旦被硬盘缓存下来,就不会再次去请求数据。
绝大部分的缓存都来自 Disk Cache
强缓存,如果缓存资源有效,直接使用缓存资源,不必向服务器发起请求。
通过http头信息中expires和cache-control属性
服务器通过在响应头中添加 Expires 属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的结果。
Expires 是 http1.0 中的方式,因为它的一些缺点,在 HTTP 1.1 中提出了一个新的头部属性就是 Cache-Control 属性,它提供了对资源的缓存的更精确的控制。
Cache-Control
可设置的字段:
public
:设置了该字段值的资源表示可以被任何对象(包括:发送请求的客户端、代理服务器等等)缓存。这个字段值不常用,private
:设置了该字段值的资源只能被用户浏览器缓存,不允许任何代理服务器缓存。在实际开发当中,对于一些含有用户信息的HTML,通常都要设置这个字段值,避免代理服务器(CDN)缓存;no-cache
:设置了该字段需要先和服务端确认返回的资源是否发生了变化,如果资源未发生变化,则直接使用缓存好的资源;no-store
:设置了该字段表示禁止任何缓存,每次都会向服务端发起新的请求,拉取最新的资源;max-age=
:设置缓存的最大有效期,单位为秒;s-maxage=
:优先级高于max-age=,仅适用于共享缓存(CDN),优先级高于max-age或者Expires头;max-stale[=]
:设置了该字段表明客户端愿意接收已经过期的资源,但是不能超过给定的时间限制。一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方式一起使用时,Cache-Control 的优先级要高于 Expires。
no-cache和no-store很容易混淆:
如果没有命中强制缓存,如果设置了协商缓存,这个时候协商缓存就会发挥作用了。
命中协商缓存的条件有两个:
max-age=xxx
过期了no-store
使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。如果资源发生了修改,则返回修改后的资源。
协商缓存也可以通过两种方式来设置,分别是 http 头信息中的 Etag 和 Last-Modified 属性。
(1)服务器通过在响应头中添加 Last-Modified 属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since 的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是 Last-Modified 标注的最后修改时间只能精确到秒级,如果某些文件在1秒钟以内,被修改多次的话,那么文件已将改变了但是 Last-Modified 却没有改变,这样会造成缓存命中的不准确。
(2)因为 Last-Modified 的这种可能发生的不准确性,http 中提供了另外一种方式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中添加了 Etag 属性,这个属性是资源生成的唯一标识符,当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match 属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的 Etag 的值来进行比较,以此来判断资源是否发生改变,是否需要返回资源。通过这种方式,比 Last-Modified 的方式更加精确。
当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag 属性。
总结:
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。
所谓的浏览器缓存指的是浏览器将用户请求过的静态资源,存储到电脑本地磁盘中,当浏览器再次访问时,就可以直接从本地加载,不需要再去服务端请求了。
使用浏览器缓存,有以下优点:
浏览器可以分为两部分,shell 和 内核。其中 shell 的种类相对比较多,内核则比较少。
浏览器内核主要分成两部分:
最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。
5大主流浏览器 | 内核 |
---|---|
IE | trident |
chrome | webkit -> blink |
safari | webkit |
firefox | gocko |
opera | presto -> blink |
浏览器的每个标签⻚都分别对应⼀个呈现引擎实例。每个标签⻚都是⼀个独⽴的进程
解析是根据收到的HTML文件生成DOM树;加载是加载内部资源,比如img等;
解析和加载异步完成,解析完毕并不说明页面加载完毕
加载,当前节点若解析完成(并不是页面解析完成),就开始加载资源
解析过程中:
CSSom类似于DOM tree的生成过程。
render tree = DOM tree + CSS tree,渲染树构建完毕后,浏览器的渲染引擎根据它绘制页面。
当浏览器渲染引擎对页面的节点操作时,就会产生回流或者重绘。回流一定重绘,重绘不一定回流
当浏览器生成渲染树以后,就会根据渲染树来进行布局。这一阶段浏览器要弄清楚各个节点在页面中的确切位置和大小,所有相对测量值都将转换为屏幕上的绝对像素。这一过程被称为 回流,也被称为“自动重排”。
当元素属性发生改变且影响布局时(宽度、高度、内外边距等),产生回流,相当于 刷新页面。
当元素属性发生改变且不影响布局时(背景颜色、透明度、字体样式等),产生重绘,相当于 不刷新页面,动态更新内容。
浏览器的渲染队列机制:浏览器会将所有的回流、重绘的操作放在一个队列中,当队列中的操作到了一定的数量或者到了一定的时间间隔,浏览器就会对队列进行批处理。这样就会让多次的回流、重绘变成一次回流重绘。
JavaScript文件的加载、解析与执行会阻塞DOM的构建,JS文件也会导致CSSOM阻塞DOM的构建
async属性的script脚本:HTML解析与script加载并行发生,但在script执行时,HTML解析被阻塞,script执行结束后,继续HTML解析。多个带async属性的标签,不能保证加载的顺序
defer属性的script脚本:HTML解析与script加载并行发生,但script的执行要等到所有的HTML解析完成后才会发生。多个带defer属性的标签,按照顺序执行
1.defer和async在网络加载过程是一致的,都是异步执行的;
2.两者的区别在于脚本加载完成之后何时执行
window.onload() 方法用于在网页加载完毕后立刻执行的操作,即当 HTML 文档加载完毕后,立刻执行某个方法。
理论上,样式表不改变DOM树,也就没有必要停下文档解析等待它们。但是,JavaScript脚本执行时可能会请求样式信息,如果样式还没有加载解析,脚本会得到错误的值。
为了避免这种错误,浏览器会先下载和构建 CSSOM,然后再执行 JavaScript,最后再继续文档的解析。至此,CSS可以阻塞文档解析
针对JavaScript:
(1)尽量将JavaScript文件放在body的最后,用async和defer属性来异步引入
(2) body中间尽量不要写<script>
标签
针对CSS:
(1)在开发过程中,导入外部样式使用link,而不用@import。
(2)如果css少,尽可能采用内嵌样式,直接写在style标签中
使用CSS有三种方式:使用link、@import、内联样式,其中link和@import都是导入外部样式。区别:
外部样式如果长时间没有加载完毕,浏览器为了用户体验,会使用浏览器会默认样式,确保首次渲染的速度。所以CSS一般写在head中,让浏览器尽快发送请求去获取css样式。
针对DOM树、CSSOM树
(1)HTML代码层级尽量不要太深,减少CSSD代码的层级
(2)使用语义化的标签,避免不标准语义化的特殊处理
减少回流与重绘
关键渲染路径是浏览器将 HTML,CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列。
在浏览器加载页面开始到页面加载完成,按顺序发生的每一件事情
生成document对象,以供DOM操作
解析文档,构建DOM树
document.readyState = ‘loading’
遇到link开始异步加载css外部文件的新线程,遇到style开始异步构建CSSOM的新线程
没有设置异步加载的script,阻塞文档解析,等到JS脚本加载并执行完成后,继续解析文档
异步加载script,异步加载JS脚本,不阻塞解析文档(不能使用documen.write)
解析文档遇到img,先解析节点。创建加载线程,异步加载图片资源,不阻塞解析文档
文档解析完成,document.readyState = ‘interactive’
defer script,JS脚本按照顺序执行
DOMContentLoaded事件,
async script加载并执行完毕,img等资源加载完毕,window对象中的onload事件才开始触发,document.readyState = ‘complete’
window.onload 和 DOMContentLoaded的区别:window.onload是页面全部加载完毕才触发,DOMContentLoaded是文档解析完成和异步脚本加载完成后就触发
浏览器本地存储方式主要5种,点击随便一个网页,F12,应用程序下的存储就可以看到了。
下面的学习对应着一个真实的网页会快很多
Cookie是最早被提出来的本地存储方式,在此之前,服务端是无法判断网络中的两个请求是否是同一用户发起的,为解决这个问题,Cookie就出现了。Cookie的大小只有4kb,它是一种纯文本文件,每次发起HTTP请求都会携带Cookie
Cookie的特性:
如果需要域名之间跨域共享Cookie,有两种方法:
Cookie的使用场景:
Cookie字段组成:
Name:cookie的名称
Value:cookie的值,对于认证cookie,value值包括web服务器所提供的访问令牌;
Size: cookie的大小
Domain:可以访问该cookie的域名,Cookie 机制并未遵循严格的同源策略,允许一个子域可以设置或获取其父域的 Cookie。
当需要实现单点登录方案时,Cookie 的上述特性非常有用,然而也增加了 Cookie受攻击的危险,比如攻击者可以借此发动会话定置攻击。因而,浏览器禁止在 Domain 属性中设置.org、.com 等通用顶级域名、以及在国家及地区顶级域下注册的二级域名,以减小攻击发生的范围。
Path:可以访问此cookie的页面路径。 比如domain是ychch.com,path是/about
,那么只有/about
路径下的页面可以读取此cookie
Secure: 指定是否使用HTTPS安全协议发送Cookie。使用HTTPS安全协议,可以保护Cookie在浏览器和Web服务器间的传输过程中不被窃取和篡改。该方法也可用于Web站点的身份鉴别,即在HTTPS的连接建立阶段,浏览器会检查Web网站的SSL证书的有效性。
但是基于兼容性的原因(比如有些网站使用自签署的证书)在检测到SSL证书无效时,浏览器并不会立即终止用户的连接请求,而是显示安全风险信息,用户仍可以选择继续访问该站点。
HTTP: 该字段包含HTTPOnly
属性 ,该属性用来设置cookie能否通过脚本来访问,默认为空,即可以通过脚本访问。在客户端是不能通过js代码去设置一个httpOnly类型的cookie的,这种类型的cookie只能通过服务端来设置。该属性用于防止客户端脚本通过document.cookie
属性访问Cookie,有助于保护Cookie不被跨站脚本攻击窃取或篡改。但是,HTTPOnly的应用仍存在局限性,一些浏览器可以阻止客户端脚本对Cookie的读操作,但允许写操作;此外大多数浏览器仍允许通过XMLHTTP对象读取HTTP响应中的Set-Cookie头。
Expires/Max-size : 此cookie的超时时间。若设置其值为一个时间,那么当到达此时间后,此cookie失效。不设置的话默认值是Session,意思是cookie会和session一起失效。当浏览器关闭(不是浏览器标签页,而是整个浏览器) 后,此cookie失效。
LocalStorage是HTML5新引入的特性。
LocalStorage的优点:
LocalStorage的缺点:
LocalStorage的常用API:
1 | // 保存数据到 localStorage |
LocalStorage的使用场景:
SessionStorage和LocalStorage都是在HTML5才提出来的存储方案,SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。
SessionStorage与LocalStorage对比:
SessionStorage的使用场景
cookies、localStorage、sessionStorage都是存储少量数据的存储方式,当需要在本地存储大量数据时,使用浏览器的 indexDB 。
这是浏览器提供的一种本地的数据库存储机制。它不是关系型数据库,更接近 NoSQL 数据库。它内部⽤键值对进⾏存储数据,可以进⾏快速读取操作,⾮常适合web场景,同时⽤JavaScript进⾏操作会⾮常方便。
IndexedDB 具有以下特点
键值对储存:IndexedDB 内部采用对象仓库(object store)存放数据,所有类型的数据都可以直接存入。
对象仓库中,数据以”键值对”的形式保存,每一个数据记录都有对应的主键,主键是独一无二的,不能有重复,否则会抛出一个错误。
异步:IndexedDB 操作时不会锁死浏览器,用户依然可以进行其他操作,这与 LocalStorage 形成对比,后者的操作是同步的。
异步设计是为了防止大量数据的读写,拖慢网页的表现。
支持事务:IndexedDB 支持事务(transaction),系列操作中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。
同源限制:IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。
储存空间大:IndexedDB 的储存空间比 LocalStorage 大得多,一般来说不少于 250MB,甚至没有上限。
支持二进制储存:IndexedDB 不仅可以储存字符串,还可以储存二进制数据(ArrayBuffer 对象和 Blob 对象)。
同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说 Web 是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。
它认为自任何站点装载的信赖内容是不安全的。当被浏览器半信半疑的脚本运行在沙箱时,它们应该只被允许访问来自同一站点的资源,而不是那些来自其它站点可能怀有恶意的资源。
同源指的是:protocol协议、domain域名、port端口号必须一致。
同源政策主要限制了三个方面:
有了跨域限制,才使我们能安全的上网。但是在实际中,有时候我们需要突破这样的限制,下面将介绍几种跨域的解决方法。
CORS是一种W3C标准,它允许服务器通过一些自定义的头部来限制哪些源可以访问它自身的资源。CORS需要浏览器和服务器同时支持。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
根据是否需要预检请求,CORS可以分为简单请求和复杂请求
满足以下两大条件,就属于简单请求
1 | (1) 请求方法是以下三种方法之一: |
对于简单请求,浏览器直接发出CORS请求。浏览器在头信息之中,自动增加一个Origin
字段。Origin
字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
如果Origin
指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
如果Origin
指定的域名在许可范围内,响应报文会多出几个头信息字段。
1 | //要么是请求时Origin字段的值,要么是一个* |
CORS请求默认不发送Cookie和HTTP认证信息,如果要把Cookie发到服务器,一方面服务器同意,指定Access-Control-Allow-Credentials = true
字段;另一方面,请求中打开withCredentials
属性
需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin
就不能设为星号,必须指定明确的、与请求网页一致的域名。
对服务器有特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
“预检”请求用的请求方法是OPTIONS
,表示这个请求是用来询问的。除了Origin
字段,”预检”请求的头信息包括两个特殊字段。
Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT
Access-Control-Request-Headers
一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
利用<script>
标签没有跨域限制,通过<script>
标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。
1 | // 原生JavaScript实现 |
JSONP的缺点:
postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一。
window.postMessage()
方法允许来自一个文档的脚本可以传递文本消息到另一个文档里的脚本,可以用这种消息传递技术来实现安全的通信,称为“跨文档消息传递”。
发送消息
postMessage(data,origin)方法接受两个参数:
接收消息
如果指定的源匹配的话,那么当调用 postMessage()
方法的时候,在目标窗口的Window对象上就会触发一个 message
事件。
Nginx跨域的原理是利用Nginx的代理功能,让浏览器认为它访问的是同源的服务器,而实际上是访问了其他的服务器。这样就避免了浏览器的跨域限制。
node中间件实现跨域代理,原理大致与nginx相同,是一种解决前后端交互时跨域问题的方法。它利用Nodejs作为中间层,转发前端的请求到后端服务器,从而绕过浏览器的同源策略。
此方案仅限主域相同,子域不同的跨域应用场景。父窗口:(domain.com/a.html),子窗口:(child.domain.com/a.html)。两个页面都通过js强制设置document.domain为基础主域domain.com,就实现了同域。
WebSocket协议是一种在浏览器和服务器之间进行全双工通信的技术。WebSocket协议不受同源策略的限制,因此可以跨域请求。
利用webSocket的API,可以直接new一个socket实例,然后通过open方法内send要传输到后台的值,也可以利用message方法接收后台传来的数据。
客户端想获得一个服务器的数据,但是因为种种原因无法直接获取。于是客户端设置了一个代理服务器,并且指定目标服务器,之后代理服务器向目标服务器转交请求并将获得的内容发送给客户端。这样本质上起到了对真实服务器隐藏真实客户端的目的。实现正向代理需要修改客户端,比如修改浏览器配置。
服务器为了能够将工作负载分不到多个服务器来提高网站性能 (负载均衡)等目的,当其受到请求后,会首先根据转发规则来确定请求应该被转发到哪个服务器上,然后将请求转发到对应的真实服务器上。这样本质上起到了对客户端隐藏真实服务器的作用。
一般使用反向代理后,需要通过修改 DNS 让域名解析到代理服务器 IP,这时浏览器无法察觉到真正服务器的存在,当然也就不需要修改配置了。
正向代理和反向代理的结构是一样的,都是 client-proxy-server 的结构,它们主要的区别就在于中间这个 proxy 是哪一方设置的。在正向代理中,proxy 是 client 设置的,用来隐藏 client;而在反向代理中,proxy 是 server 设置的,用来隐藏 server。
Nginx 是一款轻量级的 Web 服务器,也可以用于反向代理、负载平衡和 HTTP 缓存等。Nginx 使用异步事件驱动的方法来处理请求,是一款面向性能设计的 HTTP 服务器。
传统的 Web 服务器如 Apache 是 process-based 模型的,而 Nginx 是基于event-driven模型的。正是这个主要的区别带给了 Nginx 在性能上的优势。
Nginx 架构的最顶层是一个 master process,这个 master process 用于产生其他的 worker process,这一点和Apache 非常像,但是 Nginx 的 worker process 可以同时处理大量的HTTP请求,而每个 Apache process 只能处理一个。
事件是用户操作网页时发生的交互动作,比如 click/move, 事件除了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事件被封装成一个 event 对象,包含了该事件发生时的所有相关信息( event 的属性)以及可以对事件进行的操作( event 的方法)
事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型:
事件委托 是利用浏览器事件冒泡的机制,将子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件委托。
使用事件委托可以不必要为每一个子元素都绑定一个监听事件,减少内存消耗。并且还可以实现事件动态绑定,比如说新增了一个子节点,并不需要单独地为它添加一个监听事件,它绑定的事件会交给父元素中的监听函数来处理。
事件委托不是只有优点,它也是有缺点的:
比如 focus、blur 之类的事件没有事件冒泡机制,所以无法实现事件委托;mousemove、mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的。
GC就是
Garbage Collection 也就是我们常说的
垃圾回收机制,它工作在引擎的内部,对于前端来说是相对无感的。
一般的高级语言里面会自带 GC,比如 Java、Python、JavaScript 等,也有无 GC 的语言,比如 C、C++ 等,那这种就需要我们程序员手动管理内存了,相对比较麻烦。
V8 的垃圾回收策略主要基于分代式垃圾回收机制,V8 中将堆内存分为新生代和老生代两区域,采用不同的垃圾回收器。
新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持1~8M
的容量,而老生代的对象为存活时间较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存活下来的对象,容量通常比较大。
新生代对象是通过一个名为 Scavenge
的算法进行垃圾回收,主要采用了一种复制式的方法即 Cheney 算法
:
Cheney 算法
中将堆内存一分为二,一个是处于使用状态的空间我们暂且称之为 使用区
,一个是处于闲置状态的空间我们称之为 空闲区
:
当开始进行垃圾回收时,新生代垃圾回收器会对使用区中的活动对象做标记,标记完成之后将使用区的活动对象复制进空闲区并进行排序,随后进入垃圾清理阶段,即将非活动对象占用的空间清理掉。最后进行角色互换,把原来的使用区变成空闲区,把原来的空闲区变成使用区。
先来说下什么情况下对象会出现在老生代空间中:
对于大多数占用空间大、存活时间长的对象会被分配到老生代里,因为老生代中的对象通常比较大,如果再如新生代一般分区然后复制来复制去就会非常耗时,从而导致回收执行效率不高,所以老生代垃圾回收器来管理其垃圾回收执行,它的整个流程就采用的就是标记清除了。
标记阶段,从一组根元素开始,递归遍历这组根元素,遍历过程中能到达的元素称为活动对象,没有到达的元素就可以判断为非活动对象。
清除阶段,直接将非活动对象,也就是数据清理掉。
分代式机制把一些新、小、存活时间短的对象作为新生代,采用一小块内存频率较高的快速清理,而一些大、老、存活时间长的对象作为老生代,使其很少接受检查,新老生代的回收机制及频率是不同的,可以说此机制的出现很大程度提高了垃圾回收机制的效率。
JavaScript 是一门单线程的语言,它是运行在主线程上的,那在进行垃圾回收时就会阻塞 JavaScript 脚本的执行,需等待垃圾回收完毕后再恢复脚本执行,我们把这种行为叫做 全停顿
。
假如一次 GC 的时间过长,对用户来说就可能造成页面卡顿等问题。这里引入多个辅助线程来同时处理,以此加速垃圾回收的执行速度
在执行垃圾回收的过程中,会启动了多个线程来负责新生代中的垃圾清理操作,这些线程同时将对象空间中的数据移动到空闲区域,这个过程中由于数据地址会发生改变,所以还需要同步更新引用这些对象的指针,这就是并行回收。
并行回收
可以提高回收效率,对于新生代垃圾回收器能够有很好的优化,但是它还是一种全停顿式的垃圾回收方式;
对于老生代来说,它的内部存放的都是一些比较大的对象,对于这些大的对象 GC 时哪怕我们使用并行策略依然可能会消耗大量时间
在 2011 年,V8 对老生代的标记进行了优化,从 全停顿标记
切换到增量标记
如果采用非黑即白的标记策略,那在垃圾回收器执行了一段增量回收后,暂停后启用主线程去执行了应用程序中的一段JavaScript 代码,随后当垃圾回收器再次被启动,这时候内存中黑白色都有,我们无法得知下一步走到哪里了
为了解决这个问题,V8 团队采用了一种特殊方式: 三色标记法
增量标记其实只是对活动对象和非活动对象进行标记,对于真正的清理释放内存 V8 采用的是惰性清理
当增量标记完成后,假如当前的可用内存足以让我们快速的执行代码,其实我们是没必要立即清理内存的,可以将清理过程稍微延迟一下,让 JavaScript 脚本代码先执行,也无需一次性清理完所有非活动对象内存,可以按需逐一进行清理直到所有的非活动对象内存都清理完毕,后面再接着执行增量标记。
并行回收依然会阻塞主线程,增量标记同样有增加了总暂停时间、降低应用程序吞吐量两个缺点,那么怎么才能在不阻塞主线程的情况下执行垃圾回收并且与增量相比更高效呢?
并发回收了,它指的是主线程在执行 JavaScript 的过程中,辅助线程能够在后台完成执行垃圾回收的操作,辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起:
辅助线程在执行垃圾回收的时候,主线程也可以自由执行而不会被挂起,这是并发的优点,但同样也是并发回收实现的难点,因为它需要考虑主线程在执行 JavaScript 时,堆中的对象引用关系随时都有可能发生变化,这时辅助线程之前做的一些标记或者正在进行的标记就会要有所改变,所以它需要额外实现一些读写锁机制
来控制这一点。
标记清除算法
,还是使用了并行回收
、增量标记
、惰性清理
来辅提高回收效率,增量标记中使用三色标记法
来达到暂停和恢复的作用;并发回收
使回收更高效;插件就是一个store、一个组件库、接受一个vue参数
在组件库的main.js中 定义一个UI,给其一个install方法,导出出去
1 | let UI = {}; |
在使用中:
1 | import { createApp } from 'vue' |
在无限多级菜单案例中,需要用到 递归组件 的设计。
也很简单,在递归组件中 调用自己,传入所需要的参数即可。
在下面这个案例中,ReSubMenu是递归组件,自己使用自己时,传入所需要的data属性即可。
1 | <template> |