为了可以找到一个好的实习工作,今天开始复习自己所有学到的前端方面的知识。
此贴记录自己的每日学习状态和打卡记录。
基础篇
1.说一下你知道的flex属性
采用 Flex 布局的元素,称为 Flex 容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为 Flex 项目(flex item),简称"项目"。
容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。
以下6个属性设置在容器上
- flex-direction 属性决定主轴的方向(即项目的排列方向)。
- flex-wrap 默认情况下,项目都排在一条线(又称"轴线")上。
flex-wrap
属性定义,如果一条轴线排不下,如何换行。 - flex-flow
flex-flow
属性是flex-direction
属性和flex-wrap
属性的简写形式,默认值为row nowrap
- justify-content 属性定义了项目在主轴上的对齐方式。
- align-items 属性定义项目在交叉轴上如何对齐。
- align-content 属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。
以下6个属性设置在项目上。
order 属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。
flex-grow 属性定义项目的放大比例,默认为
0
,即如果存在剩余空间,也不放大。flex-shrink 属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。
flex-basis 属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。
flex
flex
属性是flex-grow
,flex-shrink
和flex-basis
的简写,默认值为0 1 auto
。后两个属性可选。align-self 属性允许单个项目有与其他项目不一样的对齐方式,可覆盖
align-items
属性。
2.css的两种盒模型
盒模型有两种, IE 怪异盒子模型、W3C 标准盒子模型;
盒模型是由: 内容(content)、内边距(padding)、边框(border)、外边距(margin) 组成的。
标准模型的宽高是指的 content 区宽高; IE 盒模型的宽高是指的 content+padding+border 的宽高。
如何设置这两种盒模型:
box-sizing: content-box; //标准盒模型
box-sizing: border-box; //ie盒模型
3.BFC
BFC(Block formatting context)直译为"块级格式化上下文"。它是一个独立的渲染区域,只有 Block-level box 参与, 它规定了内部的 Block-level Box 如何布局,并且与这个区域外部毫不相干。
BFC 作用:
-
利用 BFC 避免外边距折叠
-
清除内部浮动 (撑开高度)原理: 触发父 div 的 BFC 属性,使下面的子 div 都处在父 div 的同一个 BFC 区域之内
-
避免文字环绕
-
分属于不同的 BFC 时,可以阻止 margin 重叠
-
多列布局中使用 BFC
如何生成 BFC:(脱离文档流,满足下列的任意一个或多个条件即可)
-
根元素,即 HTML 元素(最大的一个 BFC)
-
float 的值不为 none
-
position 的值为 absolute 或 fixed
-
overflow 的值不为 visible(默认值。内容不会被修剪,会呈现在元素框之外)
-
display 的值为 inline-block、table-cell、table-caption
BFC 布局规则:
-
内部的 Box 会在垂直方向,一个接一个地放置。
-
属于同一个 BFC 的两个相邻的 Box 的 margin 会发生重叠
-
BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此, 文字环绕效果,设置 float
-
BFC 的区域不会与 float box 重叠。
-
4.git指令
git init // 初始化第一次添加项目 git status // 查看当前目录源代码(红色) git add . // 添加到暂存区 git status // 查看当前目录源代码(绿色) git commit -m " msg" // 把暂存区中所有代码添加到本地仓库中去 git branch // 查看当前分支 git checkout master // 切换到主分支 git merge login // 将login中分支合并到主分支 git push // 将本地最新主分支推送到码云 git check login git push -u origin login //(第一次) 将本地的login分支推送到云端origin中并以login为名保存 git branch git checkout -b rights // 切换到一个新分支命名为rights分支 git push -u origin rights // 第一次将rights分支推送带云端
/* 第一步 git init 第二步 打开码云 新建一个仓库 注意只选名称 公开 三个对勾都不点 跳转到新页面有 已存在中1> 2> 3>三个命令 第三步 在项目中 (1)git status (2)git add . (3)git commit -m "add file" (4)git status 第四步 复制第二步 2> 3> */
5.对象拷贝
浅拷贝三种方式:1.使用for/in
执行对象拷贝 2.Object.assign 3.使用展示语法也可以实现浅拷贝
深拷贝:
let obj1={name:"kang",user:{age:18}}
function deepcopy(obj){
let newobj=obj instanceof Array?[]:{};
for (const [k,v] of Object.entries(obj)) {
newobj[k]=typeof v=="object"?deepcopy(v):v;
}
return newobj;
}
let obj2=deepcopy(obj1);
6.原型链
原型链是什么?
JavaScript万物都是对象,对象和对象之间也有关系,并不是孤立存在的。对象之间的继承关系,在JavaScript中是通过prototype对象指向父类对象,直到指向Object对象为止,这样就形成了一个原型指向的链条。
1> 每一个构造函数都是 由 FUNCTION 构造函数创建的
2> 每一个构造函数都有一个 prototype 的属性,指向原型对象,原型的对象的作用是共享方法
3> 每一个对象都有自己的 _proto_属性,用来指向构造函数prototype的原型对象
4> 每一个原型对象都有constructor 的属性,来指向构造函数的对象
5> _ _proto_ _对象原型和原型对象prototype是等价的
Object.setPrototypeOf
可设置对象的原型,
Object.getPrototypeOf
用于获取一个对象的原型
let obj={};
obj.__proto__===Object.getPrototypeOf(obj); //true
7.继承
Es6之前:
//1.原型链继承
//缺点:(1)包含引用类型值的原型属性会被所有实例共享,这会导致对一个实例的修改会影响另一个实例;
// (2)在创建子类型的实例时,不能向超类型的构造函数中传递参数。
function person(name, age) {
this.name = name;
this.age = age;
}
person.prototype.say = function () {
console.log("hi,i am a person");
}
function student(qq) {
this.qq = qq;
}
student.prototype = new person();
var student1 = new student("kxz", 18, 82007);
student1.say();//hi,i am a person
student1.name; //undefined
//2.借用构造函数实现继承
//缺点:(1)无法继承父类原型对象上的属性和方法
//
function person(name, age) {
this.name = name;
this.age = age;
}
person.prototype.say = function () {
console.log("hi,i am a person");
}
function student(name, age, qq) {
person.call(this, name, age)
this.qq = qq;
}
var student1 = new student("kxz", 18, 82007);
student1.name; //kxz
//3.组合继承(伪经典继承)
//缺点:(1)会调用两次超类型构造函数,一次是在创建子类型原型的时候,一次是在子类型构造函数的内部
//
function person(name, age) {
this.name = name;
this.age = age;
}
person.prototype.say = function () {
console.log("hi,i am a person");
}
function student(name, age, qq) {
person.call(this, name, age)
this.qq = qq;
}
student.prototype = new person();
student.prototype.constructor = student;
var student1 = new student("kxz", 18, 82007);
student1.name; //kxz
student1.say();//"hi,i am a person
//4.原型式继承
//缺点:如果还有另外一个对象关联了person,anotherPerson修改数组friends的时候,也会体现在这个对象中。
//
var person = {
name: 'Gaosirs',
friends: ['Shelby', 'Court']
}
var anotherPerson = object(person)//var anotherPerson = Object.create(person)
console.log(anotherPerson.friends) // ['Shelby', 'Court']
//5.寄生式继承
//增强了原型式继承
//
function createAnother(o) {
var clone = Object.create(o) // 创建一个新对象
clone.sayHi = function () { // 添加方法
console.log('hi')
}
return clone // 返回这个对象
}
var person = {
name: 'GaoSirs'
}
var anotherPeson = createAnother(person)
anotherPeson.sayHi()
//6.寄生组合式继承
//
//
function jisheng(person, student) {
var _prototype = Object.create(person.prototype); // 创建对象
_prototype.constructor = person;// 增强对象
student.prototype = _prototype; // 指定对象
}
function person(name, age) {
this.name = name;
this.age = age;
}
person.prototype.say = function () {
console.log("hi,i am a person");
}
function student(name, age, qq) {
person.call(this, name, age)
this.qq = qq;
}
jisheng(person, student);
var student1 = new student("kxz", 18, 82007);
student1.name; //kxz
student1.say();//"hi,i am a person
Es6之后在class中继承
类其实是函数
class User {
}
console.log(typeof User); //function
使用 constructor
构造函数传递参数,下例中show
为构造函数方法,getName
为原型方法
constructor
会在 new 时自动执行
class User {
constructor(name) {
this.name = name;
this.show = function() {};
}
getName() {
return this.name;
}
}
const xj = new User("向军大叔");
console.log(xj);
类中定义的函数,保存在其原型上
//所以下面定义的类
class User {
constructor(name) {
this.name = name;
}
show() {
console.log(this.name);
}
}
//与下面使用函数的定义是一致的
function User(name) {
this.name = name;
}
Hd.prototype.show = function() {
console.log(this.name);
};
继承
class Person {
constructor(name) {
this.name = name;
}
show() {
return `后盾人会员: ${this.name}`;
}
}
class User extends Person {
constructor(name) {
super(name);
}
run() {
return super.show();
}
}
const xj = new User("向军");
console.dir(xj.run());
8.this指向
1> 调用方式的不同决定了this的指向不同:
普通函数调用 this指向window
构造函数调用 this指向实例对象,原型对象里的方法也指向实例对象
对象方法调用 this指向该方法所属对象
事件绑定方法 this指向绑定事件对象
定时器函数 this指向window
立即执行函数 this指向window
2> 函数内部的this指向可通过bind(),call(),apply()来处理
call()方法: fun.call(thisArg,arg1,arg2,...);作用调用函数,并改变函数内部this指向,可以实现继承。
apply()方法:fun.call(thisArg,[argsArray])作用调用函数,并改变函数内部this指向,方便用于数组操作。
bind()方法:fun.bind(thisArg,arg1,arg2,...)不会调用函数,但是能改变函数内部的this指向,返回的是改变this之后的新函数。可用于改变定时器内部的this指向。
3> 使用箭头函数;
4> 在函数内部使用`_this=this;`
9.new操作符
new操作符新建了一个空对象,这个对象原型指向构造函数的prototype,执行构造函数后返回这个对象
1、创建一个空的对象
2、链接到原型
3、绑定this指向,执行构造函数
4、确保返回的是对象
function _new(func,...args){
var obj=Object.create(func.prototype);
var res=obj.apply(func,args);
return res instanceof Object ? res : obj;
}
10.隐式转换
1.一元加运算符:+ 强制转化为数字类型
2.数字和其他类型相加,和布尔、null、undefined都是转为数字,和字符串相加数字转为字符串
3.字符串和其他类型相加,结果为字符串
4.null+undefined == NaN 两者都转数字, null转数字为0, undefined转数字为NaN
5.obj与其他类型运算的时候, 会调用valueOf方法在调用String(obj)的时候, 会调用toString()方法对象的比较简单来说就是转化成字符串之后再进行比较
11.事件循环机制macro micro
Macro Task (宏任务)和 Micro Task(微任务)
宏任务主要包含:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering。
微任务主要包含:Promise.then()、MutaionObserver、process.nextTick(Node.js 环境)
微任务跟在当前宏任务之后,代码执行到一个微任务就跟上,一个接一个
执行顺序:
1>当前脚本所有同步任务执行,遇到宏任务添加到宏任务,遇到微任务添加到微任务
2>执行当前脚本后的微任务
3>执行宏任务
如何区分宏任务和微任务呢?划分的标准是什么
宏任务本质:参与了事件循环的异步任务。
微任务本质:直接在 Javascript 引擎中的执行的,没有参与事件循环的异步任务。
宏任务,微任务的优先级
promise 是在当前脚本代码执行完后,立刻执行的,它并没有参与事件循环,所以它的优先级是高于 setTimeout。
console.log("后盾人");
setTimeout(function() {
console.log("定时器");
}, 0);
Promise.resolve()
.then(function() {
console.log("promise1");
})
.then(function() {
console.log("promise2");
});
console.log("houdunren.com");
//后盾人
//oudunren.com
//promise1
//promise2
//定时器
12.基础类型
简单数据类型(基本类型):字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol(ES6新增)。Bigint(新增)
Symbol 是 ES6 引入了一种新的原始数据类型,表示独一无二的值
复杂数据类型(引用数据类型):对象(Object)、数组(Array)、函数(Function)。日期(Date)
基本类型存储的是值,复杂类型存储的是地址(指针)。
当你创建了一个复杂类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。
当我们将变量赋值给另外一个变量时,复制的是原本变量的地址(指针),当我们进行数据修改的时候,就会修改存放在地址(指针) 上的值。
13.数据去重/交集/并集
const a = { fn: 1 };
const set = new Set([1, 1, 2, 2, 3, 4, 5, 5, 5, a, 'a']);
const b = new Set([6, 7, 8, 9, 5, 4, 3, 'a', 'v']);
const only = [...new Set([])];//去重
const union = new Set([...set, ...b]);// 并集
const intersect = new Set([...set].filter(x => b.has(x)));// 交集
14.判断是否为数组
//方式一 Array.isArray()
Array.isArray([123]);//true
//方式二 [].constructor===Array;
[1, 2, 3, 4, 5].constructor === Array; //true
//方式三 instanceof
[1, 2, 3, 4, 5] instanceof Array//true
//方式四 Object.prototype.toString()
Object.prototype.toString.call([1, 2, 3, 4]) === "[object Array]"; //true
15.变量声明提升
JS代码的执行过程分为两个阶段:
1.预解析阶段:进入某作用域时,js解析器会受限找到当前作用域中所有的变量和函数进行声明(提前在内存中为变量开辟好空间)
2.执行阶段: 执行当前作用域中的代码(遇到变量赋值则赋值)
1> 发现var 和function关键字进行声明
2> 当变量和function 同名时 函数名优先原则
Es6声明变量的六种方法:
Var命令、function命令、let命令、const命令、import命令、class命令
在es5中,var和function 声明的全局变量是顶层对象的属性
在es6中,let、const、class声明的全局变量不属于顶层对象的属性
let const变量声明提升过程:
1、预解析阶段:JS解析器找到该作用域中所用变量,进行变量声明(即在内存中开辟存储位置,但并不初始化)
2、执行阶段:对变量进行赋值等等。。。。
特点
1、let、const声明变量同样存在变量声明提升问题,但其有一个特性:暂时性死区,变量声明之前,不可调用该变量,否则会报错
2、let、const声明变量绑定声明时的作用域,这个特性其实为JS新增了块级作用域
3、作用域中不能重复声明,否则会报错,解决了变量名称冲突问题
16.函数作用域
js作为编程语言,最基本的功能是存储,只有把这个数据保存在内存中,才能参与计算和其他操作。而把数据保存到内存中并可顺利的取出来,需要规定一套机制或者说规则,在计算机中把对数据存储的规则叫作用域,而js的作用域是函数作用域,作用域是可以相互嵌套的但是不能重叠,外部无法访问内部作用域,内部可访问外部作用域。
(with、trycatch中的catch也是块级作用域,但是性能差,不推荐使用)
作用域的存在可以避免名称的污染,但是创建函数的过程中污染到了作用域,所以创建了立即执行函数
立即执行函数 iife:
开头加字符会将函数声明语句转为函数表达式
匿名函数运行的几种方式,方式一 开头()后边跟()运行,方式二 new,方式三 ! ,方式四 true&&,方式五 false||,方式六 +
iife作用
1.避免作用域下命名污染
2.对整个性能有提升,不需要到大作用域中查找
3.有利于压缩
4.避免全局命名的冲突和填坑
5.保存闭包状态
6.颠倒代码运行顺序
Es5中只有函数作用域和全局作用域,导致很多地方不合理例如1.内层变量可能会覆盖外层变量2.for循环中的i会泄露成全局变量
Es6中出现了块级作用域
Es6允许作用域的任意嵌套、但外层作用域无法读取内层作用域、内层作用域可以定义外层作用域的同名变量、
Es5规定不能再块级作用域中声明
es6中允许在块级作用域中声明函数,函数声明类似于var,即会提升到全局作用域或作用域头部,同时函数声明还会提升到所在的块级作用域的头部。
考虑到环境差异太大,应避免在块级作用域中声明函数,如果确实需要,应写成函数表达式的形式,而非函数声明语句
17.节流/防抖
防抖(debounce)
原理:事件响应函数在一段时间之后才执行。如果在这段时间内再次调用,则重新计算时间
实现注意:1.this 2.event对象
应用场景:1> scroll事件滚动触发 2>搜索框输入查询 3>表单验证 4> 按钮提交事件5>浏览器窗口缩放,resize事件
手写:
<div id="div1"></div>
<script>
var div1=document.getElementById("div1");
var count=0;
function doSome(e){
console.log(e);
count++;
console.log(count);
}
div1.onmousemove=debounce(doSome,200);
function debounce(func,wait){
let timeout;
return function(){
//1.修复函数内部指向问题
let that=this;
//2.修复了event指向问题
let arg=arguments;
clearTimeout(timeout);
timeout=setTimeout(function(){
func.apply(that,arg)
},wait);
}
}
</script>
节流(throttle)
原理:如果你持续触发事件,每隔一段时间,只执行一次事件
实现方式:1.使用时间戳 2使用定时器
应用场景:1> Dom元素的拖拽功能实现 2> 射击游戏 3> 计算鼠标移动距离 4>监听scrool滚动事件
手写:
//方式一:定时器
var div1 = document.getElementById("div1");
var count = 0;
function doSome() {
count++;
console.log(count);
}
div1.onmousemove = throttle(doSome, 2000);
function throttle(func, wait) {
let that, args, timeout;
return function () {
that = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(that, args);
}, wait);
}
}
}
//方式二:时间戳
var div1=document.getElementById("div1");
var count=0;
function doSome(){
count++;
console.log(count);
}
div1.onmousemove=throttle(doSome,2000);
function throttle(func,wait){
let that,args;
let old=0;
return function(){
that=this;
args=arguments;
let now=new Date().valueOf();
if(now-old>wait){
func.apply(that,args);
old=now;
}
}
}
18.call/apply和bind
call()方法: fun.call(thisArg,arg1,arg2,...);作用调用函数,并改变函数内部this指向,可以实现继承。
apply()方法:fun.call(thisArg,[argsArray])作用调用函数,并改变函数内部this指向,方便用于数组操作。
call与apply 用于显示的设置函数的上下文,两个方法作用一样都是将对象绑定到this,只是在传递参数上有所不同。
- apply 用数组传参
- call 需要分别传参
- 与 bind 不同 call/apply 会立即执行函数
bind()是将函数绑定到某个对象,比如 a.bind(hd) 可以理解为将a函数绑定到hd对象上即 hd.a()。
- 与 call/apply 不同bind不会立即执行
- bind 是复制函数形为会返回新函数
//手写call
var obj = {
name: "康心志"
}
function fn(a, b, c) {
console.log(a + b + c + this.name);
}
Function.prototype.mycall=function(obj){
if(typeof this !=='function'){
throw new TypeError("错误!");
}
var obj=obj||window;
let newArray=[...arguments].splice(1),
result;
obj.o=this;
result=obj.o(...newArray);
delete obj.o;
return result
}
fn.mycall(obj, "我", "的", "名字是");
//手写apply
var obj = {
name: "康心志"
}
function fn(a, b, c) {
console.log(a + b + c + this.name);
}
Function.prototype.myapply=function(obj,arr){
if(typeof this !=='function'){
throw new TypeError("错误!");
}
var obj=obj||window;
let result;
obj.o=this;
if(arr){
result=obj.o(...arr);
}else{
result=obj.o();
}
delete obj.o;
return result
}
fn.myapply(obj, ["我", "的", "名字是"]);
//手写bind
var obj = {
name: "康心志"
}
function fn(a, b, c, d) {
console.log(a + b + c + this.name + d);
}
Function.prototype.mybind = function (obj) {
if (typeof this !== 'function') {
throw new TypeError("错误!");
}
var obj = obj || window;
var newArray = [...arguments].splice(1);
var that = this;
//console.log(newArray);//["我", "的", "名字是"]
return function () {
//console.log(arguments);//[!]
var newArray2 = [...newArray, ...arguments];//["我", "的", "名字是", "!"]
that.apply(obj, newArray2);
}
}
fn.mybind(obj, "我", "的", "名字是")("!");
// fn.bind(obj, "我", "的", "名字是")("!");
19.let/const
- let和const声明的变量,只在块级作用域中有效,
- Let和const声明的变量不存在变量提升,只要在声明之前使用变量就会报错
- Let和const声明变量形成暂时性死区,暂时性死区的本质是只要进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用改变量。
- 不允许重复声明。Let不允许在相同的作用域内重复声明一个变量。
- Const声明的变量,只在块级作用域中有效,
- Const声明一个只读的常量
- Const本质不是变量的值不得改动,而是变量指向的内存地址不得改动。对于简单数据类型,变量就保存在内存地址中,所以等同于常量,而对于复杂数据类型来说,变量指向的内存地址保存的只是一个指针,只能保证这个指针是固定的,不能控制其数据结构。(可以为对象添加属性但是不能对其重新赋值)
- Const和let本质区别是编译器内部处理不同
- Const优于let: 1>const可以提醒阅读程序的人,这个变量不应该改变。2>const比较符合函数式编程思想,运算不改变值只是建立新值。3>js编译器会对const进行优化,有利于提升性能 4>js可能会有多线程的实现,这时let表示的变量只应出现在单线程运行的代码中,不能多线程共享,有利于保证线程安全。
20.箭头函数
- 箭头函数一条语句若返回对象字面量,则需要加括号
- 箭头函数在参数和箭头之间不能换行
- 立即执行函数可以写成箭头函数的形式
- 箭头函数不能用于构造函数
- 箭头函数没有prototype属性
- 箭头函数不绑定arguments,,取而代之用rest参数…解决
- 箭头函数不绑定this箭头函数没有自己的this
- 箭头函数无法使用 call()或 apply()来改变其运行的作用域
- 使用new调用箭头函数会报错,因为箭头函数没有constructor
21.promise
JavaScript
中存在很多异步操作,Promise
将异步操作队列化,按照期望的顺序执行,返回符合预期的结果。可以通过链式调用多个 Promise
达到我们的目的。
优点:Promise其实就是做了一件事情,它是对异步操纵进行了封装,然后可以将异步操纵以同步的流程表达出来,避免了层层嵌套的回调函数,同时提供了同一的接口,使得控制异步操纵更加容易。
缺点:
(1) 无法取消Promise,一旦被创建它就会立刻去执行,无法中途取消
(2) 如果不设置回调函数,Promise内部的错误无法反应到外部
(3) 当处于未完成状态时,无法得知目前进行到那个状态。
- Promise特点:
1) 对象状态不受外界影响。三种状态Pending(进行中)、Fulfilled(已成功) Rejected(已失败)
2) 状态一旦改变就不会再变称为resolved(已定型)。两种状态:成功(Pengding到fulfulled) 、失败(pengding到rejected)
- 基本用法:创建promise对象的实例,then方法指定Resolved状态和Rejected状态的回调函数。
var promise=new promise(function(resolve,reject){
if(/*异步操作成功*/){
resolve(value);
}else{
reject(error);
}
});
promise.then(function(value){
//success
},function(error){
//failure
})
2.其他:
(1) Promise.prototype.then()方法返回的是一个新的promise实例,可以再then方法的后面再次调用另一个then方法
(2) Promise.prototype.catch()方法用于指定发生错误的回调函数,错误总是会被下一个catch捕获。多个promise对象后也可跟一个catch进行错误捕获。
(3) Promise.all()和promise.race()用于将多个promise实例包装成一个新的promise实例。
(4) Promise.resolve()用于将现有对象转化成promise对象
补充:
Promise.all() 批量执行
Promise.all([p1, p2, p3])用于将多个promise实例,包装成一个新的Promise实例,返回的实例就是普通的promise
它接收一个数组作为参数
数组里可以是Promise对象,也可以是别的值,只有Promise会等待状态改变
当所有的子Promise都完成,该Promise完成,返回值是全部值得数组
有任何一个失败,该Promise失败,返回值是第一个失败的子Promise结果
let p1 = new Promise((resolve, reject) => { resolve("fulfilled"); }); let p2 = new Promise((resolve, reject) => { reject("rejected"); }); Promise.all([p1, p2]).catch(reason => { console.log(reason); });
使用Promise.race()
处理容错异步,和race
单词一样哪个Promise快用哪个,哪个先返回用哪个。
- 以最快返回的promise为准
- 如果最快返加的状态为
rejected
那整个promise
为rejected
执行cache - 如果参数不是promise,内部将自动转为promise
- promise.race() 类似于Promise.all() ,区别在于它有任意一个完成就算完成
22.async/await
async/await
本质还是promise,只是更简洁的语法糖书写async/await
使用更清晰的promise来替换 promise.then/catch 的方式
1.async是generator和promise的语法糖,利用迭代器的状态机和promise来进行自更新!
2.Async对Generator函数进行了改进。
(1) 内置执行器
(2) 更好的语义async代替*,await代替yield
(3) 更广的适用性
(4) 返回值是promise
3.async函数返回一个promise对象,async函数内部return语句返回的值,会成为then方法回调函数
4.await命令后面是一个promise对象,若不是则会被转成一个立即resolve的Promise对象
5.Async - 定义异步函数(async function someName(){...})
自动把函数转换为 Promise
当调用异步函数时,函数返回值会被 resolve 处理
异步函数内部可以使用 await
6.Await - 暂停异步函数的执行 (var result = await someAsyncCall();)
当使用在 Promise 前面时,await 等待 Promise 完成,并返回 Promise 的结果
await 只能和 Promise 一起使用,不能和 callback 一起使用
await 只能用在 async 函数中
补充:
async相当于promise
await相当于then
async
下面在 hd
函数前加上async,函数将返回promise,我们就可以像使用标准Promise一样使用了。
async function hd() { return "houdunren.com"; } console.log(hd()); hd().then(value => { console.log(value); });
如果有多个await 需要排队执行完成,我们可以很方便的处理多个异步队列
async function hd(message) { return new Promise(resolve => { setTimeout(() => { resolve(message); }, 2000); }); } async function run() { let h1 = await hd("后盾人"); console.log(h1); let h2 = await hd("houdunren.com"); console.log(h2); } run();
await
使用 await
关键词后会等待promise 完
await
后面一般是promise,如果不是直接返回await
必须放在 async 定义的函数中使用await
用于替代then
使编码更优雅
下例会在 await 这行暂停执行,直到等待 promise 返回结果后才继执行。
async function hd() { const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve("houdunren.com"); }, 2000); }); let result = await promise; console.log(result); } hd()
一般await后面是外部其它的promise对象
async function hd() { return new Promise(resolve => { setTimeout(() => { resolve("fulfilled"); }, 2000); }); } async function run() { let value = await hd(); console.log("houdunren.com"); console.log(value); } run();
23.
-
-
% 父元素宽度的比例。
-
如果对 html 元素设置 font-size 为百分比值,则是以浏览器默认的字体大小 16px 为参照计算的(所有浏览器的默认字体大小都为 16px),如 62.5%即等于 10px(62.5% * 16px = 10px)。
-
-
em 相对单位。 不同的属性有不同的参照值。
-
对于字体大小属性(font-size)来说,em 的计算方式是相对于父元素的字体大小
-
border, width, height, padding, margin, line-height)在这些属性中,使用 em 单位的计算方式是参照该元素的 font-size,1em 等于该元素设置的字体大小。同理如果该元素没有设置,则一直向父级元素查找,直到找到,如果都没有设置大小,则使用浏览器默认的字体大小。
-
-
rem 是相对于根元素 html 的 font-size 来计算的,所以其参照物是固定的。
-
好处:rem 只需要修改 html 的 font-size 值即可达到全部的修改,即所谓的牵一发而动全身。
-
-
vw, vh, vmin, vmax 相对单位,是基于视窗大小(浏览器用来显示内容的区域大小)来计算的。
-
vw:基于视窗的宽度计算,1vw 等于视窗宽度的百分之一
-
vh:基于视窗的高度计算,1vh 等于视窗高度的百分之一
-
vmin:基于 vw 和 vh 中的最小值来计算,1vmin 等于最小值的百分之一
-
-
24.
结构:
-
display:none
-
会让元素完全从渲染树中消失,渲染的时候不占据任何空间, 不能点击,
-
-
visibility: hidden
-
不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,不能点击
-
-
opacity: 0
-
不会让元素从渲染树消失,渲染元素继续占据空间,只是内容不可见,可以点击
-
继承
-
display: none 和 opacity: 0
-
非继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示。
-
-
visibility: hidden
-
继承属性,子孙节点消失由于继承了 hidden,通过设置 visibility: visible;可以让子孙节点显式。
-
性能
-
display:none
-
修改元素会造成文档回流。读屏器不会读取 display: none 元素内容,性能消耗较大
-
-
visibility:hidden
-
修改元素只会造成本元素的重绘,性能消耗较少。读屏器读取 visibility: hidden 元素内容
-
-
opacity: 0
-
修改元素会造成重绘,性能消耗较少
-
相同点: 它们都能让元素不可见、他们都依然可以被 JS 所获取到
25.CSS 优化、提高性能的方法有哪些?
-
多个 css 合并,尽量减少 HTTP 请求
-
css 雪碧图
-
抽象提取公共样式,减少代码量
-
选择器优化嵌套,尽量避免层级过深 (用‘>’替换‘ ’)
-
属性值为 0 时,不加单位
-
压缩 CSS 代码
-
避免使用 CSS 表达式
-
它们要计算成千上万次并且可能会对你页面的性能产生影响。
-
26.
-
重排 ( reflow / relayyout / 回流 ) : 当渲染树中的元素的布局
重绘和重排的关系:在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。
重排必定会引发重绘,但重绘不一定会引发重排。
优化:
-
-
需要创建多个 DOM 节点时,使用 DocumentFragment 创建完后一次性的加入 document,或使用字符串拼接方式构建好对应 HTML 后再使用 innerHTML 来修改页面
-
缓存 Layout 属性值,如:var left = elem.offsetLeft; 这样,多次使用 left 只产生一次回流
-
避免用 table 布局(table 元素一旦触发回流就会导致 table 里所有的其它元素回流)
-
避免使用 css 表达式(expression),因为每次调用都会重新计算值(包括加载页面)
-
尽量使用 css 属性简写,如:用 border 代替 border-width, border-style, border-color
-
getComputedStyle
如何理解
getComputedStyle
会获取当前元素所有最终使用的CSS属性值,window.
和getComputedStyle
document.defaultView.
等价...getComputedStyle
getComputedStyle
会引起回流,因为它需要获取祖先节点的一些信息进行计算(譬如宽高等),所以用的时候慎用,回流会引起性能问题。然后合适的话会将话题引导回流,重绘,浏览器渲染原理等等。当然也可以列举一些其它会引发回流的操作,如offsetXXX
,scrollXXX
,clientXXX
,currentStyle
等等
URL从输入到页面展示的过程
https://segmentfault.com/a/1190000013662126
-
从浏览器接收url到开启网络请求线程
- 多进程的浏览器
- 多线程的浏览器内核
- 解析URL
- 网络请求都是单独的线程
- 更多
-
开启网络线程到发出一个完整的http请求
- DNS查询得到IP
- tcp/ip请求
- 五层因特网协议栈
-
从服务器接收到请求到对应后台接收到请求
- 负载均衡
- 后台的处理
-
后台和前台的http交互
- http报文结构
- cookie以及优化
- gzip压缩
- 长连接与短连接
- http 2.0
- https
-
单独拎出来的缓存问题,http的缓存
- 强缓存与弱缓存
- 缓存头部简述
- 头部的区别
-
解析页面流程
- 流程简述
- HTML解析,构建DOM
- 生成CSS规则
- 构建渲染树
- 渲染
- 简单层与复合层
- Chrome中的调试
- 资源外链的下载
- loaded和domcontentloaded
-
CSS的可视化格式模型
- 包含块(Containing Block)
- 控制框(Controlling Box)
- BFC(Block Formatting Context)
- IFC(Inline Formatting Context)
- 其它
-
JS引擎解析过程
- JS的解释阶段
- JS的预处理阶段
- JS的执行阶段
- 回收机制
(
-
判断是否有永久重定向(301)
-
如果有,直接跳转到对应 URL
-
-
浏览器查看资源是否有强缓存,有则直接使用,如果是协商缓存则需要到服务器进行校验资源是否可用
-
检验新鲜通常有两个 HTTP 头进行控制
Expires
和Cache-Control
:-
HTTP1.0 提供 Expires,值为一个绝对时间表示缓存新鲜日期
-
HTTP1.1 增加了 Cache-Control: max-age=,值为以秒为单位的最大新鲜时间
-
-
-
浏览器解析 URL获取协议,主机,端口,path
-
浏览器组装一个 HTTP(GET)请求报文
-
DNS 解析,查找过程如下:
-
浏览器缓存
-
本机缓存
-
hosts 文件
-
路由器缓存
-
ISP DNS 缓存
-
DNS 查询(递归查询 / 迭代查询)
-
-
端口建立 TCP 链接,三次握手如下:
-
客户端发送一个 TCP 的SYN=1,Seq=X的包到服务器端口
-
服务器发回SYN=1, ACK=X+1, Seq=Y的响应包
-
客户端发送ACK=Y+1, Seq=Z
-
-
TCP 链接建立后发送 HTTP 请求
-
服务器接受请求并解析,将请求转发到服务程序,如虚拟主机使用 HTTP Host 头部判断请求的服务程序
-
服务器检查HTTP 请求头是否包含缓存验证信息如果验证缓存新鲜,返回304等对应状态码
-
处理程序读取完整请求并准备 HTTP 响应,可能需要查询数据库等操作
-
服务器将响应报文通过 TCP 连接发送回浏览器
-
浏览器接收 HTTP 响应,然后根据情况选择关闭 TCP 连接或者保留重用,关闭 TCP 连接的四次挥手如下:
-
主动方发送Fin=1, Ack=Z, Seq= X报文
-
被动方发送ACK=X+1, Seq=Z报文
-
被动方发送Fin=1, ACK=X, Seq=Y报文
-
主动方发送ACK=Y, Seq=X报文
-
-
浏览器检查响应状态吗:是否为 1XX,3XX, 4XX, 5XX,这些情况处理与 2XX 不同
-
如果资源可缓存,进行缓存
-
对响应进行解码(例如 gzip 压缩)
-
根据资源类型决定如何处理(假设资源为 HTML 文档)
-
解析 HTML 文档,构件 DOM 树,下载资源,构造 CSSOM 树,执行 js 脚本,这些操作没有严格的先后顺序,以下分别解释
-
构建 DOM 树:
-
Tokenizing:根据 HTML 规范将字符流解析为标记
-
Lexing:词法分析将标记转换为对象并定义属性和规则
-
DOM construction:根据 HTML 标记关系将对象组成 DOM 树
-
-
解析过程中遇到图片、样式表、js 文件,启动下载
-
构建CSSOM 树:
-
Tokenizing:字符流转换为标记流
-
Node:根据标记创建节点
-
CSSOM:节点创建 CSSOM 树
-
-
-
从 DOM 树的根节点遍历所有可见节点,不可见节点包括:1)
script
,meta
这样本身不可见的标签。2)被 css 隐藏的节点,如display: none
-
对每一个可见节点,找到恰当的 CSSOM 规则并应用
-
发布可视节点的内容和计算样式
-
-
js 解析如下:
-
浏览器创建 Document 对象并解析 HTML,将解析到的元素和文本节点添加到文档中,此时document.readystate 为 loading
-
HTML 解析器遇到没有 async 和 defer 的 script 时,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用 document.write()把文本插入到输入流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作 script 和他们之前的文档内容
-
当解析器遇到设置了async属性的 script 时,开始下载脚本并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器不会停下来等它下载。异步脚本禁止使用 document.write(),它们可以访问自己 script 和之前的文档元素
-
当文档完成解析,document.readState 变成 interactive
-
所有defer脚本会按照在文档出现的顺序执行,延迟脚本能访问完整文档树,禁止使用 document.write()
-
浏览器在 Document 对象上触发 DOMContentLoaded 事件
-
此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行,document.readState 变为 complete,window 触发 load 事件
-
-
显示页面
)
30.http报文结构
报文一般包括了:通用头部
,请求/响应头部
,请求/响应体
1.通用头部
Request Url: 请求的web服务器地址
Request Method: 请求方式
(Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)
Status Code: 请求的返回状态码,如200代表成功
Remote Address: 请求的远程服务器地址(会转为IP)
譬如,在跨域拒绝时,可能是method为options
,状态码为404/405
等(当然,实际上可能的组合有很多)
其中,Method的话一般分为两批次:
HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。
以及几种Additional Request Methods:PUT、DELETE、LINK、UNLINK
HTTP1.1定义了八种请求方法:GET、POST、HEAD、OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。
这里面最常用到的就是状态码,很多时候都是通过状态码来判断,如(列举几个最常见的):
200——表明该请求被成功地完成,所请求的资源发送回客户端 304——自从上次请求后,请求的网页未修改过,请客户端使用本地缓存 400——客户端请求有错(譬如可以是安全模块拦截) 401——请求未经授权 403——禁止访问(譬如可以是未登录时禁止) 404——资源未找到 500——服务器内部错误 503——服务不可用
1xx——指示信息,表示请求已接收,继续处理
2xx——成功,表示请求已被成功接收、理解、接受
3xx——重定向,要完成请求必须进行更进一步的操作
4xx——客户端错误,请求有语法错误或请求无法实现
5xx——服务器端错误,服务器未能实现合法的请求
2.请求/响应头部
常用的请求头部(部分)
Accept: 接收类型,表示浏览器支持的MIME类型 (对标服务端返回的Content-Type) Accept-Encoding:浏览器支持的压缩类型,如gzip等,超出类型不能接收 Content-Type:客户端发送出去实体内容的类型 Cache-Control: 指定请求和响应遵循的缓存机制,如no-cache If-Modified-Since:对应服务端的Last-Modified,用来匹配看文件是否变动,只能精确到1s之内,http1.0中 Expires:缓存控制,在这个时间内不会请求,直接使用缓存,http1.0,而且是服务端时间 Max-age:代表资源在本地缓存多少秒,有效时间内不会请求,而是使用缓存,http1.1中 If-None-Match:对应服务端的ETag,用来匹配文件内容是否改变(非常精确),http1.1中 Cookie: 有cookie并且同域访问时会自动带上 Connection: 当浏览器与服务器通信时对于长连接如何进行处理,如keep-alive Host:请求的服务器URL Origin:最初的请求是从哪里发起的(只会精确到端口),Origin比Referer更尊重隐私 Referer:该页面的来源URL(适用于所有类型的请求,会精确到详细页面地址,csrf拦截常用到这个字段) User-Agent:用户客户端的一些必要信息,如UA头部等
常用的响应头部(部分)
Access-Control-Allow-Headers: 服务器端允许的请求Headers Access-Control-Allow-Methods: 服务器端允许的请求方法 Access-Control-Allow-Origin: 服务器端允许的请求Origin头部(譬如为*) Content-Type:服务端返回的实体内容的类型 Date:数据从服务器发送的时间 Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档 Last-Modified:请求资源的最后修改时间 Expires:应该在什么时候认为文档已经过期,从而不再缓存它 Max-age:客户端的本地资源应该缓存多少秒,开启了Cache-Control后有效 ETag:请求变量的实体标签的当前值 Set-Cookie:设置和页面关联的cookie,服务器通过这个头部把cookie传给客户端 Keep-Alive:如果客户端有keep-alive,服务端也会有响应(如timeout=38) Server:服务器的一些相关信息
3.请求/响应体
http请求时,除了头部,还有消息实体,一般来说
请求实体中会将一些需要的参数都放入进入(用于post请求)。
譬如实体中可以放参数的序列化形式(a=1&b=2
这种),或者直接放表单对象(Form Data
对象,上传时可以夹杂参数以及文件),等等
而一般响应实体中,就是放服务端需要传给客户端的内容
一般现在的接口请求时,实体中就是对于的信息的json格式,而像页面请求这种,里面就是直接放了一个html字符串,然后浏览器自己解析并渲染。
CRLF
CRLF(Carriage-Return Line-Feed),意思是回车换行,一般作为分隔符存在
请求头和实体消息之间有一个CRLF分隔,响应头部和响应实体之间用一个CRLF分隔
一般来说(分隔符类别):
CRLF->Windows-style LF->Unix Style CR->Mac Style
如下图是对某请求的http报文结构的简要分析
31.使用setTimeout模拟实现setInterval
setInterval有一些比较致命的问题就是:
- 累计效应(上面提到的),如果setInterval代码在(setInterval)再次添加到队列之前还没有完成执行,就会导致定时器代码连续运行好几次,而之间没有间隔。
function mySetInterval(fn, millisec,count){ function interval(){ if(typeof count===‘undefined’||count-->0){ setTimeout(interval, millisec); try{ fn() }catch(e){ count = 0; throw e.toString(); } } } setTimeout(interval, millisec) }
32.进程和线程
。
浏览器都包含哪些进程?
知道了浏览器是多进程后,再来看看它到底包含哪些进程:(为了简化理解,仅列举主要进程)
-
Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用有
- 负责浏览器界面显示,与用户交互。如前进,后退等
- 负责各个页面的管理,创建和销毁其他进程
- 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
- 网络资源的管理,下载等
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
- GPU进程:最多一个,用于3D绘制等
-
浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为
- 页面渲染,脚本执行,事件处理等
浏览器内核(渲染进程)中的线程
-
GUI渲染线程
- 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
-
JS引擎线程
- 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
- JS引擎线程负责解析Javascript脚本,运行代码。
- JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
- 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
-
事件触发线程
- 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
- 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
- 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
-
注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
-
定时触发器线程
- 传说中的
setInterval
与setTimeout
所在线程 - 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
- 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
- 传说中的
-
异步http请求线程
- 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
- 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。
注意:GUI渲染线程与JS引擎线程互斥,JS阻塞页面加载,
从上述的互斥关系,可以推导出,JS如果执行时间过长就会阻塞页面。
解决办法:Web Worker
- 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
- JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)
浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤:
- 解析html建立dom树
- 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
- 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
- 绘制render树(paint),绘制页面像素信息
- 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上
注意:
- css加载不会阻塞DOM树解析(异步加载时DOM照常构建)
- 但会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)
普通图层和复合图层
渲染步骤中就提到了composite
概念。
可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层
以及复合图层
首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层
,里面不管添加多少元素,其实都是在同一个复合图层中)
其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层
。
然后,可以通过硬件加速
的方式,声明一个新的复合图层
,它会单独分配资源
(当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层
里的回流重绘)
可以简单理解下:GPU中,各个复合图层是单独绘制的,所以互不影响,这也是为什么某些场景硬件加速效果一级棒
absolute和硬件加速的区别
可以看到,absolute虽然可以脱离普通文档流,但是无法脱离默认复合层。
所以,就算absolute中信息改变时不会改变普通文档流中render树,
但是,浏览器最终绘制时,是整个复合层绘制的,所以absolute中信息的改变,仍然会影响整个复合层的绘制。
(浏览器会重绘它,如果复合层中内容多,absolute带来的绘制信息变化过大,资源消耗是非常严重的)
而硬件加速直接就是在另一个复合层了(另起炉灶),所以它的信息改变不会影响默认复合层
(当然了,内部肯定会影响属于自己的复合层),仅仅是引发最后的合成(输出视图)
复合图层的作用?
一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能
但是尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡
线程角度分析运行机制宏任务微任务
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
- 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
33.http/https/http2
HTTP 协议的特点
-
无连接
-
限制每次连接只处理一个请求
-
-
无状态
-
协议对于事务处理没有记忆能力。
-
-
简单快速
-
客户向服务器请求服务时,只需传送请求方法和路径。
-
-
灵活
-
HTTP 允许传输任意类型的数据对象。正在传输的类型由 Content-Type 加以标记。
-
HTTP 方法
-
GET
-
获取资源
-
-
POST
-
传输资源
-
-
PUT
-
更新资源
-
-
DELETE
-
删除资源
-
-
HEAD
-
获取报文首部
然后简述下http2.0与http1.1的显著不同点:
- http1.1中,每请求一个资源,都是需要开启一个tcp/ip连接的,所以对应的结果是,每一个资源对应一个tcp/ip请求,由于tcp/ip本身有并发数限制,所以当资源一多,速度就显著慢下来
- http2.0中,一个tcp/ip请求可以请求多个资源,也就是说,只要一次tcp/ip请求,就可以请求若干个资源,分割成更小的帧请求,速度明显提升。
所以,如果http2.0全面应用,很多http1.1中的优化方案就无需用到了(譬如打包成精灵图,静态资源多域名拆分等)
然后简述下http2.0的一些特性:
- 多路复用(即一个tcp/ip连接可以请求多个资源)
- 首部压缩(http头部压缩,减少体积)
- 二进制分帧(在应用层跟传送层之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量)
- 服务器端推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端)
- 请求优先级(如果流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求。)
https
https就是安全版本的http,譬如一些支付等操作基本都是基于https的,因为http请求的安全系数太低了。
简单来看,https与http的区别就是: 在请求前,会建立ssl链接,确保接下来的通信都是加密的,无法被轻易截取分析
一般来说,如果要将网站升级成https,需要后端支持(后端需要申请证书等),然后https的开销也比http要大(因为需要额外建立安全链接以及加密等),所以一般来说http2.0配合https的体验更佳(因为http2.0更快了)
34.HTTP/3 新特性
HTTP/2 的缺点
虽然 HTTP/2 解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,主要是底层支撑的 TCP 协议造成的。HTTP/2 的缺点主要有以下几点:
TCP 以及 TCP+TLS 建立连接的延时
HTTP/2 都是使用 TCP 协议来传输的,而如果使用 HTTPS 的话,还需要使用 TLS 协议进行安全传输,而使用 TLS 也需要一个握手过程,这样就需要有两个握手延迟过程:
-
在建立 TCP 连接的时候,需要和服务器进行三次握手来确认连接成功,也就是说需要在消耗完 1.5 个 RTT 之后才能进行数据传输。
-
进行 TLS 连接,TLS 有两个版本——TLS1.2 和 TLS1.3,每个版本建立连接所花的时间不同,大致是需要 1~2 个 RTT。
总之,在传输数据之前,我们需要花掉 3 ~ 4 个 RTT。
TCP 的队头阻塞并没有彻底解决
在 HTTP/2 中,多个请求是跑在一个 TCP 管道中的。但当出现了丢包时,HTTP/2 的表现反倒不如 HTTP/1 了。因为 TCP 为了保证可靠传输,有个特别的“丢包重传”机制,丢失的包必须要等待重新传输确认,HTTP/2 出现丢包时,整个 TCP 都要开始等待重传,那么就会阻塞该 TCP 连接中的所有请求。而对于 HTTP/1.1 来说,可以开启多个 TCP 连接,出现这种情况反到只会影响其中一个连接,剩余的 TCP 连接还可以正常传输数据。
HTTP/3 简介
Google 在推 SPDY 的时候就已经意识到了这些问题,于是就另起炉灶搞了一个基于 UDP 协议的“QUIC”协议
QUIC 新功能
QUIC 基于 UDP,而 UDP 是“无连接”的,根本就不需要“握手”和“挥手”,所以就比 TCP 来得快。此外 QUIC 也实现了可靠传输,保证数据一定能够抵达目的地。它还引入了类似 HTTP/2 的“流”和“多路复用”,单个“流"是有序的,可能会因为丢包而阻塞,但其他“流”不会受到影响。具体来说 QUIC 协议有以下特点:
-
实现了类似 TCP 的流量控制、传输可靠性的功能。
-
虽然 UDP 不提供可靠性的传输,但 QUIC 在 UDP 的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些 TCP 中存在的特性。
-
-
实现了快速握手功能
-
由于 QUIC 是基于 UDP 的,所以 QUIC 可以实现使用 0-RTT 或者 1-RTT 来建立连接,这意味着 QUIC 可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势。
-
-
集成了 TLS 加密功能。
-
目前 QUIC 使用的是 TLS1.3,相较于早期版本 TLS1.3 有更多的优点,其中最重要的一点是减少了握手所花费的 RTT 个数。
-
-
多路复用,彻底解决 TCP 中队头阻塞的问题
-
和 TCP 不同,QUIC 实现了在同一物理连接上可以有多个独立的逻辑数据流。实现了数据流的单独传输,就解决了 TCP 中队头阻塞的问题。
-
总结
HTTP/1.1 有两个主要的缺点:安全不足和性能不高。
HTTP/2 完全兼容 HTTP/1,是“更安全的 HTTP、更快的 HTTPS",头部压缩、多路复用等技术可以充分利用带宽,降低延迟,从而大幅度提高上网体验;
QUIC 是 HTTP/3 中的底层支撑协议,该协议基于 UDP,又取了 TCP 中的精华,实现了即快又可靠的协议。
35.Post 和 Get 的区别
-
GET 在浏览器回退时是无害的,而 POST 会再次提交
-
Get 请求能缓存,Post 不能
-
Post 相对 Get 相对安全一些,因为 Get 请求都包含在 URL 中,而且会被浏览器保存记录,Post 不会。但是再抓包的情况下都是一样的。
-
Post 可以通过 request body 来传输比 Get 更多的数据
-
URL 有长度限制,会影响 Get 请求,但是这个长度限制是浏览器规定的
-
Post 支持更多的编码类型且不对数据类型限制
-
POST,浏览器先发送 header,服务器响应 100 continue,浏览器再发送 data,服务器响应 200 ok(返回数据)
36.状态码
1XX 指示信息
表示请求已接收,继续处理
2XX 成功
-
200 OK
-
201 Created 通常指 POST 请求的结果,已在服务器上成功创建了一个或多个新资源。
-
202 Accepted 表示已接受处理请求,但处理尚未完成,表异步。
-
204 No content,表示请求成功,但响应报文不含实体的主体部分
-
205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,但是与 204 响应不同在于要求请求方重置内容
-
206 Partial Content,进行范围请求
3XX 重定向
-
301 永久性重定向,表示资源已被分配了新的 URL
-
302 临时性重定向,表示资源临时被分配了新的 URL
-
303 表示资源存在着另一个 URL,应使用 GET 方法获取资源
-
304 未修改,重定位到浏览器。自从上次请求后,请求的网页未修改过。服务器返回此响应时,不会返回网页内容。如果网页自请求者上次请求后再也没有更改过,您应将服务器配置为返回此响应(称为 If-Modified-Since HTTP 标头)。服务器可以告诉 Googlebot 自从上次抓取后网页没有变更,进而节省带宽和开销。
-
307 临时重定向,和 302 含义类似,但是期望客户端保持请求方法不变向新的地址发出请求
4XX 客户端错误
-
404 在服务器上没有找到请求的资源
-
403 Forbidden,用户被认证后,用户没有被授权在特定资源上执行操作的权限
-
400 请求报文存在语法错误
-
401 Unauthorized, 表示请求没有被认证,或者认证不正确请重新认证和重试。
5XX 服务器错误
-
500 表示服务器端在执行请求时发生了错误
-
501 表示服务器不支持当前请求所需要的某个功能
-
503 表明服务器暂时处于超负载或正在停机维护,无法处理请求
37.缓存
HTTP 缓存
对于强缓存,服务器通知浏览器一个缓存时间,在缓存时间内,下次请求,直接用缓存,不在缓存时间内,执行比较缓存策略。
对于协商缓存,将缓存信息中的Etag和Last-Modified通过请求发送给服务器,由服务器校验,返回304状态码时,浏览器直接使用缓存。返如果已过期,返回200状态码以及最新资源。
强制缓存优先于协商缓存进行,若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回 304,继续使用缓存,
强缓存与弱缓存
缓存可以简单的划分成两种类型:强缓存
(200 from cache
)与协商缓存
(304
)
区别简述如下:
- 强缓存(
200 from cache
)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起http请求 - 协商缓存(
304
)时,浏览器会向服务端发起http请求,然后服务端告诉浏览器文件未改变,让浏览器使用本地缓存
对于协商缓存,使用Ctrl + F5
强制刷新可以使得缓存无效
但是对于强缓存,在未过期时,必须更新资源路径才能发起新的请求(更改了路径相当于是另一个资源了,这也是前端工程化中常用到的技巧)
缓存头部:
强缓存
(http1.1)Cache-Control/Max-Age (http1.0)Pragma/Expires
协商缓存:
(http1.1)If-None-Match/E-tag (http1.0)If-Modified-Since/Last-Modified
首先明确,http的发展是从http1.0到http1.1
而在http1.1中,出了一些新内容,弥补了http1.0的不足。
http1.0中的缓存控制:
Pragma
:严格来说,它不属于专门的缓存控制头部,但是它设置no-cache
时可以让本地强缓存失效(属于编译控制,来实现特定的指令,主要是因为兼容http1.0,所以以前又被大量应用)Expires
:服务端配置的,属于强缓存,用来控制在规定的时间之前,浏览器不会发出请求,而是直接使用本地缓存,注意,Expires一般对应服务器端时间,如Expires:Fri, 30 Oct 1998 14:19:41
If-Modified-Since/Last-Modified
:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是If-Modified-Since
,而服务端的是Last-Modified
,它的作用是,在发起请求时,如果If-Modified-Since
和Last-Modified
匹配,那么代表服务器资源并未改变,因此服务端不会返回资源实体,而是只返回头部,通知浏览器可以使用本地缓存。Last-Modified
,顾名思义,指的是文件最后的修改时间,而且只能精确到1s
以内
http1.1中的缓存控制:
Cache-Control
:缓存控制头部,有no-cache、max-age等多种取值Max-Age
:服务端配置的,用来控制强缓存,在规定的时间之内,浏览器无需发出请求,直接使用本地缓存,注意,Max-Age是Cache-Control头部的值,不是独立的头部,譬如Cache-Control: max-age=3600
,而且它值得是绝对时间,由浏览器自己计算If-None-Match/E-tag
:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是If-None-Match
,而服务端的是E-tag
,同样,发出请求后,如果If-None-Match
和E-tag
匹配,则代表内容未变,通知浏览器使用本地缓存,和Last-Modified不同,E-tag更精确,它是类似于指纹一样的东西,基于FileEtag INode Mtime Size
生成,也就是说,只要文件变,指纹就会变,而且没有1s精确度的限制。
补充:
商缓存VS强缓存
强缓存相关的header字段
强缓存上面已经介绍了,直接从缓存中获取资源而不经过服务器;与强缓存相关的header字段有两个:
- expires,这是http1.0时的规范;它的值为一个绝对时间的GMT格式的时间字符串,如Mon, 10 Jun 2015 21:31:12 GMT,如果发送请求的时间在expires之前,那么本地缓存始终有效,否则就会发送请求到服务器来获取资源
- cache-control:max-age=number,这是http1.1时出现的header信息,主要是利用该字段的max-age值来进行判断,它是一个相对值;资源第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行;cache-control除了该字段外,还有下面几个比较常用的设置值:
- no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
- no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
- public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
- private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
- no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
注意:如果cache-control与expires同时存在的话,cache-control的优先级高于expires
协商缓存相关的header字段
协商缓存都是由服务器来确定缓存资源是否可用的,所以客户端与服务器端要通过某种标识来进行通信,从而让服务器判断请求资源是否可以缓存访问,这主要涉及到下面两组header字段,这两组搭档都是成对出现的,即第一次请求的响应头带上某个字段(Last-Modified或者Etag),则后续请求则会带上对应的请求字段(If-Modified-Since或者If-None-Match),若响应头没有Last-Modified或者Etag字段,则请求头也不会有对应的字段。
- Last-Modified/If-Modified-Since
二者的值都是GMT格式的时间字符串,具体过程:
- 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间
- 浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值
- 服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modified的header,因为既然资源没有变化,那么Last-Modified也就不会改变,这是服务器返回304时的response header
- 浏览器收到304的响应后,就会从缓存中加载资源
- 如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified的Header在重新加载的时候会被更新,下次请求时,If-Modified-Since会启用上次返回的Last-Modified值
- 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间
- Etag/If-None-Match
这两个值是由服务器生成的每个资源的唯一标识字符串,只要资源有变化就这个值就会改变;其判断过程与Last-Modified/If-Modified-Since类似,与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化。
既生Last-Modified何生Etag
你可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?HTTP1.1中Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:
-
一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
-
某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
-
某些服务器不能精确的得到文件的最后修改时间。
这时,利用Etag能够更加准确的控制缓存,因为Etag是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符。
Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。
用户的行为对缓存的影响
盗用网上的一张图,基本能描述用户行为对缓存的影响
6、强缓存如何重新加载缓存缓存过的资源
上面说到,使用强缓存时,浏览器不会发送请求到服务端,根据设置的缓存时间浏览器一直从缓存中获取资源,在这期间若资源产生了变化,浏览器就在缓存期内就一直得不到最新的资源,那么如何防止这种事情发生呢?
通过更新页面中引用的资源路径,让浏览器主动放弃缓存,加载新资源。
类似下图所示:
这样每次文件改变后就会生成新的query值,这样query值不同,也就是页面引用的资源路径不同了,之前缓存过的资源就被浏览器忽略了,因为资源请求的路径变了。
38.解析页面流程
1. 解析HTML,构建DOM树
2. 解析CSS,生成CSS规则树
3. 合并DOM树和CSS规则,生成render树
4. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
5. 绘制render树(paint),绘制页面像素信息
6. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上
39.tcp和udp
TCP(Transmission Control Protocol,传输控制协议)
UDP(User Data Protocol,用户数据报协议)
(1)TCP是面向连接的(需要三次握手建立连接),udp是无连接的即发送数据前不需要先建立链接。
(2)对系统资源的要求(TCP较多,UDP少)
(3)流模式与数据报模式 ;(TCP是面向字节流,UDP面向报文,并且网络出现拥塞不会使得发送速率降低因此会出现丢包,对实时的应用比如IP电话和视频会议等)。
(4)UDP程序结构较简单;
(5)TCP是面向连接的可靠性传输,而UDP是不可靠的TCP保证数据正确性,UDP可能丢包,TCP保证数据顺序,UDP不保证。
40.cookie`和`sessiom`
cookie`和`sessiom`是两种保持会话状态的方法。
cookie就是指客户端在向服务端发起请求的时候,服务端会在进行response的时候给当前客户端的一小段文本信息,并保存在当前的客户端的浏览器中,这一小段cookie文本信息也就是这个客户端去访问服务端的通行证,有了这个通行证,以后当这个客户端再去访问服务端的时候,服务端便知道是谁拿着通行证去进行访问了。
session和cookie的功能类似,也是一种保持会话状态的方式,在用户使用浏览器发起会话时,服务器会为每一个用户浏览器提供一个单独的session对象来保存用户的数据,并将它保存在服务端,而当用户访问其他web资源的时候,则可以从保存用户数据的session对象中把用户数据抽取出来并进行访问。
区别:
1. cookie的用户数据是保存在用户浏览器的cookie中的;
session的用户数据是保存在服务器为用户浏览器单独创建的session对象中的。
2. 数据的读取和调用,cookie可以采用request.getCookies这种方法;
session则可以用request.Session的方法。
3. 安全性,cookie是存储在用户浏览器中的;
而session是存储在服务器上的,所以session比cookoe要相对安全;
41.为什么 TCP 这么复杂?
因为既要保证可靠性, 同时又要尽可能提高性能
-
校验和
-
序列号(按序到达)
-
确认应答
-
超时重传
-
连接管理
-
流量控制
-
拥塞控制
提高性能的机制
-
滑动窗口
-
快速重传
-
延迟应答
-
捎带应答
定时器
-
超时重传定时器
-
保活定时器
-
TIME_WAIT 定时器
基于 TCP 的应用层协议
-
HTTP
-
HTTPS
-
SSH
-
Telnet
-
FTP
-
SMTP
42.js中本地存储有哪些?有什么不同?
`cookie`、`localStorage`、`sessionStorage`;
相同点:都保存在浏览器端;
不同点:
①传递方式不同
`cookie`数据始终在同源的http请求中携带(即使不需要),即`cookie`在浏览器和服务器间来回传递。
`sessionStorage`和`localStorage`不会自动把数据发给服务器,仅在本地保存。
②数据大小不同
(`cookie`数据还有路径(path)的概念,可以限制cookie只属于某个路径下。)
存储大小限制也不同,cookie数据不能超过4k,同时因为每次http请求都会携带`cookie`,所以cookie只适合保存很小的数据,如会话标识。
`sessionStorage`和`localStorage` 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大。
③数据有效期不同
`sessionStorage`:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持;
`localStorage`:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;
`cookie`只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。
④作用域不同
`sessionStorage`不在不同的浏览器窗口中共享,即使是同一个页面;
`localStorage `在所有同源窗口中都是共享的;
`cookie`也是在所有同源窗口中都是共享的。
43.闭包
闭包是什么?有什么优点和缺点?
解答:闭包是指有权访问另一个函数作用域中的变量的函数。
优点:① 能够读取函数内部的变量;②让这些变量一直存在于内存中,不会在调用结束后被垃圾回收机制回收;
缺点:由于闭包会使用函数中的变量存在在内存中,内存消耗很大,所以不能滥用闭包;解决的办法是退出函数之前,将不使用的局部变量删除;
44.<script>标签的两个属性deffer和async
1.延迟加载'defer 属性。这个属性的用途是表明脚本在执行时不会影响页面的构造。也就是说,脚本会被延迟到整个页面都解析完毕后再运行。因此,在 <script> 元素中设置defer 属性,相当于告诉浏览器立即下载,但延迟执行
2.异步加载 async 只适用于外部脚本文件,并告诉浏览器立即下载文件,下载完成后立即执行。但与 defer不同的是,标记为 async 的脚本并不保证按照指定它们的先后顺序执行
3.使用 defer 属性可以让脚本在文档完全呈现之后再执行,延迟脚本总是按照指定它们的顺序执行。
使用 async 属性可以表示当前脚本不必等待其他脚本,也不必阻塞文档呈现。不能保证异步脚本按照它们在页面中出现的顺序执行。
45.跨域
-
JSONP
-
CORS
-
Hash
-
postMessage
-
WebSoket
同源策略
-
端口相同
-
域名相同
-
协议相同
例子:http://www.example.com/dir/page.html
这个网址,协议是http
,域名是www.example.com
,端口是80
同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。是浏览器做的努力
同源策略限制范围
-
Cookie、LocalStorage 和 IndexDB 无法读取
-
DOM 无法获得
-
AJAX 请求不能发送
CROS 跨域资源请求
CORS(Cross-origin resource sharing)跨域资源请求
浏览器在请求一个跨域资源的时候,如果是跨域的 Ajax 请求,他会在请求头中加一个origin
字段,但他是不知道这个资源服务端是否允许跨域请求的。浏览器会发送到服务端,如果服务器返回的头中没有'Access-Control-Allow-Origin': '对应网址或 * '
的话,那么浏览器就会把请求内容给忽略掉,并且在控制台报错
CORS 限制
允许的请求方法
-
GET
-
POST
-
HEAD
允许的 Content-Type
-
text/plain
-
multipart/form-data
-
application/x-www-form-ulencoded
其他类型的请求方法和 Content-Type 需要通过预请求验证后然后才能发送
CORS 预请求
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站有权限访问哪些资源。另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求。
服务器在 HTTP header 中加入允许请求的方法和 Content-Type 后,其他指定的方法和 Content-Type 就可以成功请求了
'Access-Control-Allow-Headers': '允许Content-Type' 'Access-Control-Allow-Methods': '允许的请求方法' 'Access-Control-Max-Age': '预请求允许其他方法和类型传输的时间'
JSONP 跨域
浏览器上虽然有同源限制,但是像 srcipt 标签、link 标签、img 标签、iframe 标签,这种在标签上通过 src 地址来加载一些内容的时候浏览器是允许进行跨域请求的。
所以 JSONP 的原理就是:
-
创建一个 script 标签,这个 script 标签的 src 就是请求的地址;
-
这个 script 标签插入到 DOM 中,浏览器就根据 src 地址访问服务器资源
-
返回的资源是一个文本,但是因为是在 script 标签中,浏览器会执行它
-
而这个文本恰好是函数调用的形式,即函数名(数据),浏览器会把它当作 JS 代码来执行即调用这个函数
-
只要提前约定好这个函数名,并且这个函数存在于 window 对象中,就可以把数据传递给处理函数。
Hash 值跨域通信
背景:在页面 A 下提供 iframe 或 frame 嵌入了跨域的页面 B
容器页面 -> 嵌入页通信:
在 A 页面中改变 B 的 url 中的 hash 值,B 不会刷新,但是 B 可以用过window.onhashchange
事件监听到 hash 变化
postMessage 通信
// 窗口A中 window.postMessage("data", "http://A.com"); // 窗口B中 window.addEventListener("message", function (event) { console.log(event.origin); // http://A.com console.log(event.source); // A 对象window引用 console.log(event.data); // 数据 });
WebSoket 跨域通信
var ws = new WebSocket("wss://echo.websoket.org"); //这个是后端端口 ws.onopen = function (evt) { ws.send("some message"); }; ws.onmessage = function (evt) { console.log(evt.data); }; ws.onclose = function (evt) { console.log("连接关闭"); };
document.domain
该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。
只需要给页面添加 document.domain = 'test.com' 表示二级域名都相同就可以实现跨域
localhost 与 127.0.0.1
-
localhost 等于 127.0.0.1,不过 localhost 是域名,127.0.0.1 是 IP 地址
-
localhost 和 127.0.0.1 不需要联网,都是本机访问
注意:localhost 和 127.0.0.1 虽然都指向本机,但也属于跨域, (配置 localhost 出现 CORS 时,可尝试改为 127.0.0.1)
46.css实现垂直居中
1.子绝父相,子top和left 为 50%,用margin拉回,子margin-top,margin-left为-宽高一半
2.子绝父相,子 top和left,right,bottom 都为 0,margin:auto
3.子绝父相,子top 为 calc(50% 剪 一半高), left 为 calc(50% 剪 一半宽)
4.子绝父相,子top和left 为 50%用transform拉回,transform: translate(-50%, -50%);
5.flex,父元素 display: flex; justify-content: center;align-items: center;
6.flxe+margin,父元素 display: flex;,子 margin: auto;
47.css实现三栏布局
1.圣杯布局
要求:三列布局;中间宽度自适应,两边内容定宽。
好处:重要的内容放在文档流前面可以优先渲染
实现方式:
main 部分首先要放在 container 的最前部分。然后是 left,right
1.将三者都 float:left , 再加上一个 position:relative (因为相对定位后面会用到)
2.main 部分 100%占满
3.此时 main 占满了,所以要把 left 拉到最左边,使用 margin-left:-100%
4.这时 left 拉回来了,但会覆盖 main 内容的左端,要把 main 内容拉出来,所以在外围 container 加上 padding:0 220px 0 200px
5.main 内容拉回来了,right 也跟着过来了,所以要还原,就对 left 使用相对定位 left:-200px 同理,right 也要相对定位还原 right:-220px
6.到这里大概就自适应好了。如果想 container 高度保持一致可以给 left main right 都加上 min-height:130px;
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>圣杯布局</title> <style type="text/css"> .container { padding: 0 200px 0 200px; } .main { float: left; 100%; height: 200px; background-color: rosybrown; position: relative; } .left { float: left; 200px; height: 200px; background-color: silver; margin-left: -100%; position: relative; left: -200px; } .right { float: left; 200px; height: 200px; background-color: seagreen; margin-left: -200px; position: relative; right: -200px; } </style> </head> <body> <div class="container"> <div class="main">main</div> <div class="left">left</div> <div class="right">right</div> </div> </body> </html>
2.双飞翼布局
原理:主体元素上设置左右边距,预留两翼位置。左右两栏使用浮动和负边距归位。
左翅 left 有 200px,右翅 right..220px.. 身体 main 自适应未知
1.html 代码中,main 要放最前边,left right
2.将 main left right 都 float:left
3.将 main 占满 100%
4.此时 main 占满了,所以要把 left 拉到最左边,使用 margin-left:-100% 同理 right 使用 margin-left:-220px
(这时可以直接继续上边圣杯布局的步骤,也可以有所改动)
5.main 内容被覆盖了吧,除了使用外围的 padding,还可以考虑使用 margin。
给 main 增加一个内层 div-- main-inner, 然后 margin:0 220px 0 200px
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>比翼双飞布局</title> <style> .main { float: left; background-color: blue; 100%; height: 130px; } .left { float: left; background-color: aqua; 200px; height: 130px; margin-left: -100%; } .right { float: left; background-color: aquamarine; 200px; height: 130px; margin-left: -200px; } .inner { margin: 0 200px 0 200px; } </style> </head> <body> <div class="container"> <div class="main"> <div class="inner">inner</div> </div> <div class="left">left</div> <div class="right">right</div> </div> </body> </html>
3.绝对定位
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style> .container{ position: relative; } .main,.left,.right{ top: 0; min-height: 130px; } .main{ margin: 0 200px 0 200px; background-color: aqua; } .left{ position: absolute; left: 0; 200px; background-color: aquamarine; } .right{ position: absolute; right: 0; 200px; background-color: antiquewhite; } </style> </head> <body> <div class="container"> <div class="left">left</div> <div class="main">main</div> <div class="right">right</div> </div> </body> </html>
4.flex
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style> .container{ display: flex; height: 800px; } .left{ 200px; height: 100%; background-color: gray; } .main{ flex: 1; height: 100%; background-color: green; } .right{ 300px; height: 100%; background-color: gold; } </style> </head> <body> <div class="container"> <div class="left"></div> <div class="main"></div> <div class="right"></div> </div> </body> </html>
48.前端动画怎么做
-
animation 过渡动画
@keyframes anim-1 {50% {border-radius: 50%;}}
animation: anim-1 1s;
-
transition 过渡动画
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <!-- transition:动画过渡效果属性 功能:使css的属性值在一定的时间内平滑的过渡。这种效果可以在鼠标 点击、划过、获取焦点或对元素任何改变中触发,并圆滑的以动画效果改变css属性值。 可定义的属性有: transition-property:设置那些属性进行过渡,all:所有属性 transition-duration:完成过渡动画效果的时间,默认是0 transition-timing-function:设置动画的缓动效果,默认是ease(逐渐变慢)。 (其他常用值:ease-in,加速;ease-out,减速;ease-in-out,加速然后减速) transition-delay:设置动画开始的延迟时间,默认是0 示例: 使用transition,实现过渡动画。一个红色的正方形,当鼠标移入该区域后,红色正方形放大一倍,并且颜色变为灰色。鼠标移开,还原到初始状态。 --> <style> .box{ 100px; height: 100px; margin:50px auto; background-color: #f00; /* 为离开的动作也添加过渡效果 */ transition: all 1s ease-in 1s; } .box:hover{ height: 200px; 200px; border-radius: 50%; background-color: #666; /* transition-property:width,height,border-radius; transition-duration: 1s; transition-timing-function:ease-in; transition-delay:1s; */ transition: all 1s ease-in 1s; } </style> <body> <div class="box"></div> </body> </html>
-
JS 原生控制 DOM 位置
-
canvas 绘制动画
49.DOM事件
DOM 事件模型
捕获从上到下, 冒泡从下到上。 先捕获,再到目标,再冒泡
DOM 事件流
DOM 标准规定事件流包括三个阶段:
-
事件捕获阶段
-
处于目标阶段
-
事件冒泡阶段
描述 DOM 事件捕获的具体流程
从 window -> document -> html -> body -> ... -> 目标元素
事件的代理/委托
优点:
-
可以减少事件注册,节省大量内存占用
-
可以将事件应用于动态添加的子元素上
但使用不当会造成事件在不应该触发时触发
ulEl.addEventListener( "click", function (event) { var target = event.target || event.srcElement; if (target && target.nodeName.toUpperCase() === "LI") { console.log(target.innerHTML); } }, false );
DOM方法 addEventListener() 和 removeEventListener()是用来分配和删除事件的函数。 这两个方法都需要三个参数,分别为:
事件名称(String)、要触发的事件处理函数(Function)、指定事件处理函数的时期或阶段(boolean)。
由图可知捕获过程要先于冒泡过程
当第三个参数设置为true就在捕获过程中执行,反之就在冒泡过程中执行处理函数。
50.css伪类和伪元素区别
1)伪类(pseudo-classes)
- 其核⼼就是⽤来选择DOM树之外的信息,不能够被普通选择器选择的⽂档之外的元素,⽤来添加⼀些选择器的特殊效果。
- ⽐如:hover :active :visited :link :visited :first-child :focus :lang等
- 由于状态的变化是⾮静态的,所以元素达到⼀个特定状态时,它可能得到⼀个伪类的样式;当状态改变时,它⼜会失去这个样式。
- 由此可以看出,它的功能和class有些类似,但它是基于⽂档之外的抽象,所以叫 伪类。
2)伪元素(Pseudo-elements)
- DOM树没有定义的虚拟元素
- 核⼼就是需要创建通常不存在于⽂档中的元素,
- ⽐如::before ::after 它选择的是元素指定内容,表示选择元素内容的之前内容或之后内容。
- 伪元素控制的内容和元素是没有差别的,但是它本身只是基于元素的抽象,并不存在于⽂档中,所以称为伪元素。⽤于将特殊的效果添加到某些选择器
2)伪类与伪元素的区别
-
表示⽅法
- CSS2 中伪类、伪元素都是以单冒号:表示,
- CSS2.1 后规定伪类⽤单冒号表示,伪元素⽤双冒号::表示,
- 浏览器同样接受 CSS2 时代已经存在的伪元素(:before, :after, :first�line, :first-letter 等)的单冒号写法。
- CSS2 之后所有新增的伪元素(如::selection),应该采⽤双冒号的写法。
- CSS3中,伪类与伪元素在语法上也有所区别,伪元素修改为以::开头。浏览器对以:开头的伪元素也继续⽀持,但建议规范书写为::开头
-
定义不同
- 伪类即假的类,可以添加类来达到效果
- 伪元素即假元素,需要通过添加元素才能达到效果
-
总结:
- 伪类和伪元素都是⽤来表示⽂档树以外的"元素"。
- 伪类和伪元素分别⽤单冒号:和双冒号::来表示。
- 伪类和伪元素的区别,关键点在于如果没有伪元素(或伪类),
- 是否需要添加元素才能达到效果,如果是则是伪元素,反之则是伪类。
4)相同之处:
- 伪类和伪元素都不出现在源⽂件和DOM树中。也就是说在html源⽂件中是看不到伪类和伪元素的。
不同之处: - 伪类其实就是基于普通DOM元素⽽产⽣的不同状态,他是DOM元素的某⼀特征。
- 伪元素能够创建在DOM树中不存在的抽象对象,⽽且这些抽象对象是能够访问到的。
51.常见的排序算法
冒泡
function bubbleSort(arr){ let len=arr.length; for(let i=0;i<len;i++){ for(let j=i+1;j<len;j++){ if(arr[i]>arr[j]){ [arr[i],arr[j]]=[arr[j],arr[i]] } } } return arr; }
快排
function _quickSort(arr) { let firstnum = arr[0]; let right = []; let left = []; for (var i = 1; i < arr.length; i++) { if (arr[i] > firstnum) { right.push(arr[i]); } else { left.push(arr[i]); } } if (left.length >= 2) { left = _quickSort(left) }; if (right.length >= 2) { right = _quickSort(right) }; return [...left, firstnum, ...right]; }
插入
function insertSort(arr){ for(let i = 1;i < arr.length;i++){ let j = i-1; if(arr[i]<arr[j]){ let temp = arr[i]; while(j >= 0 && temp < arr[j]){ arr[j+1] = arr[j]; j--; } arr[j+1] = temp; } } return arr; }
选择
function selectSort(arr){ for(let i = 0;i < arr.length;i++){ let min = Math.min(...arr.slice(i)); let index = arr.indexOf(min); [arr[i],arr[index]] = [arr[index],arr[i]]; } return arr; }
VUE进阶篇
1.Vue双向数据绑定:Object.defineProperty和Proxy
方式一:采用Object.defineProperty对象属性拦截器
vue中实现双向数据绑定的原理是:采用数据劫持结合发布者-订阅者的方式,
通过Object.defineProperty()来劫持各个属性的setter,getter,
在数据变动时,发布消息给订阅者,触发相应的监听回调。
实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
通俗的讲,就是利用observe监听Model层的数据变化;
<body> <div id="app"> <input type="text" id="txt"> <p id="show"></p> </div> </body> <script type="text/javascript"> var obj = {} Object.defineProperty(obj, 'txt', { get: function () { return obj }, set: function (newValue) { document.getElementById('txt').value = newValue document.getElementById('show').innerHTML = newValue } }) document.addEventListener('keyup', function (e) { obj.txt = e.target.value }) </script>
方式二:采用Proxy代理拦截的方式
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
<body> <input type="text" v-model="title" /> <input type="text" v-model="title" /> <div v-bind="title"></div> </body> <script> function View() { //设置代理拦截 let proxy = new Proxy( {}, { get(obj, property) {}, set(obj, property, value) { obj[property] = value; document .querySelectorAll( `[v-model="${property}"],[v-bind="${property}"]` ) .forEach(el => { el.innerHTML = value; el.value = value; }); } } ); //初始化绑定元素事件 this.run = function() { const els = document.querySelectorAll("[v-model]"); els.forEach(item => { item.addEventListener("keyup", function() { proxy[this.getAttribute("v-model")] = this.value; }); }); }; } let view = new View().run();
</script>
Proxy 与 Object.defineProperty 优劣对比
-
Proxy 的优势如下:
- Proxy 可以直接监听对象而非属性;
-
Proxy 可以直接监听数组的变化;
- Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
- Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
- Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
-
Object.defineProperty 的优势如下:
- 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平,因此 Vue 的作者才声明需要等到下个大版本( 3.0 )才能用 Proxy 重写。
补充:请说一下响应式数据的理解
核心答案:
数组和对象类型当值变化时如何劫持到。
对象内部通过defineReactive方法,使用Object.defineProperty将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现
这里在回答时可以带出一些相关知识点(比如多层对象是通过递归来实现劫持,顺带提出
Vue3
中是使用proxy来实现响应式数据)
#补充回答:
内部依赖收集是怎样做到的,每个属性都拥有自己的dep属性,存放他所依赖的watcher,当属性变化后会通知自己对应的watcher去更新 (其实后面会讲到每个对象类型自己本身也拥有一个dep属性,这个在$set面试题中在进行讲解)
这里可以引出性能优化相关的内容 (1)对象层级过深,性能就会差 (2)不需要响应数据的内容不要放到data中 (3)
Object.freeze
() 可以冻结数据
#快速Mock:
let state = { count: 0 }; // app.innerHTML = state.count; // 1.将数据变成响应式数据 let active; function defineReactive(obj) { for (let key in obj) { let value = obj[key]; let dep = []; Object.defineProperty(obj, key, { get() { if (active) { dep.push(active); } return value; }, set(newValue) { value = newValue; dep.forEach(fn => fn()); } }); } } defineReactive(state); const watcher = (fn) => { active = fn; fn(); active = null; } watcher(() => { app.innerHTML = state.count; }); watcher(() => { console.log(state.count) }); //源码位置: src/core/observer/index.js:135
Vue
如何检测数组变化?
#核心答案:
数组考虑性能原因没有用defineProperty对数组的每一项进行拦截,而是选择重写数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写。
补充回答:
在Vue中修改数组的索引和长度是无法监控到的。需要通过以上7种变异方法修改数组才会触发数组对应的watcher进行更新。数组中如果是对象数据类型也会进行递归劫持。
那如果想更改索引更新数据怎么办?可以通过Vue.$set()来进行处理 =》 核心内部用的是splice方法
快速Mock:
//源码位置: src/core/observer/array.js:8 let state = [1,2,3]; let originArray = Array.prototype; let arrayMethods = Object.create(originArray); function defineReactive(obj) { arrayMethods.push = function (...args) { originArray.push.call(this,...args); render(); } obj.__proto__ = arrayMethods; } defineReactive(state); function render(){ app.innerHTML = state; } render(); state.push(4);
2.对于MVVM的理解?
MVVM 是 Model-View-ViewModel 的缩写。
Model代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。
View 代表UI 组件,它负责将数据模型转化成UI 展现出来。
ViewModel 监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model的对象,连接Model和View。
在MVVM架构下,View 和 Model 之间并没有直接的联系,而是通过ViewModel进行交互,Model 和 ViewModel 之间的交互是双向的, 因此View 数据的变化会同步到Model中,而Model 数据的变化也会立即反应到View 上。
ViewModel 通过双向数据绑定把 View 层和 Model 层连接了起来,而View 和 Model 之间的同步工作完全是自动的,无需人为干涉,因此开发者只需关注业务逻辑,不需要手动操作DOM, 不需要关注数据状态的同步问题,复杂的数据状态维护完全由 MVVM 来统一管理。
3.Vue的生命周期
beforeCreate(创建前) 在数据观测和初始化事件还未开始
created(创建后) 完成数据观测,属性和方法的运算,初始化事件,$el属性还没有显示出来
beforeMount(载入前) 在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。注意此时还没有挂载html到页面上。
mounted(载入后) 在el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html页面中。此过程中进行ajax交互。
beforeUpdate(更新前) 在数据更新之前调用,发生在虚拟DOM重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
updated(更新后) 在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。调用时,组件DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
beforeDestroy(销毁前) 在实例销毁之前调用。实例仍然完全可用。
destroyed(销毁后) 在实例销毁之后调用。调用后,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。
1.什么是vue生命周期?
答: Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。
2.vue生命周期的作用是什么?
答:它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
3.vue生命周期总共有几个阶段?
答:它可以总共分为8个阶段:创建前/后, 载入前/后,更新前/后,销毁前/销毁后。
4.第一次页面加载会触发哪几个钩子?
答:会触发 下面这几个beforeCreate, created, beforeMount, mounted 。
5.DOM 渲染在 哪个周期中就已经完成?
答:DOM 渲染在 mounted 中就已经完成了。
扩展:$nextTick();
补充:
生命周期钩子是如何实现的?
Vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法(合并钩子)
生命周期钩子函数在内部会转换成数组,需要调用的时候回去遍历数组,依次执行
内部主要是使用callHook方法来调用对应的方法。核心是一个发布订阅模式,将钩子订阅好(内部采用数组的方式存储),在对应的阶段进行发布
function mergeHook(parentVal, childValue) { if (childValue) { if (parentVal) { return parentVal.concat(childValue); } else { return [childValue] } } else { return parentVal; } } function mergeOptions(parent, child) { let opts = {}; for (let key in child) { opts[key] = mergeHook(parent[key], child[key]); } return opts; } function callHook(vm, key) { vm.options[key].forEach(hook => hook()); } function Vue(options) { this.options = mergeOptions(this.constructor.options, options); callHook(this, 'beforeCreate'); } Vue.options = {} new Vue({ beforeCreate() { console.log('before create') } })
4.Vue组件间的参数传递
-
props和$emit 父组件向子组件传递数据是通过prop传递的,子组件传递数据给父组件是通过$emit触发事件来做到的
-
$parent,$children 获取当前组件的父组件和当前组件的子组件
-
$attrs
和$listeners
A->B->C。Vue 2.4 开始提供了$attrs
和$listeners
来解决这个问题 -
父组件中通过
provide
来提供变量,然后在子组件中通过inject
来注入变量。 -
$refs
获取实例 -
envetBus
平级组件数据传递 这种情况下可以使用中央事件总线的方式,项目比较小时,用这个比较合适。 -
vuex
状态管理
注意:$attrs主要的作用就是实现批量传递数据。provide/inject更适合应用在插件中,主要是实现跨级数据传递
vuex 定义一个store用来存储和管理状态(数据)
使用步骤
state
//1.安装vuex依赖包,
npm install vuex --save //2.导入vuex包 import Vuex from 'vuex'; vue.use(Vuex); //3.创建store对象 const store=new Vuex.store({ state:{count:0} })
export default store;
4.将store对象挂载到路由
new Vue({ el:"#app", render:h=>h(app), router, store })
组件访问vuex中state中数据的方式一:
{{this.$store.state.全局数据名称}}
<template>
<div>count值为:{{this.$store.state.count}}<div>
<template>
组件访问vuex中state中数据的方式二:
computed:{
...mapState(['全局数据名称'])
}
<template> <div>count值为:{{count}}<div> <template> <script> import {mapState} from 'vuex' export default{ data(){ return {} }, computed:{ ...mapState(['count']) } } </script>
mutation
mutation 用于变更state中的数据;只能使用mutation变更不可以直接在组件内部修改,因为这样可以监控所有数据的变化,
const store = new Vuex.Store({ //store实例 state: { count: 0 }, mutations: { add (state) { state.count++ } } })
组件内 methods:{ handel(){ this.$store.commit('add') } }
action 触发action异步任务
const store = new Vuex.Store({ //store实例 state: { count: 0 }, mutations: { add (state) { state.count++ } }, actions:{ addAsync(content){ setTimeout(()=>{ content.commit('add') },1000) } } })
组件内 methods:{ handel(){ this.$store.dispatch('addAsync') } }
Getter
Getter用于对state的数据进行处理形成新的数据,不会修改state中的原数据,只起到包装的作用,类似于vue的计算属性
const store = new Vuex.Store({ //store实例 state: { count: 0 }, getters:{ showNum: state=>{ return '当前的最新数据是['+state.count+']' } } })
组件内
<template> <div>count值为:{{this.$store.getters.showNum}}<div>
<template>
5.Vue的路由
vue路由hash模式和history模式实现原理分别是什么,他们的区别是什么?
- hash模式:
hash
+hashChange
兼容性好但是不美观 - history模式 :
historyApi
+popState
虽然美观,但是刷新会出现404需要后端进行配置
补充:
-
hash 模式:
- #后面 hash 值的变化,不会导致浏览器向服务器发出请求,浏览器不发出请求,就不会刷新页面
- 通过监听 hashchange 事件可以知道 hash 发生了哪些变化,然后根据 hash 变化来实现更新页面部分内容的操作。
-
history 模式:
- history 模式的实现,主要是 HTML5 标准发布的两个 API,pushState 和replaceState,这两个 API 可以在改变 url,但是不会发送请求。这样就可以监听 url 变化来实现更新页面部分内容的操作
-
区别
- url 展示上,hash 模式有“#”,history 模式没有
- 刷新页面时,hash 模式可以正常加载到 hash 值对应的页面,而 history 没有处理的话,会返回 404,一般需要后端将所有页面都配置重定向到首页路由
- 兼容性,hash 可以支持低版本浏览器和 IE。
路由懒加载是什么意思?如何实现路由懒加载?
-
路由懒加载的含义:把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件
-
实现:
-
1、vue异步组件实现路由懒加载
component:resolve=>(['需要加载的路由的地址',resolve])
2、es6提出的import(推荐使用这种方式)
const HelloWorld = ()=>import('需要加载的模块地址')
-
import Vue from 'vue' import Router from 'vue-router' import Home from "../view/Home.vue" Vue.use(Router) const HelloWorld = ()=>import("../components/HelloWorld") export default new Router({ routes: [ { path: '/', name: 'Home', component:Home }, { path: '/hello', name: 'HelloWorld', component:HelloWorld } ] })
优点:减少了首屏加载的消耗
Vue-router 导航守卫有哪些
- 全局前置/钩子:beforeEach、beforeResolve、afterEach
- 路由独享的守卫:beforeEnter
- 组件内的守卫:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave
const router = new VueRouter({ ... }) router.beforeEach((to, from, next) => { if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' }) // 如果用户未能验证身份,则 `next` 会被调用两次 next() })
6.Vue与React的区别?
-
相似之处:
- 用于创建UI的js库
- 使用起来轻快便捷
- 都用了虚拟DOM
- 都是基于组件的架构
不同点 :
- vue使用的html模板;react使用的是js
- vue有双向绑定语法
- vue增加了语法糖computed和watch等,react需要自己写逻辑来实现
- react用了jsx语法
- react整体思路是编程式,推荐组件化,数据不变,单向数据流;vue数据可变,双向绑定,声明式的写法
7.vue路由的钩子函数
首页可以控制导航跳转,beforeEach,afterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。
beforeEach主要有3个参数to,from,next:
to:route即将进入的目标路由对象,
from:route当前导航正要离开的路由
next:function一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转。
8.vuex是什么?怎么使用?哪种功能场景使用它?
只用来读取的状态集中放在store中; 改变状态的方式是提交mutations,这是个同步的事物; 异步逻辑应该封装在action中。
在main.js引入store,注入。新建了一个目录store,….. export 。
场景有:单页应用中,组件之间的状态、音乐播放、登录状态、加入购物车
Vuex 使用单一状态树,即每个应用将仅仅包含一个store 实例,
-
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
有 5 种,分别是 state、getter、mutation、action、module
state
定义了应用状态的数据结构,可以在这里设置默认的初始状态。
getters
允许组件从 Store 中获取数据,类似vue的计算属性,主要用来过滤一些数据。
mutations
是唯一更改 store 中状态的方法,且必须是同步函数。
action
用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
const store = new Vuex.Store({ //store实例 state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { context.commit('increment') } } })
modules
许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
const moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: { ... }, mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { a: moduleA, b: moduleB })
什么情况下使用 Vuex?
- 如果应用够简单,最好不要使用 Vuex,一个简单的 store 模式即可
- 需要构建一个中大型单页应用时,使用Vuex能更好地在组件外部管理状态
Vuex和单纯的全局对象有什么区别?
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
为什么 Vuex 的 mutation 中不能做异步操作?
- Vuex中所有的状态更新的唯一途径都是mutation,异步操作通过 Action 来提交 mutation实现,这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
- 每个mutation执行完成后都会对应到一个新的状态变更,这样devtools就可以打个快照存下来,然后就可以实现 time-travel 了。如果mutation支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
新增:vuex的action有返回值吗?返回的是什么?
- store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise
- Action 通常是异步的,要知道 action 什么时候结束或者组合多个 action以处理更加复杂的异步流程,可以通过定义action时返回一个promise对象,就可以在派发action的时候就可以通过处理返回的 Promise处理异步流程
一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。
新增:为什么不直接分发mutation,而要通过分发action之后提交 mutation变更状态
- mutation 必须同步执行,我们可以在 action 内部执行异步操作
- 可以进行一系列的异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)
9、vue-cli如何新增自定义指令?
1.创建局部指令
var app = new Vue({ el: '#app', data: { }, // 创建指令(可以多个) directives: { // 指令名称 dir1: { inserted(el) { // 指令中第一个参数是当前使用指令的DOM console.log(el); console.log(arguments); // 对DOM进行操作 el.style.width = '200px'; el.style.height = '200px'; el.style.background = '#000'; } } } })
2.全局指令
Vue.directive('dir2', {
inserted(el) {
console.log(el);
}
})
3.指令的使用
<div id="app"> <div v-dir1></div> <div v-dir2></div> </div>
10、vue如何自定义一个过滤器?
html代码:
<div id="app">
<input type="text" v-model="msg" />
{{msg| capitalize }}
</div>
JS代码:
var vm=new Vue({
el:"#app",
data:{
msg:''
},
filters: {
capitalize: function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
}
}
})
全局定义过滤器
Vue.filter('capitalize', function (value) {
if (!value) return ''
value = value.toString()
return value.charAt(0).toUpperCase() + value.slice(1)
})
过滤器接收表达式的值 (msg) 作为第一个参数。capitalize 过滤器将会收到 msg的值作为第一个参数。
11、对keep-alive 的了解?
keep-alive是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
未添加了keep-alive标签的组件,路由在每次切换时,都会重新执行首次created()到destory()的;
添加了keep-alive标签的组件,路由在每次切换时,只会执行首次created()中的方法,并且不会执行到destory()生命周期
在vue 2.1.0 版本之后,keep-alive新加入了两个属性: include(包含的组件缓存) 与 exclude(排除的组件不缓存,优先级大于include) 。
使用方法
<keep-alive include='include_components' exclude='exclude_components'> <component> <!-- 该组件是否缓存取决于include和exclude属性 --> </component> </keep-alive>
参数解释
include - 字符串或正则表达式,只有名称匹配的组件会被缓存
exclude - 字符串或正则表达式,任何名称匹配的组件都不会被缓存
include 和 exclude 的属性允许组件有条件地缓存。二者都可以用“,”分隔字符串、正则表达式、数组。当使用正则或者是数组时,要记得使用v-bind 。
使用示例
<!-- 逗号分隔字符串,只有组件a与b被缓存。 --> <keep-alive include="a,b"> <component></component> </keep-alive> <!-- 正则表达式 (需要使用 v-bind,符合匹配规则的都会被缓存) --> <keep-alive :include="/a|b/"> <component></component> </keep-alive> <!-- Array (需要使用 v-bind,被包含的都会被缓存) --> <keep-alive :include="['a', 'b']"> <component></component> </keep-alive>
12、computed 和 watch 的区别和运用的场景?
- computed:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
- watch:没有缓存性,更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听
- 运用场景:
- 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
- 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用watch选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
13、什么是 mixin ?
- Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。
- 如果你希望再多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
- 然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
补充:Vue.mixin
的使用场景和原理?
Vue.mixin的作用就是抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用mergeOptions方法进行合并,采用策略模式针对不同的属性进行合并。如果混入的数据和本身组件中的数据冲突,会采用“就近原则”以组件的数据为准。
mixin中有很多缺陷 "命名冲突问题"、"依赖问题"、"数据来源问题",这里强调一下mixin的数据是不会被共享的!
14、在 Vue 实例中编写生命周期 hook 或其他 option/properties 时,为什么不使用箭头函数 ?
- 箭头函数自已没有定义 this 上下文中。
- 当你在 Vue 程序中使用箭头函数 ( => ) 时,this 关键字病不会绑定到 Vue 实例,因此会引发错误。所以强烈建议改用标准函数声明。
15、Vue模版编译原理知道吗,能简单说一下吗?
简单说,Vue的编译过程就是将template转化为render函数的过程。会经历以下阶段(生成AST树/优化/codegen):vue-loader依赖vue-template-compiler包
template=>ast =>codegen=>with+Function 实现render方法=>render执行完毕后生成的就是虚拟dom=>代码生成,生成真实dom
- 首先解析模版,生成AST语法树(一种用JavaScript对象的形式来描述整个模板)。使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
- Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。
- 编译的最后一步是将优化后的AST树转换为可执行的代码。
<script src="./node_modules/vue-template-compiler/browser.js"></script> <script> let { ast, render } = VueTemplateCompiler.compile('<div>hello world</div>'); console.log(ast, render); const fn = new Function(render); console.log(fn.toString()); </script>
16、diff算法说一下
- 同级比较,再比较子节点
- 先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较都有子节点的情况(核心diff)
- 递归比较子节点
补充:Vue
中的diff
原理
Vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。
- 1.先比较是否是相同节点
- 2.相同节点比较属性,并复用老节点
- 3.比较儿子节点,考虑老节点和新节点儿子的情况
- 4.优化比较:头头、尾尾、头尾、尾头
- 5.比对查找进行复用
17、说说你对keep-alive组件的了解
-
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
- 一般结合路由和动态组件一起使用,用于缓存组件;
- 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
- 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。
18、说说你对SSR的了解
-
SSR也就是服务端渲染,也就是将Vue在客户端把标签渲染成HTML的工作放在服务端完成,然后再把html直接返回给客户端
-
SSR的优势
- 更好的SEO
- 首屏加载速度更快
-
SSR的缺点
- 开发条件会受到限制,服务器端渲染只支持beforeCreate和created两个钩子
- 当我们需要一些外部扩展库时需要特殊处理,服务端渲染应用程序也需要处于Node.js的运行环境
- 更多的服务端负载
19、你都做过哪些Vue的性能优化?
-
编码阶段
- 尽量减少data中的数据,data中的数据都会增加getter和setter,会收集对应的watcher
- v-if和v-for不能连用
- 如果需要使用v-for给每项元素绑定事件时使用事件代理
- SPA 页面采用keep-alive缓存组件
- 在更多的情况下,使用v-if替代v-show
- key保证唯一
- 使用路由懒加载、异步组件
- 防抖、节流
- 第三方模块按需导入
- 长列表滚动到可视区域动态加载
- 图片懒加载
-
SEO优化
- 预渲染
- 服务端渲染SSR
-
打包优化
- 压缩代码
- Tree Shaking/Scope Hoisting
- 使用cdn加载第三方模块
- 多线程打包happypack
- splitChunks抽离公共文件
- sourceMap优化
-
用户体验
- 骨架屏
- PWA
- 还可以使用缓存(客户端缓存、服务端缓存)优化、服务端开启gzip压缩等。
20、vue2.x中如何监测数组变化?
- 使用了函数劫持的方式,重写了数组的方法,Vue将data中的数组进行了原型链重写,指向了自己定义的数组原型方法,当调用数组api时,可以通知依赖更新。
- 如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。
21、说说你对 SPA 单页面的理解,它的优缺点分别是什么?
-
SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。
-
优点:
- 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
- 基于上面一点,SPA 相对对服务器压力小;
- 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
-
缺点:
- 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScript、CSS 统一加载,部分页面按需加载;
- 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
- SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势。
22、对于即将到来的 vue3.0 特性你有什么了解的吗?
-
监测机制的改变
- 3.0 将带来基于代理 Proxy的 observer 实现,提供全语言覆盖的反应性跟踪。
- 消除了 Vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
-
只能监测属性,不能监测对象
- 检测属性的添加和删除;
- 检测数组索引和长度的变更;
- 支持 Map、Set、WeakMap 和 WeakSet。
-
模板
- 模板方面没有大的变更,只改了作用域插槽,2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0 把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。
- 同时,对于 render 函数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom 。
-
对象式的组件声明方式
- vue2.x 中的组件是通过声明的方式传入一系列 option,和 TypeScript 的结合需要通过一些装饰器的方式来做,虽然能实现功能,但是比较麻烦。
- 3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 TypeScript 的结合变得很容易
-
其它方面的更改
- 支持自定义渲染器,从而使得 weex 可以通过自定义渲染器的方式来扩展,而不是直接 fork 源码来改的方式。
- 支持 Fragment(多个根节点)和 Protal(在 dom 其他部分渲染组建内容)组件,针对一些特殊的场景做了处理。
- 基于 tree shaking 优化,提供了更多的内置功能。
23.Vue
为什么需要虚拟DOM?
Virtual DOM就是用js对象来描述真实DOM,是对真实DOM的抽象,由于直接操作DOM性能低但是js层的操作效率高,可以将DOM操作转化成对象操作,最终通过diff算法比对差异进行更新DOM(减少了对真实DOM的操作)。虚拟DOM不依赖真实平台环境从而也可以实现跨平台。
虚拟DOM的实现就是普通对象包含tag、data、children等属性对真实节点的描述。(本质上就是在JS和DOM之间的一个缓存)
24.Vue.set
方法是如何实现的?
为什么$set可以触发更新,我们给对象和数组本身都增加了dep属性。当给对象新增不存在的属性则触发对象依赖的watcher去更新,当修改数组索引时我们调用数组本身的splice方法去更新数组
25.vue的事件修饰符
.stop
:等同于JavaScript中的event.stopPropagation()
,防止事件冒泡.prevent
:等同于JavaScript中的event.preventDefault()
,防止执行预设的行为(如果事件可取消,则取消该事件,而不停止事件的进一步传播).capture
:与事件冒泡的方向相反,事件捕获由外到内.self
:只会触发自己范围内的事件,不包含子元素.once
:只会触发一次
一句话就能回答的面试题
1.css只在当前组件起作用
答:在style标签中写入scoped即可 例如:<style scoped></style>
2.v-if 和 v-show 区别
答:
- v-if 如果条件不成立不会渲染当前指令所在节点的 dom 元素
- v-show 只是切换当前 dom 的显示或者隐藏
v-if在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。v-show会被编译成指令,条件不满足时控制样式将对应节点隐藏 (内部其他指令依旧会继续执行)
扩展回答: 频繁控制显示隐藏尽量不使用v-if,v-if和v-for尽量不要连用
3.$route
和$router
的区别
答:$route
是“路由信息对象”,包括path,params,hash,query,fullPath,matched,name等路由信息参数。而$router
是“路由实例”对象包括了路由的跳转方法,钩子函数等。
4.vue.js的两个核心是什么?
答:数据驱动、组件系统
5.vue几种常用的指令
答:v-for 、 v-if 、v-bind、v-on、v-show、v-else
6.vue常用的修饰符?
答:.prevent: 提交事件不再重载页面;.stop: 阻止单击事件冒泡;.self: 当事件发生在该元素本身而不是子元素的时候会触发;.capture: 事件侦听,事件发生的时候会调用
7.v-on 可以绑定多个方法吗?
答:可以
8.vue中 key 值的作用?
答:当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素。key的作用主要是为了高效的更新虚拟DOM。
9.什么是vue的计算属性?
答:在模板中放入太多的逻辑会让模板过重且难以维护,在需要对数据进行复杂处理,且可能多次使用的情况下,尽量采取计算属性的方式。好处:①使得数据处理结构清晰;②依赖于数据,数据更新,处理结果自动更新;③计算属性内部this指向vm实例;④在template调用时,直接写计算属性名即可;⑤常用的是getter方法,获取数据,也可以使用set方法改变数据;⑥相较于methods,不管依赖的数据变不变,methods都会重新计算,但是依赖数据不变的时候computed从缓存中获取,不会重新计算。
10.vue等单页面应用及其优缺点
答:优点:Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好。
缺点:不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。
11.怎么定义 vue-router 的动态路由? 怎么获取传过来的值
答:在 router 目录下的 index.js 文件中,对 path 属性加上 /:id,使用 router 对象的 params.id 获取。
12.为什么 v-for 和 v-if 不建议用在一起
- 当 v-for 和 v-if 处于同一个节点时,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。如果要遍历的数组很大,而真正要展示的数据很少时,这将造成很大的性能浪费
- 这种场景建议使用 computed,先对数据进行过滤
- 或者使用template标签并添加v-if,
13.组件中的data为什么是一个函数?
- 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。
- 如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数。
14.子组件为什么不可以修改父组件传递的Prop?/怎么理解vue的单向数据流?
- Vue提倡单向数据流,即父级props的更新会流向子组件,但是反过来则不行。
- 这是为了防止意外的改变父组件状态,使得应用的数据流变得难以理解。
- 如果破坏了单向数据流,当应用复杂时,debug 的成本会非常高。
15.nextTick的实现原理是什么?
- 在下次 DOM 更新循环结束之后执行延迟回调,在修改数据之后立即使用 nextTick 来获取更新后的 DOM。
- nextTick主要使用了宏任务和微任务。
- 根据执行环境分别尝试采用Promise、MutationObserver、setImmediate,如果以上都不行则采用setTimeout定义了一个异步方法,多次调用nextTick会将方法存入队列中,通过这个异步方法清空当前队列。
16.Vue
中slot是如何实现的?什么时候用它?
普通插槽(模板传入到组件中,数据采用父组件数据)和作用域插槽(在父组件中访问子组件数据)
17.keep-alive
平时在哪使用?原理是?
keep-alive
主要是缓存,采用的是LRU
算法。 最近最久未使用法。
React进阶篇
https://segmentfault.com/a/1190000023221705
阮一峰
http://www.ruanyifeng.com/blog/2020/09/react-hooks-useeffect-tutorial.html
1.React Hooks
钩子(hook)
一、React 的两套 API
以前,React API 只有一套,现在有两套:类(class)API 和基于函数的钩子(hooks) API。
官方推荐使用钩子(函数),而不是类。因为钩子更简洁,代码量少,用起来比较"轻",而类比较"重"。而且,钩子是函数,更符合 React 函数式的本质。
二、类和函数的差异
严格地说,类组件和函数组件是有差异的。不同的写法,代表了不同的编程方法论。
类(class)是数据和逻辑的封装。 也就是说,组件的状态和操作方法是封装在一起的。如果选择了类的写法,就应该把相关的数据和操作,都写在同一个 class 里面。
函数一般来说,只应该做一件事,就是返回一个值。 如果你有多个操作,每个操作应该写成一个单独的函数。而且,数据的状态应该与操作方法分离。根据这种理念,React 的函数组件只应该做一件事情:返回组件的 HTML 代码,而没有其他的功能。
只进行单纯的数据计算(换算)的函数,在函数式编程里面称为 "纯函数"(pure function)。
三、副效应是什么?
函数式编程将那些跟数据计算无关的操作(比如生成日志、储存数据、改变应用状态等等),都称为 "副效应" (side effect) 。如果函数内部直接包含产生副效应的操作,就不再是纯函数了,我们称之为不纯的函数。
纯函数内部只有通过间接的手段(即通过其他函数调用),才能包含副效应。
四、钩子(hook)的作用
钩子(hook)就是 React 函数组件的副效应解决方案,用来为函数组件引入副效应。
函数组件的主体只应该用来返回组件的 HTML 代码,所有的其他操作(副效应)都必须通过钩子引入。
由于副效应非常多,所以钩子有许多种。React 为许多常见的操作(副效应),都提供了专用的钩子。
useState()
:保存状态useContext()
:保存上下文useRef()
:保存引用-
useReducer() //action 钩子
-
useEffect() //副作用钩子
- ......
上面这些钩子,都是引入某种特定的副效应,而 useEffect()
是通用的副效应钩子 。找不到对应的钩子时,就可以用它。其实,从名字也可以看出来,它跟副效应(side effect)直接相关。
五、useEffect() 的用法
useEffect()
本身是一个函数,由 React 框架提供,在函数组件内部调用即可。
举例来说,我们希望组件加载以后,网页标题(document.title
)会随之改变。那么,改变网页标题这个操作,就是组件的副效应,必须通过useEffect()
来实现。
import React, { useEffect } from 'react'; function Welcome(props) { useEffect(() => { document.title = '加载完成'; }); return <h1>Hello, {props.name}</h1>; }
上面例子中,useEffect()
的参数是一个函数,它就是所要完成的副效应(改变网页标题)。组件加载以后,React 就会执行这个函数。
useEffect()
的作用就是指定一个副效应函数,组件每渲染一次,该函数就自动执行一次。组件首次在网页 DOM 加载后,副效应函数也会执行。
六、useEffect() 的第二个参数
有时候,我们不希望useEffect()
每次渲染都执行,这时可以使用它的第二个参数,使用一个数组指定副效应函数的依赖项,只有依赖项发生变化,才会重新渲染。
function Welcome(props) { useEffect(() => { document.title = `Hello, ${props.name}`; }, [props.name]); return <h1>Hello, {props.name}</h1>; }
上面例子中,useEffect()
的第二个参数是一个数组,指定了第一个参数(副效应函数)的依赖项(props.name
)。只有该变量发生变化时,副效应函数才会执行。
如果第二个参数是一个空数组,就表明副效应参数没有任何依赖项。因此,副效应函数这时只会在组件加载进入 DOM 后执行一次,后面组件重新渲染,就不会再次执行。这很合理,由于副效应不依赖任何变量,所以那些变量无论怎么变,副效应函数的执行结果都不会改变,所以运行一次就够了。
七、useEffect() 的用途
只要是副效应,都可以使用useEffect()
引入。它的常见用途有下面几种。
- 获取数据(data fetching)
- 事件监听或订阅(setting up a subscription)
- 改变 DOM(changing the DOM)
- 输出日志(logging)
下面是从远程服务器获取数据的例子。
import React, { useState, useEffect } from 'react'; import axios from 'axios'; function App() { const [data, setData] = useState({ hits: [] }); useEffect(() => { const fetchData = async () => { const result = await axios( 'https://hn.algolia.com/api/v1/search?query=redux', ); setData(result.data); }; fetchData(); }, []); return ( <ul> {data.hits.map(item => ( <li key={item.objectID}> <a href={item.url}>{item.title}</a> </li> ))} </ul> ); } export default App;
上面例子中,useState()
用来生成一个状态变量(data
),保存获取的数据;useEffect()
的副效应函数内部有一个 async 函数,用来从服务器异步获取数据。拿到数据以后,再用setData()
触发组件的重新渲染。
由于获取数据只需要执行一次,所以上例的useEffect()
的第二个参数为一个空数组。
八、useEffect() 的返回值
副效应是随着组件加载而发生的,那么组件卸载时,可能需要清理这些副效应。
useEffect()
允许返回一个函数,在组件卸载时,执行该函数,清理副效应。如果不需要清理副效应,useEffect()
就不用返回任何值。
useEffect(() => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source]);
上面例子中,useEffect()
在组件加载时订阅了一个事件,并且返回一个清理函数,在组件卸载时取消订阅。
实际使用中,由于副效应函数默认是每次渲染都会执行,所以清理函数不仅会在组件卸载时执行一次,每次副效应函数重新执行之前,也会执行一次,用来清理上一次渲染的副效应。
九、useEffect() 的注意点
使用useEffect()
时,有一点需要注意。如果有多个副效应,应该调用多个useEffect()
,而不应该合并写在一起。
function App() { const [varA, setVarA] = useState(0); const [varB, setVarB] = useState(0); useEffect(() => { const timeoutA = setTimeout(() => setVarA(varA + 1), 1000); const timeoutB = setTimeout(() => setVarB(varB + 2), 2000); return () => { clearTimeout(timeoutA); clearTimeout(timeoutB); }; }, [varA, varB]); return <span>{varA}, {varB}</span>; }
上面的例子是错误的写法,副效应函数里面有两个定时器,它们之间并没有关系,其实是两个不相关的副效应,不应该写在一起。正确的写法是将它们分开写成两个useEffect()
。
function App() { const [varA, setVarA] = useState(0); const [varB, setVarB] = useState(0); useEffect(() => { const timeout = setTimeout(() => setVarA(varA + 1), 1000); return () => clearTimeout(timeout); }, [varA]); useEffect(() => { const timeout = setTimeout(() => setVarB(varB + 2), 2000); return () => clearTimeout(timeout); }, [varB]); return <span>{varA}, {varB}</span>; }
2.React Hooks四个常用的钩子
useState() //状态钩子 useContext() //共享状态钩子 useReducer() //action 钩子 useEffect() //副作用钩子
3.vuex和redux的不同
1)vuex是redux的基础上进行改变,对仓库的管理更加明确
2)使用mutation来替换redux中的reducer
3) vuex有自动渲染的功能,所以不需要更新
4)Redux 、Vuex 均为单向数据流
5)Redux 和 Vuex 是基于 Flux 的,Redux 较为泛用,Vuex 只能用于 vue
4.redux
redux 是一个应用数据流框架,主要是解决了组件间状态共享的问题,原理是集中式管理,主要有三个核心方法,action,store,reducer
工作流程是:
-
view 用 actionCreator 创建一个 action,里面可能包含一些数据
-
使用 store 的 dispatch 方法将 acion 传入 store
-
store 将 action 与旧的 state 转发给 reducer
-
reducer 深拷贝 state,并返回一个新的 state 给 store
-
store 接收并更新 state
-
使用 store.subscribe 订阅更新,重新 render 组件
reducer 为什么是纯函数?
微信小程序
1、微信小程序有几个文件
- WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构。内部主要是微信自己定义的一套组件
- WXSS (WeiXin Style Sheets)是一套样式语言,用于描述 WXML 的组件样式
- js 逻辑处理,网络请求
- json 小程序设置,如页面注册,页面标题及tabBar
2、微信小程序怎样跟事件传值
给 HTML 元素添加 data-*属性来传递我们需要的值,然后通过 e.currentTarget.dataset或 <onload的 param参数获取。但 data - 名称不能有大写字母和不可以存放对象
3、小程序的 wxss 和 css 有哪些不一样的地方?
- wxss的图片引入需使用外链地址
- 没有 Body;样式可直接使用 import 导入
4、小程序关联微信公众号如何确定用户的唯一性
使用 wx.getUserInfo 方法 withCredentials 为 true 时 可获取 encryptedData,里面有 union_id。后端需要进行对称解密
5、微信小程序与vue区别
- 生命周期不一样,微信小程序生命周期比较简单
- 数据绑定也不同,微信小程序数据绑定需要使用{{}},vue 直接:就可以
显示与隐藏元素,vue中,使用 v-if 和 v-show
- 控制元素的显示和隐藏,小程序中,使用wx-if 和hidden 控制元素的显示和隐藏
- 事件处理不同,小程序中,全用 bindtap(bind+event),或者 catchtap(catch+event) 绑定事件,vue:使用 v-on:event绑定事件,或者使用@event 绑定事件
- 数据双向绑定也不也不一样在 vue中,只需要再表单元素上加上 v-model,然后再绑定 data中对应的一个值,当表单元素内容发生变化时,data中对应的值也会相应改变,这是 vue非常 nice 的一点。微信小程序必须获取到表单元素,改变的值,然后再把值赋给一个 data中声明的变量。
6、小程序的双向绑定和vue哪里不一样
小程序直接 this.data 属性是不可以同步到视图的,必须调用:
this.setData({
// 这里设置
})
7、简述微信小程序原理
- 微信小程序采用 JavaScript、WXML、WXSS 三种技术进行开发,本质就是一个单页面应用,所有的页面渲染和事件处理,都在一个页面内进行,但又可以通过微信客户端调用原生的各种接口
- 微信的架构,是数据驱动的架构模式,它的 UI 和数据是分离的,所有的页面更新,都需要通过对数据的更改来实现
- 小程序分为两个部分 webview 和 appService 。其中 webview 主要用来展现 UI ,appService 有来处理业务逻辑、数据及接口调用。它们在两个进程中运行,通过系统层JSBridge 实现通信,实现 UI 的渲染、事件的处理
8、小程序的生命周期函数
- onLoad页面加载时触发。一个页面只会调用一次,可以在 onLoad 的参数中获取打开当前页面路径中的参数
- onShow() 页面显示/切入前台时触发
- onReady() 页面初次渲染完成时触发。一个页面只会调用一次,代表页面已经准备妥当,可以和视图层进行交互
- onHide() 页面隐藏/切入后台时触发。 如 navigateTo 或底部 tab 切换到其他页面,小程序切入后台等
- onUnload() 页面卸载时触发。如 redirectTo 或 navigateBack 到其他页面时
9、哪些方法可以用来提高微信小程序的应用速度
1、提高页面加载速度
2、用户行为预测
3、减少默认 data 的大小
4、组件化方案
10、微信小程序的优劣势
优势
即用即走,不用安装,省流量,省安装时间,不占用桌面
依托微信流量,天生推广传播优势
开发成本比 App 低
缺点
用户留存,即用即走是优势,也存在一些问题
入口相对传统 App 要深很多
限制较多,页面大小不能超过2M。不能打开超过10个层级的页面
11、怎么解决小程序的异步请求问题
- 小程序支持大部分 ES6 语法
- 在返回成功的回调里面处理逻辑Promise异步
12、如何实现下拉刷新
首先在全局 config 中的 window 配置enablePullDownRefresh
,在 Page 中定义onPullDownRefresh钩子函数,到达下拉刷新条件后,该钩子函数执行,发起请求方法
请求返回后,调用 wx.stopPullDownRefresh停止下拉刷新
13、bindtap和catchtap的区别是什么
相同点:首先他们都是作为点击事件函数,就是点击时触发。在这个作用上他们是一样的,可以不做区分
不同点:他们的不同点主要是bindtap是不会阻止冒泡事件的,catchtap是阻值冒泡的
14、小程序页面间有哪些传递数据的方法
1、使用全局变量实现数据传递。在 app.js 文件中定义全局变量 globalData, 将需要存储的信息存放在里面
2、使用wx.navigateTo 与 wx.redirectTo 的时候,可以将部分数据放在 url 里面,并在新页面 onLoad 的时候初始化
3、使用本地缓存 Storage 相关
15、小程序wxml与标准的html的异同?
相同:
- 都是用来描述页面的结构;
- 都由标签、属性等构成;
不同</font>
- 标签名字不一样,且小程序标签更少,单一标签更多;
- 多了一些 wx:if 这样的属性以及 {{ }} 这样的表达式
- WXML仅能在微信小程序开发者工具中预览,而HTML可以在浏览器内预览;
- 组件封装不同, WXML对组件进行了重新封装,
- 小程序运行在JS Core内,没有DOM树和window对象,小程序中无法使用window对象和document对象。
16、小程序简单介绍下三种事件对象的属性列表?
基础事件(BaseEvent)
- type:事件类型
- timeStamp:事件生成时的时间戳
- target:触发事件的组件的属性值集合
- currentTarget:当前组件的一些属性集合
自定义事件(CustomEvent)
- detail
触摸事件(TouchEvent)
- touches
- changedTouches
17、小程序对wx:if 和 hidden使用的理解?
- wx:if 有更高的切换消耗。
- hidden 有更高的初始渲染消耗。
- 因此,如果需要频繁切换的情景下,用 hidden 更好,如果在运行时条件不大可能改变则 wx:if 较好。
18、微信小程序与H5的区别?
- 运行环境的不同
传统的HTML5的运行环境是浏览器,包括webview,而微信小程序的运行环境并非完整的浏览器,是微信开发团队基于浏览器内核完全重构的一个内置解析器,针对小程序专门做了优化,配合自己定义的开发语言标准,提升了小程序的性能。
- 开发成本的不同
只在微信中运行,所以不用再去顾虑浏览器兼容性,不用担心生产环境中出现不可预料的奇妙BUG
- 获取系统级权限的不同
19、app.json 是对当前小程序的全局配置,讲述三个配置各个项的含义?
- pages字段 —— 用于描述当前小程序所有页面路径,这是为了让微信客户端知道当前你的小程序页面定义在哪个目录。
- window字段 —— 小程序所有页面的顶部背景颜色,文字颜色定义在这里的
- tab字段—小程序全局顶部或底部tab
20、小程序onPageScroll方法的使用注意什么?
由于此方法调用频繁,不需要时,可以去掉,不要保留空方法,并且使用onPageScroll时,尽量避免使用setData(),尽量减少setData()的使用频次。
21、小程序视图渲染结束回调?
使用setData(data, callback),在callback回调方法中添加后续操作代码
22、小程序同步API和异步API使用时注意事项?
wx.setStorageSync是以Sync结尾的API为同步API,使用时使用try-catch来查看异常,如果判定API为异步,可以在其回调方法success、fail、complete中进行下一步操作。
23、简述下 wx.navigateTo(), wx.redirectTo(), wx.switchTab(), wx.navigateBack(), wx.reLaunch()的区别</h5>
- wx.navigateTo():保留当前页面,跳转到应用内的某个页面。但是不能跳到 tabbar 页面
- wx.redirectTo():关闭当前页面,跳转到应用内的某个页面。但是不允许跳转到 tabbar 页面
- wx.switchTab():跳转到 abBar 页面,并关闭其他所有非 tabBar 页面
- wx.navigateBack():关闭当前页面,返回上一页面或多级页面。可通过getCurrentPages()获取当前的页面栈,决定需要返回几层
- wx.reLaunch():关闭所有页面,打开到应用内的某个页面
24、如何封装微信小程序的数据请求的?
1、将所有的接口放在统一的js文件中并导出。
2、在app.js中创建封装请求数据的方法。
3、在子页面中调用封装的方法请求数据。
25、小程序与原生App哪个好?
小程序除了拥有公众号的低开发成本、低获客成本低以及无需下载等优势,在服务请求延时与用户使用体验是都得到了较大幅度 的提升,使得其能够承载跟复杂的服务功能以及使用户获得更好的用户体验。
26、webview中的页面怎么跳回小程序中?
首先要引入最新版的jweixin-x.x.x.js,然后
wx.miniProgram.navigateTo({
url: '/pages/login/login'+'$params'
})
27、小程序关联微信公众号如何确定用户的唯一性?
使用wx.getUserInfo方法withCredentials为 true 时 可获取encryptedData,里面有union_id。后端需要进行对称解密。
28、小程序调用后台接口遇到哪些问题?
1.数据的大小有限制,超过范围会直接导致整个小程序崩溃,除非重启小程序;
2.小程序不可以直接渲染文章内容页这类型的html文本内容,若需显示要借住插件,但插件渲染会导致页面加载变慢,所以最好在后台对文章内容的html进行过滤,后台直接处理批量替换p标签div标签为view标签,然后其它的标签让插件来做,减轻前端的时间。
29、webview的页面怎么跳转到小程序导航的页面?
答:小程序导航的页面可以通过switchTab,但默认情况是不会重新加载数据的。若需加载新数据,则在success属性中加入以下代码即可:
success: function (e) {
var page = getCurrentPages().pop();
if (page == undefined || page == null) return;
page.onLoad();
}
webview的页面,则通过
wx.miniProgram.switchTab({
url: '/pages/index/index'
})
30、微信小程序的优劣势?
答:优势:
1、无需下载,通过搜索和扫一扫就可以打开。
2、良好的用户体验:打开速度快。
3、开发成本要比App要低。
4、安卓上可以添加到桌面,与原生App差不多。
5、为用户提供良好的安全保障。小程序的发布,微信拥有一套严格的审查流程,不能通过审查的小程序是无法发布到线上的。
劣势:
1、限制较多。页面大小不能超过1M。不能打开超过5个层级的页面。
2、样式单一。小程序的部分组件已经是成型的了,样式不可以修改。例如:幻灯片、导航。
3、推广面窄,不能分享朋友圈,只能通过分享给朋友,附近小程序推广。其中附近小程序也受到微信的限制。
4、依托于微信,无法开发后台管理功能。