简答题
闭包是什么?闭包的用途?闭包的优缺点?
- 一个函数和对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。也就是说,闭包记你可以在一个内层函数中访问到外层函数的作用域。在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
- 用途:1)从外部读取函数内部的变量;2)将创建的变量的值始终保持在内存中;3)封装对象的私有属性和私有方法
- 优点:可以避免全局变量污染;缺点:由于闭包会使得函数中的变量被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题。
简述事件循环的原理
执行栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放到栈中执行。每次栈内被清空,都会读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作。
虚拟DOM是什么?原理?优缺点?
- 虚拟dom是一种编程概念,意为将目标所需的UI通过数据结构“虚拟”表示出来,保存在内存中,然后将真实的dom与之保持同步。
- 原理:1)用JavaScript对象模拟真实DOM树,对真实DOM进行抽象;2)Diff算法比较两棵虚拟dom树的差异;3)pach算法将两个虚拟dom对象的差异应用到真正的dom树。
- 优点:1)保证性能下限:框架的虚拟dom需要适配任何上层api可能产生的操作,它的一些dom操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的dom操作性能要好很多,因此框架的虚拟dom至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;2)无需手动操作DOM:我们不再需要手动去操作DOM,只需要写好view-model的代码逻辑,框架会根据虚拟Dom和数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;3)跨平台:虚拟dom本质上是JavaScript对象,而dom与平台强相关,相比之下虚拟dom可以进行更方便地跨平台操作。
缺点:无法进行极致优化:虽然虚拟Dom+合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟dom无法进行针对性的极致优化。
Vue和React在虚拟dom的diff上,做了哪些改进使得速度很快?
相同点:
- 都是两组虚拟dom的对比(React16.8之后是fiber与虚拟dom对比)
- 只对同级节点进行对比,简化了算法的复杂度
- 都用key做为唯一的标识,进行查找,只有key和标签类型相同时才会复用老节点
- 遍历前都会根据老的节点构建一个map,方便根据key快速查找
不同点:
- React在Diff遍历的时候,只对需要修改的节点进行了记录,形成effect list,最后才会根据effect list进行真实dom的修改,修改时先删除,然后更新与移动,最后插入
- Vue在遍历的时候就用真实dom inserBefore方法,修改了真实dom,最后做的删除操作
- React采用单指针从左向右进行遍历
- Vue采用双指针,从两头向中间进行遍历
- React的虚拟Diff比较简单,Vue做了一些优化处理,相对复杂,但效率更高
Vue和React里的key的作用是什么?为什么不能用Index?用了会怎样?如果不加Key会怎样?
作用
Key是虚拟Dom中每一个VNode的唯一标识,当数据发生变化时,可以依靠Key,更准确,更快地拿到两个虚拟Dom进行节点的对比。
用Index作为key可能引发的问题
- 若对数据进行:逆序添加,逆序删除等破坏顺序的操作,会产生没有必要的真实Dom更新,界面效果没问题,但是效率更低
- 如果结构中还包含输入类的Dom,会产生错误Dom更新,界面有问题
不加Key导致的问题
- 就地复用节点:在比较新旧两个节点是否是同一个节点的过程中会判断成新旧两个节点是同一个节点,因为A.key和B.key都是undefined。所以不会重新创建节点和删除节点,只会在节点的属性层面上进行比较和更新。所以可能在某种程度上(创建和删除节点方面)会有渲染性能上的提升。
- 无法维持组件的状态:由于就地复用节点的关系,可能在维持组件状态方便会导致不可预知的错误,比如无法维持组件的动画效果,开关等状态
- 也有可能会带来性能下降:因为是直接就地复用节点,如何修改的组件,需要复用很多的节点,顺序又和原来的完全不同的话,那么创建和删除的节点数量就会比带Key的时候增加很多,性能就会有所下降
Vue双向绑定的原理
Vue接收一个模板和一个Data参数
- 首先将data中的数据进行遍历,对每一个属性执行Object.defineProperty,定义get和set函数。并为每个属性添加一个dep数组。当get执行时,会为调用的dom节点创建一个watcher存放在该数组中。当set执行时,重新赋值,并调用dep数组的notify方法,通知所有使用了该属性watcher,并更新对应dom的内容。
- 将模板加载到内存中,递归模板中的元素,检测到元素有v-开头的命令或者双大括号的指令,就会从data中取对应的值去修改模板内容,这个时候就将该dom元素添加到了该属性的dep数组。这就实现了数据驱动视图。在处理v-model指令的时候,为该dom添加input事件(或change),输入时就去修改对应的属性的值,实现了页面驱动数据。
- 将模板与数据进行绑定后,将模板添加到真实dom树中
Vue的keep-alive的作用是什么?怎样实现的?如何渲染的?
作用 keep-alive是一个内置组件,它的功能是在多个组件间动态切换时缓存被移除的组件实例。 原理 keep-alive中运用了LRU(Least Recently Used)算法
- 获取keep-alive包裹着的第一个子组件对象及其组件名;如果keep-alive存在多个子元素,keep-alive要求同时只有一个子元素被渲染。所以在开头会获取插槽内的子元素,调用getFirstComponentChild获取到第一个子元素的VNode.
- 根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则开启缓存策略。
- 根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键)。
- 如果不存在,则在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过了max设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key)。最后将该组件实例的keepAlive属性值设置为true。
Vue的渲染过程
Vue的渲染是从图中的render阶段开始的,但keep-alive的渲染是在patch阶段,这是构建组件树(虚拟dom树),并将VNode转换成真正dom节点的过程
- Vue在渲染的时候先调用原型上的_render函数将组件对象转化为一个VNode实例;而_render是通过调用createElement和createEmptyVNode两个函数进行转化;
- createElement的转化过程会根据不同的情形选择new VNode或者调用createComponent函数做VNode实例化;
- 完成VNode实例化后,这时候Vue调用原型中的_update函数把VNode渲染为真实DOM,这个过程又是通过调用 "patch"函数完成的(这就是patch阶段了)
keep-alive组件的渲染
Vue在初始化生命周期的时候,为组件实例建立父子关系会根据abstract属性决定是否忽略某个组件。在keep-alive中,设置了abstract:true,那Vue就会跳过该组件实例。最后构建的组件树中就不会包含keep-alive组件,那么由组件树渲染成的DOM树自然也不会有keep-alive相关的节点了。
缓存实例的生命周期
一个持续存在的组件可以通过onActivated()和onDeactivated()注册相应的两个状态的生命周期钩子。可以在这两个生命周期Hook里做相应的数据刷新等一些操作等等(Vue2是activated()和deactivated())
Vue是怎么解析template的?template会变成什么?
渲染过程
- 把模板编译为render函数
- 实例进行挂载,根据根节点render函数的调用,递归的生成虚拟dom
- 通过diff算法对比虚拟dom,渲染到真实dom
- 组件内部data发生变化,组件和子组件引用data作为props重新调用render函数,生成虚拟dom,返回到步骤3
template会变成什么?
template是模板占位符,不会渲染到页面上。
Vue指令实现原理
初始化
初始化全局API时,在platforms/web下,调用createPatchFunction生成VNode转换为真实DOM的patch方法,初始化中比较重要一步是定义了与DOM节点相对应的hooks方法,在DOM的创建(create)、激活(activate)、更新(update)、移除(remove)、销毁(destroy)过程中,分别会轮询调用对应的hooks方法,这些hooks中一部分是指令声明周期的入口。
// src/core/vdompatch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
export function createPatchFunction (backend) {
let i, j
const cbs = {}
const { modules, nodeOps } = backend
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
// modules对应vue中模块,具体有class, style, domListener, domProps, attrs, directive, ref, transition
for (j = 0; j < modules.length; ++j) {
if (isDef(modules[j][hooks[i]])) {
// 最终将hooks转换为{hookEvent: [cb1, cb2, ...], ...}形式
cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
}
// ...
return function patch (oldVnode, vnode, hydrating, removeOnly) {
// ...
}
}
模板编译
模板编译就是解析指令参数,具体解构后的ASTElement如下所示:
{
tag: 'input',
parent: ASTElement,
directives: [
{
arg: null, // 参数
end: 56, // 指令结束字符位置
isDynamicArg: false, // 动态参数,v-xxx[dynamicParams]='xxx'形式调用
modifiers: undefined, // 指令修饰符
name: "model",
rawName: "v-model", // 指令名称
start: 36, // 指令开始字符位置
value: "inputValue" // 模板
},
{
arg: null,
end: 67,
isDynamicArg: false,
modifiers: undefined,
name: "focus",
rawName: "v-focus",
start: 57,
value: ""
}
],
// ...
}
生成渲染方法
Vue推荐采用指令的方式去操作DOM,由于自定义指令可能会修改DOm或者属性,所以避免指令对模板解析的影响,在生成渲染方法时,首先处理的是指令,如v-model,本质是一个语法糖,在拼接渲染函数时,会给元素加上value属性与input事件(以input为例,这个也可以用户自定义)。
with (this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_c('input', {
directives: [{
name: "model",
rawName: "v-model",
value: (inputValue),
expression: "inputValue"
}, {
name: "focus",
rawName: "v-focus"
}],
attrs: {
"type": "text"
},
domProps: {
"value": (inputValue) // 处理v-model指令时添加的属性
},
on: {
"input": function($event) { // 处理v-model指令时添加的自定义事件
if ($event.target.composing)
return;
inputValue = $event.target.value
}
}
})])
}
生成VNode
Vue的指令设计是方便我们操作DOM,在生成VNode时,指令并没有做额外处理。
生成真实DOM
在Vue初始化过程中,我们需要记住两点:
- 状态的初始化是父 -> 子,如beforeCreate、created、beforeMount,调用顺序是父 -> 子
- 真实DOM挂载顺序是子 -> 父,如mounted,这是因为在生成真实DOM过程中,如果遇到组件,会走组件创建的过程,真实DOM的生成是从子到父一级级拼接。
在patch过程中,每此调用createElm生成真实DOM时,都会检测当前VNode是否存在data属性,存在,则会调用invokeCreateHooks,走初创建的钩子函数,核心代码如下:
// src/core/vdom/patch.js
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// ...
// createComponent有返回值,是创建组件的方法,没有返回值,则继续走下面的方法
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
const data = vnode.data
// ....
if (isDef(data)) {
// 真实节点创建之后,更新节点属性,包括指令
// 指令首次会调用bind方法,然后会初始化指令后续hooks方法
invokeCreateHooks(vnode, insertedVnodeQueue)
}
// 从底向上,依次插入
insert(parentElm, vnode.elm, refElm)
// ...
}
以上是指令钩子方法的第一个入口,是时候揭露directive.js神秘的面纱了,核心代码如下:
// src/core/vdom/modules/directives.js
// 默认抛出的都是updateDirectives方法
export default {
create: updateDirectives,
update: updateDirectives,
destroy: function unbindDirectives (vnode: VNodeWithData) {
// 销毁时,vnode === emptyNode
updateDirectives(vnode, emptyNode)
}
}
function updateDirectives (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (oldVnode.data.directives || vnode.data.directives) {
_update(oldVnode, vnode)
}
}
function _update (oldVnode, vnode) {
const isCreate = oldVnode === emptyNode
const isDestroy = vnode === emptyNode
const oldDirs = normalizeDirectives(oldVnode.data.directives, oldVnode.context)
const newDirs = normalizeDirectives(vnode.data.directives, vnode.context)
// 插入后的回调
const dirsWithInsert = []
// 更新完成后回调
const dirsWithPostpatch = []
let key, oldDir, dir
for (key in newDirs) {
oldDir = oldDirs[key]
dir = newDirs[key]
// 新元素指令,会执行一次inserted钩子方法
if (!oldDir) {
// new directive, bind
callHook(dir, 'bind', vnode, oldVnode)
if (dir.def && dir.def.inserted) {
dirsWithInsert.push(dir)
}
} else {
// existing directive, update
// 已经存在元素,会执行一次componentUpdated钩子方法
dir.oldValue = oldDir.value
dir.oldArg = oldDir.arg
callHook(dir, 'update', vnode, oldVnode)
if (dir.def && dir.def.componentUpdated) {
dirsWithPostpatch.push(dir)
}
}
}
if (dirsWithInsert.length) {
// 真实DOM插入到页面中,会调用此回调方法
const callInsert = () => {
for (let i = 0; i < dirsWithInsert.length; i++) {
callHook(dirsWithInsert[i], 'inserted', vnode, oldVnode)
}
}
// VNode合并insert hooks
if (isCreate) {
mergeVNodeHook(vnode, 'insert', callInsert)
} else {
callInsert()
}
}
if (!isCreate) {
for (key in oldDirs) {
if (!newDirs[key]) {
// no longer present, unbind
callHook(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy)
}
}
}
}
对于首次创建,执行过程如下:
- oldVnode === emptyNode, isCreate 为true,调用当前元素中所有bind钩子方法。
- 检测指令中是否存在inserted钩子,如果存在,则将insert钩子合并到VNode.data.hooks属性中。
- DOM挂载结束后,会执行invokeInsertHook,所有已挂载节点,如果VNode.data.hooks中存在insert钩子。则会调用,此时会触发指令绑定的inserted方法。
一般首次创建只会走bind和inserted方法,而update和componentUpdated则与bind和inserted对应。在组件依赖状态发生改变时,会用VNode diff算法,对节点进行打补丁式更新,其调用流程:
- 响应式数据发生改变,调用dep.notify,通知数据更新。
- 调用patchVNode,对新旧VNode进行差异化更新,并全量更新当前VNode属性(包括指令,就会进入updateDirectives方法)。
- 如果指令存在update钩子方法,调用update钩子方法,并初始化componentUpdated回调,将postpatch hooks挂载到VNode.data.hooks中。
- 当前节点及子节点更新完毕后,会触发postpatch hooks,即指令的componentUpdated方法
核心代码如下:
// src/core/vdom/patch.js
function patchVnode (
oldVnode,
vnode,
isertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// ...
const oldCh = oldVnode.children
const ch = vnode.children
// 全量更新节点的属性
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// ...
if (isDef(data)) {
// 调用postpatch钩子
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
unbind方法是在节点销毁时,调用invokeDestroyHook,
注意事项
使用自定义指令时,和普通模板数据绑定,v-model还是存在一定的差别,如虽然我传递参数(v-xxx='param')是一个引用类型,数据变化时,并不能触发指令的bind或者inserted,这是因为在指令的声明周期内,bind和inserted只是在初始化时调用一次,后面只会走update和componentUpdated。
指令的声明周期执行顺序为bind -> inserted -> update -> componentUpdated,如果指令需要依赖于子组件的内容时,推荐在componentUpdated中写相应业务逻辑。
vue中,很多方法都是循环调用,如hooks方法,事件回调等,一般调用都是try catch包裹,这样做的目的是为了防止一个处理方法报错,导致整个程序崩溃,这一点在我们开发过程中可以借鉴使用。
Vue的render和template
template模板
template用于声明组件的字符串模板会被预编译成虚拟DOM渲染函数。通过 template 选项提供的模板将会在运行时即时编译。
Vue默认推荐使用模板,有以下几点原因:
- 模板更贴近实际的HTML。这使得我们能够更方便地重用一些已有的HTML代码片段,能够带来更好的可访问性体验、能更方便地使用CSS应用样式,并且更容易使设计师理解和修改。
- 由于其确定的语法,更容易对模板做静态分析。这使得Vue的模板编译器能够应用许多编译时优化来提升虚拟DOM的性能表现。
render
用于编程式地创建组件虚拟DOM树的函数。是字符串模板template的一种替代,可以使你利用JavaScript的丰富表达力来完全编程式地声明组件最终的渲染输出。
预编译的模板,例如单文件组件中的模板,会在构建时被编译成render选项。如果一个组件中同时存在render和template,则render将具有更高的优先级。
前端工程化
把软件工程相关的方法和思想应用到前端开发中,提升开发效率、提升产品质量、降低开发难度、降低企业成本。简单理解可以分为主要5个部分:开发、构建、部署、性能、规范化;
前端性能优化
觉的性能优化方案
如何在项目中进行性能优化
- 确定优化的目标和预期
-
明确性能数据
- 网络资源请求时间
- Time To Start Render(TTSR):浏览器开始渲染的时间
- DOM Ready: 页面解析完成的时间
- Time To Interact(TTI): 页面可交互时间
- Total Blocking Time(TBT): 总阻塞时间,代表页面处于不可交互状态的耗时
- First Input Delay(FID): 从用户首次交互,到浏览器响应的时间
-
对性能数据进行目标和预期的确定:比如对比原先数据优化到多少比例,分析竞品确定目标,确定技术方案
-
技术方案调研
- 分析项目背景,挖掘项目痛点
- 分析项目现状
- 调研业界方案
-
技术方案设计,方案选型/对比
- 梳理项目现状:比如项目规模大,开发多
- 梳理项目痛点:比如不同模块变更导致性能下降,问题往往在测试时才发现
- 调研性能分析方案:通过Performace 火焰图、Lighthouse、Chrome Devtools Protocol等工具
- 根据对比和分析确定最优方案,并跟领导解析
-
Node.j异步IO模式
处理器访问任何寄存器和Cache等封装以外的数据资源都可以当成I/O操作,包括内存,磁盘,显卡等外部设备。在Nodejs中像开发者调用fs读取本地文件或网络请求等操作都属于I/O操作。(最普遍抽象I/O是文件操作和TCP/UDP网络操作)
Nodejs为单线程的,在单线程模式下,任务都是顺序执行的,但是前面的任务如果用时过长,那么势必会影响到后续任务的执行,通常I/O与CPU之间的计算是可以并行进行的,但是同步的模式下,I/O的进行会导致后续任务的等待,这样阻塞了任务的执行,也造成了资源不能很好的利用。
为了解决如下的问题,Nodejs选择了异步I/O的模式,让单线程不再阻塞,更合理的使用资源。
异步I/O操作机制
- 第一阶段:第一次异步I/O的调用,首先在Nodejs底层设置请求参数和回调函数callback,形成请求对象。
- 第二阶段:形成的请求对象,会被放入线程池,如果线程池有空闲的I/O线程,会执行此次I/O任务,得到结果。
- 第三阶段:
事件循环中I/O观察者,会从请求对象中找到已经得到结果的I/O请求对象,取出结果和回调函数,将回调函数放入事件循环中,执行回调,完成整个异步I/O任务。 - 对于如何感知异步I/O任务执行完毕的?以及如何获取完成的任务的呢?libuv作为中间层,在不同平台上,采用手段不同,在Unix下通过epoll轮询,在Windows下通过内核(IOCP)来实现,FreeBSD下通过kqueue实现。
设计模式
简介
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。
设计模式原则
-
S-Single Responsibility Principle 单一职责原则
- 一个程序只做好一件事
- 如果功能过于复杂就拆分开,每个部分保持独立
-
O-Open/Closed Principle 开放/封闭原则
- 对扩展开放,对修改封闭
- 增加需求时,扩展新代码,而非修改已有代码
-
L-Liskov Substitution Principle 里氏替换原则
- 子类能覆盖父类
- 父类能出现的地方子类就能出现
-
I-Interface Segregation Principle 接口隔离原则
- 保持接口的单一独立
- 类似单一职责原则,这里更关注接口
-
D-Dependency Inversion Principle 依赖倒置原则
- 面向接口编程,依赖于抽象而不依赖于具体
- 使用方只关注接口而不关注具体类的实现
设计模式分类(23种设计模式)
-
创建型
- 单例模式
- 原型模式
- 工厂模式
- 抽象工厂模式
- 建造者模式
-
结构型
- 适配器模式
- 装饰器模式
- 代理模式
- 外观模式
- 桥接模式
- 组合模式
- 享元模式
-
行为型
- 观察者模式
- 迭代器模式
- 策略模式
- 模板方法模式
- 职责链模式
- 命令模式
- 备忘录模式
- 状态模式
- 访问者模式
- 中介者模式
- 解释器模式
微前端
微服务,维基上对其定义为:一种软件开发技术-面向服务的体系结构(SOA)架构样式的一种变体,将应用程序构造为一组松散耦合的服务,并通过轻量级的通信协议组织起来。 具体来讲,就是将一个单体应用,按照一定规则拆分为一组服务。这些服务,各自拥有自己的仓库,可以独立开发、独立部署,有独立的边界,可以由不同的团队来管理,甚至可以使用不同的编程语言来编写。但对前端来说,仍然是一个完整的服务。
微前端常用技术方案
目前,业界主流的微前端实现方案主要有:
- 路由分发式微前端
- iframe
- single-spa
- qiankun
- webpack5: module federation
- Web Component
节流和防抖
为什么要用到防抖节流
- 当函数绑定一些持续触发的事件如:resize、scroll、mousemove,键盘输入,多次快速click等等
- 如果每次触发都要执行一次函数,会带来性能下降,资源请求太频繁等问题
什么是防抖
所谓防抖,就是指触发事件后n秒后才执行函数,如果在n秒内又触发了事件,则会重新计算函数执行时间。
- 非立即执行版本
let num = 1;
const content = document.getElementById('content);
// 功能函数
function count() {
this.innerHTML = num++;
}
content.onclick = debounce(count, 1000);
// 防抖函数,非立即执行版本
function debounce(func, wait) {
let timeout;
return function () {
const content = this;
const args = [...arguments];
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
}
}
- 立即执行版本
// 防抖函数,立即执行版本
functon debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = [..arguments];
if (timeout) clearTimeout(timeout);
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait);
callNow && func.apply(conext, args);
}
}
什么是节流
所谓节流,就是指连续触发事件但是在n秒内只执行一次函数,节流会稀释函数的执行频率。
- 时间戳
function throttle(func, wait) {
let previous = 0;
return function() {
let now = Date.now();
let context = this;
let args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}
content.onmousemove = throttle(count, 1000);
- 定时器
function throttle(func, wait) {
let timeout;
return function() {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(() => {
timeout = null;
func.apply(context, args);
}, wait)
}
}
}
Vue父子组件的通信方式
- prop(常用)--父组件传递数据给子组件
- $emit--子组件与父组件通信
- vuex(Vue2),pinia(Vue3)
- arrts和listeners(组件封装用的比较多)
- provide和inject(高阶组件/组件库用的较多)
- EventBus(声明一个全局Vue实例变量)
- $parent(访问父实例)
- $root(访问根组件实例)
async await的原理
async/await, generator, promise这三者的关联和区别是什么?
React和Vue的技术层面的区别
Vue常用的Hook都有哪些
生命周期钩子(Vue3)
onMounted(), onUpdated(), onUnmounted(), onBeforeMount(), onBeforeUpdate(), onErrorCaptured(), OnRenderTracked(), OnRenderTriggered(), OnActivated(), onDeactivated(), onServerPrefetch()
KOA洋葱模型
KOA的洋葱模型指的是以next()函数为分割点,先由外到内执行Request的逻辑,再由内到外执行Response的逻辑。通过洋葱模型,将多个中间件之间的通信等变得更加可行和简单。其实现的原理并不是很复杂,主要是compose方法。
https是如何保证安全的?是如何保证不被中间人攻击的?
HTTPS是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包。HTTPS开发的主要目的,是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。
generator是如何做到中断和恢复的
- 开始:创建生成器对象
- 暂停:SuspendGenerator的功能是暂停当前函数的执行,其字节码处理函数里面多次调用StoreObjectField来保存生成器函数当前运行的状态,最后返回累加器中的值,之前提到过,此时累加器存的是生成器对象generator。所以V8代码中的生成器对象generator返回给了JavaScript代码中的iterator。
- 恢复:ResumeGenerator恢复之前保存的状态,最后调用Dispatch函数,取出下一条字长码。
一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。
协程是一种比线程更加经量经的存在。普通线程是抢先式的,会争夺CPU资源,而协程是合作的,可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。它的运行流程大致如下:
协程A开始执行 -> 协程A执行到某个阶段,进入暂停,执行权转移到协程B -> 协程B执行完成或暂停,将执行权交还A -> 协程A恢复执行 -> 协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。
function和箭头函数的定义有什么区别?导致了在this指向这块表现不同
区别
- 函数定义的写法不同
- this的指向不同:使用function定义的函数,this的指向随着调用环境的变化而变化的,而箭头函数中的this指向是固定不变的,一直指向是的定义函数的环境。
- 构造函数:function是可以定义构造函数的,而箭头函数是不行的。
- 变量提升:由于js的内存机制,function的级别最高,而用箭头函数定义函数的时候,需要var(let const定义的时候更不必说)关键词,而var所定义的变更不能得到变更提升,故箭头函数一定要定义在调用之前。
- arguments的获取:使用function声明函数时,可以使用arguments来获取传入函数的所有参数,而使用箭头函数声明的方法无法使用arguments来获取所有参数。
宏任务和微任务的区分是什么?优先级?
区别
- 进程的切换是宏任务,线程的切换是微任务
- 微任务是在当前任务执行结束后立即执行的任务,它可以看作是在当前任务的“尾巴”添加的任务。宏任务则需要排队等待JavaScript引擎空闲时才能执行的任务。
- 同一级微任务会优先于宏任务执行
JS中微任务和宏任务执行顺序
三个原则
- 第一个原则:万物皆从全局上下文准备退出,全局的同步代码运行结束的这个时机开始
- 第二个原则:同一层级下,微任务永远比宏任务先执行
- 第三个原则:每个宏任务,都单独关联了一个微任务队列
- 首先执行当前代码(script上下文代码块),直到遇到第一个宏任务或微任务。
- 如果遇到微任务,则将它添加到微任务队列中,继续执行同步任务。
- 如果遇到宏任务,则将它添加到宏任务队列中,继续执行同步任务。
- 当前任务执行完毕后,JavaScript引擎会先执行所有微任务队列中的任务,直到微任务队列为空。
- 然后执行宏任务队列中的第一个任务,直到宏任务队列为空。
- 重复步骤4和步骤5,直到所有任务都被执行完毕。需要注意的是,同一级微任务比宏任务优先级要高,因此在同一个任务中, 如果既有微任务又有宏任务,那么微任务会先执行完毕。而在不同的任务中,宏任务的执行优先级要高于微任务,因此在一个宏任务执行完毕后,它才会执行下一个宏任务和微任务队列中的任务。举个例子,假设当前代码有一个setTimeout和一个Promise,它们分别对应一个宏任务和一个微任务。那么执行顺序如下:1)执行当前代码,将setTimeout和Promise添加到宏任务和微任务队列中;2)当前任务执行完毕,JavaScript引擎先执行微任务队列中的Promise回调函数;3)微任务队列为空后,再执行宏任务队列中的setTimeout回调函数。需要注意的是,在一些特殊情况下,微任务和宏任务的执行顺序可能会发生变化,比如在使用MutationObserver监听DOM变化时,它会被视为一个微任务,但是它的执行顺序可能会比其他微任务更靠后。因此,需要根据具体情况来理解和处理微任务和宏任务的执行顺序。
- 宏任务:setTimeout,setInterval
- 微任务:Promise.then(非new Promise),process.nextTick(node中)
实现LRU算法
解法一
使用数组和对象结合的方式
var LRUCache = function (capacity) {
// 用数组记录读和定的顺序
this.keys = []
// 用对象来保存key value值
this.cache = {}
// 容量
this.capacity = capacity
}
LRUCache.prototype.get = function (key) {
// 如果存在
if (typeof this.cache[key] !== 'undefined') {
// 先删除原来的位置
remove(this.keys, key)
// 再移动到最后一个,以保持最新访问
this.keys.push(key)
// 返回值
return this.cache[key]
}
return -1
}
LRUCache.prototype.put = function (key, value) {
if (typeof this.cache[key] !== 'undefined') {
// 存在的时候先更新值
this.cache[key] = value
// 再更新位置到最后一个
remove(this.keys, key)
this.keys.push(key)
} else {
// 不存在的时候加入
this.keys.push(key)
this.cache[key] = value
// 容量如果超过了最大值,则删除最久未使用的(也就是数组中的第一个key)
if (this.keys.length > this.capacity) {
removeCache(this.cache, this.keys, this.keys[0])
}
}
}
// 移出数组中的key
function remove (arr, key) {
if (arr.length) {
const index = arr.indexOf(key)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
// 移除缓存中的key
function removeCache (cache, keys, key) {
cache[key] = null
remove(keys, key)
}
解法二
Map实现方式
var LRUCache = function (capacity) {
this.cache = new Map()
this.capacity = capacity
}
LRUCache.prototype.get = function (key) {
if (this.cache.has(key)) {
const value = this.cache.get(key)
// 更新位置
this.cache.delete(key)
this.cache.set(key, value)
return value
}
return -1
}
LRUCache.prototype.put = function (key, value) {
// 已经存在的情况下,更新其位置到“最新”即可
// 先删除,后插入
if (this.cache.has(key)) {
this.cache.delete(key)
} else {
// 插入数据前先判断,size是否符合capacity
// 已经 >= capacity,需要把最开始插入的数据删除掉
// keys()方法得到一个可遍历对象,执行newt()拿到一个形如{value: 'xxx', done: false}的对象
if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value)
}
}
this.cache.set(key, value)
}
Promise then第二个参数和catch的区别是什么?
主要区别就是,如果在then的第一个函数里抛出了异常,后面的catch能捕获到,而then的第二个参数捕获不到,then的第二参数本来就是用来处理上一层失败状态的。
作用域链
定义:作用域的集合就是作用域链
1、函数在执行的过程中,先从自己内部寻找变量
2、如果找不到,再从创建当前函数所在的作用域去找,从此往上,也就是向上一级找。
当在作用域内访问 变量/方法 的时候,会找离自己最近的那个 变量/方法 (就近原则)
给出代码的输出顺序
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0)
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3');
});
console.log('script end');