Vue

介绍目前常见前端面试题,Vue 基础原理题

参考慕课网实战课 前端框架及项目面试-聚焦 Vue3 React Webpack 总结的知识点

Vue 基本使用

computed 和watch

  • computed 有缓存,data不变则不会重新计算
  • watch如何深度监听?
  • watch 监听引用类型,拿不到oldVal

class 和 style

  • 使用动态属性
  • 使用驼峰式写法

条件渲染

  • V-if v-else的用法,可使用变量,也可以使用===表达式
  • v-if 和 V-show的区别?
  • v-if 和 V-show的使用场景?

循环(列表)渲染

  • 如何遍历对象?——也可以用V-for
  • key的重要性。key 不能乱写(如random或者index)
  • V-for和v-if不能一起使用!

事件

  • event参数,自定义参数

    • event 是原生的
    • 事件被挂载到当前元素
    • 和 DOM 事件一样
  • 事件修饰符,按键修饰符

事件修饰符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--阻止单击事件继续传播-->
<a v-on:click.stop="doThis"></a>
<!--提交事件不再重载页面-->
<form v-on:submit.prevent="onSubmit"></form>
<!--修饰符可以串联-->
<a v-on:click.stop.prevent="doThat"></a>
<!--只有修饰符-->
<form v-on:submit.prevent></form>
<!--添加事件监听器时使用事件捕获模式-->
<!--即内部元素触发的事件先在此处理,然后才交由内部元素进行处理-->
<div v-on:click.capture="doThis">...</div>
<!--只当在event.target是当前元素自身时触发处理函数-->
<!--即事件不是从内部元素触发的-->
<div v-on:click.self="doThat">...</div>

按键修饰符

1
2
3
4
5
6
<!--即使ALt或Shift被一同按下时也会触发-->
<button @click.ctrl="onClick">A</button>
<!--有且只有Ctrl被按下的时候才触发-->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!--没有任何系统修饰符被按下的时候才触发-->
<button @click.exact="onClick">A</button>
  • 【观察】事件被绑定到哪里?

表单

  • v-model
  • 常见表单项textarea checkbox radio select
  • 修饰符lazy number trim

Vue 组件使用

  • props和$emit
  • 组件间通讯-自定义事件
  • 组件生命周期

生命周期(单个组件)

  • 挂载阶段 created mounted
  • 更新阶段 updated
  • 销毁阶段 destroyed

created mounted的区别

el temp之间,create Vue实例初始化了,但还没渲染完成,mount已经渲染完成

Vue 高级特性

  • 不是每个都很常用,但用到的时候必须要知道
  • 考察候选人对Vue的掌握是否全面,且有深度
  • 考察做过的项目是否有深度和复杂度(至少能用到高级特性)

Vue 高级特性

  • 自定义v-model
  • $nextTick
  • slot
  • 动态、异步组件
  • keep-alive
  • mixin

$nextTick

  • Vue 是异步渲染(原理部分会详细讲解)
  • data改变之后,DOM不会立刻渲染
  • $nextTick会在DOM 渲染之后被触发,以获取最新DOM节点

slot

  • 基本使用
  • 作用域插槽
  • 具名插槽

image-20220805160755455

动态组件

  • :is=”component-name”用法
  • 需要根据数据,动态渲染的场景。即组件类型不确定。

异步组件

  • import()函数
  • 按需加载,异步加载大组件

keep-alive

  • 缓存组件
  • 频繁切换,不需要重复渲染
  • Vue常见性能优化

应用场景:tab切换

在Vue层级里面缓存切换,而v-show是在css层级display:none里

mixin

  • 多个组件有相同的逻辑,抽离出来
  • mixin 并不是完美的解决方案,会有一些问题
  • Vue3提出的Composition API 旨在解决这些问题

mixin的问题

  • 变量来源不明确,不利于阅读
  • 多mixin 可能会造成命名冲突
  • mixin 和组件可能出现多对多的关系,复杂度较高

相关的面试技巧

  • 可以不太深入,但必须知道
  • 熟悉基本用法,了解使用场景
  • 最好能和自己的项目经验结合起来

Vuex 使用

  • 面试考点并不多(因为熟悉Vue之后,vuex 没有难度)
  • 但基本概念、基本使用和API必须要掌握
  • 可能会考察 state的数据结构设计(后面会讲)

Vuex 基本概念

  • state
  • mutation
  • getters
  • action

用于Vue组件

  • dispatch
  • commit
  • mapState
  • mapGetters
  • mapActions
  • mapMutations

image-20220805221041300

Actions才能做异步操作

Vue-router 使用

  • 面试考点并不多(前提是熟悉Vue)
  • 路由模式(hash、H5 history)
  • 路由配置(动态路由、懒加载)

Vue-router 路由模式

  • hash模式(默认),如http://abc.com/#/user/10
  • H5 history 模式,如http://abc.com/user/20
  • 后者需要server 端支持,因此无特殊需求可选择前者
1
2
3
4
const router = new VueRouter({
mode: ‘history', //使用h5 history模式
routes: [...]
})

动态路由

1
2
3
4
5
6
7
8
9
10
11
12
13
const User={
//获取参数如 10 20
template: '<div> User {{ $route.params.id }} </div>'
}
const router = new VueRouter({
routes:[
//动态路径参数以冒号开头。能命中/user/10 /user/20等格式的路由
{
path:‘/user/:id',
component: User
}
]
})

懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default new VueRouter({
routes:[
{
path:'/',
component:()=>import(
/* webpackChunkName:"navigator"*/
'/../components/Navigator'
)
},
{
path:'/feedback',
component:()=>import(
/* webpackChunkName:"feedback"*/
'./../components/FeedBack'
)
}
]
})

