
为了金三银四的跳槽季做准备,并且我是 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 的冗余代码CSSSourceMapVue 项目的编译优化(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] = newValuevm.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/:idpath 后面跟上对应的值/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 再触发 mutationmutation 的参数是 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 删除。