为了金三银四的跳槽季做准备,并且我是 vue
技术栈的,所以整理了若干个 vue
的面试题。
每次看别人的博客,都会不自主的去看答案,为了方便检验自己的掌握程度,我特意将答案折叠起来,大家可以先看题目,在脑海中想象一下如果你被问到会怎么回答,然后再展开答案看看和自己的答案有什么不同。
答案非官方,仁者见仁智者见智,仅供参考。
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
。kb
;SPA
仅在 Web
页面初始化时加载相应的 HTML
、JavaScript
和 CSS
。一旦页面加载完成,SPA
不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML
内容的变换,UI
与用户的交互,避免页面的重新加载。
优点:
缺点:
Web
应用功能及显示效果,需要在加载页面的时候将 JavaScript
、CSS
统一加载,部分页面按需加载;SEO
:由于所有的内容都在一个页面中动态替换显示,所以在 SEO
上其有着天然的弱势。父级 prop
的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
每次父级组件发生更新时,子组件中所有的 prop
都将会刷新为最新的值。在子组件内部改变 prop
的时候 , Vue
会在浏览器的控制台中发出警告。
子组件想修改时,只能通过 $emit
派发一个自定义事件,父组件接收到后,由父组件修改。
有两种常见的试图改变一个 prop
的情形 :
prop
用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop
数据来使用。 在这种情况下,最好定义一个本地的 data
属性并将这个 prop
用作其初始值:prop
以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop
的值来定义一个计算属性因为组件是用来复用的,且 JS
里对象是引用关系,如果组件中 data
是一个对象,那么这样作用域没有隔离,子组件中的 data
属性值会相互影响,如果组件中 data
选项是一个函数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的 data
属性值不会互相影响;而 new Vue
的实例,是不会被复用的,因此不存在引用对象的问题。
对于Computed:
Computed
中有异步操作时,无法监听数据的变化computed
属性的属性值是函数,那么默认使用 get
方法,函数的返回值就是属性的属性值;在 computed
中,属性有一个 get
方法和一个 set
方法,当数据发生变化时,会调用 set
方法。对于Watch:
data
中声明的或者父组件传递过来的 props
中的数据,当发生变化时,会触发其他操作共同点:
可以将同一函数定义为一个 method
或者一个计算属性。对于最终的结果,两种方式是相同的。
不同点:
vue
修饰符 sync
的功能是:当父组件提供了一个数据,而子组件想要去更改这个数据,但是 Vue
的规则不能让子组件去修改父组件的数据,就需要通过 this.$emit
和 $event
,来实现数据修改的目的。
:money.sync="total"
// 等价于
:money="total" v-on:update:money="total = $event"
复制代码
都可以,不带括号会传进来一个事件对象,带括号的不会
「事件修饰符」
event.target
是当前元素自身时触发处理函数「v-model 的修饰符」
change
事件再同步「键盘事件的修饰符」
「系统修饰键」
「鼠标按钮修饰符」
slot
又名插槽,是 Vue
的内容分发机制,插槽 slot
是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot
又分三类,默认插槽,具名插槽和作用域插槽。
slot
没有指定 name
属性值的时候,默认显示的插槽,一个组件内只允许有一个匿名插槽。name
属性的 slot
,一个组件可以出现多个具名插槽。实现原理:当子组件 vm
实例化时,获取到父组件传入的 slot
标签的内容,存放在 vm.$slot
中,默认插槽为 vm.$slot.default
,具名插槽为 vm.$slot.xxx
,xxx
为插槽名,当组件执行渲染函数时候,遇到 slot
标签,使用 $slot
中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
过渡效果,当然只有 dom
从显示到隐藏或隐藏到显示才能用
Vue.js
为我们提供了内置的过渡组件 transition
和 transition-group
Vue
将元素的过渡分为四个阶段,进入前,进入后,消失前,消失后
支持 mode
属性,可选值:
in-out
:要进入的先进入,然后要消失的再消失out-in
:要消失的先消失,然后要进入的再进入多个元素需要加上过渡效果可以使用 name
属性进行区分。
可以配合 animate.css
实现更多的动画效果。
过滤器是用来过滤数据的,在 Vue
选项中声明 filters
来实现一个过滤器,filters
不会修改数据,而是处理数据,改变用户看到的输出。
使用场景:
fliters
过滤器来处理数据。过滤器是一个函数,它会把表达式中的值始终当作函数的第一个参数。过滤器用在 插值表达式 {{ }}
和 v-bind
表达式 中,然后放在操作符 |
后面进行指示。
例如,在显示金额,给商品价格添加单位:
<li>商品价格:{{item.price | filterPrice}}</li>
filters: {
filterPrice (price) {
return price ? ('¥' + price) : '--'
}
}
复制代码
相同点: assets
和 static
两个都是存放静态资源文件。项目中所需要的资源文件图片,字体图标,样式文件等都可以放在这两个文件下,这是相同点
不相同点: assets
中存放的静态资源文件在项目打包时,也就是运行 npm run build
时会将 assets
中放置的静态资源文件进行打包上传,所谓打包简单点可以理解为压缩体积,代码格式化。而压缩后的静态资源文件最终也都会放置在 static
文件中跟着 index.html
一同上传至服务器。static
中放置的静态资源文件就不会要走打包压缩格式化等流程,而是直接进入打包好的目录,直接上传至服务器。因为避免了压缩直接进行上传,在打包时会提高一定的效率,但是 static
中的资源文件由于没有进行压缩等操作,所以文件的体积也就相对于 assets
中打包后的文件提交较大点。在服务器中就会占据更大的空间。
建议: 将项目中 template
需要的样式文件js文件等都可以放置在 assets
中,走打包这一流程。减少体积。而项目中引入的第三方的资源文件如iconfoont.css
等文件可以放置在 static
中,因为这些引入的第三方文件已经经过处理,我们不再需要处理,直接上传。
SSR
大致的意思就是 vue
在客户端将标签渲染成的整个 html
片段的工作在服务端完成,服务端形成的 html
片段直接返回给客户端,这个过程就叫做服务端渲染。
(1)服务端渲染的优点:
SEO
:SSR
是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;SPA
会等待所有 Vue
编译后的 js
文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR
直接由服务端渲染好页面直接返回显示,无需等待下载 js
文件及再去渲染等,所以 SSR
有更快的内容到达时间;(2) 服务端渲染的缺点:
beforCreate
和 created
两个钩子函数,dom
操作。这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行。
(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
查找性能瓶颈优化前的大小
1.图片优化
之前为了方便开法, 背景图片直接在 assets
里面扔了一个 jpg
, 导致加载这张图片的时候就用了十几秒, 于是乎我就把图片上传空间了, 然后改用网络地址。
2.禁止生成.map文件
build
出来的 dist
文件夹里面有很多的 .map
文件,这些文件主要是帮助线上调试代码,禁止生成这些文件.
在 vue.config.js
里面加上这句。
3.路由懒加载
4.cdn引入公共库
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script src="https://cdn.bootcss.com/vuex/3.0.1/vuex.min.js"></script>
<script src="https://cdn.bootcss.com/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.bootcss.com/axios/0.19.2/axios.min.js"></script>
复制代码
//cdn引入
configureWebpack: {
externals: {
'vue': 'Vue',
'element-ui': 'ELEMENT',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'axios': 'axios'
}
}
复制代码
网上说可以把 import
注释掉,亲自操作会报错,也有资料说不用注释也不会打包。
一顿操作最后的文件,效果显著,app.js还是很大
5.终极法宝 GZIP压缩
做完这个感觉前四步都是小菜一碟,直接把 1.4m
的 app.js
干成一百多 kb
,其他的都不足挂齿了。
configureWebpack: config => {
return {
//配置cdn
externals: {
'vue': 'Vue',
'element-ui': 'ELEMENT',
'vue-router': 'VueRouter',
'vuex': 'Vuex',
'axios': 'axios'
},
//配置gzip压缩
plugins: [
new CompressionWebpackPlugin({
test: new RegExp('\.(js|css)$'),
threshold: 10240,
minRatio: 0.8
})
],
}
}
复制代码
服务端也要配,不然不认识 GZIP
文件。
//配置GZIP压缩模块
const compression = require('compression');
//在所有中间件之前引入
app.use(compression());
复制代码
最垃圾的服务器通过以上几个优化,一样飞起来了!!!
使用 vue
开发时,在 vue
初始化之前,由于 div
是不归 vue
管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于 {{message}}
的字样,虽然一般情况下这个时间很短暂,但是我们还是有必要让解决这个问题的。
首先:在 css
里加上 [v-cloak] { display: none; }
。如果没有彻底解决问题,则在根元素加上 style="display: none;" :style="{display: block }"
Class
可以通过对象语法和数组语法进行动态绑定:
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
data: {
isActive: true,
hasError: false
}
复制代码
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
复制代码
Style
也可以通过对象语法和数组语法进行动态绑定:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
activeColor: 'red',
fontSize: 30
}
复制代码
<div v-bind:style="[styleColor, styleSize]"></div>
data: {
styleColor: {
color: 'red'
},
styleSize:{
fontSize:'23px'
}
}
复制代码
在组件中的 style
标签中加上 scoped
ref="domName"
用法:this.$refs.domName
vue
文件的一个加载器,把 template/js/style
转换成 js
模块。
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
在钩子函数 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
可知子组件先完成渲染
activated
和 deactivated
,当组件被激活时,触发钩子函数 activated
,当组件被移除时,触发钩子函数 deactivated
。是 vue
对 HTML
元素的扩展,给 HTML
元素增加自定义功能。vue
编译 DOM
时,会找到指令对象,执行指令的相关方法。
自定义指令有五个生命周期
v-text
和 {{}}
表达式渲染数据,不解析标签。v-html
不仅可以渲染数据,而且可以解析标签。当 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
是动态的向 DOM
树内添加或者删除 DOM
元素;v-show
是通过设置 DOM
元素的 display
样式属性控制显隐;v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show
只是简单的基于 css
切换;v-if
是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译; v-show
是在任何条件下,无论首次条件是否为真,都被编译,然后被缓存,而且 DOM
元素保留;v-if
有更高的切换消耗;v-show
有更高的初始渲染消耗;v-if
适合运营条件不大可能改变;v-show
适合频繁切换。我们在 vue
项目中主要使用 v-model
指令在表单 input
、textarea
、select
等元素上创建双向数据绑定,我们知道 v-model
本质上不过是语法糖,v-model
在内部为不同的输入元素使用不同的属性并抛出不同的事件:
text
和 textarea
元素使用 value
属性和 input
事件;checkbox
和 radio
使用 checked
属性和 change
事件;select
字段将 value
作为 prop
并将 change
作为事件。以 input
表单元素为例:
<input v-model='something'>
// 相当于
<input v-bind:value="something" v-on:input="something = $event.target.value">
复制代码
如果在自定义组件中,v-model
默认会利用名为 value
的 prop
和名为 input
的事件,如下所示:
父组件:
<ModelChild v-model="message"></ModelChild>
复制代码
子组件:
<div>{{value}}</div>
props:{
value: String
},
methods: {
test1(){
this.$emit('input', '小红')
},
},
复制代码
可以
<input type="text" v-on="{ input:onInput,focus:onFocus,blur:onBlur, }">
复制代码
(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
Vue.component()
app.component()
让多个组件使用同一个挂载点,并动态切换,这就是动态组件
简单的说,动态组件就是将几个组件放在一个挂载点下,这个挂载点就是标签,其需要绑定 is
属性,属性值为父组件中的变量,变量对应的值为要挂载的组件的组件名,然后根据父组件里某个变量来动态显示哪个,也可以都不显示。
缓存 <keep-alive>
keep-alive
是 Vue
内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
include
和 exclude
属性,两者都支持字符串或正则表达式, include
表示只有名称匹配的组件会被缓存,exclude
表示任何名称匹配的组件都不会被缓存 ,其中 exclude
的优先级比 include
高;activated
和 deactivated
,当组件被激活时,触发钩子函数 activated
,当组件被移除时,触发钩子函数 deactivated
。比如有父组件 Parent
和子组件 Child
,如果父组件监听到子组件挂载 mounted
就做一些逻辑处理,可以通过以下写法实现:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
复制代码
以上需要手动通过 $emit
触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook
来监听即可,如下所示:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
复制代码
当然 @hook
方法不仅仅是可以监听 mounted
,其它的生命周期事件,例如:created
,updated
等都可以监听。
⭐ 每个 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
之间的通信桥梁,达到一个数据响应式的效果。
无法劫持以下操作
Vue
框架是通过 遍历数组 和 递归遍历对象,从而达到利用 Object.defineProperty()
对对象和数组的部分方法的操作进行监听。
什么都不会发生,因为 Object.defineProperty()
监听不到这类变化。
可以使用 vm.$set
和 Vue.set
方法去添加一个属性。
可以使用 vm.$delete
和 Vue.delete
方法去删除一个属性。
由于 Vue
只改写了 7 种修改数组的方法,所以 Vue
不能检测到以下数组的变动:
vm.items[indexOfItem] = newValue
vm.items.length = newLength
为了解决第一个问题,Vue
提供了以下操作方法:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set,Vue.set的一个别名
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
复制代码
为了解决第二个问题,Vue
提供了以下操作方法:
// Array.prototype.splice
vm.items.splice(newLength)
复制代码
择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写
所以在 Vue
中修改数组的索引和长度是无法监控到的。需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher
进行更新
// src/obserber/array.js\
// 先保留数组原型\
const arrayProto = Array.prototype;\
// 然后将arrayMethods继承自数组原型\
// 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能\
export const arrayMethods = Object.create(arrayProto);\
let methodsToPatch = [\
"push",\
"pop",\
"shift",\
"unshift",\
"splice",\
"reverse",\
"sort",\
];\
methodsToPatch.forEach((method) => {\
arrayMethods[method] = function (...args) {\
// 这里保留原型方法的执行结果\
const result = arrayProto[method].apply(this, args);\
// 这句话是关键\
// this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4) this就是a ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例\
const ob = this.__ob__;\
\
// 这里的标志就是代表数组有新增操作\
let inserted;\
switch (method) {\
case "push":\
case "unshift":\
inserted = args;\
break;\
case "splice":\
inserted = args.slice(2);\
default:\
break;\
}\
// 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测\
if (inserted) ob.observeArray(inserted);\
// 之后咱们还可以在这里检测到数组改变了之后从而触发视图更新的操作--后续源码会揭晓\
return result;\
};\
});
复制代码
nextTick
中的回调是在下次 DOM
更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick
包装的方法。
简单的理解是:当数据更新了,在 dom
中渲染后, 自动执行该函数。Vue
实现响应式并不是数据发生变化之后 DOM
立即变化,Vue
是异步执行 DOM
更新的。created
钩子函数进行的 DOM
操作一定要放在Vue.nextTick()
的回调函数中,原因是在函数执行的时候 DOM
其实并未进行任何渲染。常用的场景是在进行获取数据后,需要对新视图进行下一步操作或者其他操作时,发现获取不到 dom
。因为赋值操作只完成了数据模型的改变并没有完成视图更新。
有一个 timerFunc
这个函数用来执行 callbacks
里存储的所有回调函数
先判断是否原生支持 promise
,如果支持,则利用 promise
来触发执行回调函数;
否则,如果支持 MutationObserver
,则实例化一个观察者对象,观察文本节点发生变化时,触发执行所有回调函数。 如果都不支持,则利用 setTimeout
设置延时为 0。
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
对象来获取对应节点,比遍历方式更快,源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
复制代码
使用 index
作为 key
和没写基本上没区别,因为不管数组的顺序怎么颠倒,index
都是 0, 1, 2...
这样排列,导致 Vue
会复用错误的旧子节点,做很多额外的工作。
addIfCondition
方法,生成 vnode
的时候会忽略对应节点,render
的时候就不会渲染;vnode
,render
的时候也会渲染成真实节点,只是在 render
过程中会在节点的属性中修改 show
属性值,也就是常说的 display
;innerHTML
为 v-html
的值。数组就是使用 object.defineProperty
重新定义数组的每一项,那能引起数组变化的方法我们都是知道的,
pop
、push
、shift
、unshift
、splice
、sort
、reverse
这七种,只要这些方法执行改了数组内容,我就更新内容就好了,是不是很好理解。
vue3:改用 proxy
,可直接监听对象数组的变化。
vue
中的模板 template
无法被浏览器解析并渲染,因为这不属于浏览器的标准,不是正确的 HTML
语法,所有需要将 template
转化成一个 JavaScript
函数,这样浏览器就可以执行这一个函数并渲染出对应的 HTML
元素,就可以让视图跑起来了,这一个转化的过程,就成为模板编译。
模板编译又分三个阶段,解析 parse
,优化 optimize
,生成 generate
,最终生成可执行函数 render
。
template
字符串进行解析,将标签、指令、属性等转化为抽象语法树 AST
。AST
,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行 diff
比较时,直接跳过这一些静态节点,优化 runtime
的性能。AST
转化为 render
函数字符串。Virtual DOM
是 DOM
节点在 JavaScript
中的一种抽象数据结构,之所以需要虚拟 DOM
,是因为浏览器中操作 DOM
的代价比较昂贵,频繁操作 DOM
会产生性能问题。虚拟 DOM
的作用是在每一次响应式数据发生变化引起页面重渲染时,Vue
对比更新前后的虚拟 DOM
,匹配找出尽可能少的需要更新的真实 DOM
,从而达到提升性能的目的。
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
功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions
方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
其实就是一个子类构造器 ,是 Vue
组件的核心 api
实现思路就是使用原型继承的方法返回了 Vue
的子类 并且利用 mergeOptions
把传入组件的 options
和父类的 options
进行了合并。
原生事件绑定是通过 addEventListener
绑定给真实元素的,组件事件绑定是通过 Vue
自定义的 $on
实现的。如果要在组件上使用原生事件,需要加 .native
修饰符,这样就相当于在父组件中把子组件当做普通 html
标签,然后加上原生事件。
on、emit
是基于发布订阅模式的,维护一个事件中心,on
的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit
将对应的事件进行发布,去执行事件中心里的对应的监听器。
虚拟 DOM
的实现原理主要包括以下 3 部分:
JavaScript
对象模拟真实 DOM
树,对真实 DOM
进行抽象;diff
算法 — 比较两棵虚拟 DOM
树的差异;pach
算法 — 将两个虚拟 DOM
对象的差异应用到真正的 DOM
树。DOM
不会进行排版与重绘操作 DOM
就是把真实 DOM
转换为 Javascript
代码DOM
进行频繁修改,然后一次性比较并修改真实 DOM
中需要改的部分,最后并在真实 DOM
中进行排版与重绘,减少过多 DOM
节点排版与重绘损耗vue-router
有 3 种路由模式:hash
、history
、abstract
:
URL hash
值来作路由。支持所有浏览器,包括不支持 HTML5 History Api
的浏览器;HTML5 History API
和服务器配置。JavaScript
运行环境,如 Node.js
服务器端。如果发现没有浏览器的 API
,路由会自动强制进入这个模式.(1)hash 模式的实现原理
早期的前端路由的实现就是基于 location.hash
来实现的。其实现原理很简单,location.hash
的值就是 URL
中 #
后面的内容。比如下面这个网站,它的 location.hash
的值为 #search
:
https://www.word.com#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
可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
复制代码
history
路由模式的实现主要基于存在下面几个特性:
pushState
和 repalceState
两个 API
来操作实现 URL
的变化 ;popstate
事件来监听 url
的变化,从而对页面进行跳转(渲染);history.pushState()
或 history.replaceState()
不会触发 popstate
事件,这时我们需要手动触发页面跳转(渲染)。监听 $route
对象
// 监听,当路由发生变化的时候执行
watch: {
$route: {
handler: function(val, oldVal){
console.log(val);
},
// 深度观察监听
deep: true
}
复制代码
$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
获取传递的值使用 location.href= /url
来跳转,简单方便,但是刷新了页面;
使用 history.pushState( /url )
,无刷新页面,静态跳转;
引进 router
,然后使用 router.push( /url )
来跳转,使用了 diff
算法,实现了按需加载,减少了 dom
的消耗。
其实使用 router
跳转和使用 history.pushState()
没什么差别的,因为 vue-router
就是用了 history.pushState()
,尤其是在 history
模式下。
用法:query
要用 path
来引入,params
要用 name
来引入,接收参数都是类似的,分别是 this.$route.query.name
和 this.$route.params.name
。
url 地址显示:query
更加类似于我们 ajax
中 get
传参,params
则类似于 post
,说的再简单一点,前者在浏览器地址栏中显示参数,后者则不显示。
注意点:query
刷新不会丢失 query
里面的数据, params
刷新会丢失 params
里面的数据。
Vue
组件会触发 (dispatch)
一些事件或动作,也就是 Actions
;vuex
中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)
到 Mutations
中;Mutations
就去改变 State
中的数据;State
中的数据被改变之后,就会重新渲染(Render)
到 Vue Components
中去,组件展示更新后的数据,完成一个流程。有五种,分别
Store
中获取数据,mapGetters
辅助函数仅仅是将 store
中的 getter
映射到局部计算属性。store
中状态的方法,且必须是同步函数。mutation
,而不是直接变更状态,可以包含任意异步操作。Store
拆分为多个 store
且同时保存在单一的状态树中。Vuex
的状态存储是响应式的。当 Vue
组件从 store
中读取状态的时候,若 store
中的状态发生变化,那么相应的组件也会相应地得到高效更新。store
中的状态。改变 store
中的状态的唯一途径就是显式地提交 mutation
。Mutation
专注于修改 State
,理论上是修改 State
的唯一途径;Action
业务代码、异步请求。Mutation
:必须同步执行;Action
:可以异步,但不能直接操作 State
。actions
,actions
再触发 mutation
mutation
的参数是 state
,它包含 store
中的数据;action
的参数是 context
,它是 state
的父级,包含 state
、getters
等。Vuex
中所有的状态更新的唯一途径都是 mutation
,异步操作通过 Action
来提交 mutation
实现,这样可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解我们的应用。mutation
执行完成后都会对应到一个新的状态变更,这样 devtools
就可以打个快照存下来。如果 mutation
支持异步操作,就没有办法知道状态是何时更新的,无法很好的进行状态的追踪,给调试带来困难。(1)最重要的区别
vuex
存储在内存中localstorage
则以文件的方式存储在本地,只能存储字符串类型的数据,存储对象需要 JSON
的 stringify
和 parse
方法进行处理。 读取内存比读取硬盘速度要快(2)应用场景
Vuex
是一个专为 Vue.js
应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。vuex
用于组件之间的传值。localstorage
是本地存储,是将数据存储到浏览器的方法,一般是在跨页面传递数据时使用 。Vuex
能做到数据的响应式,localstorage
不能(3)永久性
刷新页面时 vuex
存储的值会丢失,localstorage
不会,对于不变的数据可以用 localstorage
可以代替 vuex
。
在严格模式下,无论何时发生了状态变更且不是由 mutation
函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
在 Vuex.Store
构造器选项中开启,如下
const store = new Vuex.Store({
strict:true,
})
复制代码
使用 mapGetters
辅助函数, 利用对象展开运算符将 getter
混入 computed
对象中
import {mapGetters} from 'vuex'
export default{
computed:{
...mapGetters(['total','discountTotal'])
}
}
复制代码
使用 mapMutations
辅助函数,在组件中这么使用
import { mapMutations } from 'vuex'
methods:{
...mapMutations({
setNumber:'SET_NUMBER',
})
}
复制代码
created
周期中读取 sessionstorage
中的数据存储在 store
中,此时用 vuex.store
的 replaceState
方法,替换 store
的根状态beforeunload
方法中将 store.state
存储到 sessionstorage
中。export default {
name: 'App',
created() {
//在页面加载时读取sessionStorage里的状态信息
if (sessionStorage.getItem("store")) {
this.$store.replaceState(Object.assign({},
this.$store.state, JSON.parse(sessionStorage.getItem("store"))))
}
//在页面刷新时将vuex里的信息保存到sessionStorage里
window.addEventListener("beforeunload", () => {
sessionStorage.setItem("store", JSON.stringify(this.$store.state))
})
}
}
复制代码
Vue3.x
改用 Proxy
替代 Object.defineProperty
。因为 Proxy
可以直接监听对象和数组的变化,并且有多达 13 种拦截方法。
Proxy 的优势如下:
Proxy
可以直接监听对象而非属性;Proxy
可以直接监听数组的变化;Proxy
返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty
只能遍历对象属性直接修改;Proxy
作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;Object.defineProperty 的优势如下:
IE9
。注意:3.0
中的生命周期钩子要比2.X
中相同生命周期的钩子要快
Composition API
还新增了以下调试钩子函数:但是不怎么常用
先看看Vue2
自定义指令的钩子
DOM
的父元素时触发。在 Vue3
中,官方为了更有助于代码的可读性和风格统一,把自定义指令的钩子名称改的更像是组件生命周期,尽管他们是两回事
最后祝大家在新的一年里,都能找到满意的工作,升职加薪,赚的盆满钵满!
如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star:http://github.crmeb.net/u/defu不胜感激 !
PHP学习手册:https://doc.crmeb.com 技术交流论坛:https://q.crmeb.com
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。