Vuex Vue-router总结

  • 面试考点并不多(前提是熟悉Vue)
  • 掌握基本概念,基本使用
  • 面试官时间有限,需考察最核心、最常用的问题,而非边角问题

Vue 原理(大厂必考)

  • 面试为何会考察原理?
    • 之前然知其所以然——各行业通用的道理
    • 了解原理,才能应用的更好(竞争激烈,择优录取)
    • 大厂造轮子(有钱有资源,业务定制,技术KPI)
  • 面试中如何考察?以何种方式?
    • 考察重点,而不是考察细节。掌握好2/8原则
    • 和使用相关联的原理,例如vdom、模板渲染
    • 整体流程是否全面?热门技术是否有深度?
  • Vue 原理包括哪些?
    • 组件化
    • 响应式
    • vdom和diff
    • 模板编译
    • 渲染过程
    • 前端路由

组件化基础

  • “很久以前“就有组件化
    • asp jsp php 已经有组件化了
    • nodejs 中也有类似的组件化
  • 数据驱动视图(MVVM,setState)
    • 传统组件,只是静态渲染,更新还要依赖于操作DOM
    • 数据驱动视图-Vue MVVM

在这里插入图片描述

  • 数据驱动视图-React setState(暂时按下不表)

Vue 响应式

  • 组件data的数据一旦变化,立刻触发视图的更新
  • 实现数据驱动视图的第一步
  • 考察Vue原理的第一题
  • 核心API-Object.defineProperty
  • 如何实现响应式,代码演示
  • Object.defineProperty的一些缺点(Vue3.0启用Proxy)
    • Proxy 兼容性不好,且无法polyfill
    • Vue2.x还会存在一段时间,所以都得学
    • Vue3.0相关知识,下一章讲,这里只是先提一下

Object.defineProperty 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const data = {}
const name = 'zhangsan'
Object.defineProperty(data,"name",{
get: function(){
console.log('get')
return name
},
set: function(newVal){
console.log('set')
name=newVal
}
});
//测试
console.log(data.name) //get zhangsan
data.name='list' //set

Object.defineProperty 实现响应式

  • 如何监听对象(深度监听),监听数组(需要特殊处理)
  • 复杂对象,深度监听
  • Object.defineProperty 缺点:
    • 深度监听,需要递归到底,一次性计算量大
    • 无法监听新增属性/删除属性(Vue.set Vue.delete)
    • 无法原生监听数组,需要特殊处理

虚拟DOM(Virtual DOM)和diff

  • vdom 是实现vue和React的重要基石
  • dif 算法是vdom中最核心、最关键的部分
  • vdom是一个热门话题,也是面试中的热门问题

VDOM

原因:

  • DOM操作非常耗费性能
  • 以前用jQuery,可以自行控制DOM操作的时机,手动调整
  • Vue和React 是数据驱动视图,如何有效控制DOM操作?

解决方案-vdom

  • 有了一定复杂度,想减少计算次数比较难
  • 能不能把计算,更多的转移为JS计算?因为JS执行速度很快
  • vdom-用JS模拟DOM结构,计算出最小的变更,操作DOM

image-20220807004410446

通过snabbdom学习vdom

  • 简洁强大的vdom库,易学易用

  • Vue 参考它实现的vdom和diff

  • https://github.com/snabbdom/snabbdom

  • Vue3.0重写了vdom的代码,优化了性能

  • 但vdom的基本理念不变,面试考点也不变

  • React vdom具体实现和Vue也不同,但不妨碍统一学习

snabbdom 重点总结

  • h函数
  • vnode 数据结构
  • patch 函数

vdom总结

  • 用JS模拟DOM结构(vnode)
  • 新旧vnode对比,得出最小的更新范围,最后更新DOM
  • 数据驱动视图的模式下,有效控制DOM操作

diff 算法

  • diff 算法是vdom中最核心、最关键的部分
  • diff 算法能在日常使用vue React中体现出来(如key)
  • dif 算法是前端热门话题,面试“宠儿”

diff 算法概述

  • diff即对比,是一个广泛的概念,如linux diff命令、git diff等
  • 两个js对象也可以做 diff,如 https://github.com/cujojs/jiff
  • 两棵树做 diff,如这里的vdom diff

image-20220807012438747

树 diff 的时间复杂度O(n^3)

  • 第一,遍历tree1
  • 第二,遍历tree2
  • 第三,排序
  • 1000个节点,要计算1亿次,算法不可用

优化时间复杂度到 O(n)

  • 只比较同一层级,不跨级比较
  • tag不相同,则直接删掉重建,不再深度比较
  • tag和key,两者都相同,则认为是相同节点,不再深度比较

diff 算法总结

  • patchVnode
  • addVnodes removeVnodes
  • updateChildren(key的重要性)

vdom 和 diff-总结

  • 细节不重要,updateChildren的过程也不重要,不要深究
  • vdom核心概念很重要:h、vnode、patch、diff、key等
  • vdom存在的价值更加重要:数据驱动视图,控制DOM操作

模板编译

  • 模板是vue开发中最常用的部分,即与使用相关联的原理
  • 它不是html,有指令、插值、JS表达式,到底是什么?
  • 面试不会直接问,但会通过“组件渲染和更新过程”考察

原理

  • 前置知识:JS 的 with 语法
    • 改变 {} 内自由变量的查找规则,当做 obj 属性来查找
    • 如果找不到匹配的 obj 属性,就会报错
    • with要慎用,它打破了作用域规则,易读性变差

JS 的 with 语法

  • vue template complier 将模板编译为 render 函数
  • 执行 render 函数生成 vnode

