说一说 HTML 语义化
使用语义化标签的意义:
对于开发:语义化标签有着更好的页面结构,有利于代码的开发及后期的维护。
对于用户:当页面卡顿时有着良好的页面结构,有利于增加用户体验。
对于爬虫:有利于搜索引擎的SEO优化,有利于网站更靠前的排名。
语义化标签都有哪些:
header、nav、article(独立的、完整的文章或内容块)、aside、main、footer、h1~h6、ul、ol、li等标签。
说一说 CSS 的盒子模型
在 HTML 页面中所有的元素都可以看成是一个盒子。
盒子的组成:内容 content、内边距 padding、边框 border、外边距 margin。
盒模型的类型:
- 标准盒模型:width = content
- IE 盒模型(怪异盒模型):width = content + padding + border
控制盒模型的模式:box-sizing: content-box(默认值,标准盒模型)、border-box(IE 盒模型)。
说一说样式优先级的规则是什么?
css 样式优先级应该分为五大类:
第一类:!important,它的优先级始终是最高的。
第二类:引入方式,行内样式的优先级高于嵌入和外链,嵌入和外链如果使用的选择器相同就看它们在页面中插入的顺序,在后面的会覆盖前面的。(行内样式、嵌入样式、外链样式)
第三类:选择器,优先级:id 选择器 > (类选择器 | 伪类选择器 | 属性选择器) > 标签选择器 > 通配符选择器。
第四类:继承样式,是所有样式中优先级比较低的。
第五类:浏览器默认样式优先级最低。
能尽量不适用 !important 就不用,特别是在自己封装的组件中。
说一说 CSS 尺寸设置的单位
px:绝对长度单位,它的大小取决于屏幕分辨率,是网页开发中常用的单位。
em:相对长度单位,在 font-size 中使用是相对于父元素的字体大小,其他属性中使用是相对于自身的字体大小,如 width。如果当前元素的字体大小为设置,由于字体可继承的原因,则会逐级向上查找,最终找不到则相对于浏览器默认字体大小。
rem:相对长度单位,相对于根元素的字体大小,如果根元素大小未设置,使用浏览器默认字体大小。
vw:相对长度单位,相对与视口宽度的1%。
vh:相对长度单位,相对于视口高度的1%。
说一说浮动
浮动最早是用来实现文字环绕图片的效果的,后来发现浮动属性在布局上使用有着优势,所以现有很多页面采用浮动来解决布局问题。
开启浮动的属性 float: left / right。
设置了浮动的元素会脱离普通流,而普通流内的元素在计算自身高度时不会去计算浮动元素的高度,这时就有可能会发现父元素高度塌陷的问题,进而影响到父元素后续兄弟元素的排版问题。
解决高度塌陷问题:
1. 清除浮动
主要是为了清除浮动后造成的影响,而不是清除浮动。
- 添加额外标签设置clear:both;属性。 <br style="clear: both" />
- 伪元素设置clear属性。
2. 父元素设置bfc。
bfc 元素的高度计算会将浮动元素的高度也计算在内。所以也可以解决高度塌陷的问题。
bfc 是一个完全独立的容器,内部元素不会影响到外面。
- overflow 不为 visible, 内容溢出容易造成被裁剪的问题。
- flex
- grid
- table 容易集成表格的各种难处理的样式问题。不推荐
- html 本身就是一个bfc容器,但这不现实
- 父元素设置高度。写死高度不推荐。
说一说 BFC
BFC 即 Block Formatting Contexts(块级格式化上下文)。
具有 BFC 特性的元素可以看作是隔离了的独立容器,容器里面的元素在布局上不会影响到外面的元素,并且 BFC 具有普通容器所没有的一些特性。
可以认为 BFC 是一个封闭的盒子,内部元素的变化,不会影响到外面。
触发 BFC 的条件:
- 根元素(HTML 元素,自身就是一个 BFC)
- 浮动元素:float 属性值设置了 right / left
- 绝对定位元素:设置了 position 属性值:absolute / fixed,绝对定位或者固定定位。
- overflow 属性:overflow 属性值不为:visible 时,即指定了 auto、scroll、hidden 等的元素
- display 属性:display 属性值为 inline-block、table-cell、flex、grid 等的元素。
特点:
- 同一个 BFC 下兄弟元素间外边距会发生重叠
将其放入到不同的 BFC 容器中。
- 清除浮动,解决高度塌陷
浮动的元素会脱离文档流,如果他的父元素没有设置高度,就会导致父元素出现高度塌陷的问题。
原因是浮动元素不会参与普通流父元素的高度计算。
而 BFC 容器的高度会计算浮动元素的高度。
说几个未知宽高水平垂直居中的方法
1. absolute + translate
2. absolute + margin
3. flex
4. grid
5. table-cell
说一下三栏布局的实现方案
三栏布局主要为了实现中间一栏优先渲染
- float + margin-left + margin-right
- float + margin + position
- flex布局 + order
结构:
<div class="header"></div>
<div class="content clearfix">
<div class="center"></div>
<div class="left"></div>
<div class="right"></div>
</div>
<div class="footer"></div>
说一说 JS 数据类型有哪些,区别是什么?
js 主要有两种数据类型:基本数据类型 和 复杂数据类型。
基本数据类型存储在栈中,存储的是基本数据的原始值,所以也叫做原始数据类型。
复杂数据类型存储在堆中,存储的是复杂数据类型的地址引用,所有也叫做引用数据类型。
基本数据类型:
在 ES6 之前,基本数据类型有五种:string、number、boolean、undefined、null。以及 ES6 新增的 Symbol,ES10 新增的 BigInt。
Symbol 它表示创建一个独一无二的值,我认为它的出现主要是为了解决可能存在的变量命名重复问题。
BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全的存储和操作大整数,常用在超出 Number 能够表示的安全整数的情况。
Symbol 和 BigInt 都不是构造函数,所以不能被 new。
引用数据类型:
主要是指 Object 类型,Array、Math、Date 等都属于 Object 的子类。
栈和堆的区别:
栈是一种先入后出的数据结构,而堆没有什么规律,它只用一块足够大的空间来存储变量。
栈区内存由编译器自动释放,程序员不需要关心,存放函数的参数值,局部变量的值等。
堆区内存一般由程序员手动释放,如果程序员不释放,程序结束时可能由垃圾回收机制回收。
说一说 null 和 undefined 的区别,如何让一个属性变成 null
undefined 是未定义的意思,表示变量声明了但是还没赋值,null 常用于表示这个变量后续要作为一个对象来使用,作为一个对象占位符。直接将变量设置为 null 也常用于释放对象所占据的内存。
null 属于 Null 类型,undefined 属于 Undefined 类型。
typeof undefined === 'undefined'; typeof null === 'object';
null 的 typeof 值为 “object”,这是一个历史遗留问题。
undefined 是 全局对象的一个属性,window.undefined === 'undefined'。
以下情况会返回 undefined:
1. 当一个变量没有被赋值。
2. 一个函数没有返回值。
3. 访问某个对象内没有的属性。
4. 函数定义了形参但是没有传递实参。
undefined 不是一个关键字。这意味着我们可以将其当做变量来使用,如:let undefined = 10; 这会影响我们对 undefined 值的判断,实际开发中应避免这样使用。
null 是一个关键字。
说几种 JS 判断数据类型的方法
1. typeof 只能判断基本数据类型,typeof null === 'object', 其他基本数据类型都正常。对于引用数据类型,都是 'object'。
2. instanceof 只能判断引用数据类型,左边实例对象,右边构造函数,基本原理是判断实例对象的 __proto__ 属性是否指向构造函数的 prototype,如果不相等,则查询实例对象的 __proto__.__proto__ 依次在原型链上查找,直到找到构造函数的 prototype,如果查找失败,则会返回 false。
3. Object.prototype.toString.call() 推荐使用的方式,返回格式如下:“[object Undefined]“。
说几种数据去重的方法
1. [...new Set(arr)]
2. Array.from(new set(arr))
3. filter + indexOf
4. for + indexof 或 for + includes
说一说伪数组和数组的区别?
伪数组:我们可以理解为类似数组的一个集合,他们与数组一样,具有索引(下标)和 length 属性。可以通过 for in 进行循环。
常见的伪数组:
1. document.getElementsByClassName('right')
2. arguments
伪数组的类型是 Object,数组的类型是 Array
判断是否是数组:Array.isArray()
伪数组转化为数组:
1. [].slice.call 或者 Array.prototype.slice.call
2. Array.from
3. [...arguments]
索引不连续时转化结果是连续的,会自动补位。
说一说 map 和 forEach 的区别?
map 和 forEach 都是es6新增的高阶函数,都是数组的方法,都有三个参数,用于数组遍历。
map 和 forEach 默认都不会改变原数组(不是绝对的,可以直接修改)。
map 会在内存中开辟一块新的空间,用来返回一个新的数组。forEach 返回值为 undefined。
性能方面:for > forEach > map,因为 map 的返回值是一个等长的全新的数组,数组创建和赋值产生的性能开销很大。
说一说 ES6 中的箭头函数
1. 箭头函数没有 this,它内部的 this 指向的是上级作用域中的 this。
2. 它的写法简单。当只有一行返回值时,return 关键字 可以省略。
3. 不能作为构造函数,所以不能被 new。
4. 每一个函数都有 prototype 属性,但是箭头函数没有。
扩展运算符使用场景
1. 伪数组转数组 [...arguments]
2. 合并剩余属性 const { name, age. ...rest } = record
3. 整合形参 function(...args) {}
4. 展开对象 const { name, age, ...record }
5. set 转数组 [...new Set(arr)]
说一说你对闭包的理解
形成条件:函数嵌套,内部函数引用外部函数的(变量或函数)。
解决的问题:内部的(变量或函数)会一直存在于内存中,直到内部函数成为垃圾对象才会被销毁,用于保存变量。
存在的问题:容易造成内存泄露。
闭包的应用:
-定义 js 模块:
`- 具有特定功能的js模块`
`- 将所有的数据和方法都封装到一个函数的内部(私有的)`
`- 只向外部暴露一个包含n个方法的对象或函数`
`- 模块的使用者,只需要通过模块导出的对象调用方法来实现对应的功能。`
- 防抖和节流
说一说变量提升
变量提升指的是 JS 的变量和函数声明会在代码编译期间,提升到代码的最前面。
是有使用 var 声明的变量才会存在变量提升,只有声明提升,赋值不会提升,在 JS 中函数是一等公民,所以函数提升要优先于变量提升。
变量提升可以在变量初始化之前访问该变量,返回的是 undefined。在函数声明前可以提前调用该函数。
使用 let 和 const 声明的变量不会提升,形成暂时性死区,在初始化之前访问 变量会报错。
ES6规定,let/const 命令会使区块形成封闭的作用域。若在声明之前使用变量,就会报错。
总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。
这在语法上,称为 “暂时性死区”( temporal dead zone,简称 TDZ)。
说一说 this 指向?(普通函数、箭头函数)
普通函数的 this 永远指向调用它的对象。
箭头函数:没有 this,它内部的 this 值指向上层作用域中的 this。
- fn():window 严格模式下 undefined。
- obj.fn(): 普通函数是 obj, 箭头函数是 window 或 undefined (看是否是严格模式)。
- var p = new Fu(): 普通函数是 p, 箭头函数不能被 new,直接报错。
- fn.call(obj):普通函数是 obj, 箭头函数是 window 或 undefined (看是否是严格模式)。
说一说 call apply bind 的作用和区别?
三者的作用都是改变函数执行时的 this 指向。
call 立即执行,参数(实际调用函数的实例对象,函数的参数...)
apply 立即执行,参数(实际调用函数的实例对象,函数的参数集合)
bind 返回一个新的函数,参数(实际调用函数的实例对象,函数的参数...)
bind 在 vue 与 react 框架中使用的较多,常用来改变函数运行时的 this 指向。
call 与 apply 在编写一些工具函数,如 防抖、节流中使用较多。
说一说 js 继承的方法和优缺点?
1. 原型链继承
所有的属性都放在了原型上
缺点:
- 引用类型的属性被所有实例共享
- 在创建 Child 的实例时,不能向 Parent 传参
function Parent() {
this.name = 'parent'
this.names = ['ps', 'jack']
}
Parent.prototype.getName = function() {
console.log(this.name);
}
function Child() {}
Child.prototype = new Parent()
const child1 = new Child()
child1.names.push('ly')
const child2 = new Child();
child1.getName() // parent
console.log(child2.names); // ['ps', 'jack', 'ly']
2. 借用构造函数(经典继承)
属性都放在了实例对象上
优点:
- 避免了引用类型的属性被所有实例共享
- 可以在 Child 中向 Parent 传参
缺点:
- 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
function Parent(name, age) {
this.names = ['ps', 'jack']
this.name = name
this.age = age
this.getInfo = function() {
return `${this.name}: ${this.age}`
}
}
function Child(...args) {
Parent.call(this, ...args)
}
const child1 = new Child('ps', 18)
child1.names.push('ly')
console.log(child1.names) // ['ps', 'jack', 'ly']
console.log(child1.getInfo()) // ps: 18
const child2 = new Child('jack', 20)
console.log(child2.names) // ['ps', 'jack']
console.log(child2.getInfo()) // jack: 20
3. 组合继承
优点:
- 1.融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式
function Parent(name, age) {
this.names = ['ps', 'jack']
this.name = name
this.age = age
}
Parent.prototype.getInfo = function() {
return `${this.name}: ${this.age}`
}
function Child(...args) {
Parent.call(this, ...args)
}
Child.prototype = new Parent()
const child1 = new Child('ps', 18)
child1.names.push('ly')
console.log(child1.names) // ['ps', 'jack', 'ly']
console.log(child1.getInfo()) // ps: 18
const child2 = new Child('jack', 20)
console.log(child2.names) // ['ps', 'jack']
console.log(child2.getInfo()) // jack: 20
4. 原型式继承
缺点:
- 1.包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
5. 寄生式继承
缺点:
- 1.跟借用构造函数模式一样,每次创建对象都会创建一遍方法。
6. 寄生组合式继承
优点:
- 1.这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。
- 2.与此同时,原型链还能保持不变;
- 3.因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式
参考:segmentfault.com/a/119000000…
说一说 new 会发生什么?
// 1. 创建一个空对象
const obj = {}
// 2. 设置原型链
obj.__proto__ = constrcutor.prototype
// 3. 将构造函数作为对象的一个属性并执行获取结果
const key = Symbol()
obj[key] = constrcutor
const result = obj[key](...args)
// 4. 删除 obj 上的 函数
delete obj[key]
// 将结果返回
return typeof result === 'object' ? result : obj
说一说 defer 和 async 的 区别?
当浏览器碰到 script 脚本时:
- 不加 defer 或 async,浏览器会立即加载并执行脚本,不会等待后续文档的加载。
- async 加载和渲染后续文档的过程将和脚本的加载与执行并行进行(异步)。脚本载完就执行。中断文档的渲染。
- defer 加载后续文档的过程和脚本的加载并行进行(异步),但是脚本的执行要在所有的元素解析完成之后。
defer 与 async的区别是:
前者要等到整个页面正常渲染结束,才会执行;后者一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer 是“渲染完再执行”,async 是“下载完就执行”。另外,如果有多个 defer 脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。
说一说promise是什么与使用方法?
Promise 是一种异步编程的一种解决方案。支持 .then() 的链式调用,主要解决了异步编程所引起的回调地狱的问题。
Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态,分别是 pending、resolved 和 rejected,分别代表了进行中、已成功和已失败。实例的状态只能由 pending 转变 resolved 或者 rejected 状态,并且状态一经改变,无法再被改变了。状态的改变是通过 resolve() 和 reject() 函数来实现的,我们可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。
说一说 js 实现异步的方法?
回调函数、事件监听、setTimeout、Promise、生成器Generator、async/await。
说一说cookie sessionStorage localStorage 区别?
一、存储的时间有效期不同
1、cookie 的有效期是可以设置的,默认的情况下是关闭浏览器后失效。
2、sessionStorage 的有效期是仅保持在当前页面,关闭当前会话页或者浏览器后就会失效。如果用户在浏览器中打开新的标签页或窗口,那么新的页面将无法访问 sessionStorage 中的数据。
3、localStorage 的有效期是在不进行手动删除的情况下是一直有效的。localStorage 中的数据可以在同一浏览器的所有标签页和窗口中共享。
二、存储的大小不同
1、cookie 的存储是4kb左右,存储量较小,一般页面最多存储20条左右信息。
2、localStorage 和 sessionStorage 的存储容量是5Mb(官方介绍,可能和浏览器有部分差异性)。
三、与服务端的通信
1、cookie 会参与到与服务端的通信中,一般会携带在 http 请求的头部中。
2、localStorage 和 sessionStorage 是单纯的前端存储,不参与与服务端的通信。
四、读写操作的便捷程度
cookie 由于出现的比较早,读写设计的并不很合理。不如后两者方便。
1、cookie 创建
// 您还可以为 cookie 添加一个过期时间(以 UTC 或 GMT 时间)。默认情况下,cookie 在浏览器关闭时删除:
// 您可以使用 path 参数告诉浏览器 cookie 的路径。默认情况下,cookie 属于当前页面。
document.cookie="username=John Doe; expires=Thu, 18 Dec 2043 12:00:00 GMT; path=/";
2、cookie的读取
var x = document.cookie;
3、cookie的删除
// 删除 cookie 非常简单。您只需要设置 expires 参数为以前的时间即可,如下所示,设置为 Thu, 01 Jan 1970 00:00:00 GMT:
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT";
说一说如何实现一个可过期的 localStorage 数据。
惰性删除:
特点:键值对过期后不会立马删除,下次使用时检查到过期才得到删除。
缺点:如果一个key一直没有被用到,即使它已经过期了也永远存放在 localStorage。
定时删除:
特点:每隔一段时间执行一次删除操作。
缺点:循环定时器会一直占据内存。
实现过程:
每隔一秒执行一次定时删除,操作如下:
1、随机测试20个设置了过期时间的key。使用正则匹配。
2、删除所有发现的已过期的key。
3、若删除的key超过5个则重复步骤1,直至重复500次。
说一下 token 能放在 cookie 中吗?
可以,cookie不设置过期时间就行,但是不推荐,因为无法防范 CSRF 攻击,并且 token 本身就有用于防范 CSRF 攻击的目的。
token 一般是用来判断用户是否登录的,它内部包含的信息有:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,token 的前几位以哈希算法压缩成的一定长度的十六进制字符串) token可以存放在Cookie中,token 是否过期,应该由后端来判断,不该前端来判断,所以token 存储在 cookie 中只要不设置 cookie 的过期时间就 ok 了,如果 token 失效,就让后端在接口中返回固定的状态表示token 失效,需要重新登录,再重新登录的时候,重新设置 cookie 中的 token 就行。
token认证流程:
1. 客户端使用用户名跟密码请求登录
2. 服务端收到请求,去验证用户名与密码
3. 验证成功后,服务端签发一个 token ,并把它发送给客户端
4. 客户端接收 token 以后会把它存储起来,比如放在 cookie 里或者 localStorage 里
5. 客户端每次发送请求时都需要带着服务端签发的 token(把 token 放到 HTTP 的 Header 里)
6. 服务端收到请求后,需要验证请求里带有的 token ,如验证成功则返回对应的数据
说一说 ajax 拦截器的原理和应用
说一说创建 ajax 的过程
1、创建XHR对象:new XMLHttpRequest()
2、设置请求参数:XHR.open(Method, 服务器接口地址);
3、监听请求成功后的状态变化,根据状态码进行相应的处理。
4、发送请求: XHR.send(),如果是 get 请求不需要参数,post 请求需要参数 XHR.send(data)
readyState 值说明:
- 0:初始化,XHR 对象已经创建,还未执行 open
- 1:载入,已经调用 open 方法, 但是还没发送请求
- 2:载入完成,请求已经发送完成
- 3:交互,可以接收到部分数据
- 4:数据全部返回
status 值说明:
- 200:成功
- 404:没有发现文件、查询或URl
- 500:服务器产生内部错误
<button class="btn">点击</button>
<script>
function sendRequest() {
console.log('send');
const XHR = new XMLHttpRequest()
XHR.open('get', '/js.js', true)
XHR.onreadystatechange = function () {
if (this.readyState === 4 && XHR.status === 200) {
}
}
XHR.send()
}
document.querySelector('button.btn').onclick = sendRequest
</script>
——————————————————————————————
说一下 fetch 的请求方式
fetch 是一种 HTTP 数据请求的方式,是 XMLHttpRequest 的一种替代方案,fetch 函数就是原生的js。
fetch 对象返回一个 promise,天生支持 promise。
XMLHttpRequest 的特点:
1. 所有的功能都集中在一个对象上,代码维护成本高并且比较混乱。
2. 不能适配 promise API。
fetch 的特点:
1. 精细的功能分割,头部信息、请求信息、响应信息都分布在不用的对象上。
2. 可以适配 Promise API。
3. 同源请求也可以自定义不带 Cookie,某些服务不需要 cookie 的话还能少一些流量。
说一下有什么方法可以保持前后端实时通信?
1. websocket
2. 短轮询:每隔一段时间客户端就发出一个请求,去获取服务器最新的数据。
3. 长轮询:客户端向服务器发送请求,服务器接到请求后暂时不返回数据,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求。客户端可以设置一个超时时间,在超时时间之前服务器必须返回一次数据。
4. iframe:在页面里嵌入一个隐蔵iframe,将这个隐蔵iframe的src属性设为对一个长连接的请求或是采用xhr请求,服务器端就能源源不断地往客户端输入数据。
浏览器缓存
浏览器缓存分为强制缓存和协商缓存,强制缓存优先于协商缓存。
- 若强制缓存( Expires 和 Cache-Control,Cache-Control 优先级高于 Expires )生效则直接使用缓存。
- 若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match,其中Etag / If-None-Match的优先级比Last-Modified / If-Modified-Since高),协商缓存由服务器决定是否使用缓存
- 若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存
主要过程如下:
说一下浏览器输入 URL 发生了什么?
1. URL 解析
判断浏览器输入的是否是一个合规的 URL,如果不是合规的 URL 就会当做一个搜索内容交给搜索引擎去搜索。
URL 的组成:
例:https://www.example.com:80/path/to/myfile.html?key1=value1&key2=value2#anchor
1. 协议(protocol):互联网支持多种协议,必须指明网址使用哪一种协议,默认是 HTTP 协议。
2. 主机(host):资源所在服务器的名字,也叫做域名,但是有些主机没有域名,只有 IP 地址,所以叫主机比较合适。
3. 端口(port):同一个域名下面可能同时包含多个网站,它们之间通过端口(port)区分。默认端口是 80/443。
4. 路径(path):/path/to/myfile.html
5. 查询参数:?key1=value1&key2=value2
6. 锚点:#anchor,浏览器加载页面以后,会自动滚动到锚点所在的位置,锚点名称通过网页元素的id属性命名。
2. 查找缓存
如果有浏览器缓存并且缓存未过期则直接返回页面。没有缓存直接进行 DNS 域名解析。
3. DNS解析
DNS(Domain Names System)域名系统。是进行域名和与之相对应的 IP 地址进行转换的服务器。主要查找对应域名的主机。
1. 查找缓存:
- 浏览器缓存:检查当前域名的缓存是否在浏览器中。
- 操作系统缓存:操作系统 DNS 缓存,去本地的 hosts 文件中查找。
- 路由器缓存:路由器 DNS 缓存。
- ISP 缓存: ISP DNS 缓存(ISP DNS 就是在客户端电脑上设置的首选 DNS 服务器,又称本地的 DNS 服务器)。
2. 查询IP地址:
- 根域名服务器
- 顶级域名服务器
- 权威域名服务器
找到 IP 地址后,将它记录在缓存中,供下次使用。
4. TCP连接:三次握手
主要是为了确定双方都具有收发能力。
第一次握手:客户端主动连接服务器,等待服务器确认。-- 表明客户端具有发送能力。
第二次握手:服务器收到消息后发出应答。-- 表明服务器具有接受与发送的能力。
第三次握手:客户端收到应答后,向服务器发送 “确认发送报文段”。-- 表明客户端具有接受能力。
扩展:为什么是三次握手?
答:防止已失效的连接请求又传送到服务器端,因而产生错误
5. 浏览器发送请求
建立连接后,浏览器 HTTP 请求报文获取数据。
报文分为:请求报文、响应报文
请求报文:请求行、请求头、空行、请求体。
- 请求行:请求方法,请求 url、http 协议及其版本。例 POST /report/job/listJobs HTTP/1.1。
- 请求头:浏览器的基本信息,比如:域名、Cookie、浏览器内核、操作系统等。
- 空行:最后一个请求头之后是空行,发送回车符和换行符,通知服务器以下不再有请求头。
- 请求体:当 POST、PUT 请求时,请求数据放在请求体中。
响应报文:响应行、响应头、空行、响应体。
- 响应行:由 HTTP 版本协议字段、状态码和状态码的描述文本3个部分组成。HTTP/1.1 200 OK。
- 响应头:用于指示客户端如何处理响应体,告诉浏览器响应的类型,字符编码和字节大小等信息。
- 空行:最后一个响应头之后是空行,发送回车符和换行符,通知客户端以下不再有响应头。
- 响应体:返回客户端所需要的数据。
此时我们可以拿到返回的 HTML 文件,开始解析渲染页面。
6. 浏览器解析渲染页面
1. HTML 解析
解析 HTML 构建 DOM Tree,当遇到 script 脚本时,会阻塞 DOM Tree 的构建,首先下载并执行 js 代码,之后才继续解析 HTML,构建 DOM 树。(document Object Model)文档对象模型。
2. CSS 解析
在解析 HTML 的过程中,遇到 css 的 link 元素,会下载 css 文件,注意:下载不会阻塞 HTML 的解析。
下载完后,对 css 文件进行解析,构建出 CSSOM 树 (CSS Object Model, css 对象模型)。
3. 构建 Render Tree
DOM 树与 CSSOM 树组合构建 Render 树。
4. 布局(Layout)和绘制(Paint)
布局:
- Render 树会表示显示那些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息。
- 布局的主要目的是为了确定呈现树中所有的宽度、高度和位置信息。
绘制:
- 将每个节点绘制到页面上。
7. 断开连接:TCP 四次挥手
第一次挥手:客户端向服务器发送断开连接请求报文,等待服务器确认。
第二次挥手:服务器收到消息后发出应答。
第三次挥手:服务器向客户端发送断开连接请求报文,等待客户端确认。
第四次挥手:客户端收到消息后发出应答。
参考文章:
说一下回流、重绘的区别以及如何避免?
回流:
当一个元素的尺寸、位置等发生变化的时候,浏览器需要重新计算该元素的几何信息,并将其摆放到正确的位置的过程叫做“回流”。
回流(reflow)也可以称为重排。
- 第一次确定节点的尺寸和位置,称之为布局(Layout)。
- 之后对节点的尺寸、位置修改以及重新计算,称之为回流。
什么情况下会引起回流呢?
- Dom 结构发生变化(添加、删除节点)
- 改变了布局,修改了width、height、padding等值。
- 触发了 resize,窗口尺寸发生了变化。
- 调用 getComputedStyle 方法获取尺寸、位置信息。
重绘:
当一个元素的外观、样式发生变化而布局不会改变,重新绘制的过程叫做“重绘”。
重绘(repaint)对页面再次绘制。
- 第一次渲染内容称之为绘制(paint)。
- 之后的重新绘制称之为重绘。
什么情况下会引起重绘呢?
- 修改背景色、文字颜色、边框颜色、样式等。
联系:
- 回流一定会引起重绘,重绘不一定会引发回流。所以回流很消耗性能。
避免:
css:
- 如果需要设置动画效果,最好将元素脱离正常的文档流。
- 避免使用 CSS 表达式(例如:calc()):参与计算会使用到 getComputedStyle。
js:
- 避免频繁操作样式,最好将样式列表定义为 class 并一次性更改 class 属性。
- 避免频繁操作 DOM,创建一个 documentFragment ,在它上面处理所有 DOM 操作,最后再把它添加到文档中。
- 可以先为元素设置为 display: none,操作结束后再把它显示出来。
display: none; 元素不会出现在 render 树,但是 dom 树上还是存在的,否则无法响应事件。
说一下浏览器是如何渲染页面的?
1. 浏览器通过请求得到一个HTML文本
2. 渲染进程解析HTML文本,构建DOM树
3. 解析HTML的同时,如果遇到内联样式或者样式脚本,则下载并构建样式规则(CSSOM),若遇到JavaScript脚本,则会下载执行脚本。
4. DOM 树和 CSSOM 构建完成之后,渲染进程将两者合并成渲染树(render tree)
5. 渲染进程开始对渲染树进行布局,生成布局树(layout tree)
6. 渲染进程对布局树进行绘制,生成绘制记录
7. 渲染进程的对布局树进行分层,分别栅格化每一层,并得到合成帧
8. 渲染进程将合成帧信息发送给 GPU 进程显示到页面中
说一下浏览器的垃圾回收机制?
主要讲谷歌的 v8 引擎。
浏览器垃圾回收机制根据数据的存储方式分为"栈垃圾回收"和"堆垃圾回收"。
栈垃圾回收:
当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP(指针)来销毁该函数保存在栈中的执行上下文,对应的内存也被释放了。
堆垃圾回收:
在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
- 新生代中使用 Scavenge 算法。分为 from 和 to。使用副垃圾回收器。64位操作系统下是64MB。
- 老生代中使用标记-清除算法和标记-整理算法。使用主垃圾回收器。64位操作系统下是1400MB。
加分回答
Scavenge算法: 新生代互换
1. 标记:对 from 区域中的垃圾进行标记。
2. 清除垃圾数据。
3. 整理碎片化内存:副垃圾回收器会把存活的对象复制到 to 区域中,并且有序的排列起来,复制后 to 区域就没有内存碎片了。
4. 新生代互换:完成复制后,form 区域与 to 区域进行角色翻转(只是内存名称互换),这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
标记-清除算法:
1. 标记:标记阶段递归遍历根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
2. 清除:将垃圾数据进行清除。
产生内存碎片:对一块内存多次执行标记-清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存。
标记-整理算法:
1. 标记:和标记-清除的标记过程一样。
2. 整理:让所有存活的对象都向内存的一端移动。
3. 清除:清理边界以外的内存。
全停顿:
V8 是使用“副垃圾回收器”和“主垃圾回收器”处理垃圾回收的,不过由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿。如果执行垃圾回收的时间很长,则用户等待的时间也会很长。
增量标记算法:
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收“标记”过程和 JavaScript 脚本执行交替执行(也就是执行一段 js 脚本,再执行一段垃圾回收的标记操作,以此反复),直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法。
问:为什么老生代不使用和新生代一样的方法?
答:
新生代采用这种方式可以认为是使用空间换取时间的方法。
老生代占据内存空间大,再分为form与to区域的话,极大的浪费了内存空间。
说一说事件循环 Event Loop,宏任务与微任务?
整体 script 代码作为一个宏任务进入主线程,代码自上而下,执行同步代码。
遇到同步任务直接推入调用栈中执行,遇到异步任务,等待执行,异步任务结果有了结果,加入到对应的任务队列。
当前宏任务执行完毕,判断宏任务队列中是否还有任务,有就移到主线程执行,再进行以上的步骤。
没有就检查微任务队列,有就执行,再进行以上的步骤。
常见的宏任务有:
1. 整个 script 脚本。
2. setTimeout / setInterval
3. ajax请求
4. dom 事件回调
微任务:
1. Promise.then() 的回调
说一说跨域是什么?如何解决跨域问题?
跨域:
跨域是由于浏览器的同源策略引起的,是浏览器的一种安全策略,不允许网站执行非同源网站的脚本。
同源策略:协议、域名、端口三者有其中任意一个不一样就会触发跨域。
请求发送了,服务器也返回了,浏览器拒绝接收。
解决:
1. jsonp:利用 script 标签可以跨域请求资源,将回调函数作为参数拼接在 url 中。后端收到请求,调用该回调函数,并将数据作为参数返回去,注意设置响应头返回文档类型,应该设置成 javascript。
2. websocket:本身就不存在同源限制。
3. cors:跨域资源共享,后端设置请求头 res.setHeader('Access-Control-Allow-Origin', '*');以及资源类型为 javascript。
4. 开发时前端服务配置 proxy 代理(代理服务器请求发送数据会绕过浏览器),打包后后端 配置 nginx 反向代理。
5. postmessage:H5新增API,通过发送和接收 API 实现跨域通信。
6. iframe
Vue 相关
说说你对 SPA 单页面的理解,它的优缺点分别是什么?
SPA( single page application )单页面应用。整个应用只有一个 HTML 页面,用户与应用程序交互时动态更新该页面。
常利用路由机制实现 HTML 内容的变换,避免整个页面的重新加载。
**优点:**
- 用户体验好、交互速度快,内容的改变不需要重新加载整个页面,避免了页面的重复渲染;
- 基于上面一点,SPA 相对对服务器压力小,流量也小;
- 前后端职责分离,前端进行交互逻辑,后端负责数据处理;
**缺点:**
- 白屏时间长:需要由空的 HTML 页面根据 js 动态生成 DOM 结构,并绘制成页面,这个过程会出现白屏时间;
- SEO 难度较大长
客户端渲染(client side render)
浏览器请求 url 获取 HTML 页面,一般拿到的都是一个 HTML 的空壳,里面存在着 HTML 的基本结构以及很多的 script 脚本。
浏览器解析并构建 DOM 树时遇见 script 脚本就会去执行,script 脚本可以动态的去改变 DOM 树的结构。
这种渲染方式叫动态渲染,也叫客户端渲染。
服务端渲染(server side render)
服务端渲染就是在浏览器请求页面 URL 的时候,服务端将我们需要的 HTML 文本组装好,并返回给浏览器。
这个 HTML 文本被浏览器解析之后,不需要经过 JavaScript 脚本的执行,即可直接构建出希望的 DOM 树并展示到页面中。
这个服务端组装 HTML 的过程,叫做服务端渲染。
优势:
1. 利于SEO(只处理 HTML):
爬虫分为:
- 低级爬虫:只请求URL,URL返回的HTML是什么内容就爬什么内容。
- 高级爬虫:请求URL,加载并执行JavaScript脚本渲染页面,爬JavaScript渲染后的内容。
低级爬虫对客户端渲染的页面来说,简直无能为力,因为返回的HTML是一个空壳,它需要执行 JavaScript 脚本之后才会渲染真正的页面。
而目前像百度、谷歌、微软等公司,有一部分年代老旧的爬虫还属于低级爬虫,使用服务端渲染,对这些低级爬虫更加友好一些。
2. 白屏时间短:
服务端渲染在浏览器请求URL之后已经得到了一个带有数据的HTML文本,浏览器只需要解析HTML,直接构建DOM树就可以。
而客户端渲染,需要先得到一个空的HTML页面,这个时候页面已经进入白屏,之后还需要经过加载并执行 JavaScript、请求后端服务器获取数据、
JavaScript 渲染页面几个过程才可以看到最后的页面。特别是在复杂应用中,由于需要加载 JavaScript 脚本,越是复杂的应用,需要加载的
JavaScript 脚本就越多、越大,这会导致应用的首屏加载时间非常长,进而降低了体验感。
直接给一个数组项赋值,Vue 能检测到变化吗?
不能,用索引直接设置一个数组项时 或者 当你修改数组的长度时,Vue 不能检测到数组的变动。
也就是:
1、vm.items[index] = newValue
2、vm.items.length = newLength
为了解决 1:
// Vue.set
Vue.set(vm.items, index, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, index, newValue)
// Array.prototype.splice
vm.items.splice(index, 1, newValue)
为了解决 2:
// Array.prototype.splice 只能将数组长度变小,不能变大。
vm.items.splice(newLength)
原因:
动态添加的数组项不能被 Object.defineProperty 劫持生成 getter, setter,因此无法产生响应。
Vue 能检测到数组变动的方法:
pop, push, shifut, unshift, splice, sort, reverse 做了重写,这些方法可以改变数组。
怎样理解 Vue 的单向数据流?
Vue 单向数据流指的是父组件通过属性传递数据到子组件,子组件可以通过事件向父组件发送消息,但是子组件不能直接修改父组件传递给它的属性。
每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
说一说 vue 钩子函数
钩子函数:vue 实例创建和销毁过程中自动执行的函数,整个过程称为生命周期。
钩子函数按照组件生命周期的过程分为,挂载阶段 => 更新阶段 => 销毁阶段。
每个阶段对应的钩子函数:
挂载阶段:beforeCreate、created、beforeMount、mounted。
更新阶段:beforeUpdate、updated
销毁阶段:beforeDestroy、destroyed。
如果使用了 keep-alive 组件,又多了:activated、deactivated。属性:include、exclude、max(最大缓存数)
| 生命周期 | 描述 |
|---|---|
| beforeCreate | 实例被创建之初,组件的属性生效之前。 |
| created | 实例创建完成,可访问 data、computed、watch、methods上的方法和数据,未挂载到 DOM,$el 不可用。 |
| beforeMount | 在挂载开始之前被调用,会找到对应的 template,并编译成 render 函数。 |
| mounted | 实例挂载到 DOM 上,可以通过 DOM API 获取到 DOM 节点,$ref 属性可以访问,常用于获取 VNode 信息。 |
| beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
| update | 组件数据更新之后,避免在这个钩子函数中操作数据,可能陷入死循环。 |
| activited | keep-alive 专属,组件被激活时调用 |
| deactivated | keep-alive 专属,组件被离开时调用 |
| beforeDestory | 组件销毁前调用,this仍能获取到实例,常用于销毁定时器、解绑全局事件、销毁Echarts等插件对象操作。 |
| destoryed | 组件销毁后调用 |
Vue 父子组件生命周期钩子函数执行顺序?
挂载过程:(3父4子)
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
更新过程:
父beforeUpdate -> 子beforeUpdate -> 子updated -> 父update
销毁过程:
父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
在哪个生命周期钩子函数内调用异步请求?
可以在 created、beforeMount、mounted 中进行调用,因为这三个钩子函数中,data已经创建,可以调用实例上的props、data、methods 上的属性和方法,推荐在 created 中调用:
- 能更快的获取到后端数据,减少页面 loading 时间。对于需要操作 dom 的场景,使用 $nextTick 或 在 mounted 中调用。
- ssr 不支持 beforeMounted、mounted 钩子函数,所以放在 created 有利于一致性,减少了可能存在的代码迁移的工作。
父组件监听子组件的生命周期?
$emit:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
@hook:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
@hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created,updated 等都可以监听。
组件中 data 为什么是一个函数?
组件是用来复用的,且 JS 里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的 data 属性值会相互影响。
如果组件中 data 选项是一个函数,那么每个实例可以维护一份独立对象,组件实例之间的 data 属性值不会互相影响;
而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题。
而且函数的功能更加强大,意味着可以在 data 中写 js 代码,可以完成一些复杂数据的初始化工作。
虽然 vue 没有推荐这样使用,但是我们知道有这个功能。
v-model 的原理?
v-model 是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件。
- input 和 textarea 元素使用 value 属性和 input 事件;
- checkbox 和 radio 使用 checked 属性和 change 事件;
- select 使用 value 属性和 change 事件;
<input v-model='something'>
相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
如果在自定义组件中,v-model 默认代表的是 value 和 input。
父组件:
<ModelChild v-model="message"></ModelChild>
子组件:
<div>{{value}}</div>
props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
},
}
说一说组件通信的方式?
1. props / $emit 适用 父子组件通信
2. parent / $children 适用 父子组件通信
$ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。
$parent / $children:访问父 / 子实例。
3. EventBus (on) 适用于 父子、隔代、兄弟组件通信
创建一个空的 Vue 实例作为事件总线,用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
4. listeners 适用于 隔代组件通信
$attrs:包含了 props 声明之外的其他属性( class 和 style 除外 )。可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。inheritAttrs: true,传入的属性体现在标签上,false:传入的属性不体现在标签上。
$listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。
5. provide / inject 适用于 隔代组件通信
祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。主要解决了跨级组件间的通信问题。
6. Vuex 适用于 父子、隔代、兄弟组件通信
你使用过 Vuex 吗?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理器。核心就是 store(仓库)。“store” 基本上就是一个容器,包含着应用中大部分的状态 ( state )。
(1)Vuex 的状态存储是响应式的。就是当你的几个组件都使用到了这个Vuex的状态,一旦它改变了,所有关联的组件都会自动更新相对应的数据。
(2)改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
(3)Vuex 存储的数据会在页面刷新之后重置为初始化状态,并不能让数据持久化。可以使用一些持久化库,或者使用 localStorage 做持久化。
主要包括以下几个模块:
- State:存储所有的共享数据。定义了数据的数据结构,可以在这里设置数据的默认值。
- Getter:可以对 Store 中已有的数据加工处理之后形成的数据,类似 Vue 的计算属性。
- Mutation:是唯一更改 store 中状态的方法,且必须是同步函数。
- Action:处理异步任务变更数据通过派发(dispatch)Action,在 Action 中 commit Mutation 变更数据。
- Module:模块化 Vuex,允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。
说一说 computed 和 watch 的区别?
计算属性有缓存,watch 没有缓存。
计算属性依赖其他属性值,只有它依赖的属性值发生了变化,下一次获取计算属性的值才会重新计算。必须有返回值,不支持异步。
watch 更多的是观察的作用,支持异步,类似于某些数据的监听回调,每当监听的数据发生变化时都会执行回调进行后续操作。
immediate(立即的), deep(递归)
应用场景:
计算属性:当一个属性受多个属性影响的时候,使用 computed,比如:需要拼接展示“用户名”、“列表展示”、“购物车商品结算”。
watch:当一条数据影响多条数据的时候使用 watch,比如:监听父组件传过来的 prop、监听搜索框内容变化。
说一说 v-if 和 v-show 的区别?
作用: 都是控制元素隐藏和显示。
区别:
v-show: 控制的元素无论是 true 还是 false,都被渲染出来了,通过 display:none 控制元素隐藏。首次也渲染节点(在dom中显示)。
v-if: true 进行渲染,false 不渲染,根本在 dom 树结构中不显示。是惰性渲染,初始为 false 直接不渲染节点。是对 dom 的新增和删除操作。
应用:
v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。
说一说 vue 中的 keep-alive 组件
作用:
主要用于缓存组件,提升性能,避免重复加载一些不需要经常变动的组件。是 vue 的内置组件。
属性:
三个属性:include(只有名字匹配的才会被缓存)、exclude(名字匹配的都不会被缓存)、max(最大缓存数)。
钩子函数:
activated 组件进入时调用、deactivated 组件离开时调用。
使用:
可以搭配路由使用,在 meta 中添加 keep-alive: true, 配合 include 和 exclude 使用。
源码解析:
Vue 组件实例加载顺序:VNode->实例化->update->真实Node,在实例化的时候会判断该组件是否被 keep-alive 保存过,是的话则直接拿其中的 DOM 进行渲染。
说一说 vue 中 $nextTick 的作用和原理
使用 nextTick() 是为了可以获取更新后的 DOM。
Vue 更新 dom 节点是异步操作,即数据更新之后,视图不会马上更新,所以修改数据后,在方法中获取到的 dom 节点不是更新后的 dom 节点,只有在 nextTick 里面才能获取到更新后的 dom 节点。
触发时机:在同一事件循环中的数据变化后,DOM 完成更新,立即执行 nextTick() 的回调。
同一事件循环中的代码执行完毕 -> DOM 更新 -> nextTick callback触发
有两个场景我们会用到 nextTick:
1. created 中想要获取 DOM 时;
2. 获取列表更新后的高度;
Vue 中 key 的作用?
作用:
为了高效的更新虚拟 DOM,其原理是 vue 在 patch 过程中通过 key 和 tag 可以精准判断两个节点是否是同一个,从而避免频繁更新相同元素,减少DOM操作量,提高性能。
就地复用策略:
没有 key 地时候默认使用就地复用策略。如果在数组头部插入一条数据,vue 不是移动 DOM 元素来匹配数据项的改变,而是简单复用原来位置的每个元素,在进行比较时发现标签一样内容不一样时,就会复用之前的位置,将新的内容直接放到该位置,以此类推,只创建最后多出来的一个元素。
用index作为key可能会引发的问题:
a. 若对数据进行:逆序添加、逆序删除等破坏顺序操作,会产生没有必要的真实DOM更新 ==> 界面效果没问题,但效率低。
b. 如果结构中还包含输入类的 DOM(输入框有值的情况),会产生错误 DOM 更新 ==> 界面有问题。
开发中如何选择 key?:
a. 最好使用每条数据的唯一值。
b. 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用 index 作为 key 也是没有问题的。
vue 中 key 值的作用可以分为两种情况来考虑。
第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当我们使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此我们可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
第二种情况是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。
说一说 vue-router 实现懒加载的方法
懒加载的核心思想:按需加载,也叫异步加载。用到再加载。
作用:减少首次加载时的白屏时间,提高用户体验。
方式:
1:Vue 异步组件
2:ES6 标准语法 import()---------推荐使用
- Vue 异步加载技术:
1:vue-router 配置路由,使用 vue 的异步组件技术,可以实现懒加载,此时一个组件会生成一个js文件。
2:component: resolve => require(['@/views/404.vue'], resolve)
- ES6 推荐方式 import():
1:直接将组件引入的方式,import 是 ES6 的一个语法标准,如果需要浏览器兼容,需要转化成 es5 的语法。
2:推荐使用这种方式,但是注意 wepack 的版本 > 2.4。
3:vue 官方文档中使用的也是 import 实现路由懒加载。
4:上面声明导入,下面直接使用。
// 下面没有指定 webpackChunkName,每个组件打包成一个js文件。
const Foo = () => import('../components/Foo')
const Aoo = () => import('../components/Aoo')
// 下面2行代码,指定了相同的 webpackChunkName,会合并打包成一个js文件。
// const Foo = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/Foo')
// const Aoo = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/Aoo')
vue2 中 v-for 和 v-if 为什么不能一起使用?
v-if 不能和 v-for 一起使用的原因是 v-for 的优先级比 v-if 高,一起使用会造成性能浪费。vue2 也会报出警告。
解决方案:
- 把 v-if 放在 v-for 的外层
- 需要 v-for 的属性先从计算属性中过滤一次
- v-for 标签下添加子标签 手写 v-if。
v-if 和 v-for 的优先级问题在 vue3 中不需要考虑,vue3 更新了 v-if 和 v-for 的优先级,使 v-if 的优先级高于 v-for。
router 和 route 的区别
$router是路由实例,包含了路由跳转方法、钩子函数(路由前置导航守卫、路由后置导航守卫)等$route是当前路由信息,包括path、params、query、name等路由信息参数
v-html 的原理
会先移除节点下的所有节点,然后设置 innerHTML 为 v-html 的值。
常见的事件修饰符及其作用
(1) .stop:阻止传播。event.stopPropagation() 阻止捕获和冒泡阶段中当前事件的进一步传播。
但是,它不能防止任何默认行为的发生;例如,对链接的点击仍会被处理。
(2) .prevent:阻止默认事件。event.preventDefault() 此事件还是继续传播,除非碰到事件监听器调用 stopPropagation()。
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
(3) .capture:与事件冒泡的方向相反,事件捕获由外到内;
(4) .self:只当事件在该元素本身 (比如不是子元素) 触发时触发回调,但是不会阻止冒泡!
(5) .once:只会触发一次。
(6) .native:监听节点的原生事件。
MVVM的理解
MVVM是一种软件架构模式,MVVM 分为 Model、View、ViewModel:
Model代表数据模型,数据和业务逻辑都在Model层中定义;View代表UI视图,负责数据的展示;ViewModel负责监听Model中数据的改变控制View视图的更新,并且根据用户交互操作View更新Model中的数据;
Vue2和Vue3有哪些区别
Vue2使用的是optionsAPI,Vue3使用composition API,更好的组织代码,提高代码可维护性Vue3使用Proxy代理实现了新的响应式系统,比Vue2有着更好的性能和更准确的数据变化追踪能力。Vue3引入了Teleprot组件,可以将DOM元素移动到 Vue app 之外的位置。用于创建模态框、弹出框等。Vue3全局API名称发生了变化,同时新增了watchEffect、Hooks等功能- 生命周期不同
- 可以多个根节点
Vue3对TypeScript的支持更加友好Vue3核心库的依赖更少,减少打包体积- 3支持更好的
Tree Shanking,可以更加精确的按需要引入模块
Vue的性能优化有哪些
### 编码阶段
- `v-if`和`v-for`不一起使用
- `v-for`保证`key`的唯一性
- 使用`keep-alive`缓存组件
- 合理使用计算属性
- `v-if`和`v-show`酌情使用
- 路由懒加载
- 异步组件
- 图片懒加载
- 节流防抖
- 第三方模块按需引入,不要全部引入,减小打包体积。
- 组件卸载前及时解绑全局事件、清除定时器、以及销毁插件实例。
### 打包优化
- 压缩代码
- 使用 CDN 加载第三方模块
- 抽离公共文件
### 用户体验
- html5 语义化标签
- 骨架屏
- 客户端(浏览器)缓存
### SEO 优化
- 预渲染
- 服务端渲染
- 合理使用 `meta` 标签
双向数据绑定的原理
采用 "数据劫持" 结合 "发布者-订阅者模式" 的方式实现。
组件实例化时,会创建一个 Observe 实例,它会将 data 数据进行递归遍历,并通过 Object.defineProperty 方法,给每个属性添加上一个
getter 和一个 setter。在 getter 中收集依赖,在 setter 中通过依赖更新。
针对数组来说:还会进行数组方法的重写,在调用数组方法时,通知更新。
加分:
双向数据绑定的实现流程
(1)数据劫持:当 Vue 实例创建时,Observe 会对 data 对象进行遍历,并使用 Object.defineProperty() 将每个属性转换成 getter/setter,实现数据的劫持。
(2)编译模板:Vue 的编译器会对模板进行编译,将模板替换成对应的渲染函数。
(3)收集依赖:当渲染函数执行时,会触发数据项的 getter,将当前数据项对应的 Watcher 添加到该数据项对应的 Dep 依赖中。
(4)数据变化:当数据项发生变化时(即 setter 被触发),会触发该数据项对应的 Dep 的 notify 方法,通知所有订阅了该数据项的 Watcher 进行更新。
(5)更新视图:Watcher 接收到更新通知后,会调用其绑定的回调函数(通常是组件的渲染函数或计算属性的更新函数)来更新用户界面。
1. Observe(观察者)
Observe 负责将数据对象(通常是 Vue 实例的 data 对象)进行递归遍历,并使用 Object.defineProperty() 将属性转换成 getter/setter,使数据变得“可观察”。当数据发生变化时,setter 会自动通知相关的依赖(即订阅了这个数据的 Watcher)。
2. Dep(依赖)
Dep 是 Vue 中一个非常重要的类,它管理着与该数据项相关的所有 Watcher。当数据项发生变化时,Dep 会通知所有订阅了该数据项的 Watcher 进行更新。每个数据项(即每个通过 Object.defineProperty() 设置的 getter/setter)都会有一个与之对应的 Dep 实例。
3. Watcher(监听器)
Watcher 是 Vue 中用于观察数据变化的组件。当数据项发生变化时,Dep 会通知所有订阅了该数据项的 Watcher,然后 Watcher 会调用其绑定的回调函数(通常是组件的渲染函数或计算属性的更新函数)来更新用户界面。Watcher 可以在 Vue 组件的创建、计算属性或侦听器(watchers)的创建等过程中被创建和添加到对应的 Dep 中。
使用 Object.defineProperty() 来进行数据劫持有什么缺点?
对数组方法不友好,不能实现数据劫持。vue2 中会对数组的新增删除方法push、pop、shift、unshift、splice、sort、reserve通过重写的形式,在拦截里面进行手动触发依赖更新。
实现响应式 vue2 和 Vue3 相比有什么区别?
Vue3 采用了 Proxy 代理的方式,Proxy 是 ES6 引入的一个新特性,它可以对整个对象进行数据劫持。而 Object.defineProperty 只能监听单个属性的读写,无法监听新增、删除等操作。对数组方法友好。
Vue 是如何收集依赖的?
然后在getter中通过dep.depend()方法对数据依赖进行收集,然后在settter中通过dep.notify()通知更新。整个Dep其实就是一个观察者,把收集的依赖存储起来,在需要的时候进行调用。在收集数据依赖的时候,会为数据创建一个Watcher,当数据发生改变通知每个Watcher,由Wathcer进行更新渲染。
slot 是什么?有什么作用?原理是什么?
slot插槽,一般在封装组件的时候使用,在组件内不知道以那种形式来展示内容时,可以用slot来占据位置,最终展示形式由父组件以内容形式传递过来,主要分为三种:
- 默认插槽:又名匿名插槽,当
slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。 - 具名插槽:带有具体名字的插槽,也就是带有
name属性的slot,一个组件可以出现多个具名插槽。 - 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
对虚拟DOM的理解
虚拟 DOM 就是用 JS 对象来表述 DOM 节点,是对真实 DOM 的一层抽象。
在 Vue 中,会先把代码转换为虚拟 DOM,再最终渲染到页面,在每次数据发生变化前,都会在内存中缓存一份虚拟 DOM,通过 diff 算法来对比
新旧虚拟 DOM 的差异,将差异记录到一个对象中按需更新,最后创建真实 DOM,从而提升页面渲染性能。
说说 vue 的 diff 算法?
首先,我们拿到新旧节点的数组,然后初始化四个指针,分别指向 新旧节点 的开始位置 和 结束位置。
然后进行两两对比:
newStart 与 oldStart、newEnd 与 oldEnd、newStart 与 oldEnd、newEnd 与 oldStart 进行对比,
在这个过程中如果匹配到,那么会将 旧节点对应的真实dom 移到 新节点的位置上,并且指针往中间移动。
如果上述过程都没配有匹配上,则会判断当前新节点的第一个节点是否在旧节点中,若是存在则复用,若是不存在则创建。
当 oldStart > oldEnd 或者 newStart > newEnd 时,跳出 while 循环。
当 oldStart > oldEnd 代表 oldCh 都已匹配完成,那么代表 newStart 和 newEnd 之间的节点为需要新增的节点。
当 newStart > newEnd 代表 newCh 都已匹配完成,那么代表 oldStart 和 oldEnd 之间的节点为需要删除的节点。
在 patchVnode 过程中会调用 updateChildren,所以 vue 的 diff 算法是个深度优先算法。
Vue 的 diff 算法会直接改变旧的虚拟 DOM吗?
Vue 的 diff 算法不会直接改变旧的虚拟 DOM。
diff 算法的核心是比较新旧两个虚拟 DOM 树的差异,并生成一个 patch 对象,这个对象记录了需要更新的差异信息。
然后,Vue 会根据这个 patch 对象来更新真实的 DOM,而不是直接修改旧的虚拟 DOM。
虚拟 DOM 就一定比真实 DOM 更快吗?
虚拟 DOM 不一定比真实 DOM 更快,而是在特定情况下可以提供更好的性能。
在复杂情况下,虚拟 DOM 可以比真实 DOM 操作更快,因为它是在内存中维护一个虚拟的DOM树,将真实DOM操作转换为对虚拟DOM的操作,然后通过
`diff`算法找出需要更新的部分,最后只变更这部分到真实DOM就可以。在频繁变更下,它可以批量处理这些变化从而减少对真实DOM的访问和操作,
减少浏览器的回流重绘,提高页面渲染性能。
而在一下简单场景下,直接操作真实 DOM 可能会更快,当更新操作很少或者只是局部改变时,直接操作真实 DOM 比操作虚拟 DOM 更高效,省去了
虚拟 DOM 的计算、对比开销。
虚拟 DOM 的解析对比过程
组件实例化后,生成虚拟 dom,数据变更后,重新生成一个新的虚拟 dom。
将旧虚拟 dom 与新虚拟 dom 进行对比,根据差异更新有差异部分,完成视图的更新。
路由的 hash 和 history 模式的区别
hash 模式 开发中默认的模式,地址栏URL后携带#,后面为路由。 通过hashchange事件监听hash值变化,在页面hash值发生变化后,window就可以监听到事件改变,并按照规则加载相应的代码。hash值变化对应的URL都会被记录下来,这样就能实现浏览器历史页面前进后退。
window.addEventListener('hashchange', event => {
console.log(event);
})
history 模式 history模式中URL没有#,这样相对hash模式更好看,但是需要后台配置支持。
假设应用地址为hello.com,服务端不加额外的配置。当通过hello.com来访问时,是没有问题的,可以正常加载到html文件,之后通过route-link或者router.api来跳转也不会有问题,因为之后都不会刷新页面请求html,只是通过history.pushState或者history.replaceState`来改变history记录,修改地址栏地址而已;
如果是直接访问子路由hello.com/test时就会有问题,/test是子路由名,但是服务器中并不存在该目录,就无法索引到html文件,此种情况下就会出现404,所以不管是访问什么路径,都应该加载根目录的html文件。一般后端进行nginx配置处理。
history原理是使用HTML5 history提供的pushState、replaceState两个API,用于浏览器记录历史浏览栈,并且在修改URL时不会触发页面刷新和后台数据请求。
如何设置动态路由
-
params传参
- 路由配置:
/index/:id - 路由跳转:
this.$router.push({name: 'index', params: {id: "zs"}}); - 路由参数获取:
$route.params.id - 最后形成的路由:
/index/zs
- 路由配置:
-
query传参
- 路由配置:
/index正常的路由配置 - 路由跳转:
this.$rouetr.push({path: 'index', query:{id: "zs"}}); - 路由参数获取:
$route.query.id - 最后形成的路由:
/index?id=zs
- 路由配置:
区别
- 获取参数方式不一样,一个通过
$route.params,一个通过$route.query - 参数的生命周期不一样,
query参数在URL地址栏中显示不容易丢失,params参数不会在地址栏显示,刷新后会消失
路由守卫
- 全局前置钩子:
beforeEach、beforeResolve、afterEach - 路由独享守卫:
beforeEnter - 组件内钩子:
beforeRouterEnter、beforeRouterUpdate、beforeRouterLeave
CSS 选择器有哪些?---------------
(1)id选择器(#app)
(2)类选择器(.box)
(3)标签选择器(div,h1,p)
(4)属性选择器(a[rel="external"])
(5)伪类选择器(a:hover,li:nth-child)
(6)后代选择器(h1 p)
(7)相邻后代选择器(子)选择器(ul>li)
(8)兄弟选择器(li~a)
(9)相邻兄弟选择器(li+a)
(10)伪元素选择器(::before、::after)
(11)通配符选择器(*)
CSS 中哪些属性可以继承?
CSS 每一个属性在定义中都给出了这个属性是否具有继承性,一个具有继承性的属性会在没有指定值的时候,会使用父元素的同属性的值来作为自己的值。
一般具有继承性的属性有,字体相关的属性,font-size和font-weight等。文本相关的属性,color和text-align等。
表格的一些布局属性、列表属性如list-style等。还有光标属性cursor、元素可见性visibility。
当一个属性不是继承属性的时候,我们也可以通过将它的值设置为 inherit 来使它从父元素那获取同名的属性值来继承。
实现元素水平垂直居中
1. flex
2. absolute + margin
3. absolute + top + left + transform
4. gird
5. table
display 有哪些值?说明他们的作用。
block 块类型。默认宽度为父元素宽度,可设置宽高,换行显示。
none 元素不显示,并从文档流中移除。
inline 行内元素类型。默认宽度为内容宽度,不可设置宽高,同行显示。
inline-block 默认宽度为内容宽度,可以设置宽高,同行显示。
list-item 像块类型元素一样显示,并添加样式列表标记。
table 此元素会作为块级表格来显示。
inherit 规定应该从父元素继承display属性的值。
relative 和 absolute 定位原点是?
relative:自身位置
absolute:离自己最近设置了(绝对定位、相对定位、固定定位)的父元素。广义的父元素,一层层向上找祖先元素。
CSS3 有哪些新特性?(根据项目回答)
新增各种CSS选择器 (:not(.input):所有class不是“input”的节点)
圆角 (border-radius:8px)
多列布局 (multi-column layout)
阴影和反射 (Shadow\Reflect)
文字特效 (text-shadow)
文字渲染 (Text-decoration)
线性渐变 (gradient)
旋转 (transform)
缩放,定位,倾斜,动画,多背景
例如:transform:\scale(0.85,0.90)\translate(0px,-30px)\skew(-9deg,0deg)\Animation:
请解释一下 CSS3 的 Flex box(弹性盒布局模型),以及适用场景?
相关知识点:
Flex是FlexibleBox的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。
任何一个容器都可以指定为Flex布局。行内元素也可以使用Flex布局。注意,设为Flex布局以后,子元素的float、cl
ear和vertical-align属性将失效。
采用Flex布局的元素,称为Flex容器(flex container),简称"容器"。它的所有子元素自动成为容器成员,称为Flex
项目(flex item),简称"项目"。
容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis),项目默认沿主轴排列。
以下6个属性设置在容器上。
flex-direction属性决定主轴的方向(即项目的排列方向)。row(默认值)、 column
flex-wrap属性定义,如果一条轴线排不下,如何换行。
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属性定义了在分配多余空间之前,项目占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。
flex属性是flex-grow,flex-shrink和flex-basis的简写,默认值为0 1 auto。
align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父
元素的align-items属性,如果没有父元素,则等同于stretch。
回答:
flex布局是CSS3新增的一种布局方式,我们可以通过将一个元素的display属性值设置为flex从而使它成为一个flex
容器,它的所有子元素都会成为它的项目。
一个容器默认有两条轴,一个是水平的主轴,一个是与主轴垂直的交叉轴。我们可以使用flex-direction来指定主轴的方向。
我们可以使用justify-content来指定元素在主轴上的排列方式,使用align-items来指定元素在交叉轴上的排列方式。还可以使用flex-wrap来规定当一行排列不下时的换行方式。
对于容器中的项目,我们可以使用order属性来指定项目的排列顺序,还可以使用flex-grow来指定当排列空间有剩余的时候,项目的放大比例。还可以使用flex-shrink来指定当排列空间不足时,项目的缩小比例。
实现一个三角形
border 实现
.box {
width: 0;
height: 0;
border-top: 50px solid skyblue;
border-right: 50px solid transparent;
border-left: 50px solid transparent;
}
clip-path 实现
.box {
width: 160px;
height: 200px;
background-color: aquamarine;
clip-path: polygon(0 0, 0% 100%, 100% 50%);
}
/* polygon 多边形 */
clip-path 就是使用它来绘制多边形(或圆形、椭圆形等)并将其定位在元素内。实际上,浏览器不会绘制 clip-path 之外的任何区域,因此我们看到的是 clip-path 的边界。
使用 clip-path 可以为沿路径放置的每个点定义坐标。在这种情况下,就定义了`三个点`:`top-left (0 0)、bottom-left (0% 100%)、right-center (100% 50%)`。
超出这三个点所绘制的区域的内容都会被裁剪掉。
IFC 是什么?
IFC指的是行级格式化上下文,它有这样的一些布局规则:
(1)行级上下文内部的盒子会在水平方向,一个接一个地放置。
(2)当一行不够的时候会自动切换到下一行。
(3)行级上下文的高度由内部最高的内联盒子的高度决定。
请解释一下为什么需要清除浮动?清除浮动的方式
浮动元素可以左右移动,直到遇到另一个浮动元素或者遇到它外边缘的包含框。浮动框不属于文档流中的普通流,当元素浮动之后,不会影响块级元素的布局,只会影响内联元素布局。此时文档流中的普通流就会表现得该浮动框不存在一样的布局模式。当包含框的高度小于浮动框的时候,此时就会出现“高度塌陷”。
清除浮动是为了清除使用浮动元素产生的影响。浮动的元素,高度会塌陷,而高度的塌陷使我们页面后面的布局不能正常显示。
清除浮动的方式
(1)使用clear属性清除浮动。
(2)使用BFC块级格式化上下文来清除浮动。
因为BFC元素不会影响外部元素的特点,所以BFC元素也可以用来清除浮动的影响,因为如果不清除,子元素浮动则父元
素高度塌陷,必然会影响后面元素布局和定位,这显然有违BFC元素的子元素不会影响外部元素的设定。
使用 clear 属性清除浮动的原理?
使用clear属性清除浮动,其语法如下:
clear:none|left|right|both
如果单看字面意思,clear:left应该是“清除左浮动”,clear:right应该是“清除右浮动”的意思,实际上,这种解释是有问题的,因为浮动一直还在,并没有清除。
官方对clear属性的解释是:“元素盒子的边不能和前面的浮动元素相邻。”,我们对元素设置clear属性是为了避免浮动元素
对该元素的影响,而不是清除掉浮动。
还需要注意的一点是clear属性指的是元素盒子的边不能和前面的浮动元素相邻,注意这里“前面的”3个字,也就是clear属
性对“后面的”浮动元素是不闻不问的。考虑到float属性要么是left,要么是right,不可能同时存在,同时由于clear
属性对“后面的”浮动元素不闻不问,因此,当clear:left有效的时候,clear:right必定无效,也就是此时clear:left
等同于设置clear:both;同样地,clear:right如果有效也是等同于设置clear:both。由此可见,clear:left和cle
ar:right这两个声明就没有任何使用的价值,至少在CSS世界中是如此,直接使用clear:both吧。
一般使用伪元素的方式清除浮动
.clear::after{
content:'';
display:table;//也可以是'block',或者是'list-item'
clear:both;
}
clear属性只有块级元素才有效的,而::after等伪元素默认都是内联水平,这就是借助伪元素清除浮动影响时需要设置display属性值的原因。