前端面试技能树2023
2023-11-07 21:35:07 11 举报
AI智能生成
前端面试技能树2023涵盖了HTML、CSS、JavaScript等基础知识,以及React、Vue、Angular等主流框架的使用。此外,还包含了Webpack、Babel等构建工具的配置和使用,以及TypeScript、Node.js等后端技术的了解。在性能优化方面,要求掌握如何减少HTTP请求、使用缓存、压缩代码等方法来提高网页加载速度。同时,对于移动端和桌面端的开发也有一定的了解。此外,还需要具备良好的编程习惯,如模块化、组件化开发,以及对代码的测试和调试能力。总之,前端面试技能树2023要求候选人具备全面的前端技术知识体系,能够独立完成复杂的前端项目开发。
作者其他创作
大纲/内容
前端框架
vue和react的相同点和区别
相似处
virtual Dom的使用
React采用的Virtual DOM会对渲染出来的结果做脏检查,react如果父组件传递给自组件的数据发生变化,会引起整个组件树的变化
React 每次生成新的 Virtual DOM,与旧 Virtual DOM的 diff 操作本来就可以看做一次脏检查
不是像vue.js那样通过Object.defineproperty这种接口,就是每次更新一个组件,就检查一遍这个组件的所有依赖,
最终做到按需更新。导致会产生大量冗余的检查
所以,当然普遍的认知是,脏检查性能不咋地,大多数时候复杂度会大于 O(n)
React 每次生成新的 Virtual DOM,与旧 Virtual DOM的 diff 操作本来就可以看做一次脏检查
不是像vue.js那样通过Object.defineproperty这种接口,就是每次更新一个组件,就检查一遍这个组件的所有依赖,
最终做到按需更新。导致会产生大量冗余的检查
所以,当然普遍的认知是,脏检查性能不咋地,大多数时候复杂度会大于 O(n)
vue跟踪每一个组件的依赖关系,深度优先diff出需要更新的地方,不需要重新渲染整个组件树,且提供指令操作virtual Dom
组件化
不同之处
单向?双向数据流
react是单向数据流(只有数据影响视图),状态驱动视图state-》view-》New State-》New View -》 ui-》render(data)
pull: 代表为React,React是如何侦测到变化的,通常⽤ setState API显式更新,然后React会进 ⾏⼀层层的Virtual Dom Diff操作找出差异,然后Patch到DOM上,React从⼀开始就不知道到底是哪发⽣了变化,只是知道 「有变化了」,然后再进⾏⽐较暴⼒的Diff操作查找「哪发⽣变化了」,另外⼀个代表就是Angular的脏检查操作。
pull: 代表为React,React是如何侦测到变化的,通常⽤ setState API显式更新,然后React会进 ⾏⼀层层的Virtual Dom Diff操作找出差异,然后Patch到DOM上,React从⼀开始就不知道到底是哪发⽣了变化,只是知道 「有变化了」,然后再进⾏⽐较暴⼒的Diff操作查找「哪发⽣变化了」,另外⼀个代表就是Angular的脏检查操作。
vue是双向数据流,Vue 是MVVM框架,双向数据绑定,当ViewModel对Model进行更新时,通过数据绑定更新到View。
push: Vue的响应式系统则是push的代表,当Vue程序初始化的时候就会对数据data进⾏依赖的收集,⼀但数据发⽣变化,响应式系统就会⽴刻得知,因此Vue是⼀开始就知道是「在哪发⽣变化了」,但是这⼜会产⽣⼀个问题,如果你熟悉Vue的响 应式系统就知道,通常⼀个绑定⼀个数据就需要⼀个Watcher,⼀但我们的绑定细粒度过⾼就会产⽣⼤量的Watcher,这会 带来内存以及依赖追踪的开销,⽽细粒度过低会⽆法精准侦测变化,因此Vue的设计是选择中等细粒度的⽅案,在组件级别 进⾏push侦测的⽅式,也就是那套响应式系统,通常我们会第⼀时间侦测到发⽣变化的组件,然后在组件内部进⾏Virtual Dom Diff获取更加具体的差异,
⽽Virtual Dom Diff则是pull操作,Vue是push+pull结合的⽅式进⾏变化侦测的.
push: Vue的响应式系统则是push的代表,当Vue程序初始化的时候就会对数据data进⾏依赖的收集,⼀但数据发⽣变化,响应式系统就会⽴刻得知,因此Vue是⼀开始就知道是「在哪发⽣变化了」,但是这⼜会产⽣⼀个问题,如果你熟悉Vue的响 应式系统就知道,通常⼀个绑定⼀个数据就需要⼀个Watcher,⼀但我们的绑定细粒度过⾼就会产⽣⼤量的Watcher,这会 带来内存以及依赖追踪的开销,⽽细粒度过低会⽆法精准侦测变化,因此Vue的设计是选择中等细粒度的⽅案,在组件级别 进⾏push侦测的⽅式,也就是那套响应式系统,通常我们会第⼀时间侦测到发⽣变化的组件,然后在组件内部进⾏Virtual Dom Diff获取更加具体的差异,
⽽Virtual Dom Diff则是pull操作,Vue是push+pull结合的⽅式进⾏变化侦测的.
模版渲染
React是通过JSX来渲染模板,而Vue是通过扩展的HTML来进行模板的渲染。
组件形式
组件形式不同,Vue文件里将HTML,JS,CSS组合在一起。react提供class组件和function组
自由度
Vue封装好了一些v-if,v-for,React什么都是自己实现,自由度更高
类式的组件写法,还是声明式写法(vue3.0已支持)
react是类式写法
vue是声明式,传入各种options,api和参数都很多
是否提供内置功能
react-redux,react-router交给社区维护
vuex,vue-router官方提供
扩展
react通过高阶组件扩展
vue通过mixins扩展
Vue也能实现高阶组件,只是特别麻烦,因为Vue对与组件的option做了各种处理,想实现高阶组件就要知道每一个option是怎么处理的,然后正确的设置。具体有多复杂,可以参考下面的文章。
all in js?
react思想是all in js,通过jis来生成html,css,使用的是jsx,vue也有jsx方式,但是官方推崇使用html的语法创建.vue文件
数据是否自动watcher可变
react性能优化需要手动支持,vue自动优化,(当state太多多时候,vue的watcher也会多,造成卡顿,所以大型应用使用react会比较可控)
监听数据变化的实现原理不同
Vue通过 getter/setter以及一些函数的劫持,能精确知道数据变化。
React默认是通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染。
为什么React不精确监听数据变化呢?
这是因为Vue和React设计理念上的区别,Vue使用的是可变数据,而React更强调数据的不可变,
两者没有好坏之分,Vue更加简单,而React构建大型应用的时候更加好
为什么React不精确监听数据变化呢?
这是因为Vue和React设计理念上的区别,Vue使用的是可变数据,而React更强调数据的不可变,
两者没有好坏之分,Vue更加简单,而React构建大型应用的时候更加好
vue
1.css只在当前组件起作用
答:在style标签中写入scoped,原理
给HTML的DOM节点加一个不重复data属性(形如:data-v-2311c06a)来表示他的唯一性
在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器(如[data-v-2311c06a])来私有化样式
答:在style标签中写入scoped,原理
给HTML的DOM节点加一个不重复data属性(形如:data-v-2311c06a)来表示他的唯一性
在每句css选择器的末尾(编译后的生成的css语句)加一个当前组件的data属性选择器(如[data-v-2311c06a])来私有化样式
2.v-if 和 v-show 区别
答:v-if按照条件是否渲染,v-show是display的block或none;
答:v-if按照条件是否渲染,v-show是display的block或none;
v-if会调用addIfCondition方法,生成vnode的时候会忽略对应节点,render的时候就不会渲染; v-show会生成vnode,render的时候也会渲染成真实节点,只是在render过程中会在节点的属性中修改show属性值,也就是常说的display; v-html会先移除节点下的所有节点,调用html方法,通过addProp添加innerHTML属性,归根结底还是设置innerHTML为v-html的值
3.$route和$router的区别
答:$route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。
而$router是“路由实例”对象包括了路由的跳转方法,钩子函数等。如push .go .replace
答:$route是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。
而$router是“路由实例”对象包括了路由的跳转方法,钩子函数等。如push .go .replace
4.vue.js的两个核心是什么?
答:数据驱动、组件系统
答:数据驱动、组件系统
5.vue几种常用的指令
答:v-for 、 v-if 、v-bind、v-on、v-show、v-else,v-bind绑定属性,v-on绑定事件
答:v-for 、 v-if 、v-bind、v-on、v-show、v-else,v-bind绑定属性,v-on绑定事件
6.vue常用的修饰符?
.prevent: 提交事件不再重载页面;
.stop: 阻止单击事件冒泡;
.self: 当事件发生在该元素本身而不是子元素的时候会触发;
.capture: 事件侦听,事件发生的时候会调用
.stop: 阻止单击事件冒泡;
.self: 当事件发生在该元素本身而不是子元素的时候会触发;
.capture: 事件侦听,事件发生的时候会调用
7.v-on 可以绑定多个方法吗?
答:可以
答:可以
8.vue中 key 值的作用?
1. key的作⽤主要是为了更⾼效的对⽐虚拟DOM中的某个节点是否相同。
2. vue在patch过程中判断两个节点是否是相同节点,从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key和元素类型等,key作为唯⼀标识,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只会频繁更新dom,明显是不可取的。使得整个patch过程⽐较低效,影响性能。
3. 应该避免使⽤数组索引作为key,这可能导致⼀些隐蔽的bug;(例如当删除数组中某个值时,这时候它和它后面的index变了,但是vue并不能知道哪一个值被删除,key对应index,所以它和后面的所有vdom 都会重新渲染,没有重复利用。如果使用唯一id作为key,vue可以准确找到那个节点被删除,直接删除vdom就好了,其他直接复用)
为什么index不能作为key
用 index 作为 key 时,在对数据进行,逆序添加,逆序删除等破坏顺序的操作时,会产生没必要的真实 DOM更新,从而导致效率低
用 index 作为 key 时,如果结构中包含输入类的 DOM,会产生错误的 DOM 更新
在开发中最好每条数据使用唯一标识固定的数据作为 key,比如后台返回的 ID,手机号,身份证号等唯一值
如果不对数据进行逆序添加 逆序删除破坏顺序的操作, 只用于列表展示的话 使用index 作为Key没有毛病
什么是vue的计算属性?
computer,依赖一个或多个属性进行计算操作,支持缓存,不支持异步,属性可以直接用于template
computer,依赖一个或多个属性进行计算操作,支持缓存,不支持异步,属性可以直接用于template
①使得数据处理结构清晰;
②依赖于数据,数据更新,处理结果自动更新;
③计算属性内部this指向vm实例;
④在template调用时,直接写计算属性名即可;
⑤常用的是getter方法,获取数据,也可以使用set方法改变数据;
⑥相较于methods,不管依赖的数据变不变,methods都会重新计算,但是依赖数据不变的时候computer从缓存中获取,不会重新计算。
Vue 中的 computed 是如何实现的
computed本身是通过代理的方式代理到组件实例上的,所以读取计算属性的时候,执行的是一个内部的getter,而不是用户定义的方法。
computed内部实现了一个惰性的watcher,在实例化的时候不会去求值,其内部通过dirty属性标记计算属性是否需要重新求值。当computed依赖的任一状态(不一定是return中的)发生变化,都会通知这个惰性watcher,让它把dirty属性设置为true。所以,当再次读取这个计算属性的时候,就会重新去求值。
10.vue等单页面应用及其优缺点
优点:
1、除了首次加载页面,但页面内容改变不需要重新加载整个页面,
利用ajax实现良好的交互体验,没有页面之间的切换,不会出现白屏,也不会闪烁
2、前后端完全分离,后端不再负责模版渲染,输出页面,后端更专注业务开发和性能优化
3、单页面相对服务器压力小,服务器只用输出数据就可以
1、除了首次加载页面,但页面内容改变不需要重新加载整个页面,
利用ajax实现良好的交互体验,没有页面之间的切换,不会出现白屏,也不会闪烁
2、前后端完全分离,后端不再负责模版渲染,输出页面,后端更专注业务开发和性能优化
3、单页面相对服务器压力小,服务器只用输出数据就可以
缺点
1、首屏加载慢,如果不对路由进行处理,就会将所有组件全部加载并向服务器请求数据,这导致首屏加载速度慢
解决办法
1,路由懒加载-按需加载组件,当路由被访问的时候再加载对应组件,而不是首页的时候加载)
2、使用CDN加速,如果一些常用的库,采用CDN加速
3、异步加载组件
4、服务端渲染(有利于seo)
不利于seo
解决
页面预渲染
构建阶段生成匹配预渲染路径的 html 文件
构建阶段生成匹配预渲染路径的 html 文件
在项目构建时,通过无头浏览器模拟浏览器请求,将得到的数据插入给出的模板中,
从而生成已经包含数据的html,这样有了更多的静态资源,网络爬虫可以抓取到更多的网站信息,提升网站的搜索排名.
预渲染prerender-spa-plugin
从而生成已经包含数据的html,这样有了更多的静态资源,网络爬虫可以抓取到更多的网站信息,提升网站的搜索排名.
预渲染prerender-spa-plugin
phantomjs 页面预渲染,具体参考 phantomjs.org
使用预渲染vue-router必须使用history模式
搜索引擎能根据url爬取到独立页面的内容
(形如http://aiispo.cn/article/1),通过此URL进入文章独立页面能抓取到文章正文。
搜索引擎能根据url爬取到独立页面的内容
(形如http://aiispo.cn/article/1),通过此URL进入文章独立页面能抓取到文章正文。
vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。
如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。
如果不想要很丑的 hash,我们可以用路由的 history 模式,这种模式充分利用 history.pushState API 来完成 URL 跳转而无须重新加载页面。
要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面
vue-meta-info,这个是针对单页面的meta SEO的另一种思路,参考网站 https://zhuanlan.zhihu.com/p/29148760
nuxt 简单易用,参考网站 https://zh.nuxtjs.org/guide/installation
ssr,vue官方文档给出的服务器渲染方案,这是一套完整的构建vue服务端渲染应用的指南,具体参考https://cn.vuejs.org/v2/guide/ssr.html
11.怎么定义 vue-router 的动态路由? 怎么获取传过来的值
答:在 router 目录下的 index.js 文件中,对 path 属性加上 /:id,使用 router 对象的 params.id 获取。
答:在 router 目录下的 index.js 文件中,对 path 属性加上 /:id,使用 router 对象的 params.id 获取。
keep-alive的了解
用来对组件进行缓存,从而节省性能,<keep-alive>由于是一个抽象组件,所以在页面渲染完毕后不会被渲染成一个DOM元素,
被包裹在keep-alive中的组件的状态将会被保留,不会重新加载,不会触发mounted
被包裹在keep-alive中的组件的状态将会被保留,不会重新加载,不会触发mounted
keep-alive生命周期钩子函数:activated、deactivated,如果要在每次进入页面的时候获取最新的数据,需要在activated阶段获取数据,承担原来created钩子中获取数据的任务。
vue-cli
如何添加自定义指令
自定义过滤器
vuex
原理
1.vue.use(vuex),调用vuex 的install方法,
install源码:store注入vue实例组件的方式,是通过vue的mixin机制,
借助beforeCreat完成,即每个vue组件实例化过程中,会在before钩子前调用vuex init方法
install源码:store注入vue实例组件的方式,是通过vue的mixin机制,
借助beforeCreat完成,即每个vue组件实例化过程中,会在before钩子前调用vuex init方法
在beforeCreate钩子前混入vuexInit方法,实现了store注入vue组件实例。并注册了vuex store的引用属性$store
vuex的state是响应式的,借助vue的data实现,将state存入vue实例组件的data
vuex的getter是借助vue的计算属性computed实现数据监听
属性
state
每个应用包含一个store实例,存放数据和状态,不可以直接修改里面对数据
mutations
定义修改store数据的方法
action
通过将mutation里面的数据的方法变成异步处理数据的方法
getters
计算属性,主要用来过滤一些数据
用处
用来保存状态,组件发送dispatch触发action异步,通过commit提交mutation,相当于一个处理中心,mutations修改state,最后渲染组件
使用场景:购物车,登陆状态,音乐播放
vue实现路由的方式
hash模式
hash # 后面的字符是不包括在http请求中的,一般用来指导浏览器动作
history模式
提供pushState()、replacState(),可以对浏览器历史记录栈进行修改
history模式下,在服务端要增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面
组件间的参数传递
父传子
props
子传父
事件传递$emit(“绑定的函数名”,'子传父的值')。在父组件中都子组件标签绑定事件,子组件通过this.$emit触发,并传递参数给父组件
$parent / $children与 ref
ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就 指向组件实例
$parent / $children:访问父 / 子实例
4. $attr/$listeners (跨级组件,父子,兄弟组件)
子孙组件
v-bind="$attr"
v-on="$listeners"
v-bind="$attr"
v-on="$listeners"
父组件
Vue2.4提供了v-bind="$attrs" , v-on="$listeners" 来传递数据与事件,跨级组件之间的通讯变得更简单。
给子组件添加v-bind="$attrs" v-on="$listeners"属性
使用:子组件通过使用v-bind="$attrs",将父组件非props的属性传递给孙组件,子组件通过v-on="$listeners" 将父组件绑定的事件传递给孙组件,孙组件通过$emit()触发事件将数据传递给父组件
给子组件添加v-bind="$attrs" v-on="$listeners"属性
使用:子组件通过使用v-bind="$attrs",将父组件非props的属性传递给孙组件,子组件通过v-on="$listeners" 将父组件绑定的事件传递给孙组件,孙组件通过$emit()触发事件将数据传递给父组件
(只传递,不处理数据)
inheritAttrs:true 继承除props之外的所有属性;inheritAttrs:false 只继承class属性。
eventBus(跨级组件,父子,兄弟组件)
vue 实例 作为事件总线(事件中心)用来触发事件和监听事件
// bus.js
import Vue from 'vue'
export defult new Vue()
// bus.js
import Vue from 'vue'
export defult new Vue()
bus.$on('sendTitle',(val)=>{console.log(val)})
bus.$emit('sendTitle','传递的值')
vuex(跨级)
inject/provide(跨级/高阶用法)
祖先组件中通过provider来 提供变量,然后在子孙组件中通过inject来注入变量。
provide 和inject绑定并不是可响应的,实现响应式API Vue.observable
原理
init Injections+resolve Inject
获取vm.$options.inject,通过resolve Inject方法找到对应的key集合;
遍历key集合,对其进行响应式监听
然后通过$parent向上查找祖先节点数据;
是通过遍历source.$parent逐级向上查找的,直到找到为止
initProvide
该方法单纯把组件注册的provide值,赋值给vm._provided,resolve Inject中有使用到
获取vm.$options.inject,通过resolve Inject方法找到对应的key集合;
遍历key集合,对其进行响应式监听
然后通过$parent向上查找祖先节点数据;
是通过遍历source.$parent逐级向上查找的,直到找到为止
initProvide
该方法单纯把组件注册的provide值,赋值给vm._provided,resolve Inject中有使用到
vue响应式原理.
vue.js 采用数据劫持结合发布者-订阅者模式,
1、通过Observer类,利用Object.defineProperty()来劫持各个属性的getter/setter,
2、通过Dep类作为订阅管理中心,为每个属性管理订阅者,
3、通过Watcher类创建订阅者和绑定update()更新回调,
4、在初始化编译模版compile时触发getter,这时会为属性的Dep订阅中心添加订阅者watcher,
在数据变动触发set时通知所有订阅者watcher,触发相应update回调,更新页面
1、通过Observer类,利用Object.defineProperty()来劫持各个属性的getter/setter,
2、通过Dep类作为订阅管理中心,为每个属性管理订阅者,
3、通过Watcher类创建订阅者和绑定update()更新回调,
4、在初始化编译模版compile时触发getter,这时会为属性的Dep订阅中心添加订阅者watcher,
在数据变动触发set时通知所有订阅者watcher,触发相应update回调,更新页面
双向绑定过程
1、Observer将this.data转换成getter/setter进行响应式绑定
2、Dep【依赖管理】,收集包括{{}},watch,computed,的依赖,当数据发生变化,调用收集的回调。
3、如何收集依赖:Compile当读取属性当时候收集,就是当触发Object.defineproperty get 的时候
4、什么是订阅者:Watcher,Compile时遇到{{}},v-modle等时,创建订阅者,并触发get添加到dep依赖管理中心
2、Dep【依赖管理】,收集包括{{}},watch,computed,的依赖,当数据发生变化,调用收集的回调。
3、如何收集依赖:Compile当读取属性当时候收集,就是当触发Object.defineproperty get 的时候
4、什么是订阅者:Watcher,Compile时遇到{{}},v-modle等时,创建订阅者,并触发get添加到dep依赖管理中心
伪代码
什么是mvvm
Model是指数据模型,用于业务逻辑实现和数据改变的定义,
View是视图模型,用户将数据模型的UI展示
ViewModel:是将数据模型和视图模型连接起来,当数据改变触发页面ui的改变,这个是双向的,就是双方的改变都会被同步起来,
通过监听数据模型和管理视图变更,可以理解用于同步View和Model的对象,
好处就是,这个同步复杂的关系和逻辑开发者不需要关注,不需要处理Dom,只需把关注点放在业务逻辑上
View是视图模型,用户将数据模型的UI展示
ViewModel:是将数据模型和视图模型连接起来,当数据改变触发页面ui的改变,这个是双向的,就是双方的改变都会被同步起来,
通过监听数据模型和管理视图变更,可以理解用于同步View和Model的对象,
好处就是,这个同步复杂的关系和逻辑开发者不需要关注,不需要处理Dom,只需把关注点放在业务逻辑上
virtual Dom的理解
用Js对象结构表示的Dom树的结构,本质上就是真实dom的映射。里面包含标签名,属性和子元素对象。vue最终的工作就是通过这颗树批量生成真实dom。直接修改js这个树对象,达到统一更新dom。整个机制是在JavaScript层面计算,在完成之前并不会操作DOM,等数据稳定之后再实现精准的修改。这样更能达到优化性能的目的。
虚拟dom的作用其实有两方面:
1、提供与真实dom节点所对应的虚拟节点vnode
2、将虚拟vnode和旧vnode进行对比(diff),然后更新视图
vue2虚拟dom&diff算法
diff
1.diff 算法是虚拟 DOM 技术的必然产物:通过新旧虚拟 DOM 作对比,区分出增加、删除、移动,更新的元素,将变化更新在真实 DOM 上;Vue2.diff高效的执行对比过程,将时间复杂度降低为 O(n)
2.vue 2.x 中为了降低 Watcher 粒度,每个组件只有一个 Watcher 与之对应,引入 diff 和key结合能精确找到发生变化的地方。
3.vue 中 diff 执行的时刻是组件实例执行其更新函数patch时,它会比对上一次渲染结果 oldVnode 和新的渲染结果 newVnode,此过程称为 patch。 (diff发生的地方-patch.js--patchVnode())
4.diff 过程整体遵循深度优先、同层比较的策略;两个节点之间比较会根据它们是否拥有子节点或者文本节点做不同操作;比较两组子节点是diff算法的重点,源码在updateChildren(),首先假设头尾节点可能相同做 4 次比对尝试(首首,尾尾,首尾,尾首), 如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;借助 key 通常可以非常精确找到相同节点,因此整个patch过程非常高效。(patch.js 高效性分析-updateChildren()),不使用diff算法去比较两颗树的算法复杂度首O(n^3),使用diff算法能将复杂度降低到O(n)
2.vue 2.x 中为了降低 Watcher 粒度,每个组件只有一个 Watcher 与之对应,引入 diff 和key结合能精确找到发生变化的地方。
3.vue 中 diff 执行的时刻是组件实例执行其更新函数patch时,它会比对上一次渲染结果 oldVnode 和新的渲染结果 newVnode,此过程称为 patch。 (diff发生的地方-patch.js--patchVnode())
4.diff 过程整体遵循深度优先、同层比较的策略;两个节点之间比较会根据它们是否拥有子节点或者文本节点做不同操作;比较两组子节点是diff算法的重点,源码在updateChildren(),首先假设头尾节点可能相同做 4 次比对尝试(首首,尾尾,首尾,尾首), 如果没有找到相同节点才按照通用方式遍历查找,查找结束再按情况处理剩下的节点;借助 key 通常可以非常精确找到相同节点,因此整个patch过程非常高效。(patch.js 高效性分析-updateChildren()),不使用diff算法去比较两颗树的算法复杂度首O(n^3),使用diff算法能将复杂度降低到O(n)
虚拟dom
1、vue通过编译将template模板编译compile成渲染函数render,执行渲染函数就可以得到虚拟dom树
2、Vnode:可以代表一个真实dom节点,通过creatElement能将render成Vnode,Vnode通过_update变成真实dom,可以将vnode理解成节点描述对象
patch:虚拟dom核心,可以将vnode渲染成真实dom,这个过程是对比新旧节点有哪些不同,然后找到相同节点就复用,不同节点进行批量更新
2、Vnode:可以代表一个真实dom节点,通过creatElement能将render成Vnode,Vnode通过_update变成真实dom,可以将vnode理解成节点描述对象
patch:虚拟dom核心,可以将vnode渲染成真实dom,这个过程是对比新旧节点有哪些不同,然后找到相同节点就复用,不同节点进行批量更新
从构建VNode到生成真实节点树
初始化调用 $mount 挂载组件。
_render 开始构建 VNode,核心方法为 createElement,一般会创建普通的 VNode ,遇到组件就创建组件类型的 VNode,否则就是未知标签的 VNode,构建完成传递给 _update。
patch 阶段根据 VNode 创建真实节点树,核心方法为 createElm,首先遇到组件类型的 VNode,内部会执行 $mount,再走一遍相同的流程。普通节点类型则创建一个真实节点,如果它有子节点开始递归调用 createElm,使用 insert 插入子节点,直到没有子节点就填充内容节点。最后递归完成后,同样也是使用 insert 将整个节点树插入到页面中,再将旧的根节点移除。
_render 开始构建 VNode,核心方法为 createElement,一般会创建普通的 VNode ,遇到组件就创建组件类型的 VNode,否则就是未知标签的 VNode,构建完成传递给 _update。
patch 阶段根据 VNode 创建真实节点树,核心方法为 createElm,首先遇到组件类型的 VNode,内部会执行 $mount,再走一遍相同的流程。普通节点类型则创建一个真实节点,如果它有子节点开始递归调用 createElm,使用 insert 插入子节点,直到没有子节点就填充内容节点。最后递归完成后,同样也是使用 insert 将整个节点树插入到页面中,再将旧的根节点移除。
整个过程源码分析
vm._init()
vm.$mount()
mountCompoonent()
创建Watcher对象
vm._render() --render返回vnode
vnode=render.call(vm._renderProxy,vm.$createElement)
vm.$createElement()
h函数,用户设置的render函数中调用
createElement(vm,a,b,c,d,true)
vm._c()
h函数,模版编译的render函数中调用
createElement(vm,a,b,c,d,true)
_creatElement()
vnode = new VNode(config.parsePlatformTagName(tag),data,children,undefined,undefined,context)
vm._render()结束,返回vnode
vm._update
负责把虚拟dom渲染成真实Dom
首次:vm.__patch__(vm.$em,vnode,hydrating,false)
数据更新:vm.__patch__(preVnode,vnode)
vm.__patch__()
调用creatPathchFunction()返回patch()
patch() --diff
vnode/patch.js中creatPatchFunction返回patch函数
挂载cbs节点属性/事件/样式操作的钩子函数
判断第一个参数是真实Dom还是虚拟dom,首次加载,第一个参数就是真实dom,转换成Vnode,调用creatElm
如果是数据更新,新旧节点是sameVnode执行patchVnode,也就是diff
删除旧节点
createElm(vnode,insertedVnodeQueue)--创建真实Dom
将虚拟节点转成真实节点,并插到Dom树
把虚拟节点的children转换为真实Dom,并插入到Dom树
patchVnode--diff算法
对比新旧vnode,以及新旧vnode的字节点更新差异
如果新旧vnode都有子节点并且子节点不同,调用updateChildren对比子节点差异:双端比较
updateChildren。双端比较
从头到尾进行双端比较,找不到就使用遍历查找
如果新节点比老节点多,把新增的节点插入到dom中,如果老节点比新节点多,把多余节点删除
diff算法的双端比较
updateChildren()
updateChildren()
分层diff:不考虑跨层级移动节点,让新旧两个vdom树对比无需循环递归
同层比较,深度优先:双端比较算法(首首,尾尾,首尾,尾首)比较,按照不同情况可以找到节点是新增、删除、还是移动都情况,这过程还会结合key唯一标识来比较,能将复杂度大幅降底
痛点和vue3优化
痛点:vue2虚拟dom是全量比较的,在运行时会对所有节点生成一个虚拟节点树,当页面数据发生变更,会遍历判断vdom树所有节点,包括一些不会变的节点,虽然diff算法已经减少了很多dom节点的操作。但是如果是复杂的大型项目,其中会包含很多递归的patchVNode,最终造成VNode更新更缓慢
vue3的动静结合,在patch过程采用位运算来判断VNode类型以及通过静态提升和patchfalg标记
1、在模版编译时(生成VNode时打标记),就将一些动态标签末尾加上标记patch flag,只有pacthflag的节点才会参与对比,不用逐个遍历查找
2、patch时, 配合diff算法,对于静态标记了的元素,只会被创建一次,更新时直接复用,
1、在模版编译时(生成VNode时打标记),就将一些动态标签末尾加上标记patch flag,只有pacthflag的节点才会参与对比,不用逐个遍历查找
2、patch时, 配合diff算法,对于静态标记了的元素,只会被创建一次,更新时直接复用,
patch算法
虚拟DOM最核心的部分就是patch,它可以将vnode渲染成真实的DOM,而对比两个vnode之间的差异只是patch的一部分,
patch的目的是修改DOM节点,也可以理解为渲染视图,并且patch不是暴力替换节点,而是在现有DOM上进行修改。对现有DOM进行修改需要做三件事:
1、创建新增的DOM
2、删除已经废弃的节点
3、修改需要更新的节点
patch的目的是修改DOM节点,也可以理解为渲染视图,并且patch不是暴力替换节点,而是在现有DOM上进行修改。对现有DOM进行修改需要做三件事:
1、创建新增的DOM
2、删除已经废弃的节点
3、修改需要更新的节点
vue的优点和缺点
优点:数据的双向绑定,组件化,提高了开发效率,方便重复使用,提升了项目的可维护性,便于协同开发,前后端分离,更专注业务
缺点:不支持低版本浏览器,不适于SEO优化
vue3展望
vue3适合我嘛?会迅速替代Vue吗
升级是否平滑:vue3兼容之前的写法,仅新增少量api Composition api是可选的
生态是否跟上:相关工具、生态、库更新都需要时间,或许到普及还需要一段时间
vue3比vue2好吗:
杀手级特性:Composition setup函数,
<script setup> 代码简化
用户体验:性能提升,响应式革新
更好类型判断支持
兼容性
vue3新特性
优化点
响应式系统提升
vue2在初始化的时候,
对data中的每个属性使用definepropery调用getter和setter使之变为响应式对象。
如果属性值为对象,还会递归调用defineproperty使之变为响应式对象。
definepropety不支持监听对象上添加新属性,和数组长度变化
对data中的每个属性使用definepropery调用getter和setter使之变为响应式对象。
如果属性值为对象,还会递归调用defineproperty使之变为响应式对象。
definepropety不支持监听对象上添加新属性,和数组长度变化
vue3使用proxy重写响应式。
proxy的性能要比defineproperty好,
可以拦截整个对象属性的访问、赋值、删除等操作,
不需要初始化的时候遍历所有属性,另外有多层属性嵌套的话,
只有访问某个属性的时候,才会递归处理下一级的属性
proxy的性能要比defineproperty好,
可以拦截整个对象属性的访问、赋值、删除等操作,
不需要初始化的时候遍历所有属性,另外有多层属性嵌套的话,
只有访问某个属性的时候,才会递归处理下一级的属性
优点
可以监听数组变化,包括索引和length
可以劫持整个对象的新增删除
操作时不是对原对象操作,是 new Proxy 返回的一个新对象
可以劫持的操作有 13 种
编译优化
vue3通过标记所有静态根节点,diff的时候只比较动态节点内容
Fragments
模板不再创建唯一根节点
静态提升
静态提升(hoistStatic),当使用 hoistStatic时,所有静态的节点都被提升到 render 方法之外.只会在应用启动的时候被创建一次,
之后使用只需要应用提取的静态节点,随着每次的渲染被不停的复用。
之后使用只需要应用提取的静态节点,随着每次的渲染被不停的复用。
动态标记 patch flag
patch flag, 在动态标签末尾加上相应的标记
只能带 patchFlag 的节点才被认为是动态的元素,会被追踪属性的修改,能快速的找到动态节点,而不用逐个逐层遍历,提高了虚拟dom diff的性能。
缓存事件处理函数cacheHandler
避免每次触发都要重新生成全新的function去更新之前的函数
源码体积的优化
vue3移除了一些不常用的api,例如:inline-template、filter等 使用tree-shaking通过摇树优化核心库体积,减少不必要的代码量
Composition Api 与 Vue 2.x使用的Options Api 有什么区别
Options Api
包含一个描述组件选项(data、methods、props等)的对象 options;
API开发复杂组件,同一个功能逻辑的代码被拆分到不同选项 ;
使用mixin重用公用代码,也有问题:命名冲突,数据来源不清晰;
composition Api
它是基于函数的 api,可以更灵活的组织组件的逻辑。解决options api在大型项目中,options api不好拆分和重用的问题。
reactive
watchEffect
computed
ref
toRefs
生命周期的hooks
reactive
watchEffect
computed
ref
toRefs
生命周期的hooks
使用
setup()
beforecreat/creat前执行
参数:
props: 组件传入的属性
context(attrs、slot 和emit,分别对应 Vue2.x 中的 $attr属性、slot插槽 和$emit)
props: 组件传入的属性
context(attrs、slot 和emit,分别对应 Vue2.x 中的 $attr属性、slot插槽 和$emit)
reactive
可以代理一个对象, 但是不能代理基本类型,例如字符串、数字、boolean 等
我们需要在vue3中使用服务器的数据渲染到页面上,我使用的是reactive生成的响应式数据属性,但是在挂载后请求的数据并没有渲染显示到页面上。解决:1、使用ref存储响应式数据let menusList = ref([]);,2、使用toRefs=》...toRefs(state) ,3使用reactive+对象包裹的方式let parmar = reactive({menus: []});
ref
一般用于js 基础类型的双向绑定
原理:ref()实际就是把传入的值对象或引用对象统一转换成Proxy对象,用于监听对象的变动,因为Proxy只支持引用对象,所以对于值对象, 会转换成{ value: 值 } 再转换成Proxy对象 此时就可以监听到value值的变化
toRefs
toRefs 用于将一个 reactive 对象转化为属性全部为 ref 对象的普通对象(...toRefs(user),解构reactive的对象返回给组件使用)
isRef
computer
computed可传入get和set
watch 与 watchEffect 的用法
watchEffect 不需要手动传入依赖
watchEffect 会先执行一次用来自动收集依赖
watchEffect 无法获取到变化前的值, 只能获取变化后的值
watch和watchEffect都是监听器,但在写法和使用上有所区别。
jsx
生命周期
beforeDestroy变beforeUnmount
destroyed变onUnmounted
beforeCreate和created被setup替换了,其次,钩子命名都增加了on
新增用于调试的钩子函数onRenderTriggered和onRenderTricked
Teleport传送门(dialog插入到body上的例子)
Suspense
vue3响应式实现原理
用法变更
在 Vue2.x 中具名插槽和作用域插槽分别使用slot和slot-scope来实现, 在 Vue3.0 中将slot和slot-scope进行了合并同意使用。 Vue3.0 中v-slot:
自定义指令名称变更,update去掉,componentUpdate->update,新增beforeUpdate、beforeUnmount
v-model 升级,Vue 3 中抛弃了.async写法, 统一使用v-model
vue3优化
monorepo[mɔnəu ˈriːpəʊ]管理方式:yarn workspace,将模块拆分到package中
性能优化:引入tree-shaking,未被使用到的不会被打包
类型检测增强:从flow换到了ts
内部代码优化:- 响应式相关代码更加简洁,
数据劫持私用proxy,在使用时候劫持,
数据劫持私用proxy,在使用时候劫持,
defineProperty
vue2的响应式实现是基于defineproperty,对data做遍历+递归,每个属性设置了getter、setter,(把不需要相应的数据也进行了劫持)
Proxy
proxy是针对一个对象的,这个对象所有操作都会被监听(真正访问到的内部属性才会变成响应式,简单的可以说是按需实现响应式,减少性能消耗。)
模板编译优化
通过编译阶段对静态模板的分析,生成block tree,可以对子节点动态收集。增加了patchFlag标记动态节点,
渲染效率不再与模板大小成正相关,而是与模板中动态节点的数量成正相关。
渲染效率不再与模板大小成正相关,而是与模板中动态节点的数量成正相关。
Vue Composition API 基本使用
基于函数和逻辑复用机制
Vue Composition API 围绕一个新的组件选项 setup 而创建。setup() 为 Vue 组件提供了状态、计算值、watcher 和生命周期钩子。
并没有让原来的 API(Options-based API)消失。允许开发者 结合使用新旧两种 API(向下兼容)
vue2mixin和vue3setup使用区别
命名冲突问题
不清楚暴露出来的变量的作用
逻辑重用到其他 component 经常遇到问题
Vue3.0是如何变得更快的
diff优化
Vue2.x 中的虚拟dom是进行全量的对比。
Vue3.0 中新增了静态标记(PatchFlag):在与上次虚拟结点进行对比的时候,值对比带有patch flag的节点,并且可以通过flag 的信息得知当前节点要对比的具体内容化。
hoistStatic 静态提升
Vue2.x : 无论元素是否参与更新,每次都会重新创建。
Vue3.0 : 对不参与更新的元素,只会被创建一次,之后会在每次渲染时候被不停的复用。
事件侦听器缓存 cacheHandlers
默认情况下onClick会被视为动态绑定,所以每次都会去追踪它的变化,但是因为是同一个函数,所以没必要去追踪它的变化,想办法将它直接缓存起来复用就会提升性能。
周期函数
beforeCreate-> 使用 setup()
created-> 使用setup()
beforeMount -> onBeforeMount
Mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestory ->onBeforeUnmount
destoryed -> onUnmounted
errorCaptured->onErrorCaptured
Proxy和Object.defineProperty对比
Proxy优势
直接监听整个对象而非属性
直接监听数组的变化
13种拦截方法,ownKeys、deleteProperty、has 等是 Object.defineProperty 不具备的。
Proxy返回的是一个新对象,可以操作新对象达到目的,而Object.defineProperty只能遍历对象属性
Proxy性能更好
Proxy缺点
5. Proxy 兼容性差 ⽬前并没有⼀个完整⽀持 Proxy 所有拦截⽅法的Polyfill⽅案
Object.defineProperty缺点
只能劫持对象的属性,因此需要对每个属性进行遍历。
vue不能检测到对象属性的添加或删除,使用Vue.set
不能监听数组长度变化,通过改变.length无法被监听
vue做的处理:Vue使用了重写原型的方案代替。拦截了数组的一些方法,在这个过程中再去做通知变化等操作。
vue是循环数组八大方法并加以修改实现监听
第一步:先获取原生 Array 的原型方法,(因为拦截后还是需要原生的方法帮我们实现数组的变化。)
第二步:对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作。
第三步:把需要被拦截的 Array 类型的数据原型指向改造后原型。
vue做的处理:Vue使用了重写原型的方案代替。拦截了数组的一些方法,在这个过程中再去做通知变化等操作。
vue是循环数组八大方法并加以修改实现监听
第一步:先获取原生 Array 的原型方法,(因为拦截后还是需要原生的方法帮我们实现数组的变化。)
第二步:对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作。
第三步:把需要被拦截的 Array 类型的数据原型指向改造后原型。
vue2如何检查数组变化
Vue重写Array原型。循环数组八大方法并加以修改实现监听
对 Array 的原型方法使用Object.defineProperty 做一些拦截操作。把Array的数据原型指向改造后原型。
因为原型链的机制,找到对应的⽅法就不会继续往上找了。编译⽅法中会对⼀些会增加索引的
变异的本质就在这些方法内部加上自定义的逻辑,其实就是想监听这些方法的调用。
Vue中默认的做法就是在数组实例与它的原型之间,插入了一个新的原型对象,这个原型方法实现了这些变异方法,也就拦截了真正数组原型上的方法(因为原型链的机制,找到了就不会继续往上找了)。 变异方法中增加了自定义逻辑,也调用了真正数组原型上的方法,即实现了目的,也不会对正常使用造成影响。
如果浏览器不支持___proto__这个属性,Vue则直接在数组实例上增加这些变异方法。
Vue中默认的做法就是在数组实例与它的原型之间,插入了一个新的原型对象,这个原型方法实现了这些变异方法,也就拦截了真正数组原型上的方法(因为原型链的机制,找到了就不会继续往上找了)。 变异方法中增加了自定义逻辑,也调用了真正数组原型上的方法,即实现了目的,也不会对正常使用造成影响。
如果浏览器不支持___proto__这个属性,Vue则直接在数组实例上增加这些变异方法。
vue 设计原则的理解
渐进式vue框架
高效性
易用性
灵活性
spa/seo/ssr是什么
vue为什么要求组件模板只能有一个根元素
一个 vue 单文件就是一个 vue 实例(组件最后被webpack转换成js),而 <template> 标签中的内容就是 Vue 实例接管形成虚拟 DOM 的那部分内容,如果 template 下有多个 div ,那么无法指定 vue 实例的入口,所以需要通过唯一根节点,递归遍历整个vue树下的所有节点,并处理为vdom,最后再渲染成真正的 HTML,插入在正确的位置。
总结:需要构建VNode,而VNode只能有一个根节点(其内只能有一个节点是因为当前构建和 diff virutalDOM 的算法还未支撑这样的结构,也很难在保证性能的情况下支撑。)
vue3 fragmrnt 不再需要唯一跟元素,原理:将写的多个标签使用fragment标签包裹,是一个虚拟标签,不是真实 DOM 树的一部分,不会参与渲染
总结:需要构建VNode,而VNode只能有一个根节点(其内只能有一个节点是因为当前构建和 diff virutalDOM 的算法还未支撑这样的结构,也很难在保证性能的情况下支撑。)
vue3 fragmrnt 不再需要唯一跟元素,原理:将写的多个标签使用fragment标签包裹,是一个虚拟标签,不是真实 DOM 树的一部分,不会参与渲染
不同组件之间的嵌套,她们的生命周期是怎样
渲染过程:
⽗组件挂载/更新/销毁完成⼀定是等⼦组件都挂载/更新/销毁完成后,才算是⽗组件挂载/更新/销毁完,
所以⽗组件的mounted在⼦组件mouted之后
⽗beforeCreate -> ⽗created -> ⽗beforeMount -> ⼦beforeCreate -> ⼦created -> ⼦beforeMount
-> ⼦mounted -> ⽗mounted
⼦组件更新过程:
1. 影响到⽗组件: ⽗beforeUpdate -> ⼦beforeUpdate->⼦updated -> ⽗updted
2. 不影响⽗组件: ⼦beforeUpdate -> ⼦updated
⽗组件更新过程:
1. 影响到⼦组件: ⽗beforeUpdate -> ⼦beforeUpdate->⼦updated -> ⽗updted
2. 不影响⼦组件: ⽗beforeUpdate -> ⽗updated
销毁过程:
⽗beforeDestroy -> ⼦beforeDestroy -> ⼦destroyed -> ⽗destroyed
看起来很多好像很难记忆,其实只要理解了,不管是哪种情况,都⼀定是⽗组件等待⼦组件完成后,才
会执⾏⾃⼰对应完成的钩⼦,就可以很容易记住
⽗组件挂载/更新/销毁完成⼀定是等⼦组件都挂载/更新/销毁完成后,才算是⽗组件挂载/更新/销毁完,
所以⽗组件的mounted在⼦组件mouted之后
⽗beforeCreate -> ⽗created -> ⽗beforeMount -> ⼦beforeCreate -> ⼦created -> ⼦beforeMount
-> ⼦mounted -> ⽗mounted
⼦组件更新过程:
1. 影响到⽗组件: ⽗beforeUpdate -> ⼦beforeUpdate->⼦updated -> ⽗updted
2. 不影响⽗组件: ⼦beforeUpdate -> ⼦updated
⽗组件更新过程:
1. 影响到⼦组件: ⽗beforeUpdate -> ⼦beforeUpdate->⼦updated -> ⽗updted
2. 不影响⼦组件: ⽗beforeUpdate -> ⽗updated
销毁过程:
⽗beforeDestroy -> ⼦beforeDestroy -> ⼦destroyed -> ⽗destroyed
看起来很多好像很难记忆,其实只要理解了,不管是哪种情况,都⼀定是⽗组件等待⼦组件完成后,才
会执⾏⾃⼰对应完成的钩⼦,就可以很容易记住
vue的生命周期
1、new vue()
2、初始化生命周期钩子
2、初始化生命周期钩子
beforeCreate :
(实例初始化之后,el和data还没初始化,
无法访问methods、data、computed)
创建前:data:undefined $el:undefined
(实例初始化之后,el和data还没初始化,
无法访问methods、data、computed)
创建前:data:undefined $el:undefined
data observer和事件配置调用
初始化内部事件 inject,provide state
created:
(data已经初始化,
完成dataObserver/计算属性/event/watch事件回调,
dom还没挂载)
已创建:data:{} ,$el:undefined
(data已经初始化,
完成dataObserver/计算属性/event/watch事件回调,
dom还没挂载)
已创建:data:{} ,$el:undefined
beforeMount:
编译模版compile
(挂载之前,render函数首次被调用成虚拟Dom)
准备挂载 compile:data:xxx;$el:xxxx(虚拟)
编译模版compile
(挂载之前,render函数首次被调用成虚拟Dom)
准备挂载 compile:data:xxx;$el:xxxx(虚拟)
VNode替换成真实DOM
mounted:
(挂载完成,dom树已经完成
渲染到页面,可以进行dom操作)
挂载完成:虚拟 替换真实 data:xxx;$el:xx(真实)
(挂载完成,dom树已经完成
渲染到页面,可以进行dom操作)
挂载完成:虚拟 替换真实 data:xxx;$el:xx(真实)
beforeUpdate:
(数据有更新被调用,发生在Dom重新渲染和打补丁之前,
可以进一步修改状态,不会触发重新渲染过程)
(数据有更新被调用,发生在Dom重新渲染和打补丁之前,
可以进一步修改状态,不会触发重新渲染过程)
Updated:(虚拟dom重新渲染补丁,以最小dom开支来渲染
不建议这步进行状态修改,可能会导致无限循环)
不建议这步进行状态修改,可能会导致无限循环)
beforeDestroy:
(在实例销毁之前,实例还能使用,
可以做一些清除定时器,监听dom事件的操作)
(在实例销毁之前,实例还能使用,
可以做一些清除定时器,监听dom事件的操作)
清除watcher,子组件,事件监听器等
destroyed:
(组件被销毁后调用,事件监听会被移除,
所有实例都会被销毁,但是data和$el还是可以取到,
只会销毁实例,不会清除dom)
(组件被销毁后调用,事件监听会被移除,
所有实例都会被销毁,但是data和$el还是可以取到,
只会销毁实例,不会清除dom)
路由
路由原理
更新路由但不重新请求页面,是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有2种方式:
1、利用URL中的hash("#");
2、利用HTML5 History interface在中新增的方法pushState和replaceState;
1、利用URL中的hash("#");
2、利用HTML5 History interface在中新增的方法pushState和replaceState;
在初始化对应的history之前,会对mode做校验:若浏览器不支持HTML5History方式(通过supportsPushState变量判断),
则mode设为hash;若不是在浏览器环境下运行,则mode设为abstract;
VueRouter类中的onReady(),push()等方法只是一个代理,实际是调用的具体history对象和location的对应方法
则mode设为hash;若不是在浏览器环境下运行,则mode设为abstract;
VueRouter类中的onReady(),push()等方法只是一个代理,实际是调用的具体history对象和location的对应方法
$router.push()-->HashHistory.push()-->History.transitionTo()-->History.updateRoute()-->{app._route=route}-->vm.render()
vue路由的实现
hash
HashHistory.push
1、hash虽然出现在url中,但不会被包括在http请求中,它是用来指导浏览器动作的,对服务器端完全无用,因此,改变hash不会重新加载页面。
2、可以为hash的改变添加监听事件:
window.addEventListener("hashchange",funcRef,false),当路由改变,会触发vue实例的render()
3、每一次改变hash(window.location.hash),都会在浏览器访问历史window.history中增加一个记录。
2、可以为hash的改变添加监听事件:
window.addEventListener("hashchange",funcRef,false),当路由改变,会触发vue实例的render()
3、每一次改变hash(window.location.hash),都会在浏览器访问历史window.history中增加一个记录。
$router.push() = window.location.hash=route.fullPath
HashHistroy.replace()
replace()方法与push()方法不同之处在于,它并不是将新路由添加到浏览器访问历史栈顶,而是替换掉当前的路由:
不是直接对window.location.hash进行赋值,而是调用window.location.replace方法将路由进行替换。
$router.replace() = window.location.replace()
history
HTML5History
从HTML5开始,History interface提供了2个新的方法:pushState(),replaceState()使得我们可以对浏览器历史记录栈进行修改:
这2个方法有个共同的特点:当调用他们修改浏览器历史栈后,虽然当前url改变了,但浏览器不会立即发送请求该url,这就为单页应用前端路由,更新视图但不重新请求页面提供了基础。
window.history.pushState(stateObject,title,url)
window.history,replaceState(stateObject,title,url)
这2个方法有个共同的特点:当调用他们修改浏览器历史栈后,虽然当前url改变了,但浏览器不会立即发送请求该url,这就为单页应用前端路由,更新视图但不重新请求页面提供了基础。
window.history.pushState(stateObject,title,url)
window.history,replaceState(stateObject,title,url)
监听
重写 history.pushState/replaceState 使其在执行后触发一个自定义事件
我们通过监听这个自定义事件来接收视图变化通知
我们通过监听这个自定义事件来接收视图变化通知
vue路由的钩子函数
全局的钩子函数
1/router.beforeEach (to,from,next) 路由改变前调用 常用验证用户权限
参数
to :即将要进入的目标路由对象
from:当前正要离开的路由对象
next:路由控制参数
next():如果一切正常,则调用这个方法进入下一个钩子
next(false):中断路由(即路由不发生改变)
next('/login'):然后进行一个新的导航
next(new Error()):如果一个 Error 实例,则路由会被终止且该错误会被传递给 router.onError ()
参数
to :即将要进入的目标路由对象
from:当前正要离开的路由对象
next:路由控制参数
next():如果一切正常,则调用这个方法进入下一个钩子
next(false):中断路由(即路由不发生改变)
next('/login'):然后进行一个新的导航
next(new Error()):如果一个 Error 实例,则路由会被终止且该错误会被传递给 router.onError ()
router.beforeResolve(to,from,next) 调用全局路由解析守卫 ,在router.beforeEach和afterEach中间调用
2/router.afterEach (to,from) 路由改变后的钩子 常用自动让页面返回最顶端 用法相似,少了 next 参数
一般写在main.js可以做权限控制
单个的路由配置的钩子函数
router.beforeEnter (to,from,next)
组件内的钩子函数
钩子函数介绍
1. router.beforeRouteEnter (to,from,next)
该组件的对应路由被 comfirm 前调用。
此时实例还没被创建,所以不能获取实例(this)
1. router.beforeRouteEnter (to,from,next)
该组件的对应路由被 comfirm 前调用。
此时实例还没被创建,所以不能获取实例(this)
2. router.beforeRouteUpdate (to,from,next)
当前路由改变,但改组件被复用时候调用
该函数内可以访问组件实例(this)
当前路由改变,但改组件被复用时候调用
该函数内可以访问组件实例(this)
3. router.beforeRouteLeave (to,from,next)
当导航离开组件的对应路由时调用。
该函数内可以访问获取组件实例(this)
当导航离开组件的对应路由时调用。
该函数内可以访问获取组件实例(this)
应用场景
(一) 清除当前组件中的定时器
(二) 当页面中有未关闭的窗口, 或未保存的内容时, 阻止页面跳转
(三) 保存相关内容到Vuex中或Session中
路由调用顺序
不同组件之间的路由跳转 A-B(离开A组件,进入B组件)
:A组件beforeRouteLeave->全局beforeEach->B路由独享守卫beforeEnter->B组件beforRouteEnter->
全局路由解析beforResolve->全局afterEach->组件B渲染
:A组件beforeRouteLeave->全局beforeEach->B路由独享守卫beforeEnter->B组件beforRouteEnter->
全局路由解析beforResolve->全局afterEach->组件B渲染
复用组件的路由跳转 A-A:路由参数修改->全局路由beforeEach->复用组件beforeRouteUpdate->全局afterEach->更新Dom
vue如何实现异步渲染
Vue的异步渲染实际上是在数据每次变化时,将其所要引起页面变化的部分都放到一个异步API的回调函数里,直到同步代码执行完之后,异步回调开始执行,最终将同步代码里所有的需要渲染变化的部分合并起来,去掉重复修改的操作,最终执行一次渲染操作。这里使用异步API的优先级是promise.then>MutationObserver>setImmediate>setTimeout
如果同时两次修改val,当val第一次赋值时,页面会渲染出对应的文字,但是实际这个渲染变化会暂存,val第二次赋值时,再次暂存将要引起的变化,这些变化操作会被丢到异步API,Promise.then的回调函数中,等到所有同步代码执行完后,then函数的回调函数得到执行,然后将遍历存储着数据变化的全局数组,将所有数组里数据确定先后优先级,最终合并成一套需要展示到页面上的数据,执行页面渲染操作操作。
异步队列执行后,存储页面变化的全局数组得到遍历执行,执行的时候会进行一些筛查操作,将重复操作过的数据进行处理,实际就是先赋值的丢弃不渲染,最终按照优先级最终组合成一套数据渲染。
这里触发渲染的异步API优先考虑Promise,其次MutationObserver,如果没有MutationObserver的话,会考虑setImmediate,没有setImmediate的话最后考虑是setTimeout。
如果同时两次修改val,当val第一次赋值时,页面会渲染出对应的文字,但是实际这个渲染变化会暂存,val第二次赋值时,再次暂存将要引起的变化,这些变化操作会被丢到异步API,Promise.then的回调函数中,等到所有同步代码执行完后,then函数的回调函数得到执行,然后将遍历存储着数据变化的全局数组,将所有数组里数据确定先后优先级,最终合并成一套需要展示到页面上的数据,执行页面渲染操作操作。
异步队列执行后,存储页面变化的全局数组得到遍历执行,执行的时候会进行一些筛查操作,将重复操作过的数据进行处理,实际就是先赋值的丢弃不渲染,最终按照优先级最终组合成一套数据渲染。
这里触发渲染的异步API优先考虑Promise,其次MutationObserver,如果没有MutationObserver的话,会考虑setImmediate,没有setImmediate的话最后考虑是setTimeout。
你知道 nextTick 的原理吗?
原理就是在dom异步更新的队列后面添加nextTick的回调
1、nextTick是Vue提供的⼀个全局API,由于vue的异步更新策略导致我们对数据的修改不会⽴刻体现在dom变化上,
此时如果想要⽴即获取更新后的dom状态,就需要使⽤这个⽅法
2. Vue 在更新 DOM 时是异步执⾏的。只要侦听到数据变化,Vue 将开启⼀个队列
并缓冲在同⼀事件循环中。 nextTick 回调被添加到dom修改的异步队列后面
(vue 进行DOM 更新内部也是调用nextTick来做异步队列控制。而当我们自己调用 nextTick 的时候,它就在更新 DOM 的那个异步队列后追加了nextTick的回调函数,)从而确保我们的代码在 DOM 更新后执行,同时也避免了 setTimeout 可能存在 的多次执行问 题。
nextTick即有可能是微任务,也有可能是宏任务,从优先去Promise和MutationObserver可以看出nextTick优先微任务,其次是setImmediate和setTimeout宏任务。
如何避免同一个数据被多次修改引起的重复渲染:
如果同⼀个 watcher 被多次触发,只会被推⼊到队列中⼀次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是⾮常重要的。将多次数据变化所引起的响应变化收集后合并成一次页面渲染,从而更合理的利用机器资源,提升性能与用户体验
1、nextTick是Vue提供的⼀个全局API,由于vue的异步更新策略导致我们对数据的修改不会⽴刻体现在dom变化上,
此时如果想要⽴即获取更新后的dom状态,就需要使⽤这个⽅法
2. Vue 在更新 DOM 时是异步执⾏的。只要侦听到数据变化,Vue 将开启⼀个队列
并缓冲在同⼀事件循环中。 nextTick 回调被添加到dom修改的异步队列后面
(vue 进行DOM 更新内部也是调用nextTick来做异步队列控制。而当我们自己调用 nextTick 的时候,它就在更新 DOM 的那个异步队列后追加了nextTick的回调函数,)从而确保我们的代码在 DOM 更新后执行,同时也避免了 setTimeout 可能存在 的多次执行问 题。
nextTick即有可能是微任务,也有可能是宏任务,从优先去Promise和MutationObserver可以看出nextTick优先微任务,其次是setImmediate和setTimeout宏任务。
如何避免同一个数据被多次修改引起的重复渲染:
如果同⼀个 watcher 被多次触发,只会被推⼊到队列中⼀次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是⾮常重要的。将多次数据变化所引起的响应变化收集后合并成一次页面渲染,从而更合理的利用机器资源,提升性能与用户体验
nextTick 优先使用微任务
(微任务会先于宏任务,微任务执行完后会进入浏览器更新渲染阶段,所以更新渲染前使用微任务会比宏任务块),
其次再到宏任务。优先级为promise.then> MutationObserver> setImmediate> setTimeout。
注意:之所以将 nextTick 的回调函数放⼊到数组中⼀次性执⾏,⽽不是直接在 nextTick 中执⾏回调函数,
是为了保证在同⼀个tick内多次执⾏了 nextTcik ,不会开启多个异步任务,
⽽是把这些异步任务都压成⼀个同步任务,在下⼀个tick内执⾏完毕。
(微任务会先于宏任务,微任务执行完后会进入浏览器更新渲染阶段,所以更新渲染前使用微任务会比宏任务块),
其次再到宏任务。优先级为promise.then> MutationObserver> setImmediate> setTimeout。
注意:之所以将 nextTick 的回调函数放⼊到数组中⼀次性执⾏,⽽不是直接在 nextTick 中执⾏回调函数,
是为了保证在同⼀个tick内多次执⾏了 nextTcik ,不会开启多个异步任务,
⽽是把这些异步任务都压成⼀个同步任务,在下⼀个tick内执⾏完毕。
Vue 的模板编译原理
Template > ast 抽象语法树> render function > 执行 render function > VNode
第⼀步:解析
将模板字符串解析⽣成 AST,⽣成的AST 元素节点总共有 3 种类型,1 为普通元素, 2 为表达式,3为纯⽂本。
第⼆步:优化语法树
Vue 模板中并不是所有数据都是响应式的,有很多数据是⾸次渲染后就永远不会变化的,那么这部分数据⽣成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的⽐对。
此阶段会深度遍历⽣成的 AST 树,检测它的每⼀颗⼦树是不是静态节点,如果是静态节点则它们⽣成DOM 永远不需要改变,这对运⾏时对模板的更新起到极⼤的优化作⽤。
1. ⽣成代码
const code = generate(ast, options)
通过 generate ⽅法,将ast⽣成 render 函数。
第⼀步:解析
将模板字符串解析⽣成 AST,⽣成的AST 元素节点总共有 3 种类型,1 为普通元素, 2 为表达式,3为纯⽂本。
第⼆步:优化语法树
Vue 模板中并不是所有数据都是响应式的,有很多数据是⾸次渲染后就永远不会变化的,那么这部分数据⽣成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的⽐对。
此阶段会深度遍历⽣成的 AST 树,检测它的每⼀颗⼦树是不是静态节点,如果是静态节点则它们⽣成DOM 永远不需要改变,这对运⾏时对模板的更新起到极⼤的优化作⽤。
1. ⽣成代码
const code = generate(ast, options)
通过 generate ⽅法,将ast⽣成 render 函数。
Vue 组件 data 为什么必须是个函数而 Vue 的根实例则没有此限制
组件中的data写成⼀个函数,是给每个组件实例创建⼀个私有的数据空间,让各个组件实例维护各⾃的数据。如果写成对象形式,那么所有组件实例共⽤了⼀个数据空间data,就会造成数据污染,但由于根实例只有一个,所以不存在数据污染这种情况,也就可以使用对象了。
你了解哪些 Vue 性能优化方法
编码阶段
组件
keep-alive
data数据减少
Object.freeze不需要做响应
异步组件
路由懒加载
函数式组件
列表
v-if和v-for不要同时使用
列表绑定事件用事件代理
key保证唯一,不要用索引
⻓列表滚动到可视区域动态加载
其他
防抖节流
第三⽅模块按需导⼊
import {} from 'loadsh'
import find from 'loadsh/finds'
import find from 'loadsh/finds'
SEO优化
预渲染
服务端渲染SSR
打包优化
压缩代码
Tree Shaking/Scope Hoisting
使⽤cdn加载第三⽅模块
多线程打包happypack
splitChunks抽离公共⽂件
sourceMap优化
⽤户体验
⻣架屏
PWA
还可以使⽤缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
PWA
还可以使⽤缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
new Vue()都做了什么?
1. new Vue()是创建Vue实例,它内部执⾏了根实例的初始化过程。
2. 具体包括以下操作:
选项合并options, (⽤户选项、默认选项)
生命周期初始化
$children, $refs, $slots, $createElement等实例属性和⽅法初始化
render初始化
--------beforCreate-------
init inject
数据响应式和事件处理(methods,watch,computed,props)
init provide
-----created----
等待挂载
3. 总结:new Vue()创建了根实例并准备好数据和⽅法,未来执⾏挂载时,
此过程还会递归的应⽤于它的⼦组件上,最终形成⼀个有紧密关系的组件实例树。
2. 具体包括以下操作:
选项合并options, (⽤户选项、默认选项)
生命周期初始化
$children, $refs, $slots, $createElement等实例属性和⽅法初始化
render初始化
--------beforCreate-------
init inject
数据响应式和事件处理(methods,watch,computed,props)
init provide
-----created----
等待挂载
3. 总结:new Vue()创建了根实例并准备好数据和⽅法,未来执⾏挂载时,
此过程还会递归的应⽤于它的⼦组件上,最终形成⼀个有紧密关系的组件实例树。
computed 和 watch 有什么区别
computed 计算属性,是依赖其他属性的计算值,并且有缓存,只有当依赖的值变化时才会更新。
watch 是在监听的属性发⽣变化时,在回调中执⾏⼀些逻辑。
所以, computed 适合在模板渲染中,某个值是依赖了其他的响应式对象甚⾄是计算属性计算⽽来,
⽽ watch 适合监听某个值的变化去完成⼀段复杂的业务逻辑。
watch支持异步,computed不支持异步
watch 会生成一个watcher对象,在监视的属性每次变动时都会触发回调
computed 则是生成一个惰性的watcher,只有在取值操作(getter触发)时收集依赖且计算值 当有依赖变动时仅将 dirty 置为 true,不做计算操作 当有取值操作时,根据自身标记 dirty 属性返回上一次计算结果/重新计算值
computed 则是生成一个惰性的watcher,只有在取值操作(getter触发)时收集依赖且计算值 当有依赖变动时仅将 dirty 置为 true,不做计算操作 当有取值操作时,根据自身标记 dirty 属性返回上一次计算结果/重新计算值
Vue.component()、
Vue.use()、
Vue.directive,
this.$xxx()
Vue.use()、
Vue.directive,
this.$xxx()
Vue.component()方法注册全局组件。
第一个参数是自定义元素名称,也就是将来在别的组件中使用这个组件的标签名称。
第二个参数是将要注册的Vue组件。
vue.use()注册插件
Vue.use注册插件,这接收一个参数。这个参数必须具有install方法。Vue.use函数内部会调用参数的install方法。
如果插件没有被注册过,那么注册成功之后会给插件添加一个installed的属性值为true。Vue.use方法内部会检测插件的installed属性,从而避免重复注册插件。
插件的install方法将接收两个参数,第一个是参数是Vue,第二个参数是配置项options。
在install方法内部可以添加全局方法或者属性
Vue的prototype
vue指令(v-report)
Vue.directive('report', report(options))
自定义指令提供了几个钩子函数:
bind:指令第一次绑定到元素时调用
inserted:被绑定元素插入父节点时调用
update:所在组件的 VNode 更新时调用
自定义指令提供了几个钩子函数:
bind:指令第一次绑定到元素时调用
inserted:被绑定元素插入父节点时调用
update:所在组件的 VNode 更新时调用
Vue 实现一个高阶组件
高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件。
在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为。
在不改变对象自身的前提下在程序运行期间动态的给对象添加一些额外的属性或行为。
特点
1、高阶组件(HOC)应该是无副作用的纯函数,且不应该修改原组件
2、高阶组件(HOC)不关心你传递的数据(props)是什么,并且被包装组件(WrappedComponent)不关心数据来源
3、高阶组件(HOC)接收到的 props 应该透传给被包装组件(WrappedComponent)
vue的runtime版本和带有compiler的版本
Runtime Only
vue-loader预编译+runtime
vue-loader预编译+runtime
1、指定render函数
2、通常借助webpack的vue-loader工具,在构建时进行了预编译(将.vue文件编译为js)。所以该版本只包含运行时的 Vue.js 代码
3、webpack打包时已经将template编译为render函数(template 会通过 vue-template-compiler 转换为 render 函数),不需要在客户端进行编译
2、通常借助webpack的vue-loader工具,在构建时进行了预编译(将.vue文件编译为js)。所以该版本只包含运行时的 Vue.js 代码
3、webpack打包时已经将template编译为render函数(template 会通过 vue-template-compiler 转换为 render 函数),不需要在客户端进行编译
runtime-compiler
运行时编译
运行时编译
编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码。
1、有指定template字符串,如上图,或者挂载到一个元素上并以其 DOM 内部的 HTML 作为模板
2、如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,需要在客户端编译模板
3、打包时不进行编译,在运行的时候,才去编译 template
4、vue.js体积较大,
1、有指定template字符串,如上图,或者挂载到一个元素上并以其 DOM 内部的 HTML 作为模板
2、如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,需要在客户端编译模板
3、打包时不进行编译,在运行的时候,才去编译 template
4、vue.js体积较大,
vue运行时版本和带编译的版本区别
运行时构建: 不包含模板编译器(Runtime Only)
渲染过程: render -> Virtual DOM -> UI
独立构建:包含模板编译器(runtime-compiler)
渲染过程: template -> AST(抽象语法树) -> render -> Virtual DOM -> UI
渲染过程: render -> Virtual DOM -> UI
独立构建:包含模板编译器(runtime-compiler)
渲染过程: template -> AST(抽象语法树) -> render -> Virtual DOM -> UI
运行时版本(Runtime Only)和带编译(runtime-compiler)的版本主要区别在于对template的处理,使用带编译功能的vue版本,能支持template属性。
一句话来讲就是:如果有render函数就可以使用运行时版本的vuejs,否则必须使用Runtime + Compiler版本的vuejs。
一句话来讲就是:如果有render函数就可以使用运行时版本的vuejs,否则必须使用Runtime + Compiler版本的vuejs。
template逻辑
1、先判断是否有template属性
2、如果没有,则直接通过el中的html代码作为模版
3、如果有,判断是否是字符串(非字符串的形式暂不讨论)
4、是字符串的情况下,是否以#字符开头
5、如果是,则获取对应id的innerHTML作为模版
6、如果不是以#字符开头,则直接作为作为模版
2、如果没有,则直接通过el中的html代码作为模版
3、如果有,判断是否是字符串(非字符串的形式暂不讨论)
4、是字符串的情况下,是否以#字符开头
5、如果是,则获取对应id的innerHTML作为模版
6、如果不是以#字符开头,则直接作为作为模版
vue-loader预编译+runtime :"开发时预编译,上线使用运行时”的方式既满足了开发需要又减少了线上文件的大小。
指令的本质
自定义指令生命周期
bind
组件生命周期render之后
组件生命周期render之后
bind:只调用一次,指令第一次绑定到元素时调用,用这个钩子函数可以定义一个绑定时执行一次的初始化动作。
inserted
insert再mounted之后
insert再mounted之后
被绑定元素插入父节点时调用(父节点存在即可调用,不必存在于document中)。
update
被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。
通过比较更新前后的绑定值,可以忽略不必要的模板更新。
通过比较更新前后的绑定值,可以忽略不必要的模板更新。
componentUpdated
componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
unbind
unbind在destroyed之后
unbind在destroyed之后
unbind:只调用一次,指令与元素解绑时调用。
指令工作原理
初始化
初始化全局API时,调用createPatchFunction生成VNode转换为真实DOM的patch方法,
初始化中比较重要一步是定义了与DOM节点相对应的hooks方法,
在DOM的创建(create)、激活(avtivate)、更新(update)、移除(remove)、销毁(destroy)过程中,
分别会轮询调用对应的hooks方法,这些hooks中一部分是指令声明周期的入口。
初始化中比较重要一步是定义了与DOM节点相对应的hooks方法,
在DOM的创建(create)、激活(avtivate)、更新(update)、移除(remove)、销毁(destroy)过程中,
分别会轮询调用对应的hooks方法,这些hooks中一部分是指令声明周期的入口。
模板编译
模板编译就是解析指令参数,具体解构后的ASTElement如下所示:
生成渲染方法
vue推荐采用指令的方式去操作DOM,由于自定义指令可能会修改DOM或者属性,所以避免指令对模板解析的影响,
在生成渲染方法时,首先处理的是指令,如v-model,本质是一个语法糖,在拼接渲染函数时,
会给元素加上value属性与input事件(以input为例,这个也可以用户自定义)。
在生成渲染方法时,首先处理的是指令,如v-model,本质是一个语法糖,在拼接渲染函数时,
会给元素加上value属性与input事件(以input为例,这个也可以用户自定义)。
生成真实DOM
在patch过程中,每此调用createElm生成真实DOM时,都会检测当前VNode是否存在data属性,存在,
则会调用invokeCreateHooks,走初创建的钩子函数
指令钩子入口insert(parentElm, vnode.elm, refElm)
则会调用invokeCreateHooks,走初创建的钩子函数
指令钩子入口insert(parentElm, vnode.elm, refElm)
执行过程
对于首次创建,执行过程
调用当前元素中所有bind钩子方法。
检测指令中是否存在inserted钩子,如果存在,则将insert钩子合并到VNode.data.hooks属性中。
DOM挂载结束后,会执行invokeInsertHook,
所有已挂载节点,如果VNode.data.hooks中存在insert钩子。有则会调用
所有已挂载节点,如果VNode.data.hooks中存在insert钩子。有则会调用
更新过程
一般首次创建只会走bind和inserted方法,而update和componentUpdated则与bind和inserted对应。
在组件依赖状态发生改变时,会用VNode diff算法,对节点进行打补丁式更新
在组件依赖状态发生改变时,会用VNode diff算法,对节点进行打补丁式更新
响应式数据发生改变,调用dep.notify,通知数据更新。
调用patchVNode,对新旧VNode进行差异化更新,并全量更新当前VNode属性(包括指令,就会进入updateDirectives方法)。
如果指令存在update钩子方法,调用update钩子方法,并初始化componentUpdated回调,将postpatch hooks挂载到VNode.data.hooks中。
当前节点及子节点更新完毕后,会触发postpatch hooks,即指令的componentUpdated方法
computer的源码实现
computed里面控制缓存最重要的一点就是脏数据标记为dirty, dirty是watcher的一个属性。
当dirty为true时,读取computed会重新计算
当dirty为false时,读取computed会使用缓存
当dirty为true时,读取computed会重新计算
当dirty为false时,读取computed会使用缓存
computed
页面P依赖计算属性C, 计算属性C又依赖data里面的A, computed更新步骤如下:
由于C依赖了A, A可以收集到C的watcher
当A发生变化时,会将watcher的脏数据标记位dirty设置为true
并且A会收集到页面P的watcher,A通知P进行更新,从而页面P重新读取计算属性C, 由于此时dirty为true,此时的计算属性会重新计算。
computed更新完毕,重新将脏数据标记位dirty设置为false,如果其依赖的A不发生改变,那下次再进入就会读取缓存
由于C依赖了A, A可以收集到C的watcher
当A发生变化时,会将watcher的脏数据标记位dirty设置为true
并且A会收集到页面P的watcher,A通知P进行更新,从而页面P重新读取计算属性C, 由于此时dirty为true,此时的计算属性会重新计算。
computed更新完毕,重新将脏数据标记位dirty设置为false,如果其依赖的A不发生改变,那下次再进入就会读取缓存
computed本身是通过代理的方式代理到组件实例上的,所以读取计算属性的时候,执行的是一个内部的getter,而不是用户定义的方法。
computed内部实现了一个惰性的watcher,在实例化的时候不会去求值,其内部通过dirty属性标记计算属性是否需要重新求值。当computed依赖的任一状态(不一定是return中的)发生变化,都会通知这个惰性watcher,让它把dirty属性设置为true。所以,当再次读取这个计算属性的时候,就会重新去求值。
computed内部实现了一个惰性的watcher,在实例化的时候不会去求值,其内部通过dirty属性标记计算属性是否需要重新求值。当computed依赖的任一状态(不一定是return中的)发生变化,都会通知这个惰性watcher,让它把dirty属性设置为true。所以,当再次读取这个计算属性的时候,就会重新去求值。
如何简单实现一个mvvm模型
v-model与v-bind区别
1、首先是使用范围:
1)v-bind适用于所有元素,且主要是绑定元素的attributions(包括class、name、src、value、id、style等);
2)v-model只能用于表单元素,表单之外的元素使用了无意义,且只能绑定元素的值,其他属性还是需要使用v-bind;
2、使用模式:
1)v-model是MVVM模式(双向),即可以通过改变Model(即data数据)来改变View;也可以通过改变View来改变Model;
2)v-bind是MV模式(单向),即通过控制Model来改变View;
1)v-bind适用于所有元素,且主要是绑定元素的attributions(包括class、name、src、value、id、style等);
2)v-model只能用于表单元素,表单之外的元素使用了无意义,且只能绑定元素的值,其他属性还是需要使用v-bind;
2、使用模式:
1)v-model是MVVM模式(双向),即可以通过改变Model(即data数据)来改变View;也可以通过改变View来改变Model;
2)v-bind是MV模式(单向),即通过控制Model来改变View;
v-model是value+input的语法糖,是v-bind和v-on的简洁写法。v-model就实现了双向数据绑定
v-bind绑定一个value属性
v-on指令给当前元素绑定input事件
v-bind绑定一个value属性
v-on指令给当前元素绑定input事件
react基础
原理
setState()是异步更新数据的
父组件引起子组件更新,纯组件利用shouldComponentUpdate提升性能
虚拟Dom和diff结合保证更新效率
虚拟Dom是state+JSX 结合生成
虚拟Dom真正的价值不只是性能,而是跨平台。虚拟dom摆脱浏览器限制,只要在可以运行js的环境下都可以使用
路由原理
1、点击Link组件(a标签),修改了浏览器地址栏的url
2、React路由监听到地址栏url的变化
3、React路由内部遍历所有Router组件,使用路由规则(path)与pathNmae进行匹配
4、当路由规则(path)能够匹配搭配地址栏中的path时,展示Route组件内容
路由基础
Router组件包裹整个应用,只需要使用一次
Link组件是入口,Route是组件出口
通过props.history实现编程式导航
默认模糊匹配,添加exect变精确匹配
React路由一切都是组件,可以像思考组件一样思考路由
redux原理
redux和vuex
相同点
state 共享数据
流程一致:定义全局state,触发,修改state
原理相似,通过全局注入store。
不同点
从实现原理上来说
Redux 使用的是不可变数据,而Vuex的数据是可变的。Redux每次都是用新的state替换旧的state,而Vuex是直接修改
Redux 在检测数据变化的时候,是通过 diff 的方式比较差异的,而Vuex其实和Vue的原理一样,是通过 getter/setter来比较的
从表现层来说
vuex定义了state、getter、mutation、action四个对象;
redux定义了state、reducer、action。
redux定义了state、reducer、action。
vuex中state统一存放,方便理解;
redux state依赖所有reducer的初始值
redux state依赖所有reducer的初始值
vuex有getter,目的是快捷得到state;redux没有这层,
react-redux mapStateToProps参数做了这个工作。
react-redux mapStateToProps参数做了这个工作。
vuex中mutation只是单纯赋值(很浅的一层);
redux中reducer只是单纯设置新state(很浅的一层)。他俩作用类似,但书写方式不同
redux中reducer只是单纯设置新state(很浅的一层)。他俩作用类似,但书写方式不同
vuex中action有较为复杂的异步ajax请求;
redux中action中可简单可复杂,简单就直接发送数据对象({type:xxx, your-data}),复杂需要调用异步ajax(依赖redux-thunk插件)。
redux中action中可简单可复杂,简单就直接发送数据对象({type:xxx, your-data}),复杂需要调用异步ajax(依赖redux-thunk插件)。
vuex触发方式有两种commit同步和dispatch异步;redux同步和异步都使用dispatch
vuex的流向:
view——>commit——>mutations——>state变化——>view变化(同步操作)
view——>dispatch——>actions——>mutations——>state变化——>view变化(异步操作)
redux的流向:view——>actions——>reducer——>state变化——>view变化(同步异步一样)
view——>commit——>mutations——>state变化——>view变化(同步操作)
view——>dispatch——>actions——>mutations——>state变化——>view变化(异步操作)
redux的流向:view——>actions——>reducer——>state变化——>view变化(同步异步一样)
组件之间通信
props
childer props
render props
消息订阅-发布
pubs-sub(消息订阅-发布)
event
集中式管理
redux
Context|Provider|Consumer
const {Provider,Consumer} = React.createContext()
react异步
React 中 setState 什么时候是同步的,什么时候是 异步的
1、由 React 控制的事件处理程序,以及生命周期函数调用 setState 不会同步更 新 state 。
2、React 控制之外的事件中调用 setState 是同步更新的。比如原生 js 绑定的事 件,setTimeout/setInterval 等
我这里还是用最简单的语言让你理解:在 React 的 setState 函数实现中,会根据 isBatchingUpdates(默认是 false) 变量判断是否直接更新 this.state 还是放到队列中稍后更新。然后有一个 batchedUpdate 函数,可以修改 isBatchingUpdates 为 true,当 React 调用事件处理函数之前,或者生命周期函数之前就会调用 batchedUpdate 函数,这样的话,setState 就不会同步更新 this.state,而是放到更新队列里面后续更新。
这样你就可以理解为什么原生事件和 setTimeout/setinterval 里面调用 this.state 会同步更新了吧,因为通过这些函数调用的 React 没办法去调用 batchedUpdate 函数将 isBatchingUpdates 设置为 true,那么这个时候 setState 的时候默认就是 false,那么就会同步更新。
这样你就可以理解为什么原生事件和 setTimeout/setinterval 里面调用 this.state 会同步更新了吧,因为通过这些函数调用的 React 没办法去调用 batchedUpdate 函数将 isBatchingUpdates 设置为 true,那么这个时候 setState 的时候默认就是 false,那么就会同步更新。
前端工程化
构建和打包
webapck
webpack基础配置
开发时
entry
output
mode
webpack-dev-server、webpack-dev-middleware、sourceMap
plugins
解决loader无法解决的事
解决loader无法解决的事
html-webpack-plugin:自动在express根目录下生成html,且自动引入bundle.js
clean-webpack-plugin:自动清理dist
copy-webpack-plugin:将静态资源一起打包到dist里
BannerPlugin:在bundle.js加入注释
mini-css-extract-plugin:提取css到单独文件
clean-webpack-plugin:自动清理dist
copy-webpack-plugin:将静态资源一起打包到dist里
BannerPlugin:在bundle.js加入注释
mini-css-extract-plugin:提取css到单独文件
loader
用于对源码转换,处理webpack不能处理的资源,如css,ts,html,图片等
用于对源码转换,处理webpack不能处理的资源,如css,ts,html,图片等
css-loader、解析css文件
style-loader、在html页面中插入style标签
less-loader、sass-loader、
url-loader:将图片资源转成dataUrl并打包到文件中,设置limit,超出限制就使用file-loader,copy资源到单独文件夹中
1、如果页面图片较多,发很多http请求,会降低页面性能。这个问题可以通过url-loader解决。
url-loader会将引入的图片编码,生成dataURl并将其打包到文件中,最终只需要引入这个dataURL就能访问图片了。
当然,如果图片较大,编码会消耗性能。因此url-loader提供了一个limit参数,小于limit字节的文件会被转为DataURl,
大于limit的还会使用file-loader进行copy
file-loader:解决开发环境和生产环境下资源目录不一致的问题,
并加上hash配置来解决缓存问题(加载.jpg\png文件图片资源)、
babel-loader、将高级语法es6/es7解析成低版本的es5
style-loader、在html页面中插入style标签
less-loader、sass-loader、
url-loader:将图片资源转成dataUrl并打包到文件中,设置limit,超出限制就使用file-loader,copy资源到单独文件夹中
1、如果页面图片较多,发很多http请求,会降低页面性能。这个问题可以通过url-loader解决。
url-loader会将引入的图片编码,生成dataURl并将其打包到文件中,最终只需要引入这个dataURL就能访问图片了。
当然,如果图片较大,编码会消耗性能。因此url-loader提供了一个limit参数,小于limit字节的文件会被转为DataURl,
大于limit的还会使用file-loader进行copy
file-loader:解决开发环境和生产环境下资源目录不一致的问题,
并加上hash配置来解决缓存问题(加载.jpg\png文件图片资源)、
babel-loader、将高级语法es6/es7解析成低版本的es5
webpack 编译过程
是一个串行的过程
是一个串行的过程
初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
开始编译:用上一步得到的参数初始化 Compiler 对象,
加载所有配置的插件,执行对象的 run方法开始执行编译;
加载所有配置的插件,执行对象的 run方法开始执行编译;
确定入口:根据配置中的 entry 找出所有的入口文件;
编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,
再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,
得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,
再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
在以上过程中,Webpack 会在特定的时间点⼴播出特定的事件,插件在监听到感兴趣的事件后会执⾏特定的逻辑,
并且插件可以调⽤ Webpack 提供的 API 改变 Webpack 的运⾏结果。
并且插件可以调⽤ Webpack 提供的 API 改变 Webpack 的运⾏结果。
webpack的compiler和compilation
compiler
compiler对象包含了webpack环境所有的配置信息,这个对象在启动webpack时被一次性建立,
并配置好所有操作的设置,包括options,loader,plugin,当webpack环境中应用一个插件时,
插件将收到此compiler对象的引用,可以用来访问webpack的主环境
并配置好所有操作的设置,包括options,loader,plugin,当webpack环境中应用一个插件时,
插件将收到此compiler对象的引用,可以用来访问webpack的主环境
compilation
对象包含当前的环境资源、编译生产资源、变化的文件等,当webpack开发环境中间件时,
每当检测到一个文件变化,就会创建一个新的compilation,从而生成一组新的编译资源。
compilation对象也提供很多关键时机的回调,以供插件做自定义事件时使用
每当检测到一个文件变化,就会创建一个新的compilation,从而生成一组新的编译资源。
compilation对象也提供很多关键时机的回调,以供插件做自定义事件时使用
webpack高级配置
多页应用打包
entry
第三方库的引入方式
noparse,optimization.splitChunks
区分配置文件打包
webpack.dev.config.js
webpack.pro.config.js
环境变量
cross-env + process.env
devServer.proxy
HMR
loader
作用:帮助webpack处理非js内容,例如css,html,图片资源等,也可以是js内容,对匹配资源进行翻译转换加工
原理:从下至上,右至左加载loader,将源代码管道式进行加工处理后返回给下一个loader
如何编写loader
其实loader就是对外暴露一个函数
1、module.exports = function(source) {
//处理源码后返回
return source
}
webpack.config.js加载loader
rules: [{ test: /.js$/, use:"./loaders/myloader.js"; }]
常用loader
css-loader、解析css文件,使用style插入文件
less-loader、
sass-loader、
file-loader、处理本地和生产环境路径不一致问题
url-loader、处理图片资源等,转成dataUrl格式,超过limit的使用file-loader处理复制到dist目录内
vue-loader、
babel-loader
less-loader、
sass-loader、
file-loader、处理本地和生产环境路径不一致问题
url-loader、处理图片资源等,转成dataUrl格式,超过limit的使用file-loader处理复制到dist目录内
vue-loader、
babel-loader
plugins
原理
自定义插件就是在webpack编译过程的生命周期钩子中,进行编码开发,实现一些功能。
插件可以使用户直接触及到编译过程(compilation process)。
插件可以将处理函数(handler)注册到编译过程中的不同事件点上运行的生命周期钩子函数上。
而且能访问到当前编译(compilation)状态。------发布和订阅者模式
插件可以将处理函数(handler)注册到编译过程中的不同事件点上运行的生命周期钩子函数上。
而且能访问到当前编译(compilation)状态。------发布和订阅者模式
编写插件
创建一个类class或js构造函数,类中添加上定义一个 apply 方法(或函数的prototype上定义apply)。
- 从apply参数传入的compiler取出指定事件钩子,在钩子回调中取出complitaion对象的compile\emit\done
- 通过 compilation 处理 webpack 内部特定的实例数据
如果是插件是异步的,在插件的逻辑编写完后调用 webpack 提供的 callback
在webpack内部实现插件事件流机制的核心就在于tapable
Tapable包本质上是为我们更方面创建自定义事件和触发自定义事件的库,
类似于Nodejs中的EventEmitter Api。
类似于Nodejs中的EventEmitter Api。
Webpack中的插件机制就是基于Tapable实现与打包流程解耦,插件的所有形式都是基于Tapable实现。
1. 定义钩子
2. 在特定的阶段调用钩子,触发事件
3. 使用者注册事件
webpackCompiler注册了这些钩子并在编译期间触发调用,
所有注册了这些钩子的都会被触发调用
2. 在特定的阶段调用钩子,触发事件
3. 使用者注册事件
webpackCompiler注册了这些钩子并在编译期间触发调用,
所有注册了这些钩子的都会被触发调用
webpack插件的组成
es5实现
- 一个 JavaScript 命名函数。
- 在插件函数的 prototype 上定义一个 apply 方法。
- 指定一个绑定到 webpack 自身的事件钩子。
- 处理 webpack 内部实例的特定数据。
- 功能完成后调用 webpack 提供的回调
- 一个 JavaScript 命名函数。
- 在插件函数的 prototype 上定义一个 apply 方法。
- 指定一个绑定到 webpack 自身的事件钩子。
- 处理 webpack 内部实例的特定数据。
- 功能完成后调用 webpack 提供的回调
常用plugins
html-webpack-plugin: 打包时会自动生成index.html
devServer时根据模板在express项目根目录下生成html文件
自动引入bundle.js
terser-webpack-plugin 压缩js
mini-css-extract-plugin:提取css到单独文件
IgnorePlugin:忽略库部分内容,这部分不进行打包,如moment的本地化内容
optimize[ˈɒptɪmaɪz]-css-assets-webpack-plugin 压缩css
clean-webpack-plugin:自动清理dist
copy-webpack-plugin:将静态资源一起打包到dist里
devServer时根据模板在express项目根目录下生成html文件
自动引入bundle.js
terser-webpack-plugin 压缩js
mini-css-extract-plugin:提取css到单独文件
IgnorePlugin:忽略库部分内容,这部分不进行打包,如moment的本地化内容
optimize[ˈɒptɪmaɪz]-css-assets-webpack-plugin 压缩css
clean-webpack-plugin:自动清理dist
copy-webpack-plugin:将静态资源一起打包到dist里
谈谈webpack的理解:
1、 作用:
前端项目构建工具,
可以将各种资源打包成一个bundle,包括css,html,图片资源,.vue文件等。
2、 webpack编译过程
3、webpack两大核心:loader和plugins
(1)常用loader 常用plugins
(2)loader原理
(3)plugins 原理
4、webpack优化:3种代码抽离+noparse+extranal+IgnorePlugin
5、webapck本身自带优化:production模式下的tree-shaking+scope hoisting
6、开发环境配置:source-map+dev-server根据开发环境或者是生产环境进行的不同配置优化
1、 作用:
前端项目构建工具,
可以将各种资源打包成一个bundle,包括css,html,图片资源,.vue文件等。
2、 webpack编译过程
3、webpack两大核心:loader和plugins
(1)常用loader 常用plugins
(2)loader原理
(3)plugins 原理
4、webpack优化:3种代码抽离+noparse+extranal+IgnorePlugin
5、webapck本身自带优化:production模式下的tree-shaking+scope hoisting
6、开发环境配置:source-map+dev-server根据开发环境或者是生产环境进行的不同配置优化
作用
1. 递归构建一个依赖关系图
2. 将这些模块打包成一个或者多个bundle
2. 将这些模块打包成一个或者多个bundle
bundle文件分析
分析bundle.js可知:
bundle是一个自调用函数,传入一个以路径作为key,文件内容作为value的对象。
然后执行这个自调用函数,先创建一个__webpack_require__函数,
__webpack_require__主要是缓存处理和value函数的调用,
先读取缓存,缓存有就使用缓存,没有就创建缓存,然后调用传入参数对象的value,
传入三个参数给value函数。module(上面的键值对),
module.exports(一个空对象{}),自身__webpack_require__
然后开始递归调用文件内容
bundle是一个自调用函数,传入一个以路径作为key,文件内容作为value的对象。
然后执行这个自调用函数,先创建一个__webpack_require__函数,
__webpack_require__主要是缓存处理和value函数的调用,
先读取缓存,缓存有就使用缓存,没有就创建缓存,然后调用传入参数对象的value,
传入三个参数给value函数。module(上面的键值对),
module.exports(一个空对象{}),自身__webpack_require__
然后开始递归调用文件内容
是一个自调用函数
递归分析项目依赖,生成一份键值对作为自调用函数都参数传入,路径作为key,文件内容转化成eval函数作为value。
__webpack_require__函数
__webpack_require__函数可以类比CommonJS的require,都是加载模块代码。和NodeJS的设计很类似,都是先从缓存取用,否则加载模块并放入缓存。
__webpack_require__所在的闭包能访问外层变量modules和缓存installedModules。这个很关键,因为modules是webpack打包后立即执行函数传入的参数。
modules是一个object,key是string类型,value是function类型。
__webpack_require__所在的闭包能访问外层变量modules和缓存installedModules。这个很关键,因为modules是webpack打包后立即执行函数传入的参数。
modules是一个object,key是string类型,value是function类型。
webpack实现事件流的原理
在webpack内部实现事件流机制的核心就在于tapable,
有了它就可以通过事件流的形式,将各个插件串联起来,
tapable类似于node中的events库,
核心原理也是**发布订阅模式**
有了它就可以通过事件流的形式,将各个插件串联起来,
tapable类似于node中的events库,
核心原理也是**发布订阅模式**
1. 定义钩子
2. 使用者注册事件
3. 在合适的阶段调用钩子,触发事件
webpack 通过 tapable 这种巧妙的钩子设计很好的将实现与流程解耦开来
特点
1. 默认只支持js模块,每个js文件都是一个模块
2. 由入口main.js开始,根据依赖关系,将module => 组装成bundle
3. 其中遇到css,图片等非js模块,需要不同的Loader提供不同的打包能力
两大核心
loader:(处理非js文件)
模块加载器,原内容 => 新内容 加载非js模块
plugin:(通过生命钩子扩展webpack功能)
webpack构建时机(生命周期)暴露出来钩子,扩展自己的逻辑达到自己的目的
常用功能
1. 文件优化:js, html, css压缩,图片合并等
2. 代码转换:ts => js scss/less/stylus => css
3. 模块合并:模块化开发的项目中模块众多,把相关模块合并成一个文件
4. 代码分割:多个页面的公共模块可以提取出来,首屏不需要加载的模块可以采用异步加载
babel
作用
将一些es6,es7高级语法转换成目前浏览器能解析的es5
babel只是转译新标准引入的语法,比如ES6的箭头函数转译成ES5的函数;
而新标准引入的新的原生对象,部分原生对象新增的原型方法,新增的API等(如Proxy、Set等),
这些babel是不会转译的。需要用户自行引入polyfill来解决
而新标准引入的新的原生对象,部分原生对象新增的原型方法,新增的API等(如Proxy、Set等),
这些babel是不会转译的。需要用户自行引入polyfill来解决
原理
babel的转译过程也分为三个阶段
parsing/或者是babel.parse
ES6代码输入 ==》 babylon进行解析 ==》 得到抽象语法树AST
traverse
plugins:plugin用babel-traverse对AST树进行遍历转译 ==》 得到新的AST树,如果这个阶段不使用任何插件,那么babel会原样输出代码。
generating
用babel-generator通过AST树生成ES5代码
babel的转译过程分为三个阶段:parsing、transforming、generating,以ES6代码转译为ES5
代码为例,babel转译的具体过程如下:
1. ES6代码输入
2. babylon 或者@babel/parse进行解析得到 AST
3. plugin 用 babel-traverse 对 AST 树进行遍历转译,得到新的AST树
4. 用 babel-generator 通过 AST 树生成 ES5 代码
代码为例,babel转译的具体过程如下:
1. ES6代码输入
2. babylon 或者@babel/parse进行解析得到 AST
3. plugin 用 babel-traverse 对 AST 树进行遍历转译,得到新的AST树
4. 用 babel-generator 通过 AST 树生成 ES5 代码
核心包
babel-core:babel转译器本身,提供了babel的转译API,如babel.transform等,用于对代码进行转译。像webpack的babel-loader就是调用这些API来完成转译过程的。
babel-traverse:用于对AST(抽象语法树,想了解的请自行查询编译原理)的遍历,主要给plugin用
babel-generator:根据AST生成代码
工具包
babel-cli:babel的命令行工具,通过命令行对js代码进行转译
babel-register:通过绑定node.js的require来自动转译require引用的js代码文件
功能包
babel-types:用于检验、构建和改变AST树的节点
babel-template:辅助函数,用于从字符串形式的代码来构建AST树节点
babel-helpers:一系列预制的babel-template函数,用于提供给一些plugins使用
babel-code-frames:用于生成错误信息,打印出错误点源代码帧以及指出出错位置
babel-plugin-xxx:babel转译过程中使用到的插件,其中babel-plugin-transform-xxx是transform步骤使用的
babel-preset-xxx:transform阶段使用到的一系列的plugin
babel-polyfill:JS标准新增的原生对象和API的shim,实现上仅仅是core-js和regenerator-runtime两个包的封装
babel-runtime:功能类似babel-polyfill,一般用于library或plugin中,因为它不会污染全局作用域
polyfill和runtime
polyfill
polyfill是一个针对ES2015+环境的shim,实现上来说babel-polyfill包只是简单的把core-js和regenerator runtime包装了下,这两个包才是真正的实现代码所在
使用babel-polyfill会把ES2015+环境整体引入到你的代码环境中,让你的代码可以直接使用新标准所引入的新原生对象,新API等,
一般来说单独的应用和页面都可以这样使用。
如果只是需要引入部分新原生对象或API,那么可以按需引入,而不必导入全部的环境,具体见下文的core-js
一般来说单独的应用和页面都可以这样使用。
如果只是需要引入部分新原生对象或API,那么可以按需引入,而不必导入全部的环境,具体见下文的core-js
如果需要使用ES6/7中对象原型提供的新方法,babel默认情况无法转换,
即使用了`transform-runtime`的插件也不支持转换原型上的方法,这时要使用babel-polyfill
即使用了`transform-runtime`的插件也不支持转换原型上的方法,这时要使用babel-polyfill
runtime
babel-runtime(提供一整套runtime实现)
babel-runtime就是一个提供了regenerator、core-js和helpers的运行时库。
babel-runtime其实也不是真正的实现代码所在,真正的代码实现是在core-js中
babel-runtime其实也不是真正的实现代码所在,真正的代码实现是在core-js中
transform-runtime(按需导出使用的新特性的runtime实现)
babel-plugin-transform-runtime插件依赖babel-runtime,babel-runtime是真正提供runtime环境的包;
也就是说transform-runtime插件是把js代码中使用到的新原生对象和静态方法转换成对runtime实现包的引用
babel-plugin-transform-runtime既保持了新特性的功能,同时又没有像polyfill那样污染全局环境
(因为最终生成的代码中,并没有对Symbol的引用)
也就是说transform-runtime插件是把js代码中使用到的新原生对象和静态方法转换成对runtime实现包的引用
babel-plugin-transform-runtime既保持了新特性的功能,同时又没有像polyfill那样污染全局环境
(因为最终生成的代码中,并没有对Symbol的引用)
babel使用
`npm i babel-loader @babel/core @babel/preset-env webpack -D`
命令行 babel-cli
创建.babelrc文件
plugins
presets:
直接使用预设就不用逐个配置plugins
直接使用预设就不用逐个配置plugins
如果要自行配置转译过程中使用的各类插件,那太痛苦了,
所以babel官方帮我们做了一些预设的插件集,称之为preset,
这样我们只需要使用对应的preset就可以了。以JS标准为例,
babel提供了如下的一些preset:es2015 es2016 es2017 env(最新标准)
所以babel官方帮我们做了一些预设的插件集,称之为preset,
这样我们只需要使用对应的preset就可以了。以JS标准为例,
babel提供了如下的一些preset:es2015 es2016 es2017 env(最新标准)
预设
把 modules 设置为 false,就是告诉 babel 不要编译模块代码。这会让 Babel 保留我们现有的 es2015 import/export 语句。
划重点:所有可需要 tree-shaking 的代码必须以这种方式编译。因此,如果你有要导入的库,则必须将这些库编译为 es2015 模块以便进行 tree-shaking 。如果它们被编译为 commonjs,那么它们就不能做 tree-shaking ,并且将会被打包进你的应用程序中。许多库支持部分导入,lodash 就是一个很好的例子,它本身是 commonjs 模块,但是它有一个 lodash-es 版本,用的是 es2015模块。
此外,如果你在应用程序中使用内部库,也必须使用 es2015 模块编译。为了减少应用程序包的大小,必须将所有这些内部库修改为以这种方式编译。
此外,如果你在应用程序中使用内部库,也必须使用 es2015 模块编译。为了减少应用程序包的大小,必须将所有这些内部库修改为以这种方式编译。
webpack性能优化
webpack自带优化详解
tree shaking
默认在production和使用ES6 import下使用,自动移除未引用的代码
package.json:
"sideEffects": false,
sideEffects如果设为false,webpack就会认为所有没用到的函数都是没副作用的,即删了也没关系。
"sideEffects": false,
sideEffects如果设为false,webpack就会认为所有没用到的函数都是没副作用的,即删了也没关系。
scope Hoisting作用域提升
在模块之间关系进行结果推测,让打包文件体积更少,运行更快
原理:分析模块之间的依赖关系,尽可能把打散的模块合并到一个函数中去
原理:分析模块之间的依赖关系,尽可能把打散的模块合并到一个函数中去
scope hoisting 需要分析出模块之间的依赖关系,
因此源码必须采用 ES6 模块化语句,不然它将无法生效。
原因和tree shaking一样。
因此源码必须采用 ES6 模块化语句,不然它将无法生效。
原因和tree shaking一样。
terser-webpack-plugin
虽然webpack自带 terser-webpack-plugin 但是它不支持我们通过配置,
给它自带的terser-webpack-plugin传递参数,如果想要自定义压缩,
还是要手动安装terser-webpack-plugin,引入然后配置
(webpack在 v4.26.0 将默认的压缩插件从 uglifyjs-webpack-plugin 改成 teaser-webpack-plugin )
给它自带的terser-webpack-plugin传递参数,如果想要自定义压缩,
还是要手动安装terser-webpack-plugin,引入然后配置
(webpack在 v4.26.0 将默认的压缩插件从 uglifyjs-webpack-plugin 改成 teaser-webpack-plugin )
css优化
mini-css-extract-plugin:
提取到单独的css文件,通过link方式加载(只能用在webpack4/webpack5中)
提取到单独的css文件,通过link方式加载(只能用在webpack4/webpack5中)
`postcss-loader`和`autoprefixer`插件- 自动为css添加浏览器前缀
optimize[ˈɒptɪmaɪz]-css-assets-webpack-plugin 压缩css
(因为css压缩会覆盖webpack默认的js压缩配置,
导致JS代码无法压缩,所以要手动优化js,
要引入`terser-webpack-plugin`)
(因为css压缩会覆盖webpack默认的js压缩配置,
导致JS代码无法压缩,所以要手动优化js,
要引入`terser-webpack-plugin`)
js优化
代码分离
手动配置多入口、
抽取公共代码
CommonsChunkPlugin
optimization[,ɒptɪmaɪ'zeɪʃən].splitChunks.cacheGroups
vendor提取:node_modules
common:提取多次公用文件到common.js,
包括同步和异步代码(chunks: 'all',)
包括同步和异步代码(chunks: 'all',)
styles:提取公共css
路由懒加载
会根据注释打包出单独文件,需要使用时才会加载代码
component: () => import(/* webpackChunkName: "Home" */ '/Home.vue')}]
component: () => import(/* webpackChunkName: "Home" */ '/Home.vue')}]
noParse
不需要解析依赖关系的库
不需要解析依赖关系的库
阻止webpack浪费精力去解析这些明知道没有依赖的库,例如jq bootstrap,chart.js
externals[ɪkˈstɜːrnlz]
不需要打包到bundle的库
不需要打包到bundle的库
开发组件库时webpack不打包的库
cdn引入的包不需要打包
防止将某些 import 的包(package)打包到 bundle 中,
而是在运行时(runtime)再去从外部获取这些扩展依赖
例如,从 CDN 引入 jQuery,而不是把它打包:
而是在运行时(runtime)再去从外部获取这些扩展依赖
例如,从 CDN 引入 jQuery,而不是把它打包:
IgnorePlugin
可以忽略库中某些内容
可以忽略库中某些内容
需要忽略第三方模块内部依赖,例如忽略 moment 的本地化内容
DllPlugin:
将固定库抽取成动态链接库节省资源
将固定库抽取成动态链接库节省资源
--DllPlugin--webpack提供的插件 --将一些不修改的依赖只打包一次,
下次不打包(vue,react),只打包业务代码大大提升了构建的速度
下次不打包(vue,react),只打包业务代码大大提升了构建的速度
DllReferencePlugin,用于主webpack.config.js配置中
--add-asset-html-webpack-plugin:
主要思想是将一些不做修改依赖,提前打包,当发布代码的时候就不用对这部分代码进行打包,从而节约打包速度
主要思想是将一些不做修改依赖,提前打包,当发布代码的时候就不用对这部分代码进行打包,从而节约打包速度
js代码压缩
terser-webpack-plugin
虽然webpack自带 terser-webpack-plugin 但是它不支持我们通过配置,
给它自带的terser-webpack-plugin传递参数,如果想要自定义压缩,
还是要手动安装terser-webpack-plugin,引入然后配置
给它自带的terser-webpack-plugin传递参数,如果想要自定义压缩,
还是要手动安装terser-webpack-plugin,引入然后配置
代码压缩:uglify
打包工具bundle完就是一个单文件代码串,然后通过uglify进行代码静态分析,
分析函数或变量是否被引用并使用或是否存在副作用,如果都没有则将该代码进行移除
分析函数或变量是否被引用并使用或是否存在副作用,如果都没有则将该代码进行移除
编译器将Dead Code 从 AST(抽象语法树)中删除,
tree shaking
默认在production和使用ES6 import下使用,自动移除未引用的代码
CommonJS 模块是动态的,因此无法正确评估以消除死代码
scope hoist作用域提升
在模块之间关系进行结果推测,让打包文件体积更少,运行更快
原理:分析模块之间到依赖关系,尽可能把打散的模块合并到一个函数中去
devtool:sourceMap
生产环境:devtool: none,不使用source-map
不会产生文件,而是集成在打包后的文件中,不会产生列
devtool:'cheap-module-eval-source-map'
devtool:'cheap-module-eval-source-map'
开发环境:eval-cheap-module-source-map
source-map:源码
使用 cheap 方式只有行信息,没有列信息,大幅提高速度
使用 module 可支持 babel 这种预编译工具,映射转换前的代码
使用 eval 模式可以减少网络请求。不产生文件,bundle本身包含完整 sourcemap 信息
但是不适合用到生产环节,会导致bundle太大
source-map:源码
使用 cheap 方式只有行信息,没有列信息,大幅提高速度
使用 module 可支持 babel 这种预编译工具,映射转换前的代码
使用 eval 模式可以减少网络请求。不产生文件,bundle本身包含完整 sourcemap 信息
但是不适合用到生产环节,会导致bundle太大
happypack多进程打包
- 打包分析
webpack-bundle-analyzer
speed-measure-webpack-plugin
speed-measure-webpack-plugin
webpack --profile --json > stats.json
将stats.json文件放入工具中进行分析
将stats.json文件放入工具中进行分析
如何使用webpack对项目对优化
前向治理
(提升构建速度)
(提升构建速度)
speed-measure-webpack-plugin
采集性能指标,可以得到webpack在整个编译过程中在loader、plugin上花费的时间,基于该数据可以专项的进行优化和治理。
开启缓存:如果通过SMP分析得知在loader编译过程耗时较多,那么可以在核心loader,例如babel-loader中添加缓存
开启happyPack多线程编译
dll可以简单理解成提前打包,
例如lodash、echarts等大型npm包,可以通过dll将其提前打包好,
这样在业务开发过程中就不用再重复去打包了,可以大幅缩减打包时间。
例如lodash、echarts等大型npm包,可以通过dll将其提前打包好,
这样在业务开发过程中就不用再重复去打包了,可以大幅缩减打包时间。
hard-source-webpack-plugin
直接从缓存中读取编译过的文件,提升二次构建速度
直接从缓存中读取编译过的文件,提升二次构建速度
IgnorePlugin
忽略不需要打包的内容
忽略不需要打包的内容
moment 的本地化内容,new webpack.IgnorePlugin(/.~Klocale/, /moment/) 打包后 250kb->56kb
webpack精准过滤不需要解析的文件
noParse: /chart.js/,jq,boostrat
升级webpack5
webpack5利用 持久缓存 来提高构建性能,或许升级webpack后,前述的各种优化,都将成为历史。
后向治理
(保证构建质量)
(保证构建质量)
可视化分析构建结果
webpack-bundle-analyzeer
是否需要按需加载
是否需要提取公共代码
是否需要制定cacheGroup的策略
tree-shaking
提取公共chunk(splitChunks)
清理deadcode
未使用的文件
未使用的已暴露变量
业务开发过程中,随着业务迭代,经常有些文件、模块及代码被废弃,
这些废弃代码随着时间推移,将逐渐变为历史包袱,所以针对构建后结果,
我们要做的就是清理其中的deadcode。
这些废弃代码随着时间推移,将逐渐变为历史包袱,所以针对构建后结果,
我们要做的就是清理其中的deadcode。
执行:`webpack --profile --json > stats.json`
通过对modules和chunks加以分析,就可以得到webpack完整的依赖关系,
从而梳理出废弃文件及废弃代码,同时也可以根据业务形态进行定制
通过 webpack 编译源文件时,生成的包含有关于模块的统计数据的 JSON 文件,
这些统计数据不仅可以帮助开发者来分析应用的依赖图表,还可以优化编译的速度。
通过对modules和chunks加以分析,就可以得到webpack完整的依赖关系,
从而梳理出废弃文件及废弃代码,同时也可以根据业务形态进行定制
通过 webpack 编译源文件时,生成的包含有关于模块的统计数据的 JSON 文件,
这些统计数据不仅可以帮助开发者来分析应用的依赖图表,还可以优化编译的速度。
webpack-deadcode-plugin
前面提到分析stats.json,但因为是原始数据,数据量比较大,
有一定处理和清洗成本,所以可以使用开源的webpack-deadcode-plugin[5]这个插件
通过webpack-deadcode-plugin,可以快速筛选出:
未使用的文件
未使用的已暴露变量
有一定处理和清洗成本,所以可以使用开源的webpack-deadcode-plugin[5]这个插件
通过webpack-deadcode-plugin,可以快速筛选出:
未使用的文件
未使用的已暴露变量
结合eslint、tslint进行治理
lint可以快速的扫描出未使用的变量,这能够极大的提升我们的deadcode清理效率。
首先通过lint对未使用变量进行清理
再通过webpack-deadcode-plugin再扫描出未使用文件和未使用的导出变量
首先通过lint对未使用变量进行清理
再通过webpack-deadcode-plugin再扫描出未使用文件和未使用的导出变量
实际项目中优化过程
背景
基于webpack3版本的,采用babel6版本处理js,
并对app和single两个版本分别写了配置,提供了开发、
生产环境一系列脚本
并对app和single两个版本分别写了配置,提供了开发、
生产环境一系列脚本
现存问题
1. 项目热更新配置可能存在问题,热更新速度很慢
2. 项目整体打包速度比较慢,打包生成的chunk很大
3. chunk间没有提取公共模块,只是简单打了一个vendor
4. 没有区分dev和prod环境,缺少一些针对性的配置
5. /pocnode在没有对应pocnode开发需求的时候,
希望代理到现网,不需要同时启动前后端项目
希望代理到现网,不需要同时启动前后端项目
6. /poc模块希望能够在特定条件下,直接打到线上机,模拟线上的登录态
7. vue-resource模块在vue2.0官方已经不推荐使用了,并且不再维护。
方案
1. webpack3升级webpack4。
webpack4对production的时候会自动开启Scope hoisting和Tree shaking等等,提高编译速度。
webpack4对production的时候会自动开启Scope hoisting和Tree shaking等等,提高编译速度。
2. 重新规划了构建相关的目录,更有利于后续扩展。scripts下放构建相关的script,
具体的webpack配置放在config目录下,并且区分了app/single环境,区分了dev和prod环境。
具体的webpack配置放在config目录下,并且区分了app/single环境,区分了dev和prod环境。
3. dev环境启动devServer时候,使用portfinder寻找能用的端口,并提示出messages在命令行中,可以快速打开。
4. prod打包优化:利用optimization提取第三方库,提取重复使用模块,单独提取css
5. 针对/poc/想要使用外网环境这一诉求,增加npm run dev:app-out支持
6. 针对只想做前端开发,不想起后台服务这一诉求,增加npm run dev:app-fe支持
7. vue-resouce更新为axios。替代方案,axios不仅适用于客户端,也适用于nodejs环境,
并且在客户端侧做了xsrf防护,还具有自动处理json数据格式,abort等功能。
并且在客户端侧做了xsrf防护,还具有自动处理json数据格式,abort等功能。
splitChunks
vender
common
styles
8. 接入sentry: sentry地址 - https://sentry.oa.com/Tencent/mmbizpoc/ 。
sentry目前采样率设置1,只有生产环境会init。
sentry目前采样率设置1,只有生产环境会init。
引入eslint
引入项目自动构建
webhook + pre-commit +jenkins
开发注意事项
项目优化结果
Dev:89718ms 速度提升2/3
前:43.5 MB 50472ms
后:19.45 MB 232484ms 体积减少一倍
后:19.45 MB 232484ms 体积减少一倍
dll
Build 186016ms Dev:89718ms 速度提升2/3
webpack和gulp、grunt区别
glup 基于流的构建工具
gulp和grunt是任务执行工具,
用来自动化处理常见的开发任务,
例如,lint(代码检测)、build(构建)、test(测试)
可以配合各种插件做JS压缩,CSS压缩,less编译
重点在于实现自动化工作来提高前端工作效率
用来自动化处理常见的开发任务,
例如,lint(代码检测)、build(构建)、test(测试)
可以配合各种插件做JS压缩,CSS压缩,less编译
重点在于实现自动化工作来提高前端工作效率
1.构建工具
2.自动化
3.提高效率
2.自动化
3.提高效率
gulp和webpack的集成方案
有一些功能重叠,但是如果使用方式正确,任务运行工具和模块打包工具
还是能够一起协同工作。
webpack 可以通过各种扩展,将它用于任务运行工具的常见工作。
集成一个单独的工具会增加复杂度,因此在开始前一定要权衡利弊。
还是能够一起协同工作。
webpack 可以通过各种扩展,将它用于任务运行工具的常见工作。
集成一个单独的工具会增加复杂度,因此在开始前一定要权衡利弊。
webpack(为SPA 大应用而生)
webpack是文件打包工具,可以把项目的各种js文件,css文件等打包合成一个或多个文件,
将它们转换为适合浏览器的可用格式
例如,可以通过 压缩、分离 chunk 和 惰性加载我们的 JavaScript来提高性能。
目的是在于分析项目依赖打包整个项目
将它们转换为适合浏览器的可用格式
例如,可以通过 压缩、分离 chunk 和 惰性加载我们的 JavaScript来提高性能。
目的是在于分析项目依赖打包整个项目
1.打包工具
2.模块化识别
3.编译模块代码方案
2.模块化识别
3.编译模块代码方案
webpack-dev-server原理
做了一个express服务器,托管在根目录,找到根目录的index.html
热更新服务原理
主要流程
Webpack编译初期,webpack-dev-server启动本地express server,托管打包好的文件
为entry 注入热更新代码(websocket),
让浏览器可以请求本地的静态资源
为entry 注入热更新代码(websocket),
让浏览器可以请求本地的静态资源
页面首次打开后,服务端与客户端通过 websocket 建立长连接,
服务器把下一次用于请求资源的 hash 返回前端
(webpack-dev-server使用websocket通信,webpack-hot-middleware使用Eventsocket)
服务器把下一次用于请求资源的 hash 返回前端
(webpack-dev-server使用websocket通信,webpack-hot-middleware使用Eventsocket)
修改页面代码后,Webpack 监听到文件修改后,开始重新编译,
webpack-dev-server 调用 webpack api 监听 compile的 done 事件,
当compile 完成后,webpack-dev-server将编译打包后的新模块 hash 值发送到浏览器端。
webpack-dev-server 调用 webpack api 监听 compile的 done 事件,
当compile 完成后,webpack-dev-server将编译打包后的新模块 hash 值发送到浏览器端。
客户端获取到新的hash,客户端使用上一次的hash,分别发送ajax请求和jsonp请求到到服务器,
ajax请求来获取包含更新模块的集合json,jsonp是获取更新的模块代码(hot-update.json包含了需要更新资源的集合的chunkId,表示哪个 chunk 发生了改变)
返回的js代码通过script插入主文档,然后修改bundle文件中对应的模块代码
ajax请求来获取包含更新模块的集合json,jsonp是获取更新的模块代码(hot-update.json包含了需要更新资源的集合的chunkId,表示哪个 chunk 发生了改变)
返回的js代码通过script插入主文档,然后修改bundle文件中对应的模块代码
hash.js 插入成功后,重新 render 组件, 继而实现 UI 无刷新更新。
hot load 失败。则调用window.location.reload
webpack和webpack-dev-middleware配合实现的热更新
第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。
第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。
第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。
第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。
webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。
而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。
最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。
其他重要概念
为什么webpack dev-server
下没在文件夹中生成文件
下没在文件夹中生成文件
webpack在watch模式下,webpack监听文件变化,根据配置重新编译打包,并将打包后的代码以js对象保存在内存中
不生成文件的原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销
不生成文件的原因就在于访问内存中的代码比访问文件系统中的文件更快,而且也减少了代码写入文件的开销
客户端和服务端如何实现通信
websocket建立长连接,将 webpack 编译和打包的各个阶段状态告知浏览器,
最关键的步骤还是 webpack-dev-server 调用 webpack api 监听 compile的 done 事件,
当compile 完成后,webpack-dev-server通过 _sendStatus 方法将编译打包后的新模块 hash 值发送到浏览器端。
webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据
webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
最关键的步骤还是 webpack-dev-server 调用 webpack api 监听 compile的 done 事件,
当compile 完成后,webpack-dev-server通过 _sendStatus 方法将编译打包后的新模块 hash 值发送到浏览器端。
webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据
webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。
浏览器何时更新页面
通过建立websocket实现服务端和客户端的双向通讯,当我们的服务端发生变化时可以通知客户端进行页面的刷新
webpack 接收到最新 hash 值验证并请求模块代码(成功后客户端构造hot-update.js script链接,然后插入主文档)
代码位置
HotModuleReplacement.runtime 对模块进行热更新
1、找到缓存中的依赖,
2、删除缓存中过期的依赖,
3、将新模块添加到modules中,给下次调用webpack_require()用
HotModuleReplacement.runtime 对模块进行热更新
1、找到缓存中的依赖,
2、删除缓存中过期的依赖,
3、将新模块添加到modules中,给下次调用webpack_require()用
为什么我们没在bundle文件没添加websocket代码,
浏览器可以利用websocket和服务端建立连接
浏览器可以利用websocket和服务端建立连接
webpack-dev-server 修改了webpack 配置中的 entry 属性,
在bundle.js里面添加了 webpack-dev-client 的代码,
这样在最后的 bundle.js 文件中就会有接收 websocket 消息的代码了。
webpack-dev-server/client 当接收到 type 为 hash 消息后会将 hash 值暂存起来,
当接收到 type 为 ok 的消息后对应用执行 reload 操作
在bundle.js里面添加了 webpack-dev-client 的代码,
这样在最后的 bundle.js 文件中就会有接收 websocket 消息的代码了。
webpack-dev-server/client 当接收到 type 为 hash 消息后会将 hash 值暂存起来,
当接收到 type 为 ok 的消息后对应用执行 reload 操作
什么时候刷新整个页面
Webpack 会从修改模块开始根据依赖关系往入口方向查找热加载接收代码。
如果没有找到的话,默认是会刷新整个页面的。如果找到的话,会替换那个修改模块的代码为修改后的代码,
并且从修改模块到接收热加载之间的模块的相关依赖模块都会重新执行返回新模块值,替换点模块缓存。
如果没有找到的话,默认是会刷新整个页面的。如果找到的话,会替换那个修改模块的代码为修改后的代码,
并且从修改模块到接收热加载之间的模块的相关依赖模块都会重新执行返回新模块值,替换点模块缓存。
hot-update.json
hot-update.json包含了需要更新资源的集合的chunkId,表示哪个 chunk 发生了改变,是通过 XMLHttpRequest(ajax) 的方式,用上一次保存的 hash 值请求 hot-update.json 文件。
hot-update.js
客户端发送jsonp请求,接收hot-update.js,触发组件render流程
hot-update.js构造成script链接,通过 JSONP 的方式,利用 hot-update.json 返回的 chunkId 及 上一次保存的 hash 拼接文件名进而获取文件内容。
hot-update.js构造成script链接,通过 JSONP 的方式,利用 hot-update.json 返回的 chunkId 及 上一次保存的 hash 拼接文件名进而获取文件内容。
为什么更新模块的代码不直接通过 websocket 发送到浏览器端,
而是通过 jsonp 来获取呢?
而是通过 jsonp 来获取呢?
我的理解是,功能块的解耦,各个模块各司其职,webpack-dev-server/client 只负责消息的传递而不负责新模块的获取,
而这些工作应该有 HMR runtime 来完成,HMR runtime 才应该是获取新代码的地方。
再就是因为不使用 webpack-dev-server 的前提,使用 webpack-hot-middleware
和 webpack 配合也可以完成模块热更新流程,
在使用 webpack-hot-middleware 中有件有意思的事,它没有使用 websocket,
而是使用的 EventSource。综上所述,HMR 的工作流中,不应该把新模块代码放在 websocket 消息中。
而这些工作应该有 HMR runtime 来完成,HMR runtime 才应该是获取新代码的地方。
再就是因为不使用 webpack-dev-server 的前提,使用 webpack-hot-middleware
和 webpack 配合也可以完成模块热更新流程,
在使用 webpack-hot-middleware 中有件有意思的事,它没有使用 websocket,
而是使用的 EventSource。综上所述,HMR 的工作流中,不应该把新模块代码放在 websocket 消息中。
webpack --watch和webpack-dev-server
webpack根据这个来进行js的打包和编译工作。虽然webpack提供了webpack --watch的命令来动态监听文件的改变并实时打包,
输出新bundle.js文件,这样文件多了之后打包速度会很慢,此外这样的打包的方式不能做到hot replace,即每次webpack编译之后,
你还需要手动刷新浏览器。
webpack-dev-server其中部分功能就能克服上面的2个问题。webpack-dev-server --hot主要是启动了一个使用express的Http服务器。
它的作用主要是用来伺服资源文件。此外这个Http服务器和client使用了websocket通讯协议,
原始文件作出改动后,webpack-dev-server会实时的编译,但是最后的编译的文件并没有输出到目标文件夹
wepack --watch 只会在文件变化时实时打包,并不会触发页面更新
webpack-dev-server会监听文件变化并实施打包后通知页面进行更新
输出新bundle.js文件,这样文件多了之后打包速度会很慢,此外这样的打包的方式不能做到hot replace,即每次webpack编译之后,
你还需要手动刷新浏览器。
webpack-dev-server其中部分功能就能克服上面的2个问题。webpack-dev-server --hot主要是启动了一个使用express的Http服务器。
它的作用主要是用来伺服资源文件。此外这个Http服务器和client使用了websocket通讯协议,
原始文件作出改动后,webpack-dev-server会实时的编译,但是最后的编译的文件并没有输出到目标文件夹
wepack --watch 只会在文件变化时实时打包,并不会触发页面更新
webpack-dev-server会监听文件变化并实施打包后通知页面进行更新
对 tree-shaking 的了解
DCE,擦除无用的代码,擦除类型有:
不会被运行的代码。
没有被使用的代码。
这几类代码都会被tree shaking擦除。
不会被运行的代码。
没有被使用的代码。
这几类代码都会被tree shaking擦除。
rollup 和 webpack 的 shaking 程度不同,
以一个 Class 为例子(webpack4 + babel7已解决了class和跨文件的移除)
原理 编译器将Dead Code从AST(抽象语法树)中删除
以一个 Class 为例子(webpack4 + babel7已解决了class和跨文件的移除)
原理 编译器将Dead Code从AST(抽象语法树)中删除
tree shaking原理
treeshaking依赖ES6 modules,因为ES6是静态语言,对引入进行静态分析,
故而编译的时候就能正确判断到底加载了那些模块
Tree Shaking` 指基于 ES Module 进行静态分析,通过 AST 将用不到的函数进行移除,从而减小打包体积。
故而编译的时候就能正确判断到底加载了那些模块
Tree Shaking` 指基于 ES Module 进行静态分析,通过 AST 将用不到的函数进行移除,从而减小打包体积。
webpack4升级webpack5
新增长期缓存配置cache
webpack5移除了node环境的polyfill
在Webpack 5中移除了Node模块的兼容。
file-loader和url-loader删除
webpack、webpack-cli、webpack-dev-server是需要版本匹配的
node版本不兼容,node升到12
url-loader弃用, 使用 asset modules 代替
devServer去除了很多旧的配置项目
devServer 参数变化,例如before->onBeforeSetupMiddleware
devServer 参数变化,例如before->onBeforeSetupMiddleware
optimization
chunkIds: "deterministic",
moduleIds: "deterministic",
mangleExports: "deterministic"
特有的文件监听
新增性能优化
HTML插件不可用
html-webpack-plugin用不了,可以参考这个Issue。这个问题可大了,总不能构建完没有HTML页面吧。
不过好在这问题不难修复,具体代码是index.js中的145行,
把这一行中的compilation.compilationDependencies.add修改为compilation.fileDependencies.add就可以正常运行了。
不过好在这问题不难修复,具体代码是index.js中的145行,
把这一行中的compilation.compilationDependencies.add修改为compilation.fileDependencies.add就可以正常运行了。
to v4 from v3
Node.js v4—>Node.js v6
webpack-cli 匹配安装
mode必写
production 自带代码压缩
弃用json-loader
支持es6的方式导入JSON文件,并且可以过滤无用的代码
去CommonsChunk插件
将CommonsChunkPlugin被删除。相反,optimization.splitChunks可以使用这些选项。
移除loaders,必须使用rules
更新第三方插件
弃用/删除的插件
这些插件可以从配置中删除,因为它们在生产模式下是默认的:
- new NoEmitOnErrorsPlugin(),
- new ModuleConcatenationPlugin(),
- new DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") })
- new UglifyJsPlugin()
- new NoEmitOnErrorsPlugin(),
- new ModuleConcatenationPlugin(),
- new DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") })
- new UglifyJsPlugin()
这些插件在开发模式下是默认的
new NamedModulesPlugin()
new NamedModulesPlugin()
这些插件已被弃用,现在已删除:
- new NoErrorsPlugin(),
- new NewWatchingPlugin()
- new NoErrorsPlugin(),
- new NewWatchingPlugin()
webpack5
Improve build performance with Persistent Caching (通过使用持久性缓存来提高构建性能)
Improve Long Term Caching with better algorithms and defaults.(通过算法和默认设置来提升长期缓存)
Improve bundle size with better Tree Shaking and Code Generation.(通过更好的 Tree Shaking 和代码生成来压缩包大小)
Improve compatibility with the web platform. (提升网络平台的兼容性)
Clean up internal structures that were left in a weird state while implementing features in v4 - without introducing any breaking changes.
(清除一些内部架构,这些架构在v4版本中很奇怪.但并不会引起重大的改变)
Prepare for future features by introducing breaking changes now, allowing us to stay on v5 for as long as possible.
(为了以后的release,我们现在引入了一些重大的改变,使我们可以在v5停留尽可能长的时间
Improve Long Term Caching with better algorithms and defaults.(通过算法和默认设置来提升长期缓存)
Improve bundle size with better Tree Shaking and Code Generation.(通过更好的 Tree Shaking 和代码生成来压缩包大小)
Improve compatibility with the web platform. (提升网络平台的兼容性)
Clean up internal structures that were left in a weird state while implementing features in v4 - without introducing any breaking changes.
(清除一些内部架构,这些架构在v4版本中很奇怪.但并不会引起重大的改变)
Prepare for future features by introducing breaking changes now, allowing us to stay on v5 for as long as possible.
(为了以后的release,我们现在引入了一些重大的改变,使我们可以在v5停留尽可能长的时间
__webpack_require__做了什么
__webpack_require__函数可以类比CommonJS的require,都是加载模块代码。和NodeJS的设计很类似,都是先从缓存取用,否则加载模块并放入缓存
__webpack_require__所在的闭包能访问外层变量modules和缓存installedModules。这个很关键,因为modules是webpack打包后立即执行函数传入的参数。modules是一个object,key是string类型,value是function类型。
Chunk VS Module
chunk
Chunk是Webpack打包过程中,一堆module的集合。我们知道Webpack的打包是从一个入口文件开始,也可以说是入口模块,入口模块引用这其他模块,模块再引用模块。Webpack通过引用关系逐个打包模块,这些module就形成了一个Chunk。
如果我们有多个入口文件,可能会产出多条打包路径,一条路径就会形成一个Chunk。出了入口entry会产生Chunk,
如果我们有多个入口文件,可能会产出多条打包路径,一条路径就会形成一个Chunk。出了入口entry会产生Chunk,
Chunk是过程中的代码块,Bundle是结果的代码块
产生Chunk的三种途径
entry入口
entry的配置有三种方式
传递一个字符串
传递数组
传递对象
异步加载模块
代码分割(code spliting)
Module
Webpack可以看做是模块打包机,我们编写的任何文件,对于Webpack来说,都是一个个模块。所以Webpack的配置文件,有一个module字段,module下有一个rules字段,rules下有就是处理模块的规则,配置哪类的模块,交由哪类loader来处理。
为什么在webpack中可以书写import ES6模块,也支持require CommonJS模块?
webpack根据webpack.config.js中的入口文件,在入口文件里识别模块依赖,不管这里的模块依赖是用CommonJS写的,还是ES6 Module规范写的,webpack会自动进行分析,并通过转换、编译代码,打包成最终的文件。最终文件中的模块实现是基于webpack自己实现的webpack_require(es5代码),所以打包后的文件可以跑在浏览器上。
同时以上意味着在webapck环境下,你可以只使用ES6 模块语法书写代码(通常我们都是这么做的),也可以使用CommonJS模块语法,甚至可以两者混合使用。因为从webpack2开始,内置了对ES6、CommonJS、AMD 模块化语句的支持,webpack会对各种模块进行语法分析,并做转换编译。
(es6的语法被babel-loader处理成es5)
同时以上意味着在webapck环境下,你可以只使用ES6 模块语法书写代码(通常我们都是这么做的),也可以使用CommonJS模块语法,甚至可以两者混合使用。因为从webpack2开始,内置了对ES6、CommonJS、AMD 模块化语句的支持,webpack会对各种模块进行语法分析,并做转换编译。
(es6的语法被babel-loader处理成es5)
webpack对于ES模块/CommonJS模块的实现,是基于自己实现的webpack_require,所以代码能跑在浏览器中。
从 webpack2 开始,已经内置了对 ES6、CommonJS、AMD 模块化语句的支持。但不包括新的ES6语法转为ES5代码,这部分工作还是留给了babel及其插件。
在webpack中可以同时使用ES6模块和CommonJS模块。因为 module.exports很像export default,所以ES6模块可以很方便兼容 CommonJS:import XXX from 'commonjs-module'。反过来CommonJS兼容ES6模块,需要额外加上default:require('es-module').default。
webpack异步加载模块实现流程跟jsonp基本一致。
webpack如何异步加载模块原理
从上面源码可以知道,webpack实现模块的异步加载有点像jsonp的流程。在主js文件中通过在head中构建script标签方式,异步加载模块信息;再使用回调函数webpackJsonpCallback,把异步的模块源码同步到主文件中,所以后续操作异步模块可以像同步模块一样。 源码具体实现流程:
遇到异步模块时,使用__webpack_require__.e函数去把异步代码加载进来。该函数会在html的head中动态增加script标签,src指向指定的异步模块存放的文件。
加载的异步模块文件会执行webpackJsonpCallback函数,把异步模块加载到主文件中。
所以后续可以像同步模块一样,直接使用__webpack_require__("./src/async.js")加载异步模块
vite
原理:
基于浏览器对es模块的支持,启动一个koa服务器拦截浏览器的请求。
请求模块时按需动态编译显示(根据请求路径后缀读取依赖文件,按需以ES模块格式返回给客户端)
因为跳过了模块分析和打包过程,做到项目项目极速启动,模块热更新,按需编译的功能
(由于 Vite 在启动的时候不需要打包,也就意味着不需要分析模块的依赖、
不需要编译。因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。)
基于浏览器对es模块的支持,启动一个koa服务器拦截浏览器的请求。
请求模块时按需动态编译显示(根据请求路径后缀读取依赖文件,按需以ES模块格式返回给客户端)
因为跳过了模块分析和打包过程,做到项目项目极速启动,模块热更新,按需编译的功能
(由于 Vite 在启动的时候不需要打包,也就意味着不需要分析模块的依赖、
不需要编译。因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。)
1、利用浏览器es module imports,关键变化是index.html中的文件导入方式
<script type="module" src="/src/main.js"></script>
<script type="module" src="/src/main.js"></script>
2、第三方依赖打包,并将导入地址修改为相对地址,从而统一所有请求为相对地址
3、启动一个开发服务器处理这些资源请求
4、vite需要根据请求资源做不同解析工作,
比如App.vue,将导入模块解析成一个render函数,
然后将render设置在函数设置到组件配置对象中,使用
比如App.vue,将导入模块解析成一个render函数,
然后将render设置在函数设置到组件配置对象中,使用
vite对js/ts处理使用esbuild,支持babel,压缩等功能
vite&webpack区别
本地启动项目流程
Webpack
1. 分析各个模块的依赖关系
2. 资源编译,编译为浏览器可识别的代码
3. 一系列处理后打包代码,交给本地服务器进行渲染
Vite
1. 启动本地服务器
2. 请求模块时按需动态编译显示
打包原理
Webpack
1. 分析各个模块的关系,构建依赖图谱
2. 将代码转换成AST抽象语法树、加工处理成浏览器可识别的代码
Vite
1. 利用浏览器对 ESM 的支持,劫持浏览器的 HTTP 请求( Module )
2. 在后端进行相应的处理将项目中使用的文件通过简单的分解与整合,然后再返回给浏览器
HRM
Webpack:将该模块的所有依赖重新编译,Webpack 5.0 优化 Tree Shaking,提升了编译效率
Vite:
1. 当某个模块内容改变时,对该模块处理后,浏览器去重新请求该模块即可
2. 利用 HTTP 缓存策略,依赖会利用 HTTP 强缓存进行缓存,而源码会利用 HTTP 协商缓存进行缓存
1. 当某个模块内容改变时,对该模块处理后,浏览器去重新请求该模块即可
2. 利用 HTTP 缓存策略,依赖会利用 HTTP 强缓存进行缓存,而源码会利用 HTTP 协商缓存进行缓存
依赖构建
Webpack 5.0 通过增量构建缓存依赖包,默认缓存到 node_modules/.cache/webpack 目录下
Vite 通过 esbuild 进行预依赖构建,esbuild 是采用 go 语言编写,速度相比 node.js 提升10 - 100倍。默认缓存的目录:node_modules/.vite
生态
webpack:loader 与 plugin 丰富,生态相对成熟
Vite:由于刚出来,还没有被广泛使用,生态不够友好
vite的优点是不需要配置loader。其中许多是内置的
Typescript(ts)
TS有什么优势
静态输入:静态类型化是一种功能,可以在开发人员编写脚本时检测错误。
大型的开发项目:使用TypeScript工具来进行重构更变的容易、快捷。
更好的协作:类型安全是在编码期间检测错误,而不是在编译项目时检测错误。
更强的生产力:干净的 ECMAScript 6 代码,自动完成和动态输入等因素有助于提高开发人员的工作效率。
interface 和 type的区别
interface 只能定义对象类型。type声明可以声明任何类型。
interface 能够声明合并,两个相同接口会合并。Type声明合并会报错
type可以类型推导,typeof
监控
页面埋点
idkey+img src
性能监控
性能数据采集需要使用
window.performance API
JS库 web-vitals
性能数据采集需要使用
window.performance API
JS库 web-vitals
FP(首次绘制)
FCP(首次内容绘制 First contentful paint)
LCP(最大内容绘制时间 Largest contentful paint)
FPS(每秒传输帧数)
TTI(页面可交互时间 Time to Interactive)
HTTP 请求响应时间
DNS 解析时间
TCP 连接时间
异常监控方法
sentry
Vue.config.errorHandler,(Vue提供只能捕获其页面生命周期内的函数,比如created,mounted)
window.onerror()当有js运行时错误触发时,onerror可以接受多个参数(message, source, lineno, colno, error)。
window.addEventListener('error'), function(e) {}, true 会比window.onerror先触发,不能阻止默认事件处理函数的执行,但可以全局捕获资源加载异常的错误
网页崩溃
Service Worker 有自己独立的工作线程,与网页区分开,网页崩溃了,Service Worker 一般情况下不会崩溃;
Service Worker 生命周期一般要比网页还要长,可以用来监控网页的状态
性能
图片优化
懒加载
cdn
防抖节流
预加载
css优化
前端性能优化
js性能优化
网站查看性能优化
实际使用过程的前端性能优化
测试
重构
移动端
前端性能优化
1、前端性能优化
1) 缓存,主要有 cdn、浏览器缓存、本地缓存以及应用离线包
(详细资料:解析Web缓存及其最佳实践)
(详细资料:解析Web缓存及其最佳实践)
2) 图⽚优化
3) 静态⽂件优化
4) 浏览器优化
5) ⽂件合并压缩(js、css压缩,tree-shaking等)
预加载/按需加载/异步加载
预加载 :资源预拉取(prefetch)则是另一种性能优化的技术。
通过预拉取可以告诉浏览器用户在未来可能用到哪些资源。
prefetch支持预拉取图片、脚本或者任何可以被浏览器缓存的资源。
在head里 添加 <linkrel="prefetch"href="image.png">
通过预拉取可以告诉浏览器用户在未来可能用到哪些资源。
prefetch支持预拉取图片、脚本或者任何可以被浏览器缓存的资源。
在head里 添加 <linkrel="prefetch"href="image.png">
路由懒加载-按需加载组件,当路由被访问的时候再加载对应组件,
而不是首页的时候加载)、第三方依赖按需加载
图片懒加载(IntersectionObserver)
而不是首页的时候加载)、第三方依赖按需加载
图片懒加载(IntersectionObserver)
1)节流防抖
2)按需执⾏
3)回流重绘
4)框架优化
(⽐如vue3的静态标记和diff优化,webpack5 tree-shaking)
(⽐如vue3的静态标记和diff优化,webpack5 tree-shaking)
2、前端性能监控
1、前端性能指标分析
2、关键性能指标统计
FP、FCP、FMP与TTI都表示浏览器在屏幕上渲染像素的时间点。
3、性能分析工具
3、框架性能优化
1、Vue性能优化
组件
keep-alive
data数据减少
Object.freeze不需要做响应
路由懒加载,异步组件
函数式组件
列表
v-if和v-for不要同时使用
列表绑定时间用事件代理
key保证唯一,不要用索引
⻓列表滚动到可视区域动态加载
其他
防抖节流
第三⽅模块按需导⼊
2、SEO优化
预渲染;
服务端渲染SSR;
3、用户体验优化
骨架屏;
PWA;
还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
4、长列表优化
长列表1、vue-virtual-scroll-list优化长列表
虚拟列表的实现原理:
只渲染可视区的 dom 节点,其余不可见的数据卷起来,只会渲染可视区域的 dom 节点,
提高渲染性能及流畅性,优点是支持海量数据的渲染;
只渲染可视区的 dom 节点,其余不可见的数据卷起来,只会渲染可视区域的 dom 节点,
提高渲染性能及流畅性,优点是支持海量数据的渲染;
github地址:https://github.com/tangbc/vue-virtual-scroll-list
2、Object.freeze优化长列表
Object.freeze()方法可以冻结一个对象。一个被冻结的对象再也不能被修改;
冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,
不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。
冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,
不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。
对于data()或vuex中冻结的对象,vue不会做getter和setter的转换。因此对于一个不变的、
大数据量的数组或Object数据来说,使用Object.freeze()可以有效地提升性能。优化
大数据量的数组或Object数据来说,使用Object.freeze()可以有效地提升性能。优化
5、打包优化
压缩代码;uglifyjsplugin
(webpack4废弃,自带了terser-webpack-plugin)
(webpack4废弃,自带了terser-webpack-plugin)
Tree Shaking/Scope Hoisting
使用cdn加载第三方模块
多线程打包happypack
splitChunks抽离公共文件
sourceMap优化(开发环境下开启)
测试
测试分类
单元测试
对于单片代码或者一个函数
集成测试
对于某一个功能或者页面
端到端测试E2E
对于整个程序而言
工具
测试框架:mocha、jasmine、jest、Karma、Nightmare
提供ui界面或者CLI工具:Karma、jasmine、jest、testCafe、Cypress
断言库:用于判断结果是否符合预期
should.js、chai、expect
断言库提供很多语义化方法来对值做判断
(例如mocha+chai结合)
should.js、chai、expect
断言库提供很多语义化方法来对值做判断
(例如mocha+chai结合)
karma
是一个测试运行环境,高效、可扩展、运行在真实设备、无缝使用流程,
优点:是能通过插件的方式继承大部分主流的测试框架和前端库,方便一次在多浏览器环境执行测试用例,并生成测试覆盖率报告
缺点:对测试页面环境的搭建和资源文件的加载不常见,配置不直观
缺点:对测试页面环境的搭建和资源文件的加载不常见,配置不直观
jasmine
带有断言库和环境模拟和mocks,配置较简单
mocha
提供开发者的一个基础测试结构,不包含mock、断言、环境模拟
jest
AVA
异步特性和并发运行测试
TDD(测试驱动)/BDD(行为驱动)
BDD:更偏向于系统功能和业务逻辑的自动化测试
TDD:快速开发并测试功能模块的过程中更加高效,达到快速完成开发目的
nginx
Nginx是一款轻量级HTTP服务器(又叫web服务器),
采用事件驱动的异步非阻塞处理方式框架,这让其具有极好的IO性能,
时常用于服务器的反向代理和负载均衡
采用事件驱动的异步非阻塞处理方式框架,这让其具有极好的IO性能,
时常用于服务器的反向代理和负载均衡
特点
轻量级web服务器
设计思想是事件驱动的异步非阻塞
占用内存少,启动快,并发强
C语言开发
扩展性好,第三方插件多
启动停止命令
配置文件
/usr/local/etc/nginx/nginx.conf (nginx配置文件路径)
/usr/local/var/www (nginx服务器默认的根目录)
/usr/local/Cellar/nginx/1.17.9 (nginx的安装路径)
/usr/local/var/log/nginx/error.log (nginx默认的日志路径)
nginx作用
动静分离
动静分离其实就是 Nginx 服务器将接收到的请求分为动态请求和静态请求。
静态请求直接从 nginx 服务器所设定的根目录路径去取对应的资源,
动态请求转发给真实的后台(前面所说的应用服务器,如图中的Tomcat)去处理。
这样做不仅能给应用服务器减轻压力,将后台api接口服务化,还能将前后端代码分开并行开发和部署。
(传送门:nginx动静分离的好处)
动态请求转发给真实的后台(前面所说的应用服务器,如图中的Tomcat)去处理。
这样做不仅能给应用服务器减轻压力,将后台api接口服务化,还能将前后端代码分开并行开发和部署。
(传送门:nginx动静分离的好处)
反向代理
(负载均衡、跨域、安全)
(负载均衡、跨域、安全)
是什么
浏览器或其他终端最终拿到了他想要的内容,但是具体从哪儿拿到的这个过程它并不知道
作用
保障应用服务器的安全(增加一层代理,可以屏蔽危险攻击,更方便的控制权限)
实现负载均衡(稍等~下面会讲)
实现跨域(号称是最简单的跨域方式)
配置
负载均衡
是什么
在服务器集群中,Nginx 可以将接收到的客户端请求“均匀地”(严格讲并不一定均匀,可以通过设置权重)
分配到这个集群中所有的服务器上。这个就叫做负载均衡。
分配到这个集群中所有的服务器上。这个就叫做负载均衡。
作用
分摊服务器集群压力
保证客户端访问的稳定性
配置
正向代理
(VPN、webpack-dev-server.proxy)
(VPN、webpack-dev-server.proxy)
是什么
意思是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,
客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得
的内容返回给客户端。客户端才能使用正向代理。当你需要把你的服务器作为代理服务器的时候,
可以用Nginx来实现正向代理。
客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得
的内容返回给客户端。客户端才能使用正向代理。当你需要把你的服务器作为代理服务器的时候,
可以用Nginx来实现正向代理。
科学上网vpn(俗称翻墙)其实就是一个正向代理工具。
该 vpn 会将想访问墙外服务器 server 的网页请求,
代理到一个可以访问该网站的代理服务器 proxy 上。
这个 proxy 把墙外服务器 server 上获取的网页内容,
再转发给客户
该 vpn 会将想访问墙外服务器 server 的网页请求,
代理到一个可以访问该网站的代理服务器 proxy 上。
这个 proxy 把墙外服务器 server 上获取的网页内容,
再转发给客户
脚手架cli
如何开发脚手架
yeoman:老牌的项目脚手架工具
hygen:快速且可扩展的代码生成器
commander:Nodejs处理控制台命令
co:异步控制,同步的逻辑来表达异步的流程
co-prompt:分步接收用户输入
chalk:控制台彩色字体
ora:控制台loading,
exec :require('child_process').exec,执行命令
hygen:快速且可扩展的代码生成器
commander:Nodejs处理控制台命令
co:异步控制,同步的逻辑来表达异步的流程
co-prompt:分步接收用户输入
chalk:控制台彩色字体
ora:控制台loading,
exec :require('child_process').exec,执行命令
思路
package.json添加script执行入口文件, 或者bin+npm link全局使用
入口文件#!/usr/bin/env node 以node环境执行
#!就是代表此文件可以当做脚本运行
/usr/bin/env node这行的意思就是用node来执行此文件
node怎么来呢,就去用户(usr)的安装根目录(bin)下的env环境变量中去找
node怎么来呢,就去用户(usr)的安装根目录(bin)下的env环境变量中去找
commander库 添加控制台命运选项
引入模板配置文件。获取用户输入(require('co-prompt')),看是否存在文件
拼接拉取项目代码的命令并执行,拼接git命令行+切换对应分支(exec)
组件库实践
monorepo
lerna+yarn workspace
lerna
lerna是一个管理多个npm模块的工具,是用来维护Monorepo,不负责项目构建,优化维护多包的工作流,解决多个包相互依赖,且发布需要手动维护多个包的问题
目前最常见的monorepo解决方案是lerna和yarn的workspaces特性,基于lerna和yarn workspace的monorepo工作流。
由于yarn和lerna在功能上有较多的重叠,我们采用yarn来处理依赖问题,用lerna来处理发布问题,能用yarn做的就用yarn做
由于yarn和lerna在功能上有较多的重叠,我们采用yarn来处理依赖问题,用lerna来处理发布问题,能用yarn做的就用yarn做
yarn
npm安装依赖比yarn慢,yarn会对已下载过的依赖包进行缓存,其他项目再次安装时直接使用缓存,下载速度大大提升
对代码仓库下,多个 package 的依赖,进行管理:将共同的依赖,做 hosting(提升)。这样,可以防止 package 中的包重复安装。workspace 机制,会在根目录下,统一安装依赖到 node_module,并生成 yarn.lock。单个 package 下,不需要再生成 yarn.lock
设计模式
(单车迭代观,发明祖母)
(单车迭代观,发明祖母)
一、单例模式
保证一个类仅有一个实例,并提供一个访问它都全局访问切点(window)、Vuex.stroe全局只有一个实例
工厂模式
工厂模式主要是为创建对象提供了接口
二、策略模式
定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。
a、 一件事情,有很多方案可以实现。
b、我可以在任何时候,决定采用哪一种实现。
c.、未来可能增加更多的方案。
d、 策略模式让方案的变化不会影响到使用方案的客户。
三、代理模式
为一个对象提供一个代用品或占位符,以便控制对它的访问
四、迭代器模式
提供一种方法顺序访问一个聚合对象中的各个元素
1、generator 天生为迭代器的 API
2、(jq中的迭代器$.each(function(i,j){....i...j}))
3、Map 迭代器
4、 KOA 由插件调用 next() 控制迭代
五、发布—订阅模式/观察者模式
观察者模式
定义
观察者模式是一种行为型设计模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,
当主题对象的状态发生变化时,它会通知所有观察者对象,使它们能够自动更新自己。
当主题对象的状态发生变化时,它会通知所有观察者对象,使它们能够自动更新自己。
优点
抽象耦合
广播通信
缺点
通知效率
状态一致性
适用场景
松散耦合的对象通知
GUI事件处理
电商网站订单系统
发布订阅模式
定义
发布订阅模式是一种消息型设计模式,它定义了一个调度中心,称为发布者或者代理,它维护了一个订阅者列表,
当有新的消息时,它会遍历这个列表,将消息发送给所有订阅者
当有新的消息时,它会遍历这个列表,将消息发送给所有订阅者
优点
完全解藕
异步通信
缺点
系统复杂度和开销
消息可靠性和一致性
适用场景
完全解藕的对象通知
分布式系统、微服务架构、事件驱动系统
新闻网站推送系统
区别
简单理解,观察者模式中,发布者和订阅者是知道对方存在的,实现上使用了array;发布订阅模式,发布者和订阅者都不知道对方存在,定义了一个中介对象(可抽离成单独文件),实现上使用了object。
六、命令模式
和策略模式很相似,命令模式是含有不同的命令(含有接收者的请求):做不同的事情;隐藏接收者执行细节。常见菜单事件,
七、组合模式
使用树形方式创建对象的结构,把相同的操作应用在对象和单个对象
八、模板方法模式
是一种典型的通过封装变化提高系统扩展性的设计模式,将相同逻辑抽象都父类模版中,子类具体自定义(高阶函数更好)
九、享元模式
十、职责链模式
十一、中介者模式
十二、装饰者模式
十三、状态模式
十四、适配器模式
十五、外观模式
构造函数模式
运维
docker三大核心
镜像
包含一种轻量级、可执行的独立软件包,它包含运行一个程序所需要的内容,把应用程序打包成一个可交付的运行环境(包括代码、运行时所需要的库、变量环境和配置文件等)这个打包好的运行环境就是image镜像文件
容器
容器基于镜像创建,是运行镜像之后的一个实例,容器才是真的运行业务程序的地方。如果把镜像比作程序里面的类,那么容器就是对象
镜像仓库
存放镜像的地方,研发工程师打包好镜像之后将镜像上传到镜像仓库里,然后就可以运行仓库权限的人拉取镜像来运行容器
文件概念
dockerfile
dockerfile是一种文本文件,包含一些列指令,用于构建docker镜像。通过dockerfile,可以定义应用程序的运行环境、依赖关系、配置信息等,以及构建过程中需要执行的操作,例如安装删除修改文件
dockerCompose
是Docker官方提供的容器编排工具,可以通过YAML文件定义多个容器组成的应用程序,并且可以统一管理容器的配置、网络、数据卷等,可以通过命令行或图形化界面进行操作
docker使用流程
一切从dockerfile开始。dockerfile是镜像的源代码
创建dockerfile后,可以创建容器的镜像。镜像只是“源代码”的“编译版本”
获得容器镜像后,应使用注册表重新分发容器。注册表就像一个git存储库-可以推送和拉取镜像
接下来,可以使用该镜像来运行容器,在许多方面,正在运行的容器与虚拟机(但没有管理程序)非常相似
命令
1.启动docker服务 sudo service docker start
2.停止docker服务 sudo service docker stop
3.检查docker 守护进程是否在运行 sudo docker stats
4.查看docker相关信息 sudo docker info
5.列出所有容器 sudo docker ps -a
6.最后一次运行的容器 sudo docker ps -l
7.重新启动已停止的容器 sudo docker start 容器名(也可以使用容器ID)
8.获取容器的日志 sudo docker logs 容器名
获取最后几条日志 sudo docker -f 容器名
9.列出镜像 sudo docker images
10.拉取镜像 sudo docker pull 镜像名
11.删除所有容器 sudo docker rm $(docker ps -a -q)
12.删除单个容器 sudo docker rm 容器名
13.删除所有镜像
sudo docker rmi $(docker images | grep none | awk '{print $3}' | sort -r)
14.保存镜像
sudo docker save 镜像名 > /home/新镜像名.tar
14.加载自定义镜像
sudo docker load < /home/自定义镜像15.获取容器更多信息
sudo docker inspect 容器名
16.删除为none的镜像
docker images --no-trunc| grep none | awk '{print $3}' | xargs -r docker rmi
常用参数:
-i:以交互模式运行容器,通常与 -t 同时使用
-t:为容器重新分配一个伪输入终端,通常与 -i 同时使用
-p : 端口映射 格式为[主机端口:容器端口]
-d : 后台模式运行
-name : 给容器一个新的名称
-v:挂载主机的目录
-e: username="ritchie": 设置环境变量
-m:设置容器使用内存最大值
--env-file=[]:从指定文件读入环境变量
2.停止docker服务 sudo service docker stop
3.检查docker 守护进程是否在运行 sudo docker stats
4.查看docker相关信息 sudo docker info
5.列出所有容器 sudo docker ps -a
6.最后一次运行的容器 sudo docker ps -l
7.重新启动已停止的容器 sudo docker start 容器名(也可以使用容器ID)
8.获取容器的日志 sudo docker logs 容器名
获取最后几条日志 sudo docker -f 容器名
9.列出镜像 sudo docker images
10.拉取镜像 sudo docker pull 镜像名
11.删除所有容器 sudo docker rm $(docker ps -a -q)
12.删除单个容器 sudo docker rm 容器名
13.删除所有镜像
sudo docker rmi $(docker images | grep none | awk '{print $3}' | sort -r)
14.保存镜像
sudo docker save 镜像名 > /home/新镜像名.tar
14.加载自定义镜像
sudo docker load < /home/自定义镜像15.获取容器更多信息
sudo docker inspect 容器名
16.删除为none的镜像
docker images --no-trunc| grep none | awk '{print $3}' | xargs -r docker rmi
常用参数:
-i:以交互模式运行容器,通常与 -t 同时使用
-t:为容器重新分配一个伪输入终端,通常与 -i 同时使用
-p : 端口映射 格式为[主机端口:容器端口]
-d : 后台模式运行
-name : 给容器一个新的名称
-v:挂载主机的目录
-e: username="ritchie": 设置环境变量
-m:设置容器使用内存最大值
--env-file=[]:从指定文件读入环境变量
CICD
git
git中 rebase和merge的区别是什么
rebase把当前的commit放到公共分支的最后面,所以叫变基,就好像从公共分支又重新拉出来这个分支一样
merge把当前的commit和公共分支合并在一起
merge把当前的commit和公共分支合并在一起
用merge命令解决完冲突后产生一个commit,而用rebase命令解决完冲突后不会产生额外的commit
git pull和git pull --rebase区别
git pull做了两个操作分别是‘获取’和合并,所以加了rebase就是以rebase的方式进行合并分支,默认为merge。
子主题
常见问题
怎么做反爬虫,设计思路
后端
反爬虫(判断真人访问)
User-Agent + Referer检测
账号及Cookie验证
验证码
IP限制频次
反反爬虫(模拟真人访问)
chrome headless或phantomjs来模拟浏览器环境
tesseract识别验证码
代理IP淘宝就能买到
前端
1、FONT-FACE拼凑式,不是真正的数字,使用font-face定义字符集并通过unicode去映射展示,图像识别还需要爬取字符集
BACKGROUND拼凑式(与font的策略类似,美团里用到的是background拼凑。数字其实是图片,根据不同的background偏移,显示出不同的字符。)
4、伪元素隐藏式:爬取网页,必须得解析css,需要拿到伪元素的content
元素定位覆盖式(利用css,爬到数据和展示数九以一样)
IFRAME异步加载式(html源码里几乎只有一个iframe,并且它的src是空白的:about:blank。接着js开始运行,把整个页面的框架异步塞到了iframe里面)
字符集替换式,html里明明写的3211,视觉上展示的却是1231
如何搭建mock系统
mock-api
(项目地址:/Users/zhoupeiyi/Desktop/面试复习/code/mock-api)
(项目地址:/Users/zhoupeiyi/Desktop/面试复习/code/mock-api)
🔥内置支持热Mocker文件替换。
🚀通过JSON快速轻松地配置API。
🌱模拟API代理变得简单。
💥可以独立使用,无需依赖webpack和webpack-dev-server。
低代码底层设计思路
1、建立思维导图,明确需求和功能。
2、使用低代码开始搭应用模型(搭应用有2种方式,一种是从已有模板内导入应用,一种是创建空白应用)
3、定义数据字段类型,设置关联关系,上传数据。
4、设定工作流、审批流、成员权限。
5、api连接、集成第三方平台&外部系统。
6、产品功能测试、用户测试验收。
7、部署生产环境
8、搭建生产环境监控系统。
计算机基础
浏览器对tcp请求有限制吗?
网页中的图片资源为什么分放在不同的域名下?
HTTP/1.1,开始支持长连接,但是 HTTP/1.1 协议中 「浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞」
浏览器与服务器建立一个TCP连接后,
是否会在完成一个http请求后断开?
什么条件下会断开?
是否会在完成一个http请求后断开?
什么条件下会断开?
HTTP/1.1将Connection写入了标准,默认值为keep-alive。除非强制设置为Connection: close,才会在请求后断开TCP连接。
默认情况下建立的TCP连接不会断开,只有在请求头中设置Connection: close才会在请求后关闭TCP连接
默认情况下建立的TCP连接不会断开,只有在请求头中设置Connection: close才会在请求后关闭TCP连接
一个TCP连接可以同时处理几个HTTP请求?
HTTP/1.1中,单个TCP连接,在同一时间只能处理一个http请求,虽然存在Pipelining技术支持多个请求同时发送,但由于实践中存在很多问题无法解决,
所以浏览器默认是关闭,所以可以认为是不支持同时多个请求。
所以浏览器默认是关闭,所以可以认为是不支持同时多个请求。
HTTP2提供了多路传输功能,多个http请求,可以同时在同一个TCP连接中进行传输
浏览器http请求的并发性是如何体现的?
并发请求的数量有没有限制?
并发请求的数量有没有限制?
若在 HTTP/1.1 时代,那个时候没有多路传输,当浏览器拿到一个有几十张图片的网页,肯定不能只开一个 TCP 连接顺序下载,那样用户肯定等的很难受,但是如果每个图片都开一个 TCP 连接发 HTTP 请求,那电脑或者服务器都可能受不了,要是有 1000 张图片的话总不能开 1000个TCP 连接吧。
页面资源请求时,浏览器会同时和服务器建立多个TCP连接,在同一个TCP连接上顺序处理多个HTTP请求。
所以浏览器的并发性就体现在可以建立多个TCP连接,来支持多个http同时请求。
页面资源请求时,浏览器会同时和服务器建立多个TCP连接,在同一个TCP连接上顺序处理多个HTTP请求。
所以浏览器的并发性就体现在可以建立多个TCP连接,来支持多个http同时请求。
Chrome浏览器最多允许对同一个域名Host建立6个TCP连接,不同的浏览器有所区别。
如果图片都是 HTTPS 连接并且在同一个域名下,那么浏览器在 SSL 握手之后会和服务器商量能不能用HTTP2,如果能的话就使用多路复用。不过也未必会所有挂在这个域名的资源都会使用一个 TCP 连接去获取。如果发现用不了 HTTP2 呢?或者用不了 HTTPS(现实中的 HTTP2 都是在 HTTPS 上实现的,所以也就是只能使用 HTTP/1.1)。
浏览器就会在一个 HOST 上建立多个 TCP 连接,连接数量的最大限制取决于浏览器设置,这些连接会在空闲的时候被浏览器用来发送新的请求, 如果所有的连接都正在发送请求的话,那其他的请求就只能等等了。
如果图片都是 HTTPS 连接并且在同一个域名下,那么浏览器在 SSL 握手之后会和服务器商量能不能用HTTP2,如果能的话就使用多路复用。不过也未必会所有挂在这个域名的资源都会使用一个 TCP 连接去获取。如果发现用不了 HTTP2 呢?或者用不了 HTTPS(现实中的 HTTP2 都是在 HTTPS 上实现的,所以也就是只能使用 HTTP/1.1)。
浏览器就会在一个 HOST 上建立多个 TCP 连接,连接数量的最大限制取决于浏览器设置,这些连接会在空闲的时候被浏览器用来发送新的请求, 如果所有的连接都正在发送请求的话,那其他的请求就只能等等了。
深度遍历和广度遍历,怎么进行非递归广度遍历
什么是纯函数
相同输入总是会返回相同的输出。
不产生副作用。
不依赖于外部状态。
学习和习惯纯函数可以使你更轻松地测试和调试代码
运算符优先级,&&和++
"++,--" 大于 "&&" 大于 "||"
jq
jquery是把源码封装在一个匿名函数的自执行环境中, 有助于防止变量全局污染
然后再通过传入window对象作为window的局部变量使用, 可以很快的访问window对象
传入undefined参数可以缩短查找undefined的时间
jquery将一些原型属性和方法封装在jquery.prototype中为了缩短使用名称又赋值给了jquery.fn
c++、Java、js区别
C++,java是一门面向对象的编程语言
而且JavaScript 是动态类型语言,而 Java 是静态类型语言
C++、Java是后端语言,是面向对象编程语言,javascript是前端语言,一种弱类型语言。虽然javascript和java是名称很相似,但它们也没有关系
图片懒加载原理
滚动到可视区域再加载
什么是渐进加载
渐进式渲染就是图片的内容从模糊到清晰的过程
预加载(预见性的加载一些不可见区域的资源,提高用户在快速滚动浏览器时候的体验)。
骨架屏
vue-lazyload
页面滚动需要请求加载数据太频繁如何提高性能
getBoundingClientRect:该方案需要监听scroll事件,注意节流处理
new IntersectionObserver: 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。Intersection Observer API 允许你配置一个回调函数,每当目标(target)元素与设备视窗或者其他指定元素发生交集的时候执行。设备视窗或者其他元素我们称它为根元素或根(root)。
PS:该方案较前者的优点就是不需要监听,其实兼容性在chrome中还不错
PS:该方案较前者的优点就是不需要监听,其实兼容性在chrome中还不错
异步加载JS方式
app与H5 如何通讯交互的
// 兼容IOS和安卓
callMobile(parameters,messageHandlerName) {
//handlerInterface由iOS addScriptMessageHandler与andorid addJavascriptInterface 代码注入而来。
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
// alert('ios')
window.webkit.messageHandlers[messageHandlerName].postMessage(JSON.stringify(parameters))
} else {
// alert('安卓')
//安卓传输不了js json对象,只能传输string
window.webkit[messageHandlerName](JSON.stringify(parameters))
}
}
由app将原生方法注入到window上供js调用
messageHandlerName 约定的通信方法parameters 需要传入的参数
callMobile(parameters,messageHandlerName) {
//handlerInterface由iOS addScriptMessageHandler与andorid addJavascriptInterface 代码注入而来。
if (/(iPhone|iPad|iPod|iOS)/i.test(navigator.userAgent)) {
// alert('ios')
window.webkit.messageHandlers[messageHandlerName].postMessage(JSON.stringify(parameters))
} else {
// alert('安卓')
//安卓传输不了js json对象,只能传输string
window.webkit[messageHandlerName](JSON.stringify(parameters))
}
}
由app将原生方法注入到window上供js调用
messageHandlerName 约定的通信方法parameters 需要传入的参数
--save 和 --save-dev 的作用和区别简单描述
简单来说: 使用命令 --save 或者说不写命令 --save ,都会把信息记录到 dependencies 中;
dependencies 中记录的都是项目在运行时需要的文件;
使用命令 --save-dev 则会把信息记录到 devDependencies 中;
devDependencies 中记录的是项目在开发过程中需要使用的一些文件,在项目最终运行时是不需要的;
也就是说我们开发完成后,最终的项目中是不需要这些文件的;
dependencies 中记录的都是项目在运行时需要的文件;
使用命令 --save-dev 则会把信息记录到 devDependencies 中;
devDependencies 中记录的是项目在开发过程中需要使用的一些文件,在项目最终运行时是不需要的;
也就是说我们开发完成后,最终的项目中是不需要这些文件的;
手写代码
柯里化
JSON.parse()
eval("("+json+")")
(new Function('return'+json))()
使用eval和new Function可实现序列化json,
但是要注意xss
但是要注意xss
冒泡排序
插入排序
二分法查找
广度优先排列
归并排序
函数防抖
函数节流
继承
解析url为参数对象
快排
排序
偏函数
浅拷贝
去重
深度优先
深拷贝
事件代理
事件总线
数据类型判断
数组扁平es5
数组扁平es6
数组去重
图片懒加载
找出出现次数最多的元素
找出页面出现最多的元素
字符串模版
AJAX
apply-call-bind
asyncAwait
EventLoop
filtter
forEach
instanceof
JSON.parse
JSON.stringify.js
JSONP
map
new
Object.assign
Object.create
Promise
reduce
setInterval
trim
https://juejin.cn/user/1978776660216136/posts
算法
排序
冒泡排序O(n^2)
对比相邻分的两个,比较大的右移,做两个嵌套循环,每一次循环将最大数移到最右边
优化版
插入排序O(n^2)
将一个记录插入到已经排好序的有序表中,从而一个新的、记录数增1的有序表。在其实现过程使用双层循环,外层循环对除了第一个元素之外的所有元素,内层循环对当前元素前面有序表进行待插入位置查找,并进行移动
归并排序O(NlogN)、稳定
将数组均分两份,直到分到最小单位,排序,最后合并俩个有序数组
快速排序O(nlongn)-n^2、不稳定
以左边第一位作为标记,分别将小于标记安置在左侧,大于标记的安置在右侧,递归两边
复杂度
O(lgn)的解释是:
将一个数据集分成两半,然后将分开的每一半再分成两半,依此类推
O(nlgn)的解释是:
将一个数据集分成两半,然后将分开的每一半再分成两半,依此类推,在此过程中同时遍历每一半数据
以归并排序为例,可以把排序的过程看成一个倒立的二叉树:
从上面看到,倒立的二叉树叶子节点比较的次数,最差的情况下与二叉树的深度相同:就是从root找到一个叶子结点,复杂度为树高,也就是 log2 N。
每个叶子节点比较的次数就可以理解为从root找到每一个叶子结点,复杂度为树高(log2N)*叶子结点个数(N),也就是log2 N * N。
将一个数据集分成两半,然后将分开的每一半再分成两半,依此类推
O(nlgn)的解释是:
将一个数据集分成两半,然后将分开的每一半再分成两半,依此类推,在此过程中同时遍历每一半数据
以归并排序为例,可以把排序的过程看成一个倒立的二叉树:
从上面看到,倒立的二叉树叶子节点比较的次数,最差的情况下与二叉树的深度相同:就是从root找到一个叶子结点,复杂度为树高,也就是 log2 N。
每个叶子节点比较的次数就可以理解为从root找到每一个叶子结点,复杂度为树高(log2N)*叶子结点个数(N),也就是log2 N * N。
效率:O(1) > O(log2n)> o(n)> o(nlog2n) > o(n^2) > o(n^3) > o(2^n) > o(n!) > o(n^n)
空间复杂度:就是消耗的内存空间(包括代码的物理空间、输入数据所需空间、辅助变量所需的空间)
查找
二分查找O(log2n)
深度优先&广度优先
面试
前端缓存
HTTP缓存
强缓存200 from cache
cache-contorl(max-age相对时间)
expires
过期时间
协商缓存304
Etag/if-none-Match
文件标识
last-modify(响应头)/if-modify-since(请求头)
文件更新最后时间,只能精确到1s以内
浏览器缓存
本地缓存
webStorage
localStorage
SessionStorage
Cookie
websql
indexDB
应用缓存,Application Cache
PWA(渐进式页面应用)
主要功能
web app Manifest(实现添加至主屏幕)
service worker实现离线缓存(主要功能)
push notification(实现消息推送)
基础进阶部分
html
哪些标签可以跨域加载资源
script、img、link(href)、iframe
attribute和property有什么区别
attribute是HTML标签属性,值只能是字符串
property是DOM对象的属性,是js对象
img的titl和alt有什么区别
title是鼠标移动到图片上的展示,alt是图片加载不出来时显示
标签语义化
用正确的标签做正确的事
例如header,nav,footer,article
作用:让人和机器更易读懂,也有利于搜索引擎
html5新特性
语义标签 比如 article、footer、header、nav、section
增强型表单 calendar[ˈkælɪndə]、date、time、email、url、search
视频和音频
audio
video
canvas
原理
canvas只是一个h5标签,本身并不具备绘画能力,它本身只是一个画布,是一个容器。
绘图能力是基于html5的getContext('2d')返回的CanvasRenderingContext2D对象来完成的,该对象实现了一个画布所使用的大多数方法。
绘图能力是基于html5的getContext('2d')返回的CanvasRenderingContext2D对象来完成的,该对象实现了一个画布所使用的大多数方法。
const canvas = document.getElementById('payAbilityLoginTree');//获取canvas dom对象
const ctx = canvas.getContext('2d'); //获取绘图对象
<canvas id="payAbilityLoginTree" width="1000" height="800"></canvas>
canvas是一个二维网络,以画布左上角(0,0)为坐标原点,x轴向右延伸,y轴向下延伸。所以canvas画布中的坐标全为正数,没有负数。
CanvasRenderingContext2D对象提供了很多绘图方法,我们可以通过这些方法来绘制任何你需要的图形,我就不一一介绍了,大家可以参考HTML5 参考手册。
const ctx = canvas.getContext('2d'); //获取绘图对象
<canvas id="payAbilityLoginTree" width="1000" height="800"></canvas>
canvas是一个二维网络,以画布左上角(0,0)为坐标原点,x轴向右延伸,y轴向下延伸。所以canvas画布中的坐标全为正数,没有负数。
CanvasRenderingContext2D对象提供了很多绘图方法,我们可以通过这些方法来绘制任何你需要的图形,我就不一一介绍了,大家可以参考HTML5 参考手册。
svg
地理位置
拖放api
web worker
为js创建多线程环境。web worker是运行在后台的javaScript 独立于其他脚本,不会影响页面性能,可以继续做其他事情:如点击
实战场景
1、加密数据:有些加解密的算法比较复杂,或者在加解密很多数据的时候,这会非常耗费计算资源,导致UI线程无响应,因此这是使用Web Worker的好时机,使用Worker线程可以让用户更加无缝的操作UI。
2、预取数据:有时候为了提升数据加载速度,可以提前使用Worker线程获取数据,因为Worker线程是可以是用 XMLHttpRequest 的。
3、预渲染:在某些渲染场景下,比如渲染复杂的canvas的时候需要计算的效果比如反射、折射、光影、材料等,这些计算的逻辑可以使用Worker线程来执行,也可以使用多个Worker线程,这里有个射线追踪的示例。
4、复杂数据处理场景:某些检索、排序、过滤、分析会非常耗费时间,这时可以使用Web Worker来进行,不占用主线程。
5、预加载图片:有时候一个页面有很多图片,或者有几个很大的图片的时候,如果业务限制不考虑懒加载,也可以使用Web Worker来加载图片
2、预取数据:有时候为了提升数据加载速度,可以提前使用Worker线程获取数据,因为Worker线程是可以是用 XMLHttpRequest 的。
3、预渲染:在某些渲染场景下,比如渲染复杂的canvas的时候需要计算的效果比如反射、折射、光影、材料等,这些计算的逻辑可以使用Worker线程来执行,也可以使用多个Worker线程,这里有个射线追踪的示例。
4、复杂数据处理场景:某些检索、排序、过滤、分析会非常耗费时间,这时可以使用Web Worker来进行,不占用主线程。
5、预加载图片:有时候一个页面有很多图片,或者有几个很大的图片的时候,如果业务限制不考虑懒加载,也可以使用Web Worker来加载图片
虽然使用worker线程不会占用主线程,但是启动worker会比较耗费资源
主线程中使用XMLHttpRequest在请求过程中浏览器另开了一个异步http请求线程,但是交互过程中还是要消耗主线程资源
主线程中使用XMLHttpRequest在请求过程中浏览器另开了一个异步http请求线程,但是交互过程中还是要消耗主线程资源
相同点
1.包含完整的JS运行时(JS运行时有两个阶段:编译阶段和执行阶段),支持ECMAScript规范定义的语言语法和内置对象。
2.支持XmlHttpRequest,能独立发送网络请求与后台交互。
3.包含只读的Location,指向Worker线程执行的script url,可通过url传递参数给Worker环境。
4.包含只读的Navigator,用于获取浏览器信息,如通过Navigator.userAgent识别浏览器。
5.支持setTimeout/setinterval计时器,可用于实现异步逻辑。
6.支持WebSocket进行网络I/O;支持IndexedDB进行文件I/O。
不同点
1.Worker线程没有DOM API,无法新建和操作DOM;也无法访问到主线程的DOM Element。
2.Worker线程和主线程间内存独立,Worker线程无法访问页面上的全局变量(window,document等)和JavaScript函数。
3.Worker线程不能调用alert()或confirm()等UI相关的BOM API。
4.Worker线程被主线程控制,主线程可以新建和销毁Worker。
5.Worker线程可以通过self.close自行销毁。
1.包含完整的JS运行时(JS运行时有两个阶段:编译阶段和执行阶段),支持ECMAScript规范定义的语言语法和内置对象。
2.支持XmlHttpRequest,能独立发送网络请求与后台交互。
3.包含只读的Location,指向Worker线程执行的script url,可通过url传递参数给Worker环境。
4.包含只读的Navigator,用于获取浏览器信息,如通过Navigator.userAgent识别浏览器。
5.支持setTimeout/setinterval计时器,可用于实现异步逻辑。
6.支持WebSocket进行网络I/O;支持IndexedDB进行文件I/O。
不同点
1.Worker线程没有DOM API,无法新建和操作DOM;也无法访问到主线程的DOM Element。
2.Worker线程和主线程间内存独立,Worker线程无法访问页面上的全局变量(window,document等)和JavaScript函数。
3.Worker线程不能调用alert()或confirm()等UI相关的BOM API。
4.Worker线程被主线程控制,主线程可以新建和销毁Worker。
5.Worker线程可以通过self.close自行销毁。
web Storage
localStorage
清除缓存就没有
sessionStorage
窗口关闭就没有
webSocket
是h5提供的一种在单个tcp连接上进行双工通讯的协议
浏览器和服务器只需做一个握手动作,就建立一条快速通道,两者之间可以数据相互传送
iframe特点
1、阻塞onload事件
iframe和主页面共享连接池,影响页面并行加载
2、不利于seo
3、增加服务器http请求
4、无法兼容移动设备
xml和html区别
xml可扩展标记语言
html超文本标记语言
xml写法严谨,要求闭合/对大小写敏感、正确嵌套,html没这么严格要求,
HTML是静态的,用于显示数据。 XML是动态的,用于传输数据
svg和canvas区别
svg
不依赖分辨率
使用xml绘制的2d图形语言
可以为某个元素附加javascript事件处理
每个被绘制的图形均被视为对象,如果对象属性变化会重新绘制
复杂度高会减慢渲染速度
(原理都是操作dom。svg要有一个根节点,叫svg标签,就相等于html)
(原理都是操作dom。svg要有一个根节点,叫svg标签,就相等于html)
不适合游戏
适合带有大型渲染区域的应用程序
SVG功能更完善,适合静态图片展示,高保真文档查看和打印的应用场景
svg输出的图形是矢量图形,后期可以修改参数来自由放大缩小,不会失真和锯齿
canvas
依赖分辨率
canvas是逐个像素进行渲染的
通过js绘制出来的2D图形
不支持js事件处理
能够以.jpg和png保存的图片
适合图像密集型游戏
canvas是一个二维网络,以画布左上角(0,0)为坐标原点,x轴向右延伸,y轴向下延伸。所以canvas画布中的坐标全为正数,没有负数。
canvas输出标量画布,就像一张图片一样,放大会失真或者锯齿
echarts
Svg 和 Canvas 是两个可以选择的类库之一,其中 svg 交互性更好,性能较弱,不适用于移动端,在绘制数万个点时会崩溃。
而 canvas 的渲染速度和性能更好,echarts 在 canvas 上构建一层 MVC层,使得它可以像 svg 一样交互。
而 canvas 的渲染速度和性能更好,echarts 在 canvas 上构建一层 MVC层,使得它可以像 svg 一样交互。
html5 的drag api
dragstart
darg
dragenter
dragover
dragleave
drop
gragend
javascript
基础问题
基础语法
字符串
toUpperCase()
toLowerCase()
indexOf()
slice(开始位置,结束位置)
切割数组和字符串,不改变原来的,返回切割后的数组
切割数组和字符串,不改变原来的,返回切割后的数组
和substring差不多,但是接收负数的时候不一样,
参数是负数就从后面数起
substring(开始位置,结束位置)
s.substring(0,5) 从索引0-5(不包括5)
s.substring(7) 从索引7到结束
参数是负数,就是重0开始
substr(开始位置,长度)
参数是负数就从后面数起
split
字符串切割成数组
includles
数组
for...in
遍历对象、数组、字符的key
遍历对象、数组、字符的key
遍历对象的key
如果是循环数组或者字符串,返回是索引
for...of
支持数组、字符串遍历,不支持对象
支持数组、字符串遍历,不支持对象
遍历数组,返回是数组里的value
for..of适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合.
但是不能遍历对象,因为没有迭代器对象.
但是不能遍历对象,因为没有迭代器对象.
与forEach()/map不同的是,它可以正确响应break、continue和return语句
map
es6引入了map(键可以是任意类型)
代码
使用map,对象会占用内存,可能不会被垃圾回收。Map对一个对象是强引用
遍历操作
Map.prototype.keys():返回键名的遍历器。
Map.prototype.values():返回键值的遍历器。
Map.prototype.entries():返回所有成员的遍历器。
Map.prototype.forEach():遍历 Map 的所有成员。
WeakMap
只接受对象作为键名(null除外),不接受其他类型的值作为键名。
Weakmap它不会阻止关键对象的垃圾回收
Map 和Set可以被遍历, WeakMap 和WeakSet不能被遍历,没有size方法
因为WeakMap和WeakSet都是对象的弱引用,会可能被垃圾回收,所以不能遍历
因为WeakMap和WeakSet都是对象的弱引用,会可能被垃圾回收,所以不能遍历
WeakMaps 保持了对键名所引用的对象的弱引用,即垃圾回收机制不将该引用考虑在内。只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。也正是因为这样的特性,WeakMap 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakMap 不可遍历。所以 WeakMap 不像 Map,一是没有遍历操作(即没有keys()、values()和entries()方法),也没有 size 属性,也不支持 clear 方法,所以 WeakMap只有四个方法可用:get()、set()、has()、delete()。
WeakMap和Map的区别
Map的键值可以是原始数据类型和引用类型,WeakMap的键值只能是引用类型
Map可以迭代遍历键,WeakMap不可迭代遍历键–因为WeakMap的值任何时候都可能被摧毁(第三点垃圾回收),没必要提供迭代其值的能力
Map所构建的实例是需要手动清理,才能被垃圾回收清除,而WeakMap只要外部的引用消失,所对应的键值对就会自动被垃圾回收清除
为什么WeakMap/WeakSet的键只能是对象
如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的字符串字面量是一个相等的字符串了。
set
和map类似,但没value,但是set不重复,多用于去重数组
代码
用add方法添加(Map用set(key,'关联的值')),参数是放一个值(map是两个(key,value)),是唯一的
遍历操作
Set.prototype.keys():返回键名的遍历器
Set.prototype.values():返回键值的遍历器
Set.prototype.entries():返回键值对的遍历器
Set.prototype.forEach():使用回调函数遍历每个成员
去重原理
set() 函数中会先调用对象的 hash() 方法,获取 hash 结果;
如果 hash 结果相同,用比较操作符 == (也就是调用函数 eq())判断二者的值是否相等;
如果都相等,去重;否则,set() 认为二者不同,两个都保留到结果中。
如果 hash 结果相同,用比较操作符 == (也就是调用函数 eq())判断二者的值是否相等;
如果都相等,去重;否则,set() 认为二者不同,两个都保留到结果中。
WeakSet
的成员只能是对象,而不能是其他类型的值。WeakSet 不可遍历。其余区别和WeakMap一样
forEach
没有返回值
不能return停止
for、forEach、map区别
返回值
map 返回一个数组,在 map 的回调函数中,不使用 return 返回值的话,会返回 undeifned。for 和 forEach 没有返回值。
是否改变原数组
map 不改变原数组,for 和 forEach 可以改变原数组。
中止循环
for 使用return 、 break,是跳出了整个循环,forEach 使用return只是跳出了当前的循环,但无法终止, 使用break报语法错误。
使用return
作用:跳出整个循环
适用范围:任何循环结构
示例代码:
```
for (let i =0; i <5; i++) {
if (i ===3) {
return; // 跳出整个循环 }
console.log(i);
}
```
使用break
作用:跳出当前循环
适用范围:for循环、while循环、do-while循环
示例代码:
```
for (let i =0; i <5; i++) {
if (i ===3) {
break; // 跳出当前循环 }
console.log(i);
}
```
使用return、break的限制
forEach中使用return
作用:跳出当前循环
适用范围:forEach循环
示例代码:
```
[1,2,3,4,5].forEach((num) => {
if (num ===3) {
return; //无法终止循环,只能跳出当前循环 }
console.log(num);
});
```
forEach中使用break
限制:语法错误,forEach不支持使用break关键字
思维导图
使用return、break的区别
使用return
跳出整个循环
适用范围:任何循环结构
使用break
跳出当前循环
适用范围:for循环、while循环、do-while循环
使用return、break的限制
forEach中使用return
跳出当前循环
适用范围:forEach循环
forEach中使用break
限制:语法错误,forEach不支持使用break关键字
输出格式:txt
forEach 无法在所有元素都传递给调用的函数之前终止遍历
for of有个很大的特点是支持数组的break中断
效率
.map()要比.forEach()执行速度更快
不需要计算集合长度时,for效率比forEach高
map因为返回数组所以可以链式操作,foreach不能
indexOf()
concat
数组拼接,不改变原来的数组并返回一个新的数组
slice(start,end)
截取slice(0,3) 从索引0开始,到索引3结束(不包括3)
slice(3),从索引3开始到结束
不改变原数组,返回截取的数组
join
以某字符连接数组
reverse()
反转数组
sort(fn)
排序,返回true就交换
sort()方法没有参数时,按照ascii码进行排序
通过给sort()的参数返回一个负值可以实现数组reverse()效果
sort(next,prev) 参数返回 next - prev时,数组是升序,返回-(next - prev) 即prev - next时,数组是降序
当数组长度小于等于10的时候,采用插入排序,大于10的时候,采用快排。
splice[splaɪs](index,howmany,item1,...itemX)
删除、添加和替换
删除、添加和替换
三个参数(开始位置,删除个数,添加的值)
改变原数组,返回被删除的数组
slice(start,end)和splice(start,num,item1,...itemX)区别
splice改变原数组,返回被删除的item
slice返回新数组,但不改变原数组
slice返回新数组,但不改变原数组
slice可以用于切割字符串slice(start,end),substring(start,end),subStr(start,length)
pop/push
栈常用
删除/添加数组最后一位
unshift/shift
unshift 队头添加
shift 队头删除
js判断数组
typeof无法判断数组,
数组在typeof中归到object中(null,object ,array都归到Object中)
数组在typeof中归到object中(null,object ,array都归到Object中)
typeof 1 // "number"
typeof '1' // "string"
typeof true // "boolean"
typeof Symbol(1) // "symbol"
typeof {} // "object"
typeof [] // "object",小坑
typeof function(){} // "function"
typeof Symbol(1) // "symbol"
typeof undefined // "undefined"
typeof null // "object",出名的坑
typeof可以判断基础类型(除null)+function, 不能判断引用类型(object|Array)
typeof '1' // "string"
typeof true // "boolean"
typeof Symbol(1) // "symbol"
typeof {} // "object"
typeof [] // "object",小坑
typeof function(){} // "function"
typeof Symbol(1) // "symbol"
typeof undefined // "undefined"
typeof null // "object",出名的坑
typeof可以判断基础类型(除null)+function, 不能判断引用类型(object|Array)
instanceof
要判断是不是数组,如果这个Object是原型链上能找到Array构造函数的话,那么这个Object应该是一个数组,如果只能找到Object构造函数的话,那么它就不是一个数组
const a = [];
const b = {};
console.log(a instanceof Array);//true
console.log(a instanceof Object);//true,在数组的原型链上也能找到Object构造函数
console.log(b instanceof Array);//false
instanceof的坑
基础类型一定要是个包装对象才行
基础类型一定要是个包装对象才行
基础类型的判断
检查类型一定要是对象才行,基础类型不一定正确
let num = 1
num instanceof Number // false
typeod num //number
num = new Number(1)
num instanceof Number // true
tyoeof num //Object
1与new Number(1)几乎是一样的,只是区别在于是否封装成对象,所以instanceof的结果是不同的。
所以 1 和 new Number(1)的区别就是原始类型和包装对象的区别
js有八种数据类型,其中null,undefine,Number,String,Boolean是原始类型
除了null,undefine每个原始数据类型都对应一个包装对象
let num = 1
num instanceof Number // false
typeod num //number
num = new Number(1)
num instanceof Number // true
tyoeof num //Object
1与new Number(1)几乎是一样的,只是区别在于是否封装成对象,所以instanceof的结果是不同的。
所以 1 和 new Number(1)的区别就是原始类型和包装对象的区别
js有八种数据类型,其中null,undefine,Number,String,Boolean是原始类型
除了null,undefine每个原始数据类型都对应一个包装对象
复杂类型。 array object不像基础类型会发生以上情况,但是因为都是Object,所以instanceof object 都true
其他类型RegExp Date Function
Function
也正常能分辩类型,但是 Function.prototype和Object所除于原型链的地位一致
也正常能分辩类型,但是 Function.prototype和Object所除于原型链的地位一致
function A() {}
let a = new A()
a instanceof Function // false
a instanceof Object // true
A instanceof Function // true
如上所述,A是个函数,因此没什么概念上的问题。
但是要知道A.__proto__即Function.prototype是ƒ () { [native code] },这是与object以后处于原型链上层的存在,而且与object平级,检测如下:
let obj = {}
obj.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
obj.__proto__.prototype // undefined
let A = function() {}
A.__proto__ // ƒ () { [native code] }
A.__proto__.prototype // undefined
let a = new A()
a instanceof Function // false
a instanceof Object // true
A instanceof Function // true
如上所述,A是个函数,因此没什么概念上的问题。
但是要知道A.__proto__即Function.prototype是ƒ () { [native code] },这是与object以后处于原型链上层的存在,而且与object平级,检测如下:
let obj = {}
obj.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
obj.__proto__.prototype // undefined
let A = function() {}
A.__proto__ // ƒ () { [native code] }
A.__proto__.prototype // undefined
RegExp Date
正常,没什么意外问题
正常,没什么意外问题
let reg = new RegExp(//)
reg instanceof RegExp // true
reg instanceof Object // true
let date = new Date()
date instanceof Date // true
date instanceof Object // true
reg instanceof RegExp // true
reg instanceof Object // true
let date = new Date()
date instanceof Date // true
date instanceof Object // true
constructor
a.constructor 实例的constructor指向生成这个实例的方法(构造函数),但是要保证这个属性不被改写
Object.prototype.toString.call(arr) === '[object Array]'
每一个继承自Object的对象都拥有toSring方法,返回"[object type]"
因为数组的.toString() 方法只返回字符串,所以要借用Object的toString() + call 改变this指向
const a = ['Hello','Howard'];
const b = {0:'Hello',1:'Howard'};
const c = 'Hello Howard';
Object.prototype.toString.call(a);//"[object Array]"
Object.prototype.toString.call(b);//"[object Object]"
Object.prototype.toString.call(c);//"[object String]"
const b = {0:'Hello',1:'Howard'};
const c = 'Hello Howard';
Object.prototype.toString.call(a);//"[object Array]"
Object.prototype.toString.call(b);//"[object Object]"
Object.prototype.toString.call(c);//"[object String]"
es5 arr.isArray()
js改变原数组的方法:pop,push,shift,unshift,splice,reverse,sort
js不改变原数组的方法:concat,join,slice
forEach----只遍历数组, 不返回新数组
map----对每项元素做改变后,返回新数组
reduce----对每项元素做叠加,返回叠加后的值
some----判断数组中某些项是否符合条件(内部return true时跳出整个循环)
every----判断数组中每一项是否符合条件(内部return false时跳出整个循环)
filter----筛选出符合条件的数组
find----筛选数组:找元素
findIndex----筛选数组:找索引
includes----判断数组是否含有某值
map----对每项元素做改变后,返回新数组
reduce----对每项元素做叠加,返回叠加后的值
some----判断数组中某些项是否符合条件(内部return true时跳出整个循环)
every----判断数组中每一项是否符合条件(内部return false时跳出整个循环)
filter----筛选出符合条件的数组
find----筛选数组:找元素
findIndex----筛选数组:找索引
includes----判断数组是否含有某值
函数
arguments
代表传入的参数
函数作用域
函数作用域(函数调用栈,变量环境),函数内部作用域不影响外部作用域,但是内部可以向上访问变量,函数作用域在函数定义是已确定,和调用时位置无关
在还没有ES6的let、const之前,只有函数作用域和全局作用域
变量提升&函数提升
定义在函数内部的变量,会先被提前声明到顶部,执行函数前会先扫描函数内部,
最开始这样设计的原因:函数提升就是为了解决相互递归的问题
最开始这样设计的原因:函数提升就是为了解决相互递归的问题
函数的优先权是最高的,它永远被提升至作用域最顶部,然后才是函数表达式和变量按顺序提升,这一点要牢记
JS的变量提升和函数声明提升规则:(相同覆盖,不同1、变量赋值覆盖函数声明 (函数声明不赋值除外))
call/apply/bind
高阶函数
将函数作为参数传递给其他函数
将函数作为参数传递给其他函数
map
处理数组返回新数组
reduce
把结果继续和下一个元素累积计算
累加/累积
filter
用于过滤数组
sort
排序
every
判断所有元素是否符合,只有又一个不符合就false
some
判断是否有其中一个符合,全部不符合false
find
找出第一个符合条件的元素,没找到就返回undefind
findIndex
和find()类似,不过是返回下标
forEach(fn)
和map类似,不过不会返回数组,fn(值,下标,数组)
闭包
定义:闭包是指有权访问另一个函数作用域中的变量的函数。
函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。
函数 A 返回了一个函数 B,并且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。
原理:利用js的函数作用域,在函数内建立函数来进行,函数的作用域是向上访问变量
被内部函数访问的外部函数的变量可以保存在外部函数作用域中而不被回收(遇到闭包,可以重点关注这个被内部函数引用的变量)
例子
作用
封装变量:管理私有变量和私有方法,将对变量的变化封装在安全的环境中
避免命名全局污染问题,命名可以和全局一致
缺点:由于没有垃圾回收,可能会内存泄露
genertor生成器
可以分步执行js代码
使用yield可以类似中断一样,通过.next()来调用下一步运行
分类
IIFF(立即执行函数)
使用时加()是因为将函数声明转换成函数表达式来调用
这里创建了一个匿名函数(在第一个括号内),第二个括号(2, 3)用于调用该匿名函数,并传入参数。括号是表达式,是表达式就有返回值,所以可以在后面加一对括号让它们执行
(function {})是表达式, js会去对它求解得到返回值, 由于返回值是一 个函数, 故而遇到();时, 便会被执行
(function {})是表达式, js会去对它求解得到返回值, 由于返回值是一 个函数, 故而遇到();时, 便会被执行
箭头函数(匿名函数)
函数表达式&函数声明
对象
浅拷贝和深拷贝
深拷贝
JSON.parse(JSON.stringify(test))
如果对象中包含 function 或 RegExp Date这些就不能用这种方法了。
如果对象中包含 function 或 RegExp Date这些就不能用这种方法了。
递归克隆
(就是只能实现特定的object的深度复制(比如数组和函数),不能实现包装对象Number,String, Boolean,以及Date对象,RegExp对象的复制)
(就是只能实现特定的object的深度复制(比如数组和函数),不能实现包装对象Number,String, Boolean,以及Date对象,RegExp对象的复制)
栈的实现
引入一个数组 uniqueList 用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在 uniqueList 中了,如果在的话就不执行拷贝逻辑了。
破解递归爆栈
浅拷贝
直接赋值
obj = {...obj1}
obj = Object.assign({},obj1)
当对象中只有一级属性是深拷贝,但是对象中有对象的时候,是浅拷贝
当对象中只有一级属性是深拷贝,但是对象中有对象的时候,是浅拷贝
api
删除对象
delete user.name;
检查属性是否存在
object.hasOwnProperty(key)
for...in
遍历对象、数组、字符的key
遍历对象、数组、字符的key
遍历对象的key
如果是循环数组或者字符串,返回是索引
常用方法
Object.keys(obj) —— 返回一个包含该对象所有的键的数组。
Object.values(obj) —— 返回一个包含该对象所有的值的数组。
Object.entries(obj) —— 返回一个包含该对象所有 [key, value] 键值对的数组。
Object.is(NaN,NaN)=== true // 相当于判断 a === b成不成立
Object.defineProperty(),它可以劫持属性的 setter 与 getter:
标准对象
Date
var now = new Date();
now; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
now.getFullYear(); // 2015, 年份
now.getMonth(); // 5, 月份,注意月份范围是0~11,5表示六月
now.getDate(); // 24, 表示24号
now.getDay(); // 3, 表示星期三
now.getHours(); // 19, 24小时制
now.getMinutes(); // 49, 分钟
now.getSeconds(); // 22, 秒
now.getMilliseconds(); // 875, 毫秒数
now.getTime(); // 1435146562875, 以number形式表示的时间戳
RegExp正则
api
创建new RegExp()
验证字符串 reg.test(str)
分组reg.exec(str)
切分str.split(reg)
str.replace
replace 函数用于在字符串中用一些字符替换另一些字符
str.match
返回的结果与 exec 函数的结果比较类似,match 是字符串的方法
特殊字符,用于匹配某类字符
^ 匹配字符串的开头
$ 匹配字符串的结尾
[] 匹配给定字符集中的一个字符
a|b 匹配a或b,
. 匹配除了换行符\n之外的任何字符;
\s 匹配任意空白字符,等价于[\t\n\r\f\v]
\S 匹配任意非空白字符,等价于[^\t\n\r\f\v]
\w 匹配字母数字下划线,等价于[0-9a-zA-Z_]
\W 匹配非字母数字及下划线,等价于[^0-9a-zA-Z_]
\d 匹配任意一个数字,等价于[0-9]
\D 匹配任意一个非数字,等价于[^0-9]
\b 匹配边界
$ 匹配字符串的结尾
[] 匹配给定字符集中的一个字符
a|b 匹配a或b,
. 匹配除了换行符\n之外的任何字符;
\s 匹配任意空白字符,等价于[\t\n\r\f\v]
\S 匹配任意非空白字符,等价于[^\t\n\r\f\v]
\w 匹配字母数字下划线,等价于[0-9a-zA-Z_]
\W 匹配非字母数字及下划线,等价于[^0-9a-zA-Z_]
\d 匹配任意一个数字,等价于[0-9]
\D 匹配任意一个非数字,等价于[^0-9]
\b 匹配边界
量词
* 匹配前一个表达式0次或多次,等价于{0,}
+ 匹配前一个表达式1次或多次,等价于{1,}
? 匹配前一个表达式0次或1次,等价于{0,1}
{m,n} 匹配前一个表达式m至n次
{m} 精确匹配前一个表达式m次
{m,} 匹配前一个表达式至少m次
字符集
字符集,字符集规定了可选的字符,通常表示为 []。
0到9之间的任意字符可以表示为 [0-9],在字符集里面,- 在两个字符之间,和两边的字符连在一起表示为两个字符之间的全部字符。
在字符集里面,"." 没有特殊含义。
值得注意的是在没有使用量词的情况下,在正则表达式中,一个字符集占用一个字符的位置,只是该位置的字符可能是字符集中的任意一个字符。如果要表示两个字符,在不适用量词的前提下,要使用两个字符集。
0到9之间的任意字符可以表示为 [0-9],在字符集里面,- 在两个字符之间,和两边的字符连在一起表示为两个字符之间的全部字符。
在字符集里面,"." 没有特殊含义。
值得注意的是在没有使用量词的情况下,在正则表达式中,一个字符集占用一个字符的位置,只是该位置的字符可能是字符集中的任意一个字符。如果要表示两个字符,在不适用量词的前提下,要使用两个字符集。
组()
/^abc{3}$/ //abccc
/^(abc){3}$/ abcabcabc //abc重复3次
/^(abc){3}$/ abcabcabc //abc重复3次
Json
json字符串转json对象
var obj = eval('('+str+')');
var obj = JSON.parse(str);
var obj = (new Function('renturn'+json))()
json对象转为json字符串
var str = JSON.stringify(obj);
其他
怎样判断类型
判断基础类型:typeof()
判断对象:instanceof instanceof 运算符用来检测 constructor.prototype 是否存在于参数 object 的原型链上。
Object.prototype.toString.call()等
js有几种类型,分别怎样存储
javascript一共有8种数据类型,原始数据类型(7种)和引用数据类型(1种 object)
原始数据类型, undefined null number string boolean、
symbol (ES6,用于创建一个独一无二的标识)、
bigint (ES10解决超大整数值16位的精度问题)
以上都是通过栈存储
symbol (ES6,用于创建一个独一无二的标识)、
bigint (ES10解决超大整数值16位的精度问题)
以上都是通过栈存储
引用数据类型object,包括(对象, 数组, 函数) 通过堆存储, 因为他们大小不固定如果存在栈中会影响性能
子主题
栈(Stack)
栈是一种只能先进后出的内存结构。
栈只能在一端对数据进行操作,也就是栈顶端进行操作。
栈也是一种内存自我管理的结构,压栈自动分配内存,出栈自动清空所占内存
栈中的内存不能动态请求,只能为大小确定的数据分配内存,灵活性不高,但是栈的执行效率很高
栈的可用空间并不大,所以我们在操作分配到栈上的数据时要注意数据的大小带来的影响
堆(Heap)
相比栈只能在一端操作,堆中的数据可以随意存取。
能存储大量数据,而且堆能够动态分配存储空间
但堆的结构使得堆的执行效率不如栈高,而且不能自动回收使用过的对象
原文链接:https://blog.csdn.net/nicepainkiller/article/details/78213694
==,===,Object.is 区别
数字
0.1+0.2 !== 0.3
原因:js对于超大整数超过16位的会出现精度丢失问题,
计算机使用二进制表示0.1和0.2都是一个无限数,所以相加得出的数是一个接近于0.3的数,所以就有0.1+0.2 !== 0.3
计算机使用二进制表示0.1和0.2都是一个无限数,所以相加得出的数是一个接近于0.3的数,所以就有0.1+0.2 !== 0.3
解决:取整
移位符
x >> 1
x除以2后取整
取整:
x >>> 0 本质上就是保证x有意义(为数字类型),且为正整数
~~3.14 = 3
3.14 >> 0 = 3
取整
保留数据的整数,去除小数点后的数据parseInt();
向上取整,只要有小数,就个位加一Math.ceil();
向下取整,只要有小数,就个位减一Math.floor();
四舍五入取整Math.round();
保留小数点后的2位小数toFixed(2);
隐式转换
undefined、null、0、-0、NaN、“”为false
除了undefined==null//true
undefined、null 和谁== 都false
undefined、null 和谁== 都false
NaN 和什么比都false(NaN是number类型)
面试
instanceof的坑
基础类型的判断
检查类型一定要是对象才行,基础类型不一定正确
let num = 1
num instanceof Number // false
num = new Number(1)
num instanceof Number // true
1与new Number(1)几乎是一样的,只是区别在于是否封装成对象,所以instanceof的结果是不同的。
let num = 1
num instanceof Number // false
num = new Number(1)
num instanceof Number // true
1与new Number(1)几乎是一样的,只是区别在于是否封装成对象,所以instanceof的结果是不同的。
复杂类型array object 不像基础类型医院发生这种情况,但是因为都是Object,所以instanceof object 都true
其他类型RegExp Date Function
Function
也正常能分辩类型,但是 Function.prototype和Object所除于原型链的地位一致
也正常能分辩类型,但是 Function.prototype和Object所除于原型链的地位一致
function A() {}
let a = new A()
a instanceof Function // false
a instanceof Object // true
A instanceof Function // tru
如上所述,A是个函数,因此没什么概念上的问题。
但是要知道A.__proto__即Function.prototype是ƒ () { [native code] },这是与object以后处于原型链上层的存在,而且与object平级,检测如下:
let obj = {}
obj.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
obj.__proto__.prototype // undefined
let A = function() {}
A.__proto__ // ƒ () { [native code] }
A.__proto__.prototype // undefined
let a = new A()
a instanceof Function // false
a instanceof Object // true
A instanceof Function // tru
如上所述,A是个函数,因此没什么概念上的问题。
但是要知道A.__proto__即Function.prototype是ƒ () { [native code] },这是与object以后处于原型链上层的存在,而且与object平级,检测如下:
let obj = {}
obj.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
obj.__proto__.prototype // undefined
let A = function() {}
A.__proto__ // ƒ () { [native code] }
A.__proto__.prototype // undefined
RegExp Date
正常,没什么意外问题
正常,没什么意外问题
let reg = new RegExp(//)
reg instanceof RegExp // true
reg instanceof Object // true
let date = new Date()
date instanceof Date // true
date instanceof Object // true
reg instanceof RegExp // true
reg instanceof Object // true
let date = new Date()
date instanceof Date // true
date instanceof Object // true
typeof
(基本上可以判断大部分基础类型和function,除了typeof null == object)
(基本上可以判断大部分基础类型和function,除了typeof null == object)
typeof 1 // "number"
typeof '1' // "string"
typeof true // "boolean"
typeof Symbol(1) // "symbol"
typeof {} // "object"
typeof [] // "object",小坑
typeof function(){} // "function"
typeof Symbol(1) // "symbol"
typeof undefined // "undefined"
typeof null // "object",出名的坑
typeof可以判断基础类型(除null)+function, 不能判断引用类型(object|Array)
typeof '1' // "string"
typeof true // "boolean"
typeof Symbol(1) // "symbol"
typeof {} // "object"
typeof [] // "object",小坑
typeof function(){} // "function"
typeof Symbol(1) // "symbol"
typeof undefined // "undefined"
typeof null // "object",出名的坑
typeof可以判断基础类型(除null)+function, 不能判断引用类型(object|Array)
面向对象编程,JavaScript继承
Iterator迭代器
对象的成员可以遍历,是因为该对象实现了Iterator 接口
es6+
es6新特性
let const变量
var、let、const区别
变量提升
var声明的变量存在变量提升,可以先试用再声明,let、const不存在变量提升
声明&初始化
var:遇到有var的作用域,在任何语句执行前都已经完成了声明和初始化,也就是变量提升而且拿到undefined的原因由来
function: 声明、初始化、赋值一开始就全部完成,所以函数的变量提升优先级更高
let:解析器进入一个块级作用域,发现let关键字,变量只是先完成声明,并没有到初始化那一步。此时如果在此作用域提前访问,则报错Cannot access 'value' before initialization,这就是暂时性死区的由来。等到解析到有let那一行的时候,才会进入初始化阶段。如果let的那一行是赋值操作,则初始化和赋值同时进行
const、class都是同let一样的道理
块级作用域
var声明的变量是全局的变量,let、const声明的变量在声明的代码块作用域中生效
重复声明
var重复声明变量会向上覆盖,let和const不能在同一块级作用域下重复声明
修改声明的变量
var和let可以改变变量的值,const不可以,一旦声明、必须赋值,并且常量的值不可以改变
内存分配
var的话会直接在栈内存里预分配内存空间,然后等到实际语句执行的时候,再存储对应的变量,如果传的是引用类型,那么会在堆内存里开辟一个内存空间存储实际内容,栈内存会存储一个指向堆内存的指针
let的话,是不会在栈内存里预分配内存空间,而且在栈内存分配变量时,做一个检查,如果已经有相同变量名存在就会报错
const的话,也不会预分配内存空间,在栈内存分配变量时也会做同样的检查。不过const存储的变量是不可修改的,对于基本类型来说你无法修改定义的值,对于引用类型来说你无法修改栈内存里分配的指针,但是你可以修改指针指向的对象里面的属性
模板字符串
展开运算符
解构赋值
for of
箭头函数(表达式)
class
super
extends
在子类构造函数中,使用this前,必须先调用超级类super
WeakMap 和 WeakSet
Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
const p = new Proxy(target, handler)
generator
es7新特性
includes
求幂运算 **
es8
async/await
object.value() & Object.entries()
String padStart()和padStart()
Object.getOwnPropertyDescriptors()
es9
for await of
Object Rest& Spread
Promise.prototype.finally
es10
try...catch
flat()
flatMap()
Object.fromEntries()
String.trimStart & String.trimEnd
trimStart:去除头部空格
trimEnd:去除尾部空格
String.prototype.matchAll
BigInt
Symbol.prototype.descripition
Function.prototype.toString()
es11
BigInt
空值合并运算 ??
Optional Chaining(可选链) ?.
Global this
比如在浏览器获取全局对象是this、window
比如在Node中我们需要通过global来获取
比如在Node中我们需要通过global来获取
Promise.allSettled
es12
WeakRef
||=
&&=
??=
&&=
??=
Numeric Separator(数字分割):123_450_330
String.replaceAll:字符串替换
Symbol
代表用给定名称作为唯一标识
代表用给定名称作为唯一标识
创建let id = Symbol('aa')
特性
唯一
Symbol确保唯一,即使采用相同的名称。也会产生不同的值
隐藏
不能for in 来遍历得到,大多数库内置方法和语法结构约定Symbol是隐藏的
内置Object.getOwnPropertSymbol(obj) 获取所有Symbol
或者Reflect.ownKeys(obj), 返回对象所有key,包括Symbol
应用
创建唯一的属性键
防止和第三方属性名冲突
Typescript使用Symbol定义私有方法和属性
箭头函数
不能new ,因为是匿名函数,不能作为构造函数
没有arguments,使用展开运算... 解决
不绑定this,捕获其所在的上下文的this值,作为自己的this值。this不被改变,call,bind apply都不行
有__proto__ ,没有原型prototype (箭头函数的构造函数是Funtion)
fn.__proto__ === Function.prototype
fn.__proto__ === Function.prototype
不能用generator(因为标准规范定义了生成器是function*。箭头函数就无法匹配function*关键字)
Promise
async await
是promise和generator语法糖 ,async和await本身返回的也是一个Promise,
它只是把await后面的代码放到了await返回的Promise的.then后面,以此来实现的。
就是将generator函数的*换成async,将yield替换成await。
它只是把await后面的代码放到了await返回的Promise的.then后面,以此来实现的。
就是将generator函数的*换成async,将yield替换成await。
await async2();
console.log("async1 end");
// 可以理解为这样
// new Promise((reslove) => {
// 同步执行
// async2()
// }).then(() => {
// // await 等待后面所有语句
// console.log('async1 end')
// })
console.log("async1 end");
// 可以理解为这样
// new Promise((reslove) => {
// 同步执行
// async2()
// }).then(() => {
// // await 等待后面所有语句
// console.log('async1 end')
// })
async函数对generator的改进
(1)内置执行器,不需要使用next()手动执行。
(2)await命令后面可以是Promise对象或原始类型的值,yield命令后面只能是Thunk函数或Promise对象。
(3)返回值是Promise。返回非Promise时,async函数会把它包装成Promise返回。(Promise.resolve(value))
意义:使用同步方式运行异步代码,解决地狱回调
Promise.all和Promise.race
generator
以迭代器的方式调用异步代码,使用function* 和yield控制函数的返回,通过返回一个对象,使用. next()来触发函数一步一步的调用
Generator 函数,是可以暂停执行的,函数名之前要加’*’。其实整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器
yield 异步操作需要暂停的地方
next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,这个对象就是具有两个属性
yield 异步操作需要暂停的地方
next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,这个对象就是具有两个属性
手写
手写Pormise
如果已经有三个promise,A、B和C,想串行执行,该怎么写
```JavaScript
// promise
A.then(B).then(C).catch(...)
// async/await
(async () => {
await a();
await b();
await c();
})()
```
规范
js严格模式
use strict
避免不必要性能损耗并且安全
规范
https://www.jianshu.com/p/14d285639aaf
所有变量需要定义var
全局this指向undefined
禁用whit(),whit性能不好
参数和属性不可以重名
禁止用八进制表示法
函数有声明的必须写在顶层
eval单独作用域
无法删除变量
模块化
说一下Commonjs/AMD/CMD/ES6
CommonJs
NodeJS、webpack都是基于该规范来实现的
浏览器可以运行commonjs吗
浏览器不兼容CommonJS的根本原因,在于缺少四个Node.js环境的 变量。module、exports、require、global
只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。
var module = {exports: {} };
(function(module, exports) {
exports.multiply = function (n) { return n * 1000 };
}(module, module.exports))
var f = module.exports.multiply; f(5) // 5000
上面代码向一个立即执行函数提供 module 和 exports 两个外部变量,模块就放在这个立即执行函数里面。模块的输出值放在 module.exports 之中,这样就实现了模块的加载
webpack做的事情主要是实现前端模块化(即:让前端也可以像node端一样适用require方法加载模块)和借助插件实现编译、热加载等功能
只要能够提供这四个变量,浏览器就能加载 CommonJS 模块。
var module = {exports: {} };
(function(module, exports) {
exports.multiply = function (n) { return n * 1000 };
}(module, module.exports))
var f = module.exports.multiply; f(5) // 5000
上面代码向一个立即执行函数提供 module 和 exports 两个外部变量,模块就放在这个立即执行函数里面。模块的输出值放在 module.exports 之中,这样就实现了模块的加载
webpack做的事情主要是实现前端模块化(即:让前端也可以像node端一样适用require方法加载模块)和借助插件实现编译、热加载等功能
ES6 Module
在ES6前,前端也实现了一套相同的模块规范(如AMD),自ES6之后,引入了一套新的ES6 Module规范,有望成为浏览器和服务端通用的模块管理方案,
但目前浏览器对ES6 模块兼容还不太好,我们平时在 Webpack 中使用的 export 和 import,会经过 Babel 转换为 CommonJS 规范。完整的流程是 es6->es5(commonJS规范)->浏览器可执行代码。 重点只在于他们是直接用es5写还是用es6写,用es6的话就多了一个转换的步骤
但目前浏览器对ES6 模块兼容还不太好,我们平时在 Webpack 中使用的 export 和 import,会经过 Babel 转换为 CommonJS 规范。完整的流程是 es6->es5(commonJS规范)->浏览器可执行代码。 重点只在于他们是直接用es5写还是用es6写,用es6的话就多了一个转换的步骤
ES6 Module导出的不是一个对象,导出的是一个个接口,在import时可以指定加载某个输出值,而不是加载整个模块。因此在编译时就引入模块代码,就能确定模块之间的依赖关系,而不是在代码运行时加载,所以才说ES6 Module是静态加载的。Tree Shaking就是根据这个特性在编译阶段摇掉无用模块的。
ES6 的模块自动采用严格模式
import命令输入的变量都是只读的
import命令具有提升效果,会提升到整个模块的头部,首先执行
由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。
因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。
本质上,export default就是输出一个叫做default的变量或方法
import命令输入的变量都是只读的
import命令具有提升效果,会提升到整个模块的头部,首先执行
由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。
因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。
本质上,export default就是输出一个叫做default的变量或方法
import()
import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。
适用场合
按需加载
条件加载
动态的模块路径
es6和CommonJs区别
CommonJS模块输出是一个值的拷贝,ES6模块输出的是值的引用
CommonJS模块是运行时加载,ES6是编译时就能确定模块依赖关系
CommonJS是单个值导出,ES6Module可以导出多个
CommonJS是动态语法(运行时加载)可以写在任何地方,ES6Module是静态加载(编译时加载)import 和 export只能写在作用域顶层,
不能写在块级作用域中。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷
不能写在块级作用域中。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷
CommonJS的this是当前模块,ES6 Module的this是undefined
CommonJS是同步导入,而ES6 Module是异步导入
因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。
而ES6 Module是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。
而ES6 Module是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响
AMD
RequireJs-异步加载js文件,依赖前置,提前执行
通过define第一个参数引入包,return 输出结果,
所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
requireJs它本身就是先加载后执行
RequireJS的做法是并行加载所有依赖的模块, 并完成解析后, 再开始执行其他代码, 因此执行结果只会停顿1次, 完成整个过程是会比SeaJS要快.
原理
requireJS 的核心原理是什么?(如何动态加载的?如何避免多次加载的?如何缓存的?
它需要依次的加载模块然后去进行相应的操作,加载模块就是要引入这个文件,那么这里也还是通过动态加载 script 的方法,并通过 onload 去执行后面的回调了。
1,概念
requireJS是基于AMD模块加载规范,使用回调函数来解决模块加载的问题。
2,原理
requireJS是使用创建script元素,通过指定script元素的src属性来实现加载模块的。
3,特点
1. 实现js文件的异步加载,避免网页失去响应
2,管理模块之间的依赖,便于代码的编写和维护
4,项目优化
r.js 是基于requirejs模块化的基础上进一步的压缩和打包成一个js,请求数大大减少,便于优化
————————————————
原文链接:https://blog.csdn.net/mzrxLXR/article/details/81703099
requireJS是基于AMD模块加载规范,使用回调函数来解决模块加载的问题。
2,原理
requireJS是使用创建script元素,通过指定script元素的src属性来实现加载模块的。
3,特点
1. 实现js文件的异步加载,避免网页失去响应
2,管理模块之间的依赖,便于代码的编写和维护
4,项目优化
r.js 是基于requirejs模块化的基础上进一步的压缩和打包成一个js,请求数大大减少,便于优化
————————————————
原文链接:https://blog.csdn.net/mzrxLXR/article/details/81703099
CMD
seajs 通过require引入依赖,CMD依赖就近,延迟执行
SeaJS一样是并行加载所有依赖的模块, 但不会立即执行模块, 等到真正需要(require)的时候才开始解析
按需加载
当用户触发了动作时才加载对应的功能。触发的动作,是要看具体的业务场景而言,包括但不限于以下几个情况:鼠标点击、输入文字、拉动滚动条,鼠标移动、窗口大小更改等。加载的文件,可以是JS、图片、CSS、HTML等。
动画
setTimeout
低端机出现动画不流畅
执行时间不确定(seTimeout等主线程执行完毕在执行异步队列
分辨率刷新时间和设置时间不一样同步(使用requestanimationframe)
requestanimationframe
进阶
通讯
原生ajax封装成Promise
异步加载js的方式
async script插入到dom中通过加载后执行callback函数来进行加载
浏览器之间的通讯
cookie
同域可传送
大小4k,太多影响速度和流量
兼容性好,每次http请求都携带
cookie 设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
sessionStorage
不支持跨标签页面共享数据
关闭标签就数据消失
5m
localStorage
大小5m
localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据
web worker
为js创建多线程环境
注意
同源限制
DOM限制
只能读取navigator和location对象
通讯限制
和主线程不在一个上下文环境,不能直接通讯,必须通过消息完成postMessage
脚本限制
不能用alert()和confirm()
文件限制
无法打开本机文件系统
ajax、fetch和axios
ajax
手写ajax
原理
在用户和服务器之间加了一个中间层,通过XMLHttpRequest()对象来向服务器发送异步请求
通过监听readyStatechange事件,通过实例的readyState来判断ajax状态
readyState的5种状态
0:未初始化
1:正在加载
2:已加载
3:交互中
4:完成
优点
提高了性能和速度:减少了客户端和服务器之间的流量传输,同时减少了双方响应的时间,响应更快,因此提高了性能和速度
交互性好:使用ajax,可以开发更快,更具交互性的Web应用程序
异步调用:Ajax对Web服务器进行异步调用。这意味着客户端浏览器在开始渲染之前避免等待所有数据到达。
节省带宽:基于Ajax的应用程序使用较少的服务器带宽,因为无需重新加载完整的页面
底层使用XMLHttpRequest
拥有开源JavaScript库 : JQuery,Prototype,Scriptaculous等。
AJAX通过HTTP协议进行通信。
缺点
Ajax应用程序中的安全性较低(容易收到CSRF和XSS攻击),因为所有文件都是在客户端下载的
JQuery整个项目太大,单纯使用ajax却要引入整个JQuery非常的不合理(采取个性化打包的方案又不能享受CDN服务)
配置和调用方式非常混乱,而且基于事件的异步模型不友好。
fetch
优点
语法简洁,更加语义化
基于标准 Promise 实现,支持 async/await,支持链式调用
更加底层,提供的API丰富(request,response)
脱离了XHR,是ES规范里新的实现方式
缺点
fetch只对网络请求报错,对400,500都当做成功的请求,需要封装去处理
fetch默认不会带cookie,需要添加配置项
fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject和Promise.race结合setTimeout实现的超时控制并不能阻止请求过程继续在后台执行,
造成了量的浪费
造成了量的浪费
fetch没有办法原生监测请求的进度,而XHR可以
axios
优点
从node.js创建http请求
在浏览器中创建XMLHttpRequest
支持Promise API
提供了一些并发请求的接口
支持拦截请求和响应
转换请求和响应数据
取消请求
自动转换JSON数据
客户端支持防御CSRF(跨站请求攻击)/XSRF
让每一个请求带一个从cookies中拿到带key,同源策略假冒网站是拿不到cookies的,
这样,后台就可以辨别这个请求是否假冒网站误导输入
让每一个请求带一个从cookies中拿到带key,同源策略假冒网站是拿不到cookies的,
这样,后台就可以辨别这个请求是否假冒网站误导输入
浏览器缓存问题
强缓存200 from cache
expires(http/1.0)
Expires: Wed, 22 Oct 2018 08:41:00 GMT
表示资源过期时间
Catch-Control(http/1.1)
优先级较expires高
Cache-control: max-age=30
资源多少s后过期
协商缓存304
Last-Modified响应头和If-Modified-Since请求头(htpp/1.0),记录文件最后修改时间,只能精确到1s以内
ETagh(响应头)和If-None-Match(请求头)(http/1.1)(优先级高,记录文件标识序列)
用户行为对浏览器缓存的影响
打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache。
强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache(为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。
http、https
七层网络协议(巫术忘传会飙鹰)
从应用层的发送http请求,
到传输层通过三次握手建立tcp/ip连接,
再到网络层的ip寻址,
再到数据链路层的封装成帧,
最后到物理层的利用物理介质传输。
从应用层的发送http请求,
到传输层通过三次握手建立tcp/ip连接,
再到网络层的ip寻址,
再到数据链路层的封装成帧,
最后到物理层的利用物理介质传输。
1.应用层(dns,http) DNS解析成IP并发送http请求
表示层:主要处理两个通信系统中交换信息的表示方式,包括数据格式交换,数据加密与解密,数据压缩与终端类型转换等
会话层:它具体管理不同用户和进程之间的对话,如控制登陆和注销过程
2.传输层(tcp,udp) 建立tcp连接(三次握手)
3.网络层(IP,ARP) IP寻址
4.数据链路层(PPP) 封装成帧
5.物理层(利用物理介质传输比特流) 物理传输(然后传输的时候通过双绞线,电磁波等各种介质)
TCP(传输层)
面向连接、传输可靠、用于传输大量数据(流模式)、速度慢,需要建立连接三次握手开销大(面向场景:http、https、邮件)
UDP(传输层)
面向非连接、传输不可靠、用于传输少量的数据、速度快、可能丢包(主要用于视频会议、聊天,缺少点数据无关紧要,可靠性要求比较低,传输速度快的应用。)
https
超文本传输安全协议,默认端口443。经由http通信+SSL/TLS来加密信息包,主要采用身份认证和数据加密等手段来保证传输安全
HTTPS在TCP三次握手阶段之后,还需要进行SSL 的handshake,协商加密使用的对称加密密钥
HTTPS在TCP三次握手阶段之后,还需要进行SSL 的handshake,协商加密使用的对称加密密钥
过程
浏览器向服务器发送加密通信的请求(请求包包括:随机数R1+支持的加密算法+SSL版本)
服务器返回R2随机数+确认使用的加密算法+确认的SSL版本+服务器证书(包括了用来加密数据是公钥)
浏览器收到证书,校验证书合法性,生成R3随机数,用证书的服务器公钥加密发给服务器
服务器收到数据用私钥解密,这时,服务器和浏览器都有了R1+R2+R3随机数生成的一对对称加密使用的秘钥
校验证书合法性
直接使用非对称加密可能被中间人篡改公钥
CA机构颁发的证书(SSL)
来保证非对称加密过程本身的安全
因为证书的签名是由服务器端网址等信息生成的,
并且通过第三方机构的私钥加密中间人无法篡改; 所以最关键的问题是证书签名的真伪;
直接使用非对称加密可能被中间人篡改公钥
CA机构颁发的证书(SSL)
来保证非对称加密过程本身的安全
因为证书的签名是由服务器端网址等信息生成的,
并且通过第三方机构的私钥加密中间人无法篡改; 所以最关键的问题是证书签名的真伪;
浏览器读取证书中的证书所有者、有效期等信息进行一一校验
浏览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发
如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。
如果找到,那么浏览器就会从操作系统中取出 颁发者CA 的公钥,然后对服务器发来的证书里面的签名进行解密
浏览器使用相同的hash算法计算出服务器发来的证书的hash值,将这个计算的hash值与证书中签名做对比 (防止信息被修改)
对比结果一致,则证明服务器发来的证书合法,没有被冒充, 此时浏览器就可以读取证书中的公钥,用于后续加密了
浏览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发
如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的。
如果找到,那么浏览器就会从操作系统中取出 颁发者CA 的公钥,然后对服务器发来的证书里面的签名进行解密
浏览器使用相同的hash算法计算出服务器发来的证书的hash值,将这个计算的hash值与证书中签名做对比 (防止信息被修改)
对比结果一致,则证明服务器发来的证书合法,没有被冒充, 此时浏览器就可以读取证书中的公钥,用于后续加密了
http2.0和http1.1
区别
HTTP/1.0
增加了POST和HEAD方法
可以传输任何格式的内容
请求和响应都加上了header
不能保持长连接
支持缓存
HTTP/1.1
支持长连接(Connection: keep-alive)和流水线
协商缓存支持Etag
支持断点续传(只请求一部分,header中的range字段)状态码是206
增加HOST头,一台物理服务器(一个ip)可以对应多个虚拟主机(域名)
HTTP/2.0
多二 服头
多二 服头
多路复用
HTTP1.0,因为不支持长连接,每发送一次请求就要建立一次TCP连接。
HTTP/1.1,开始支持长连接,但是 HTTP/1.1 协议中 「浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞」
HTTP/2.0,支持多路复用,即并行复用TCP连接,多个请求可以同时发送,对同一个域名的多个请求只用建立1个TCP连接了
http1.1keep-alive和htttp2,0多路复用区别:
http1:要按顺序返回响应,等待上一个响应后才发起下一个响应,为解决队头阻塞使用多个域名开启多个tcp,单个TCP连接同一时刻只能处理一个请求
http2:二进制分帧下,不需按照顺序发送和返回,不需等待上一次请求返回,同一个域名下建立一个tcp,处理多个同域名的http请求,同一时刻可发送多个请求和响应
HTTP/1.1,开始支持长连接,但是 HTTP/1.1 协议中 「浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制。超过限制数目的请求会被阻塞」
HTTP/2.0,支持多路复用,即并行复用TCP连接,多个请求可以同时发送,对同一个域名的多个请求只用建立1个TCP连接了
http1.1keep-alive和htttp2,0多路复用区别:
http1:要按顺序返回响应,等待上一个响应后才发起下一个响应,为解决队头阻塞使用多个域名开启多个tcp,单个TCP连接同一时刻只能处理一个请求
http2:二进制分帧下,不需按照顺序发送和返回,不需等待上一次请求返回,同一个域名下建立一个tcp,处理多个同域名的http请求,同一时刻可发送多个请求和响应
HTTP1.1 keep-alive和http2.0多路复用区别:
HTTP/1.x 虽然引入了 keep-alive 长连接,但它每次请求必须等待上一次响应之后才能发起,所谓队头阻塞
HTTP/1.x 是基于文本的,只能整体去传;HTTP/2 是基于二进制流的,可以分解为独立的帧,交错发送
HTTP/1.x keep-alive 必须按照请求发送的顺序返回响应;HTTP/2 多路复用不按序响应
HTTP/1.x keep-alive 为了解决队头阻塞,将同一个页面的资源分散到不同域名下,开启了多个 TCP 连接;HTTP/2 同域名下所有通信都在单个连接上完成
HTTP/1.x keep-alive 单个 TCP 连接在同一时刻只能处理一个请求(两个请求的生命周期不能重叠);HTTP/2 单个 TCP 同一时刻可以发送多个请求和响应
二进制分帧
在应用层跟传送层之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量)
二进制分帧层中, HTTP/2 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码
其中 HTTP1.x 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面
二进制分帧层中, HTTP/2 会将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码
其中 HTTP1.x 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面
服务端推送
服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端
服务端能够更快的把资源推送给客户端。例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 再发送这些请求。当客户端需要的时候,它已经在客户端了。
服务端能够更快的把资源推送给客户端。例如服务端可以主动把 JS 和 CSS 文件推送给客户端,而不需要客户端解析 HTML 再发送这些请求。当客户端需要的时候,它已经在客户端了。
header压缩
HTTP2.0的副作用
HTTP2.0不会是万金油,HTTP2.0最大的亮点在于多路复用,而多路复用的好处只有在http请求量大的场景下才明显,所以有人会觉得只适用于浏览器浏览大型站点的时候。这么说其实没错,但http2.0的好处不仅仅是multiplexing,请求压缩,优先级控制,server push等等都是亮点。对于内容型移动端app来说,比如淘宝app,http请求量大,多路复用还是能产生明显的体验提升。
如果http2.0全面应用,很多http1.1中的优化方案就无需用到了(譬如打包成精灵图,静态资源多域名拆分等)
浏览器xss(跨站脚本攻击)
CSRF(跨站请求伪造)
CSRF(跨站请求伪造)
xss(跨站脚本攻击)
嵌入js脚本
嵌入js脚本
如何攻击:
反射型 XSS 攻击
诱导用户去访问一个包含恶意脚本的 URL,当用户访问这个带有恶意脚本的 URL 时,网站又把恶意 JavaScript 脚本返回给用户执行
反射型 XSS 通常出现在网站的搜索栏、用户登录口等地方,常用来窃取客户端 Cookies 或进行钓鱼欺骗
简单解释一下大致意思,当用户在网站的地址栏输入URL,从服务器端获取到数据时,网站页面会根据返回的信息而呈现不同的返回页面,如果这个时候恶意用户在页面中输入的数据不经过验证且不经过超文本标记语言的编码就录入到脚本中,就会产生漏洞(就是直接输入脚本,发送请求都时候,没有服务端没做验证直接返回页面)
存储型 XSS 攻击
存储型 XSS 攻击是指黑客利用站点漏洞将一段恶意 JavaScript 代码提交到网站的数据库中 存储 ,当用户访问网站的时候,网站将恶意脚本同正常页面一起返回,浏览器解析执行了网站中的恶意脚本,将用户的 Cookie 信息等数据上传到恶意服务器
存储型 XSS 攻击经常出现在个人信息或发表文章等地方,插入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,用户访问该页面的时候触发代码执行。这种 XSS 比较危险,容易造成蠕虫,盗窃 cookie 等
基于 DOM 的 XSS 攻击
基于 DOM 的 XSS 攻击是指通过恶意脚本修改页面的 DOM 结构,是纯粹发生在客户端的攻击。
DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞。
产生主要原因
产生xss的主要原因是程序对输入输出的控制不够严格,导致恶意脚本输入后,再输到前端被浏览器当有效代码解析,从而产生危害
如何阻止
1.对脚本的检验:过滤特殊字符,或对特定字符进行编译转码
2. 对cookies限制。对重要的 cookie 设置 httpOnly,无法通过js脚本将无法读取到cookie信息
3. 对URL检验:URLEncode 操作
将不可信的值输出 URL参数之前,进行 URLEncode操作。对于从 URL 参数中获取值一定要进行格式检测(比如你需要的时URL,就判读是否满足URL格式)
Web 安全头支持
浏览器自带的防御能力,一般是通过开启 Web 安全头生效的
浏览器自带的防御能力,一般是通过开启 Web 安全头生效的
CSP :W3C 的 Content Security Policy,简称 CSP,主要是用来定义页面可以加载哪些资源,减少 XSS 的发生。要配置 CSP , 需要对 CSP 的 policy 策略有了解,具体细节可以参考 CSP 是什么。
X-Download-Options: noopen :默认开启,禁用 IE 下下载框 Open 按钮,防止 IE 下下载文件默认被打开 XSS。
X-Content-Type-Options: nosniff :禁用 IE8 自动嗅探 mime 功能例如 text/plain 却当成 text/html 渲染,特别当本站点 server 的内容未必可信的时候。
X-XSS-Protection :IE 提供的一些 XSS 检测与防范,默认开启
后果
Cookie 信息
document.cookie 获取 Cookie 信息,然后通过 XMLHttpRequest 或 Fetch 加上 CORS 功能将数据发送给恶意服务器
监听用户行为
通过 addEventListener 监听用户行为,获取用户输入的银行卡支付密码等信息
更改 DOM 结构
伪造登录、输入支付秘密等窗口,获取用户私密信息
在页面内生成浮窗广告
劫持流量实现恶意跳转
CSRF(跨站请求伪造)
盗用身份,以你的名义发送恶意请求
盗用身份,以你的名义发送恶意请求
攻击流程如下:
受害者登录 http://a.com ,并保留了登录凭证(Cookie)
攻击者引诱受害者访问了 http://b.com
http://b.com 发送了一个请求:http://a.com/act=xx 。浏览器会默认携带 http://a.com 的Cookie。
http://a.com 接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
http://a.com 以受害者的名义执行了act=xx。
攻击完成,攻击者在受害者不知情的情况下冒充受害者,让 http://a.com 执行了自己定义的操作。
攻击者引诱受害者访问了 http://b.com
http://b.com 发送了一个请求:http://a.com/act=xx 。浏览器会默认携带 http://a.com 的Cookie。
http://a.com 接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
http://a.com 以受害者的名义执行了act=xx。
攻击完成,攻击者在受害者不知情的情况下冒充受害者,让 http://a.com 执行了自己定义的操作。
产生条件
目标站点一定要有 CSRF 漏洞
(1)登录受信任网站A,并在本地生成Cookie。(如果用户没有登录网站A,那么网站B在诱导的时候,请求网站A的api接口时,会提示你登录)
(2)在不登出A的情况下,访问危险网站B(其实是利用了网站A的漏洞)。
几种常见的攻击方式
自动发起 GET 请求的 CSRF
<img src="http://a.com/pay?amount=10000&for=hacker" >
攻击者将支付的接口请求隐藏在 img 标签内,在加载这个标签时,浏览器会自动发起 img 的资源请求,a.com 就会收到包含受害者登录信息的一次跨域请求
自动发起 POST 请求的 CSRF
访问该页面后,表单会自动提交,相当于模拟用户完成了一次 POST 操作。
引诱用户点击链接的 CSRF
链接类型的CSRF并不常见,比起其他两种用户打开页面就中招的情况,这种需要用户点击链接才会触发。这种类型通常是在论坛中发布的图片中嵌入恶意链接,或者以广告的形式诱导用户中招
如何冒充身份
登陆原理:由于http无状态,所以维持登陆一般方式就是利用cookies,发送请求判断这个cookies来看登陆态,一般分两种:1/登陆后将用户信息加密得到token存到cookies里,验证通过解密检查是否存在某个字段,2/使用session,服务器存一份key-value的session信息表,将登陆后用户赋予一个sessionid并存到cookies。
冒充原理:利用之前登陆的cookies,冒充用户访问一个域名下的其他页面,让浏览器携带这个cookies
防范
token验证
请求中添加token字段
后端给cookies添加一个csrftoken ,请求的时候取出csrftoken字段放到body中,后端对比token是否个cookies的token一致(利用同源能访问该源下的cookie)
验证referer
验证是否同域发送的请求(referer可以被修改,不可靠)
验证用户身份:例如验证码
内存与事件
内存泄漏是什么原因怎么解决
原因
一个对象不再使用应该回收没被回收,导致停留在堆内存中
没被声明的全局变量,关闭页面前都不会被释放
在移除dom 时候没解除事件绑定
绑定的事件函数其实是一个闭包,闭包的存在使得该DOM在内存中始终占用着内存,如果不解除绑定的话。
删除的只是DOM结构,内存中依旧保存着数据。所以要手动将DOM占用的内存清空
删除的只是DOM结构,内存中依旧保存着数据。所以要手动将DOM占用的内存清空
目前chrome浏览器在移除dom后,其 绑定 的事件自动 会 被gc回收的。
定时器没移除
闭包
内存分配有限,当超过限制就造成内存溢出
嵌套递归
解决办法:尾调用
某个A函数的最后一步是调用另一个B函数。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。即只保留内层函数的调用帧
原理:代码执行时基于执行栈的,所以在一个函数中调用另一个函数的时候,会保留调用者A的执行上下文(保留直到函数A执行完毕!),然后将被调用的函数B的执行上下文加入到执行栈中
显而易见的是,如果函数之间的相互调用层数过深,就会导致很多的函数执行上下文都被加入到执行栈中,导致栈的溢出!
采用尾调用优化之后的函数,根据定义,函数A只有可能在最后一步才调用函数B,既然是最后一步那么js引擎就无需将A的执行上下文保存在执行栈中了!
显而易见的是,如果函数之间的相互调用层数过深,就会导致很多的函数执行上下文都被加入到执行栈中,导致栈的溢出!
采用尾调用优化之后的函数,根据定义,函数A只有可能在最后一步才调用函数B,既然是最后一步那么js引擎就无需将A的执行上下文保存在执行栈中了!
解决
减少全局变量
避免死循环
不用都东西要及时回收
减少层级过多都引用
js垃圾回收
概念
浏览器垃圾回收会间隔周期性的执行。
数据在没有任何引用的时候才会被定时的垃圾回收机制回收。
数据在没有任何引用的时候才会被定时的垃圾回收机制回收。
栈内存中的基本类型,可以通过操作系统直接处理;
而堆内存中的引用类型,正是由于可以经常变化,大小不固定,因此需要 JavaScript 的引擎通过垃圾回收机制来处理。
而堆内存中的引用类型,正是由于可以经常变化,大小不固定,因此需要 JavaScript 的引擎通过垃圾回收机制来处理。
Chrome 的 JavaScript 引擎 V8 将堆内存分为两类 新生代的回收机制和老生代的回收机制
面试题
浏览器中不同类型变量的内存都是何时释放
Javascritp 中类型:值类型,引用类型。
引用类型
在没有引用之后,通过 V8 自动回收。
值类型
如果处于闭包的情况下,要等闭包没有引用才会被 V8 回收。
非闭包的情况下,等待 V8 的新生代切换的时候回收。
引用类型
在没有引用之后,通过 V8 自动回收。
值类型
如果处于闭包的情况下,要等闭包没有引用才会被 V8 回收。
非闭包的情况下,等待 V8 的新生代切换的时候回收。
哪些情况会导致内存泄露?如何避免?
以 Vue 为例,通常有这些情况:
监听在 window/body 等事件没有解绑
绑在 EventBus 的事件没有解绑
Vuex 的 $store,watch 了之后没有 unwatch
使用第三方库创建,没有调用正确的销毁函数
监听在 window/body 等事件没有解绑
绑在 EventBus 的事件没有解绑
Vuex 的 $store,watch 了之后没有 unwatch
使用第三方库创建,没有调用正确的销毁函数
解决办法:beforeDestroy 中及时销毁
绑定了 DOM/BOM 对象中的事件 addEventListener ,removeEventListener。
观察者模式 $on,$off处理。
如果组件中使用了定时器,应销毁处理。
如果在 mounted/created 钩子中使用了第三方库初始化,对应的销毁。
使用弱引用 weakMap、weakSet。
绑定了 DOM/BOM 对象中的事件 addEventListener ,removeEventListener。
观察者模式 $on,$off处理。
如果组件中使用了定时器,应销毁处理。
如果在 mounted/created 钩子中使用了第三方库初始化,对应的销毁。
使用弱引用 weakMap、weakSet。
闭包会导致内存泄露吗
闭包会导致内存泄露吗?正确的答案是不会。
内存泄露是指你「用不到」(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。
闭包里面的变量就是我们需要的变量,不能说是内存泄露。
内存泄露是指你「用不到」(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。
闭包里面的变量就是我们需要的变量,不能说是内存泄露。
weakMap weakSet 和 Map Set 有什么区别?
在 ES6 中为我们新增了两个数据结构 WeakMap、WeakSet,就是为了解决内存泄漏的问题。
它的键名所引用的对象都是弱引用,就是垃圾回收机制遍历的时候不考虑该引用。
只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。
也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用
它的键名所引用的对象都是弱引用,就是垃圾回收机制遍历的时候不考虑该引用。
只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。
也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用
递归也导致的内存溢出 怎么解决(面试题)
垃圾回收过程
1、标记空间中的可达值
V8 采用的是可达性 (reachability) 算法来判断堆中的对象应不应该被回收
(遍历算法)
(遍历算法)
从根节点出发,遍历所有对象
这些根节点不是垃圾,不可能被回收。
这些根节点不是垃圾,不可能被回收。
在浏览器环境下,根节点有很多,主要包括这几种:
全局变量 window,位于每个 iframe 中
文档 DOM 树
存放在栈上的变量
可以遍历到的对象是可达的
没有遍历的对象是不可达的
2、回收不可达的内存
3、做内存整理
在第二部内存回收之后会产生大量的不连续空间(内存碎片)
如果出现太多内存碎片时,需要分配连续的空间时,可能内存不足
什么时候内存回收
浏览器进行内存回收时会暂停javascript脚本,等待垃圾回收完毕后再继续执行
为避免长期阻塞js脚本会分为
分代收集、增量收集、闲时收集
分代收集、增量收集、闲时收集
分代收集
一句话总结分代回收就是:将堆分为新生代与老生代,
多回收新生代,少回收老生代。
一句话总结分代回收就是:将堆分为新生代与老生代,
多回收新生代,少回收老生代。
长久对象-老生代区域
(新生代中的变量如果经过回收之后依然一直存在,
那么就会被放入到老生代内存中)
主垃圾回收复制对老生代区域垃圾回收
(新生代中的变量如果经过回收之后依然一直存在,
那么就会被放入到老生代内存中)
主垃圾回收复制对老生代区域垃圾回收
全局对象,window,Dom,WebApi
临时对象-新生代区域
副垃圾回收器负责对新生代区域的垃圾回收
副垃圾回收器负责对新生代区域的垃圾回收
大部分对象,如函数内部声明的变量,块级作用域中变量
当函数或者代码执行结束后就会销毁函数作用域中的变量
当函数或者代码执行结束后就会销毁函数作用域中的变量
主垃圾回收器
对象占用空间大。
对象存活时间长。
新生代中的变量如果经过回收之后依然一直存在,那么就会被放入到老生代内存中
对象存活时间长。
新生代中的变量如果经过回收之后依然一直存在,那么就会被放入到老生代内存中
使用「标记-清除」的算法执行垃圾回收
标记
从一组根元素开始,递归遍历这组根元素。
在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据
在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据
清除+整理
对标记的垃圾数据进行清理
多次标记-清除后,会产生大量不连续的内存碎片,需要进行内存整理。
副垃圾回收器
负责新生代的垃圾回收,通常只支持 1~8 M 的容量。
一半是对象区域,一半是空闲区域。
一旦检测到对象区域装满了,就执行垃圾回收
一旦检测到对象区域装满了,就执行垃圾回收
标记+清除
先给对象区域所有垃圾做标记。清理标记垃圾
复制+排序
存活的对象被复制到空闲区域,并且将他们有序的排列一遍
角色互换
复制完成后,对象区域会和空闲区域进行对调。将空闲区域中存活的对象放入对象区域里
增量收集
如果脚本中有许多对象,引擎一次性遍历整个对象,会造成一个长时间暂停。
所以引擎将垃圾收集工作分成更小的块,每次处理一部分,多次处理。
这样就解决了长时间停顿的问题。
所以引擎将垃圾收集工作分成更小的块,每次处理一部分,多次处理。
这样就解决了长时间停顿的问题。
闲时收集
垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
垃圾回收有两种方法
标记清除
它从一个根节点出发将可到达的对象标记, 然后移除未标记的。
引用计数
会记录每个值被引用的次数,当引用次数变成0后,就会被释放掉。
在IE中虽然JavaScript对象通过标记清除的方式进行垃圾回收,但BOM与DOM对象却是通过引用
计数回收垃圾的, 也就是说只要涉及BOM及DOM就会出现循环引用问题。两个对象的引用次数都为2,而不被回收
计数回收垃圾的, 也就是说只要涉及BOM及DOM就会出现循环引用问题。两个对象的引用次数都为2,而不被回收
GC的缺陷
GC时,停止响应其他操作,在100ms
GC优化策略
分代回收(Generation GC)
多回收“临时对象”区(young generation)
少回收“持久对象”区(tenured generation)
减少每次需遍历的对象,从而减少每次GC的耗时
浏览器垃圾回收
js的防抖节流
节流
保证一段时间内调用一次时间
防抖
每触发一次重新计算时间
event loop
异步的原理
js采用eventloop(事件循环)来解决单线程运行时阻塞带来的问题,
(在程序中设置两个线程:一个负责程序本身的运行,称为“主线程”,另一个负责其他进程),被称为eventloop
(在程序中设置两个线程:一个负责程序本身的运行,称为“主线程”,另一个负责其他进程),被称为eventloop
事件轮询就是让 javaScript做到既是单线程,又不会阻塞的核心机制,
是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制
是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制
事件循环event loop
1、首先执行宏任务的同步代码,同步代码在主线程中执行,形成一个执行栈(可理解为函数调用栈)
2、当执行栈中的函数遇到到一些异步执行的API(例如异步Ajax,DOM事件,setTimeout等API),
则会开启对应的线程(Http异步请求线程,事件触发线程和定时器触发线程)进行监控和控制。
则会开启对应的线程(Http异步请求线程,事件触发线程和定时器触发线程)进行监控和控制。
3、当异步任务满足触发条件时,对应的线程则会把该回调添加到事件调用线程的消息队列中,等待主线程读取执行。
4、当主线程上的同步任务完毕,则会读取消息队列中的事件,将消息队列中的事件按顺序推进主线程中执行
5、重复以上过程,这就是事件循环(Event Loop)的过程。
宏任务和微任务定义
宏任务:遇到宏任务会在下一次事件循环前执行。
————在一次新的事件循环的过程中,遇到宏任务时,宏任务将被加入任务队列,但需要等到下一次事件循环才会执行。
常见的宏任务有setTimeout,setImmediate,requestAnimationFrame[freim]
————在一次新的事件循环的过程中,遇到宏任务时,宏任务将被加入任务队列,但需要等到下一次事件循环才会执行。
常见的宏任务有setTimeout,setImmediate,requestAnimationFrame[freim]
微任务:只要有微任务就会继续执行,而不是放到下一个事件循环才执行。常见的微任务有MutationObserver,Promise.then
宏任务和微任务优先问题
宏任务队列有多个,微任务只有一个
微任务执行时机
宏任务-> 微任务event queue->宏任务event queue
宏任务-> 微任务event queue->宏任务event queue
可以理解是在当前宏任务执行结束后立即执行微任务
dom渲染在当前宏任务后,下一个宏任务之前,且在渲染之前
在当前宏任务中执行:所有微任务->dom渲染->下一个宏任务
所以微任务它的响应速度相比setTimeout会更快因为无需等待渲染
在某一个宏执行完成后,就会将在它执行期间产生的所有微都执行完毕(在渲染前)
宏任务和DOM渲染
第一个宏任务会从头到尾将这个任务执行完毕,不会执行其它 浏览器为了能够使得JS内部宏任务与DOM任务能够有序的执行,
会在一个宏任务执行结束后,在下一个宏任务执行开始前,对页面进行重新渲染(task->渲染->task->…)
会在一个宏任务执行结束后,在下一个宏任务执行开始前,对页面进行重新渲染(task->渲染->task->…)
微任务是ES6语法规定,宏任务是浏览器规定
宏任务(macro Task)
script(整体代码)
AJAX
DOM事件
setTimeout/setInterval
setImmediate(Node环境)
setImmediate比setTimeout(fn,0)快
I/O
UI 渲染
requestAnimationFrame
如鼠标点击事件,键盘事件,ajax请求,dom操作等
微任务(micro Task)
Promise的then()、catch()、finally()里面的回调
process.nextTick(Node 环境)优先级较高
也可以在这个网站中查看浏览器执行代码时的实时事件循环和任务队列情况
例子
> ***script(主程序代码)—>process.nextTick—>Promises...——>setTimeout——>setInterval——>setImmediate——>
> I/O——>UI rendering***
事件
dom事件传播机制
事件捕获
从外向里依次查找元素
target.addEventListener('click',fn(){},true)
目标阶段
从当前事件源本身的操作
冒泡阶段
从内到外依次触发相关行为(最常用是冒泡阶段)
target.addEventListener('click',fn(){},false)
或
target.addEventListener('click',fn(){})
或
target.addEventListener('click',fn(){})
停止事件传播
stopPropagation()
停止后面的事件传播-绑定在当前目标上的其他剩余监听器仍将调用
stopImmediatePropagation
立即停止传播-甚至阻止当前节点上的其他监听器被调用。
事件的取消
preventDefault()
取消默认的浏览器操作。例如,单击一个链接或单击表单提交按钮分别会使浏览器导航到新页面和提交表单。
事件代理(事件委托)
概念:把原本要处理的事情委托给父元素
优点:利用的是DOM事件冒泡,提高性能,节省内存占用,减少事件注册
例子,点击ul里面的li,使用冒泡,将事件绑定在ul上,当点击li,触发冒泡绑定的事件,并且使用target来确定目标节点,而不是把每一个事件都绑定在li上
js高级
js作用域有什么用
避免全局污染
提升性能
避免命名冲突
有利于压缩
保存闭包状态
模块化
原型和原型链
原型链
什么是原型链
所有对象都有自己的原型对象(__proto__),指向它的构造函数的prototype。由于原型对象也是对象,所以它也有自己的原型__proto__。因此,就会形成一个“原型链”,(prototype chain)通过一层层向上追溯最后指向null,当试图访问对象的某个属性时,不仅在该对象上找,还会在对象的原型和对象原型的原型上找,直到匹配到一样名字的属性或者达到原型链的末端
什么是原型对象
每个对象拥有一个原型对象,从原型上继承方法和属性,这些属性和方法定义在对象的构造器函数的 prototype 属性上,而非实例本身
绝⼤部分的函数都有⼀个 prototype 属性,所有通过这个构造函数new出来的实例都会共享构造函数的原型对象prototype属性和方法
例如Object是构造函数,而Object.prototype是构造函数的原型对象。构造函数自身的属性和方法无法被共享,而原型对象prototype的属性和方法可以被所有实例对象所共享。[].__proto__===Array.prototype. / a.__proto__ == Object.prototype
Object:Object是一个函数对象,Object的原型就是一个Object对象,它里面存在着一些对象的方法和属性,例如最常见的toString方法。
js中的原型
新建对象:用new Object或者{}建的对象是普通对象,它没有prototype属性,只有__proto__属性,它指向Object.prototype。
Array:Array也是一个函数对象,它的原型就是Array.prototype,它里面存在着一些数组的方法和属性,例如常见的push,pop等方法。
Function:Function也是一个函数对象,但它有点特殊,它的原型就是一个function空函数。
自定义函数:它的原型就是你给它指定的那个东西。如果你不指定,那它的原型就是一个Object.prototype。
prototype和__proto__、construct
每个函数都有一个原型属性prototype指向自身的原型,而由这个函数创建的对象也有一个__proto__属性指向这个原型
fn.__proto__=== Object.prototype
函数有prototype和__proto__,对象有__proto__
fn.__proto__=== Object.prototype
函数有prototype和__proto__,对象有__proto__
js在创建对象(不论是创建普通对象还是函数对象,都有一个叫__proto__的属性,用于指向创建它的函数对象的prototype原型对象)
Symbol 作为构造函数来说并不完整,因为不支持语法 new Symbol(),但其原型上拥有 constructor 属性,即 Symbol.prototype.constructor。
引用类型 constructor 属性值是可以修改的,但是对于基本类型来说是只读的,当然 null 和 undefined 没有 constructor 属性。
__proto__ 是每个实例上都有的属性,prototype 是构造函数的属性,在实例上并不存在,所以这两个并不一样,但 p.__proto__ 和 Parent.prototype 指向同一个对象。
__proto__ 属性在 ES6 时被标准化,但因为性能问题并不推荐使用,推荐使用 Object.getPrototypeOf()。
每个对象拥有一个原型对象,通过 __proto__ 指针指向上一个原型 ,并从中继承方法和属性,同时原型对象也可能拥有原型,这样一层一层,最终指向 null,这就是原型链。
构造对象的__proto__
数组的__proto__
函数的__proto
普通对象的__proto__
继承
1构造函数继承
call和apply一份给自己,是一次性拷贝,对方修改不知道
只能实现部分继承,无法继承父类原型链的属性方法
只能实现部分继承,无法继承父类原型链的属性方法
2.原型链继承
原型中都引用类型属性被所有实例共享,就是其中一个实例对父级引用类型属性的修改,导致所有实例都会被改变
3.组合继承
调用了两次父类构造函数, 性能浪费, 第一次new一个你的构造函数返回给我, 目的是为了成为你的实例好去引用你的原型
4.原型式继承(继承对象)
es5的object.create(对象),原理是浅拷贝,直接抄,不想改
5.寄生式继承(继承对象)
原型链基础上添加属性和方法
6. 寄生组合式继承(常用!)
(寄生+组合)
(寄生+组合)
核心:因为是对父类原型的复制,所以不包含父类的构造函数,
也就不会调用两次父类的构造函数造成浪费。解决了之前组合
继承的两次调用父类构造函数问题
也就不会调用两次父类的构造函数造成浪费。解决了之前组合
继承的两次调用父类构造函数问题
7.es6 class 继承
原理
Class 基于原型链实现,本质是function,作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
Class内不同方式定义函数的区别
this 指向
实例属性this在定义时就已确定指向当前实例(箭头函数)。
类的方法(包括get/set函数)this默认指向当前实例,但一旦遇到解构时(上下文环境变化),this的值可能变化。
继承时的原型链
类继承时,属性是直接继承,而原型链上的方法是一层一层的继承。
类的继承其实就是基于寄生组合继承来实现的
使用借用构造函数(call)来继承父类this声明的属性/方法
通过寄生式封装函数设置父类prototype为子类prototype的原型来继承父类的prototype声明的属性/方法。
通过寄生式封装函数设置父类prototype为子类prototype的原型来继承父类的prototype声明的属性/方法。
继承的时候extends干了什么
extends在实现继承方面,本质上也是原型链继承,该方法实现了两步原型链继承
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。
Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。(把子类构造函数(Child)的原型(__proto__)指向了父类构造函数(Parent),)
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。
Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。(把子类构造函数(Child)的原型(__proto__)指向了父类构造函数(Parent),)
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
Extents 和supper
主动调用super(),否则无法调用父构造函数,无法完成继承。
super() 等同于Parent.prototype.construtor.call(this) //调用父类构造器,并改变指向
默认的构造函数中会主动调用父类构造函数,并默认把当前constructor传递的参数传给了父类。
Es5/Es6的继承除了写法以为有什么区别
ES5 和 ES6 子类 this 生成顺序不同。ES5 的继承先生成了子类实例,再调用父类的构造函数修饰子类实例,ES6 的继承先生成父类实例,再调用子类的构造函数修饰父类实例。这个差别使得 ES6 可以继承内置对象。
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。构造继承关键在于,通过在子类的内部调用父类,即通过使用apply()或call()方法可以在将来新创建的对象上获取父类的成员和方法。
es6继承,子类没有自己的this对象,只能继承父类的this对象,然后对其进行加工而super()就是将父类的this对象继承给子类,没有super(),子类就得不到this对象
ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。构造继承关键在于,通过在子类的内部调用父类,即通过使用apply()或call()方法可以在将来新创建的对象上获取父类的成员和方法。
es6继承,子类没有自己的this对象,只能继承父类的this对象,然后对其进行加工而super()就是将父类的this对象继承给子类,没有super(),子类就得不到this对象
es5继承通过原型和构造函数实现
es6通过class关键字定义类,里面有构造方法,类之间通过extends关键字来实现继承
子类必须在constructor方法中调用super方法,否则报错。因为子类没有自己的this,而是继承父类的this对象,然后对其进行加工,如果不调用super,子类得不到this对象
function声明会提升,但不会初始化赋值。foo进入暂时性死区,类似于let、const
class 声明内部启用严格模式
class所有方法(包括静态方法)都是不可枚举的
class的所有方法都没有原型对象prototype,所以也是没有constructor,不能使用new来调用class里的方法
必须使用new 来调用class,不能直接像构造函数那样执行
class内部无法重写类名,构造函数可以在内部function里重写类名
new操作符做了什么
new 操作符新建了一个空对象,这个对象原型指向构造函数的prototype,执行构造函数后返回这个对象。
如果要读取或修改对象的 [[Prototype]] 属性,建议使用如下方案,但是此时设置对象的 [[Prototype]] 依旧是一个缓慢的操作,如果性能是一个问题,就要避免这种操作
__proto__ 属性在 ES6 时才被标准化,以确保 Web 浏览器的兼容性,但是不推荐使用,除了标准化的原因之外还有性能问题。为了更好的支持,推荐使用 Object.getPrototypeOf()。
__proto__ 属性在 ES6 时才被标准化,以确保 Web 浏览器的兼容性,但是不推荐使用,除了标准化的原因之外还有性能问题。为了更好的支持,推荐使用 Object.getPrototypeOf()。
优化实现 new
继承对象的属性和方法
Object.create(p);
创建一次对象,这个对象的原型对象(__proto__)指向传入的对象
Object.create(Object.prototype) 等价于 new Object() 等价于 {} 方式创建的对象
this
this的指向是在函数被调用的时候确定的
(类比动态作用域:只关心它们从何处调用)
(类比动态作用域:只关心它们从何处调用)
this指向
普通函数指向window
定时器指向window
匿名函数this指向window,var a = function(){}
立即执行this指向window
对象的方法指向对象
构造函数中,this指向实例
在事件中,指触发这个事件的对象
箭头函数没有自己的this,默认指向作用域的this
call/apply/bind(改变this指向)
call
手写fn.call(o,1,2)
apply
手写fn.apply(o,[1,2])
bind
fn.bind(o,1,2) 返回 一个新函数,不调用
执行上下文 vs 作用域
函数调用过程
1、全局上下文的变量对象初始化全局对象
2、函数上下文的变量对象初始化 只包括 Arguments 对象(初始化时)
3、在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值(变量提升过程)
4、在代码执行阶段,会再次修改变量对象的属性值(赋值过程)
2、函数上下文的变量对象初始化 只包括 Arguments 对象(初始化时)
3、在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值(变量提升过程)
4、在代码执行阶段,会再次修改变量对象的属性值(赋值过程)
执行上下文=执行环境
(包含变量对象VO,this,作用域链)
(包含变量对象VO,this,作用域链)
函数每调用一次,都会产生一个新的执行上下文环境。因为不同的调用可能就有不同的参数。因此执行上下文是在执行的时候定义的
执行全局代码时,
1、执行代码之前,首先创建全局上下文环境。包括变量函数提升(活动对象)
2、每次调用函数都又会创建新的执行上下文环境。(然后将当前执行上下文环境压栈,设置为活动状态)
3、当函数调用完成时,这个上下文环境以及其中的数据都会被消除(当然了闭包并不会乖乖就范),最后剩下全局上下文
1、执行代码之前,首先创建全局上下文环境。包括变量函数提升(活动对象)
2、每次调用函数都又会创建新的执行上下文环境。(然后将当前执行上下文环境压栈,设置为活动状态)
3、当函数调用完成时,这个上下文环境以及其中的数据都会被消除(当然了闭包并不会乖乖就范),最后剩下全局上下文
变量对象VO:变量对象即包含变量的对象
(arguments,函数,var变量)
(arguments,函数,var变量)
每个执行上下文都会产生关联的变量对象(VO),这个上下文定义的所有变量和函数都存在在这个VO中。
变量对象(缩写VO)就是保存执行上下文中所有变量和函数的地方。
活动对象AO:函数调用时激活活动对象
变量对象和活动对象是函数激活前后的区别
变量对象和活动对象是函数激活前后的区别
当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),
这时候活动对象上的各种属性才能被访问。
这时候活动对象上的各种属性才能被访问。
[[scope]]属性:指向作用域链
上下文中的代码在执行的时候,会创建变量对象的一个作用域链。决定各级上下文中的代码在访问变量和函数时的顺序
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级执行上下文的变量对象中查找,
一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
this
主要是关键字this的值,这个是由函数运行时决定的,简单来说就是谁调用此函数,this就指向谁,所以this类似于动态作用域。只有等调用时根据调用关系才能确定
作用域
作用域是将一个变量与值关联的封闭上下文。(全局作用域,函数作用域、块级作用域)
变量的作用域:全局变量和局部变量
全局作用域:最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的
局部作用域:局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的
变量的作用域:全局变量和局部变量
全局作用域:最外层函数定义的变量拥有全局作用域,即对任何内部函数来说,都是可以访问的
局部作用域:局部作用域一般只在固定的代码片段内可访问到,而对于函数外部是无法访问的
除了全局作用域,函数会创建自己的作用域。作用域在函数定义时就已经确定了(基于js是词法作用域),
不是在函数调用确定(区别于执行上下文环境,当然this也是上下文环境里的成分)
不是在函数调用确定(区别于执行上下文环境,当然this也是上下文环境里的成分)
两者关系
作用域只是一个“地盘”,其中没有变量。变量是通过作用域对应的执行上下文环境中的变量对象来实现的。
所以作用域是静态观念的,而执行上下文环境是动态上的,两者并不一样。
所以作用域是静态观念的,而执行上下文环境是动态上的,两者并不一样。
let& const
暂时性死区:在代码块内,使用 let 和 const 命令声明变量之前,该变量都是不可用的,语法上被称为暂时性死区。
原因:解析代码的时候,遇到var关键字会声明变量并初始化,为变量预分配栈空间,并初始化为undefined,而遇到const和let关键字会声明变量,但不会初始化变量, 也不会预分配空间,这就是在let、const变量声明前访问变量会报错,暂时性死区的原因
在创建阶段,函数声明存储在环境中,var会被设置为 undefined
let ,const 保持未初始化。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),
但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。
在创建阶段,函数声明存储在环境中,var会被设置为 undefined
let ,const 保持未初始化。所以这就是为什么可以在声明之前访问 var 定义的变量(尽管是 undefined ),
但如果在声明之前访问 let 和 const 定义的变量就会提示引用错误的原因。这就是所谓的变量提升。
词法作用域(静态作用域)与动态作用域
事实上JavaScript并不具有动态作用域,它只有词法作用域,简单明了,但是this机制某种程度上很像动态作用域
词法作用域:是一套引擎寻找变量范围和寻找的规则,它是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里来决定的。
函数运行的所在作用域:是在定义它们的作用域里运行,而不是在执行他们的作用域里运行。
词法作用域的函数中遇到既不是形参也不是函数内部定义的局部变量的自由变量时,去函数定义时的环境中查询
函数运行的所在作用域:是在定义它们的作用域里运行,而不是在执行他们的作用域里运行。
词法作用域的函数中遇到既不是形参也不是函数内部定义的局部变量的自由变量时,去函数定义时的环境中查询
动态作用域:动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。类似this的指向问题
面试题
var i = 1
function b() {
console.log(i)
}
function a() {
var i = 2
b()
}
a() //1
function b() {
console.log(i)
}
function a() {
var i = 2
b()
}
a() //1
这道题考察的是作用域的问题,foo是在全局作用域中定义的,无论在何处调用它的作用域都不会发生变化,都是全局作用域(根据词法作用域来看)。所以会找响应的全局中的变量a,若没找到就会输出undefined。因为js是词法作用域也就是静态作用域,在写代码阶段就作用域就已经确定了,换句话说,是在函数定义的时候确定的,而不是执行的时候,所以a函数是在全局作用域中定义的,虽然在b函数内调用,但是它只能访问到全局的作用域而不能访问到b函数的作用域。
var tt = 'aa';
function test(){
alert(tt);
var tt = 'dd';
alert(tt);
}
test();
function test(){
alert(tt);
var tt = 'dd';
alert(tt);
}
test();
调用对象位于作用域链的前端,局部变量(在函数内部用var声明的变量)、函数参数及Arguments对象都在函数内的作用域中——这意味着它们隐藏了作用域链更上层的任何同名的属性。
即,在以上程序片段中,test函数内部的“var tt='dd'”将会致使“var tt='aa'”在test函数被调用时完全被隐藏。而且,tt是在第一个alert语句之后定义,所以在调用到第一个alert时,tt是还没有被赋值 的。这样说可能会清楚一点,即,在定义test函数时,当定义第一个alert(tt)时,这里会记录tt是作用域链中的一个变量但不会记录它(tt)的 值,函数定义完毕后tt就添加到作用域里,所以第一个alert语句能够找到该作用域里的tt(即,相当于找到一个已经在函数内部声明,但未被赋值的 tt)。
css
盒子模型
border
content
padding
margin
BFC(块级格式化上下文)
什么是BFC
BFC(Block formatting context),即块级格式化上下文,它作为HTML页面上的一个独立渲染区域,
只有区域内元素参与渲染,且不会影响其外部元素。简单来说,可以将 BFC 看做是一个“围城”,
外面的元素进不来,里面的元素出不去(互不干扰)
只有区域内元素参与渲染,且不会影响其外部元素。简单来说,可以将 BFC 看做是一个“围城”,
外面的元素进不来,里面的元素出不去(互不干扰)
形成BFC的条件
触发BFC布局:浮动、定位、display=table.. 、overflow、html
触发BFC布局:浮动、定位、display=table.. 、overflow、html
1、浮动元素,float 除 none 以外的值;
2、定位元素,position(absolute,fixed);
3、display 为以下其中之一的值 inline-block,table-cell,table-caption;
4、overflow 除了 visible 以外的值(hidden,auto,scroll);
5、html 根元素
BFC 一般用来解决以下几个问题
边距重叠问题
消除浮动问题
自适应布局问题
左边float,右边BFC
浮动元素塌陷
渲染规则
BFC内部元素在垂直方向上的margin会发生重叠,但是两个BFC之间就不会发生margin重叠
当BFC元素身边存在浮动时,BFC不会与浮动元素重叠(自适应布局)
它拒绝了float的包裹性独立成一个容器不与其重叠;
计算BFC元素高度时,浮动元素也会参与计算(解决了父元素塌陷的问题)
BFC是独立的容器,不会影响里面的元素,里面的元素也不会影响外面元素
IFC(内联格式化上下文)
IFC的line box(线框)高度由其包含行内元素中最高的实际高度计算而来(不受到竖直方向的padding/margin影响)I
作用
水平居中:当一个块要在环境中水平居中时,设置其为inline-block则会在外层产生IFC,通过text-align则可以使其水平居中。
垂直居中:创建一个IFC,用其中一个元素撑开父元素的高度,然后设置其vertical-align:middle,其他行内元素则可以在此父元素下垂直居中
FFC(弹性盒模型、自适应格式上下文)
display值为flex或者inline-flex
清除浮动
添加新的元素 、应用 clear:both;
BFC:父级div定义 overflow: hidden/auto/scroll
使用伪元素来清除浮动(:after,注意:作用于浮动元素的父亲)
使用双伪元素清除浮动
flex
容器上的属性
align-content
flex-start | flex-end | center | space-between | space-around | stretch;
align-items
flex-start | flex-end | center | baseline | stretch;
justify-content
flex-start | flex-end | center | space-between | space-around;
flex-flow
flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap。
flex-wrap
nowrap | wrap | wrap-reverse;
flex-direction
row | row-reverse | column | column-reverse
单个item属性
order
数值越小排列越前
flex-grow
number /* default 0 */,放大比例,如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍
flex-shrink
f缩小比例,默认为1,即如果空间不足,该项目将缩小。
flex-basis
沿着主轴方向的元素大小,如300px
flex
flex: none | flex属性是flex-grow, flex-shrink 和 flex-basis的简写;它的默认值是0(grow:有剩余空间时不扩展宽度),1(shrink:剩余空间不足时,以一倍速度缩小宽度)和 auto(basis)
常用值
flex: 0 auto 等同于 flex: initial 等同于 flex: 0 1 auto 的简写表达。
它根据元素自身的width 或 height 属性来调节元素大小。
当还剩余一些空闲空间时,它使 flex 元素呈现的是固定大小的样式;
当没有足够的空间时,它允许它收缩到最小。 auto 边距可用于根据主轴来对齐元素。
它根据元素自身的width 或 height 属性来调节元素大小。
当还剩余一些空闲空间时,它使 flex 元素呈现的是固定大小的样式;
当没有足够的空间时,它允许它收缩到最小。 auto 边距可用于根据主轴来对齐元素。
flex: auto 等同于 flex: 1 1 auto ,
它根据元素的 width 或 height 属性调整元素的大小,
但是其非常灵活,以便让它们吸收沿主轴的任何额外空间。
它根据元素的 width 或 height 属性调整元素的大小,
但是其非常灵活,以便让它们吸收沿主轴的任何额外空间。
flex: none 等同于 flex: 0 0 auto 。
它根据 width 和 `height 来调节元素大小,
但是完全不灵活。不会按照容器宽度伸缩
它根据 width 和 `height 来调节元素大小,
但是完全不灵活。不会按照容器宽度伸缩
align-self
align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。
auto | flex-start | flex-end | center | baseline | stretch;
兼容
position
动画
居中
水平居中
text-align:center
absolute+margin-left、transform
flex+justify:center
text-align:center
absolute+margin-left、transform
flex+justify:center
行内元素
text-align:center
块级元素
宽度固定margin/absolute
margin:0 auto
absolute+left:50%+margin-left:-px
absolute+left:calc(50%-2/宽度)
absolute+left0+right0+margin:0 auto
宽度不固定transform/flex
position+left
tansform:translate(-50%,0)
tansform:translate(-50%,0)
flex布局
justify-content: center;
display: flex;
display: flex;
垂直居中
1、line-height
2、table-cell+vertical-align:middle
3、父grid+子margin:auto
4、absoliute+transform、margin-top
5、flex+align-item:center
1、line-height
2、table-cell+vertical-align:middle
3、父grid+子margin:auto
4、absoliute+transform、margin-top
5、flex+align-item:center
行内元素display
单行文本
line-height
多行文本
父:display: table;
子: display: table-cell;
vertical-align: middle;
display:inline-block + vertical-align
.parent3{
display: grid;
}
.child3{
margin: auto;
}
块级元素
宽高固定 position:absoliute
absolute+margin-top
absolute+calc
absolute+top0+bottom0+margin:auto
宽高不定tansform/flex
transform
position+top
position+top
flex
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
css三列布局
float布局:左边左浮动,右边右浮动,中间margin:0 100px;
Position布局: 左边left:0; 右边right:0; 中间left: 100px; right: 100px;
table布局: 父元素 display: table; 左右 width: 100px; 三个元素display: table-cell;
弹性(flex)布局:父元素 display: flex; 左右 width: 100px;
网格(gird)布局:
避免CSS全局污染
scoped 属性
css in js
CSS Modules
使用less,尽量少使用全局对选择器
浏览器
通信
跨域
含义
跨域是指当一个网页(比如客户端)从一个域名的网页去请求另外一个域名的资源时,就会发生跨域。在安全策略下,Web 浏览器通常允许与当前网页具有相同源(协议、域名和端口)的服务器进行交互,而禁止与不同源的服务器进行交互。
跨域产生的原因主要是出于安全考虑。浏览器为了防止恶意攻击者通过脚本窃取用户数据,限制了页面内不同源之间的交互。如果没有这个限制,黑客可以通过编写恶意脚本获取用户在其他站点上的敏感信息并发送到自己的服务器上。
举例来说,假设今天有一个网站 A,其访问地址为 http://www.example.com, 其中包含一些 JavaScript 代码。这段 JavaScript 代码试图像网站 B 的某个 URL 上发起 GET 请求,但网站 B 的 URL 地址为 http://www.test.com, 并且由于安全策略,网站 A 的 JavaScript 脚本无法直接与网站 B 进行通信,这就是跨域问题的产生。
解决方案
jsonp
jsonp 可以解决老版本浏览器跨域访问,但是只能get不能post 原理是创建script标签获取资源
原理:jsonp原理是插入script标签,利用src向服务器发送(跨域)请求,前端将callback挂在window,
等待请求回来调用callback,后台通过callback参数将数据传递到浏览器
等待请求回来调用callback,后台通过callback参数将数据传递到浏览器
服务器代理
node中间件转发请求
nginx反向/正向代理
CORS(跨域资源共享)
最常用的跨域方式,支持post
最常用的跨域方式,支持post
什么是CORS
CORS(跨域资源共享 Cross-origin resource sharing)允许浏览器向跨域服务器发出XMLHttpRequest请求,从而克服跨域问题,它需要浏览器和服务器的同时支持。
CORS(跨域资源共享 Cross-origin resource sharing)允许浏览器向跨域服务器发出XMLHttpRequest请求,从而克服跨域问题,它需要浏览器和服务器的同时支持。
实现cors(跨域资源共享)关键是服务器,只有服务器实现了CORS,就能跨域通信,
服务器端需要设置响应头的Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Origin等字段,指定允许的方法,头部,源等信息
后台需要允许请求跨域,access-contorl-allow-origin 该值要和Origin一致才能生效。
websocket跨域
原理:websocket本身就是支持跨域的,利用webSocket的API,
可以直接new一个socket实例,然后通过open方法内send要传输到后台的值,也可以利用message方法接收后台传来的数据。
后台是通过new WebSocket.Server({port:3000})实例,利用message接收数据,利用send向客户端发送数据。
可以直接new一个socket实例,然后通过open方法内send要传输到后台的值,也可以利用message方法接收后台传来的数据。
后台是通过new WebSocket.Server({port:3000})实例,利用message接收数据,利用send向客户端发送数据。
document.domain+iframe
location.hash+iframe
window.name++iframe
携带数据页面跳转
当a页面中有一个b页面的iframe时,两个页面共享window.name。在b页面拿到数据后赋值给window.name。但是由于不使用postMessage API,数据无法传输给a页面。我们可以将location.href跳转为domain1(a页面所在的域名)下的c页面,由c页面调用a页面的回调方法。
postMessage跨域+iframe
window.postMessage
当一个域名为domain1下的页面A想要向domain2发出ajax请求时,由于同源策略的限制无法直接请求到数据,但是可以在页面A中动态添加一个display设置为none的iframe,该iframe的src为domain2下的html页面B,由页面B来发出ajax请求,因为页面B是domain2下的所以可以成功发出请求。当B页面拿到数据之后再传递给A页面。
websocket
websocket是什么
WebSocket是HTML5中的协议,本质上是基于TCP
WebSocket在建立握手连接时,数据是通过http协议传输的。建立连接后通过TCP进行数据传输的
websocket协议包含两部分:一部分是“握手”,一部分是“数据传输”
ws协议等价于 = 实现了 握手、发送数据、接受数据、关闭数据 这4个协议的tcp协议
WebSocket在建立握手连接时,数据是通过http协议传输的。建立连接后通过TCP进行数据传输的
websocket协议包含两部分:一部分是“握手”,一部分是“数据传输”
ws协议等价于 = 实现了 握手、发送数据、接受数据、关闭数据 这4个协议的tcp协议
和http关系
WebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议,
WebSocket在建立握手连接时,数据是通过http协议传输的。建立连接后通过TCP的系统接口进行传输的
WebSocket在建立握手连接时,数据是通过http协议传输的。建立连接后通过TCP的系统接口进行传输的
和http有什么区别
1、WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息,
而HTTP是单向的;http只能由客户端发起,http不能做到服务器主动向客户端
发送信息,基于这种单向的特点,如果服务器有连续的状态变化,只能轮询,
但是轮询很浪费资源(不停连接),
而HTTP是单向的;http只能由客户端发起,http不能做到服务器主动向客户端
发送信息,基于这种单向的特点,如果服务器有连续的状态变化,只能轮询,
但是轮询很浪费资源(不停连接),
2、WebSocket是需要浏览器和服务器握手进行建立连接的,
而http是浏览器发起向服务器的连接。
而http是浏览器发起向服务器的连接。
3、HTTP2的推送是浏览器和服务端之间的一个推送的概念,比如我在HTTP2中请求 a.html 服务端会自动把css和一些其他资源一并返回,
而websocket的推送是有API的,可以在客户端手动控制的,这就是区别
而websocket的推送是有API的,可以在客户端手动控制的,这就是区别
特点
建立在tcp上,服务器实现比较容易
它是一个有状态协议,客户端和服务器之间连接保持活动状态
与http协议有好的兼容性,握手阶段采用http
数据格式比较轻,性能开销小,通讯高效
可以发文本和二进制
没有同源限制,客户端可以任意服务器通信
协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
new WebSocket('ws://localhost:8000')
new WebSocket('ws://localhost:8000')
作用
聊天室
游戏
交易
使用
前端
this.ws = new WebSocket('ws://localhost:8000')
this.ws.onopen = this.onOpen
this.ws.onmessage = this.onmessage
this.ws.onerror = this.onError
this.ws.send(JSON.stringify({}))
this.ws.onopen = this.onOpen
this.ws.onmessage = this.onmessage
this.ws.onerror = this.onError
this.ws.send(JSON.stringify({}))
后端
const WebSocket = require('ws')
const wss = new WebSocket.Server({ port: 8000 })
Socket.io 支持向下兼容:socket.io封装了websocket,同时包含了其它的连接方式,你在任何浏览器里都可以使用
socket.io来建立异步的连接。socket.io包含了服务端和客户端的库,如果在浏览器中使用了socket.io的js,服务端也
必须同样适用。
socket.io底层是基于engine.io这个库。engine.io为 socket.io 提供跨浏览器/跨设备的双向通信的底层库。
engine.io使用了 Websocket 和 XHR 方式封装了一套 socket 协议。在低版本的浏览器中,不支持Websocket,
为了兼容使用长轮询(polling)替代
socket.io来建立异步的连接。socket.io包含了服务端和客户端的库,如果在浏览器中使用了socket.io的js,服务端也
必须同样适用。
socket.io底层是基于engine.io这个库。engine.io为 socket.io 提供跨浏览器/跨设备的双向通信的底层库。
engine.io使用了 Websocket 和 XHR 方式封装了一套 socket 协议。在低版本的浏览器中,不支持Websocket,
为了兼容使用长轮询(polling)替代
web worker
HTML页面中,如果在执行脚本时,页面的状态是不可响应的,直到脚本执行完成后,页面才变成可响应。
web worker是运行在后台的js,独立于其他脚本,不会影响页面你的性能。
并且通过postMessage将结果回传到主线程。这样在进行复杂操作的时候,就不会阻塞主线程了
并且通过postMessage将结果回传到主线程。这样在进行复杂操作的时候,就不会阻塞主线程了
创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
限制
通信限制,dom限制,脚本限制,文件访问限制、跨域限制
如何实现会话跟踪
cookie
session
url重写
隐藏的input
ip地址
状态码
1xx(消息)
100 继续Continue。客户端应继续其请求
101--切换协议Switching。服务器根据客户端的请求切换协议。只能切换到更高级的协议,
例如,切换到的websocket协议
例如,切换到的websocket协议
2xx(成功)
200 OK客户端请求成功
201 Created已创建。成功请求并创建了新的资源
202 Accepted 服务器已接受请求,但尚未处理
203 No-Authoritative[ɔːˈθɒrətətɪv]非授权信息,服务器已成功处理了请求,但返回的信息可能来自另一来源。
204,No Content请求成功,但没资源返回
3xx(重定向)
300 Multiple Choices多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择
301 Move Permanently永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
302 Move Temporarily临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
303 See Other查看其它地址。与301类似。使用GET和POST请求查看
304 Not Modified,未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
305 Use Proxy使用代理。所请求的资源必须通过代理访问
4xx(请求错误)
400 Bad Request客户端请求有语法错误,不能被服务器所理解
401--Unauthorized请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
402 Payment Required 该状态码是为了将来可能的需求而预留的
403Forbidden 服务器收到请求,但是拒绝提供服务
404Not Found //请求资源不存在
405 Method Not Allowed客户端请求中的方法被禁止
5xx(服务器错误)
500 Internal Server Error //服务器发生不可预期的错误
501 Not Implemented[ˈɪmplɪmentɪd]服务器不支持请求的功能,无法完成请求
502 Bad Gateway作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应
503 Server Unavailable //超载或系统维护,服务器当前不能处理客户端的请求,一段时间后可能恢复正常
504 Gateway Timeout充当网关或代理的服务器,未及时从远端服务器获取请求
505 HTTP Version Not Supported服务器不支持请求的HTTP协议的版本,无法完成处理
存储
cookies/sessionStorage/localStorage
cookies
和localsession区别
生存周期被指定一个maxAge的值,关闭浏览器还在
使用麻烦,需要封装setCookies,getCookies
由服务器的请求来传递每次携带在htttp请求头中,如果使用过多cookies会影响性能
4k
cookie可以跨越同域名下的多个网页,但不能跨越多个域名使用
同一个网站中所有页面共享一套cookie
cookie机制将信息存储于用户硬盘,因此可以作为跨页面全局变量, 这是它最大的一个优点
同一个网站中所有页面共享一套cookie
cookie机制将信息存储于用户硬盘,因此可以作为跨页面全局变量, 这是它最大的一个优点
作用
由于http是一个无状态协议,因此可以借助cookies存储sessionId来唯一标识用户,cookies存在浏览器,session存在服务器
保存用户登陆状态:例如将用户id存在cookies,下次用户访问页面不需要重新登陆,可以设置过期时间
跟踪用户行为,记住用户选择,下次打开,仍然保留该选择
举个登陆栗子
cookies如何防范xss攻击
XSS(跨站脚本攻击)是指攻击者在返回的HTML中嵌入javascript脚本,为了减轻这些攻击,需要在HTTP头部配上,set-cookie:httponly
httponly-这个属性可以防止XSS,它会禁止javascript脚本来访问cookie。
secure - 这个属性告诉浏览器仅在请求为https的时候发送cookie。
response.setHeader("Set-Cookie", "cookiename=httponlyTest;Path=/;Domain=domainvalue;Max-Age=seconds;HTTPOnly");
localStorage
除非清除,否则一直存储
5M
不参与服务器通信
较易使用
同一浏览器的相同域名和端口的不同页面间可以共享相同的 localStorage
sessionStorage
会话存储
5M
不参与服务器通信
较易使用
不同页面间无法共享sessionStorage的信息
不同浏览器无法共享localStorage和sessionStorage中的信息。
同一浏览器的相同域名和端口的不同页面间可以共享相同的 localStorage,但是不同页面间无法共享sessionStorage的信息。
解决:目前广泛采用的是postMessage和iframe相结合的方法。postMessage(data,origin)方法允许来自不同源的脚本采用异步方式进行通信,
可以实现跨文本档、多窗口、跨域消息传递。
同一浏览器的相同域名和端口的不同页面间可以共享相同的 localStorage,但是不同页面间无法共享sessionStorage的信息。
解决:目前广泛采用的是postMessage和iframe相结合的方法。postMessage(data,origin)方法允许来自不同源的脚本采用异步方式进行通信,
可以实现跨文本档、多窗口、跨域消息传递。
缓存
强缓存
200 OK (from cache)
200 OK (from cache)
Cache-Control(较高优先级):请求/响应头,精确控制缓存策略
可以是一个相对时间
no-cache;max-age=<1000000>
no-cache;max-age=<1000000>
Expires(ɪk;spaɪəz):响应头,代表资源过期时间
是一个具体时间(会被客户端影响)
两者区别
expires是http1.0的产物,Cache-contorl是http1.1的产物
cache-control优先级高于expires
expires是一个具体的服务时间,cache-control是一个时间段,控制比较容易
协商缓存
304 Not Modified
304 Not Modified
If-None-Match/Etag(资源标识,优先级较高)
If-None-Match:请求头,浏览器->服务器
Etag:响应头,服务器->浏览器
If-Modified-Since/Last-Moddified(资源最近修改时间)
If-Modified-Since:请求头,浏览器再次请求:浏览器->服务器,比较两个字段是否一致
Last-Moddified:响应头,首次请求 服务器->浏览器。只能精确到1s以内
整个缓存分析过程
(先看强缓存是否符合,然后再协商缓存)
(先看强缓存是否符合,然后再协商缓存)
浏览器第一次加载资源,服务器返回200,浏览器将资源文件从服务器上请求下载下来,并把response header及该请求的返回时间一并缓存
1、下一次加载资源时先比较当前时间和上一次返回200时的时间差,如果没有超过cacha-control设置的max-age,则没有过期,命中强缓存,返回200
2、如果时间过期,则向服务器发送header带有if-None-Match和If-Modified-Since请求
3、服务器收到请求后,优先根据Etag的值判断被请求的文件有没有被修改,Etag值一致则没有修改,命中协商缓存,返回304,如果不一致则有改动,直接返回新的资源文件带上新的Etag值并返回200;若无ETag值就比较If-Modified-Since和被请求文件的最后修改时间,一致就命中协商缓存,返回304;不一致就返回新的last-modified和文件并返回200
存储位置
service worker
自由控制缓存哪些文件,如何匹配缓存、如何读取缓存、并且缓存是持续性的
memory cache
内存缓存,读取高效、缓存时间短
disk cache
硬盘缓存,读取速度慢点、但容量大、存储时效性长
push cache
只会在会话中存在,一旦会话结束就被释放,并且缓存时间也短暂、在chrome浏览器只有5分钟左右
用户行为
地址栏访问,链接跳转是正常行为,将会触发浏览器缓存机制
F5 浏览器设置max-age=0,跳过强缓存,进行协商缓存判断
ctrl+F5刷新,跳过强缓存和协商缓存,直接从服务器拉取资源
请求相关
常用的请求头部(部分):
Accept: 接收类型,表示浏览器支持的MIME类型
(对标服务端返回的Content-Type)
Accept-Encoding:浏览器支持的压缩类型,如gzip等,超出类型不能接收
Content-Type:客户端发送出去实体内容的类型
Cache-Control: 指定请求和响应遵循的缓存机制,如no-cache
If-Modified-Since:对应服务端的Last-Modified,用来匹配看文件是否变动,只能精确到1s之内,http1.0中
Expires:缓存控制,在这个时间内不会请求,直接使用缓存,http1.0,而且是服务端时间
Max-age:代表资源在本地缓存多少秒,有效时间内不会请求,而是使用缓存,http1.1中
If-None-Match:对应服务端的ETag,用来匹配文件内容是否改变(非常精确),http1.1中
Cookie: 有cookie并且同域访问时会自动带上
Connection: 当浏览器与服务器通信时对于长连接如何进行处理,如keep-alive
Host:请求的服务器URL
Origin:最初的请求是从哪里发起的(只会精确到端口),Origin比Referer更尊重隐私
Referer:该页面的来源URL(适用于所有类型的请求,会精确到详细页面地址,csrf拦截常用到这个字段)
User-Agent:用户客户端的一些必要信息,如UA头部等
(对标服务端返回的Content-Type)
Accept-Encoding:浏览器支持的压缩类型,如gzip等,超出类型不能接收
Content-Type:客户端发送出去实体内容的类型
Cache-Control: 指定请求和响应遵循的缓存机制,如no-cache
If-Modified-Since:对应服务端的Last-Modified,用来匹配看文件是否变动,只能精确到1s之内,http1.0中
Expires:缓存控制,在这个时间内不会请求,直接使用缓存,http1.0,而且是服务端时间
Max-age:代表资源在本地缓存多少秒,有效时间内不会请求,而是使用缓存,http1.1中
If-None-Match:对应服务端的ETag,用来匹配文件内容是否改变(非常精确),http1.1中
Cookie: 有cookie并且同域访问时会自动带上
Connection: 当浏览器与服务器通信时对于长连接如何进行处理,如keep-alive
Host:请求的服务器URL
Origin:最初的请求是从哪里发起的(只会精确到端口),Origin比Referer更尊重隐私
Referer:该页面的来源URL(适用于所有类型的请求,会精确到详细页面地址,csrf拦截常用到这个字段)
User-Agent:用户客户端的一些必要信息,如UA头部等
常用的响应头部(部分):
Access-Control-Allow-Headers: 服务器端允许的请求Headers
Access-Control-Allow-Methods: 服务器端允许的请求方法
Access-Control-Allow-Origin: 服务器端允许的请求Origin头部(譬如为*)
Content-Type:服务端返回的实体内容的类型
Date:数据从服务器发送的时间
Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档
Last-Modified:请求资源的最后修改时间
Expires:应该在什么时候认为文档已经过期,从而不再缓存它
Max-age:客户端的本地资源应该缓存多少秒,开启了Cache-Control后有效
ETag:请求变量的实体标签的当前值
Set-Cookie:设置和页面关联的cookie,服务器通过这个头部把cookie传给客户端
Keep-Alive:如果客户端有keep-alive,服务端也会有响应(如timeout=38)
Server:服务器的一些相关信息
Access-Control-Allow-Methods: 服务器端允许的请求方法
Access-Control-Allow-Origin: 服务器端允许的请求Origin头部(譬如为*)
Content-Type:服务端返回的实体内容的类型
Date:数据从服务器发送的时间
Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档
Last-Modified:请求资源的最后修改时间
Expires:应该在什么时候认为文档已经过期,从而不再缓存它
Max-age:客户端的本地资源应该缓存多少秒,开启了Cache-Control后有效
ETag:请求变量的实体标签的当前值
Set-Cookie:设置和页面关联的cookie,服务器通过这个头部把cookie传给客户端
Keep-Alive:如果客户端有keep-alive,服务端也会有响应(如timeout=38)
Server:服务器的一些相关信息
举个栗子
一般来说,请求头部和响应头部是匹配分析的。
譬如,请求头部的Accept要和响应头部的Content-Type匹配,否则会报错
譬如,跨域请求时,请求头部的Origin要匹配响应头部的Access-Control-Allow-Origin,否则会报跨域错误
譬如,在使用缓存时,请求头部的If-Modified-Since、If-None-Match分别和响应头部的Last-Modified、ETag对应
譬如,请求头部的Accept要和响应头部的Content-Type匹配,否则会报错
譬如,跨域请求时,请求头部的Origin要匹配响应头部的Access-Control-Allow-Origin,否则会报跨域错误
譬如,在使用缓存时,请求头部的If-Modified-Since、If-None-Match分别和响应头部的Last-Modified、ETag对应
页面渲染这个过程
浏览器从url到输入到显示页面
浏览器查找当前URL的DNS缓存记录
DNS解析url对应的ip
浏览器输入url后,首先要经过域名解析,因为浏览器不能之间找到服务器,要通过ip地址
DNS服务就是通过域名查找ip,或者逆向查询
先找缓存,没有再找DNS。浏览器缓存->系统缓存->路由器缓存->系统hosts->DNS查询(浏览器发送一个DNS的系统调用,DNS请求达到宽带运营服务器)
DNS查询方式
递归查询
到DNS服务器查询,查不到,DNS发请求到其他服务器直到查到为止
迭代查询(常用)
DNS查询,查不到,返回一个可能查到到DNS 服务器地址给浏览器,浏览器再查询,直到查到为止
拿到域名对应的IP并缓存
宽带运营商服务器缓存DNS
结果返回操作系统并缓存DNS
结果返回浏览器并缓存DNS
根据IP建⽴TCP连接(三次握⼿)
SYN:同步标识,表示TCP连接已初始化。
ACK:确认标识,用于表示对数据包的成功接收。
ack,是对上一个包的序号进行确认的号,ack=seq+1。
Seq:随机数序号
三次握手的目的:为了防止已经失效的连接请求报文段突然又传送到了服务器端,从而产生错误。
连接成功后,浏览器向服务器发起标准Http请求
构建Http请求报文
请求行。格式:Method Request-URL HTTP-Version CRLF,如:GET index.html HTTP/1.1
Method可选项:GET, POST, PUT, DELETE, OPTIONS, HEAD
请求报头
允许客户端向服务器传递请求的附加信息
常见请求报头:Content-Type, Cache-Control,CookieAccept-Encoding,Accept-Language,等
请求正文
当使用POST, PUT等方法时,通常需要客户端向服务器传递数据。
请求行。格式:Method Request-URL HTTP-Version CRLF,如:GET index.html HTTP/1.1
Method可选项:GET, POST, PUT, DELETE, OPTIONS, HEAD
请求报头
允许客户端向服务器传递请求的附加信息
常见请求报头:Content-Type, Cache-Control,CookieAccept-Encoding,Accept-Language,等
请求正文
当使用POST, PUT等方法时,通常需要客户端向服务器传递数据。
服务器收到请求后,经过后端处理返回结果。(前后端分离)
浏览器渲染页面
1)HTML解析,处理HTML标记并构建DOM树。
2)CSS解析,处理CSS标记并构建CSS树。
3)将DOM树和CSSOM合并称render tree(渲染树)。
将每条css规则按照【从右至左】的方式在dom树上进行逆向匹配,然后生成具有样式规则描述的渲染树。
为什么重右到左
因为左到右匹配,发现不符合规则就需要回溯,损失性能,如果右到左,先向上找到父节点直接找到跟元素或者满足条件的匹配规则,则结束这个分支遍历
节约css的匹配复杂度:如果从右到左匹配在第一步就筛选掉大量不符合条件的最右节点,而左向右的性能都浪费在查找上。
4 Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小)
5 Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
5 Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素
reflow与repaint的时机
渲染树转换为网页布局,称为“布局流”(flow),布局显示到页面这个过程叫“绘制”(paint),
都具有阻塞效果,且耗费时间和资源
页面生成之后,脚本和样式操作,都会触发reflow和repaint,两个不一定同时发生,重流必会重绘,重绘不一定重流,
display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发生位置变化。
有些情况下,比如修改了元素的样式,浏览器并不会立刻 reflow 或 repaint 一次,
而是会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。
有些情况下,比如 resize 窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行 reflow。
大多数浏览器会限制重绘发生在子树上面,最小化耗费资源,而不会全局重新生成页面
那些添加了 float或者 position:absolute的元素,因为它们脱离了正常的文档流,构造Render树的时候会针对它们实际的位置进行构造。
都具有阻塞效果,且耗费时间和资源
页面生成之后,脚本和样式操作,都会触发reflow和repaint,两个不一定同时发生,重流必会重绘,重绘不一定重流,
display:none 会触发 reflow,而 visibility:hidden 只会触发 repaint,因为没有发生位置变化。
有些情况下,比如修改了元素的样式,浏览器并不会立刻 reflow 或 repaint 一次,
而是会把这样的操作积攒一批,然后做一次 reflow,这又叫异步 reflow 或增量异步 reflow。
有些情况下,比如 resize 窗口,改变了页面默认的字体等。对于这些操作,浏览器会马上进行 reflow。
大多数浏览器会限制重绘发生在子树上面,最小化耗费资源,而不会全局重新生成页面
那些添加了 float或者 position:absolute的元素,因为它们脱离了正常的文档流,构造Render树的时候会针对它们实际的位置进行构造。
回流reflow
添加或者删除可见的 DOM 元素的时候
元素的位置、尺寸、内容改变
页面第一次渲染的时候
获取 dom 的样式例如:offsetWidth, offsetHeight, clientWidth, clientHeight
列举一些相关的 CSS 样式:width、height、line-height、padding、margin、diaplay、border、top、position、float、font-size、overflow 等
元素的位置、尺寸、内容改变
页面第一次渲染的时候
获取 dom 的样式例如:offsetWidth, offsetHeight, clientWidth, clientHeight
列举一些相关的 CSS 样式:width、height、line-height、padding、margin、diaplay、border、top、position、float、font-size、overflow 等
重绘(repaint):
改变某个元素的背景色、文字颜色、边框颜色等等不影响它周围或内部布局的属性时,
屏幕的一部分要重画,但是元素的几何尺寸没有变。
列举一些相关的 CSS 样式:color、background、background-size、visibility、box-shadow
屏幕的一部分要重画,但是元素的几何尺寸没有变。
列举一些相关的 CSS 样式:color、background、background-size、visibility、box-shadow
优化方案
减少逐项更改样式,最好一次性更改style,或者将样式定义为class并一次性更新
避免循环操作dom,创建一个documentFragment或div,在它上面应用所有DOM操作,最后再把它添加到window.document
避免多次读取offset等属性。无法避免则将它们缓存到变量
将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高
避免循环操作dom,创建一个documentFragment或div,在它上面应用所有DOM操作,最后再把它添加到window.document
避免多次读取offset等属性。无法避免则将它们缓存到变量
将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高
5)开始进行页面绘制(Paint)
遍历render tree节点,将每个节点绘制到屏幕上。
遍历render tree节点,将每个节点绘制到屏幕上。
断开连接,TCP四次挥手
(由浏览器发起,告诉服务器,我响应报文接受完了,我准备关闭了,你也准备吧)
(由浏览器发起,告诉服务器,我响应报文接受完了,我准备关闭了,你也准备吧)
1) 发起方向被动方发送报文,Fin、Ack、Seq,表示已经没有数据传输了。并进入 FIN_WAIT_1 状态。(第一次挥手:由浏览器发起的,发送给服务器,我请求报文发送完了,你准备关闭吧)
2) 被动方发送报文,Ack、Seq,表示同意关闭请求。此时主机发起方进入 FIN_WAIT_2 状态。(第二次挥手:由服务器发起的,告诉浏览器,我请求报文接受完了,我准备关闭了,你也准备吧)
3) 被动方向发起方发送报文段,Fin、Ack、Seq,请求关闭连接。并进入 LAST_ACK 状态。(第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完了,你准备关闭吧)
4) 发起方向被动方发送报文段,Ack、Seq。然后进入等待 TIME_WAIT 状态。被动方收到发起方的报文段以后关闭连接。发起方等待一定时间未收到回复,则正常关闭。(第四次挥手:由浏览器发起,告诉服务器,我响应报文接受完了,我准备关闭了,你也准备吧)
为什么是四次挥手
服务端收到FIN请求后要先应答ACK,
服务器等待请求报文接收完毕 和 响应报文发送完毕在通知浏览器关闭
TCP断开连接为什么比建立连接多一个步骤呢?其实很简单,因为刚开始时候还没有共同财产,但是分手的时候还需要分东西呀
服务端收到FIN请求后要先应答ACK,
服务器等待请求报文接收完毕 和 响应报文发送完毕在通知浏览器关闭
TCP断开连接为什么比建立连接多一个步骤呢?其实很简单,因为刚开始时候还没有共同财产,但是分手的时候还需要分东西呀
css\js\dom之间加载关系
dom构建和渲染与css、js加载位置的影响
在dom树的构建过程中如果遇到JS脚本和外部JS连接,则会停止构建DOM树来执行和下载相应的代码,这会造成阻塞,这就是为什么推荐JS代码应该放在html代码的后面
CSS 不会阻塞 DOM 的解析,但会阻塞 DOM 渲染。有例外,media query声明的CSS是不会阻塞渲染的
JS 阻塞 DOM 解析和渲染,但浏览器会偷看DOM,预先下载相关资源。
CSS加载阻塞JS脚本执行。浏览器遇到 <script>且没有defer或async属性的 标签时,会触发页面渲染,因而如果前面CSS资源尚未加载完毕时,
浏览器会等待它加载完毕在执行脚本。
所以,<script>最好放底部,<link>最好放头部,如果头部同时有<script>与<link>的情况下,最好将<script>放在<link>上面
假如我们将js放在header,js将阻塞解析dom解释和渲染,dom的内容会影响到First Paint,导致First Paint延后。
所以说我们会将js放在后面,以减少First Paint的时间,但是不会减少DOMContentLoaded(DOM解析后)被触发的时间。
JS 阻塞 DOM 解析和渲染,但浏览器会偷看DOM,预先下载相关资源。
CSS加载阻塞JS脚本执行。浏览器遇到 <script>且没有defer或async属性的 标签时,会触发页面渲染,因而如果前面CSS资源尚未加载完毕时,
浏览器会等待它加载完毕在执行脚本。
所以,<script>最好放底部,<link>最好放头部,如果头部同时有<script>与<link>的情况下,最好将<script>放在<link>上面
假如我们将js放在header,js将阻塞解析dom解释和渲染,dom的内容会影响到First Paint,导致First Paint延后。
所以说我们会将js放在后面,以减少First Paint的时间,但是不会减少DOMContentLoaded(DOM解析后)被触发的时间。
js的defer和async
都是异步加载,
但是async阻塞dom渲染
但是async阻塞dom渲染
defer(推迟):异步加载,等待dom渲染完毕再按照顺序执行脚本,执行完毕后会触发DOMContentLoaded
async:异步加载,阻塞dom渲染,加载完毕即刻执行脚本,不按加载顺序顺序执行,谁加载完就立即执行,再继续前面的dom的渲染,
DOMContentLoaded是在html解析完就触发,不等async的js脚本执行完,可能是js执行的前面可能js执行后面
"js高级程序设计"上说当多个js有async标识时,js脚本一定会在load执行前执行,但不一定在DOMContentLoaded事件触发前后执行。
DOMContentLoaded是在html解析完就触发,不等async的js脚本执行完,可能是js执行的前面可能js执行后面
"js高级程序设计"上说当多个js有async标识时,js脚本一定会在load执行前执行,但不一定在DOMContentLoaded事件触发前后执行。
正常情况下(没有async和defer)
阻塞Dom解析和渲染;转而去处理script脚本,
如果脚本是内联的,浏览器会先去执行这段内联的脚本;
如果是外链的,会先加载脚本js文件,后执行js文件。在处理完js脚本后,浏览器便继续解析HTML文档。
如果脚本是内联的,浏览器会先去执行这段内联的脚本;
如果是外链的,会先加载脚本js文件,后执行js文件。在处理完js脚本后,浏览器便继续解析HTML文档。
DOMContentLoaded和window.onload和首屏时间关系
关于首屏时间和css/js加载问题
如果script在底部。css才是影响页面首屏时间的罪魁祸首,如果css太大,页面白屏的时间就长了,也就是render tree被推迟了
所以如果css太大就需要把非首屏css也放到底部,如果script标签的位置不在首屏范围内,不影响首屏时间
js只不过是影响用户和js相关的那些操作的时间而已。如果面试的话,就是这么回答。
所以如果css太大就需要把非首屏css也放到底部,如果script标签的位置不在首屏范围内,不影响首屏时间
js只不过是影响用户和js相关的那些操作的时间而已。如果面试的话,就是这么回答。
关于执行顺序
解析HTML结构。
加载外部脚本和样式表文件。
解析并执行脚本代码。//js之类的
DOM树构建完成。//DOMContentLoaded
加载图片等外部文件。
页面加载完毕。//load
加载外部脚本和样式表文件。
解析并执行脚本代码。//js之类的
DOM树构建完成。//DOMContentLoaded
加载图片等外部文件。
页面加载完毕。//load
首屏加载结论:以下首屏是指(首次绘制dom在页面上的时间,从白屏到显示一部分元素。也就是第一次Painting)
首屏时间和DomContentLoad事件没有必然的先后关系(首屏=renderTree第一次绘制时间,DomContentLoad表示dom构建完毕)
CSS尽早加载是减少首屏时间的最关键
js的下载和执行会阻塞Dom树的构建
script标签放在body底部,做与不做async或者defer处理,都不会影响首屏时间,但影响DomContentLoad和load的时间,进而影响依赖他们的代码的执行的开始时间。
---------------------------------------
如果script标签的位置不在首屏范围内,不影响首屏时间
但从性能最优的角度考虑,即使在body底部的script标签也会拖慢首屏出来的速度,因为浏览器在最一开始就会请求它对应的js文件,
这就占用了有限的TCP链接数、带宽甚至运行它所需要的CPU。这也是为什么script标签会有async或defer属性的原因之一。
首屏时间和DomContentLoad事件没有必然的先后关系(首屏=renderTree第一次绘制时间,DomContentLoad表示dom构建完毕)
CSS尽早加载是减少首屏时间的最关键
js的下载和执行会阻塞Dom树的构建
script标签放在body底部,做与不做async或者defer处理,都不会影响首屏时间,但影响DomContentLoad和load的时间,进而影响依赖他们的代码的执行的开始时间。
---------------------------------------
如果script标签的位置不在首屏范围内,不影响首屏时间
但从性能最优的角度考虑,即使在body底部的script标签也会拖慢首屏出来的速度,因为浏览器在最一开始就会请求它对应的js文件,
这就占用了有限的TCP链接数、带宽甚至运行它所需要的CPU。这也是为什么script标签会有async或defer属性的原因之一。
dom解析中没有js
dom解析中遇到css
dom解析中遇到css,js
DOMContentLoaded
DOMContentLoaded 事件触发时,仅当DOM构建完成,不包括样式表,图片(譬如如果有async加载的脚本就不一定完成)。
所以DOMContentLoaded之后用户就可以进行交互(这时候已经html解析完),绑定事件可以在DOMContentLoaded后绑定,因为这时候已经能够获取到DOM节点。
所以DOMContentLoaded之后用户就可以进行交互(这时候已经html解析完),绑定事件可以在DOMContentLoaded后绑定,因为这时候已经能够获取到DOM节点。
load 事件触发时
load 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了(包括async)。
嵌入的JS注意点
1、放在底部,只会阻塞所有内容的呈现,但是不会则阻塞资源的下载(js放在</body>前)
2、如果嵌入JS放在head中,则嵌入JS放在css头部
3、使用defer
4、不要在嵌入的JS中调用执行时间较长的函数,如果一定要用,可以用setTimeout来调用
5、动态脚本元素使用js动态创建HTML的几乎全部文档内容
2、如果嵌入JS放在head中,则嵌入JS放在css头部
3、使用defer
4、不要在嵌入的JS中调用执行时间较长的函数,如果一定要用,可以用setTimeout来调用
5、动态脚本元素使用js动态创建HTML的几乎全部文档内容
资源预加载
注意:在现代浏览器中,为减缓渲染被阻塞的情况,现代浏览器都使用了猜测预加载技术。当解析被阻塞时,浏览器会有一个轻量级HTML(或CSS)扫描器(scanner)继续在文档中扫描,查找那些将来可能能够用到的resource资源文件url,在渲染器使用它们之前将其下载下来。
以上结论:
CSS加载阻塞JS脚本执行,JS阻塞Dom构建和dom渲染,CSS 不阻塞DOM解析,但阻塞DOM渲染 ,media query声明的例外
html内容从上到下解析,浏览器遇到body标签开始显示内容。CSS 不会阻塞 DOM 的解析,JS 会阻止DOM的解析。
当文档加载过程中遇到JS文件,HTML文档会挂起渲染过程,不仅要等到文档中JS文件加载完毕还要等待解析执行完毕,才会继续HTML的渲染过程。
现代浏览器都使用了预加载器,在js挂起DOM解析时,会继续解析后面的html,寻找需要下载的资源。预加载器下载这些资源,以减少JS阻塞带来的影响
defer和async异步加载,defer等dom渲染完延时按顺序执行之后DOMContentLoaded,
async谁加载完就马上执行,因为不确定Dom解析好没有,所以DOMContentLoaded可能在之前或者之后。
async谁加载完就马上执行,因为不确定Dom解析好没有,所以DOMContentLoaded可能在之前或者之后。
onload是资源加载完毕后执行,包括图片css,js资源(包括async的js)
DOMContentLoaded等html构建成dom tree执行,如果文档中包含js和css,并css在js前加载,就要等待css加载完和js解析完后执行,
总的来说,当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件;
如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等 CSSOM 构建完成才能执行。
总的来说,当文档中没有脚本时,浏览器解析完文档便能触发 DOMContentLoaded 事件;
如果文档中包含脚本,则脚本会阻塞文档的解析,而脚本需要等 CSSOM 构建完成才能执行。
在任何情况下,DOMContentLoaded 的触发不需要等待图片等其他资源加载完成。
首屏
和文档中css的资源大小有关。css影响render tree生成
文档中的js也可能影响,取决于加载的位置,如果script标签的位置不在首屏范围内,不影响首屏时间
页面渲染优化
http请求数减少,如:雪碧图、合并CSS/JS文件、缓存资源等(针对http1.1)
http请求资源体积减少,如:启用gzip压缩、图片压缩、减少cookie、按需加载等
css放在head中。由于同时具有 DOM 和 CSSOM 才能构建渲染树,所以HTML 和 CSS 都是阻塞渲染的资源,所以尽量精简CSS也是优化方式之一。
js放在body底部,减少白屏时间。因为js会阻止浏览器解析。
减少回流和重绘制,比如不要一条一条修改DOM样式、使用documentFragment操作DOM等。
浏览器内核
1、IE浏览器内核:Trident内核,也被称为IE内核;
2、Chrome浏览器内核:
Chromium内核 → Webkit内核 → Blink内核;
Chromium内核 → Webkit内核 → Blink内核;
4、Safari浏览器内核:Webkit内核;
3、Firefox浏览器内核:Gecko内核,也被称Firefox内核;
5、Opera浏览器内核:最初是自主研发的Presto内核,后跟随谷歌,从Webkit到Blink内核;
6、360浏览器、猎豹浏览器内核:IE+Chrome双内核;
7、搜狗、遨游、QQ浏览器内核:Trident(兼容模式)+Webkit(高速模式);
8、百度浏览器、世界之窗内核:IE内核;
浏览器运行机制
(JS运行机制、渲染进程)
(JS运行机制、渲染进程)
浏览器的进程
Browser进程(负责地址栏、书签栏、前进后退、网络请求、文件访问等)
渲染进程(负责一个Tab内所有和网页渲染有关的所有事情,是最核心的进程,渲染进程,js单线程运行的地方)
核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,
排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中
核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,
排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中
GPU进程(负责GPU相关的任务,用于3D绘制等等)
Plugin进程(负责Chrome插件相关的任务)
网络进程(主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。)
开启一个页面至少有多少个进程
四个:GPU进程,渲染进程,网络进程,Browser进程
虽然多进程模型提升了浏览器的稳定性、流畅性和安全性,但同样不可避免地带来了一些问题:更高的资源占用。因为每个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。更复杂的体系架构。浏览器各模块之间耦合性高、扩展性差等问题,会导致现在的架构已经很难适应新的需求了。
进程和线程的关系
进程是资源分配的基本单位,线程是CPU独立运行和独立调度的基本单位
(一个进程包含多个线程,可以理解为一个进程中执行的代码片段)
(一个进程包含多个线程,可以理解为一个进程中执行的代码片段)
一个进程下面的线程是可以去通信的,共享资源
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,
而线程共享本进程的地址空间和资源。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉。
而线程共享本进程的地址空间和资源。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉。
那么Chrome中JavaScript运行的位置在哪里呢?
渲染进程(Renderer Process)中的主线程(JS引擎线程)
JS单线程指的是浏览器中负责解释和执行JS代码的只有一个线程,即为 JS引擎线程
JavaScript是采用异步式I/O+事件循环模式的
浏览器的渲染进程是多线程的,chrome使用多个渲染引擎实例,每个Tab页一个,即每个Tab都是一个独立进程。
渲染进程中的线程
每一个tab页面就是一个浏览器内核进程(渲染进程)
每一个tab页面就是一个浏览器内核进程(渲染进程)
GUI线程/渲染线程
负责html/css构建render tree页面渲染、reflow、repaint
负责html/css构建render tree页面渲染、reflow、repaint
负责渲染浏览器界面,解析HTML,CSS,构建DOM树和Render Object树,布局和绘制等。
当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),
GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
主线程
JS引擎线程(V8引擎)所在的线程
负责处理javascript脚本
JS引擎线程(V8引擎)所在的线程
负责处理javascript脚本
JS为处理页面中用户的交互,负责处理Javascript脚本程序
这个v8引擎主要由两个部分组成,Memory Heap 和 Call Stack,即内存堆和调用栈。(只负责取消息,不负责生产消息)
内存堆:进行内存分配。如变量赋值。
调用栈:这是代码在栈帧中执行的地方。调用栈中顺序执行主线程的代码,当调用栈中为空时,js引擎会去消息队列取消息,取到后就执行。
内存堆:进行内存分配。如变量赋值。
调用栈:这是代码在栈帧中执行的地方。调用栈中顺序执行主线程的代码,当调用栈中为空时,js引擎会去消息队列取消息,取到后就执行。
GUI渲染线程与JS引擎线程互斥
是由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JavaScript线程和UI线程同时运行),
那么渲染线程前后获得的元素数据就可能不一致。
当JavaScript引擎执行时GUI线程会被挂起,
GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
由于GUI渲染线程与JS执行线程是互斥的关系,
因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
那么渲染线程前后获得的元素数据就可能不一致。
当JavaScript引擎执行时GUI线程会被挂起,
GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。
由于GUI渲染线程与JS执行线程是互斥的关系,
因此如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。
JS引擎线程和其他线程
当遇到计时器、DOM事件监听或者是网络请求的任务时,JS引擎会将它们直接交给WebAPI,
也就是浏览器提供的相应线程(如定时器线程为setTimeout计时、异步http请求线程处理网络请求)去处理,
而JS引擎线程继续后面的其他任务,这样便实现了 异步非阻塞。
定时器触发线程也只是为 setTimeout(..., 1000) 定时而已,时间一到,还会把它对应的回调函数(callback)交给 消息队列 去维护,
JS引擎线程会在适当的时候去消息队列取出消息并执行。这里,JavaScript 通过 事件循环 event loop 的机制来解决这个问题
也就是浏览器提供的相应线程(如定时器线程为setTimeout计时、异步http请求线程处理网络请求)去处理,
而JS引擎线程继续后面的其他任务,这样便实现了 异步非阻塞。
定时器触发线程也只是为 setTimeout(..., 1000) 定时而已,时间一到,还会把它对应的回调函数(callback)交给 消息队列 去维护,
JS引擎线程会在适当的时候去消息队列取出消息并执行。这里,JavaScript 通过 事件循环 event loop 的机制来解决这个问题
事件触发线程
负责管理事件循环和任务队列(宏任务)维护
只要异步任务有了运行结果,就在任务队列之中放置一个事件
负责管理事件循环和任务队列(宏任务)维护
只要异步任务有了运行结果,就在任务队列之中放置一个事件
事件循环 机制和 消息队列 的维护是由事件触发线程控制的。事件触发线程 同样是浏览器渲染引擎提供的,
它会维护一个 消息队列。然后由事件触发线程将异步对应的回调函数加入到消息队列中,等待JS引擎的处理。
它会维护一个 消息队列。然后由事件触发线程将异步对应的回调函数加入到消息队列中,等待JS引擎的处理。
这些事件可以是当前执行的代码块如定时任务、也可来自浏览器内核的其他线程如 鼠标点击、AJAX异步请求等,
但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
事件触发是异步的,但它们的处理程序在调用堆栈上同步执行。
事件触发线程与宏任务微任务之间的关系
宏任务在事件触发线程维护
macrotask中的事件都是放在一个事件队列中的
macrotask中的事件都是放在一个事件队列中的
微任务在队列在JS引擎线程中维护
microtask中的所有微任务都是添加到微任务队列中,等待当前macrotask执行完毕后执行
microtask中的所有微任务都是添加到微任务队列中,等待当前macrotask执行完毕后执行
定时器线程
setInterval与setTimeout所在线程
负责计时
setInterval与setTimeout所在线程
负责计时
浏览器定时计数器并不是由JS引擎计数的, 因为JS引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确,
因此通过单独线程来计时并触发定时是更为合理的方案。
因此通过单独线程来计时并触发定时是更为合理的方案。
为什么有时候setTimeout推入的事件不能准时执行?因为可能在它推入到消息队列时,主线程还不空闲,正在执行其它代码,
setTimeout中低于4ms的时间间隔算为4ms
setTimeout而不是setInterval
setInterval有一些比较致命的问题就是:
累计效应(上面提到的),如果setInterval代码在(setInterval)再次添加到队列之前还没有完成执行,
就会导致定时器代码连续运行好几次,而之间没有间隔。
就算正常间隔执行,多个setInterval的代码执行时间可能会比预期小(因为代码执行需要一定时间)
目前一般认为的最佳方案是:用setTimeout模拟setInterval,或者特殊场合直接用requestAnimationFrame
补充:JS高程中有提到,JS引擎会对setInterval进行优化,如果当前事件队列中有setInterval的回调,不会重复添加。
累计效应(上面提到的),如果setInterval代码在(setInterval)再次添加到队列之前还没有完成执行,
就会导致定时器代码连续运行好几次,而之间没有间隔。
就算正常间隔执行,多个setInterval的代码执行时间可能会比预期小(因为代码执行需要一定时间)
目前一般认为的最佳方案是:用setTimeout模拟setInterval,或者特殊场合直接用requestAnimationFrame
补充:JS高程中有提到,JS引擎会对setInterval进行优化,如果当前事件队列中有setInterval的回调,不会重复添加。
异步http请求线程
每建立一个请求建立一个线程
每建立一个请求建立一个线程
在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
各线程之间的关系
事件循环机制
JS引擎解析过程
JS的解释阶段
javascript属于动态语言,所以它无需提前编译,而是由解释器实时运行,在运行时编译,所以在运行时确认数据类型和调用关系
引擎对JS的编译过程
1.创建一个[作用域链]。2.创建变量,函数和参数。3.确定this的值。
1.创建一个[作用域链]。2.创建变量,函数和参数。3.确定this的值。
1. 读取代码,进行词法分析(Lexical analysis),然后将代码分解成词元(token)
2. 对词元进行语法分析(parsing),然后将代码整理成语法树(syntax tree)
3. 使用翻译器(translator),将代码转为字节码(bytecode)
4. 使用字节码解释器(bytecode interpreter),将字节码转为机器码
最终计算机执行的就是机器码。
为了提高运行速度,现代浏览器一般采用即时编译(JIT-Just In Time compiler)
即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache)
这样整个程序的运行速度能得到显著提升。
而且,不同浏览器策略可能还不同,有的浏览器就省略了字节码的翻译步骤,直接转为机器码(如chrome的v8)
总结起来可以认为是: 核心的JIT编译器将源码编译成机器码运行
读取代码->词法分析->语法分析->字节码(chrome v8忽略这步)->机器码->运行
为了提高运行速度,现代浏览器一般采用即时编译(JIT-Just In Time compiler)
即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存(inline cache)
这样整个程序的运行速度能得到显著提升。
而且,不同浏览器策略可能还不同,有的浏览器就省略了字节码的翻译步骤,直接转为机器码(如chrome的v8)
总结起来可以认为是: 核心的JIT编译器将源码编译成机器码运行
读取代码->词法分析->语法分析->字节码(chrome v8忽略这步)->机器码->运行
JS的预处理阶段
譬如变量提升
一般包括函数提升和变量提升
分号补全等
JS的执行阶段
赋值,执行代码
赋值,执行代码
执行上下文,执行堆栈概念
(如全局上下文,当前活动上下文)
(如全局上下文,当前活动上下文)
执行上下文简单解释
JS有执行上下文
浏览器首次载入脚本,它将创建全局执行上下文,并压入执行栈栈顶(不可被弹出)
然后每进入其它作用域就创建对应的执行上下文并把它压入执行栈的顶部
一旦对应的上下文执行完毕,就从栈顶弹出,并将上下文控制权交给当前的栈。
这样依次执行(最终都会回到全局执行上下文)
JS有执行上下文
浏览器首次载入脚本,它将创建全局执行上下文,并压入执行栈栈顶(不可被弹出)
然后每进入其它作用域就创建对应的执行上下文并把它压入执行栈的顶部
一旦对应的上下文执行完毕,就从栈顶弹出,并将上下文控制权交给当前的栈。
这样依次执行(最终都会回到全局执行上下文)
每一个执行上下文,都有三个重要属性:
变量对象(Variable object,VO)
作用域链(Scope chain)
this
VO(变量对象)和AO(活动对象)
VO中会存放一些变量信息
(如声明的变量,函数,arguments参数等等)
VO中会存放一些变量信息
(如声明的变量,函数,arguments参数等等)
VO是执行上下文的属性(抽象概念),但是只有全局上下文的变量对象允许通过VO的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象)
AO(activation object),当函数被调用者激活,AO就被创建了,局部变量、内部函数、形式参数储存在给定函数的激活对象中。
作用域链
它是执行上下文中的一个属性,原理和原型链很相似,作用很重要。
在函数上下文中,查找一个变量foo
如果函数的VO中找到了,就直接使用
否则去它的父级作用域链中(__parent__)找
如果父级中没找到,继续往上找
直到全局上下文中也没找到就报错
如果函数的VO中找到了,就直接使用
否则去它的父级作用域链中(__parent__)找
如果父级中没找到,继续往上找
直到全局上下文中也没找到就报错
this机制等
this是没有一个类似搜寻变量的过程
当代码中使用了this,这个 this的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻
this的值只取决中进入上下文时的情况
当代码中使用了this,这个 this的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻
this的值只取决中进入上下文时的情况
就要明白了上面this的介绍,上述例子很好理解
var baz = 200;
var bar = {
baz: 100,
foo: function() {
console.log(this.baz);
}
};
var foo = bar.foo;
// 进入环境:global
foo(); // 200,严格模式中会报错,Cannot read property 'baz' of undefined
// 进入环境:global bar
bar.foo(); // 100
var baz = 200;
var bar = {
baz: 100,
foo: function() {
console.log(this.baz);
}
};
var foo = bar.foo;
// 进入环境:global
foo(); // 200,严格模式中会报错,Cannot read property 'baz' of undefined
// 进入环境:global bar
bar.foo(); // 100
this取决于调用函数的方式。当调用括号的左边不是引用类型而是其它类型,这个值自动设置为null,结果为全局对象
window
navgator
navigator.appName:浏览器名称;
navigator.appVersion:浏览器版本;
navigator.language:浏览器设置的语言;
navigator.platform:操作系统类型;
navigator.userAgent -- 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
navigator.cookieEnabled -- 返回浏览器是否支持(启用)cookie
screen
screen.width:屏幕宽度,以像素为单位;
screen.height:屏幕高度,以像素为单位;
screen.colorDepth:返回颜色位数,如8、16、24
location
location.protocol; //http
location.host; // www.example.com
location.port; // 8080
location.pathname; // /path/index.html
location.search; // ?a=1&b=2
location.hash; // TOP
Dom
添加/删除节点
document.createElement(element) 创建 HTML 元素
document.removeChild(element) 删除 HTML 元素
document.appendChild(element) 添加 HTML 元素
document.replaceChild(element) 替换 HTML 元素
document.write(text) 写入 HTML 输出流
查找 HTML 元素
document.getElementById(id) 通过元素 id 来查找元素
document.getElementsByTagName(name) 通过标签名来查找元素
document.getElementsByClassName(name) 通过类名来查找元素
document.querySelectorAll("p.intro") CSS 选择器查找 HTML 元素
parentNode
childNodes[nodenumber]
firstChild
lastChild
nextSibling
previousSibling
改变 HTML 元素
element.innerHTML = new html content 改变元素的 inner HTML
element.attribute = new value 改变 HTML 元素的属性值
element.setAttribute(attribute, value) 改变 HTML 元素的属性值
element.style.property = new style 改变 HTML 元素的样式
添加事件处理程序
document.getElementById(id).onclick = function(){code} 向 onclick 事件添加事件处理程序
history
history.go() -- 前进或后退指定的页面数 history.go(num);
history.back() -- 后退一页
history.forward() -- 前进一页
replaceState会替换当前的history中的记录,并且不刷新浏览器
history.replaceState({}, "title", "/test.html")
history.replaceState({}, "title", "/test.html")
pushState只会在当前history中添加一条记录,并不会刷新浏览器
history.pushState({}, "title", "/test.html")
history.pushState({}, "title", "/test.html")
Promise
手写promise
Promise().then().then....catch() 多任务串行执行.
Promise.all([p1,p2,...]) 多任务并行执行
Promise.race([p1,p2,...]) 多任务赛跑.
node
nodejs事件循环
在浏览器或者nodejs环境中,运行时对js脚本的调度方式就叫做事件循环
浏览器和 Node 事件循环的区别
其中一个主要的区别在于浏览器的event loop 和nodejs的event loop 在处理异步事件的顺序是不同的,
nodejs 中有 micro event;其中 Promise 属于 micro event 该异步事件的处理顺序就和浏览器不同.
nodejs V11.0 以上 这两者之间的顺序 就相同了
代码在Node 10以前版本的执行结果
执行完一个阶段的所有任务
执行完nextTick队列里面的内容
然后执行完微任务队列的内容
node11及浏览器的执行结果一致
执行一只task(宏任务)
执行完micro-task队列 (微任务)
nodejs 中有 micro event;其中 Promise 属于 micro event 该异步事件的处理顺序就和浏览器不同.
nodejs V11.0 以上 这两者之间的顺序 就相同了
代码在Node 10以前版本的执行结果
执行完一个阶段的所有任务
执行完nextTick队列里面的内容
然后执行完微任务队列的内容
node11及浏览器的执行结果一致
执行一只task(宏任务)
执行完micro-task队列 (微任务)
6个阶段
timers(定时器) : 此阶段执行那些由 `setTimeout()` 和 `setInterval()` 调度的回调函数.
pending callbacks(I/O回调) : 执行延迟到下一个循环迭代的 I/O 回调。此阶段会执行几乎所有的回调函数, 除了 close callbacks(关闭回调) 和 那些由 timers 与 setImmediate()调度的回调
.setImmediate 约等于 setTimeout(cb,0)
.setImmediate 约等于 setTimeout(cb,0)
idle(空转), prepare : 此阶段只在内部使用
poll(轮询): 检索新的I/O事件; 在恰当的时候Node会阻塞在这个阶段
(其中poll阶段除了执行当前阶段的队列里的所有callback之外,
poll还负责检测是否有timer的callback,如timer到达时间,并且timer的callback还未执行,
那么就循环至开头执行timer的callback
(其中poll阶段除了执行当前阶段的队列里的所有callback之外,
poll还负责检测是否有timer的callback,如timer到达时间,并且timer的callback还未执行,
那么就循环至开头执行timer的callback
check(检查) : `setImmediate()` 设置的回调会在此阶段被调用
close callbacks(关闭事件的回调): 诸如 `socket.on('close', ...)` 此类的回调在此阶段被调用
其中promise.then,porcess.nextTick 在每次事件阶段切换时被调用
阶段执行规则
当事件循环进入到某一个阶段,其会执行该阶段的任何操作。直到这个阶段的任务队列为空,或者是回调函数的调用次数达到上限。此时事件循环将会到下一个阶段。
当我们在事件循环的某个阶段执行某个回调时候,该回调可能会生成更多的回调。这些回调都是被添加到对应阶段的队列中。因此,长时间运行的回调可以允许轮询阶段的运行时间远远超过计时器的阀值
当我们在事件循环的某个阶段执行某个回调时候,该回调可能会生成更多的回调。这些回调都是被添加到对应阶段的队列中。因此,长时间运行的回调可以允许轮询阶段的运行时间远远超过计时器的阀值
poll轮询阶段
功能
计算应该阻塞和轮询 I/O 的时间。
处理轮询队列里的事件。
进入轮询阶段
且代码未设定timer
如果轮询队列不是空的
那么事件循环将循环访问回调队列并同步执行它们,直到清空队列,或者达到了最大限制
进入轮询阶段且一旦队列为空
在轮询阶段的执行过程中,一旦轮询队列为空,事件循环将检查是否有到期的定时器settimeout或setintval。
如果一个或多个定时器已准备就绪,则事件循环将绕回timer阶段以执行这些定时器的回调。
如果一个或多个定时器已准备就绪,则事件循环将绕回timer阶段以执行这些定时器的回调。
如果轮询队列是空的(这时可能会阻塞)
有没有setImmediate() 调度
有:那么事件循环将结束轮询阶段,并到check阶段以执行那些被调度的代码。
没有:event loop将阻塞在该阶段等待callbacks加入poll queue,一旦到达就立即执行
设定了timer且queue为空
有到期的timer则进入timer阶段执行到期的timer callback
setImmediate() 与 setTimeout()执行顺序
setImmediate()处于check阶段,设计用于在当前轮询阶段后执行脚本
setTimeout() 安排在经过最小阀值后执行的脚本
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
//setImmediate和setTimeout执行顺序不确定
受程序启动时间限制。设置的 delay 是 0,但其实是1,就经过了check阶段,直接进入下一个timer阶段,所以有时setImmediate先执行,有时setTimeout,
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
//setImmediate和setTimeout执行顺序不确定
受程序启动时间限制。设置的 delay 是 0,但其实是1,就经过了check阶段,直接进入下一个timer阶段,所以有时setImmediate先执行,有时setTimeout,
执行定时器的顺序将根据调用他们的上下文有所不同,
如果是从主模块中调用两者,那么时间将会受到进程性能的限制;
如果这两者运行在非I/O周期内,这两者的顺序无法保证,
如果是在I/O周期内的话,setImmediate()优先于setTimeout()
如果是从主模块中调用两者,那么时间将会受到进程性能的限制;
如果这两者运行在非I/O周期内,这两者的顺序无法保证,
如果是在I/O周期内的话,setImmediate()优先于setTimeout()
nodeJS中. settimeout(fn,1) === setTimeout(fn,0)
微任务优先级:porcess.nextTick() > Promise.then()
node运行原理
Node.js 在主线程里维护了一个事件队列,当接到请求后,就将该请求作为一个事件放入这个队列中,然后继续接收其他请求。当主线程空闲时(没有请求接入时),就开始循环事件队列,检查队列中是否有要处理的事件,这时要分两种情况:如果是非 I/O 任务,就亲自处理,并通过回调函数返回到上层调用;如果是 I/O 任务,就从 线程池 中拿出一个线程来处理这个事件,并指定回调函数,然后继续循环队列中的其他事件。
当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop),其运行原理如下图所示
当线程中的 I/O 任务完成以后,就执行指定的回调函数,并把这个完成的事件放到事件队列的尾部,等待事件循环,当主线程再次循环到该事件时,就直接处理并返回给上层调用。 这个过程就叫 事件循环 (Event Loop),其运行原理如下图所示
应用层: 即 JavaScript 交互层,常见的就是 Node.js 的模块,比如 http,fs
V8引擎层: 即利用 V8 引擎来解析JavaScript 语法,进而和下层 API 交互
NodeAPI层: 为上层模块提供系统调用,一般是由 C 语言来实现,和操作系统进行交互 。
LIBUV层: 是跨平台的底层封装,实现了 事件循环、文件操作等,是 Node.js 实现异步的核心
思考
模块化实现源码 lib/internal/modules/cjs/loader.js
模块化实现源码 lib/internal/modules/cjs/loader.js
为什么在模块中有全局的require、module.exports、exports、__dirname、__filename等关键字,它们是从哪来的?
在模块内部,拥有require、module、exports等全局变量
// 原理是通过compiledWrapper.call执行函数,把这些内容传入到模块内部
// 原理是通过compiledWrapper.call执行函数,把这些内容传入到模块内部
// require方法挂载到Module原型链上
Module.prototype.require = function(id) {
return Module._load(id, this, /* isMain */ false);
};
Module.prototype.require = function(id) {
return Module._load(id, this, /* isMain */ false);
};
可以看到require方法是绑定在Module类的原型链方法,说明只有获取到当前实例module才能调用require。而每个模块都可以拿到自己的当前实例module变量,它是如何把实例module注入到模块中的呢?答案是使用沙箱环境,以闭包函数的方式传入当前module。
node模块系统路径加载多种多样,有内置的、有从相对位置读取、有从绝对位置读取,加载详细规则可以看NodeJS官方文档 modules_file_modules (opens new window)。想了解具体实现原理可以看下Module._resolveFilename方法源码,该方法主要确定模块加载的绝对路径。
为什么一定要使用module.exports或者exports导出模块信息?
node源码上导出的是exports/module.exports内容
可以看到Module._load方法通过new Module()来创建一个空的module实例,然后通过原型方法module.load真正的去读取模块内容。注意return导出的是module.exports,这就解释了CommonJS规范中要求的最终导出的内容是module.exports(第二个问题答案)。至于exports是module.exports的简写,即exports = module.exports,
module.exports和exports的区别,它们之间的关系是什么?
exports和module.exports的关系即exports = module.exports = {}
node模块化源码分析
加载模块,是通过沙箱方式,把字符串拼接成闭包函数的形式,把实例module、exports、require、__filename、__dirname以参数的方式注入到环境变量中。
导出模块的内容必须是module.exports的内容,exports是module.exports简写,指向相同块内存。
Exports = module.exports,但是exports被覆盖时,exports被分配的是一个新开辟的内存,不再指向module.exports。所以官网建议不要在模块内部直接覆盖exports,即代码不要写exports = ...。
可以使用nodejs vm模块,将拼接字符串代码转执行代码,解决一些非常规需求,如用户自定义执行函数、自定义Mock函数、自定义模块加载器等
nodejs多进程
nodejs单线程,在处理http请求的时候一个错误都会导致整个进程的退出,这是灾难级的。
总结: 线程快而进程可靠性高
**1)需要频繁创建销毁的优先用线程**
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的
**2)需要进行大量计算的优先使用线程**
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
这种原则最常见的是图像处理、算法处理。
**3)强相关的处理用线程,弱相关的处理用进程**
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
**4)可能要扩展到多机分布的用进程,多核分布的用线程**
**5)都满足需求的情况下,用你最熟悉、最拿手的方式
**1)需要频繁创建销毁的优先用线程**
这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的
**2)需要进行大量计算的优先使用线程**
所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。
这种原则最常见的是图像处理、算法处理。
**3)强相关的处理用线程,弱相关的处理用进程**
什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。
一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。
**4)可能要扩展到多机分布的用进程,多核分布的用线程**
**5)都满足需求的情况下,用你最熟悉、最拿手的方式
node相关api
Process 进程
属性
1. **process.argv** 属性,返回一个数组,包含了启动 node 进程时的命令行参数
2. **process.env** 返回包含用户环境信息的对象,可以在 脚本中对这个对象进行增删改查的操作
3. **process.pid** 返回当前进程的进程号
4. **process.platform** 返回当前的操作系统
5. **process.version** 返回当前 node 版本
方法
1. **process.cwd()** 返回 node.js 进程当前工作目录
2. process.chdir() 变更 node.js 进程的工作目录
3. **process.nextTick(fn)** 将任务放到当前事件循环的尾部,添加到 ‘next tick’ 队列,
一旦当前事件轮询队列的任务全部完成,在 next tick 队列中的所有 callback 会被依次调用
一旦当前事件轮询队列的任务全部完成,在 next tick 队列中的所有 callback 会被依次调用
4. **process.exit()** 退出当前进程,很多时候是不需要的
5. process.kill(pid[,signal]) 给指定进程发送信号,包括但不限于结束进程
事件
1. beforeExit 事件,在 Node 清空了 EventLoop 之后,再没有任何待处理任务时触发,可以在这里再部署一些任务,使得 Node 进程不退出,显示的终止程序时(process.exit()),不会触发
2. exit 事件,当前进程退出时触发,回调函数中只允许同步操作,因为执行完回调后,进程立即退出
(重要‼️)错误捕获. uncaughtException 事件,当前进程抛出一个没有捕获的错误时触发,可以用它在进程结束前进行一些已分配资源的同步清理操作,尝试用它来恢复应用的正常运行的操作是不安全的
4. warning 事件,任何 Node.js 发出的进程警告,都会触发此事件
child_process 子进程
exec():执行shell,并返回输出
execSync(). exec()的同步版本
execFile(),参数有区别的exec
execFile方法直接执行特定的程序shell,参数作为数组传入,不会被bash解释,因此具有较高的安全性。
spawn 和execFile类似,但没有回调函数,只能通过监听事件,来获取运行结果。
fork() 用于进程之间的通信,也是IPC通信的基础
Clusternode进行多进程的模块
属性和方法
1. **isMaster** 属性,返回该进程是不是主进程
2. **isWorker** 属性,返回该进程是不是工作进程
3. **fork()** 方法,只能通过主进程调用,衍生出一个新的 worker 进程,返回一个 worker 对象。和process.child的区别,不用创建一个新的child.js
4. setupMaster([settings]) 方法,用于修改 fork() 默认行为,一旦调用,将会按照cluster.settings进行设置。
5. settings 属性,用于配置,参数 exec: worker文件路径;args: 传递给 worker 的参数;execArgv: 传递给 Node.js 可执行文件的参数列表
事件
1. **fork** 事件,当新的工作进程被 fork 时触发,可以用来记录工作进程活动
2. **listening** 事件,当一个工作进程调用 listen() 后触发,事件处理器两个参数 worker:工作进程对象
3. **message**事件, 比较特殊需要去在单独的worker上监听。
4. online 事件,复制好一个工作进程后,工作进程主动发送一条 online 消息给主进程,主进程收到消息后触发,回调参数 worker 对象
5. **disconnect** 事件,主进程和工作进程之间 IPC 通道断开后触发
6. **exit** 事件,有工作进程退出时触发,回调参数 worker 对象、code 退出码、signal 进程被 kill 时的信号
7. setup 事件,cluster.setupMaster() 执行后触发
**cluster多进程模型**
Node.js 的适用场景
1. 高并发
2. 聊天
3. 实时消息推送
优点:Nodejs 是单线程,⾮阻塞 I/O,事件驱动,它的特点决定了它适合做⼀些⼤量 I/O 的东⻄,⽐
如,聊天室,表单提交等不需要⼤量计算的功能。做⼀些微信后端开发,或者做消息系统等。可以整个
项⽬⽤, 也可以根据它的特点在某个模块使⽤,⽐如 socketio,打造⼀个消息系统等。
如,聊天室,表单提交等不需要⼤量计算的功能。做⼀些微信后端开发,或者做消息系统等。可以整个
项⽬⽤, 也可以根据它的特点在某个模块使⽤,⽐如 socketio,打造⼀个消息系统等。
Nodejs 中的 Stream 和 Buffer 有什么区别
Buffer:为数据缓冲对象,是⼀个类似数组结构的对象,可以通过指定开始写⼊的位置及写⼊的
数据⻓度,往其中写⼊⼆进制数据。
Stream:是对 buffer 对象的⾼级封装,其操作的底层还是 buffer
对象, stream 可以设置为可读、可写,或者即可读也可写,在 nodejs 中继承了 EventEmitter 接⼝,
可以监听读⼊、写⼊的过程。具体实现有⽂件流,httpresponse 等。
数据⻓度,往其中写⼊⼆进制数据。
Stream:是对 buffer 对象的⾼级封装,其操作的底层还是 buffer
对象, stream 可以设置为可读、可写,或者即可读也可写,在 nodejs 中继承了 EventEmitter 接⼝,
可以监听读⼊、写⼊的过程。具体实现有⽂件流,httpresponse 等。
其他
npx原理
npx的主要功能是让我们可以在命令行管理操作项目的npm依赖。避免全局安装模块,在项目中直接运行指令
npx的执行顺序机制是首先会自动检查当前项目中的可执行依赖文件(即./node_modules/.bin下面的可用依赖),如果不存在就会去环境变量path中寻找,如果还没有就会自动安装,其安装的依赖位于node安装目录中的node_cache/_npx之中,所以安装的依赖只是临时的。
第一种:根据路径来执行webpack的脚本:
./node_modules/.bin/webpack
第二种:使用 npm-run-script 的方式,在package.json的script字段里面执行操作:
首先在script字段定义命令
"script": {"webpack": "webpack"}
然后在命令行执行 npm run webpack
npx的执行顺序机制是首先会自动检查当前项目中的可执行依赖文件(即./node_modules/.bin下面的可用依赖),如果不存在就会去环境变量path中寻找,如果还没有就会自动安装,其安装的依赖位于node安装目录中的node_cache/_npx之中,所以安装的依赖只是临时的。
第一种:根据路径来执行webpack的脚本:
./node_modules/.bin/webpack
第二种:使用 npm-run-script 的方式,在package.json的script字段里面执行操作:
首先在script字段定义命令
"script": {"webpack": "webpack"}
然后在命令行执行 npm run webpack
npx和npm区别
npx和npm是两个nodejs包管理工具中的命令行工具
npm是nodejs的包管理器,用于安装和管理js模块,以及在项目中运行脚本。npx是npm更高版本中包含一个命令行工具,用于执行本地安装或在线安装的nodejs包中的命令
npm安装包需要在本地全局或项目依赖中进行,才能在命令行中直接使用。而npx可以在不需要全局安装的情况下直接运行某个包中的命令
npx会首先检查本地是否存在指定的包,如果存在直接运行,如果不存在先下载,再执行其中命令。使得npx更加灵活,不需要事先安装一个包就能立即运行
npm link
npm link 操作会在全局 node_modules 目录(如 MacOS 默认的是 /usr/local/lib/node_modules)下创建一个指向想项目目录的超链接
此时只需要指定 module-name,在项目的 node_modules 目录下创建一个 module-name 的超链接,链接到 /usr/local/lib/node_modules/module-name,然后再由全局目录下的超链接,链接到具体的代码目录下。
此时只需要指定 module-name,在项目的 node_modules 目录下创建一个 module-name 的超链接,链接到 /usr/local/lib/node_modules/module-name,然后再由全局目录下的超链接,链接到具体的代码目录下。
node框架
express[ik'spres]
1、express是⼀个快速开发,极简的web开发框架。
2、express的四⼤模块为application、request、response、router;
3、express基于ES5语法,
对于回调地狱只能通过插件引⼊promise或者async/await才可以;
2、express的四⼤模块为application、request、response、router;
3、express基于ES5语法,
对于回调地狱只能通过插件引⼊promise或者async/await才可以;
使用:webpack-dev-server
koa
使用
vite
1、koa是express原版⼈⻢打造,但是koa致⼒于成为⼀个更⼩、更富有表现⼒、更健壮的 Web 框架,
2、koa也是四个模块,分别是Application、Request、Response、Context。
3、⽽koa本身就追随ECMAScript规范,从⼀代generator到⼆代async/await均有⽀持。
2、koa也是四个模块,分别是Application、Request、Response、Context。
3、⽽koa本身就追随ECMAScript规范,从⼀代generator到⼆代async/await均有⽀持。
认识洋葱模型
代码
当程序运行到await next()的时候就会暂停当前程序,进入下一个中间件,处理完之后才会回过头来继续处理。
原理
核心:中间件管理和next实现,其中next是巧妙的使用了Promise特性。洋葱模型,本质上是Promise.resolve()的递归。
洋葱模型实现原理
next()返回的是promise,需要使用await去等待promise的resolve值。
promise的嵌套就像是洋葱模型的形状就是一层包裹着一层,直到await到最里面一层的promise的resolve值返回。
在洋葱模型中,每一层相当于一个中间件,用来处理特定的功能,比如错误处理、Session 处理等等。其处理顺序先是 next() 前请求(Request,从外层到内层)然后执行 next() 函数,最后是 next() 后响应(Response,从内层到外层),也就是说每一个中间件都有两次处理时机。
在 koa 中,中间件被 next() 方法分成了两部分。next() 方法上面部分会先执行,下面部门会在后续中间件执行全部结束之后再执行
Koa 的洋葱模型指的是以 next() 函数为分割点,先由外到内执行 Request 的逻辑,再由内到外执行 Response 的逻辑。通过洋葱模型,将多个中间件之间通信等变得更加可行和简单。其实现的原理并不是很复杂,主要是 compose 方法。
为什么 Koa 使用洋葱模型
比如,我们需要知道一个请求或者操作 db 的耗时是多少,而且想获取其他中间件的信息。在 koa 中,我们可以使用 async await 的方式结合洋葱模型做到。
ap.use方法
use 方法就是做了一件事,维护得到 middleware 中间件数组
listen 方法
egg
由阿里巴巴团队开源的一套基于koa的应用框架,已经在集团内部服务了大量的nodejs系统。
**Egg.js 为企业级框架和应用而生**,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本。
**Egg.js 为企业级框架和应用而生**,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本。
Egg 奉行『**约定优于配置**』,按照[一套统一的约定](https://eggjs.org/zh-cn/advanced/loader.html)进行应用开发,
团队内部采用这种方式可以减少开发人员的学习成本,开发人员不再是『钉子』,可以流动起来。
团队内部采用这种方式可以减少开发人员的学习成本,开发人员不再是『钉子』,可以流动起来。
特点
- 提供基于 Egg [定制上层框架](https://eggjs.org/zh-cn/advanced/framework.html)的能力
- 高度可扩展的[插件机制](https://eggjs.org/zh-cn/basics/plugin.html)
- 内置[多进程管理](https://eggjs.org/zh-cn/advanced/cluster-client.html)
- 基于 [Koa](http://koajs.com/) 开发,性能优异
- 框架稳定,测试覆盖率高
- 渐进式开发
- 高度可扩展的[插件机制](https://eggjs.org/zh-cn/basics/plugin.html)
- 内置[多进程管理](https://eggjs.org/zh-cn/advanced/cluster-client.html)
- 基于 [Koa](http://koajs.com/) 开发,性能优异
- 框架稳定,测试覆盖率高
- 渐进式开发
关系:
1.express和koa都是基于nodejs的web开发框架,都可以作为HTTP Server或者TCP Server使
⽤,
2.并且koa是express原版⼈⻢打造。
3.Koa 是一个非常优秀的框架,然而对于企业级应用来说,它还比较基础
4.Egg 选择了 Koa 作为其基础框架,在它的模型基础上,进一步对它进行了一些增强。
约定优于配置,高度可扩展,是一个企业级框架。
1.express和koa都是基于nodejs的web开发框架,都可以作为HTTP Server或者TCP Server使
⽤,
2.并且koa是express原版⼈⻢打造。
3.Koa 是一个非常优秀的框架,然而对于企业级应用来说,它还比较基础
4.Egg 选择了 Koa 作为其基础框架,在它的模型基础上,进一步对它进行了一些增强。
约定优于配置,高度可扩展,是一个企业级框架。
graphQl
特点
一种api查询语言,是一种接口开发标准,支持常见的服务端语言,如java、PHP、javascript
- 精确获取需要的信息
- 通过单个请求获取各种资源
- 通过类型系统描述查询
- 强大的调试工具
移动端
移动端适配方案
rem是相对于HTML的根元素em相对于父级元素的字体大小。VW,VH 屏幕宽度高度的高分比
viewport 适配
<meta name="viewport" content="width=750,initial-scale=0.5">
vw 适配(部分等比缩放)
header {
font-size: calc(28vw * var(--width))
}
弹性盒模型布局
流式布局(百分比布局)
响应式布局(媒体查询)
app与H5 如何通讯交互的?
electron
主进程负责管理应用窗口和原生资源,以及与操作系统交互。
渲染进程负责处理每个窗口的前端内容,例如HTML、CSS和JavaScript。
主进程和渲染进程之间可以使用Electron的IPC模块进行通信
0 条评论
下一页