编译模板

  • 模板不是html,有指令、插值、JS表达式,能实现判断、循环
  • html是标签语言,只有JS才能实现判断、循环(图灵完备的)
  • 因此,模板一定是转换为某种JS代码,即编译模板
  • 原理:
    • 模板编译为render函数,执行render 函数返回vnode
    • 基于vnode 再执行patch 和diff(后面会讲)
    • 使用webpack vue-loader,会在开发环境下编译模板(重要)
  • vue 组件中使用render代替 template
    • 讲完模板编译,再讲这个render,就比较好理解了
    • 在有些复杂情况中,不能用template,可以考虑用 render
    • React 一直都用render(没有模板),和这里一样
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.component('heading',{
// template: `xxxx`,
render: function(createElement){
return createElement(
'h'+this.level,
[
createElement('a', {
attrs: {
name:'headerId',
href: '#' + 'headerId'
}
},' this is a tag')
]
)
}
})

总结

  • with 语法
  • 模板到 render 函数,再到 vnode,再到渲染和更新
  • vue 组件可以用 render 代替 template

组件渲染/更新过程

  • 一个组件渲染到页面,修改data触发更新(数据驱动视图)
  • 其背后原理是什么,需要掌握哪些要点?
  • 考察对流程了解的全面程度

回顾学过的知识

  • 响应式:监听 data 属性 getter setter(包括数组)
  • 模板编译:模板到render函数,再到vnode
  • vdom:patch(elem,vnode)和patch(vnode,newVnode)

组件渲染/更新过程

  • 初次渲染过程
    • 解析模板为render函数(或在开发环境已完成,vue-loader)
    • 触发响应式,监听 data 属性 getter setter
    • 执行render函数,生成vnode,patch(elem,vnode)
      • 注意点 :执行 render 函数会触发 getter
  • 更新过程
    • 修改data,触发setter(此前在getter中已被监听)
    • 重新执行render 函数,生成newVnode
    • patch(vnode,newVnode)
  • 异步渲染
    • 回顾$nextTick
    • 汇总data的修改,一次性更新视图
    • 减少DOM操作次数,提高性能

完成流程图

总结

  • 渲染和响应式的关系
  • 渲染和模板编译的关系
  • 渲染和vdom的关系
  • 初次渲染过程
  • 更新过程
  • 异步渲染

前端路由原理

  • 稍微复杂一点的SPA(单页面应用),都需要路由
  • vue-router 也是vue全家桶的标配之一
  • 属于“和日常使用相关联的原理”,面试常考
  • 回顾 vue-router 的路由模式
    • hash(默认)
      • hash变化会触发网页跳转,即浏览器的前进、后退
      • hash 变化不会刷新页面,SPA必需的特点
      • hash 永远不会提交到server端(前端自生自灭)
    • H5 history
      • 用url规范的路由,但跳转时不刷新页面
      • history.pushState
      • window.onpopstate

网页url组成部分

hash代码演示

H5 history代码演示

总结

  • hash - window.onhashchange
  • H5 history - history.pushState 和 window.onpopstate
  • H5 history 需要后端支持

两者选择

  • to B 的系统推荐用 hash,简单易用,对url规范不敏感
    • 后台管理页面
  • to C 的系统,可以考虑选择 H5 history,但需要服务端支持
  • 能选择简单的,就别用复杂的,要考虑成本和收益

面试题

❤ v-if 和 v-show区别

  • 得分点 v-show true/false都渲染 、 v-if true渲染 false不渲染
  • 标准回答
    • 作用: 都是控制元素隐藏和显示的指令
    • 区别:
      • v-show: 控制的元素无论是true还是false,都被渲染出来了,通过CSS display:none控制元素隐藏
      • v-if: 控制的元素是true,进行渲染,如果是false不渲染,根本在dom树结构中不显示
  • 加分回答
    • 应用:
      • v-show: 适合使用在切换频繁显示/隐藏的元素上
      • v-if: 适合使用在切换不频繁,且元素内容很多,渲染一次性能消耗很大的元素上

❤ v-for 为什么要用 key

  • 得分点 性能优化、diff算法节点比对、key不能是index
  • 标准回答
    • 为了性能优化 因为vue是虚拟DOM,更新DOM时用diff算法对节点进行一一比对,比如有很多li元素,要在某个位置插入一个li元素,但没有给li上加key,那么在进行运算的时候,就会将所有li元素重新渲染一遍,但是如果有key,那么它就会按照key一一比对li元素,只需要创建新的li元素,插入即可,不需要对其他元素进行修改和重新渲染。
    • 必须用key,且不能是index和random
    • 原理:diff 算法中通过 tag 和 key 来判断,是否是 sameNode
    • 效果:减少渲染次数,提升渲染性能
  • 加分回答
    • key也不能是li元素的index,因为假设我们给数组前插入一个新元素,它的下标是0,那么和原来的第一个元素重复了,整个数组的key都发生了改变,这样就跟没有key的情况一样了

