金三银四的 Vue 面试准备
前言
为了金三银四的跳槽季做准备,并且我是 vue
技术栈的,所以整理了若干个 vue
的面试题。
每次看别人的博客,都会不自主的去看答案,为了方便检验自己的掌握程度,我特意将答案折叠起来,大家可以先看题目,在脑海中想象一下如果你被问到会怎么回答,然后再展开答案看看和自己的答案有什么不同。
答案非官方,仁者见仁智者见智,仅供参考。
基础使用
MVVM、MVC 有什么区别
MVC
通过分离 Model
、View
和 Controller
的方式来组织代码结构。
其中
View
负责页面的显示逻辑,Model
负责存储页面的业务数据,以及对相应数据的操作。Controller
层是View
层和Model
层的纽带,它主要负责用户与应用的响应操作,当用户与页面产生交互的时候,Controller
中的事件触发器就开始工作了,通过调用Model
层,来完成对Model
的修改,然后Model
层再去通知View
层更新。
MVVM
分为 Model
、View
、ViewModel
。
Model
代表数据模型,数据和业务逻辑都在Model
层中定义;View
代表UI
视图,负责数据的展示;ViewMode
负责监听Model
中数据的改变并且控制视图的更新,处理用户交互操作;
Model
和 View
并无直接关联,而是通过 ViewModel
来进行联系的,Model
和 ViewModel
之间有着双向数据绑定的联系。因此当 Model
中的数据改变时会触发 View
层的刷新,View
中由于用户交互操作而改变的数据也会在 Model
中同步。 这种模式实现了 Model
和 View
的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作 DOM
。
Vue
并没有完全遵循 MVVM
思想呢?
严格的
MVVM
要求View
不能和Model
直接通信,而Vue
提供了$refs
这个属性,让Model
可以直接操作View
,违反了这一规定,所以说Vue
没有完全遵循MVVM
。
Vue 的优点
渐进式框架:可以在任何项目中轻易的引入;
轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十
kb
;简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;
双向数据绑定:在数据操作方面更为简单;
组件化:很大程度上实现了逻辑的封装和重用,在构建单页面应用方面有着独特的优势;
视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;
对 SPA 单页面的理解,它的优缺点分别是什么?
SPA
仅在 Web
页面初始化时加载相应的 HTML
、JavaScript
和 CSS
。一旦页面加载完成,SPA
不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML
内容的变换,UI
与用户的交互,避免页面的重新加载。
优点:
用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
有利于前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
缺点:
初次加载耗时多:为实现单页
Web
应用功能及显示效果,需要在加载页面的时候将JavaScript
、CSS
统一加载,部分页面按需加载;不利于
SEO
:由于所有的内容都在一个页面中动态替换显示,所以在SEO
上其有着天然的弱势。
怎样理解 Vue 的单向数据流?
父级 prop
的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
每次父级组件发生更新时,子组件中所有的 prop
都将会刷新为最新的值。在子组件内部改变 prop
的时候 , Vue
会在浏览器的控制台中发出警告。
子组件想修改时,只能通过 $emit
派发一个自定义事件,父组件接收到后,由父组件修改。
有两种常见的试图改变一个 prop
的情形 :
这个
prop
用来传递一个初始值;这个子组件接下来希望将其作为一个本地的prop
数据来使用。 在这种情况下,最好定义一个本地的data
属性并将这个prop
用作其初始值:这个
prop
以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个prop
的值来定义一个计算属性
Data 为什么是一个函数?
因为组件是用来复用的,且 JS
里对象是引用关系,如果组件中 data
是一个对象,那么这样作用域没有隔离,子组件中的 data
属性值会相互影响,如果组件中 data
选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data
属性值不会互相影响;而 new Vue
的实例,是不会被复用的,因此不存在引用对象的问题。
Computed 和 Watch 有什么区别?
对于 Computed:
它支持缓存,只有依赖的数据发生了变化,才会重新计算
不支持异步,当
Computed
中有异步操作时,无法监听数据的变化如果一个属性是由其他属性计算而来的,这个属性依赖其他的属性,一般会使用 computed
如果
computed
属性的属性值是函数,那么默认使用get
方法,函数的返回值就是属性的属性值;在computed
中,属性有一个get
方法和一个set
方法,当数据发生变化时,会调用set
方法。
对于 Watch:
它不支持缓存,当一个属性发生变化时,它就会触发相应的操作
支持异步监听
监听的函数接收两个参数,第一个参数是最新的值,第二个是变化之前的值
监听数据必须是
data
中声明的或者父组件传递过来的props
中的数据,当发生变化时,会触发其他操作函数有两个的参数:immediate:组件加载立即触发回调函数 deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。
Computed 和 Methods 的区别
共同点:
可以将同一函数定义为一个 method
或者一个计算属性。对于最终的结果,两种方式是相同的。
不同点:
computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;
method: 调用总会执行该函数。
.Sync 的作用是什么?
vue
修饰符 sync
的功能是:当父组件提供了一个数据,而子组件想要去更改这个数据,但是 Vue
的规则不能让子组件去修改父组件的数据,就需要通过 this.$emit
和 $event
,来实现数据修改的目的。
绑定事件 @click=handler 和 @click=handler() 那个正确?有什么区别?
都可以,不带括号会传进来一个事件对象,带括号的不会
事件有哪些修饰符?
「事件修饰符」
.stop 阻止事件继续传播
.prevent 阻止标签默认行为
.capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
.self 只当在
event.target
是当前元素自身时触发处理函数.once 事件将只会触发一次
.passive 告诉浏览器你不想阻止事件的默认行为
「v-model 的修饰符」
.lazy 通过这个修饰符,转变为在
change
事件再同步.number 自动将用户的输入值转化为数值类型
.trim 自动过滤用户输入的首尾空格
「键盘事件的修饰符」
.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space
.up
.down
.left
.right
「系统修饰键」
.ctrl
.alt
.shift
.meta
「鼠标按钮修饰符」
.left
.right
.middle
什么是插槽?具名插槽?作用域插槽?原理是什么?
slot
又名插槽,是 Vue
的内容分发机制,插槽 slot
是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot
又分三类,默认插槽,具名插槽和作用域插槽。
默认插槽:又名匿名插槽,当
slot
没有指定name
属性值的时候,默认显示的插槽,一个组件内只允许有一个匿名插槽。具名插槽:带有具体名字的插槽,也就是带有
name
属性的slot
,一个组件可以出现多个具名插槽。作用域插槽:可以是匿名插槽,也可以是具名插槽,该插槽在渲染时,父组件可以使用子组件内部的数据。
实现原理:当子组件 vm
实例化时,获取到父组件传入的 slot
标签的内容,存放在 vm.$slot
中,默认插槽为 vm.$slot.default
,具名插槽为 vm.$slot.xxx
,xxx
为插槽名,当组件执行渲染函数时候,遇到 slot
标签,使用 $slot
中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
Vue 中如何实现过度效果?如何实现列表过度?
过渡效果,当然只有 dom
从显示到隐藏或隐藏到显示才能用
Vue.js
为我们提供了内置的过渡组件 transition
和 transition-group
Vue
将元素的过渡分为四个阶段,进入前,进入后,消失前,消失后
支持 mode
属性,可选值:
in-out
:要进入的先进入,然后要消失的再消失out-in
:要消失的先消失,然后要进入的再进入
多个元素需要加上过渡效果可以使用 name
属性进行区分。
可以配合 animate.css
实现更多的动画效果。
过滤器的作用,如何实现一个过滤器
过滤器是用来过滤数据的,在 Vue
选项中声明 filters
来实现一个过滤器,filters
不会修改数据,而是处理数据,改变用户看到的输出。
使用场景:
需要格式化数据的情况,比如需要处理时间、价格等数据格式的输出 / 显示。
比如后端返回一个 年月日的日期字符串,前端需要展示为 多少天前 的数据格式,此时就可以用
fliters
过滤器来处理数据。
过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在 插值表达式 {{ }}
和 v-bind
表达式 中,然后放在操作符 |
后面进行指示。
例如,在显示金额,给商品价格添加单位:
assets 和 static 的区别
相同点: assets
和 static
两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点
不相同点: assets
中存放的静态资源文件在项目打包时,也就是运行 npm run build
时会将 assets
中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在 static
文件中跟着 index.html
一同上传至服务器。static
中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是 static
中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets
中打包后的文件提交较大点。在服务器中就会占据更大的空间。
建议: 将项目中 template
需要的样式文件 js 文件等都可以放置在 assets
中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css
等文件可以放置在 static
中,因为这些引入的第三方文件已经经过处理,我们不再需要处理,直接上传。
对 SSR 的理解
SSR
大致的意思就是 vue
在客户端将标签渲染成的整个 html
片段的工作在服务端完成,服务端形成的 html
片段直接返回给客户端,这个过程就叫做服务端渲染。
(1)服务端渲染的优点:
更好的
SEO
:SSR
是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;首屏加载更快:
SPA
会等待所有Vue
编译后的js
文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR
直接由服务端渲染好页面直接返回显示,无需等待下载js
文件及再去渲染等,所以SSR
有更快的内容到达时间;
(2) 服务端渲染的缺点:
更多的开发条件限制:例如服务端渲染只支持
beforCreate
和created
两个钩子函数,不能进行
dom
操作。
这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行。
Vue 的性能优化有哪些
(1)代码层面的优化
v-if
和v-show
区分使用场景computed
和watch
区分使用场景v-for
遍历必须为item
添加key
,且避免同时使用v-if
长列表性能优化
事件的销毁
图片资源懒加载
路由懒加载
第三方插件的按需引入
优化无限列表性能
服务端渲染
(2)Webpack 层面的优化
Webpack
对图片进行压缩减少
ES6
转为ES5
的冗余代码提取公共代码
模板预编译
提取组件的
CSS
优化
SourceMap
构建结果输出分析
Vue
项目的编译优化
(3)基础的 Web 技术的优化
开启
gzip
压缩浏览器缓存
CDN
的使用使用
Chrome Performance
查找性能瓶颈
Vue 的首屏加载性能优化有哪些
优化前的大小
1.图片优化
之前为了方便开法, 背景图片直接在 assets
里面扔了一个 jpg
, 导致加载这张图片的时候就用了十几秒, 于是乎我就把图片上传空间了, 然后改用网络地址。
2.禁止生成.map 文件
build
出来的 dist
文件夹里面有很多的 .map
文件,这些文件主要是帮助线上调试代码,禁止生成这些文件.
在 vue.config.js
里面加上这句。
3.路由懒加载
4.cdn 引入公共库
网上说可以把 import
注释掉,亲自操作会报错,也有资料说不用注释也不会打包。
一顿操作最后的文件,效果显著,app.js 还是很大
5.终极法宝 GZIP 压缩
做完这个感觉前四步都是小菜一碟,直接把 1.4m
的 app.js
干成一百多 kb
,其他的都不足挂齿了。
服务端也要配,不然不认识 GZIP
文件。
最垃圾的服务器通过以上几个优化,一样飞起来了!!!
vue 初始化页面闪动问题
使用 vue
开发时,在 vue
初始化之前,由于 div
是不归 vue
管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于 {{message}}
的字样,虽然一般情况下这个时间很短暂,但是我们还是有必要让解决这个问题的。
首先:在 css
里加上 [v-cloak] { display: none; }
。如果没有彻底解决问题,则在根元素加上 style="display: none;" :style="{display: block }"
Class 与 Style 如何动态绑定?
Class
可以通过对象语法和数组语法进行动态绑定:
对象语法:
数组语法:
Style
也可以通过对象语法和数组语法进行动态绑定:
对象语法:
数组语法:
如何让 CSS 只在当前组件中起作用?
在组件中的 style
标签中加上 scoped
如何获取 dom
ref="domName"
用法:this.$refs.domName
vue-loader 是什么?使用它的用途有哪些?
vue
文件的一个加载器,把 template/js/style
转换成 js
模块。
生命周期
Vue 有哪些生命周期钩子?
Vue
的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)。
beforeCreate
:是new Vue()
之后触发的第一个钩子,在当前阶段data
、methods
、computed
以及watch
上的数据和方法都不能被访问。created
:在实例创建完成后发生,当前阶段已经完成了数据观测,也就是可以使用数据,更改数据,在这里更改数据不会触发updated
函数。可以做一些初始数据的获取,在当前阶段无法与Dom
进行交互,如果非要想,可以通过vm.$nextTick
来访问Dom
。beforeMount
:发生在挂载之前,在这之前template
模板已导入渲染函数编译。而当前阶段虚拟Dom
已经创建完成,即将开始渲染。在此时也可以对数据进行更改,不会触发updated
。mounted
:在挂载完成后发生,在当前阶段,真实的Dom
挂载完毕,数据完成双向绑定,可以访问到Dom
节点,使用$refs
属性对Dom
进行操作。beforeUpdate
:发生在更新之前,也就是响应式数据发生更新,虚拟dom
重新渲染之前被触发,你可以在当前阶段进行更改数据,不会造成重渲染。updated
:发生在更新完成之后,当前阶段组件Dom
已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新。beforeDestroy
:发生在实例销毁之前,在当前阶段实例完全可以被使用,我们可以在这时进行善后收尾工作,比如清除计时器。destroyed
:发生在实例销毁之后,这个时候只剩下了dom
空壳。组件已被拆解,数据绑定被卸除,监听被移出,子实例也统统被销毁。
如果需要发送异步请求,最好放在哪个钩子内?
可以在钩子函数 created
、beforeMount
、mounted
中进行调用,因为在这三个钩子函数中,data
已经创建,可以将服务端端返回的数据进行赋值。
推荐在 created
钩子函数中调用异步请求,有以下优点:
能更快获取到服务端数据,减少页面
loading
时间;ssr
不支持beforeMount
、mounted
钩子函数,所以放在created
中有助于一致性;
第一次页面加载会触发哪几个钩子?
beforeCreate
,created
,beforeMount
,mounted
哪个钩子可以进行 dom 操作?
在钩子函数 mounted
被调用前,Vue
已经将编译好的模板挂载到页面上,所以在 mounted
中可以访问操作 DOM
。
父子组件嵌套时,父组件和子组件生命周期钩子执行顺序是什么?
Vue
的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:
加载渲染过程
父beforeCreate
->父created
->父beforeMount
->子beforeCreate
->子created
->子beforeMount
->子mounted
->父mounted
子组件更新过程
父beforeUpdate
->子beforeUpdate
->子updated
->父updated
父组件更新过程
父beforeUpdate
->父updated
销毁过程
父beforeDestroy
->子beforeDestroy
->子destroyed
->父destroyed
父子组件嵌套时,父组件视图和子组件视图谁先完成渲染?
加载渲染过程
父beforeCreate
->父created
->父beforeMount
->子beforeCreate
->子created
->子beforeMount
->子mounted
->父mounted
可知子组件先完成渲染
keep-alive 中的生命周期哪些
对应两个钩子函数
activated
和deactivated
,当组件被激活时,触发钩子函数activated
,当组件被移除时,触发钩子函数deactivated
。
指令相关
说说 vue 内置指令
什么是自定义指令?有哪些生命周期?
是 vue
对 HTML
元素的扩展,给 HTML
元素增加自定义功能。vue
编译 DOM
时,会找到指令对象,执行指令的相关方法。
自定义指令有五个生命周期
bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
update:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。
componentUpdated:被绑定元素所在模板完成一次更新周期时调用。
unbind:只调用一次,指令与元素解绑时调用。
v-text 和 v-html 有什么区别?
v-text
和{{}}
表达式渲染数据,不解析标签。v-html
不仅可以渲染数据,而且可以解析标签。
v-if 和 v-for 的优先级
当 v-if
与 v-for
一起使用时,v-for
具有比 v-if
更高的优先级,这意味着 v-if
将分别重复运行于每个 v-for
循环中。所以,不推荐 v-if
和 v-for
同时使用。如果 v-if
和 v-for
一起用的话,vue
中的的会自动提示 v-if
应该放到外层去。
V-if 和 v-show 有什么区别?
手段:
v-if
是动态的向DOM
树内添加或者删除DOM
元素;v-show
是通过设置DOM
元素的display
样式属性控制显隐;编译过程:
v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show
只是简单的基于css
切换;编译条件:
v-if
是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译;v-show
是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且DOM
元素保留;性能消耗:
v-if
有更高的切换消耗;v-show
有更高的初始渲染消耗;使用场景:
v-if
适合运营条件不大可能改变;v-show
适合频繁切换。
组件的 v-model 是如何实现的?
我们在 vue
项目中主要使用 v-model
指令在表单 input
、textarea
、select
等元素上创建双向数据绑定,我们知道 v-model
本质上不过是语法糖,v-model
在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text
和textarea
元素使用value
属性和input
事件;checkbox
和radio
使用checked
属性和change
事件;select
字段将value
作为prop
并将change
作为事件。
以 input
表单元素为例:
v-model 可以被用在自定义组件上吗?如果可以,如何使用?
如果在自定义组件中,v-model
默认会利用名为 value
的 prop
和名为 input
的事件,如下所示:
v-on 可以监听多个方法吗?
可以
组件相关
组件通信的 N 种方式
(1)props / $emit
适用 父子组件通信
(2)ref
适用 父子组件通信
ref
:如果在普通的DOM
元素上使用,引用指向的就是DOM
元素;如果用在子组件上,引用就指向组件实例
(3)$parent
/ $children
/ $root
:访问父 / 子实例 / 根实例
(4)EventBus ($emit / $on)
适用于 父子、隔代、兄弟组件通信
这种方法通过一个空的 Vue
实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
(5)$attrs
/$listeners
适用于 隔代组件通信
$attrs
:包含了父作用域中不被prop
所识别 (且获取) 的特性绑定 (class
和style
除外 )。当一个组件没有声明任何prop
时,这里会包含所有父作用域的绑定 (class
和style
除外 ),并且可以通过v-bind="$attrs"
传入内部组件。通常配合inheritAttrs
选项一起使用。$listeners
:包含了父作用域中的 (不含.native
修饰器的)v-on
事件监听器。它可以通过v-on="$listeners"
传入内部组件
(6)provide / inject
适用于 隔代组件通信
祖先组件中通过 provide
来提供变量,然后在子孙组件中通过 inject
来注入变量。provide / inject API
主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
(7)Vuex
适用于 父子、隔代、兄弟组件通信
Vuex
是一个专为 Vue.js
应用程序开发的状态管理模式。每一个 Vuex
应用的核心就是 store
(仓库)。store
基本上就是一个容器,它包含着你的应用中大部分的状态 ( state
)。
Vuex
的状态存储是响应式的。当Vue
组件从store
中读取状态的时候,若store
中的状态发生变化,那么相应的组件也会相应地得到高效更新。改变
store
中的状态的唯一途径就是显式地提交(commit) mutation
。这样使得我们可以方便地跟踪每一个状态的变化。
(8)插槽
Vue3
可以通过 usesolt
获取插槽数据。
(9)mitt.js
适用于任意组件通信
Vue3
中移除了 $on
,$off
等方法,所以 EventBus
不再使用,相应的替换方案就是 mitt.js
Vue3 和 vue2 全局组件和局部组件注册的方式?
Vue2:
Vue.component()
Vue3:
app.component()
什么是动态组件?动态组件的钩子如何执行?
让多个组件使用同一个挂载点,并动态切换,这就是动态组件
简单的说,动态组件就是将几个组件放在一个挂载点下,这个挂载点就是标签,其需要绑定 is
属性,属性值为父组件中的变量,变量对应的值为要挂载的组件的组件名,然后根据父组件里某个变量来动态显示哪个,也可以都不显示。
缓存 <keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
可以将动态组件放到组件内对动态组件进行缓存,这样动态组件进行切换的时候,就不会每次都重新创建了。
Keep-alive 的作用?使用 keep-alive 的组件如何监控组件切换?
keep-alive
是 Vue
内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
一般结合路由和动态组件一起使用,用于缓存组件;
提供
include
和exclude
属性,两者都支持字符串或正则表达式,include
表示只有名称匹配的组件会被缓存,exclude
表示任何名称匹配的组件都不会被缓存 ,其中exclude
的优先级比include
高;对应两个钩子函数
activated
和deactivated
,当组件被激活时,触发钩子函数activated
,当组件被移除时,触发钩子函数deactivated
。
父组件如何监听子组件的生命周期?
比如有父组件 Parent
和子组件 Child
,如果父组件监听到子组件挂载 mounted
就做一些逻辑处理,可以通过以下写法实现:
以上需要手动通过 $emit
触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook
来监听即可,如下所示:
当然 @hook
方法不仅仅是可以监听 mounted
,其它的生命周期事件,例如:created
,updated
等都可以监听。
原理相关
Vue 初始化时都做了什么?
第一部分
⭐ 每个 vue
实例都有一个 _uid
,并且是依次递增的,确保唯一性。
⭐ vue
实例不应该是一个响应式的,做个标记。
第二部分
⭐ 如果是子组件,将组件配置对象上的一些深层次属性放到 vm.$options
选项中,以提高代码的执行效率。
⭐ 如果是根组件,对 options
进行合并,vue
会将相关的属性和方法都统一放到 vm.$options
中。vm.$options
的属性来自两个方面,一个是 Vue
的构造函数 vm.constructor
预先定义的,一个是 new Vue
时传入的入参对象。
第三部分
⭐ initProxy / vm._renderProxy 在非生产环境下执行了 initProxy
函数,参数是实例;在生产环境下设置了实例的 _renderProxy
属性为实例自身。
⭐ 设置了实例的 _self
属性为实例自身。
⭐ initLifecycle 初始化组件实例关系属性 , 比如 $parent
、$children
、$root
、$refs
等 (不是组件生命周期 mounted
, created
...)
⭐ initEvents 初始化自定义事件。
⭐ initRender 初始化插槽 , 获取 this.slots
, 定义 this._c
, 也就是 createElement
方法 , 平时使用的 h
函数。
⭐ callHook 执行 beforeCreate
生命周期函数。
⭐ initInjections 初始化 inject
选项
⭐ initState 响应式原理的核心 , 处理 props
、methods
、computed
、data
、watch
等。
⭐ initProvide 解析组件配置项上的 provide
对象,将其挂载到 vm._provided
属性上。
⭐ callHook 执行 created
生命周期函数。
第四部分
⭐ 如果有 el
属性,则调用 vm.$mount
方法挂载 vm
,挂载的目标就是把模板渲染成最终的 DOM
。
⭐ 不存在 el
的时候不挂载 , 需要手动挂载。
数据响应式的原理
Vue.js
是采用 数据劫持 结合 发布者-订阅者模式 的方式,通过 Object.defineProperty()
来劫持各个属性的 setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
使用
observe
对需要响应式的数据进行递归,将对像的所有属性及其子属性,都加上setter
和getter
这样的话,给这个对象的某个属性赋值的时候,就会触发setter
,那么就能监听到了数据变化。compile
解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。Watcher
订阅者是Observer
和Compile
之间通信的桥梁,主要做的事情是:
在自身实例化时往属性订阅器(
dep
)里面添加自己自身必须有一个
update()
方法待属性变动触发
dep.notice()
通知时,能调用自身的update()
方法,并触发Compile
中绑定的回调,完成视图更新。
总结:通过 Observer
来监听自己的 model
数据变化,通过 Compile
来解析编译模板指令,最终利用 Watcher
搭起 Observer
和 Compile
之间的通信桥梁,达到一个数据响应式的效果。
使用 Object.defineProperty() 来进行数据劫持有什么缺点?
无法劫持以下操作
给对象新增属性
给对象删除属性
大部分的操作数组
Vue 框架怎么实现对象和数组的监听?
Vue
框架是通过 遍历数组 和 递归遍历对象,从而达到利用 Object.defineProperty()
对对象和数组的部分方法的操作进行监听。
Vue 中给 data 中的对象属性添加一个新的属性或删除一个属性时会发生什么?如何解决?
什么都不会发生,因为 Object.defineProperty()
监听不到这类变化。
可以使用 vm.$set
和 Vue.set
方法去添加一个属性。
可以使用 vm.$delete
和 Vue.delete
方法去删除一个属性。
如何解决索引赋值或者修改数组长度无法改变视图?
由于 Vue
只改写了 7 种修改数组的方法,所以 Vue
不能检测到以下数组的变动:
当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:
vm.items.length = newLength
为了解决第一个问题,Vue
提供了以下操作方法:
为了解决第二个问题,Vue
提供了以下操作方法:
数组的响应式是怎么实现的?
择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写
所以在 Vue
中修改数组的索引和长度是无法监控到的。需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher
进行更新
$nextTick 的原理是什么?nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法。
简单的理解是:当数据更新了,在 dom 中渲染后, 自动执行该函数。Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,Vue 是异步执行 DOM 更新的。created 钩子函数进行的 DOM 操作一定要放在 Vue.nextTick() 的回调函数中,原因是在函数执行的时候 DOM 其实并未进行任何渲染。常用的场景是在进行获取数据后,需要对新视图进行下一步操作或者其他操作时,发现获取不到 dom。因为赋值操作只完成了数据模型的改变并没有完成视图更新。
列表循环时 key 有什么作用?key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速。Vue 的 diff 过程可以概括为:oldCh 和 newCh 各有两个头尾的变量 oldStartIndex、oldEndIndex 和 newStartIndex、newEndIndex,它们会新节点和旧节点会进行两两对比,即一共有 4 种比较方式:newStartIndex 和 oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,如果以上 4 种比较都没匹配,如果设置了 key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。
所以 Vue 中 key 的作用是:key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速,因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下:
为什么不建议用 index 作为 key?
使用 index
作为 key
和没写基本上没区别,因为不管数组的顺序怎么颠倒,index
都是 0, 1, 2...
这样排列,导致 Vue
会复用错误的旧子节点,做很多额外的工作。
v-if、v-show、v-html 的原理
v-if 会调用
addIfCondition
方法,生成vnode
的时候会忽略对应节点,render
的时候就不会渲染;v-show 会生成
vnode
,render
的时候也会渲染成真实节点,只是在render
过程中会在节点的属性中修改show
属性值,也就是常说的display
;v-html 会先移除节点下的所有节点,设置
innerHTML
为v-html
的值。
Vue 中封装的数组方法有哪些,其如何实现页面更新
数组就是使用 object.defineProperty
重新定义数组的每一项,那能引起数组变化的方法我们都是知道的,
pop
、push
、shift
、unshift
、splice
、sort
、reverse
这七种,只要这些方法执行改了数组内容,我就更新内容就好了,是不是很好理解。
是用来函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新。
数组里每一项可能是对象,那么我就是会对数组的每一项进行观测,(且只有数组里的对象才能进行观测,观测过的也不会进行观测)
vue3:改用 proxy
,可直接监听对象数组的变化。
Vue 模板渲染的原理是什么?
vue
中的模板 template
无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML
语法,所有需要将 template
转化成一个 JavaScript
函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML
元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。
模板编译又分三个阶段,解析 parse
,优化 optimize
,生成 generate
,最终生成可执行函数 render
。
parse 阶段:使用大量的正则表达式对
template
字符串进行解析,将标签、指令、属性等转化为抽象语法树AST
。optimize 阶段:遍历
AST
,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff
比较时,直接跳过这一些静态节点,优化runtime
的性能。generate 阶段:将最终的
AST
转化为render
函数字符串。
说一下什么是 Virtual DOM
Virtual DOM
是 DOM
节点在 JavaScript
中的一种抽象数据结构,之所以需要虚拟 DOM
,是因为浏览器中操作 DOM
的代价比较昂贵,频繁操作 DOM
会产生性能问题。虚拟 DOM
的作用是在每一次响应式数据发生变化引起页面重渲染时,Vue
对比更新前后的虚拟 DOM
,匹配找出尽可能少的需要更新的真实 DOM
,从而达到提升性能的目的。
Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?
Vue
是异步执行DOM
更新。只要观察到数据变化,
Vue
将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个
watcher
被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM
操作上非常重要。然后,在下一个的事件循环
tick
中,Vue
刷新队列并执行实际 (已去重的) 工作。Vue
在内部尝试对异步队列使用原生的Promise.then
和MessageChannel
,如果执行环境不支持,会采用setTimeout(fn, 0)
代替。
例如,当你设置 vm.someData = 'new value'
,该组件不会立即重新渲染。
当刷新队列时,组件会在事件循环队列清空时的下一个
tick
更新。多数情况我们不需要关心这个过程,但是如果你想在
DOM
状态更新后做点什么,这就可能会有些棘手。虽然
Vue.js
通常鼓励开发人员沿着 “数据驱动” 的方式思考,避免直接接触DOM
,但是有时我们确实要这么做。为了在数据变化之后等待Vue
完成更新DOM
,可以在数据变化之后立即使用Vue.nextTick(callback)
。这样回调函数在DOM
更新完成后就会调用。
Vue.mixin 的使用场景和原理
在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过 Vue
的 mixin
功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions
方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
Vue.extend 作用和原理
其实就是一个子类构造器 ,是 Vue
组件的核心 api
实现思路就是使用原型继承的方法返回了 Vue
的子类 并且利用 mergeOptions
把传入组件的 options
和父类的 options
进行了合并。
Vue 事件绑定原理
原生事件绑定是通过 addEventListener
绑定给真实元素的,组件事件绑定是通过 Vue
自定义的 $on
实现的。如果要在组件上使用原生事件,需要加 .native
修饰符,这样就相当于在父组件中把子组件当做普通 html
标签,然后加上原生事件。
on、emit
是基于发布订阅模式的,维护一个事件中心,on
的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit
将对应的事件进行发布,去执行事件中心里的对应的监听器。
虚拟 DOM 实现原理?
虚拟 DOM
的实现原理主要包括以下 3 部分:
用
JavaScript
对象模拟真实DOM
树,对真实DOM
进行抽象;diff
算法 — 比较两棵虚拟DOM
树的差异;pach
算法 — 将两个虚拟DOM
对象的差异应用到真正的DOM
树。
虚拟 dom 和真实 dom 的区别
虚拟
DOM
不会进行排版与重绘操作虚拟
DOM
就是把真实DOM
转换为Javascript
代码虚拟
DOM
进行频繁修改,然后一次性比较并修改真实DOM
中需要改的部分,最后并在真实DOM
中进行排版与重绘,减少过多DOM
节点排版与重绘损耗
生态相关
vue-router 路由模式有几种?
vue-router
有 3 种路由模式:hash
、history
、abstract
:
hash: 使用
URL hash
值来作路由。支持所有浏览器,包括不支持HTML5 History Api
的浏览器;history : 依赖
HTML5 History API
和服务器配置。abstract : 支持所有
JavaScript
运行环境,如Node.js
服务器端。如果发现没有浏览器的API
,路由会自动强制进入这个模式.
路由的 hash 和 history 模式的区别
(1)hash 模式的实现原理
早期的前端路由的实现就是基于 location.hash
来实现的。其实现原理很简单,location.hash
的值就是 URL
中 #
后面的内容。比如下面这个网站,它的 location.hash
的值为 #search
:
hash
路由模式的实现主要是基于下面几个特性:
URL
中hash
值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash
部分不会被发送;hash
值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash
的切换;可以通过
a
标签,并设置href
属性,当用户点击这个标签后,URL
的hash
值会发生改变;或者使用JavaScript
来对loaction.hash
进行赋值,改变URL
的hash
值;我们可以使用
hashchange
事件来监听hash
值的变化,从而对页面进行跳转(渲染)。
(2)history 模式的实现原理
HTML5
提供了 History API
来实现 URL
的变化。其中做最主要的 API
有以下两个:history.pushState()
和 history.repalceState()
。这两个 API
可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:
history
路由模式的实现主要基于存在下面几个特性:
pushState
和repalceState
两个API
来操作实现URL
的变化 ;我们可以使用
popstate
事件来监听url
的变化,从而对页面进行跳转(渲染);history.pushState()
或history.replaceState()
不会触发popstate
事件,这时我们需要手动触发页面跳转(渲染)。
如何获取页面的 hash 变化
监听 $route
对象
route 和 router 的区别
$router
是 VueRouter
的实例,在 script
标签中想要导航到不同的 URL
,使用 $router.push
方法。返回上一个历史 history
用 $router.to(-1)
$route
为当前 router
跳转对象。里面可以获取当前路由的 name
,path
,query
,parmas
等。
如何定义动态路由?如何获取传过来的动态参数?
可以通过query
,param
两种方式
区别:query
通过url
传参,刷新页面还在;params
属性页面不在
params
的类型:
配置路由格式:
/router/:id
传递的方式:在
path
后面跟上对应的值传递后形成的路径:
/router/123
通过
$route.params.id
获取传递的值
query
的类类型
配置路由格式
:/router
也就是普通配置传递的方式:对象中使用
query
的key
作为传递方式传递后形成的路径
:/route?id=123
通过
$route.query
获取传递的值
Vue-router 导航守卫有哪些
Vue-router 跳转和 location.href 有什么区别
使用 location.href= /url
来跳转,简单方便,但是刷新了页面;
使用 history.pushState( /url )
,无刷新页面,静态跳转;
引进 router
,然后使用 router.push( /url )
来跳转,使用了 diff
算法,实现了按需加载,减少了 dom
的消耗。
其实使用 router
跳转和使用 history.pushState()
没什么差别的,因为 vue-router
就是用了 history.pushState()
,尤其是在 history
模式下。
params 和 query 的区别
用法:query
要用 path
来引入,params
要用 name
来引入,接收参数都是类似的,分别是 this.$route.query.name
和 this.$route.params.name
。
url 地址显示:query
更加类似于我们 ajax
中 get
传参,params
则类似于 post
,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示。
注意点:query
刷新不会丢失 query
里面的数据, params
刷新会丢失 params
里面的数据。
Vuex 的原理
Vue
组件会触发(dispatch)
一些事件或动作,也就是Actions
;在组件中发出的动作,肯定是想获取或者改变数据的,但是在
vuex
中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)
到Mutations
中;然后
Mutations
就去改变State
中的数据;当
State
中的数据被改变之后,就会重新渲染(Render)
到Vue Components
中去,组件展示更新后的数据,完成一个流程。
Vuex 有哪几种属性
有五种,分别
State:定义了应用状态的数据结构,可以在这里设置默认的初始状态。
Getter:允许组件从
Store
中获取数据,mapGetters
辅助函数仅仅是将store
中的getter
映射到局部计算属性。Mutation:是唯一更改
store
中状态的方法,且必须是同步函数。Action:用于提交
mutation
,而不是直接变更状态,可以包含任意异步操作。Module:允许将单一的
Store
拆分为多个store
且同时保存在单一的状态树中。
Vuex 和单纯的全局对象有什么区别?
Vuex
的状态存储是响应式的。当Vue
组件从store
中读取状态的时候,若store
中的状态发生变化,那么相应的组件也会相应地得到高效更新。不能直接改变
store
中的状态。改变store
中的状态的唯一途径就是显式地提交mutation
。
Vuex 中 action 和 mutation 的区别
Mutation
专注于修改State
,理论上是修改State
的唯一途径;Action
业务代码、异步请求。Mutation
:必须同步执行;Action
:可以异步,但不能直接操作State
。在视图更新时,先触发
actions
,actions
再触发mutation
mutation
的参数是state
,它包含store
中的数据;action
的参数是context
,它是state
的父级,包含state
、getters
等。
为什么 Vuex 的 mutation 中不能做异步操作?
Vuex
中所有的状态更新的唯一途径都是mutation
,异步操作通过Action
来提交mutation
实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。每个
mutation
执行完成后都会对应到一个新的状态变更,这样devtools
就可以打个快照存下来。如果mutation
支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。
Vuex 和 localStorage 的区别
(1)最重要的区别
vuex
存储在内存中localstorage
则以文件的方式存储在本地,只能存储字符串类型的数据,存储对象需要JSON
的stringify
和parse
方法进行处理。 读取内存比读取硬盘速度要快
(2)应用场景
Vuex
是一个专为Vue.js
应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。vuex
用于组件之间的传值。localstorage
是本地存储,是将数据存储到浏览器的方法,一般是在跨页面传递数据时使用 。Vuex
能做到数据的响应式,localstorage
不能
(3)永久性
刷新页面时 vuex
存储的值会丢失,localstorage
不会,对于不变的数据可以用 localstorage
可以代替 vuex
。
Vuex 的严格模式是什么,有什么作用,如何开启?
在严格模式下,无论何时发生了状态变更且不是由 mutation
函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
在 Vuex.Store
构造器选项中开启,如下
如何在组件中批量使用 Vuex 的 getter 属性
使用 mapGetters
辅助函数, 利用对象展开运算符将 getter
混入 computed
对象中
如何在组件中重复使用 Vuex 的 mutation
使用 mapMutations
辅助函数,在组件中这么使用
Vuex 页面刷新数据丢失怎么解决
在
created
周期中读取sessionstorage
中的数据存储在store
中,此时用vuex.store
的replaceState
方法,替换store
的根状态在
beforeunload
方法中将store.state
存储到sessionstorage
中。
3.0 相关
Vue3.0 有什么更新
Vue3.0 defineProperty 和 proxy 的区别
Vue3.x
改用 Proxy
替代 Object.defineProperty
。因为 Proxy
可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。
Proxy 与 Object.defineProperty 优劣对比
Proxy 的优势如下:
Proxy
可以直接监听对象而非属性;Proxy
可以直接监听数组的变化;Proxy
返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty
只能遍历对象属性直接修改;Proxy
作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
Object.defineProperty 的优势如下:
兼容性好,支持
IE9
。
Vue 3.0 生命周期有哪些变化?
注意:3.0
中的生命周期钩子要比2.X
中相同生命周期的钩子要快
Composition API
还新增了以下调试钩子函数:但是不怎么常用
onRenderTracked
onRenderTriggered
Vue 3.0 自定义指令有哪些变化?
先看看Vue2
自定义指令的钩子
bind:当指令绑定在对应元素时触发。只会触发一次。
inserted:当对应元素被插入到
DOM
的父元素时触发。update:当元素更新时,这个钩子会被触发(此时元素的后代元素还没有触发更新)。
componentUpdated:当整个组件(包括子组件)完成更新后,这个钩子触发。
unbind:当指令被从元素上移除时,这个钩子会被触发。也只触发一次。
在 Vue3
中,官方为了更有助于代码的可读性和风格统一,把自定义指令的钩子名称改的更像是组件生命周期,尽管他们是两回事
bind => beforeMount
inserted => mounted
beforeUpdate: 新的钩子,会在元素自身更新前触发
update => 移除!
componentUpdated => updated
beforeUnmount: 新的钩子,当元素自身被卸载前触发
unbind => unmounted
后语
最后祝大家在新的一年里,都能找到满意的工作,升职加薪,赚的盆满钵满!
最后
如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163 相互学习,我们会有专业的技术答疑解惑
如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点 star:http://github.crmeb.net/u/defu不胜感激 !
PHP 学习手册:https://doc.crmeb.com
技术交流论坛:https://q.crmeb.com
评论