css相关
分析比较 opacity: 0、visibility: hidden、display: none 优劣和适用场景。
display: none (不占空间,不能点击)(场景,显示出原来这里不存在的结构)
visibility: hidden(占据空间,不能点击)(场景:显示不会导致页面结构发生变动,不会撑开)
opacity: 0(占据空间,可以点击)(场景:可以跟 transition 搭配)
盒模型分为标准盒模型和怪异盒模型
- 标准盒模型:实际宽度由 宽度 + padding + border 组成(box-sizing: content-box)
- 怪异盒模型:内容宽度为 定义的宽度 - padding - border(box-sizing: border-box)
什么是BFC, BFC的条件, BFC的作用
概念
BFC 即 Block Formatting Contexts(块级格式上下文)。它是页面中的一块区域,它决定了子元素将如何定位,以及和其他元素的关系和相互作用。
具有BFC特性的元素可以看做是隔离的容器,它的子元素在布局上不会影响到外面的元素
条件
- float 不为 none
- position 的值不是 static 或 relative
- overflow 的值不是 visible
- display 的值是 inline-block / table-cell / flex / inline-flex / table-caption
作用
- 解决 margin-top 向上传递的问题
- 解决 margin 上下叠加的问题
- 清除浮动
清除浮动的方式
同一层级的元素:另外的元素设置样式 clear: both;
子元素清除父元素的浮动:
- clear属性
- BFC
- 空元素
- 给父元素设置 .clearfix::after{}
js 相关
箭头函数和普通函数有什么区别呢?
- 箭头函数没有自己独立的作用域,即它的 this 指向它定义时的作用域
- 箭头函数没有 prototype 属性
- 箭头函数没有 arguments 和 caller
- 箭头函数不能作为构造函数
全局作用域中,用 const 和 let 声明的变量不在window 上,那到底在哪里?如何去获取?
-
ES6 规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性,但let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。
-
在全局作用域中,用 let 和 const 声明的全局变量并没有在全局对象中,只是一个块级作用域(Script)中怎么获取?在定义变量的块级作用域中就能获取啊,既然不属于顶层对象,那就不加window(global)呗。
介绍 Set Map WeakSet WeakMap 的特点和区别?
- Set——对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用,在Set中NaN === NaN
- WeakSet——成员都是对象;成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏
- Map——本质上是键值对的集合,类似集合;可以遍历,方法很多,可以跟各种数据格式转换
- WeakMap——只接受对象最为键名(null 除外),不接受其他类型的值作为键名;键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的;不能遍历,方法有 get、set、has、delete
Symbol
- Symbol 除了表示独一无二的值
- 还具有元编程的能力,比如我们手写 Promise 的时候,如果不定义 Symbol.toStringTag 为 Symbol,那么通过 Object.prototype.toString.call 得到的结果就是 [object Object]
- 还可以用于判断某对象是否为某构造器的实例 Symbol.hasInstance,很多人手写 instanceof 的时候都是通过 proto 的方式,这在 ES5 是没问题的,然而在 ES6 是通过 Symbol.hasInstance
Reflect
Reflect 将对象的操作集中起来,可以通过 Reflect. 的方式来使用,比如:
- Reflect.ownKeys 可以获取对象的普通属性和Symbol类型的属性,如果不使用 Reflect.ownKeys() ,就要使用 Object.keys() 和 Object.getOwnPropertySymbols 将获取到的普通类型的属性和 Symbol 类型的属性拼接起来
- Reflect.has 可以判断一个对象是否存在某个属性,如果不用 Reflect.has 就要使用 key in object
this 指向的几种情况
- 自执行函数 -> window(非严格模式)
- 普通函数执行 -> window
- .执行 -> . 前面的内容(非严格模式)
- 箭头函数 -> 函数定义时的上下文
- call / apply / bind -> 第一个参数(一旦绑定,this 指向不能被改变)
- 事件绑定 -> 绑定事件的元素(IE 6 ~ 8 attachEvent 绑定的事件指向 window)
继承的几种方式
- 原型链继承:在构造函数的原型上扩展属性和方法
- 原型式继承:使用
Object.create()
- 拷贝继承
- 构造函数继承:只能继承父类的私有属性和方法,无法继承父类原型的属性和方法
- 组合继承:两次调用了构造函数,且子类实例的原型链不干净,包含了父类的私有属性和方法
- 寄生组合继承
扩展:ES6的class继承是使用的哪种继承方式?
寄生组合继承
JS事件循环
- 为什么要有事件循环 因为 JS 是单线程的,如果某些任务特别耗时,没有事件循环,那么其他的任务就一直被堵塞。
- 为什么要有微任务?只有宏任务不行吗? 宏任务的执行顺序总是和加入宏任务队列的顺序相关,如果此时我们有优先级较高的任务需要执行,只有宏任务是不够的,所以需要引入微任务,提高任务的优先级
- 宏任务和微任务
- 浏览器
宏任务:
- setTimeout
- setInterVal
- requestFrameAnimation
- I/O
微任务:
- Promise
- MutationObserver
宏任务和微任务的执行顺序:
在一个 script 标签里,代码是自上向下执行的,如果遇到微任务会把这个微任务加入微任务队列,如果遇到宏任务会把宏任务加入宏任务队列。当同步任务执行完成后,会先清理微任务队列,如果微任务里又有微任务,宏任务,也会被加入到微任务,宏任务队列。当所有的微任务执行完之后,再去清理宏任务队列
Promise 和 async/await 的区别
- Promise 是基于约定管理异步编程,async/await 是基于 Generator 管理异步编程
- Promise 可以通过 then 的第二个参数 和 catch 捕获异常,async/await 通过 try catch 捕获异常
遍历对象的方法有哪些
- for in
- Object.keys()
- Object.values()
- Object.getOwnPropertyNames() 可以遍历不可枚举的属性,但是不能遍历 Symbol 类型的属性
- Reflect.ownKeys()
ajax相关
一个完整的 AJAX 请求包括五个步骤:
- 创建 XMLHTTPRequest 对象
- 使用 open 方法创建 http 请求,并设置请求地址
- xhr.open(get/post,url,async,true(异步),false(同步))经常使用前三个参数
- 设置发送的数据,用 send 发送请求注册事件(给 ajax 设置事件)
- 获取响应并更新页面
- 0: UNSENT 最开始
- 1: OPENED 最开始
- 2: HEADERS_RECEIVED 服务器已经返回了响应头
- 3: LOADING 响应主体信息正在处理和返回
- 4: DONE 响应主体信息已经获取,此时证明 Ajax 请求结束了
Vue相关
MVVM的理解
MVVM 由 Model,View,ViewModel 三部分构成,Model 代表数据模型,View 代表 UI组件,ViewModel 负责同步 View 和 Model(双向绑定),Model 变化了,View 会自动跟着变化,同样的 View 变化了,Model 也会跟着变化。
补充:为什么 Vue 的官网说 Vue 没有完全遵循 MVVM 的思想?
严格的 MVVM 要求 Model 和 View 是不能直接通信的,但是 Vue 提供了 $refs 属性,让 Model 可以直接操作 View
数据响应式
- 首先判断数据的类型,如果是基础数据类型,直接返回,如果已经有
__ob__
属性,表示已经是响应式的数据了,直接返回该数据。如果是对象就走第2步,如果是数组就走第3步 - 对象是通过 Object.defineProperty,在 getter 里收集依赖,在 setter 里触发更新
- 数组是首先拷贝数组的原型,然后基于拷贝的原型改写(push,pop,unshift,shift,sort,reverse,splice)七个可以改变数组长度的方法,然后将改写后的原型赋给数组的隐式原型
- 对数组的隐式原型赋值后,还要观测数组的每一项,重复第一步
- 如果 Object.defineProperty 的 setter 里赋值,如果新赋的值是对象,也要进行观测
- 如果对数组的操作是有数据新增(push,unshift,splice),还需要观测数组新增的每一项,同第4步(这里Vue源码的实现是给每个响应式数据[对象和数组]新增了一个不可枚举的属性
__ob__
,它的作用有三,其一是用来判断数据是否已经是响应式的数据,如果是就不需再次观测,其二是属性__ob__
是 Observer 类的一个实例,实例上有对数组每一项进行响应式处理的方法),其三是 $set 方法中,__ob__
用来判断要设置属性的对象是不是响应式的对象,如果它本身就不是响应式对象,则该属性无需定义为响应式的属性
对象是在 Object.defineProperty 的 getter 里进行依赖的收集,在 setter 里触发更新。具体是通过观察者模式,每一个属性都有一个 Dep 类的实例,Dep.target 有值即指向 watcher 的时候,在 dep 内收集 watcher,并且在 watcher 内收集 dep,dep 和 watcher 是多对多的关系,因为一个组件会有多个属性,而 watcher 是组件级的,所以 一个 watcher 可能对应多个 dep ,dep 可能对应多个组件,组件内部的 computed 和 watch 都是 watcher。 不管是根组件还是非根组件(函数),它们的 data 最终的值都是对象,所以只会在 data 最外层对象的某些属性值是数组,所以在 Object.defineProperty 的 getter 里对数组进行依赖收集,我们知道依赖的收集是调用 dep 类上收集依赖的方法,Vue 的做法是在创建 Observer 类的实例的时候,定义了一个属性 dep,dep 是 Dep 类的实例。对于多维数组和数组新增的数据,Vue 的做法是,在创建 Observer 类的实例的时候,设置了一个不可枚举的属性 ob ,它的值是 Observer 类的实例,所以我们在对多维数组进行依赖收集的时候,可以调用 ob 的 dep 的方法,对于数组新增的数据,调用 ob 上的方法对数组的每一项做数据响应式,并且调用 ob.dep 上的 notify 方法触发更新。
数据初始化的顺序:props -> methods -> data -> computed -> watch
data 中能否直接访问 props 中的数据?
可以,因为 data 是在 props 之后初始化的
通过对 data 进行递归的 Object.defineProperty() 对对象的每一个 key 做数据劫持和派发更新
- 如果 data 的层级过深会影响性能
- 对象有新增和删除属性没办法做数据的响应式处理(通过$set解决)
- 如果给对象的属性赋值为对象,也会对赋值后的对象进行响应式处理
data 中数组的响应式处理是通过改写数组原型上的七个方法(push/pop/shift/unshift/sort/reverse/splice)
- 在重写数组原型之前,Vue 给每个响应式数据新增了一个不可枚举的 ob 属性,这个属性指向了 Observer 实例,可以用来防止已经被响应式处理的数据反复被响应式处理,其次,响应式的数据可以通过 ob 获取到 Observer 实例的相关方法
- 对于数组的新增操作(push/unshift/splice),会对新增的数据也做响应式处理
- 通过索引修改数组内容和直接修改数组长度是观测不到的
Vue如何进行依赖收集的?
- 每个属性都有 dep 实例,dep 实例用来收集它所依赖的 watcher
- 在模板编译的时候,会取值触发依赖的收集
- 当属性发生变化时会触发 watcher 更新
Vue的更新粒度是组件级?
首先渲染 watcher 是组件级的。在初始化的时候,会调用 _init 方法,_init 内部会调用 mount 方法会调用 mountComponent 方法,mountComponent 方法内部定义了 updateComponent 方法,updateComponent 方法内部就是调用 _update 方法将 vnode 渲染成真实 DOM,mountComponent 方法会 new 一个渲染 watcher,并把 updateComponent 传给渲染 watcher ,所以渲染 watcher 可以重新渲染DOM(试想一下,如果我们没有把更新DOM渲染的方法传递给 watcher ,更改数据后,我们需要手动去调用DOM渲染的方法;传递给 watcher 后,数据变化后,可以让 watcher 自动的去调用更新DOM渲染的方法)
在 render 函数生成 vnode 时,会判断是否是原生的HTML标签,如果不是原生HEML标签即是 组件
,会创建组件的 vnode,子组件本质是 VueComponent 函数,VueComponent 内部会调用 _init 方法,所以创建子组件 vnode 的时候,也会 new 一个渲染 watcher,所以说渲染 watcher 是组件级的,也就是说 Vue 的更新粒度是组件级的
模板编译原理
注意一:我们平时开发中使用的是不带编译的 Vue 版本(runtime-only),所以在传入选项的时候是不能使用 template 的
注意二:我们 .vue 文件中的 template 是经过 vue-loader 处理的,vue-loader 其实也是使用 vue-template-compiler 处理的
- 如果选项 options 里有 render 直接使用 render,如果没有 render 看选项里有没有 tempalte,如果有就用 template,如果没有就看选项里有没有 el,如果有 template = document.querySelector(el),最后用 compileToFunctions(tempalte) 生成render
最终都是生成 render 函数,优先级是 render > tempalte > el
- 模板编译的整体逻辑主要分为三个部分:
- 第一步:将模板字符串转换成 element ASTs (解析器)
- 第二步:对 AST 进行静态节点标记,主要用来做虚拟 DOM 的渲染优化 (优化器)(进行新旧vnode对比的时候可以跳过静态节点)
- 第三步:使用 elements ASTs 生成 render 函数代码字符串 (代码生成器)
生成 AST 的过程
其实就是 while 循环里不断的通过正则匹配字符串,如果是匹配到是开始标签,就触发 start 钩子处理开始标签和属性,如果匹配到文本,就触发 chars 钩子处理文本,如果匹配到结束标签,就调用 end 钩子处理结束标签。处理完后就把模板中已经匹配到子串截取出来,一直这样循环操作,直到模板的字符串被截取成空串跳出 while 循环。
在匹配到开始标签后,就把开始标签压入栈中,匹配到结束标签就把栈顶元素出栈。第一个进栈的元素就是根节点,除了第一根元素外,其他元素在进栈之前,栈顶的元素就是该元素的父亲节点,所以可以维护元素之间的父子关系(入栈元素的parent是栈顶元素,该入栈元素是栈顶元素的儿子),当栈被清空之后,根节点就是生成的 AST 匹配到文本内容是没有子节点的,所以它直接作为栈顶元素的儿子即可。
解析器运行过程
AST 是用 JS 中的对象来描述节点,一个对象代表一个节点,对象的属性用来保存节点所需的各种数据。
解析器内部分了好几个子解析器,比如 HTML解析器,文本解析器,过滤器解析器。其中最主要的是 HTML解析器,HTML解析器的作用就是解析HTML,它在解析的过程中会不断的触发各种钩子函数。这些钩子函数包括,开始标签钩子函数(start)、结束标签钩子函数(end),文本钩子函数(chars)和注释钩子函数(comment)。
实际上,模板解析的过程就是不断的调用钩子函数的过程,读取 template,使用不同的正则表达式匹配到不同的内容,然后触发对应的钩子函数处理匹配到的字符串截取片段。比如比配到开始标签,触发 start 钩子函数,start 钩子函数处理匹配到开始标签片段,生成一个标签节点添加到抽象语法树上。
HTML解析器解析HTML的过程就是循环(while循环)的过程,简单来说就是利用 HTML 模板字符串来循环,每轮循环都从 HTML字符串中截取一小段字符串,重复以上过程,一直到 HTML字符串被截取成一个空串结束循环,解析完毕。
在解析开始标签和结束标签是用栈来维护的,解析到开始标签就压入栈中,解析到结束标签,就从栈顶取出对应的开始标签的AST,栈顶的前一个开始标签就是该标签的父元素,然后就可以建立父子元素之间的关系。
文本解析器是对 HTML 解析器解析出来的文本进行二次加工。文本分为两种类型,一种是纯文本,一种是带变量的文本。HTML解析器在解析文本的时候,并不会区分是纯文本还是带变量的文本,如果是纯文本,不需要进行任何处理,带变量的文本需要文本解析器的进一步解析,因为带变量的文本在使用虚拟DOM进行渲染时,需要将变量替换成变量中的值。
文本解析器通过正则匹配出变量,把变量改写成 _s(x)的形式添加到数组中
初始渲染原理
- 首先是生成 render 函数
- vm._render 函数生成虚拟DOM render 函数主要返回了这样的代码 _c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world")))),所以需要定义 _c, _v, _s 这样的函数才能真正转换成虚拟DOM
- vm._update 方法将生成的虚拟DOM进行实例挂载 update 方法的核心是利用 patch 方法来渲染和更新视图,这里是初次渲染,patch 方法的第一个参数是真实DOM,更新阶段第一个参数是 oldVnode
生命周期钩子(Hook)是如何实现的?
生命周期钩子就是回调函数,在创建组件实例的时候,会在相应的时间节点(渲染的不同时期)调用这些钩子函数;Vue内部会对这些钩子函数进行处理,在内部将钩子函数维护成数组
Vue.mixin 的使用场景和原理
- Vue.mixin的作用就是抽离公共的业务逻辑,原理类似 “对象的继承”,当组件初始化的时候会调用 mergeOptions 方法进行合并,对于不同的 key(data,hooks,components...)有不同的合并策略。如果混入的数据和组件本身的数据有冲突,会采用“就近原则”,以组件本身的为准。
- mixin有很多的缺陷:命名冲突,来源不清晰,依赖问题
watch 原理
- watch 的使用方式,可以是对象,可以是函数,也可以是数组
- 不论是哪种使用方式,watch 的每一个属性对应的函数(数组的使用方式,数组中的每一项(函数))都是一个 用户watcher,其实现都是调用的
$watch(vm, handler)
- $watch 方法的实现都是
new Watcher()
,只不过是 options 参数里标记了是用户自定义的 watcher(options.user = true
) - watch 的属性对应的函数里有新值和旧值,我们是如何返回新值和旧值的呢?
- new Watcher() 的时候传递的是属性的 key,我们要把它包装成一个函数(函数内部就是根据 key 取值),赋值给 Watcher类的 getter 属性,在 Watcher 类实例化的时候,会调用一次 get 方法,我们就可以拿到它的值(取值同时会进行依赖收集)
- 在值更新后,会再次调用 Watcher 类的 get 方法获得新值
- 然后判断 watcher 的类型,如果是用户 watcher ,执行 callback,把新值旧值传递给 callback
watch api 不管是哪种使用方式,最终都是一个 key, 一个函数,对应一个 user watcher ,每一个 watcher 都有一个 getter 方法,watch api 对应的 getter 方法是根据 key 来封装的,getter 方法就是取 key 对应的数据,因为 watcher 在初始化的时候默认会调用一次 getter ,所以就拿到 key 对应的旧值了,取值也就进行了依赖收集,当 key 对应的数据改变了,watcher 的 getter 方法会再次执行,这时就拿到了新值,然后调用 key 对应的回调函数,将新值和旧值传给它
computed 原理
- 每个计算属性本质上也是一个用户 watcher,在它取值的时候进行依赖收集,computed 依赖的值改变后触发更新
- 计算属性的 watcher 在初始化的时候会有两个属性 lazy 和 dirty
- watcher 在初始化的时候,会默认调用一次 get 方法,但是 computed 默认是不执行的,所以用 lazy 属性来标记是 computed watcher
- computed 是有缓存的,即依赖的值没有发生改变,多次获取,是不会多次调用 watcher 的 get 方法获取值的,所以用 dirty 属性来标记是否需要重新计算值,如果不需要计算,直接返回 watcher 的 value,如果需要计算,再来调用 get 方法获取新的值,再返回 watcher 的 value
补充:什么时候 dirty 的值是 true 呢?
- computed watcher 初始化的时候
- computed watcher 依赖的值改变时(调用了 computed watcher 的 update 方法,即可表示依赖的值改变了)
Vue.set的实现原理
Vue.set 是为了解决给对象新增属性,以及直接通过索引修改数组时,数据不是响应式的问题
- 如果是通过索引来修改数组,会使用数组的
splice
方法替换对应索引的操作 - 如果是对象,通过 defineReactive 方法将新增属性定义为响应式的数据
虚拟DOM的作用
- 虚拟DOM就是用JS对象来描述真实DOM,是对真实DOM的抽象
- 由于直接操作DOM性能低,但是JS的操作效率高,可以将对 DOM 的操作转化为对JS的操作,最终通过 diff 算法比对差异进行更新DOM
- 虚拟DOM不依赖真实的平台环境,从而可以实现跨平台
diff算法
Vue的diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归和双指针的方式
- 首先比对是否是相同的节点,如果不是删除旧的DOM,生成新的DOM插入
- 如果是相同的节点,比对更新属性
- 判断是否是文本节点,如果是,判断文本内容是否相同,不同更新文本内容
- 比对新旧子节点,如果只有新的有子节点,新增子节点插入;如果只有旧的有子节点,将元素的 innerHTML 置为空
- 如果新旧都有子节点,比对新旧子节点(采用双指针)
- 依次是头头、尾尾、头尾、尾头比较,没有匹配到,就乱序比对
- 乱序比对:建立旧的节点的映射表(key->index)
- 新的起始节点是否能在旧的映射表中找到,不能找到直接在旧的前面插入,如果找到,将映射表找到的旧的节点,移动到前面,并将该位置置为null
- 因为在乱序比对中,有将旧节点置为 null 的情况,所以在进行子节点比对前,先判断该节点是否为null,为null顺移
- 比对完之后如果新的节点还有,插入新的节点(插入的位置要判断是否在哪里插入),如果旧的节点还有,删除旧的节点(null的位置跳过)
既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff算法比对差异呢?
如果给每个属性都添加 watcher 用于更新的话,会产生大量的 watcher 从而降低性能。一个组件才有一个渲染 watcher 用于更新,降低了粒度,所以需要 diff 算法来比对差异
Vue中key的作用和原理,谈谈你的理解
- diff 算法比对差异的时候,会通过 tag 和 key 来判断是否是相同的节点,相同节点可以复用
- 无 key 的时候,tag 相同就会被当做是相同的节点,在更新的时候可能会导致问题(实际工作中有遇到下拉框选项内容不对的问题)
- 尽量不要用索引作为 key
谈一谈对组件化的理解
- 组件化开发可以大幅提高应用开发效率、测试性、复用性等
- 常用的组件化技术:属性,自定义事件、插槽等
- 降低更新范围,只重新渲染变化的组件(Vue的更新是组件级的)
- 组件的特点:高内聚、低耦合、单向数据流
Vue组件的粒度越细越好吗?
在创建 vnode 的过程中,如果遇到组件,会创建组件 vnode,用来维护组件的状态和数据,拆分的粒度过细,导致组件嵌套层级过深,在应用的初始化时,会消耗更长的时间,并占用更多的内存空间
异步组件的实现原理
所谓异步组件,就是刚开始渲染的时候是拿不到组件的实际内容,异步执行的时候才能拿到组件的实际内容
- 一开始渲染一个占位符
- 等组件的实际内容拿到之后,强制更新(局部更新)
函数式组件的优势及原理
- 函数式组件的特性:无状态(data)、无生命周期、无this
- 函数式组件优势:性能更高(普通组件继承自Vue,创建vnode的需要new VueComponent,函数式组件直接执行render方法生成vnode)
props和$emit的实现原理
子组件会把父组件提供的 props定义到自己的 _props 上,并且把 _props 上的数据代理到子组件自己的 vm 上,所以我们可以跟 data 中的数据一样,直接通过 this.xxx 访问 props 上的数据
children的实现原理
在子组件 initLifecycle 的时候,首先子组件自己不是抽象组件(抽象组件是不建立父子关系的)时,会在子组件的options里拿到parent,把parent设为自己的children中
listeners的实现原理
在子组件 initRender 的时候,会给子组件定义个只读的响应式属性 listeners,它的值是父组件 vnode 的 _parentListeners
$refs的实现原理
- 如果是组件,ref的值组件的实例,如果是真实的HTML元素,ref的值是真实的HTML元素
- 如果 ref 是在 v-for 里定义的,ref的值就是一个数组
provide和inject的实现原理
- provide 的组件会在实例上新增一个 _provided 属性提供数据
- inject 的组件会一直向上查找 $parent._provided 获取值
- 将找到所有值在定义到自己的身上
$attrs是为了解决什么问题出现的?
attrs 传递给组件C。$attrs 没有层级的限制,只要是祖代关系的组件都可以传递。
26. slot 的实现原理
- 普通插槽:父组件渲染完毕后替换子组件的内容(编译成_t(slotName),_t(slotName)替换子组件中的vnode)
- 作用域插槽:子组件中渲染父组件中的内容(编译成_u([{}]))
27. Vue.use 是干什么的的?原理是什么?
Vue.use是用来注册插件的,可以扩展全局组件、指令、原型方法等
- Vue.use注册插件是单例模式的,已经注册过的插件不会重复注册,内部的 _installedPlugins 存储了所有注册过的插件
- Vue.use函数内部会判断插件的类型,如果是函数,直接执行该函数,如果是对象,会执行对象的install方法,并把Vue构造函数传给插件的install方法,这样插件内部可以使用Vue,而不需依赖Vue库
Vue 的事件修饰符有哪些?实现原理是什么?
- .stop
- .prevent
- .capture
- .self
- .once
- .passive
在模板编译的时候会给绑定的事件名前面加上一些标记符号(capture -> !
, once -> ~
, passive -> &
)
在给元素绑定事件的时候在去添加相应的配置
.sync 修饰符的原理
在模板编译的时候除了绑定的 value 还会添加 "update:value" 函数
自定义指令的实现原理
- 在生成抽象语法树的时候,遇到指令会给当前元素添加 directives 属性
- 通过 genDirectives 生成指令代码
- 在 patch 前将指令的钩子提取到 cbs 中,在 patch 过程中,调用对应的钩子
- 当执行 cbs 对应的钩子时,调用指令定义的方法
keep-alive的实现原理
- 对于 keep-alive 的组件,首次渲染的时候会添加 keepAlive 标记
- 再次渲染的时候会判断时候有 keepAlive 属性,如果有直接返回 vnode
Vue中的性能优化有哪些?
- 数据层级不易过深(递归设置响应式数据),合理设置响应式的数据(页面中用不到的数据没必要定义在 data 上)
- 使用数据时,合理缓存值的结果,而不是每次都去获取值
- 合理设置 key
- 合理使用 v-if 和 v-show
- v-for 和 v-if 不要使用在同一个元素上
- 合理使用异步组件(借助webpack的分包能力)
- 合理采用函数式组件(无状态组件可以使用函数式组件降低开销)
- 控制组件粒度(组件级更新)
- 使用 keep-alive 缓存组件
Vue中使用哪些设计模式
- 工厂模式 - 传入参数即可创造实例(vnode)
- 单例模式(插件注册)
- 发布订阅模式(on)
- 观察者模式(Dep,Watcher)
- 策略模式(mixin配置项和合并)
- 代理模式(Vue3 Proxy)
- 装饰器模式
Vue各个生命周期做了哪些事情?
- beforeCreate
- 在 initLifecycle 里,给组件初始化了 root, ref
- 在 initEvents 里,初始化了组件的事件监听
- 在 initRender 里,定义 attrs 和 $listeners
- created
- 在 initInjections 里,初始化 inject
- 在 initState 里,初始化 props, methods, data, computed, watch
- 在 initProvides 里,初始化 provide
- beforeMount
在定义 updateComponent 方法和 new Watcher(vm, updateComponent) 之前调用 beforeMount
- mounted
在 new Wacther 之后调用 mounted,在 new Watcher 的时候将 updateComponent 方法传给了 watcher ,updateComponent 内部在生产环境就做了一件事情,就是调用 _update 方法将 _render 方法返回的 vnode 渲染成真实节点,挂载到页面上。渲染 watcher 在初始化实例的时候,默认会调用一次传入的函数。
总结:
- 在 beforeCreate 生命周期 里可以访问 children, ref
- 在 created 生命周期里 可以访问 props, methods, data, computed
Vue的初始化渲染流程
- 首先是调用 _init 方法,init 方法里面初始化了状态(数据),调用了 beforeCreate 和 created,最后调用 $mount 方法进行渲染
- $mount 方法里面调用 mountComponent 方法
- mountComponent 方法首先调用 beforeMount ,然后定义了 updateComponent 方法,接着 new Watcher 将 updateComponent 传给 watcher,最后调用 mounted
- updateComponent 方法在生成环境就做了一件事情,就是调用 _update 方法将 _render 方法生成的 vnode 渲染成真实节点,挂载到页面上
- _update 方法里面渲染的核心方法是 patch
- patch 方法根据第一个参数是真实 DOM 元素还是 VNode 来判断是初次渲染还是更新渲染
Vue异步更新的原理
我们可能在一个操作中,可能修改一个或多个数据,一个属性对应一个 dep,一个属性它可能在模板中被访问,也可能在 computed 里面被访问,还可能在 watcher 中被访问,不管是在模板、computed 还是 watcher 中访问,它们都是相应的 watcher ,也就是说,该属性对应的 dep 会收集到多个 watcher ,当属性对应的数据改变后,这些 watcher 都应该执行 update 方法。所以我们应该把这些 watcher 先用队列收集起来,然后批量更新。
Vue.extend 方法的作用
Vue.extend 方法的作用是创建一个继承自 Vue 的类 VueComponent,它具有 Vue 所有的功能
- VueComponent 的原型是拷贝的 Vue 的原型,所以它具有 Vue 所有的功能
- VueComponent 的 options 合并了 Vue 的 options,所以通过 Vue.mixin 给 Vue options 扩展的选项,VueComponent 的 options 中也有
- VueComponent 函数内部就做了一件事,就是调用 Vue 的 _init 方法(VueComponent 也是一个 render watcher)
computed 里属性 c 依赖 data 中属性 a 和 属性 b 值,如果 c 的值并没有地方使用到,如果修改了 a 和 b 值,c 会重新计算吗?
不会。因为没有地方访问 c ,所以 c 就没有执行,也就是没有访问 a 和 b,a 和 b 的 dep 属性不会收集 c 的 computed watcher
Dep.target 为什么要用一个数组来维护呢?
因为 computed 里依赖的数据,不仅要收集 computed watcher ,也要收集渲染 watcher 。假设 Dep.target 不是用一个栈来维护的,当 computed 里的数据先被访问,data 中的数据后被访问,那么当模板编译的时候,访问到 data 中的数据时,此时该数据的 dep 收集到的只是 computed watcher ,那么当它的值改变后页面的值并不会更新。如果 Dep.target 用一个栈来维护,当 computed 取值完成后,computed watcher 的 get 方法执行后,此时 Dep.target 指向渲染 watcher
vue-router有几种钩子函数,具体是什么及执行流程
钩子函数种类:全局守卫,路由守卫,组件守卫
流程:
- 失活组件内部
beforeRouteLeave
- 全局
beforeEach
- 在重用的组件
beforeRouteUpdate
- 在路由配置里调用
beforeEnter
- 在激活组件里调用
beforeRouteEnter
- 调用全局的
beforeResolve
- 调用全局的
afterEach
前端模块化相关
CommonJs
CommonJS 主要是 Node.js 使用,通过 require 同步加载
模块,exports 导出内容。在 CommonJS 规范下,每一个 JS 文件都是独立的模块,每个模块都有独立的作用域,模块里的本地变量都是私有的
AMD(Asynchronous Module Definition)
AMD,即异步模块定义。AMD定义了一套JavaScript模块依赖异步加载标准,用来解决浏览器端模块加载的问题。AMD主要是浏览器端使用,通过 define 定义模块和依赖,require 异步加载模块,推崇依赖前置
CMD(Common Module Definition)
CMD,即通用模块定义。CMD定义了一套JavaScript模块依赖异步加载标准,用来解决浏览器端模块架子啊的问题。CMD主要是浏览器端使用,通过 define 定义模块和依赖,require 异步加载模块,推崇依赖就近
UMD(Universal Module Definition)
UMD,即通用模块定义。UMD主要为了解决 CommonJS 和 AMD 规范下的代码不通用的问题,同时还支持将模块挂载到全局,是跨平台的解决方案
ESM(ECMAScript Module)
ESM,即ESModule。官方模块化规范,现代浏览器支持,通过 import 加载模块,export 导出内容
ES6 代码转成 ES5 代码的实现思路是什么
ES6 转 ES5 目前行业标配是用Babel,转换的大致流程如下:
1.解析:解析代码字符串,生成 AST;
2.转换:按一定的规则转换、修改 AST;
3.生成:将修改后的 AST 转换成普通代码。
如果不用工具,纯人工的话,就是使用或自己写各种 polyfill 了。
介绍下 webpack 热更新原理,是如何做到在不刷新浏览器的前提下更新页面的
1.当修改了一个或多个文件;
2.文件系统接收更改并通知 webpack;
3.webpack 重新编译构建一个或多个模块,并通知 HMR 服务器进行更新;
4.HMR Server 使用 webSocket 通知 HMR runtime 需要更新,HMR 运行时通过 HTTP 请求更新 jsonp;
5.HMR 运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新。
介绍下 npm 模块安装机制
发出 npm install 命令 查询 node_modules 目录之中是否已经存在指定模块若存在,不再重新安装若不存在npm 向 registry 查询模块压缩包的网址下载压缩包,存放在根目录下的.npm 目录里解压压缩包到当前项目node_modules 目录。
与 webpack 类似的工具还有哪些?谈谈你为什么选择使用 webpack 或放弃 webpack
gulp
- 基于 nodejs 的 steam 流打包
- 定位是基于任务流的自动化构建工具
- gulp是通过 task 对整个开发过程进行构建
gulp优点:
- 流式写法简单直观
- API简单,代码量少
- 易于学习和使用
- 适合多页面应用开发
gulp缺点:
- 异常处理比较麻烦
- 工作流程顺序难以精细控制
- 不太适合单页或者自定义模块的开发
webpack
- webpack 是模块化管理工具和打包工具。通过 loader 装换,任何形式的资源都可以视为模块,比如 CommonJS模块、AMD模块、ES6模块、CSS、图片等。它可以将许多松散的模块按照依赖和打包规则打包成符合生产环境部署的前端资源
- 还可以将按需加载的模块进行代码分割,等到实际需要的时候再异步加载
- 它的定位是模块打包器,而 gulp 属于构建工具。webpack 可以代替 gulp 的一些功能,但不是一个职能的工具,可以配合使用
webpack优点:
- 可以模块化打包任何资源
- 适配任何模块系统
- 适合单页面应用的开发
webpack缺点
- 学习成本高,配置复杂
- 通过 babel 编译后的 JS 代码体积较大
rollup
- rollup 下一代ES6模块化工具,最大的亮点是利用ES6模块设计,利用 tree-shaking 生成更简洁、更简单的代码
rollup优点
- 用标准化的格式(ES6)来写代码,通过减少死代码尽可能地缩小包体积
rollup缺点
- 对代码拆分、静态资源、CommonJS模块支持并不好
parcel
- parcel是快速、零配置的 web应用程序打包器
- 目前parcel只能用来构建运行在浏览器中的网页,这也是它的出发点和关注点
parcel优点
- parcel内置了常见场景的构建方案及其依赖,无需安装各种依赖
- parcel能以HTML为入口,自动检测和打包依赖资源
- parcel默认支持模块热更新,开箱即用
parcel缺点
- 不支持 sourceMap
- 不支持 tree-shaking
- 配置不灵活(零配置)
loader 和 plugin 的不同
- loader直译为加载器。webpack将一切文件视为模块,但是 webpack 原生只能解析 js 文件,如果想将其他文件也打包的话,就会用到 loader。Loader 的作用是让 webpack 拥有了加载和解析非 javascript 文件的能力
- plugin直译为插件。plugin可以扩展webpack的功能,让 webpack 具有更多的灵活性。webpack 运行的生命周期中会广播很多的事件,plugin可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。
有哪些常见的 loader 和 plugin?它们是解决什么问题的?
loader
- babel-loader: 把 ES6 转换成 ES5
- file-loader: 把文件输出到一个文件夹中,在代码中通过相对路径去引用输出的文件
- url-loader: 和 file-loader 类似,但是能在文件较小的情况下以 base64 的方式注入到代码中去
- style-loader: 把 css 代码通过 style 标签的形式插入到页面中
- css-loader: 加载 css,支持模块化、压缩、文件导入等特性
- postcss-loader: 使用 postcss 处理 css,常用来加厂商前缀
- sass-loader: 把 sass/scss 文件编译成 css
plugin
- case-sensitive-paths-webpack-plugin: 如果路径有误直接报错
- terser-webpack-plugin: 使用 terser 压缩丑化代码
- html-webpack-plugin: 自动生成带有入口文件的 index.html
- copy-webpack-plugin: 复制不需打包的文件
- optimize-css-assets-webpack-plugin: 用于优化或压缩 css 资源
- mini-css-extract-plugin: 将 css 提取为单独的文件
- hot-module-replacement-plugin: 启用模块热替换
- define-plugin: 定义全局变量
source-map是什么?生成环境怎么用?
- source-map 是为了解决开发代码和实际运行代码不一致时,帮助我们 debug 到原始代码的技术
- webpack 通过配置生成 source-map 文件,map 文件是一种对应编译文件和原始文件的方法
source-map类型: 看似配置项很多,其实是五个关键字 eval、source-map、cheap、module和inline的任意组合
- none: 不生成 .map 文件
- eval: 不生成 .map 文件,可以通过 eval 函数定位错误文件在哪一行
- source-map: 生成 .map 文件,可以定位代码错误的行和列
- cheap: 生成 .map 文件,不包含列信息,代码报错只能定位到哪一行
- module: 包含 Loader 的 source-map
- inline: 不生成 .map 文件,将 .map 文件作为 dataUrl 嵌入
webpack中 hash、chunkhash、contenthash的区别
- hash: 是整个项目的 hash 值,其根据每次编译的内容计算得到的,每次编译后都会生成新的 hash,即修改任何文件都会导致所有文件的 hash 值跟着变化
- chunkhash: chunkhash和hash不一样,它根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值
- contenthash: 使用 chunkhash 存在一个问题,就是当在一个 JS 文件中引入 css 文件,编译后它们的 hash 值是相同的,而且只要 js 文件发生改变,关联的 css 的 hash 也会跟着改变,这个时候可以在 mini-css-extract-plugin 里设置 contenthash,保证 css 文件所处的模块里就算其他文件内容发生改变,只要 css 文件内容不变,那么不会重构建
webpack 的工作流程
- 初始化参数:把配置文件和shell语句中的参数合并,得到最终的参数
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有的插件,执行 Compiler 对象的 run 方法执行编译。
- 确定入口:根据配置中的 entry 找出所有的入口模块
- 编译模块:从入口文件出发,调用所有的 loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过本步骤的处理
- 完成模块编译:在经过使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入文件系统
tree shaking 机制的原理
- tree shaking 也叫
摇树优化
,通过移除多余代码来优化打包体积,生产环境下默认开启 - 可以在编译阶段,分析出不需要的代码
- 利用 ES6 模块化的规范
- ES Module 引入静态分析,故而编译的时候可以分析出到底加载了哪些模块
- 静态分析程序流,判断哪些模块和变量未被使用或者引用,进而删除对应代码
module chunk 和 bundle
- webpack 中一切皆模块,每个文件都是一个模块
- chunk 是 webppack 打包过程中 modules 的集合,它是
打包过程中
的概念,(enrty, splitChunk, runtimeChunk, import异步加载会产生 chunk) - bundle 打包后最终输出的一个或多个文件
chunk 和 bundle 之间的关系
- 大多数情况下一个 chunk 对应一个 bundle
- 如果加了 source-map ,一个 chunk 就对应两个 bundle
- chunk 是打包过程中的概念,bundle 是打包完成后输出的代码块,chunk 在构建完成后就呈现为 bundle
如何提高 webpack 的构建速度
-
费时分析:使用 speed-measure-webpack-plugin
-
缩小范围:
- 如果使用 require 或者 import 导入文件时未加文件扩展名,会依次尝试添加扩展名进行匹配
resolve: {
extensions: ['.js', '.jsx', '.vue']
}
- 配置文件别名: 配置别名可以加快 webpack 查找模块的速度
resolve: {
alias: {
'@src': 'xxx'
}
}
-
noParse: 用来指定哪些模块的文件内容不需要进行解析,比如 lodash,jquery 等库并没有第三方依赖,所以可以不用进行依赖分析
-
IgnorePlugin: 用于忽略某些特定的模块,让 webpack 不把这些指定的模块打包进去,比如 moment.js 中的语言包文件就不需要打包进去
-
利用缓存:
- babel-loader开启缓存:第一次编译完成后,第二次内容没有发生改变的不会再次编译
- 使用cache-loader:在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存在磁盘中
-
多进程打包:把 thread-loader 放置在其他 loader 之前,放置在 thread-loader 之后的 loader 就会在一个单独的 worker 池中进行,happyPack 多进程打包
-
ParallelUglifyPlugin: 开启多进程并行压缩丑化 JS
-
动态链接库文件:
- DllPlugin: 用于打包出一个个动态链接库
- DllReferencePlugin: 在配置文件中引入 DllPlugin 插件打包好的动态链接库
-
自动刷新
-
模块热更新
生产环境:
- noParse 一些没有依赖其他库的第三方库可以不进行解析
- IgnorePlugin 忽略一些不需要打包的模块
- babel-loader 开启缓存
- happyPack 开启多进程打包
- parallelUglifyPlugin 开启多进程压缩 JS 文件
开发环境:
- 自动刷新
- 热更新
- 动态链接库文件
webpack 性能优化 - 产出代码
最终应达到的目的:体积更小、合理分包,不重复加载、速度更快,内存占用更小
- 小图片 base64 格式(url-loader)
- bundle 使用 contenthash(缓存)
- 懒加载(异步加载,webpack 的魔法注释)
- 抽离公共代码(splitChunks)
- IgnorePlugin(忽略不需要打包的资源)
- 使用 CDN 加速(配置publicPath)
- 压缩代码
- 启动 tree-shaking
- scope hosting(把多个函数合并到一个函数)
babel 的原理
babel 是 JavaScript 编译器,它能让开发者在开发过程中,直接使用各类方言(如 ts jsx)或新的语法特性,而不需要考虑运行环境,因为 babel 可以做到按需转换为低版本支持的代码;babel 内部原理是将 JS 代码转换为 AST,对 AST 应用各种插件进行处理,最终输出编译后的 JS 代码
babel 编译流程
- 解析阶段:babel 默认使用的是
@babel/parser
将代码转换为 AST。解析一般分为两个阶段:词法分析和语法分析。
- 词法分析:对输入的字符序列做标记化(tokenization)操作
- 语法分析:处理标记与标记之间的关系,最终形成一颗完整的 AST 结构
-
转换阶段:babel 使用的是
@babel/traverse
提供的方法对 AST 进行深度优先遍历,调用插件对关注节点的处理函数,按需对 AST 节点进行增删改操作 -
生成阶段:babel 默认使用的是
@babel/generator
将上一阶段处理后的 AST 转换为代码字符串
HTTP相关
http 和 https 的区别
- http传输的数据是未加密的,也就是明文的,不安全;https 是 http + ssl,ssl 协议对 http 协议传输的数据进行加密,也就是说 https 可以对数据进行加密和身份验证更安全
- http的默认端口是80,https的默认端口是443
- http是无状态的,https可以进行身份认证
- https需要申请证书,需要一定的费用
options预检请求是什么?
浏览器在发送非简单请求时,会先向服务器发送一个 options 预检请求(增加一次http请求),以获知服务器是否允许
该请求
http1.0 http1.1 和 http2.0 在并发请求上的主要区别是什么?
- http1.0
每个 TCP 连接只能发送一个请求,当服务器响应后,下一次请求需要再次建立 TCP 连接
- http1.1
默认采用长连接,即一个 TCP 连接发送完一个请求后,默认不会关闭 TCP 连接,下个请求还会使用这个连接(Response Header: Connection: keep-alive)
追问:如何关闭长连接呢? 服务器端设置响应头 Connection: close
管道机制:在同一个 TCP 连接里,允许多个请求同时发送,所有的数据通信是有顺序的(A, B, C),但是服务器处理请求还是一个一个来的,所以会有队头阻塞的问题。
- http2.0
加了全双工模式,服务器也能同时处理多个请求了,解决了队头阻塞的问题。 多路复用:没有次序概念了。 加了服务器推送功能。
简单讲解一下 http2 的多路复用
HTTP2 采用二进制格式传输,取代了 HTTP1.x 的文本格式,二进制格式解析更高效。
多路复用代替了 HTTP1.x 的序列和阻塞机制,所有的相同域名请求都通过同一个 TCP 连接并发完成。在 HTTP1.x 中,并发多个请求需要多个 TCP 连接,浏览器为了控制资源会有 6-8个 TCP 连接都限制。
HTTP2 中同域名下所有通信都在单个连接上完成,消除了因多个 TCP 连接而带来的延时和内存消耗。单个连接上可以并行交错的请求和响应,之间互不干扰
为什么 http1.1 不能实现多路复用?
http2.0 是基于二进制帧的协议,而 http1.1 是基于文本分隔解析协议
http1.1 的报文结构里,服务器需要不断的读入字节,直到遇到换行符,处理的顺序是串行的
http2.0 以帧为最小单位,每个帧都会有标识自己属于哪个流,多个帧组成一个流。多路复用其实就是一个 TCP 里存在多条流
强缓存和协商缓存
强缓存 Expires(HTTP/1.0) / Cache-control(HTTP/1.1)
浏览器对于强缓存的处理:根据第一次请求资源时返回的响应头来确定的
- Expires: 缓存过期时间,用来指定资源到期的时间(HTTP/1.0)
- Cache-Control: cache-control: max-age=2592000 第一次拿到资源后的2592000秒内(30天),再次发送请求,读取缓存中的信息(HTTP/1.1)
- 两者都存在的话,Cache-Control 优先级高于 Expires
cache-control的取值:
- max-age: 浏览器的缓存过期时间
- s-maxage: 代理服务器的缓存过期时间
强缓存是由服务端设置的,并且基于响应头返回给客户端,客户端浏览器接收到响应后,会自己建立缓存机制,不需前端人员写代码处理
Expires 的值是一个绝对时间的 GMT 格式的时间字符串,由于失效时间是一个绝对时间,所以当服务器与客户端之间时间偏差较大时,会导致缓存混乱
Cache-Control 的取值:
- no-store: 直接禁止浏览器缓存数据(强缓存和协商缓存都不缓存),每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源
- no-cache: 不使用本地缓存,需要使用协商缓存
- immutable: 在缓存有效期内,即使用户刷新浏览器也不会向浏览器发起 http 请求
- public: 可以被所有的用户缓存,包括终端用户和 CDN 等中间代理服务器
- private: 只能被终端用户的浏览器缓存,不允许 CDN 等中间代理服务器缓存
协商缓存 Last-Modified(HTTP/1.0) / Etag(HTTP/1.1)
协商缓存就是强制缓存失效后,浏览器携带缓存标识
向服务器发送请求,由服务器根据缓存标识来决定是否使用缓存的过程
- 客户端携带获取的缓存标识发送 HTTP 请求
If-Modified-Since / If-None-Match
If-Modified-Since = Last-Modified 的值,If-None-Match = Etag 的值 - 浏览器根据资源文件是否更新返回: + 2.1 没更新,返回 304,通知客户端读取缓存的信息 + 2.2 更新了,返回 200 及最新的资源信息,以及 Last-Modified / Etag
Etag 是什么?
Etag 是一个数据签名,唯一的,根据资源内容生成的,类似于 webpack 打包出来的 contenthash
为什么会有 Etag ?
Etag 是 HTTP/1.1 新增的,主要是为了解决几个 Last-Modified 难以解决的问题:
- 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅只是修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新 get
- 某些文件修改非常频繁,比如在秒以下的时间内修改了,if-modified-since 能检查到的粒度是秒级的,这种修改无法判断
- 某些服务器不能精确的得到文件的最后修改时间
强缓存和协商缓存的区别: 协商缓存总会和服务器协商,所以协商缓存总会向浏览器发送请求
强缓存和协商缓存都是针对静态资源文件
浏览器安全
你了解哪些前端安全相关的知识?或者说你了解哪些名词?
浏览器相关:
- XSS
- XSRF
- HTTPS
- CSP(Content-Security-Policy): 内容安全策略,可以禁止加载外域的代码,禁止外域的提交
- HSTS: 强制使用 https 与服务器建立连接
- X-Frame-Options: 控制当前页面是否允许被嵌入到 iframe 中
- SRI(Subresource Integrity): 检查获得的子资源(例如从CDN)是否被篡改。将使用 base64 编码后文件哈希值写入 script 或 link 标签的 integrity 属性
- Referer-Policy: 控制 referer 的携带策略
Node(服务端)相关:
- 本地文件操作相关:路径拼接导致的文件泄露(拼接后的路径读取到了不该读取的文件)
- ReDOS:正则表达式攻击(地狱式回溯)。当编写校验的正则表达式存在缺陷或者不严谨时, 攻击者可以构造特殊的字符串来大量消耗服务器的系统资源,造成服务器的服务中断或停止
- 时序攻击
- ip origin referrer 等 request headers 的校验
你可以稍微详细介绍一下XSS吗?
XSS(Cross-site scripting)跨站脚本攻击,攻击者利用这种漏洞在网站上注入恶意攻击的代码
从外在表现上,有哪些攻击场景呢?
- 评论区植入JS代码(即可输入的地方)
- url 上拼接JS代码
从技术角度上,有哪些类型的XSS攻击呢?
- 存储型XSS
注入型脚本永久存储在目标服务器上,当浏览器请求数据时,脚本从服务器上传回并执行
攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中。
- 用户打开目标网站的时候,服务器将恶意代码从数据库中取出,拼接到 html 返回给浏览器
- 用户浏览器接收到 html 后,混在其中的恶意代码就会被执行
- 窃取用户数据,发送到攻击者网站
- 反射型XSS
攻击者结合各种手段,诱导用户点击恶意的url
攻击步骤:
- 攻击者构造出自己恶意的url
- 直接执行可执行的恶意代码
- 基于DOM的XSS
DOM型的XSS攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞
如何防范XSS攻击呢?
主旨:防止攻击者提交恶意代码,防止浏览器执行恶意代码
- 对数据进行严格的输入编码。比如 html 元素,js, css, url
- CSP(内容安全策略) default-src 'self'
- 输入验证
- 开启浏览器的XSS防御:http only,浏览器无法获取 cookie
- 验证码
能在详细介绍一下CSRF吗?
CSRF(Cross-site Request Forgery)跨站请求伪造,是一种冒充受信任的用户,向服务器发送非预期请求的攻击方式。
攻击步骤:
- 受害者登录了 a.com,并且保留了登录凭证 cookie
- 攻击者诱导受害者访问了 b.com
- b.com 向 a.com 发送请求,a.com/xxx,浏览器会直接带上 a.com 的 cookie
- a.com 收到请求了,忠实的执行了对应的操作
- 攻击者在受害者不知情的情况下,冒充受害者让 a.com 执行了攻击者定义的操作
如何防范CSRF攻击?
CSRF一般都是发生在第三方域名,攻击者无法获取到cookie信息
-
阻止第三方域名的访问
- 同源检测
- Cookie SameSite
-
提交请求的时候附加额外信息
- Token
- 双重Cookie(第二个cookie不是http only的,还是可能被攻击)
- 设置白名单,仅支持安全域名请求
- 验证码
Git相关
你有用到哪些 git 指令呢
- git fetch
- git pull
- git add
- git commit -m
- git commit --amend -m 在不创建新的 commit 记录的情况下,将修改的内容提交到最近一个 commit 上
- git push
- git reset HEAD
- git reset hard --commitId 回撤到指定版本
- git checkout 切换分支
- git checkout -b 创建分支并切换到新创建的分支上
- git branch 创建分支
- git stash 暂存修改的内容
- git stash pop 弹出暂存修改的内容
- git merge
- git rebase
- git log
- git rm
- git tag
- git diff 查看修改了哪些文件
git merge 和 git rebase 的区别
- git merge 是当前分支去合并其他分支的内容,git rebase 是被合并的分支合并到要合并的分支上
- git merge 解决冲突后, git commit 提交,会产生一条 commit 记录;git rebase 解决冲突后,git rebase --continue 提交,不会产生额外的 commit 记录
- git merge 合并分支后,commit 的记录是按时间顺序的;git rebase 是把自己的 commit 都放在被合并的那个分支的 commit 之后