❤ Vue 组件生命周期

  • 得分点 beforeCreate、created、beforeMounted、mounted beforeUpdate、updated 、 beforeDestroy、destroyed

  • 标准回答

    • 钩子函数用来描述一个组件从引入到退出的全过程中的某个过程,整个过程称为生命周期。
    • 钩子函数按照组件生命周期的过程分为,挂载阶段=>更新阶段=>销毁阶段。
    • 每个阶段对应的钩子函数
      • 挂载阶段:beforeCreate、created、beforeMounted、mounted
      • 更新阶段:beforeUpdate、updated
      • 销毁阶段:beforeDestroy、destroyed
    • 每个阶段特点与适合做什么
      • created:实例创建完成,可访问data、computed、watch、methods上的方法和数据,未挂载到DOM,不能访问到el属性,el属性,ref属性内容为空数组常用于简单的ajax请求,页面的初始化
      • beforeMount:在挂载开始之前被调用,beforeMount之前,会找到对应的template,并编译成render函数
      • mounted:实例挂载到DOM上,此时可以通过DOM API获取到DOM节点,$ref属性可以访问常用于获取VNode信息和操作,ajax请求
      • beforeupdate:响应式数据更新时调用,发生在虚拟DOM打补丁之前,适合在更新之前访问现有的DOM,比如手动移除已添加的事件监听器
      • updated:虚拟 DOM 重新渲染和打补丁之后调用,组件DOM已经更新,可执行依赖于DOM的操作避免在这个钩子函数中操作数据,可能陷入死循环
      • beforeDestroy:实例销毁之前调用。这一步,实例仍然完全可用,this仍能获取到实例,常用于销毁定时器、解绑全局事件、销毁插件对象等操作
  • 加分回答

    父子组件钩子函数在生命周期的调用顺序:

    • 渲染顺序:先父后子,完成顺序:先子后父
    • 更新顺序:父更新导致子更新,子更新完成后父
    • 销毁顺序:先父后子,完成顺序:先子后父

    加载渲染过程

    父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted

    子组件更新过程

    父beforeUpdate->子beforeUpdate->子updated->父updated

    父组件更新过程

    父 beforeUpdate->父 updated

    销毁过程

    父beforeDestroy->子beforeDestroy->子destroyed->父destroyed

生命周期是什么

Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是Vue的生命周期

各个生命周期的作用

生命周期 描述
beforeCreate 组件实例被创建之初,组件的属性生效之前
created 组件实例已经完全创建,属性也绑定,但真实dom还没有生成,$el还不可用
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用
mounted el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdate 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
update 组件数据更新之后
activated keep-alive专属,组件被激活时调用
deactivated keep-alive专属,组件被销毁时调用
beforeDestroy 组件销毁前调用
destroyed 组件销毁后调用

img

什么是vue生命周期?

  • 答: Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。

vue生命周期的作用是什么?

  • 答:它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。

vue生命周期总共有几个阶段?

  • 答:它可以总共分为8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。

第一次页面加载会触发哪几个钩子?

  • 答:会触发下面这几个beforeCreatecreatedbeforeMountmounted

DOM 渲染在哪个周期中就已经完成?

  • 答:DOM 渲染在 mounted 中就已经完成了

❤ Vue 组件通信的方式

  • 得分点 父子通信、自定义属性、props$emitEventBus$onVueX

  • 标准回答

  • Vue组件的通信方式分为两大类,一类是父子组件通信,另一类是任何关系类型组件通信(父子、兄弟、非兄弟)

    • 父子组件的通信方式:
      • 父给子传递数据,通过给子组件添加自定义属性,比如:list是父组件给子组件传递的数据。
      • 子获取父的数据,在子组件中使用props属性获取,props是只读,不可以被修改,所有被修改都会失效和被警告
      • 子给父传递数据,通过给子组件传递父组件的方法,子组件调用父组件的方法传递数据,比如:deleteHandler就是父组件的函数,在子组件中通过this.$emit('方法名',参数),调用父组件的方法,并把数据传递到父组件。
    • 任何关系类型组件通信(父子、兄弟、非兄弟)方式: 自定义事件
      • EventBus: 使用方法是创建一个新的Vue实例,需要通信的组件都引入该Vue实例,传递数据的组件使用 event.$emit('名称',参数)发送数据,接收数据的组件使用 event.$on('名称',方法)接收数据。
    • VueX: 集中管理项目公共数据,Vuex 的状态存储是响应式的,当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。
  • 加分回答

    EventBus的优缺点

    • 缺点:
      • vue是单页应用,如果你在某一个页面刷新了之后,与之相关的EventBus会被移除,这样就导致业务走不下去。
      • 同时如果页面中有反复操作的业务,EventBus在监听的时候就会触发很多次,需要好好处理EventBus在项目中的关系。
      • 在vue页面销毁时,同时移除EventBus事件监听。
    • 优点:
      • 解决了多层组件之间繁琐的事件传播,使用原理十分简单,代码量少。
      • 适合于简单,组件传递数据较少的项目,大型项目业务复杂的还是尽量使用VueX

❤ Vue 组件渲染和更新的过程

  • 初次渲染过程
    • 解析模板为render函数(或在开发环境已完成,vue-loader)
    • 触发响应式,监听 data 属性 getter setter
    • 执行render函数,生成vnode,patch(elem,vnode)
      • 注意点 :执行 render 函数会触发 getter
  • 更新过程
    • 修改data,触发setter(此前在getter中已被监听)
    • 重新执行render 函数,生成newVnode
    • patch(vnode,newVnode)
  • 异步渲染
    • 回顾$nextTick
    • 汇总data的修改,一次性更新视图
    • 减少DOM操作次数,提高性能

完成流程图

❤ Vue2 双向绑定的原理与缺陷

  • 得分点: Object.definePropertygettersetter
  • 标准回答: Vue响应式指的是:组件的data发生变化,立刻触发视图的更新
    • 原理: Vue 采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,通过Object.defineProperty()来劫持数据的settergetter,在数据变动时发布消息给订阅者,订阅者收到消息后进行相应的处理。 通过原生 js 提供的监听数据的API,当数据发生变化的时候,在回调函数中修改dom
    • 核心API:Object.defineProperty() API 的使用
      • 作用:用来定义对象属性
      • 特点: 默认情况下定义的数据的属性不能修改;描述属性和存取属性不能同时使用,使用会报错
      • 响应式原理: 获取属性值会触发getter方法;设置属性值会触发setter方法 在setter方法中调用修改dom的方法
  • 加分回答: Object.defineProperty缺点
    • 一次性递归到底开销很大,如果数据很大,大量的递归导致调用栈溢出
    • 不能监听对象的新增属性和删除属性
    • 无法正确的监听数组的方法,当监听的下标对应的数据发生改变时

