HTML 和 CSS
H5新增特性
- webscoket
前端使用websocket的时候,后端也要添加webstocket。
注:这里面试官还可能会问到【tcp三次握手和四次挥手的全过程】、【setTimeout和setInterval的区别】 - 音频
- 视频
- 表单控件
- Canvas 拖拽
Css3新增属性
- 渐变
- 过渡
- 动画
canvas、svg、webgl
WebGL是OpenGL的版本,OpenGL是3D引擎.WebGL可以帮助用户在Web浏览器中执行3D操作。
区别:
- canvas画图基于像素点,是位图,如果进行放大或缩小会失真
svg基于图形,用html标签描绘形状,放大缩小不会失真 - canvas需要js中绘制,svg在html中绘制
- canvas无法对已经绘制的图像进行修改、操作,svg可以获取到标签进行操作
- Canvas 适用于位图,高数据量高绘制频率(帧率)的场景,如动画、游戏;SVG 适用于矢量图,低数据量低绘制频率的场景,如图形、图表;
position定位
- absulute定位相对
父级容器
()定位,而不是浏览器
。 - fixed定位,相对于
浏览器
,而不是父容器
,不添加top,bottom,left,right样式,通过margin定位。
float 会引发什么问题 怎么清除浮动
float会引发父元素高度塌陷
。
清除浮动的方法:
- 空标签。在浮动的最后一个标签后添加一个空标签,并给齐添加上
style = "clear:both"
,使其闭合浮动。 - BFC。通过float(不能为none)、position(不能为static或者relative)、display(不能为inline-block、table-cell、flex、table-caption或者inline-flex)、overflow(不能为visible)开启块级格式化上下文。
- 伪类。
.clearfix:after{/*伪元素是行内元素 正常浏览器清除浮动方法*/ content: ""; display: block; height: 0; clear:both; visibility: hidden; } .clearfix{ zoom: 1; /*ie6清除浮动的方式 *号只有IE6-IE7执行,其他浏览器不执行*/ }
- 双伪类
.clearfix:after,.clearfix:before{ content: ""; display: table; } .clearfix:after{ clear: both; } .clearfix{ zoom: 1; }
常用的CSS选择器
- class选择器
- id选择器
- 标签选择器
- 伪类选择器
- ::v-deep、/deep/
- ::ng-deep
不定宽元素水平居中
- position
- transform
- flex
flex :1
flex:1是 flex: 1 1 auto
的缩写, 即 flex: flex-grow flex-shrink flex-base。
- flex-grow放大比例,当子元素小于父元素,通过放大比例分配剩余空间。
- flex-shrink缩小比例,当子元素宽度大于父元素宽度, 通过缩小比例缩小。
- flex-basis替换原有的所占宽度。
响应式布局
- rem相对于html的font-size大小。(html中font-size: 12px; 2rem就是24px)。
- 媒体查询.
// 如果文档宽度小于 300 像素则修改背景颜色background-color
@media screen and (max-width: 300px) {
body {
background-color:lightblue;
}
}
弹性盒布局
父盒子 flex属性
-
flex-direction 子元素 排列顺序 row默认水平 row-resever 水平反方向 column 水平垂直 column-resever水平垂直反方向
-
flex-warp 默认主轴 横向排列 默认(nowrap) 不换行 wrap换行 第一行在上面 wrap-resever 第二行在上面
-
flex-flow flex-direction 和 flex-warp 缩写 默认 row nowrap
-
justfly-content 主轴对齐方式 center 居中 space-around space-betworrn flex-start flex-end
- align-items flex-start flex-end center stretch baseline
- align-content
在这里插入图片描述
子属性
oeder 排序
flex-grow 放大比例
flex-shrink 缩小比例
flex-basis 性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
flex flex-grow flex-shrink flex-basis
align-self 性允许单个项目有与其他项目不一样的对齐方式,可覆盖可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。
样式前缀是配浏览器
-webkit 只能适配部分浏览器,如果要是配所有的浏览器可以使用webpack的postcss-loader
插件的autoprefixer自动加前缀。
重绘(Repaint)、回流(Reflow)
回流:布局引擎会根据各种样式计算每个盒子在页面上的大小与位置。
重绘:当计算好盒模型的位置、大小及其他属性后,浏览器根据每个盒子特性进行绘制。
如何降低重绘回流的次数
- 放弃传统操作DOM的时代,基于Vue/React开发;
- 分离读写(浏览器都有渲染队列机制,遇到修改样式的代码后不会立刻渲染,而是添加到队列中,等到下一行是非修改样式代码再去将队列中的修改一起执行);
// 错误写法 会导致2次回流 let box = document.getElementById("box") box.style.width = "200px" console.log(box.clientWidth) box.style.height = "200px" // 正确写法 执行1次回流 let box = document.getElementById("box") box.style.width = "200px" box.style.height = "200px" console.log(box.clientWidth)
- 集中样式更改;
- 缓存处理;
// 错误 回流2次 box.style.width = box.clientWidth + "200px" box.style.height = box.clientHeight + "300px" // 正确 回流1次 let a = box.clientWidth let b = box.clientHeight box.style.width = a + "200px" box.style.height = b + "300px"
- 元素批量修改;
// 错误 引发5次回流 for(let i=0; i<5; i++) { let li = document.createElement("li") li.innerHTML = i box.appendChild(li) } // 正确 引发1次回流 // 1.文档碎片 let frg = document.createDocumentFragment() for(let i=0; i<5; i++){ let li = document.createElement("li") li.innerHTML = i frg.appendChild(li) } box.appendChild(frg) frg = null // 2.字符串拼接 let str = `` for(let i=0; i<5; i++){ str += `<li>${i}</li>` } box.innerHTML = str
- 动画效果应用到position属性为absolute / fixed 的元素上(脱离文档流)
- CSS3 硬件加速(GPU加速,transform / opacity / filters 等等会触发硬件加速,不会引发重绘和回流;
- 牺牲平滑换取速度,比如有个动画是每秒移动1个像素点,改为移动10个像素点;
- 避免 table 布局和使用 css 的javascript。
rem和em的区别
rem:由html标签的font-size去决定大小。
html { font-size: 14px; }
1rem = 14px;
em:由父元素的font-size决定大小。
font-size的继承性
雪碧图
把多张图片合成一张,然后通过background-position
来定位使用,节省http的请求次数,这里可能会问到http2多路复用。
除了使用雪碧图还可以使用以下方法:
- 字体图标 - (阿里图标库-iconfont);
- svg;
- dataURL嵌套;
使用: 1. 在项目中将小于10k的图片配置成dataURL,使用时不会发生额外的请求; 2. canvas.toDataURL(),将canvas转化为data URL的格式
JS
如何实现position: sticky效果(吸顶效果)
使用getBoundingClientRect或者IntersectionObserve计算指定元素的位置,到达一定距离更改指定元素的位置为fixed,从而实现sticky效果。
注意:
- 父元素(包含祖先元素)不能overflow:hidden或者overflow:auto属性;
- 必须指定top、bottom、left、right4个值之一,否则只会处于相对定位;
- 父元素的高度不能低于sticky元素的高度;
- sticky元素仅在其父元素内生效。
日期格式化Date.format
这里需要引入 jutils 库
import jutils from '@/utils/jutils';
var date = jutils.formatDate(new Date(1533686888*1000),"YYYY-MM-DD HH:ii:ss");
console.log(date);
// 2019-07-09 19:44:01
手动实现:
通过get数据,获取输入的分隔符,实现拼接。
new操作符
new操作符创建对象可以分为以下四个步骤
- 创建一个空对象
- 将所创建对象的__proto__属性值设为构造函数的prototype的属性值
- 执行构造函数中的代码,构造函数中的this指向该对象
- 返回对象
JavaScript的数据类型
string、number、Boolean、null、undefined、Object、Symbol、bigInt
判断类型的方式
typeof:只能判断(string、number、Boolean、undefined、function、Symbol、bigInt),不能区分(Array,Object,null);
A instanceof B:判断A是否是B的实例对象,从而判断A是否为B的类型。
Object.propotype.toString.call(obj).splice(8, -1)可以判断所有的数据类型。
js数据存储方式的区别
基本数据类型存储在栈中,复杂数据类型的数据存储在堆中。
requestAnimationFrame
requestAnimationFrame是为了实现更流畅和性能更好的动画,自带函数节流功能,在60帧每秒的浏览器中基本可以保证在 16.6 毫秒内只执行一次,并且该函数的延时效果是精确的,没有其他定时器时间不准的问题,requestAnimation属于高优先级任务,每一帧渲染的都会执行回调。
const element = document.getElementById('parent');
let start;
function step(timestamp) {
if (start === undefined)
start = timestamp;
const elapsed = timestamp - start;
//这里使用`Math.min()`确保元素刚好停在200px的位置。
element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';
if (elapsed < 2000) { // 在两秒后停止动画
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
注意:动画中元素样式最好根据时间进行计算,而不是使用需要样式计算的DOM API(例如ele.style.offsetWidth),否则会强制重新渲染。
requestIdleCallback
requestIdleCallback为了在渲染空闲时间执行优先级不高的操作,以避免阻塞渲染。
回调在浏览器空闲时间执行。注意如果浏览器一直渲染没有空闲,可能就一直执行不到requestIdelCallback注册的回调,因此可以设置一个超时时间,超时之后会执行注册的回调。
取消回调的方法:
var handle = window.requestIdleCallback(callback, {timeout: 1000});
cancelIdleCallback(handle);
注意:requestIdleCallback用来执行优先级低的任务,由于其执行时机不可控,因此尽量不要执行DOM操作。
作用域
- 全局作用域
- 局部作用域(函数作用域)。
闭包就是利用函数作用域
这里会涉及到:
- 变量提升和函数提升(只有通过var声明的变量或者函数才会有提升的问题)
- 作用域链(
就近原则
一层一层向上查找) - 闭包
闭包及其使用
什么是闭包?
闭包就是有权访问一个函数内部变量的函数,也就是常说的函数内部嵌套函数,内部函数访问外部函数变量,从而导致垃圾回收机制没有将当前变量回收掉。在一个函数里面嵌套另一个函数,被嵌套的那个函数的作用域是一个闭包。这样的操作,有可能会带来内存泄漏。好处就是可以设计私有的方法和变量(能让函数外部访问函数内部的一个函数)。
作用:创建私有变量,减少全局变量,防止变量名污染。可以操作外部作用域的变量,变量不会被浏览器回收,减少变量的值。使用闭包可以形成独立的空间,延长变量的生命周期,报存中间状态值 。读取函数内部的变量值;让这些变量值始终保存着(在内存中)。
优点:延长函数内部变量的使用寿命,使函数外部能够访问函数内部。
缺点:滥用闭包导致内存溢出,页面卡顿。
为什么在函数外查看函数内部的变量会报错
作用域、函数执行完成之后函数内部的变量被销毁了。
在函数内部的变量被存在Script中,和函数同级的存储在Local中(相当于是一个全局的)。
当同时执行两次方法中的变量还是一样的吗
截图中hobby不是同一个hobby。
考点 AO 【Activation Object临时执行对象/执行期上下文】,函数执行完成之后函数内部的变量被销毁了,再次执行的时候,变量又是重新定义的。
防抖节流体现了闭包的什么特性
利用私有变量的持久化,延长了局部变量的生命周期。
vue data() 体现了闭包什么特性
vue的data就是一个闭包,体现了闭包私有空间,返回什么数据就可以拿取什么数据,整个data都是私有的,这样才能保证各组件之间的数据不会相互污染。
闭包的使用
- return 一个函数
- 函数作为参数
- IIFE(自执行函数)
- 循环赋值
- 回调函数
注意
使用闭包容易导致内存泄漏
。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。过度使用闭包会导致内存占用过多,所以要慎用
。
作用
读取函数内部的变量 ,变量的值始终保持在内存中。
Set和Map
Set 和 Map 主要的应用场景在于 数据重组 和 数据储存。
Set
Set 是一种叫做集合的数据结构。
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
// Set 只存储了值, 和数组类似
let mySet = new Set(['HerayChen', '女', 18]);
// 添加
mySet.add(1);
console.log(mySet); // Set{'HerayChen', '女', 18, 1}
// 查找
mySet.forEach((item) => {
console.log(item); // 依次打印输出 'HerayChen', '女', 18, 1
})
// 判断是否存在某值
console.log(mySet.has(1)); // true
// 删除具体的值
mySet.delete(1);
console.log(mySet); // Set{'HerayChen', '女', 18}
// 清空所有元素
mySet.clear();
console.log(mySet); // Set{size: 0}
使用场景:
// 数组去重
let arr = [1, 2, 3, 4, 5, 6, 6];
console.log([...new Set(arr)]); // [1, 2, 3, 4, 5, 6]
Map
Map 是一种叫做字典的数据结构。
对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值)都可以作为一个键或一个值。
// Map对象是一个构造函数,初始化的时候可以传参
// 传递的参数 默认是一个 二维数组
let defaultMap = new Map([['name', 'HerayChen'], ['age', 18]]);
// 插入数据
defaultMap.set('sex', '女'); // Map{ {'name' => 'HerayChen'}, {'age' => 18}, {'sex' => '女'}}
console.log(defaultMap);
// 查询
// key为 name 的 value 值
let hobby = defaultMap.get('name'); // HerayChen
console.log(hobby);
// 删除
defaultMap.delete('name');
console.log(defaultMap); // Map{ {'age' => 18}, {'sex' => '女'}}
// 判断值是否存在
console.log(defaultMap.has('name')); // false
// 清空集合
defaultMap.clear();
console.log(defaultMap); // Map{size: 0}
使用场景:
// 数字类型充当键
let errors = new Map([
[400, 'InvalidParameter'],
[404, 'Not found'],
[500, 'InternalError']
]);
console.log(errors);
// 可以通过接口返回的状态码,自定义错误提示
errors.get(404) // Not found
Map 和 Set的区别
- Map和Set查找速度都非常快,时间复杂度为O(1),而数组查找的时间复杂度为O(n)。
- Map对象初始化的值为一个二维数组,Set对象初始化的值为一维数组。
- Map对象和Set对象都不允许键重复(可以将Set对象的键想象成值)。
- Map对象的键是不能改的,但是值能改,Set对象只能通过迭代器来更改值。
WeakSet和WeakMap
WeakSet
- 不能遍历,方法有add、delete、has。
- 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏。
let weakmap=new WeakMap();
let key={};
weakmap.set(key,1);
console.log(weakmap); //WeakMap { {}=>1 }
let key = {};
let obj = {"foo": 1};
weakmap.set(key, obj);
console.log(weakmap);
//WeakMap { {}=>1 , {}=>{"foo":1} }
obj = null;
console.log(weakmap);
//WeakMap { {}=>1 , {}=>{"foo":1} }
WeakMap
- 不能遍历,方法有add、delete、has。
- 只接受对象作为键名(null除外),不接受其他类型的值作为键名。
键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的。
原型链 ,原型,作用域链,作用域,构造函数,实例
原型链:是一条对象隐式原型不往上指向的一条指向链,尽头时Object的隐式原型(null)。
作用:构造函数的原型上的东西可以让实例共享,节省空间。
在原型链中的指向是:函数 → 构造函数 → Function.prototype → Object.protype → null ;
原型、原型链、构造函数、实例的关系:
原型链搜索直到null就停止搜索。
箭头函数
特点:
- this指向定义时所在的父级作用域中,且不能通过call apply bind 改变this指向;
- 箭头函数arguments 会解析到 外层 arguments;
- 不能当做构造函数 通过new关键字实例化对象;
- 简写方式 会自动return。
箭头函数与普通函数的区别
普通函数是很早就提出的,而箭头函数是es6提出的,他们两个在语法上不一样,并在普通函数与箭头函数他们this的指向也不要一样,普通函数内的this指向是如果没有绑定事件元素的时候,this指向的window,或者在闭包中this指向的也是window,如果函数绑定了事件,但并没有产生闭包,这个this指向的是当前调用的事件对象,箭头函数内this的指向是父作用域。
箭头函数不能使用arguments,普通函数可以使用,arguments是以集合的方式获取函数传递的参数。
箭头函数不能实例化为构造函数,而普通函数可以进行实例化。
call、apply、bind的区别
call直接传参,apply传递的参数是一个数组。
bind传参后不立即执行,会返回一个函数,这个函数可以继续传参且执行(bind函数可以分两次传参)。
new操作的原理
- 创建一个空对象 => {};
- 将创建的空对象的__proto__属性指向构造函数的prototype属性;
- 将构造函数的this指定为创建的空对象;
- 执行构造函数(为this绑定属性、复制等操作);
- 如果构造函数有返回值,且返回类型为引用类型,则将构造函数的返回值作为new的结果返回;
- 如果构造函数有返回值,且返回类型为基础数据类型,如int,则无论构造函数返回什么都无效,都将1中创建对象作为构造函数返回值返回;
- 如果构造函数没有返回值,则将1中创建对象作为构造函数返回值返回。
深拷贝和浅拷贝
浅拷贝:只拷贝第一层,如 […a],Object.assign();
深拷贝:所有的层都会拷贝,如JSON.parse(JSON.stringify()) ;
赋值: 赋值指针指向,还是用的同一个内存空间。
继承
-
原型链继承 (
var a = new Fuction()
)- 优点:方便简洁;
- 缺点:只能数据共享,不能传递参数,只能通过
a.属性
来修改添加属性。
-
构造函数继承(
Parent.call(this, newFunction)
)- 优点:解决了原型链实现继承的不能传参的问题和父类的原型共享的问题;
- 缺点:只能在构造函数中定义,并且子类也不可共享。
-
组合继承(构造函数 + 原型链继承)(
Person.prototype.getInfo
+Person.call
)- 优点:可以数据共享和数据共享;
- 缺点:会调用两次超类型构造函数(创建构造函数 + 构造函数内部)。
-
原型式继承
- 方法一:创建一个空函数,将所有的共享信息写在一个obj对象上,然后将共享信息绑定到函数的原型上(
Function.prototype = obj
),然后返回; - 方法二:
Object.create(obj)
; - 优点:不需要单独创建构造函数;
- 缺点:数据共享局限性,子类不能向父类传值。
- 方法一:创建一个空函数,将所有的共享信息写在一个obj对象上,然后将共享信息绑定到函数的原型上(
-
寄生式继承(公共继承函数【var a = 原型式继承方法一函数,将a当作一个对象,向对象上添加函数,最后返回a】)
- 优点:不需要单独创建构造函数;
- 缺点:寄生式继承给对象添加函数会导致函数难以复用,开发效率低。
-
寄生组合式继承(构造函数继承属性,原型链混合继承方法)
- 优点:降低了父类构造函数的开销;
- 缺点:代码复杂。
-
ES6 super 和 Class
-
ES6继承
-
Class继承
- 将要继承的标识符写在constructor函数中,通过向类的原型上添加值,实例化类传参修改值实现继承
- 子类有构造函数和没有构造函数的情况
-
发布订阅模式和观察者模式的区别
在设计模式中都被称为发布订阅模式。
发布订阅模式
基于一个事件(主题)通道,希望接收通知的对象 Subscriber 通过自定义事件订阅主题,被激活事件的对象 Publisher 通过发布主题事件的方式通知各个订阅该主题的 Subscriber 对象(vue兄弟组件之间通信bus就是使用的发布订阅模式)。
发布订阅模式是观察者模式中的一种。
三个角色:发布者 Publisher ,事件调度中心 Event Channel ,订阅者 Subscriber 。
// 发布订阅
class Events {
constructor() {
this.sub = {} // 容器
}
// 根绝不同 name,订阅对应函数
$on(name, fn) {
const wrap = this.sub[name] || (this.sub[name] = [])
wrap.push(fn)
}
// 遍历所有相同name的订阅函数,并发布
$emit(name, ...args) {
const fns = this.sub[name] || []
fns.forEach(fn => {
fn.apply(this, args)
})
}
// 销毁,避免内存泄漏
$of(name){
this.sub[name] = null
}
}
// event 相当于中转器
const event = new Events()
// 订阅
event.$on('eventname', function (a, b) {
console.log(a, b)
})
event.$on('eventname', function (a, b) {
console.log(a, b)
})
// 发布
event.$emit('eventname', 'a', 'b')
缺点:当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中,会导致内存泄漏。所以该消息不使用的时候,调用$of销毁,以避免内存泄漏。
观察者模式
当对象之间存在一对多的依赖关系时,其中一个对象的状态发生改变,所有依赖它的对象都会收到通知,这就是观察者模式(vue的数据劫持就用到了观察者模式)。
// Subject为被观察者,Subject中的状态(state)改变,就通知 Observer更新
class Subject {
constructor() {
this.observes = []
this.state = false
}
// this.observes存储观察者
attach(observe){
this.observes.push(observe)
}
// 状态改变,通知 Observer 触发更新
setState(newState){
this.state = newState
this.observes.forEach( observer => observer.update(newState))
}
}
// Observer为观察者,观察Subject的状态是否改变
class Observer {
constructor(name) {
this.name = name
}
// 更新
update(state){
console.log(this.name + ",接收到了通知,被观察者的属性变为 " + state)
}
}
var sub = new Subject()
var obs1 = new Observer('1')
var obs2 = new Observer('2')
sub.attach(obs1)
sub.attach(obs2)
// 被观察者的状态改变,触发观察者更新
sub.setState(true)
同步异步
同步异步的起源:JS是单线程的。
同步任务:同步任务都在主线程上执行,形成一个执行栈。
异步任务:JS的异步是通过回调函数实现的。
异步任务的三种类型:
1.普通事件,如click、resize等;
2.资源加载,如load、error等;
3.定时器,包括setInterval、setTimeout等。
异步任务相关回调函数添加到任务队列中(任务队列也称为消息队列)
事件循环:不断获取任务、执行任务的过程就是事件循环。
多核:
TCP三次握手
- 客户端发送syn包(seq=x)到服务器,并进入SYN_SENT状态,等待服务器确认;
- 服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(seq=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
- 客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED状态,完成三次握手。
握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。
TCP四次挥手
- 主动关闭方发送一个FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不 会再给你发数据了(当然,在fin包之前发送出去的数据,如果没有收到对应的ack确认报文,主动关闭方依然会重发这些数据),但是,此时主动关闭方还可 以接受数据;
- 被动关闭方收到FIN包后,发送一个ACK给对方,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号);
- 被动关闭方发送一个FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了;
- 主动关闭方收到FIN后,发送一个ACK给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手。
在地址栏里输入一个地址回车会发生哪些事情【经典面试题】
例如:
htttps://www.baidu.com
构成:
url:统一资源标识符
https:传输协议
www:服务器
baidu.com:域名
- 解析URL:首先会对 URL 进行解析,分析所需要使用的传输协议和请求的资源的路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,如果存在非法字符,则对非法字符进行转义后再进行下一过程;
- 缓存判断:浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里并且没有失效,那么就直接使用,否则向服务器发起新的请求;
- DNS解析: 下一步首先需要获取的是输入的 URL 中的域名的 IP 地址,首先会判断本地是否有该域名的 IP 地址的缓存,如果有则使用,如果没有则向本地 DNS 服务器发起请求。本地 DNS 服务器也会先检查是否存在缓存,如果没有就会先向根域名服务器发起请求,获得负责的顶级域名服务器的地址后,再向顶级域名服务器请求,然后获得负责的权威域名服务器的地址后,再向权威域名服务器发起请求,最终获得域名的 IP 地址后,本地 DNS 服务器再将这个 IP 地址返回给请求的用户。用户向本地 DNS 服务器发起请求属于递归请求,本地 DNS 服务器向各级域名服务器发起请求属于迭代请求;
- 获取MAC地址: 当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相与,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址;
- TCP三次握手: 下面是 TCP 建立连接的三次握手的过程,首先客户端向服务器发送一个 SYN 连接请求报文段和一个随机序号,服务端接收到请求后向客户端发送一个 SYN ACK报文段,确认连接请求,并且也向客户端发送一个随机序号。客户端接收服务器的确认应答后,进入连接建立的状态,同时向服务器也发送一个ACK 确认报文段,服务器端接收到确认后,也进入连接建立状态,此时双方的连接就建立起来了;
- HTTPS握手: 如果使用的是 HTTPS 协议,在通信前还存在 TLS 的一个四次握手的过程。首先由客户端向服务器端发送使用的协议的版本号、一个随机数和可以使用的加密方法。服务器端收到后,确认加密的方法,也向客户端发送一个随机数和自己的数字证书。客户端收到后,首先检查数字证书是否有效,如果有效,则再生成一个随机数,并使用证书中的公钥对随机数加密,然后发送给服务器端,并且还会提供一个前面所有内容的 hash 值供服务器端检验。服务器端接收后,使用自己的私钥对数据解密,同时向客户端发送一个前面所有内容的 hash 值供客户端检验。这个时候双方都有了三个随机数,按照之前所约定的加密方法,使用这三个随机数生成一把秘钥,以后双方通信前,就使用这个秘钥对数据进行加密后再传输;
- 返回数据: 当页面请求发送到服务器端后,服务器端会返回一个 html 文件作为响应,浏览器接收到响应后,开始对 html 文件进行解析,开始页面的渲染过程;
- 页面渲染: 浏览器首先会根据 html 文件构建 DOM 树,根据解析到的 css 文件构建 CSSOM 树,如果遇到 script 标签,则判端是否含有 defer 或者 async 属性,要不然 script 的加载和执行会造成页面的渲染的阻塞。当 DOM 树和 CSSOM 树建立好后,根据它们来构建渲染树。渲染树构建好后,会根据渲染树来进行布局。布局完成后,最后使用浏览器的 UI 接口对页面进行绘制。这个时候整个页面就显示出来了;
- TCP四次挥手: 最后一步是 TCP 断开连接的四次挥手过程。若客户端认为数据发送完成,则它需要向服务端发送连接释放请求。服务端收到连接释放请求后,会告诉应用层要释放 TCP 链接。然后会发送 ACK 包,并进入 CLOSE_WAIT 状态,此时表明客户端到服务端的连接已经释放,不再接收客户端发的数据了。但是因为 TCP 连接是双向的,所以服务端仍旧可以发送数据给客户端。服务端如果此时还有没发完的数据会继续发送,完毕后会向客户端发送连接释放请求,然后服务端便进入 LAST-ACK 状态。客户端收到释放请求后,向服务端发送确认应答,此时客户端进入 TIME-WAIT 状态。该状态会持续 2MSL(最大段生存期,指报文段在网络中生存的时间,超时会被抛弃) 时间,若该时间段内没有服务端的重发请求的话,就进入 CLOSED 状态。当服务端收到确认应答后,也便进入 CLOSED 状态。
evenloop事件循环
循环过程:
- 选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即null,则执行跳转到微任务(MicroTask)的执行步骤。
- 将事件循环中的任务设置为已选择任务。
- 执行任务。
- 将事件循环中当前运行任务设置为null。
- 将已经运行完成的任务从任务队列中删除。
- microtasks步骤:进入microtask检查点。
- 更新界面渲染。
- 返回第一步。
hash和history区别
hash 当 # 后面的路径发生变化时,浏览器并不会重新发起请求,
而是会触发 onhashchange 事件,(hash的改变是记录在window.history 中)不利于 SEO 优化。hash 只能修改 # 后面的部分,所以只能跳转到与当前 url 同文档的 url 。
history API 是 H5 提供的新特性,允许开发者直接更改前端路由,即更新浏览器 URL 地址而不重新发起请求。通过 pushState 、 replaceState 来实现无刷新跳转的功能.在使用 history 模式时,需要通过服务端来允许地址可访问,如果没有设置,就很容易导致出现404的情况。
垃圾回收方法
1.标记清除(mark and sweep)
这是JavaScript最常见的垃圾回收方式
,当变量进入执行环境的时候,比如函数中声明一个变量,垃圾回收器将其标记为“进入环境”,当变量离开环境的时候(函数执行结束)将其标记为“离开环境”。
垃圾回收器会在运行的时候给存储在内存中的所有变量加上标记,然后去掉环境中的变量以及被环境中变量所引用的变量(闭包),在这些完成之后仍存在标记的就是要删除的变量了
2.引用计数(reference counting)
在低版本IE中经常会出现内存泄露,很多时候就是因为其采用引用计数方式进行垃圾回收。引用计数的策略是跟踪记录每个值被使用的次数,当声明了一个变量并将一个引用类型赋值给该变量的时候这个值的引用次数就加1,如果该变量的值变成了另外一个,则这个值得引用次数减1,当这个值的引用次数变为0的时 候,说明没有变量在使用,这个值没法被访问了,因此可以将其占用的空间回收,这样垃圾回收器会在运行的时候清理掉引用次数为0的值占用的空间。
在IE中虽然JavaScript对象通过标记清除的方式进行垃圾回收,但BOM与DOM对象却是通过引用计数回收垃圾的,也就是说只要涉及BOM及DOM就会出现循环引用问题。
如何减少垃圾回收
- 解除引用(变量、对象等不再使用时设置为null);
- 使用const和let( const 和 let都是以块为作用域,相比使用var,使用这两个新关键字可能会更早的让垃圾回收程序接入,尽早回收应该回收的内存)。
内存泄漏
内存泄漏指任何对象在您不再拥有或需要它之后仍然存在。 垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用 数量为 0(没有其他对象引用过该对象),或对该对象的唯一引用是循环的,那么该对象 的内存即可回收。
下面几种情况都会导致内存泄露:
- setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏;
- 闭包;
- 控制台日志;
- 循环(在两个对象彼此引用且彼此保留时,就会产生一个循环)。
页面滚动图片加载
- 给页面绑定滚动事件;
- 加载页面的时候把真正的图片地址放在某属性中;
- 然后再滚动过程中判断元素是否进入当前浏览器窗口内;
- 最后加载图片。
var scrollLoad = (function (options) {
var defaults = (arguments.length == 0) ? { src: 'xSrc', time: 300} : { src: options.src || 'xSrc', time: options.time ||300};
var camelize = function (s) {
return s.replace(/-(\w)/g, function (strMatch, p1) {
return p1.toUpperCase();
});
};
this.getStyle = function (element, property) {
if (arguments.length != 2) return false;
var value = element.style[camelize(property)];
if (!value) {
if (document.defaultView && document.defaultView.getComputedStyle) {
var css = document.defaultView.getComputedStyle(element, null);
value = css ? css.getPropertyValue(property) : null;
} else if (element.currentStyle) {
value = element.currentStyle[camelize(property)];
}
}
return value == 'auto' ? '' : value;
};
var _init = function () {
var offsetPage = window.pageYOffset ? window.pageYOffset : window.document.documentElement.scrollTop,
offsetWindow = offsetPage + Number(window.innerHeight ? window.innerHeight : document.documentElement.clientHeight),
docImg = document.images,
_len = docImg.length;
if (!_len) return false;
for (var i = 0; i < _len; i++) {
var attrSrc = docImg[i].getAttribute(defaults.src),
o = docImg[i], tag = o.nodeName.toLowerCase();
if (o) {
postPage = o.getBoundingClientRect().top + window.document.documentElement.scrollTop + window.document.body.scrollTop; postWindow = postPage + Number(this.getStyle(o, 'height').replace('px', ''));
if ((postPage > offsetPage && postPage < offsetWindow) || (postWindow > offsetPage && postWindow < offsetWindow)) {
if (tag === "img" && attrSrc !== null) {
o.setAttribute("src", attrSrc);
}
o = null;
}
}
};
window.onscroll = function () {
setTimeout(function () {
_init();
}, defaults.time);
}
};
return _init();
});
scrollLoad();
eval
把对应的字符串解析成js代码并运行。
eval("const x = 1")
//相当于const x = 1;
缺点:1.声明不会被变量提升;2.耗性能,会执行两次,一次解析成js语句,一次执行。
对象“是否存在”环“(对象有环引用)
定义一个空数组,对目标对象进行递归,每次递归判断递归项是否为对象,是放进数组,且每次判断属性值是否在数组里,是证明存在“环”。
function (obj) {
const arr = [obj];
let flag = false;
function cycle(o) {
const keys = Object.keys(o);
for (const i of keys) {
const temp = o[i];
if (typeof temp === 'object' && tamp !== null) {
if (arr.indexOf(temp) >= 0) {
flag = true;
return;
}
}
}
}
cycle(obj);
return flage;
}
LRU Catch(LRU 缓存)
LRU 是一种常用的页面置换算法
,淘汰久未使用的页面。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
实现思路:
- 有限的存储空间;
- 存储有序数据(按顺序删除数据,Array、Map 数据存储);
- 添加 删除、添加、获取方法(操作制定数据);
- 存储空间存满之后,在添加数据时,会自动删除时间最久远的那条数据。
class LRUCache {
constructor(length) {
this.length = length // 存储长度
this.map = new Map() // 存储数据
}
// 存储数据,通过键值对的方式
set(key, value) {
if(this.map.has(key)) {
this.map.delete(key)
}
this.map.set(key, value)
// 如果超出了容量,则删除最久的数据(map中即第一个)
if(this.map.size > this.length) {
this.map.delete(this.map.keys().next().value)
}
}
// 获取数据
get(key) {
if(!this.map.has(key)) {
return null
}
const val = this.map.get(key) // 获取元素
this.map.delete(key) // 删除元素
this.map.set(key, val) // 重新插入元素,更新存储顺序
}
}
const lruCache = new LRUCache(3)
lruCache.set('name', 'wft')
lruCache.set('age', 18)
lruCache.set('sex', '男')
lruCache.set('len', '超出长度')
性能优化
使用闭包
9个使用场景:
- 返回值(最常用);
function getName() { const name = 'HerayChen'; return function() { return name; } } const nameVal = getName(); console.log(nameVal()); // 打印输出 HerayChen
- 函数赋值;
let fun2 = ''; function getNameFun() { const name = 'HerayChen'; // 给函数赋值 fun2 = function() { return name; } } // 先调用函数,给函数赋值 getNameFun(); console.log(fun2()); // 打印输出 HerayChen
- 函数参数;
function getNameFun() { const name = 'HerayChen'; // 给函数赋值 fun2 = function() { return name; } } let nameVal = getNameFun(); function selectPersonInfo(f) { console.log('当前显示的是' + f() + '的信息') } selectPersonInfo(nameVal); //当前显示的是 HerayChen 的信息
- IIFE自执行函数;
(function() { const name = 'HerayChen'; // 给函数赋值 fun2 = function() { return name; } selectPersonInfo(fun2); // 当前显示的是 HerayChen 的信息 })(); function selectPersonInfo(f) { console.log('当前显示的是' + f() + '的信息') }
- 循环赋值;
for (let index = 0; index < 10; index++) { setTimeout((i) => { console.log(i); // 依次打印输出0-9 }, 100, index); }
- getter和setter;
function getPersonInfo() { let name = 'HerayChen'; getName = function() { return name; } setName = function (nameVal) { name = nameVal; } // 返回 getter 和 setter 方法 return { getName, setName } } const f = getPersonInfo(); console.log(f.getName()); // 打印输出 HerayChen // 修改闭包里面的值 f.setName('天天'); console.log(f.getName()); // 打印输出 天天
- 迭代器
执行一次函数,向下取值。function getPersonInfo(arr) { let i = 0; return function () { return arr[i++] || '没有拉!' } } var next = getPersonInfo([1, 2, 3, 4]); console.log(next()); // 打印输出 1 console.log(next()); // 打印输出 2 console.log(next()); // 打印输出 3 console.log(next()); // 打印输出 4 console.log(next()); // 打印输出 没有拉!
- 首次区分;
相同参数不会再被执行。var fun = (function () { let arr = []; return function (val) { if (arr.indexOf(val) == -1) { arr.push(val) } console.log(arr); } })(); fun(1) // 打印输出 [1] fun(2) // 打印输出 [1, 2] fun(1) // 打印输出 [1, 2]
- 缓存
let fn = (function () { let cache = {}; let calc = function (arr = []) { const sum = arr.reduce((sum, current) => sum + current, 0); return sum; }; return function(){ var args = Array.prototype.slice.call(arguments,0);//arguments转换成数组 var key = args.join(",");//将args用逗号连接成字符串 var result , tSum = cache[key]; if(tSum){//如果缓存有 console.log('从缓存中取:',cache)//打印方便查看 result = tSum; }else{ //重新计算,并存入缓存同时赋值给result result = cache[key]=calc(args); console.log('存入缓存:',cache)//打印方便查看 } return result; } })(); fn(1,2,3,4,5); // 存入缓存: {1,2,3,4,5: 15} fn(1,2,3,4,5); // 从缓存中取: {1,2,3,4,5: 15} fn(1,2,3,4,5,6); // 存入缓存: {1,2,3,4,5: 15, 1,2,3,4,5,6: 21} fn(1,2,3,4,5,8); // 存入缓存: {1,2,3,4,5: 15, 1,2,3,4,5,6: 21, 1,2,3,4,5,8: 23} fn(1,2,3,4,5,6); // 从缓存中取: {1,2,3,4,5: 15, 1,2,3,4,5,6: 21, 1,2,3,4,5,8: 23}
释放内存
JavaScript 采用引用计数的方式来动态管理内存。JavaScript 引擎在运行时统计一块内存被引用的次数,并定期进行垃圾回收(GC)。如果一块堆内存的引用次数变为 0,则会在后续的 GC 中被回收,也就是我们常说的这块内存释放
了。
手动解除引用的方式:
- 使用 undefined,例如 data.value = undefined;
- 使用 null,例如 data.value = null;
- 使用 delete,例如 delete data.value。
通过设为 undefined 或 null 来解引用,比 delete 快近 70 倍。
在性能敏感的场景解引用,通过赋值为 undefined 或 null 性能更佳。
防抖
让事件在一定时间里只触发一次。
防抖是触发间隔大于timer才会触发,所以每次在小于间隔time要清除定时器;
let input = document.querySelector("input");
//input事件触发的时候,执行的是debounce返回的function,这个function里面有防抖和真正的业务逻辑
input.addEventListener('input', debounce(function () {
/*
用debounce函数去约束input这个事件,实际执行的函数是function函数,防抖的延迟是500ms
*/
console.log(this.value);
}, 500));
function debounce(fn, delay) {
let time = null; // time用来控制事件的触发
return function () {
if (time !== null) {
clearTimeout(time);
}
time = setTimeout(() => {
fn.call(this);
//利用call(),让this的指针从指向window 转成指向input
}, delay)
}
}
节流函数
节流是不管time内触发多少次,只会每间隔time时间才会触发一次,所以用return
let input = document.querySelector("input");
function throttle(fn, delay) {
let timer = null
return function () {
if(timer) return
timer = setTimeout(() => {
fn.apply(this,arguments)
timer = null
})
}
}
input.addEventListener('drag', throttle((e) => {
console.log(e.offsetX,e.offsetY)
}), 200)
类数组对象
1,由下标,有length,有push和splice,就是一个类数组;
2,既可以当数组用,也可以当对象用;
3,DOM 里面的数组都是类数组,因为DOM 还要当对象用,而原数组不能解决这个需求(document.querySelectorAll)。
简单类数组:
var arrLike = {
0: 'name',
1: 'age',
2: 'job',
length: 3
}
arguments、callee(调用者)和caller(被调用者)
在调用函数时,浏览器每次都会传递进两个隐含的参数:
1.函数的上下文对象this
2.封装实参的对象arguments
callee属性对应一个函数对象,就是当前正在指向的函数的对象。
callee是arguments的一个属性,指向其函数自身。
function fn (a, b) {
console.log(arguments.callee) // 打印输出当前函数
console.log(arguments.callee == fn) // true
}
fn(1, 2)
caller是指向调用当前函数的函数。
var a = function() {
console.log(a.caller); // 打印输出调用的b函数
}
var b = function() {
a();
}
b();
window.onload和document.ready
document.ready怎么实现:利用DOMContentLoaded
监听。
区别是:
document.ready方法在DOM树加载完成后就会执行,而window.onload是在页面资源(比如图片和媒体资源,它们的加载速度远慢于DOM的加载速度)加载完成之后才执行。
也就是说$(document).ready要比window.onload先执行。
web hosting(网站托管)
iframe
优点:
1、iframe能够原封不动的把嵌入的网页展现出来;
2、如果有多个网页引用iframe,那么只需要修改iframe的内容,就可以实现调用每一个页面的更改,方便快捷;
3、网页如果为了统一风格,头部和版本都是一样的,就可以写成一个页面,用iframe嵌套,可以增加代码的可重用;
4、如果遇到加载缓慢的第三方内容,如图标或广告,这些问题可以由iframe来解决;
5、iframe会堵塞主页面的Onload事件;
6、iframe和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。
缺点:
1、iframe会阻塞主页面的Onload事件;
2、iframe和主页面共享链接池,而浏览器对相同城的链接有限制,所以会影响页面的并行加载;
3、使用iframe之前需要考虑这两个缺点,如果需要使用iframe,最好是通过JavaScript;
4、动态给iframe添加src属性值,这样可以可以绕开以上两个问题
5、不利于seo
6、代码复杂,无法一下被搜索引擎索引到
7、iframe框架页面会增加服务器的http请求,对于大型网站不可取。
8、很多的移动设备无法完全显示框架,设备兼容性差。
CDN内容分发网络
通过IP访问实际资源时,如果CDN上并没有缓存资源,则会到源站请求资源,并缓存到CDN节点上,这样,用户下一次访问时,该CDN节点就会有对应资源的缓存了。
DOM节点太多
会导致页面卡顿(监听事件的内存的占用增大,到一定程度时,网页就出现了卡顿)。
优化:
- 重复利用dom结构,创建虚拟列表
- 使用事件委托,将监听事件绑定到父元素上
缓存优化
优化:最常用的就是http缓存。
使用方式:
- HTML缓存;
通过 meta标签 的http-equiv和content来设置报文头:Cache-Control和Expires。
用meta标签的http-equiv属性来设置强缓存。用法简单,不需要服务器支持。适合更新频率低的页面。<meta http-equiv="Expires" content="Mon, 20 Jul 2013 23:00:00 GMT" /> <meta http-equiv="Cache-Control" content="max-age=7200" />
- nginx缓存;
当nginx作为静态资源服务器时,通过配置http头保证浏览器缓存行为一致。# html使用协商缓存 location ~.*\.html$ { add_header Cache-Control no-cache; # 作用跟Cache-Control no-cache一致;兼容HTTP/1.0 add_header Pragma no-cache; } # 对于更新频率低的,缓存有效时间可设置长一点。 location ~.*\.(js|css|png|jpg)$ { expires 365d; }
- webpack文件名hash。
通过webpack打包,自动给文件名加上hash值。其中,contenthash表示hash值由文件内容计算得到,内容不同产生的contenthash值也不一样。entry:{ main: path.join(__dirname,'./main.js'), vendor: ['react', 'antd'] }, output:{ path:path.join(__dirname,'./dist'), publicPath: '/dist/', filname: 'bundle.[contenthash].js' }
ES6
var let const
- var存在变量名提升,当使用var声明变量的时候,在声明前调用变量,变量是
undefined
; - let存在块级作用域没有变量名提升,在未声明前使用会报错;
- const简单数据类型(数字、字符)的数据不能修改【内存地址保存的数据不能修改】,复杂类型的数据(数组或对象)指针是固定的可以修改。
如何让const声明的复杂类型不能修改【阿里面试真题-对象冻结】
function constTize(obj) {
// 判断对象是否被冻结
if (Object.isFrozen(obj)) {
// 冻结就返回
return obj;
}
// 没有冻结实现 -- 深度冻结
// 获取键值也可以通过 Reflect.ownKeys(obj) 来获取
Object.keys(obj).forEach(key => {
// 递归冻结
typeof obj[key] === 'object' && (obj[key] = constTize(obj[key]));
});
return Object.freeze(obj);
}
const obj = {
name: 'HerayChen',
hobby: {
one: '游泳',
two: 'Sing'
}
}
// 使用对象冻结
const custObj = constTize(obj);
// 解冻
try {
custObj.name = '123';
custObj.hobby = {}
} catch (err) {
console.log(err.message);
}
console.log(custObj); // 这里打印的数据是 obj 对象数据
字符串
- for of遍历字符串
- for (let codePoint of ‘foo’) {console.log(codePoint)};
- 依次打印输出 f、o、o。
- 模板字符串
- 变量转换,
User ${user.name} is not authorized to do ${action}.
; - 换行,直接在模板字符串内换行就会按照换行内容解析;
- 计算,
${x} + ${y} = ${x + y}
; - 调用函数,
foo ${fn()} bar
;
- 变量转换,
注意:模板字符无法嵌入其他语言,遇到不合法的字符串转义,就返回undefined
。
数组
解构赋值
console.log(1, ...[2, 3, 4], 5);
打印输出 1,2, 3,4,5`
函数的调用
function push(array, ...items) {
array.push(...items);
return array;
}
function add(x, y) {
return x + y;
}
const numbers = [4, 38];
console.log(add(...numbers)); // 42
console.log(push([1, 2, 3], ...numbers)); // [1, 2, 3, 4, 8]
```
- 只有函数调用时,扩展运算符才可以放在圆括号中
```js
console.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected number
```
- 替代函数的apply方法
```js
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
复制数组
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
合并数组
合并后的数组是浅拷贝,如果修改了引用指向的值,会同步反映到新数组。
const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];
const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];
a1[0].foo = 3;
console.log(a3); // 打印输出 [{foo: 3}, {bar: 2}]
console.log(a3[0] === a1[0]); // true
console.log(a4[0] === a1[0]); // true
与解构赋值结合
扩展运算符用于数组赋值,只能放在参数的最后一位
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
- 字符串
将字符串转换为真正的数组
[...'hello']
// [ "h", "e", "l", "l", "o" ]
实现了 遍历接口的对象
扩展运算符可以将其转为真正的数组
<div>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</div>
// querySelectorAll()方法返回的是一个NodeList对象
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
// array可以查看当前div下的所有结构属性方法,array.children可以查看子元素的值
console.log(array);
Map 和 Set 结构,Generator 函数
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
let map = new Set([1, 2, 3, 1, 2]);
// map 是 Set后的对象
console.log([...map]); // [1, 2, 3]
console.log(Array.from(map)); // [1, 2, 3]
const go = function*(){
yield 1;
yield 2;
yield 3;
};
[...go()] // [1, 2, 3]
Array.from
将类似数组的对象转换为真正的数组。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5 的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6 的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
Array.from的第二个参数。
let arrayLike = {
'0': '3',
'1': '2',
'2': '4',
length: 3
};
let arr2 = Array.from(arrayLike, x => x * x);
// 相当于
let arr3 = Array.from(arrayLike).map(x => x * x);
console.log(arr2); // [9, 4, 16]
console.log(arr3); // [9, 4, 16]
Array.of
将一组值,转换为数组。
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
Array.of是为了弥补Array的不足,Array参数不同,会有不同的返回值。
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
copyWithin【不常用】
copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。
// target 替换值的位置索引
// start 替换的值的索引, 默认为 0
// end 读取数组的长度,默认为 数组的长度
Array.prototype.copyWithin(target, start = 0, end = this.length);
从左向右,从0开始。
从右向左,从-1开始。
find
找出第一个符合条件的数组成员,没有符合条件的成员,则返回undefined
。
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
findIndex
返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
find 和 findIndex 这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。
function f(v){
return v > this.age;
}
let person = {name: 'John', age: 20};
// f 函数的 this 指向 person
[10, 12, 26, 15].find(f, person); // 26
类似的方法还有indexOf
, 没有返回 -1。
let a = [1, 5, 10, 15].indexOf(10);
console.log({a}); // 2
弥补了数组的indexOf()不能处理 NaN[Not-A-Number(不是一个数字)] 的情况.
[NaN].indexOf(NaN); // -1
// Object.is() 和 严格比较运算符(===)一样
// +0 === -0 //true
// NaN === NaN // false
// Object.is(+0, -0) // false
// Object.is(NaN, NaN) // true
[NaN].findIndex(y => Object.is(NaN, y)); // 0
[NaN].findLastIndex(y => Object.is(NaN, y)); // 0
findLast
从后向前找出第一个符合条件的数组成员,没有符合条件的成员,则返回undefined
。
[1, 5, 10, 15].findLast(function(value, index, arr) {
return value > 9;
}) // 15
findLastIndex
返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
[1, 5, 10, 15].findLastIndex(function(value, index, arr) {
return value > 9;
}) // 3
fill【不常用】
填充一个数组。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
// fill(填充的值, start, end)
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
keys
对键名的遍历。
for (let index of ['a', 'b'].keys()) {
console.log(index);
}
// 0
// 1
values
对键值的遍历。
for (let elem of ['a', 'b'].values()) {
console.log(elem);
}
// 'a'
// 'b'
entries
对键值对的遍历。
for (let [index, elem] of ['a', 'b'].entries()) {
console.log(index, elem);
}
// 0 "a"
// 1 "b"
includes
判断数组中是否存在给定的值。
// 一个参数
// 判断数组中是否含有括号中的值
[1, 2, 3].includes(2) // true
// 两个参数
// 第二个参数,是搜索的起始位置,默认为0,第二个参数为负数,则表示倒数的位置,参数位置大于数组长度默认重置为0
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
flat【常考手写题】
flat扁平化数组,默认拉平一层。
[1, 2, [3, 4]].flat(1)
// [1, 2, 3, 4]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
// 不管几维数组,都解析成一维数组
[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]
// flat会默认跳过空位
[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]
flatMap
flatMap对原数组的每个成员执行函数方法。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
对象
简写
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
console.log({user, foo})
// {user: {name: "test"}, foo: {bar: "baz"}}
属性名表达式
// 方法一
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;
方法的name属性
const person = {
sayName() {
console.log('hello!');
},
};
person.sayName.name // "sayName"
属性的可枚举性
Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123, 包含这个属性的属性值
// writable: true, 可写性,可以修改属性的值
// enumerable: true, 可枚举性,可以通过for in遍历属性
// configurable: true 可修改,可以通过delete重新定义属性
// }
属性的遍历
for in
for…in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
Object.keys
Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
Object.getOwnPropertyNames
Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
Object.getOwnPropertySymbols
Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。
Reflect.ownKeys
Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。
super
this关键字总是指向函数所在的当前对象。
super的this指向当前对象的原型对象。
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
对象的扩展运算符
解构赋值
- 等号右边是undefined或null,就会报错,因为它们无法转为对象;
- 解构赋值必须是最后一个参数,否则会报错。
扩展运算符
- 拷贝对象;
let z = { a: 3, b: 4 }; let n = { ...z }; n // { a: 3, b: 4 } let foo = { ...['a', 'b', 'c'] }; foo // {0: "a", 1: "b", 2: "c"} // 等同于 {...Object(true)} {...true} // {} // 等同于 {...Object(undefined)} {...undefined} // {} // 等同于 {...Object(null)} {...null} // {}
- 合并对象;
let aClone = { ...a }; // 等同于 let aClone = Object.assign({}, a); let ab = { ...a, ...b }; // 等同于 let ab = Object.assign({}, a, b);
模板字符串
模板字符串(template string)是增强版的字符串,用反引号(`)标识。
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
let x = 1;
let y = 2;
`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"
函数扩展
函数参数默认值
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
与结构赋值一起使用
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
默认值的位置
如果传入undefined,将触发该参数等于默认值,null则没有这个效果。
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null)
// 5 null
函数的length
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
扩展参数不计入length中。
(function(...args) {}).length // 0
如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
字符串、数组、对象的常用方法
字符串
数组
对象
本地存储
本地存储分为cookie、sestionstorage 、 localstorage 。
cookie
cookie设计初衷是用来和服务器通讯,而不是本地存储,他只是被借用到本地存储。
缺点:存储空间小,最大4k、http请求时需要发送到服务器,增加请求数据量、只能用document.cookie='…' 来修改,太过简陋。
Cookies.set(name, value);
Cookies.get(name);
Cookies.remove(name)
sestionstorage
在HTML5 中,新加入了一个localStorage/sessionString特性,这个特性主要是用来作为本地存储来使用的,解决了cookie 存储空间不足的问题(cookie中每条cookie的存储空间为4k),localStorage中一般浏览器支持的是5M大小。
sessionStorage.setItem(key, value);
sessionStorage.getItem(key);
sessionStorage.removeItem(key);
sessionStorage 数据只会存在于当前会话,浏览器关闭则清空。
localstorage
localStorage只支持string类型的存储。
localStorage.setItem(key, value);
localStorage.getItem(key);
localStorage.removeItem(key);
localStorage数据会永久存储,除非代码或手动删除,一般用localStorage会更多一些。
存:JSON.stringify(转换字符串);
取:JSON.parse。
http
缓存(浏览器缓存策略)
强缓存
分类:
- from memory cache
不访问服务器,直接读缓存,从内存中读取缓存。此时的数据时缓存到内存中的,当浏览器关闭后,数据将不存在。
- from disk cache
不访问服务器,直接读缓存,从磁盘中读取缓存,当浏览器关闭后,数据还是存在。
强缓存主要由三个字段名来控制:
- Expires过期时间,它的值是 GMT时间; 在浏览器发起请求时,会根据系统时间和 Expires 的值进行比较,如果系统时间超过了 Expires 的值,那么就代表缓存失效了;
- Cache-Control缓存控制,常用的值
- Pragma 只有一个属性值,就是 no-cache ,效果和 Cache-Control 中的 no-cache 一致,不使用强缓存,需要与服务器验证缓存是否新鲜,在 3 个头部属性中的优先级最高。
协商缓存
缺点:
状态码
1xx:指示消息,表示请求已接收,继续处理
2xx:成功,表示请求已被成功接收,处理
200 OK:客户端请求成功
204 No Content:无内容。服务器成功处理,但未返回内容。一般用在只是客户端向服务器发送信息,而服务器不用向客户端返回什么信息的情况。不会刷新页面。
206 Partial Content:服务器已经完成了部分GET请求(客户端进行了范围请求)。响应报文中包含Content-Range指定范围的实体内容
3xx 重定向
301 Moved Permanently:永久重定向,表示请求的资源已经永久的搬到了其他位置。
302 Found:临时重定向,表示请求的资源临时搬到了其他位置
303 See Other:临时重定向,应使用GET定向获取请求资源。303功能与302一样,区别只是303明确客户端应该使用GET访问
307 Temporary Redirect:临时重定向,和302有着相同含义。POST不会变成GET
304 Not Modified:表示客户端发送附带条件的请求(GET方法请求报文中的IF…)时,条件不满足。返回304时,不包含任何响应主体。虽然304被划分在3XX,但和重定向一毛钱关系都没有
4xx:客户端错误
400 Bad Request:客户端请求有语法错误,服务器无法理解。
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden:服务器收到请求,但是拒绝提供服务
404 Not Found:请求资源不存在。比如,输入了错误的url
415 Unsupported media type:不支持的媒体类型
5xx:服务器端错误,服务器未能实现合法的请求
500 Internal Server Error:服务器发生不可预期的错误。
503 Server Unavailable:服务器当前不能处理客户端的请求(务器繁忙),一段时间后可能恢复正常,
https http 协议 区别
- HTTPS协议需要CA证书,费用较高;而HTTP协议不需要 ;
- HTTP协议是超文本传输协议,信息是明文传输的,HTTPS则是具有安全性的SSL加密传输协议;;
- 使用不同的连接方式,端口也不同,HTTP协议端口是80,HTTPS协议端口是443;;
- HTTP协议连接很简单,是无状态的;HTTPS协议是具有SSL和HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP更加安全。
https的工作原理
http的加密过程
https是由http通讯,用SSL/TLS(完全传输层协议)来加密数据包的,https主要是提供对网站服务器的身份认证,保护交换数据的隐私和完整性。
传输加密过程:
- 客户端先向服务端发起https请求,客户端生成一个随机数发给服务端,传输的时候会带上客户端支持的加密方式套件,还带上协议版本号(随机数 + 加密方式套件 + 协议版本号);
- 服务端收到请求后存放随机数,确认加密方法,也生成一个随机数伴随着公钥(数字证书)一起传给客户端(加密方法+随机数+公钥);
- 客户端确认服务器证书后对证书进行递归验证并获取公钥,把收到的公钥生成一个预主密钥,再生成一个用公钥加密后的随机数一起发给服务端,还会发一个用hash加密过的内容(预主密钥+随机数);
- 服务端拿到预主秘钥后,用私钥解密信息拿到预主密钥;
- 客户端和服务端同时对这三个随机数用同一套加密方式算出一个主密钥,所以这个主密钥只有客户端和服务端知道,中间件是破解不了的;
- 客户端finished,发送共享秘钥key加密过的“finished”信号;
- 服务端finished,发送共享秘钥key加密过的“finished”信号;
- 达成安全通信。
http 与 webscorket 的关系
相同点:
1、都是基于TCP协议的,都是可靠性的传输协议。
2、都是应用层上的协议。
异点:
1、WebSocket是一种双向通信协议
(Http是单向的),模拟Socket协议,在建立连接后,WebSocket可以使服务器与客户端(Browser/Client)两者都能主动向对方发送和接收数据,就是Socket一样。
2、WebSocket需要类似TCP的握手方式才能建立起连接,连接成功后才能正常相互通信。(其实,Http请求的最开头,也是需要三次握手
的,因为它是基于TCP的)
3、WebSocket建立连接后,是类似Socket(TCP)的长连接,服务端与客户端可以随时通信。(Http是请求-应答模式的短连接,一次通信后连接就会断开)
304缓存的原理
服务器首先为请求生成ETag【响应头】,服务器可在稍后的请求中,使用它来判断页面是否已经修改。本质上,客户端通过将该记号传回服务器要求服务器验证其(客户端)是否缓存。
304是HTTP状态码,服务器用它来标识这个文件没有修改,不返回内容,浏览器在接收到个状态码后,会使用浏览器已缓存的文件。
客户端请求页面A。服务器返回页面A,并给A加上一个ETag。客户端展现该页面,并将页面连同ETag一起缓存。
客户端再次请求页面A,并将上次请求时服务器返回的ETag起传递给服务器。
服务器检查该ETag,并判断岀该页面自上次客户端请求之后还未被修改,直接返回响应304(未修改一 Not modified)和一个空的响应体。
http2多路复用
多路复用代替原来的序列和阻塞机制,所有就是请求的都是通过一个 TCP 连接并发完成。同时也很好的解决了浏览器限制同一个域名下的请求数量的问题。
在 HTTP/1 中,每次请求都会建立一次HTTP连接,也就是我们常说的3次握手4次挥手,这个过程在一次请求过程中占用了相当长的时间,即使开启了 Keep-Alive ,解决了多次连接的问题,但是依然有两个效率上的问题:
- 串行的文件传输。当请求a文件时,b文件只能等待,等待a连接到服务器、服务器处理文件、服务器返回文件,这三个步骤。我们假设这三步用时都是1秒,那么a文件用时为3秒,b文件传输完成用时为6秒,依此类推。(注:此项计算有一个前提条件,就是浏览器和服务器是单通道传输)
- 连接数过多。我们假设Apache设置了最大并发数为300,因为浏览器限制,浏览器发起的最大请求数为6,也就是服务器能承载的最高并发为50,当第51个人访问时,就需要等待前面某个请求处理完成。
HTTP/2的多路复用就是为了解决上述的两个性能问题。
在 HTTP/2 中,有两个非常重要的概念,分别是帧(frame)和流(stream)。
帧代表着最小的数据单位,每个帧会标识出该帧属于哪个流,流也就是多个帧组成的数据流。
多路复用,就是在一个 TCP 连接中可以存在多条流
。换句话说,也就是可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。
跨域原理
跨域产生的原因:浏览器的同源策略(协议、IP地址、端口号相同)
常见处理跨域的方式
document.domain
通过JS将两个页面中的document.domain属性设置为相同的基础主域 eg:abc.com,
从而实现同域.
跨域资源共享(CORS)
服务端通过设置 Access-Control-Allow-Origin
标识哪些站点可以请求文件,或者设置 Access-Control-Allow-Origin 为"*" 允许任意站点请求文件
postMessage
jsonp的原理及实现
原理:利用script、img等标签能够跨域请求资源的特点实现(html 中的一些标签 如
实现:
假设JSONP请求如下:
jsonp({
url: ‘http://path/to/server/b’,
params: {A: a, B: b},
success: function myCallback (response) {}
})
背后其实在进行:
(1) 拼接一个script标签,,从而触发对指定地址的GET请求
(2) 服务器端对这个GET请求进行处理,并返回字符串 “myCallback(‘response value’)”
(3) 前端script加载完之后,其实就是在script中执行myCallback(‘response value’)
nginx 反向代理跨域
设置一个 nginx 作为代理服务器,页面所有请求被 nginx 代理服务器监听,相当于页面向 nginx 发送请求,而 nginx 将该请求转发到真实请求的目标服务器上,但是实际上请求的域 都是 nginx 服务器的域名
只要 请求页面 和 nginx 服务器的域名同源 就行了。
浏览器渲染机制流程原理
浏览器的渲染机制可以分为五步:
-
解析html,构建DOM树;
-
解析CSS,生成CSSOM树;
-
合并dom树和css规则树,生成render渲染树;
-
根据render渲染树进行布局;
-
调用GPU对渲染树进行绘制,合成图层,显示在屏幕上。
简述ajax的过程
1.创建XMLHttpRequest对象,也就是创建一个异步调用对象;
2.创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息;
3.设置响应HTTP请求状态变化的函数;
4.发送HTTP请求;
5.获取异步调用返回的数据。
ajax请求的时候get和post方式的区别
get一般用来进行查询操作,url地址有长度限制,请求的参数都暴露在url地址当中,如果传递中文参数,需要自己进行编码操作,安全性较低。
post请求方式主要用来提交数据,没有数据长度的限制,提交的数据内客存在于http请求体中,数据不会暴漏在url地址中。
Get和Post的区别,何时使用Post?
GET:一般用于信息获取,使用URL传递参数,对所发送信息的数量也有限制,一般在2000个字符,有的浏览器是8000个字符。
P0ST:一般用于修改服务器上的资源,对所发送的信息没有限制在以下情况下,请使用POST请求:
1.无法使用缓存文件(更新服务器上的文件或数据库)
2.向服务器发送大量数据(post没有数据量限制)
3.发送包含未知字符的用户输入时,POST比GET更稳定也更可靠
Web Worker的使用场景
通常用于计算量大的场景,比如大文件上传计算哈希,视频解码。webWorker里面不可以访问dom,加载的文件只能是来源于网络的文件,和主线程通信要通过postmessage。
Node
node中操作文件的方法
在操作文件时需要先引入fs
,const fs = require("fs")
。
readFile、writeFile、stat、unlink、rmdir、rename、readdir、appendFile等。
对promise(承诺)的理解
promise是异步编程的一种解决方案,他比传统的回调函数加事件更加合理和强大,目前我用promise除了使用他的异步操作外,还使用promise在项目中解决了回调地狱
等问题。
特点:
- 对象不受外界影响,并且promise一共有三个状态,分别是进行中,成功,或者失败,只有异步操作的结果,可以决定是哪一种状态,任何其他的操作都无法改变这个状态;
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果,promise的状态改变只有两种可能,要么是成功,要么失败。
- 如果要使用promise必须还要对promise进行实例化,实例化之后promise内有一个回调函数,这个函数里面有两个参数,分别是resolve和reject,当我们的状态发生变化的时候,如果是成功则会通过resolve将成功的结果返回出去,如果失败,那么可以通过reject将错误的信息也返回出去,我们可以通过.then方法接收返回的是成功的结果,通过catch可以接受失败的结果。
promise 状态
中断promise
在then/catch后添加一个返回一个永远 pending 的 promise 。
.catch((err) => {
console.log('onRejected', err);
// 中断promise链:
return new Promise(() => {})
})
promise的链式调用
注意:
- 链式调用时then回调函数必须要有
返回值
,不然下一个then函数执行的时候会是undefind; - 回调函数的返回值,决定了下一个回调函数的的回调(onResolved / onRejected)
- 回调函数的穿透,一般不在then函数中不传递第二个参数,会默认返回error函数,这也是为啥能在catch中拿到数据的原因。
.then( (data) => { ...处理data }, // 不写第二个参数, 相当于默认传了: (err) => Promise.reject(err), // 或 (err) => { throw err; } ).then()
链式调用的原理:
promise 每次返回的都是一个新的promise
。
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function Promise(excutor) {
let that = this;
that.status = PENDING;
that.value = undefined;
that.reason = undefined;
that.onFulfilledCallbacks = [];
that.onRejectedCallbacks = [];
function resolve(value) {
if (value instanceof Promise) {
return value.then(resolve, reject);
}
setTimeout(() => {
if (that.status === PENDING) {
that.status = FULFILLED;
that.value = value;
that.onFulfilledCallbacks.forEach(cb => cb(that.value));
}
});
}
function reject(reason) {
setTimeout(() => {
if (that.status === PENDING) {
that.status = REJECTED;
that.reason = reason;
that.onRejectedCallbacks.forEach(cb => cb(that.reason));
}
});
}
try {
excutor(resolve, reject);
} catch (e) {
reject(e);
}
}
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError("Chaining cycle detected for promise!"));
}
let called = false;
if (x instanceof Promise) {
if (x.status === PENDING) {
x.then(
y => {
resolvePromise(promise2, y, resolve, reject);
},
reason => {
reject(reason);
}
);
} else {
x.then(resolve, reject);
}
} else if (x != null && (typeof x === "object" || typeof x === "function")) {
try {
let then = x.then;
if (typeof then === "function") {
then.call(
x,
y => {
if (called) return;
called = true;
resolvePromise(promise2, y, resolve, reject);
},
reason => {
if (called) return;
called = true;
reject(reason);
}
);
} else {
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
resolve(x);
}
}
Promise.prototype.then = function(onFulfilled, onRejected) {
const that = this;
let newPromise;
onFulfilled =
typeof onFulfilled === "function" ? onFulfilled : value => value;
onRejected =
typeof onRejected === "function"
? onRejected
: reason => {
throw reason;
};
if (that.status === FULFILLED) {
return (newPromise = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onFulfilled(that.value);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
if (that.status === REJECTED) {
return (newPromise = new Promise((resolve, reject) => {
setTimeout(() => {
try {
let x = onRejected(that.reason);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
if (that.status === PENDING) {
return (newPromise = new Promise((resolve, reject) => {
that.onFulfilledCallbacks.push(value => {
try {
let x = onFulfilled(value);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
that.onRejectedCallbacks.push(reason => {
try {
let x = onRejected(reason);
resolvePromise(newPromise, x, resolve, reject);
} catch (e) {
reject(e);
}
});
}));
}
};
promise捕捉错误的方式
- 在then中处理;
.then( (res) => { // 处理请求成功的返回数据 }, (err) => { // 处理请求失败的返回数据 reject(err); }, )
- 在catch中处理。
.then((res) => {}) .catch((err) => { reject(err) })
async await 语法糖
async + await 是 generator 的语法糖(便捷写法)。
await是根据generator函数来的。
async/asait作用:以同步的方式执行异步操,实现排队效果。
async:函数执行返回一个Promise。
await:后面接Promise等待Promise返回结果,接普通函数则执行函数接收返回结果。
await只能在async中使用,但浏览器调试中await可以单独使用。
function ajax(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(new Error(xhr.statusText))
}
}
xhr.send()
})
}
async function main() {
try {
const userInfo = await ajax('/api/users.json')
console.log(users)
} catch (e) {
console.log(e)
}
}
async 只能和 await一起使用。
function ajax(url) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.responseType = 'json'
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.response)
} else {
reject(new Error(xhr.statusText))
}
}
xhr.send()
})
}
async function main() {
try {
const userInfo = await ajax('/api/users.json')
console.log(users)
} catch (e) {
console.log(e)
}
}
线程休眠
function sleep(times) {
return new Promise(resolve => {
setTimeout(() => resolve(times), times);
})
}
async function test() {
console.log('1');
await sleep(2000); // 线程休眠
console.log('4')
}
// 调用
test(); // 先打印1隔2秒后再打印4
能不能用 async await 替代Promise【快手真题】
Promise不能被替代,Promise.all可以实现并发, async await只能实现串行(只能一行一行的执行)
function sleep(times) {
return new Promise(resolve => {
setTimeout(() => {
console.log(Match.random());
resolve();
}, times)
})
}
// 如何使用一个方法让sleep执行3次
async function demo1() {
await sleep(2000);
await sleep(2000);
await sleep(2000);
}
async function demo2() {
let task = [];
for (let i = 0; i < 3; i++) {
task.push(sleep(2000));
}
await Promise.all(task);
}
demo1(); // 固定两秒钟触发执行一次 sleep 函数【打印一个随机数】
demo2(); // 一次打印出三个随机数
Generator函数的使用
- function关键字与函数名之间有一个星号;
- 函数体内部使用yield语句,定义不同的内部状态(yield在英语里的意思就是“产出”)。
function* getInfo() {
yield '1';
yield '2';
yield '3';
// f.next(); 调用第四次时,会返回 {value: "over", done: true}
// return 代表该函数执行完成,下面再写多少个yield都无效
return 'over';
}
// g()并不会执行g函数,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是迭代器对象(Iterator Object)
const f = getInfo();
// 返回Object {value: "1", done: false}, 这里的done代表的是函数还没执行完,只是在这暂停
// 如果函数没有yield 和 return 使用next时会返回{value: undefined, done: true}
// 如果函数只有 return ,第一次返回 {value: "1", done: true},后面一直是{value: undefined, done: true}
f.next();
promise方法
then
then() 接受两个回调函数作为参数。当 Promise 执行的内容符合成功条件时,调用resolve函数,失败就调用 reject 函数。其中第二个参数可以省略(catch()方法的作用),then方法返回的是一个新的 Promise实例,因此可以链式调用。
let getResolve = new Promise((resolve,reject)=>{
// to do (异步操作)
})
// promise 的链式调用
getResolve.then((data) => {
console.log(data);
})
.then((data) => {
console.log(data);
})
catch
catch() 该方法相当于then方法的第二个参数,指向reject的回调函数;还有一个作用是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。
let getResolve = new Promise((resolve,reject)=>{
// to do (异步操作)
})
// promise 的链式调用
getResolve.then((data) => {
console.log(data);
}).catch((err) => {
console.log('rejected',err);
});
finally
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。
let getResolve = new Promise((resolve,reject)=>{
// to do (异步操作)
})
// promise 的链式调用
getResolve.then((data) => {
console.log(data);
}).finally(() => {
console.log('最终执行的结果’);
});
all
all()方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个 promise对象。当数组中所有的 promise的状态都达到 resolved的时候,all方法的状态就会变成 resolved,如果有一个状态变成了 rejected,那么 all方法的状态就会变成 rejected。
let promise1 = new Promise((resolve,reject)=>{
resolve(1);
});
let promise2 = new Promise((resolve,reject)=>{
resolve(2);
});
Promise.all([promise1,promise2]).then(res=>{
console.log(res); //[1,2]
})
reace
race() 接受的参数也是一个每项都是 promise的数组,当最先执行完的事件执行完之后,就直接返回该 promise对象的值。如果第一个 promise对象状态变成 resolved,那自身的状态变成了resolved;反之第一个 promise变成 rejected,那自身状态就会变成 rejected。
let promise1 = new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1);
},1000)
});
let promise2 = new Promise((resolve,reject)=>{
resolve(2);
});
Promise.reace([promise1,promise2]).then(res=>{
console.log(res); //[2,1]
})
reject 和 resolve
const promise = new Promise(function(resolve, reject) {
if (/* 异步操作成功 */){
resolve(value);
} else {
// 处理失败的操作结果
reject(error);
}
});
Promise.resolve(1).then(function(value){
console.log(value); // 打印出1
});
Promise.reject(new Error(“错了”));
promise.all和async…await的区别
- promise.all 是同时执行多个promise,当所有的promise resolve后返回一个包含所有结果的数组;
- Async …await 是es7提供的用来处理异步的语法,是用形似同步的异步写法,让代码看起来更加的简洁。
promise 手写流程【待完善】
- 创建 Promise 对象;
new Promise((resolve, reject) => {})
- 通过执行函数控制Promise的状态;
resolve:fulfiled - 成功 reject:rejected - 失败
- 确认Promise的状态;
pending - 等待中(只能转换为成功或者失败) fulfiled - 成功 rejected - 失败
- 状态判断 - then;
new Promise((resolve, reject) => { }).then(() => { })
- 处理返回值。
resolve - 处理成功的回调 reject - 处理失败的回调
手写:
class MyPromise {
constructor(executor) {
this.state = 'padding';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallBacks = [];
this.onRejectedCallBacks = [];
let resolve = (data) => {
if (this.state === 'padding') {
this.value = data;
this.state = 'resolved';
this.onResolvedCallBacks.forEach(fn => fn())
console.log(this.state);
console.log('resolved', this.value);
console.log(this.onResolvedCallBacks);
}
}
let reject = (data) => {
if (this.state === 'padding') {
this.value = data;
this.state = 'rejected';
this.onRejectedCallBacks.forEach(fn => fn());
console.log(this.state);
console.log('rejected', this.value);
console.log(this.onRejectedCallBacks);
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e)
}
}
}
async await和promise
async function async1() {
console.log('start');
await async2();
console.log('end');
}
相当于
async function async1() {
console.log('start');
new Promise((resolve, reject) => {
resolve();
}).then((res) => {
console.log('end');
})
}
axios请求封装
axios的特性:
- 从浏览器中创建 XMLHttpRequests
- 从 node.js 创建 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换 JSON 数据
- 客户端支持防御 XSRF
安装axios:
npm install axios
环境地址映射
config.js文件:
let baseUrl = '';
const environment = process.env.NODE_ENV === 'production' ? 'pro' : 'dev' //dev开发环境 pro 生产环境
// 开发环境
if(environment === 'dev'){
baseUrl = 'XXX';
// 线上环境
}else if(environment === 'pro'){
baseUrl = 'XXX';
}
export baseUrl;
全局设置请求信息和错误信息处理(请求拦截处理)
serve.js文件:
import axios from 'axios'
import router from './../router'
// 根据请求的code自定义返回message
const showStatus = (status) => {
let message = ''
switch (status) {
case 400:
message = '请求错误(400)'
break
case 401:
message = '未授权,请重新登录(401)'
break
case 402:
message = '拒绝访问(402)'
break
case 404:
message = '请求出错(404)'
break
case 408:
message = '请求超时(408)'
break
case 500:
message = '服务器错误(500)'
break
case 501:
message = '服务未实现(501)'
break
case 502:
message = '网络错误(502)'
break
case 503:
message = '服务不可用(503)'
break
case 504:
message = '网络超时(504)'
break
case 505:
message = 'HTTP版本不受支持(505)'
break
default:
message = `连接出错(${status})!`
}
return `${message},请检查网络或联系管理员!`
}
const service = axios.create({
// 联调
headers: {
'content-type': 'application/json;charset=UTF-8'
},
// 是否跨站点访问控制请求
withCredentials: true,
timeout: 30000,
validateStatus() {
// 使用async-await,处理reject情况较为繁琐,所以全部返回resolve,在业务代码中处理异常
return true
}
})
// 请求拦截器
service.interceptors.request.use(
config => {
return config
},
(err) => {
err.message = '服务器异常,请联系管理员!'
// 错误抛到业务代码
return Promise.reject(err)
}
)
// 响应拦截器
service.interceptors.response.use(
response => {
const status = response.status
let msg = ''
if (status < 200 || status >= 300 && status != 401 && status != 500) {
// 处理http错误,抛到业务代码
msg = showStatus(status)
if (typeof response.data === 'string') {
response.data = { msg }
} else {
response.data.msg = msg
}
return response
}else if(status == 200){
return response
}else if(status == 500){
msg = showStatus(status)
response.data = {msg:msg}
router.replace({name:'exception',query:{type:500}})
return response
}
},
(err)=>{
err.message = '请求超时或服务器异常,请检查网络或联系管理员!'
return Promise.reject(err)
}
)
export default service
请求封装
request.js文件:
import config from './config' // 基础路径 baseUrl
import service from './service' //封装的axios
/**
* data 是post传参
*
*/
export const getMessage = (url, method, params)=> service({
url: `${baseUrl}${url}`,
method,
data: params,
headers:{
'content-type': 'application/json;charset=UTF-8'
},
})
使用:
import {http} from @/utils/request.js;
http('/userInfo', get, {id: 8}).then((res)=>{
//返回的数据
})
.catch((err)=>{
//错误信息
})
Vue
生命周期
-
beforeCreate(创建前):vue实例初始化之前调用;
此阶段为实例初始化之后,此时的数据观察和事件配置都还没有准备好,而此时的实例中的data和el还是underfined状态,不可用的,dom元素也未加载,此时使用html片段代码我们加上ref属性,用于获取DOM元素的操作会报错,详细效果请使用代码测试。 -
created(创建后):vue实例初始化之后调用;
beforeCreate之后紧接着的钩子就是创建完毕created,此时我们能读取到data的值,但是DOM还没有生成,所以属性el还是不存在的,dom元素也未加载。 -
beforeMount(载入前):挂载到DOM树之前调用;
此时的$el成功关联到我们指定的DOM节点,但是此时的DOM元素还未加载,如果此时在DOM元素中绑定数据使用{{name}}后里边的name不能成功地渲染出我们data中的数据 -
mounted(载入后):挂载到DOM树之后调用;
挂载完毕阶段,到了这个阶段数据就会被成功渲染出来。DOM元素也加载出来了,html片段代码我们加上ref属性,可以获取DOM元素。 -
beforeUpdate(更新前):数据更新之前调用;
当修改Vue实例的data时,Vue就会自动帮我们更新渲染视图,在这个过程中,Vue提供了beforeUpdate的钩子给我们,在检测到我们要修改数据的时候,更新渲染视图之前就会触发钩子beforeUpdate。html片段代码我们加上ref属性,用于获取DOM元素。Dom元素上的数据还没改变。 -
updated(更新后):数据更新之后调用;
此阶段为更新渲染视图之后,此时再读取视图上的内容,已经是最新的内容。 此时加载的DOM元素上的数据更新了。 -
beforeDestroy(销毁前):vue实例销毁之前调用;
调用实例的destroy()方法可以销毁当前的组件,在销毁之前,会触发beforeDestroy钩子。 -
destroyed(销毁后):vue实例销毁之后调用;
成功销毁之后,会触发destroyed钩子,此时该实例与其他实例的关联已经被清除,它与视图之间也被解绑,此时再修改name的值,试图不在更新,说明实例成功被销毁了。
传参方式
props, e m i t , emit, emit,on
p a r e n t , parent, parent,children
a t t r s 和 attrs 和 attrs和listeners
provide,inject
$refs
eventBUS
VUEX
-
父传子;
props// 父组件 <parent :name = "name"/> // 子组件 <child /> props: { name: { type: String, default: 'HerayChen' } }
-
子传父;
$emit// 父组件 <child @func="speak" ref="son" /> // 1. 事件传递 speak(msg){ console.log(msg);//我是子组件发送的消息! } // 2. 事件监听 this.$refs['son'].$on('func',(msg)=>{ console.log(msg); }) // 子组件中 <button @click="send">send</button> send() { this.$emit('func',"我是子组件发送的消息!"); }
-
非父子组件传值。
方法一:安装 pubsub-js 插件: npm i pubsub-js -s 可实现全局参数传递
组件A:<template> <div class="wrap"> <div>我是组件A</div> <button @click="sendMsg">发送</button> </div> </template> <script> import pubsub from 'pubsub-js' export default { name: "A", methods:{ sendMsg(){ pubsub.publishSync("sendMsg","这是A组件发布的消息!"); } } } </script>
组件B:
<template> <div> <div>我是组件B</div> </div> </template> <script> import pubsub from 'pubsub-js' export default { name: "B", mounted(){ pubsub.subscribe("sendMsg",(e,msg)=>{ console.log(e,msg);//sendMsg 这是A组件发布的消息! }) }, } </script>
同步发送消息 - publishSync
同步发送消息 - publish
订阅消息 - subscribe
卸载特定订阅 - unsubscribe
清除所有订阅 - clearAllSubscriptions方法二:EventBus传参 在main.js种挂载全局EventBus。 ```js `Vue.prototype.$EventBus = new Vue() ``` A组件: ```js <template> <div class="wrap"> <div>我是组件A</div> <button @click="sendMsg">发送</button> </div> </template> <script> export default { name: "A", methods:{ sendMsg(){ this.$EventBus.$emit('sendMsg',"这是组件A发送的消息!") } } } </script> ``` B组件: ```js <template> <div> <div>我是组件B</div> </div> </template> <script> export default { name: "B", mounted(){ this.$EventBus.$on('sendMsg',(msg)=>{ console.log(msg);//这是组件A发送的消息! }) }, } </script> ```
父子组件 生命周期 执行顺序
挂载阶段:
父beforecreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
更新阶段:
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
销毁阶段:
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
规律:父组件先开始执行,然后等到子组件执行完,父组件收尾。
条件渲染v-if v-show 区别
v-show 是通过控制display属性来进行dom的显示与隐藏。
v-if 是真正意义上的条件渲染,为true是渲染(dom存在),为false时不存在(dom不存在)。
区别:
1、v-if有更高的切换开销,v-show有更高的初始渲染开销。如果需要频繁的切换,使用v-show比较好,如果运行条件很少改变,使用v-if比较好。
2、v-show比v-if性能更高,因为v-show只能动态的改变样式,不需要增删DOM元素。所以当程序不是很大时候,v-if和v-show区别都不大,如果项目很大,推荐多用v-show,较少浏览器后期操作的性能。
3、需要多种条件场景,比如id=1,=2,=3…时候,因为只有v-if,可以和v-else等连用,这种比较适合用v-if。
4、v-show不支持语法。
5、v-if切换时候回实时的销毁和重建内部的事件、钩子函数等,v-show只会初始化渲染时候执行,再切换时候不会执行生命后期的过程。
v-if v-for优先级
- v-if 和 v-for不能放在一起使用,一起使用要用标签包裹 或者 template,还可以考虑使用计算属性;
- v-for 的优先级大于v-if的优先级 ,嵌套使用性能会很差;
- v-if > v-for 可能会拿到一个不存在属性。
template和render(jsx)的联系
nexTick作用 以及实现原理
nexTick存在是因为Vue异步更新策略
,响应式数据发生变更 vue不会立即更新Dom,而是开启队列,将更新函数存入队列中,同一事件循环中更新函数数据 会批量异步更新,这一策略使得我们更新数据 不会立刻体现到Dom。
作用:获取异步更新后的DOM(DOM更新的操作被Vue记录了下来,存到了需要更新的任务队列里,等待更新)。
在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。
先把传入的回调函数 cb 推入 回调队列callbacks 数组,同时在接收第一个回调函数时,执行能力检测中对应的异步方法(异步方法中调用了回调函数队列)。最后一次性地根据 useMacroTask 条件执行 macroTimerFunc 或者是 microTimerFunc,而它们都会在下一个 tick 执行 flushCallbacks,flushCallbacks 的逻辑非常简单,对 callbacks 遍历,然后执行相应的回调函数。
const callbacks = [] // 回调队列
let pending = false // 异步锁
// 执行队列中的每一个回调
function flushCallbacks () {
pending = false // 重置异步锁
// 防止出现nextTick中包含nextTick时出现问题,在执行回调函数队列前,提前复制备份并清空回调函数队列
const copies = callbacks.slice(0)
callbacks.length = 0
// 执行回调函数队列
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 将回调函数推入回调队列
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 如果异步锁未锁上,锁上异步锁,调用异步函数,准备等同步函数执行完后,就开始执行回调函数队列
if (!pending) {
pending = true
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// 如果没有提供回调,并且支持Promise,返回一个Promise
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
nexTick为什么是微任务
nextTick作用:在下次DOM更新循环结束之后执行的延迟回调。
nexTick是 Vue 利用浏览器本身提供的原生异步API封装而成的一个异步封装方法。
它对于浏览器异步API的选用规则如下,Promise存在取由Promise.then,不存在Promise则取MutationObserver,MutationObserver不存在setImmediate,setImmediate不存在最后取setTimeout来实现。
nextTick即有可能是微任务,也有可能是宏任务,从优先去Promise和MutationObserver可以看出nextTick优先微任务,其次是setImmediate和setTimeout宏任务。
同步代码执行完毕之后,优先执行微任务,其次才会执行宏任务。
手动实现一个nexTick
public myNextTick(callback: Function) {
return Promise.resolve().then(() => {
callback();
});
}
组件渲染和更新的过程
初次渲染:
- 解析模板为 render 函数(把 vue 语法编译 成 js 语法,通过执行 vue-template-compiler 的 compiler 函数,得到 render );
- 触发响应式,监听data属性(响应式关键 Object.defineProperty(),将模版初次渲染使用到的变量绑定到 Object.defineProperty() 中,首次渲染只会 触发 getter );
- 执行render 函数,生成vNode,进行path(执行render函数, 将 vue 语法转成 h 函数的结果,也就是 vNode)
渲染完成(更新视图):
- 修改data触发了setter
- 重新执行render函数,生成newVnode
- 执行patch(vnode, newVnode),diff算法更新视图。
异步渲染:
当有很多dom的增删改情况发生时,vue会汇总dom的修改和操作,一次性更新视图,提升性能,这也是vue为什么做异步渲染的原因。
什么是MVVM
视图模型双向绑定,是Model-View-ViewModel
的缩写,也就是把MVC中的Controller演变成ViewModel。Model层代表数据模型,View代表UI组件,ViewModel是View和Model层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。以前是操作DOM结构更新视图,现在是数据驱动视图。
优点:
1.低耦合。视图(View)可以独立于Model变化和修改,一个Model可以绑定到不同的View上,当View变化的时候Model可以不变化,当Model变化的时候View也可以不变;
2.可重用性。你可以把一些视图逻辑放在一个Model里面,让很多View重用这段视图逻辑。
3.独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。
4.可测试。
watch computed 区别
-
功能上:computed是计算属性,watch是监听一个值的变化,然后执行对应的回调;
-
是否调用缓存:computed中的函数所依赖的属性没有发生变化,那么调用当前的函数的时候会从缓存中读取,而watch在每次监听的值发生变化的时候都会执行回调;
-
是否调用return:computed中的函数必须要用return返回,watch中的函数不是必须要用return;
-
computed默认第一次加载的时候就开始监听;watch默认第一次加载不做监听,如果需要第一次加载做监听,添加immediate属性,设置为true(immediate:true);
-
使用场景:computed----当一个属性受多个属性影响的时候,使用computed-----购物车商品结算。watch–当一条数据影响多条数据的时候,使用watch-----搜索框(computed:一对多,watch:多对一)。
computed支持缓存,相依赖的数据发生改变才会重新计算;watch不支持缓存,只要监听的数据变化就会触发相应操作
computed不支持异步,当computed内有异步操作时是无法监听数据变化的;watch支持异步操作
computed属性的属性值是一函数,函数返回值为属性的属性值,computed中每个属性都可以设置set与get方法。watch监听的数据必须是data中声明过或父组件传递过来的props中的数据,当数据变化时,触发监听器。
计算属性的实现原理
vue解析属性的顺序:props > methods > data > computed > watch。
computed watcher,计算属性的监听器。
computed watcher 持有一个dep实例,通过dirty属性标记计算属性是否需要重新求值。
当computed的依赖值改变后,就会通知订阅的watcher进行更新,对于computed watcher 会将dirty属性设置为true,并且进行计算属性方法的调用。
1.computed 所谓的缓存是指什么?
计算属性是基于它的响应式依赖进行缓存的,只有依赖发生改变的时候,才会重新求值。
2.那computed缓存存在的意义是什么?或者说你经常在什么时候使用?
比如计算属性方法内部操作非常的耗时,遍历一个极大的数组,计算一次可能要耗时1s。
3.没有经过data初始化的数据,只在computed中变化,页面是监听不到数据变化的。
vue 响应式原理
Vue2的响应式原理是Object.defineProperty
,只对初始对象里的属性有监听作用,对新增的属性无效。。
Vue3的响应式原理是Proxy
。
不需要响应式数据的处理
使用场景:从项目开始到结束一直不会变的数据。
处理方法:
- 将数据定义在data之外;
data() { this.motto = ‘Tomorrow will be better than today’; return { } }
- 使用
Object.freeze()
冻结一个对象,使之不能被修改,让defineReactive的时候不能修改get set进行依赖收集【如果你有一个巨大的数组或Object,并且确信数据不会修改,使用Object.freeze()可以让性能大幅提升,提升大约有5~10倍,对于纯展示的大数据,都可以使用Object.freeze提升性能】;data() { return { motto: Object.freeze({‘Tomorrow will be better than today’}) } }
- 将对象的
configurable
设置为false【仅适用于vue2】。
扩展:
还有一个和Object.freeze()很像的方法object.seal
。
把你了解的vue响应式原理阐述一下
- Observer:给对象属性添加getter和setter,用于 依赖搜集和 派发更新
- Dep:用同样手机当前响应式对象的依赖关系,每个响应式对象都有一个Dep实例,dep.sub = watcher[],当数组发生变更时,或通过dep.notify通知各个watcher
- Watcher:观察者对象,render,watcher,computed watcher,user watcher
依赖搜集:
- initState,对computed属性初始化时,会触发computed watcher依赖收集
- initState,对属性初始化的时候,触发的user watcher依赖收集
- render,触发render watcher依赖收集
派发更新:
Object.defineProperty
- 组件中对响应的数据进行了修改,会触发setter逻辑
- dep.notify()
- 遍历所有subs,调用每一个watcher的update方法
总结原理:
当创建vue实例时,vue会遍历data里的属性, Object.defineProperty为属性添加getter和setter对数据的读取进行劫持。
getter:依赖收集
setter:派发更新
每个组件的实例都会对应的watcher实例。
Vue.nextTick的原理
vue的dom更新是异步的,一旦观察到数据的变化,会开启异步队列,把同一个event loop中观察数据变化的watcher对送到这个队列。在下一次事件循环时,vue会清空异步队列,进行dom更新。
Vue.nextTick(() => {})
await Vue.nextTick()
宏任务 -> 微任务 -> UI render(只是浏览器把元素渲染到了浏览器的行为)
一般什么时候用到nextTick?
在数据变化之后要执行的某个操作,而这个操作依赖因数据改变的dom,这个操作应该被放到 Vue.nextTick回调中.
图片懒加载
vue中的vue-lazyload
使用:
- 安装
vue-lazyload
插件;npm i vue-lazyload -D
- 在main.js文件中全局使用插件;
import Vue from 'vue' import App from './App.vue' import VueLazyload from 'vue-lazyload' //引入这个懒加载插件 Vue.use(VueLazyLoad, { preLoad: 1, // 图片加载错误时显示的图片 error: require('./assets/img/error.jpg'), // 图片加载时显示的图片 loading: require('./assets/img/loading.jpg'), attempt: 2, })
- 在图片上通过指令使用实现懒加载。
// 把v-lazy当作图片的src来使用 <img v-lazy="item" alt="">
Astro极致的首屏优化
Astro默认会全部服务端静态渲染生成,但是在调用组件的时候如果加入参数client:load那么它会变成客户端渲染,优化速度是未使用前的10倍。
原生js实现图片的懒加载
打开网站一下把所有的图片加载出来会造成页面的卡顿和白屏现象。
IntersectionObserver
IntersectionObserver滚动动画。
方法:
observe(element):监听某个元素,传入要监听的元素作为参数
unobserve(element):停止监听某个元素,传入停止监听的元素作为参数
disconnect():使监听器停止工作
takeRecords():返回所有正在监听的元素的IntersectionObserverEntry对象数组
使用:
//html
<div class="imgWarp">
<img alt="加载"
class="lazyload"
src=""
data-origin="XXX.png">
<img alt="加载"
class="lazyload"
src=""
data-origin="XXX.png">
<img alt="加载"
class="lazyload"
src=""
data-origin="XXX.png">
</div>
//js
function Observer () {
// 所有需要实现懒加载的img集合
let images = document.querySelectorAll(".lazyload");
// 懒加载实现方法
let observer = new IntersectionObserver(entries => {
entries.forEach(item => {
// isIntersecting 判断目标是否出现在视窗底部
if (item.isIntersecting) {
item.target.src = item.target.dataset.origin; // 开始加载图片,把data-origin的值放到src
observer.unobserve(item.target); // 停止监听已开始加载的图片
}
});
},
{
rootMargin: "0px 0px -100px 0px" // 交叉过视图的100,才开始派发事件
}
);
// 调用滚动动画
images.forEach(item => observer.observe(item));
}
getBoundingClientRect
getBoundingClientRect获取图片离顶部的距离。
通过getBoundingClientRect().top计算图片距离浏览器视口顶部的距离,用自定义data-src属性替换掉图片的真实src。
// 获取所有图片
const imgList = document.querySelectorAll('img')
// 用于记录当前显示到了哪一张图片
let index = 0;
function lazyload() {
// 获取浏览器视口高度,这里写在函数内部是考虑浏览器窗口大小改变的情况
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight
for (let i = index; i < imgList.length; i++) {
// 这里用可视区域高度减去图片顶部距离可视区域顶部的高度
const distance = viewPortHeight - imgList[i].getBoundingClientRect().top;
// 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明图片已经出现在了视口范围内
if (distance >= 0) {
// 给图片赋值真实的src,展示图片
imgList[i].src = imgList[i].getAttribute('data-src');
// 前i张图片已经加载完毕,下次从第i+1张开始检查是否需要显示
index = i + 1;
}
}
}
// 定义一个防抖函数
function debounce(fn, delay = 500) {
let timer = null;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// 页面加载完成执行一次lazyload,渲染第一次打开的网页视口内的图片
window.onload = lazyload;
// 监听Scroll事件,为了防止频繁调用,使用防抖函数进行优化
window.addEventListener("scroll", debounce(lazyload, 600));
// 浏览器窗口大小改变时重新计算
window.addEventListener("resize", debounce(lazyload, 600));
IntersectionObserver和getBoundingClinetRect的区别
IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。
Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。
这两种方法都可以获取当前元素到视口边界的高度,但是他们两个的区别是Intersection Observer在性能上会更好,因为它不会引起回流。
手写一个简单的vue,实现响应式的更新
vue 检测对象 、数组方式
vue 检测对象 其实是递归 利用object.definePrototype() get set读写。
vue 检测数组是对数组身上的 push pop shift unshift resove splice方法进行包装 。
数据改变 页面不更新 原因
vue响应式的原理是object.definePrototype()
, 如果get set读写的属性,是 data 中没被定义的属性 (新增 )无法响应式 。
自定义指令
全局注册 - directive
directive自定义指令的方法。
在main.js
中全局注册:
Vue.directive("resize", {
// 指令的钩子
// 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
bind() {},
// 被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
inserted() {},
// 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
update() {},
// 指令所在组件的 VNode及其子 VNode全部更新后调用。
componentUpdated() {},
// 只调用一次,指令与元素解绑时调用。
unbind() {},
});
// 指令的使用
v-resize
注意:全局指令一次只能注册一个。
局部注册 - directives
<script>
export default {
name: "mainCOmponent",
components: {},
// 局部注册指令
directives: {
resize: {
bind() {},
inserted() {},
update() {},
componentUpdated() {},
unbind() {},
},
},
};
</script>
自定义指令v-resize指令,窗口大小发生变化时,实时打印最新的窗口宽高。
<template>
<div id="app">
<h2>窗口宽度:{{ innerWidth }}</h2>
<h2>窗口高度:{{ innerHeight }}</h2>
<div style="height: 300px; width: 80%; background: blue" v-resize></div>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
innerHeight: window.innerHeight,
innerWidth: window.innerWidth,
};
},
components: {},
directives: {
resize: {
bind() {
console.log("bind");
},
inserted(el, binding, vnode) {
console.log("inserted");
console.log(el, binding);
let that = vnode.context;
// 监听浏览器的resize事件
window.addEventListener("resize", () => {
that.innerHeight = window.innerHeight;
that.innerWidth = window.innerWidth;
});
},
update() {
console.log("VNode更新了");
},
componentUpdated() {
console.log("componentUpdated");
},
unbind() {
console.log("unbind");
window.removeEventListener("resize");
},
},
},
};
</script>
组件的扩展
- mixins ( 分为全局 、局部混入 , 混入数据来源不明确 , methods 依赖 waccher会存在 覆盖问题)
- plugins extends (一次只能扩展一个组件 extends 传入的是 对象 或者函数)
- composition api (Vue3就是这样使用的)
mixin 混入
可以把多个组件共用的配置提取成一个混入对象,是实现类继承的重要方式。
mixin混入文件【minix.js】:
export default {
data() {
return {
x:100,
y:200
}
},
methods: {
showName(){
alert(this.name)
}
},
mounted() {
console.log('你好啊!')
},
}
全局混入
全局混入:Vue.mixin(xxx)。
//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
// 全局混入
import minix from './mixin'
//关闭Vue的生产提示
Vue.config.productionTip = false
// 全局混入使用
Vue.mixin(minix)
//创建vm
new Vue({
el:'#app',
render: h => h(App)
})
局部混入
局部混入:mixins:[‘xxx’]。
<template>
<div>
<h2 @click="showName">学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
</div>
</template>
<script>
//引入一个minix (局部)
import minix from './mixin'
export default {
name:'School',
data() {
return {
name:'xxx大学',
address:'深圳',
x:666 //数据和混入整合时以当前数据为主,但是生命周期钩子是都要,并且混入里面的钩子先执行
}
},
// 使用一个minix(局部)
mixins:[minix],
}
</script>
plugins 插件使用
vue.use()
是注册插件的一种方式,它也可以用来注册组件。 install(Vue,自带带参数)。
定义插件:
import Vue from "vue"
export default {
install(){
// 1. 添加全局过滤器
Vue.filter('mySlice', function(value){
return value.slice(0.4)
})
// 2. 添加全局指令
Vue.directive('fbind',{
// 初始化(指令与元素成功绑定时调用)
bind(element, binding){
element.value = binding.value
},
// 插入页面
inserted(element, value){
element.focus()
},
// 更新/重新解析
update(element, binding){
element.value = binding.value
}
})
// 3. 配置全局混入
Vue.mixin({
data(){
return{
name: 'HerayChen'
}
}
})
// 4. 添加实例方法
vue.prototype.sayName= ()=>{
consOle.log('HerayChen');
}
}
}
使用:
在main.js中全局引用插件
// 引入插件文件
import plugins from './plugins';
// 使用
Vue.use(plugins);
子组件能否修改父组件传递值
官网明确指出 props单向数据流动,非常不推荐修改 会造成 状态朝不可控方向。
如果想要修改 可自定义属性 接受 props 保证子组件只修改自身状态 或者使用computed计算数据进行重新 接收。
虚拟 DOm和Diff算法 遍历中 key作用
虚拟DOM的Diff算法:
- 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构;
- 一层级的一组节点,他们可以通过唯一的id进行区分。
虚拟DOM的Diff算法的复杂度从O(n^3)降到了O(n)。
当页面的数据发生变化时,Diff算法只会比较同一层级的节点:
- 如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了;
- 如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新。
当某一层有很多相同的节点时,也就是列表节点时,Diff算法的更新过程默认情况下也是遵循以上原则,所以需要用key来给每个节点做一个唯一标识,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。
key的作用主要是为了高效的更新虚拟DOM
。
路由懒加载实现
Es6的import【常用】
import直接将组件引入,打包时import的内容是分包的,加载时也是分包加载,如果没有分包就是一整个同时加载,加载效率比较低。
路由文件routes.js:
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
// 根页面
function _import(filePath, basePath = 'views') {
return () => import(`@/${basePath}/${filePath}.vue`);
}
const routes = [
{
path: '/',
component: _import('login/login'),
name: '登录'
}
];
const router = new VueRouter({
routes,
});
export default router;
Vue异步加载
使用vue的异步组件,实现按需加载,每个组件生成一个js文件,实现组件的懒加载。
{
path: '/problem',
name: 'problem',
// component: resolve => require(['加载的路由对应的文件'], resolve)
component: resolve => require(['../pages/home/problemList'], resolve)
}
webpack的require.ensure
如何实现 vue-router
路由: 通过改变URL,在不重新请求页面的情况下,更新页面视图(跳转不刷新)。
路由模式:
- Hash — 默认值 (“#”) 【http://localhost:8080/#/login
】; - History-- 利用URL中的路径(“/”)【http://localhost:8080/login
】;
vue中设置路由模式:
const router=new VueRouter({
mode:'history',
routes:[...]
});
Hash
使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载,其显示的网路路径中会有 “#” 号,有一点点丑。这是最安全的模式,因为他兼容所有的浏览器和服务器。
HashHistory.push();
HashHistory.replace()
设置路由到视图更新的过程:
- $router.push() ;
- 根据hash模式调用,设置hash并添加到浏览器历史记录HashHistory.push()(添加到栈顶)(window.location.hash= XXX);
- 监测更新调用History.transitionTo();
- 更新路由History.updateRoute() ;
- 替换当前app路由{app._route= route} ;
- 更新视图vm.render() 。
History
依赖于Html5 的history,pushState API,所以要担心IE9以及一下的版本,感觉不用担心。并且还包括back、forward、go三个方法,对应浏览器的前进,后退,跳转操作。就是浏览器左上角的前进、后退等按钮进行的操作。
缺点:前端刷新是实实在在地去请求服务器,消耗的时间很多。
window.history.pushState(data, title, targetURL);
window.history.replaceState(data, title, targetURL);
data: 传给目标路由的信息,可为空;
title:目前所有浏览器都不支持,填空字符串即可;
targetURL:目标url,不会检查url是否存在,且不能跨域。如不传该项,即给当前url添加data.
abstract (抽象的)
abstract是用来在不支持浏览器API的环境中,充当fallback,而不论是hash还是history模式都会对浏览器上的url产生作用(在已存在的路由页面中内嵌其他的路由页面,而保持在浏览器当中依旧显示当前页面的路由path)。
abstract与浏览器分离的路由模式,在不改变当前页面path的前提下加载其他路由中的views。
适用于所有JavaScript环境,例如服务器端使用Node.js。如果没有浏览器API,路由器将自动被强制进入此模式。
<template>
<el-drawer
:visible.sync="visible"
v-bind="$attrs"
:before-close="handleClose"
>
<router-view />
</el-drawer>
</template>
<script>
import { routes } from "../router/index";
import VueRouter from "vue-router";
export default {
name: "router-drawer",
props: {
path: {
type: Object,
required: true,
},
visible: {
type: Boolean,
required: true,
default: false,
},
},
// 此处实例化一个新的router来配合当前页面的router-view
router: new VueRouter({
mode: "abstract",
base: "/",
routes,
}),
methods: {
handleClose() {
this.$emit("update:visible", false);
},
},
mounted() {
console.log("drawer router", this.$router);
this.$router.push(this.path);
},
};
</script>
vue3中ref跟reactive区别
ref 中定义的值, 需要通过value属性;
reactive()使用和ref()函数区别在于可以直接拿到对象;
toRefs使用和reactive()函数区别在于可以直接使用对象中name属性.
- ref() 函数根据给定的值创建一个响应式的数据对象,传入的为基本数据类型,例如字符串、数字、boolean 等,返回值是一个对象,这个对象上只包含一个 value 属性,只在setup函数内部访问ref函数需要加.value
// ref 定义基本数据类型的数据 let a = ref(10); // a是一个 RefImpl 对象 console.log(a.value); // 10
- reactive函数传入的为引用类型,例如数组、对象等,但不能代理基本类型值,返回一个响应式的数据对象, 想要使用创建的响应式数据也很简单,创建出来之后,在setup中return出去,直接在template中调用;
// reactive 用于定义复杂数据类型的数据 // 不推荐这样使用 let b = reactive(10) // b 是一个数值类型的 10 // 正确使用方法 let c = reactive({val: 10}); console.log(c); // {val: 10}
区别:
-
reactive 和 ref 都是用来定义
响应式数据
的, reactive更推荐去定义复杂的数据类型,ref 更推荐定义基本类型; -
ref 和 reactive 本质我们可以简单地理解为
ref是对reactive的二次包装
, ref定义的数据访问的时候要多一个.value; -
使用ref定义基本数据类型,ref也可以定义数组和对象。
基本数据类型:字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined), 唯一(Symbol )。
复杂数据类型:对象(Object)、数组(Array)、函数(Function)、日期(Date)、正则(RegExp)等等。
vue实例挂载流程 (mounted阶段)
- 初始化init,data watch dep 数据响应式处理 ;
- 建立更新机制 ,初始化更新函数,依赖收集 。
vue样式模块化
vue是通过·scope
标签实现样式模块化的。
在Vue项目中,Vue Loader支持样式模块化的方式两种分别是Scoped CSS和CSS Modules。
Scoped CSS
Scoped CSS是Vue Loader默认支持的,因此在Vue项目中可以直接使用。
使用:
<style scoped>
...
</style>
为什么scope可以支持样式模块化?
添加scope后的样式,编译后会使用hash值封装,确保了样式的唯一性。
CSS Modules
CSS Modules被Vue Loader视为模拟Scoped CSS的替代方案,也提供了集成来支持,但是需要写入一些配置。
// vue.config.js
// 使用vue-cli的话,可以直接在该文件中配置,不用暴露webpack的配置
module.exports = {
css: {
// 开启CSS Modules
requireModuleExtension: true;
loaderOptions: {
// 向loader传递配置选项
css: {
modules: {
// 自定义类命名规则
// 在我们的项目中,对类的命名是根据环境不同而改变的,开发环境中会展示该类的文件路径方便调试,其他环境中会 使用hash值封装
localIdentName: process.env.NODE_ENV === 'development' ? '[path][name]__[local]' : '[hash:base62:8]',
}
}
}
}
}
使用:
// 在vue3中还可以自定义module命名,<style module="assets">
<style module>
.content {
color: red;
}
</style>
编译后:
<!-- 用自定义属性把类名封装起来了 -->
<style>
.content[data-v-f3f3eg9] {
color: red;
}
</style>
vue组件只有一个根节点的原因
vue2中只能有一个根标签,但是在vue3中根组件已经可以有多个根节点了。
在vue2中只所以这么做是因为vdom是一颗单根树形结构
,patch方法在遍历的时候从根节点开始遍历,它要求只有一个根节点,组件也会转换为一个vdom,自然满足这个要求
vue3中值所以可以有多个节点,是因为引入了Fragment(片段)
的概念,这是一个抽象的节点,如果发现组件有多个根,就创建一个Fragment节点,把多个根节点作为它的children,将来path的时候,如果发现是一个Fragement节点,则直接遍历children创建或更新。
vue数据响应式原理(双向绑定实现原理)
Vue2
数据响应式原理的核心就是采用了数据劫持 + 发布订阅者模式
的方式来实现数据的响应式,通过Object.defineProperty()
对数据进行拦截,把这些属性全部转换成getter/setter,get()方法可以读取数据、收集依赖,set()方法可以改写数据,在数据变动时会对数据进行比较,如果数据发生了变化,会发布消息通知订阅者,触发监听回调,更新视图。
注意:Object.defineProperty 不是为了实现数据响应式出现的,而是 vue 利用了 Object.defineProperty 的特性从而实现了数据响应式。
Vue模板编译 & 组件化机制
模板编译
模板编译的主要目标是将模板(template)转换为渲染函数(render)。
- 解析 path 解析为语法树 ast对象类型虚拟dom;
- 各种transform对ast深加工 比如指令 属性绑定 形成抽象语法树;
- 将asd对象转化为 js 渲染函数对象。
如果用户设置了template 或者是el 选项,最终就会执行编译函数;
若指定 template 或 el 选项,则会执行编译。
编译的过程:
- 解析 - parse ;
- 优化 - optimize;
- 生成 - generate。
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
// 1、解析:将模板转换为对象 AST(抽象语法树)
// parse解析器将模板解析为抽象语法树,基于AST可以做优化或者代码生成工作。
const ast = parse(template.trim(), options);
//2、标记静态节点(页面中不会变的地方)
// optimize优化器的作用是在AST中找出静态子树并打上标记
/**
标记静态子树的好处:
1. 每次重新渲染,不需要为静态子树创建新节点;
2. 虚拟DOM 中 patch 时,可以跳过静态子树.
**/
if (options.optimize !== false) {
optimize(ast, options)
}
//3、代码生成 转换 ast 为代码字符串 new Function(code) 这样就可以得到一个真正的render 函数了
// 将AST 转换成渲染函数中的内容,即代码字符串。
// generate方法生成渲染函数代码
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
组件
首先创建的是根组件,首次 _render() 时,会得到整棵树的 VNode 结构。
new Vue() => $mount() => vm._render() => createElement() => createComponent() =》 patch = 》 createElm = 》 createComponent()
- 创建自定义组件VNode(保存组件构造函数, props ,事件);
- 创建自定义组件实例。
Vue3
响应式抽离为reactive包 , 不依赖vue使用。
在vue3中要通过effect()来触发响应式回调,Vue的响应式是通过Proxy来实现的,所以我们触发响应式回调的对象应该是Proxy 类型的。
const { reactive, effect } = VueObserver
const origin = {
count: 0,
}
// 将回调的对象转换为 Proxy 对象
const state = reactive(origin);
const fn = () => {
const count = state.count
}
// 响应式回调
effect(fn)
vue3 使用了Proxy代理对象【Proxy只能代理一层】,改写了对象的setter和getter操作,来实现依赖收集和响应触发。
vue3中是使用Reflect来操作数据。
let data = { bar: { key: 1 } }
let p = new Proxy(data, {
get(target, key, receiver) {
console.log("get value:", key);
// 使用Reflect.get操作读取数据
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log("set value:", key, value)
// 使用Reflect.set来操作触发数据
return Reflect.set(target, key, value, receiver)
},
})
// 通过 Proxy 修改data的值
// 这里只会监听到bar这一层的数据
// 所以会打印 get value: bar
p.bar.key = 2
由于Proxy只能代理一层,所以就出现了深度响应式。
Reflect
get(target, name, receiver)
Reflect.get 方法查找并返回 target 对象的 name 属性,如果没有该属性,则返回 undefined
。
const obj = {
foo: 1
}
// Reflect.get 的第一个参数一定是一个对象
Reflect.get(obj, 'foo') // obj.foo 1
Reflect.get(obj, 'name') // undefined
const obj2 = {
foo: 3
}
// 如果 name 属性部署了读取函数( getter ),则读取函数的 this 绑定 receiver
Reflect.get(obj, 'foo', obj2) // 3
set(target, name, value, receiver)
Reflect.set 方法设置 target 对象的 name 属性等于 value。
const obj = {
foo: 1
}
Reflect.set(obj, 'foo', 2) // obj.foo 2
// 如果 name 属性设置了赋值函数,则赋值函数的 this 绑定 receiver
const obj2 = {
foo: 0
}
// obj.foo 1
// obj2.foo 2
Reflect.set(obj, 'foo', 2, obj2)
深度响应式
// 接收第二个参数,标记为是否为浅响应
function createReactive(obj, isShallow = false) {
return new Proxy(obj, {
get(target, key, receiver) {
// 访问raw时,返回原对象
if(key === 'raw') return target
track(target, key)
const res = Reflect.get(target, key, receiver)
// 如果是浅响应,直接返回值
if(isShallow) {
return res
}
// 判断res是否为对象并且不为null,循环调用reatcive
if(typeof res === 'object' && res !== null) {
return reatcive(res)
}
return res
},
// ...省略其他
})
使用:
// 深响应
function reactive(obj) {
return createReactive(obj)
}
// 浅响应
function shallowReactive(obj) {
return createReactive(obj, true)
}
vue权限管理
- 动态路由;
- 路由过滤;
- 自定义指令过滤
路由传参的方式
query传参
声明式 router-link
// 配置
{
path: '/resolved',
name: 'Resolved',
component: Resolved
}
// 传参
<router-link :to="{name: 'Resolved', query: {id: 123}}">
跳转
</router-link>
// 获取传递的参数
this.$route.query.id
编程式 this.$router.push
// 配置
{
path: '/resolved',
name: 'Resolved',
component: Resolved
}
// 传参
this.$router.push({
name: 'Resolved',
query: {
id: 123
}
});
// 获取传递的参数
this.$route.query.id
params 传参
显示参数
声明式 router-link
// 配置
{
path: '/resolved/:id',
component: Resolved
}
// 传参
<router-link :to="/resolved/123">
跳转
</router-link>
// 获取传递的参数
this.$route.params.id
编程式 this.$router.push
// 配置
{
path: '/resolved/:id',
component: Resolved
}
// 传参
this.$router.push({
path: '/resolved/${id}'
});
// 获取传递的参数
this.$route.params.id
不显示参数
// 配置
{
path: '/resolved',
name: Resolved,
component: Resolved
}
// 传参
<router-link :to="{name: 'Resolved', params: {id: 123}}">
跳转
</router-link>
或者
this.$router.push({
// 注意这里一定是name
name: 'Resolved',
params: {
id: 123
}
});
// 获取传递的参数
this.$route.params.id
路由导航守卫
全局 beforeEach afterEach
beforeEach 路由进入之前。
一般在main.js
中使用:
router.beforeEach((to, from, next) => {
console.log(to) => // 到哪个页面去
console.log(from) => // 从哪个页面来
next() => {} // 下一步
}
afterEach 路由进入之后。
router.afterEach((to, from) => {
// 不会接受 next 函数也不会改变导航本身
// 常用于
// 修改每个页面的title 【document.title=to.meta.title】
// 每次切换页面的时候,让页面滚动到最顶部 【 window.scrollTo(0,0)】
})
局部beforeEnter (to,from ,next)
beforeEnter 路由进入之前。
只在进入路由时触发,不会在 params、query 或 hash 改变时触发。
const routes = [
{
path: '/users/:id',
component: User,
beforeEnter: (to, from) => {
},
},
]
组件内部 beforeRouteEnter beforeRouteleave
beforeRouteEnter 路由进入之前。
beforeRouteEnter(to,from,next){
next(vm => {
// 通过 `vm` 访问组件实例
})
}
beforeRouteUpdate 路由更新之前。
beforeRouteUpdate(to,from,next){
next(vm => {
// 通过 `vm` 访问组件实例
})
}
beforeRouteLeave 路由离开之前。
beforeRouteLeave(to,from,next){
next(vm => {
// 通过 `vm` 访问组件实例
})
}
路由缓存(缓存 更新组件)
keep-alive
// 全部缓存
<keep-alive>
<router-view></router-view>
</keep-alive>
includes
// 缓存单个指定路由
<keep-alive includes="缓存页面的name">
<router-view></router-view>
</keep-alive>
// 如果有多个用,隔开
<keep-alive exclude="缓存页面的name">
<router-view></router-view>
</keep-alive>
// 缓存多个指定路由
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
// 使用两个router-view标签作为缓存和不缓存的路由出口,在路由配置的时候,只需要给要缓存的页面加上meta属性,然后再添加keepAlive属性设置为true。
{
path: '/text/',
name: 'Text',
component: Text,
// true 缓存 false 不缓存
meta: {keepAlive: true}
}
<template>
<div>
<p>home</p>
</div>
</template>
<script>
export default {
name: 'home',
data () {
return {
};
},
activated () {
console.log('进入组件');
},
deactivated () {
console.log('l离开组件');
}
}
</script>
<style scoped>
</style>
插槽
使用
插槽文件:
<template>
<div>
<div>头部</div>
<div>主体部分
// 匿名插槽
<slot>
<h1>匿名插槽</h1>
</slot>
//具名插槽
<slot name="solt1">
<h1>具名插槽</h1>
</slot>
</div>
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
使用:
<template>
<div id="app">
<dialog-win>
// 匿名插槽
<template v-slot:default>
<h1>匿名插槽1</h1>
</template>
// 具名插槽的两种使用方式
// 1. 插入 name="solt1" 的插槽中
<template slot="solt1">
<h1>具名插槽1</h1>
</template>
// 2. 插入 name="solt1" 的插槽中
<template v-slot:xujiang>
<h1>具名插槽2</h1>
</template>
</dialog-win>
</div>
</template>
<script>
import DialogWin from './components/DialogWin.vue'
export default {
name: 'App',
components: {
DialogWin
}
}
</script>
传参
和props一样传参,使用时通过在template上添加 slot-scope="scope"
来使用,scope 是接收传递的所有数据。
父组件
<template>
<Son>
<template slot-scope="scope"> //scope 所有数据
{{scope.data}} //'1'
</template>
<template slot="slots" slot-scope="scope"> //具名插槽
{{scope.msg}} //'2'
</template>
</Son>
</template>
<script>
import Child from '@/components/Child'
export default {
componens:{Child}
}
</script>
子组件
<template>
<div>
<slot :data="data"></slot> //自定义变量
<slot name="solts" :msg="abc"></slot> //具名插槽
</div>
</template>
<script>
export default {
name:'Child',
data(){
return{
data:'1'
abc:'2'
}
},
}
</script>
vuex
Vuex是一个专为 Vue.js 应用程序开发的状态管理模式 + 库
。
使用:
-
在项目中安装
vuex
依赖;npm install vuex --save
-
在src目录下新建一个
store
文件夹,并在store目录下新建index.js
,使用vuex;import Vue from ‘vue’
import Vuex from ‘vuex’
//使用vuex
Vue.use(Vuex)
//导出store
export default new Vuex.Store({}) -
在main.js中引入store,然后全局注入一下,这样就可以在任何一个组件里面使用它了:
import Vue from 'vue' import App from './App' import router from './router' //引入store import store from './store' new Vue({ el: '#app', router, //注册store store, components: { App }, template: '<App/>' })
-
使用
vuex的整体流程:
1. 在组件内部,通过dispatch来分发action。
2. 再通过action来第调用mutation
3. 触发mutation内部的commit来修改state
4. 改变state,导致页面重新render。
vuex的使用场景:
1. 全局共享的data,例如全局消息提醒、控件权限控制等等。
2. vuex可配合sessionStorage做用户基本信息的持久化存储。
3. 多级组件的数据需要共享或多个页面之间的数据存在因果关系时,可以使用vuex。
state
定义存储数据。
state: {
user : {
name:'HerayChen', // 使用 this.$store.state.user.name
age: 18
}
},
辅助函数 mapState
<script>
import { mapState } from "vuex";
export default {
name: "Home",
computed: {
...mapState(["user"]) // 使用 {{ user.name }}
}
}
</script>
getters
相当于是vue中的计算属性computed。
// 使用 {{ $store.getters.info }}
info(state) {
return `name: ${state.user.name} ,age: ${state.user.age}`
}
辅助函数 mapState
<script>
import { mapState } from "vuex";
export default {
name: "Home",
computed: {
...mapState(["info"]) // 使用 {{ info }}
}
}
</script>
mutations
修改state里面的数据。
vuex中:
mutations: {
// state 就是 state里面的数据 , payload就是载荷修改的值(通常是一个对象)
updated(state, payload) {
state.user = payload
},
}
使用:
// 调用mutations中updated的方法,修改state中user的name和age
this.$store.commit("updated", {
name: "1",
age: 18,
});
actions
发送异步请求,提交给mutations
。
步骤:
-
提交actions异步更新;
// 调用vuex中的 actions对象 下的 getName方法 this.$store.dispatch('getName')
-
发送异步请求(只读);
state: { name: 'HerayChen' }, mutations : { // mutations 只能传递一个载荷, // 多个参数应该放在对象中 updateName(state, obj) { // 修改数据 state.name = obj; } }, actions: { getName() { axios({ url: '127.18.0.0.1' method: 'get' }).then(res = > { // 执行第三步 // 提交 mutations (将异步转为同步) context.commit('updateName', res.data.data) }) } }
-
提交 mutations ;
-
修改数据。
modules
分模块使用
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
// 模块单独导入
modules: {
moduleA,
moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
vuex存储用户权限与服务器时间差值怎么算
服务器传递过来的时间和计算机的系统时间比对。
vue Loader
作用:解析和转换 .vue 文件,提取出其中的逻辑代码 script、样式代码 style、以及 HTML 模版 template,再分别把它们交给对应的 Loader 去处理。
vue-loader 会解析.vue文件,提取每个语言块,如有必要会通过其它 loader 处理,最后将他们组装成一个 CommonJS 模块,module.exports 出一个 Vue.js 组件对象。
webPack loader
- 对模块源代码进行转换,在使用 import或者加载 模块时候 进行预处理文件;
- 将文件不同语言入TS 转化为 Jacascript,或者将内敛图像转化为 dataURL 。可以运允许你在 js文件中 importCSS。
大文件秒传
大文件的上传方式:
- 秒传;
- 断点续传;
- 分片上传。
秒传
当要上传的东西时,服务器会先做MD5验证。如果服务器上有同样的东西,它会直接给你一个新地址。事实上,你下载的都是服务器上的同一个文件。如果想秒传,其实只要改MD5就行了,只需要修改文件本身(不能改名字)。例如,如果您在文本文件中添加几个单词,MD5将会改变,并且不会在几秒钟内通过。
核心逻辑:
1. 使用redis的set方法存储文件上传状态,其中key为文件上传的md5,
value为上传是否完成的标志位。
2. 当标志位为真表示上传已经完成,如果此时上传的是同一文件,则进入
第二传输逻辑。如果标志位为假,则表示上传尚未完成。此时需要调用
set方法保存块号文件记录的路径,key为上传的文件md5添加固定前缀,
value为块号文件记录的路径。
分片上传
分段上传是指将要上传的文件按照一定的大小划分成多个数据块(我们称之为Part)单独上传。上传后,服务器会将所有上传的文件汇总整合成原始文件。
使用场景:
1.上传大文件。
2.网络环境不好,有需要重传风险的场景。
超文本传送协议(Hyper Text Transport Protocol的缩写)
断点续传
断点续传是在下载或上传时人为地将下载或上传任务(一个文件或一个压缩包)分成几个部分,每个部分由一个线程上传或下载。如果出现网络故障,您可以从已经上传或下载的部分继续上传或下载未完成的部分,而不必从头开始上传或下载。本文的断点续传主要针对断点上传场景。
核心逻辑:
- 在片段上传过程中,如果因为系统崩溃或网络中断等异常因素导致上传中断,客户端需要
记录上传进度
。以后支持再次上传时,可以从上次上传中断的地方继续上传; - 为了避免上传后删除客户端的进度数据而导致从头开始重新上传的问题,服务器还可以提供相应的接口,方便客户端查询上传的分片数据,让客户端知道上传的分片数据,从下一个分片数据继续上传。
实现流程:
方案一
- 将待上传的文件按照一定的划分规则划分为大小相同的数据块;
- 初始化一个片段上传任务,并返回该片段上传的唯一标识符;
- 按照一定的策略(串行或并行)发送每个分片的数据块;
- 发送后,服务器判断数据上传是否完成,如果完成,则合成数据块,得到原始文件。
方案二
- 前端(客户端)需要按照固定的大小对文件进行切片,后端(服务器)应该用切片序列号和大小进行请求。
- 在服务器上创建一个conf文件来记录块的位置。conf文件的长度是块的总数。上传的每个块都用127写入,因此未上传的位置默认为0,上传的位置为字节。MAX_VALUE 127(这一步是实现断点续传和二次传输的核心步骤)。
- 服务器根据请求数据中给定的片段号和每个片段的块大小(片段大小固定且相同)计算起始位置,并将其与读取的文件片段数据一起写入文件。
大文件秒传是怎么实现的
所谓的"秒传"其实是同一个文件防止重复上传,核心逻辑是服务端会对文件做md5校验。判断hash是否上传过,有且完整,秒传,有但不完整,断点,没有切片上传
大文件上传实现“秒传”实际上是对文件内容生成hash,在切片并发上传之前将这个文件hash请求到服务端,服务端判断这个hash已经存在,则说明这个文件已经被上传到服务侧了,即使文件名不同,根据其内容生成的hash是准确的,这时候告诉前端无须上传(服务端已经有这个文件了),即实现了秒传。
实现过程:
1)前端计算好文件md5,并将md5赋值给文件唯一标识。
2)前端向后端发送一个get请求,携带md5值,询问后端是否该秒传文件。
3)验证md5,其实就是查数据库,如果有,则返回秒传标识,如果没有,则按正常上传流程继续上传文件。
4)上传完成后,将该文件md5值记录到数据表中,以便下次验证md5。
vue项目流程
- 脚手架项目构建;
- 引入必要插件 代码规范 提交规范 常用组件库组件。
如何划分结构和组件
组件:
- 业务组件;
- 公共组件。
前端性能优化方案
对于大型应用程序 我们可以使用 lightHouse 灯塔进行性能测试 测试报告。
网络层
- 构建策略:基于构建工具(Webpack/Rollup/Parcel/Esbuild/Vite/Gulp)
1. 减少打包时间:缩减范围、缓存副本、定向搜索、提前构建、并行构建、可视结构 ; 2. 配置include/exclude缩小Loader对文件的搜索范围; 3. 分割各个模块代码,提取相同部分代码,好处是减少重复代码的出现频率; 4. 删除项目中未被引用代码,好处是移除重复代码和未使用代码; 5. 将路由页面/触发性功能单独打包为一个文件,使用时才加载,好处是减轻首屏渲染的负担; 6. 压缩HTML/CSS/JS代码,压缩字体/图像/音频/视频,好处是更有效减少打包体积:分割代码、摇树优化、动态垫片、按需加载、作用提升、压缩资源.
- 图像策略:基于图像类型(JPG/PNG/SVG/WebP/Base64)
1. 根据实际颜色选择色深、压缩; 2. 使用css精灵技术(合并图片通过`background-position `来精确定位); 3. 不在html中拉伸图片
- 分发策略:基于内容分发网络(CDN)
所有静态资源走CDN,
缓存策略:基于浏览器缓存(强缓存/协商缓存)
缓存策略通过设置HTTP报文实现,在形式上分为强缓存/强制缓存和协商缓存/对比缓存。
渲染层和网络层:
1. CSS策略:基于CSS规则
2. DOM策略:基于DOM操作
3. 阻塞策略:基于脚本加载
4. 回流重绘策略:基于回流重绘
5. 异步更新策略:基于异步更新
如何提升首屏加载速度
静态资源、第三方包可采用cdn,prefetch preload,tree sharking ,DllPugin,SplitChunksPlugins, polyfill,gzip,骨架屏, 减少dom重绘回流, CDN, minimize, preload 路由图片懒加载,组件抽离,服务端渲染所有数据请求和 html内容已在服务端处理完成,浏览器收到的是完整的 html 内容,首页加载更快。
SPA(单页面)模式下的SSR(服务端渲染)的核心是同构
:路由同构,预取同构,渲染同构。
像动态页面缺点其实就是你首屏还是需要去做一次异步请求拉一下数据再去渲染就会产生一个页面抖动。
ssr处理其实就是在我们建立完TCP连接发起http请求之后网关层面做一些转发的时候我们可以在这时候由服务器去拉取数据,返回已经有数据的html页面。由于服务器之间的请求更加稳定高效所以效果其实是很好的,但是有个问题就是服务器去拉数据的话还是有可能失败的超时的,所以前端还是需要做一个补偿,例如onload后再去异步请求数据。还有就是这个服务端的拉取数据的方式利用redis缓存的话平均耗时要比http请求快10倍左右。
数据渲染太慢,性能差怎么优化
使用createDocumentFragment和requestAnimationFrame。
setTimeout(() => {
// 插⼊⼗万条数据
const total = 100000; // ⼀次插⼊ 20 条,如果觉得性能不好就减少
const once = 20;
// 渲染数据总共需要⼏次
const loopCount = total / once;
let countOfRender = 0;
let ul = document.querySelector("ul");
function add() {
// 优化性能,插⼊不会造成回流
const fragment = document.createDocumentFragment();
for (let i = 0; i < once; i++) {
const li = document.createElement("li");
li.innerText = Math.floor(Math.random() * total);
fragment.appendChild(li);
}
ul.appendChild(fragment);
countOfRender += 1;
loop();
}
function loop() {
if (countOfRender < loopCount) {
window.requestAnimationFrame(add);
}
}
loop();
}, 0);
渲染优化是怎么来衡量的【为处理】
SPA和SSR
SPA(单页面)
SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称 CSR。SPA 所需的资源,如 HTML、CSS 和 JS 等,会在一次请求中就加载完成。
优点:
- 页面切换速度快(页面每次切换跳转时,并不需要向服务端请求 HTML 文件,这样就节约了很多网络延时,所以我们在切换页面的时候速度很快。其次页面内容的改变一般也不需要重新加载整个页面,避免了不必要的跳转和重复渲染);
- 可以减轻服务器端压力(因为服务器先将一份包含了静态资源、JavsScript 和模板的静荷数据(payload)发送到了客户端,之后客户端只需要获取渲染页面或视图所需要的数据即可)。
缺点:
- 首屏加载时间过长(SPA 需要在用户进入网站时将 JavaScript、CSS 等资源一次性全部加载,这样就会导致首屏加载时间过长,影响用户体验);
- 增加浏览器内存占用(客户端渲染会频繁绘制页面,导致浏览器内存占用增加,浪费内存);
- 不利于 SEO(SEO(Search Engine Optimization)也就是 搜索引擎优化。SPA 所有的内容都是在一个页面中通过 JavaScript 动态生成,然后替换显示的,而搜索引擎只能抓取真实 HTML 里的内容,后期动态渲染生成的内容,搜索引擎是无法抓取到的)。
SSR(服务端渲染)
SSR(Server Side Render)即 服务端渲染。一般也称为 多页面应用(Mulpile Page Application),简称 MPA。
和传统的SPA的区别:传统的SPA是服务端返回客户端(浏览器)空的html,由js动态生成页面,而SSR是提前在服务端将html生成好,直接返回给客户端,好处就是降低白屏时间。
原理: SSR会将页面通过服务器生成html字符串,然后发送到浏览器,服务器渲染能够更有利于SEO和首屏渲染时间,但是会给服务器带来压力。
优点:
- 首屏加载快(首屏时间,就是页面首个屏幕的内容展现的时间,当我们访问某个页面的时候,服务器会直接返回一个 HTML,而不会像 SPA 一样返回跟当前页面无关的内容,这样所请求的页面就会迅速展示出来)
- 有利于 SEO(服务端渲染返回给客户端的是已经渲染好的最终 HTML,搜索引擎可以很方便的抓取到页面信息)。
缺点:
- Web 服务器配置相对麻烦(SPA 只需要一个静态文件服务器,而 SSR 需要服务器安装 node.js 运行一个服务);
- 需要更多的服务器资源(由于页面的渲染都是由服务端渲染。会给服务器造成一定压力,尤其是高并发访问的情况)。
Vue多语言(i18国际化)
在vue中实现多语言通常是使用i18
国际化。
i18国际化的使用:
- 通过vue-cli脚手架创建项目;
1. 安装node 2. 安装vue-cli脚手架(npm install -g @vue/cli) 3. 通过脚手架创建项目(vue create vueDemo)
- 运行项目(npm run serve);
- 安装i18国际化(npm i vue-i18n --save);
- 在src下创建i18文件夹存储多语言代码;
- 在i18文件夹下创建locale文件夹,放置语言包【语言包文件一般是json格式的】【zh-CN.json、en.json】;
zh-CN.json
en.json{ "message": "你好世界" }
{ "message": "Hello Word" }
- 在i18文件下创建
index.js
,创建i18实例;import Vue from 'vue'; import VueI18n from 'vue-i18n'; // 导入语言包信息 import zhCN from "./locale/zh-CN.json"; import en from "./locale/en.json"; // 注册i18n Vue.use(VueI18n); // 语言包根据语言环境分类 const messages = { en, "zhCN": zhCN, } // 通过选项创建 VueI18n 实例并导出 export default VueI18n({ locale: localStorage.getItem('language') || 'zhCN', // 设置当前语言环境,默认中文简体 messages, // 设置语言环境对应信息 })
- 在
main.js
中将i18n注册到Vue实例中;import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' import i18n from './i18n';// 引入i18n实例 new Vue({ router, store, i18n, // 将i18挂载到实例中 render: h => h(App) }).$mount('#app')
- 在组件中使用。
使用的时候是通过使用i18n挂载后的全局$t方法来使用的。<template> <div id="app"> <!-- 切换语言 --> <label for>选择语言:</label> <select v-model="locale"> <option v-for="(locale, index) in locales" :key="index" :value="locale.value" >{{ locale.label }}</option> </select> <!-- 渲染对应语言文件内容 --> <h1>{{ $t('message') }}</h1> </div> </template> <script> export default { data() { return { locale: this.$i18n.locale, //使用i18n实例默认定义的语言 locales: [ { value: "en", label: "英文" }, { value: "zh-CN", label: "中文简体" } ] }; }, watch: { // 监听语言环境,动态修改对应语言 locale(val) { this.$i18n.locale = val; localStorage.setItem("language", val); } }, mounted() { console.log(this.$t()); } }; </script>
具体在项目中使用的时候还要处理接口数据的多语言,一般会通过axios的interceptor请求拦截给header头加上 Accept-Language 来处理。
- 安装axios ( npm install axios --save-dev);
- 请求拦截
const instance = axios.create({ // 超时时间 timeout: 5000, // 服务器路径 baseURL: 'https://xxxxxxxxx/xxxx/', }); // 请求拦截 instance.interceptors.request.use( function(config) { // 在发送请求之前做些什么 config.headers['accept-language'] = localStorage.getItem('language'); return config; }, function(error) { return Promise.reject(error); } );
Vue2和Vue3虚拟Dom优化
vue3的跳过静态节点处理。
vue2和vue3的区别
- 双向数据绑定的原理;
vue2:使用Object.definepropert() 对数据进行劫持 + 发布订阅 vue3:使用proxy API ,defineProperty只能监听某个属性,不能对全对象进行监听;可以省去for in 、闭包等内容来提升效率(直接绑定整个对象即可);可以监听数组,不用再去单独的对数组做特异性操作,vue3可以检测到数组内部数据的变化
- vue3可以支持多根节点;
vue2一个组件只支持一个根节点
- api;
vue2:选项类型api(data,computed,methods等) vue3:合成型api(setup)
- data;
vue2是把数据放入data中,vue3就需要使用一个新的setup()方法,此方法在组件初始化构造得时候触发。使用一下三个步骤来简=建立反应性数据: 1. 从vue引入reactive;使用reactive() 方法来声明数据为响应性数据;3. 使用setup()方法来返回我们得响应性数据,从而template可以获取这些响应性数据。
- 生命周期;
vue2 --------------- vue3 beforeCreate -> setup() Created -> setup() beforeMount -> onBeforeMount mounted -> onMounted beforeUpdate -> onBeforeUpdate updated -> onUpdated beforeDestroyed -> onBeforeUnmount destroyed -> onUnmounted activated -> onActivated deactivated -> onDeactivated
- 传参。
1. setup()函数接收两个参数:props、context(包含attrs、slots、emit) 2. setup函数是处于生命周期beforeCreated和created俩个钩子函数之前 3. 执行setup时,组件实例尚未被创建(在setup()内部,this不会是该活跃实例得引用,即不指向vue实例,Vue为了避免我们错误得使用,直接将setup函数中得this修改成了undefined) 4. 与模板一起使用时,需要返回一个对象 5. 因为setup函数中,props是响应式得,当传入新的prop时,它将会被更新,所以不能使用es6解构,因为它会消除prop得响应性,如需解构prop,可以通过使用setup函数中得toRefs来完成此操作。 6. 父传子,用props,子传父用事件 Emitting Events。在vue2中,会调用this$emit然后传入事件名和对象;在vue3中得setup()中得第二个参数content对象中就有emit,那么我们只要在setup()接收第二个参数中使用分解对象法取出emit就可以在setup方法中随意使用了。 7. 在setup()内使用响应式数据时,需要通过 .value 获取 ```js import { ref } from 'vue' const count = ref(0) console.log(count.value) ``` 8. 从setup() 中返回得对象上得property 返回并可以在模板中被访问时,它将自动展开为内部值。不需要在模板中追加.value。 9. setup函数只能是同步的不能是异步的。
vue3响应式的优点
- 响应式;
可以监听动态新增的属性; 可以监听删除的属性 ; 可以监听数组的索引和 length 属性;
- 编译优化;
跳过静态节点,直接对比动态节点,缓存事件处理函数; vue.js 3.x中标记和提升所有的静态节点,diff的时候只需要对比动态节点内容;
- 源码体积;
移除了一些不常用的api
vue3的自定义指令
<script>
export default{
setup() {},
directives: {
// 指令名
focus: {
// 生命周期
mounted(el) {
// 处理DOM的逻辑
el.focus();
},
}
}
}
</script>
<template>
<input v-focus />
</template>
pinia和vuex,pinia的持久化
import { defineStore } from 'pinia'
// 1. 创建store
// 参数1:store的唯一表示
// 参数2:对象,可以提供state actions getters
const useCounterStore = defineStore('counter', {
state: () => {
return {
count: 0,
}
},
getters: {
double() {
return this.count * 2
},
},
actions: {
increment() {
this.count++
},
incrementAsync() {
setTimeout(() => {
this.count++
}, 1000)
},
},
})
export default useCounterStore
使用:
<h1>根组件---{{ counter.count }}</h1>
<h3>{{ counter.double }}</h3>
Vue3新增的 api
- set up
- composition api
- suspanse
- telepolent
- $emit
Vue3优势
- vm实例更轻量;
- 使用composition api 比如 refs reactive toRefs toreactive raws suspanse telempolt 等等;
- 打包体积小 ;
数据大屏
- 修饰框架dataV
- 媒体查询页面适配;
- PostCSS
Webpack插件配置
var path = require('path');
module.exports = {
context: path.join(__dirname, 'app'),
// 入口
entry: './app',
// 输出
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
// 配置如何处理模块
module: {
// 在webpack的打包流程中,获取的入口文件以及相关的文件之后,帮助识别原本webpack不能识别的代码
loaders: [
{
// 命中 CSS 文件
test: /\.css$/,
loader: "style-loader!css-loader!postcss-loader"
}
]
},
postcss: function () {
// autoprefixer 自动管理浏览器前缀的插件,解析CSS文件并且添加浏览器前缀到CSS内容里
return [require('autoprefixer')];
}
}
webpack
通过commonjs生成bundle包;
vite将一切东西转换为esm。
如何全局引入样式
使用sass-resources-loader或者style-resources-loader等webpack插件。
优化首屏加载速度
-
区分环境(process.env.NODE_ENV === ‘production’);
-
压缩代码(UglifyJsPlugin、ParallelUglifyPlugin插件)
插件配置const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); module.exports = { plugins: [ // 压缩输出的 JS 代码 new UglifyJSPlugin({ compress: { // 在UglifyJs删除没有用到的代码时不输出警告 warnings: false, // 删除所有的 `console` 语句,可以兼容ie浏览器 drop_console: true, // 内嵌定义了但是只用到一次的变量 collapse_vars: true, // 提取出出现多次但是没有定义成变量去引用的静态值 reduce_vars: true, }, output: { // 最紧凑的输出 beautify: false, // 删除所有的注释 comments: false, } }), ], };
-
CDN 加速(资源分类挂在到不同的cdn服务)
CDN 又叫内容分发网络,通过把资源部署到世界各地,用户在访问时按照就近原则从离用户最近的服务器获取资源,从而加速资源的获取速度。静态资源的导入 URL 需要变成指向 CDN 服务的绝对路径的 URL 而不是相对于 HTML 文件的 URL。
静态资源的文件名称需要带上有文件内容算出来的 Hash 值,以防止被缓存。
不同类型的资源放到不同域名的 CDN服务上去,以防止资源的并行加载被阻塞。const path = require('path'); const ExtractTextPlugin = require('extract-text-webpack-plugin'); const {WebPlugin} = require('web-webpack-plugin'); module.exports = { // 省略 entry 配置... output: { // 给输出的 JavaScript 文件名称加上 Hash 值 filename: '[name]_[chunkhash:8].js', path: path.resolve(__dirname, './dist'), // 指定存放 JavaScript 文件的 CDN 目录 URL publicPath: '//js.cdn.com/id/', }, module: { rules: [ { // 增加对 CSS 文件的支持 test: /\.css$/, // 提取出 Chunk 中的 CSS 代码到单独的文件中 use: ExtractTextPlugin.extract({ // 压缩 CSS 代码 use: ['css-loader?minimize'], // 指定存放 CSS 中导入的资源(例如图片)的 CDN 目录 URL publicPath: '//img.cdn.com/id/' }), }, { // 增加对 PNG 文件的支持 test: /\.png$/, // 给输出的 PNG 文件名称加上 Hash 值 use: ['file-loader?name=[name]_[hash:8].[ext]'], }, // 省略其它 Loader 配置... ] }, plugins: [ // 使用 WebPlugin 自动生成 HTML new WebPlugin({ // HTML 模版文件所在的文件路径 template: './template.html', // 输出的 HTML 的文件名称 filename: 'index.html', // 指定存放 CSS 文件的 CDN 目录 URL stylePublicPath: '//css.cdn.com/id/', }), new ExtractTextPlugin({ // 给输出的 CSS 文件名称加上 Hash 值 filename: `[name]_[contenthash:8].css`, }), // 省略代码压缩插件配置... ], };
-
使用 Tree Shaking(删除 JavaScript 中用不上的死代码,只能删除ES6 模块化语法);
-
提取公共代码(公共代码common.js,基础库base.js);
-
按需加载(用的时候再引用)。
lerna
多包管理工具(多个package js项目 包管理, 解决packages之间的依赖关系, 项目中只用来发布)。