代码示例用到 observe-demo 项目的 observe.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// 触发更新视图
function updateView () {
console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})

// 重新定义属性,监听起来
function defineReactive (target, key, value) {
// 深度监听
observer(value)

// 核心 API
Object.defineProperty(target, key, {
get () {
return value
},
set (newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)

// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue

// 触发更新视图
updateView()
}
}
})
}

// 监听对象属性
function observer (target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}

// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }

if (Array.isArray(target)) {
target.__proto__ = arrProto
}

// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}

// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
data.name = 'lisi'
data.age = 21
// console.log('age', data.age)
data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

❤ Vue3 实现数据双向绑定的方法

  • 得分点 Proxy、数据拦截、劫持整个对象、返回一个新对象、有13种劫持
  • 标准回答 Vue3.0 是通过Proxy实现的数据双向绑定,Proxy是ES6中新增的一个特性,实现的过程是在目标对象之前设置了一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
    • 用法: ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。 var proxy = new Proxy(target, handler) target: 是用Proxy包装的被代理对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
    • handler: 是一个对象,其声明了代理target 的一些操作,其属性是当执行一个操作时定义代理的行为的函数。
  • 加分回答
    • Object.defineProperty 的问题:在Vue中,Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。目前只针对以上方法做了hack处理,所以数组属性是检测不到的,有局限性Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue里,是通过递归以及遍历data对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象,不管是对操作性还是性能都会有一个很大的提升。
    • Proxy的两个优点:可以劫持整个对象,并返回一个新对象,有13种劫持

❤ Vuex 是什么,每个属性是干嘛的,如何使用 ?

  • 得分点:state、mutations、getters、actions、module、store.commit、store.dispatch
  • 标准回答: Vuex是集中管理项目公共数据的。
    • Vuex 有state、mutations 、getters、actions、module属性。
    • state 属性用来存储公共管理的数据。
    • mutations 属性定义改变state中数据的方法, 注意:不要在mutation中的方法中写异步方法ajax,那样数据就不可跟踪了 。
    • getters 属性可以认为是定义 store 的计算属性。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
    • action属性类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。
    • moudle属性是将store分割成模块。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块,从上至下进行同样方式的分割
      • 使用方法: state :直接以对象方式添加属性 mutations :通过store.commit调用 action:通过 store.dispatch 方法触发 getters:直接通过store.getters.调用
  • 加分回答: 可以使用mapState、mapMutations、mapAction、mapGetters一次性获取每个属性下对应的多个方法。 VueX在大型项目中比较常用,非关系组件传递数据比较方便。

请简单实现双向数据绑定mvvm

1
2
3
4
5
6
7
8
9
10
11
12
<input id="input"/>
const data = {};
const input = document.getElementById('input');
Object.defineProperty(data, 'text', {
set(value) {
input.value = value;
this.value = value;
}
});
input.onChange = function(e) {
data.text = e.target.value;
}

❤ HashRouter 和 HistoryRouter的区别和原理?

  • 得分点 # window.onhashchange history.pushState window.onpopstate
  • 标准回答
    • HashRouter HistoryRouter区别
      1. history 和 hash都是利用浏览器的两种特性实现前端路由,history是利用浏览历史记录栈的API实现,hash是监听location对象hash值变化事件来实现
      2. history的url没有’#’号,hash反之
      3. 相同的url,history会触发添加到浏览器历史记录栈中,hash不会触发,history需要后端配合,如果后端不配合刷新新页面会出现404,hash不需要。
    • HashRouter的原理:通过window.onhashchange方法获取新URL中hash值,再做进一步处理
    • HistoryRouter的原理:通过history.pushState 使用它做页面跳转不会触发页面刷新,使用window.onpopstate 监听浏览器的前进和后退,再做其他处理
  • 加分回答
    • hash模式下 url 会带有#,需要url更优雅时,可以使用history模式。 需要兼容低版本的浏览器时,建议使用hash模式。 需要添加任意类型数据到记录时,可以使用history模式。
    • 两者优势:
      • to B 的系统推荐用 hash,简单易用,对url规范不敏感 (后台管理页面)
      • to C 的系统,可以考虑选择 H5 history,但需要服务端支持
      • 能选择简单的,就别用复杂的,要考虑成本和收益

❤ Diff 算法

  • 得分点 patch、patchVnode、updateChildren(key重要性)、vue优化时间复杂度为O(n)
  • 标准回答
    • Diff算法比较过程
    • 第一步:patch函数中对新老节点进行比较 如果新节点不存在就销毁老节点 如果老节点不存在,直接创建新的节点 当两个节点是相同节点的时候,进入 patchVnode 的过程,比较两个节点的内部
    • 第二步:patchVnode函数比较两个虚拟节点内部 如果两个虚拟节点完全相同,返回 当前vnode 的children 不是textNode,再分成三种情况
      • 有新children,没有旧children,创建新的
      • 没有新children,有旧children,删除旧的
      • 新children、旧children都有
      • 执行updateChildren比较children的差异,这里就是diff算法的核心 当前vnode 的children 是textNode,直接更新text
    • 第三步:updateChildren函数子节点进行比较
      • 第一步 头头比较。若相似,旧头新头指针后移(即 oldStartIdx++ && newStartIdx++),真实dom不变,进入下一次循环;不相似,进入第二步。
      • 第二步 尾尾比较。若相似,旧尾新尾指针前移(即 oldEndIdx-- && newEndIdx--),真实dom不变,进入下一次循环;不相似,进入第三步。
      • 第三步 头尾比较。若相似,旧头指针后移,新尾指针前移(即 oldStartIdx++ && newEndIdx--),未确认dom序列中的头移到尾,进入下一次循环;不相似,进入第四步。
      • 第四步 尾头比较。若相似,旧尾指针前移,新头指针后移(即 oldEndIdx-- && newStartIdx++),未确认dom序列中的尾移到头,进入下一次循环;不相似,进入第五步。
      • 第五步 若节点有key且在旧子节点数组中找到sameVnode(tag和key都一致),则将其dom移动到当前真实dom序列的头部,新头指针后移(即 newStartIdx++);否则,vnode对应的dom(vnode[newStartIdx].elm)插入当前真实dom序列的头部,新头指针后移(即 newStartIdx++)。
      • 但结束循环后,有两种情况需要考虑:
        • 新的字节点数组(newCh)被遍历完(newStartIdx > newEndIdx)。那就需要把多余的旧dom(oldStartIdx -> oldEndIdx)都删除,上述例子中就是c,d
        • 新的字节点数组(oldCh)被遍历完(oldStartIdx > oldEndIdx)。那就需要把多余的新dom(newStartIdx -> newEndIdx)都添加。

updateChildren函数图示

不使用key和使用key的对比

❤ Vue 的 keep-alive

  • 得分点 缓存组件、条件缓存、路由配合条件缓存、不重新加载、activated、deactivated
  • 标准回答
    • <keep-alive>作用:缓存组件,提升性能,避免重复加载一些不需要经常变动且内容较多的组件。
    • <keep-alive>的使用方法:使用<keep-alive>标签对需要缓存的组件进行包裹,默认情况下被<keep-alive>标签包裹的组件都会进行缓存,区分被包裹的组件是否缓存有两种方法,
      • 第一种是给keepalive 添加属性,组件名称指的是具体组件添加的name,不是路由里面的name。include 包含的组件(可以为字符串,数组,以及正则表达式,只有匹配的组件会被缓存)。exclude 排除的组件(以为字符串,数组,以及正则表达式,任何匹配的组件都不会被缓存)。
      • 第二种也是最常用的一种是,和路由配合使用:在路由中添加meta属性。 使用keepalive导致组件不重新加载,也就不会重新执行生命周期的函数,如果要解决这个问题,就需要两个属性进入时触发:activated 退出时触发:deactivated
  • 加分回答
    • <keep-alive>适用的场景:首页展示固定数据的组件,比如banner九宫格

❤ computed 和 watch 的区别

  • 得分点 computed值有缓存、触发条件是依赖值发生更改、 watch无缓存支持异步、监听数据变化
  • 标准回答
    • computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
    • watch: 更多的是观察的作用,支持异步,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
  • 加分回答
    • computed应用场景:需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
    • watch应用场景:需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

❤ Vue 中 $nextTick 作用与原理

  • 得分点 异步渲染、获取DOM、Promise
  • 标准回答 Vue 在更新 DOM 时是异步执行的,在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。所以修改完数据,立即在方法中获取DOM,获取的仍然是未修改的DOM。
    • $nextTick的作用是:该方法中的代码会在当前渲染完成后执行,就解决了异步渲染获取不到更新后DOM的问题了。
    • $nextTick的原理:$nextTick 本质是返回一个Promise
  • 加分回答
    • 应用场景:在钩子函数created()里面想要获取操作Dom,把操作DOM的方法放在 $nextTick

❤ vue-router 实现懒加载的方法

  • 得分点 import、require

  • 标准回答

    • vue-router 实现懒加载的方法有两种:

      • ES6 的 impot方式:

        1
        2
        3
        4
        component: () => import(
        /* webpackChunkName: "about" */
        '../views/About.vue'
        )
      • VUE 中的异步组件进行懒加载方式:

        1
        2
        3
        component: resolve=>(
        require(['../views/About'],resolve)
        )
  • 加分回答

    • vue-router 实现懒加载的作用:性能优化,不用到该路由,不加载该组件。

vue、react、angular

  • Vue.js 一个用于创建 web 交互界面的库,是一个精简的 MVVM。它通过双向数据绑定把 View 层和 Model 层连接了起来。实际的 DOM 封装和输出格式都被抽象为了DirectivesFilters
  • AngularJS 是一个比较完善的前端MVVM框架,包含模板,数据双向绑定,路由,模块化,服务,依赖注入等所有功能,模板功能强大丰富,自带了丰富的 Angular指令
  • react React 仅仅是 VIEW 层是facebook公司。推出的一个用于构建UI的一个库,能够实现服务器端的渲染。用了virtual dom,所以性能很好。

未解决的问题:

如何设计一个组件

我用vue开发的所有项目,都是采用组件化的思想开发的。一般我在搭建项目的时候,会创建一个views目录和一个commen目录和一个feature目录,views目录中放页面级的组件,commen中放公共组件(如:head(公共头组件),foot(公共底部组件)等),feature目录内放功能组件(如:swiper(轮播功能组件),tabbar(切换功能组件)、list(上拉加载更多功能组件))

首先,组件可以提升整个项目的开发效率。能够把页面抽象成多个相对独立的模块,解决了我们传统项目开发:效率低、难维护、复用性低等问题。

使用Vue.extend方法创建一个组件,然后使用Vue.component方法注册组件。但是我们一般用脚手架开发项目,每个 .vue单文件就是一个组件。在另一组件import 导入,并在components中注册,子组件需要数据,可以在props中接受定义。而子组件修改好数据后,想把数据传递给父组件。可以采用$emit方法。

Vue 面试真题演练

  • 自己觉得是面试重点
  • 网上收集整理的面试题
  • 热门技术和知识点

Vue3

面试题

  • Vue3比Vue2有什么优势?
  • 描述Vue3生命周期
  • 如何看待Composition API和Options API?
  • 如何理解 ref toRef 和 toRefs?
  • Vue3升级了哪些重要的功能?
  • Composition API如何实现代码逻辑复用?
  • Vue3如何实现响应式?
  • watch和watchEffect的区别是什么?
  • setup中如何获取组件实例?
  • Vue3为何比Vue2快?
  • Vite是什么?
  • Composition API和React Hooks的对比

Vue3 比 Vue2 有什么优势?

  • 性能更好
  • 体积更小
  • 更好的ts支持
  • 更好的代码组织
  • 更好的逻辑抽离
  • 更多新功能

Vue2 和 Vue3有什么区别

  • 重构响应式系统,使用Proxy替换Object.defineProperty,使用Proxy优势:
  • 可直接监听数组类型的数据变化

  • 监听的目标为对象本身,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升

  • 可拦截apply、ownKeys、has等13种方法,而Object.defineProperty不行

  • 直接实现对象属性的新增/删除

  • 新增 Composition API,更好的逻辑复用和代码组织

  • 重构 Virtual DOM

    • 模板编译时的优化,将一些静态节点编译成常量
    • slot优化,将slot编译为lazy函数,将slot的渲染的决定权交给子组件
    • 模板中内联事件的提取并重用(原本每次渲染都重新生成内联函数)
  • 代码结构调整,更便于Tree shaking,使得体积更小

  • 使用Typescript替换Flow

vue3 带来的新特性/亮点

参考链接

1. 压缩包体积更小

当前最小化并被压缩的 Vue 运行时大小约为 20kB(2.6.10 版为 22.8kB)。Vue 3.0捆绑包的大小大约会减少一半,即只有10kB!

2. Object.defineProperty -> Proxy

  • Object.defineProperty是一个相对比较昂贵的操作,因为它直接操作对象的属性,颗粒度比较小。将它替换为es6的Proxy,在目标对象之上架了一层拦截,代理的是对象而不是对象的属性。这样可以将原本对对象属性的操作变为对整个对象的操作,颗粒度变大。
  • javascript引擎在解析的时候希望对象的结构越稳定越好,如果对象一直在变,可优化性降低,proxy不需要对原始对象做太多操作。

3. Virtual DOM 重构

vdom的本质是一个抽象层,用javascript描述界面渲染成什么样子。react用jsx,没办法检测出可以优化的动态代码,所以做时间分片,vue中足够快的话可以不用时间分片

4. Performance

vue3在性能方面比vue2快了2倍。

  • 重写了虚拟DOM的实现
  • 运行时编译
  • update性能提高
  • SSR速度提高

5. Tree-shaking support

vue3中的核心api都支持了tree-shaking,这些api都是通过包引入的方式而不是直接在实例化时就注入,只会对使用到的功能或特性进行打包(按需打包),这意味着更多的功能和更小的体积。

6. Composition API

vue2中,我们一般会采用mixin来复用逻辑代码,用倒是挺好用的,不过也存在一些问题:例如代码来源不清晰、方法属性等冲突。基于此在vue3中引入了Composition API(组合API),使用纯函数分隔复用代码。和React中的hooks的概念很相似

  • 更好的逻辑复用和代码组织
  • 更好的类型推导

7. 新增的三个组件Fragment、Teleport、Suspense

8. Better TypeScript support

在vue2中使用过TypesScript的童鞋应该有过体会,写起来实在是有点难受。vue3则是使用ts进行了重写,开发者使用vue3时拥有更好的类型支持和更好的编写体验。

Vue3 生命周期

  • Options API 生命周期
    • beforeDestroy 改为 beforeUnmount
    • destroyed 改为 unmouted
    • 其他沿用 Vue2 的生命周期
  • Composition API 生命周期

代码示例用到 vue3-demo 项目的 life-cycles 组件

Composition API 对比 Options API

  • Composition API 带来了什么?

    • 更好的代码组织

      代码组织对比

  • 更好的逻辑复用(有一道专门的面试题)

  • 更好的类型推导

  • Composition API 和 Options API 如何选择?

    • 不建议共用,会引起混乱
    • 小型项目、业务逻辑简单,用 Options API
    • 中大型项目、逻辑复杂,用 Composition API
  • 别误解 Composition API

    • Composition API 属于高阶技巧,不是基础必会
    • Composition API 是为解决复杂业务逻辑而设计
    • Composition API 就像 Hooks 在 React 中的地位

为何需要 toRef 和 toRefs?

  • 初衷:不丢失响应式的情况下,把对象数据分解/扩散
  • 前提:针对的是响应式对象( reactive 封装的)非普通对象
  • 注意:不创造响应式,而是延续响应式

如何理解 ref toRef 和 toRefs

  • ref toRef toRefs 是什么
  • 最佳使用方式
  • 进阶,深入理解

ref

  • 生成值类型的响应式数据
  • 可用于模板和 reactive
  • 通过 .value 修改值

代码示例用到 vue3-demo 项目的 Ref 组件

toRef

  • 针对一个响应式对象( reactive 封装)的 prop
  • 创建一个 ref,具有响应式
  • 两者保持引用关系

代码示例用到 vue3-demo 项目的 ToRef 组件

toRefs

  • 将响应式对象(reactive封装)转换为普通对象
  • 对象的每个prop都是对应的ref
  • 两者保持引用关系

代码示例用到 vue3-demo 项目的 ToRefs 组件

合成函数返回响应式对象

合成函数返回响应式对象

最佳使用方式

  • 用 reactive 做对象的响应式,用 ref 做值类型响应式
  • setup 中返回 toRefs(state),或者 toRef(state,‘xxx’)
  • ref 的变量命名都用 xxxRef
  • 合成函数返回响应式对象时,使用 toRefs

进阶,深入理解

  • 为何需要 ref ?
    • 返回值类型,会丢失响应式
    • 如在 setup、computed、合成函数,都有可能返回值类型
    • Vue 如不定义 ref,用户将自造 ref,反而混乱
  • 为何需要 .value ?
    • ref 是一个对象(不丢失响应式),value 存储值
    • 通过 .value 属性的 get 和 set 实现响应式
    • 用于模板、reactive 时,不需要 .value,其他情况都需要
  • 为何需要 toRef to Refs ?
    • 初衷:不丢失响应式的情况下,把对象数据分解/扩散
    • 前提:针对的是响应式对象(reactive封装的)非普通对象
    • 注意:不创造响应式,而是延续响应式

Vue3 升级了哪些重要的功能

  • createApp

createApp

  • emits 属性

emits 属性

  • 生命周期
  • 多事件处理
多事件处理
  • Fragment
Fragment
  • 移除 .sync
移除.sync
  • 异步组件的写法

异步组件

  • 移除 filter

移除 filter

  • Teleport

Teleport

  • Suspense
Suspense
  • Composition API
    • reactive
    • ref 相关
    • readonly
    • watch 和 watchEffect
    • setup
    • 生命周期钩子函数

Composition API 如何实现代码逻辑复用?

  • 抽离逻辑代码到一个函数
  • 函数命名约定为 useXxxx 格式( React Hooks 也是)
  • 在 setup 中引用 useXxx 函数

代码示例用到 vue3-demo 项目的 MousePosition 组件

Vue3 如何实现响应式?

  • 回顾 Vue2.x 的 Object.defineProperty
  • 学习 Proxy 语法
  • Vue3 如何用 Proxy 实现响应式

Proxy 实现响应式

  • 回顾 Object.defineProperty
    • Object.defineProperty 的缺点
      • 深度监听需要一次性递归
      • 无法监听新增属性/删除属性 ( Vue.set Vue.delete )
      • 无法原生监听数组,需要特殊处理
    • 深度监听影响性能
    • 数组监听需要编写代码
    • 监听不到属性的新增和删除属性

代码示例用到 observe-demo 项目的 observe.js

  • Proxy 实现响应式

    • 基本使用

      Proxy实现响应式基本使用

      代码示例用到 observe-demo 项目的 proxy-demo.js

    • Reflect

      • 和 Proxy 能力一一对应
      • 规范化、标准化、函数式
      • 替代掉 Object 上的工具函数
    • 实现响应式

      • 深度监听,性能更好
      • 可监听新增/删除属性
      • 可监听数组变化

      代码示例用到 observe-demo 项目的 proxy-observe.js

  • 两者对比

总结

  • Proxy 能规避 Object.defineProperty 的问题
  • Proxy 无法兼容所有浏览器,无法 polyfill

v-model 和 .sync 修饰符 的对比

Vue2.3.0+新增的,用于双向绑定的,由于绑定代码的复杂而实现的语法糖

.sync 修饰符

v-model

v-model代码演示

代码示例用到 vue3-demo 项目的 VModel 组件

watch 和 watchEffect 的区别

  • 两者都可监听 data 属性变化
  • watch 需要明确监听哪个属性
  • watchEffect 会根据其中的属性,自动监听其变化

watch 和 watchEffect 的区别

代码示例用到 vue3-demo 项目的 Watch 组件

setup 中如何获取组件实例?

  • 在 setup 和其他 Composition API 中没有 this
  • 可通过 getCurrentlnstance 获取当前实例
  • 若使用 Options API 可照常使用 this

setup代码演示

代码示例用到 vue3-demo 项目的 GetInstance 组件

Vue3 为何比 Vue2 快?

  • Proxy 响应式
  • PatchFlag
  • hoistStatic
  • cacheHandler
  • SSR 优化
  • tree-shaking

详细讲解:

编译模板网站:https://vue-next-template-explorer.netlify.app/

  • Proxy 响应式

    • 上面提过,Proxy 能规避 Object.defineProperty 的问题
  • PatchFlag 静态标记

    • 编译模板时,动态节点做标记
    • 标记,分为不同的类型,如 TEXT PROPS
    • diff 算法时,可以区分静态节点,以及不同类型的动态节点

    PatchFlag

    PatchFlag对比
  • hoistStatic 静态提升

    • 将静态节点的定义,提升到父作用域,缓存起来
    • 多个相邻的静态节点,会被合并起来
    • 典型的拿空间换时间的优化策略

    hoistStatic

  • cacheHandler 缓存事件

    cacheHandler

  • SSR 优化

    • 静态节点直接输出,绕过了 vdom
    • 动态节点,还是需要动态渲染

    SSR 优化

  • tree-shaking 优化

    • 编译时,根据不同的情况,引入不同的 API

Vite 是什么?

  • 一个前端打包工具,Vue 作者发起的项目
  • 借助 Vue 的影响力,发展较快,和 webpack 竞争
  • 优势:开发环境下无需打包,启动快

Vite 为何启动快?

  • 开发环境使用 ES6Module,无需打包——非常快
  • 生产环境使用 rollup,并不会快很多

Vite 为何启动快

代码示例用到 es-module-demo 项目

Composition API 和 React Hooks 的对比

  • 前者 setup 只会被调用一次,而后者函数会被多次调用
  • 前者无需 useMemo useCallback,因为 setup 只调用一次
  • 前者无需顾虑调用顺序,而后者需要保证 hooks 的顺序一致
  • 前者 reactive + ref 比后者 useState,要难理解