大家好,我是忆白,本文是根据慕课网双越老师框架面试课程的Vue部分做的一些整理以及相应扩展。
1. 父子组件创建和挂载生命周期的先后
父组件先创建(created)、再子组件创建(created);子组件先挂载(mounted),父组件再挂载(mounted),因为父组件创建了,子组件才能添加进来,子组件渲染完成了,父组件才能叫做渲染完成。
2. 自定义v-model
在定义组件时,可以指定一个model属性,允许一个自定义组件在使用 v-model
时定制 prop 和 event。 props为使用组件时,v-model后面跟的变量,event为自定义事件。 默认情况下,一个组件上的 v-model
会把 value
用作 prop 且把 input
用作 event
v-model
在内部为不同的输入元素使用不同的 property 并抛出不同的事件(下面以input为例):
- text 和 textarea 元素使用
value
property 和input
事件; - checkbox 和 radio 使用
checked
property 和change
事件; - select 字段将
value
作为 prop 并将change
作为事件。
子组件(CustomVModel):
<template>
<!-- 例如:vue 颜色选择 -->
<input type="text"
:value="text1"
@input="$emit('change1', $event.target.value)"
>
<!--
1. 上面的 input 使用了 :value 而不是 v-model
2. 上面的 change1 和 model.event1 要对应起来
3. text1 属性对应起来
-->
</template>
<script>
export default {
model: {
prop: 'text1', // 对应 props text1
event: 'change1'
},
props: {
text1: {
type: String,
default() {
return '';
}
}
}
}
</script>
父组件
<p>{{name}}</p>
<CustomVModel v-model="name"/>
- 这个name就会传递到子组件model属性的props中,然后子组件input的value绑定这个name变量,
- 子组件中input的值改变时,就会触发input事件,然后在input回调中,使用$emit触发model中指定的change1事件,这个事件会自动使父组件中name同步更新为input框的值。
- 所以我们平日直接在input框使用v-model可以写成
<input :value="name" @input="name = $event.target.value">
,先用v-bind绑定一个数据name,然后在input框改变时触发input事件,在事件回调中把name的值赋值为当前input框的值,因此v-model实际上是一个语法糖。
3. 动态组件
使用component标签,使用is属性传入组件名称,动态决定渲染哪个组件
<component :is="componentName">
4. Vue异步加载组件
- import函数:普通import导入是同步的,而import函数返回的是一个Promise,可以import('../xxx').then()
- import函数异步加载,可以让异步组件打包成单独的js,用到的时候再加载
<template>
<FormDemo v-if="showFormDemo"></FormDemo>
<button @click="showFormDemo = true">show Form demo</button>
</template>
<script>
export default {
components: {
FormDemo: () => import('../BaseUse/FormDemo')
}
data() {
return {
showDemo: false;
}
}
}
</script>
5. keep-alive缓存组件
- 缓存组件
- 频繁切换,不需要重复渲染
- Vue常见的性能优化
- 用到的时候再加载,然后mounted挂载,不用的时候不会destory而是缓存起来,下次用的时候直接从缓存使用,也不用重新渲染,mounted钩子只执行一次
6. mixin
- 多个组件有相同的逻辑,抽离出来
- mixin 并不是完美的解决方案,会有一些问题
- Vue3 提出的 Composition API 旨在解决这些问题
mixin的问题:
- 变量来源不明确,不利于阅读
- 引入多个 mixin 可能会造成命名冲突的问题(不过生命周期钩子内容会合并在一起)
- mixin 和组件可能出现多对多的关系,复杂度较高
7. Vue响应式
7.1 Vue2响应式实现
- observer方法中传入需要观察的对象
- defineReactive(target, key, value)方法用来重新定义target对象的set和get,key为属性,value为key对应的值
- 数组的响应式不能用Object.defineProperty,在这里我们新建了一个对象arrProto,这个对象的原型指向原始数组原型,在这个对象中重写了一些数组方法,在重写的方法中触发了视图更新,之后如果需要监听数组,我们就让数组的原型指向arrProto,这样就会使用我们重写后的方法。(所以当数组arr需要监听时,它的原型链是这样的:arr -> arrProto -> Array.prototype)
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
// 先保存原本的数组原型
const oldArrayProperty = Array.prototype
// 创建一个新对象arrProto,它的原型指向 oldArrayProperty 也就是原始数组原型,新对象上再扩展新的方法不会影响原始数组原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
// 在新建的arrProto中实现这些方法,添加视图更新并执行原始数组的该方法
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
}
// 如果传入数组,则让数组的原型指向我们上面创建的新对象arrProto,这样执行方法的时候是执行带有视图更新的重写方法
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)
7.2 Object.definedProperty 缺点
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性/删除属性(通过Vue.set / Vue.delete解决)
- 无法原生监听数组,需要特殊处理(重写数组方法)
8. 虚拟DOM(Virtual DOM)和 diff
- vdom是实现 vue 和 React 的重要基石
- diff 算法是 vdom 中最核心、最关键的部分
DOM 操作非常耗费性能,解决方案 vdom
8.1用 JS 模拟 DOM 结构
<div id="div1" class="container">
<p>vdom</p>
<ul style="font-size: 20px">
<li>a</li>
</ul>
</div>
{
tag: 'div',
props: {
className: 'container',
id: 'div1'
}
children: [
{
tag: 'p',
children: 'vdom'
},
{
tag: 'ul',
props: { style: 'font-size: 20px' }
children: [
{
tag: 'li'
children: 'a'
}
]
}
]
}
8.2 diff算法概述
两棵树进行diff的时间复杂度是 O(n^3)
优化时间复杂度到 O(n)
-
只比较同一层级,不跨级比较
-
tag 不相同,则直接删掉重建,不再深度比较
- tag 和 key,两者都相同,则认为是相同节点,不再深度比较
patchVnode:
-
新节点没有text文本
- 新旧都有children,则执行updataChildren
- 新有children,旧没有,则清空旧的text,并添加children
- 旧有children,新没有,则移除旧的children
- 旧有text,直接清空
-
新节点有text,则没有children子节点
- 如果旧节点有子节点children,直接移除,然后设置旧节点text为新节点text值
updataChildren:
-
定义四个指针,分别指向新节点子节点children的头和尾,以及旧节点子节点children的头和尾
-
然后新旧子节点开始和开始对比,结束和结束对比,开始和结束对比,结束和开始对比,对应上之后指针都往中间走
-
如果都没命中,拿新节点的key,对应旧children中的key,以及相应的元素类型
- 如果一个key都对应不上,说明这个节点是新增的,直接插入
- 如果对应上,并且元素类型也对应上,说明是相同节点,就把旧节点直接移动到对应位置
9. 模板编译
- 前置知识:JS 的 with 语法
- vue template complier 将模板编译成 render 函数
- 执行 render 函数生成 vnode
9.1 with语法
- 改变 { } 内自由遍历的查找规则,当作 obj 属性来查找
- 如果找不到匹配的 obj 属性,就会报错
- with 要慎用,它打破了作用域规则,易读性变差
不使用with:
const obj = {a: 100, b: 200};
console.log(obj.a);
console.log(obj.b);
console.log(obj.c); // undefined
使用with时,能改变{}中自由变量的查找方式,将{}内的自由变量,当作 obj 的属性来查找
with (obj) {
console.log(a); // 相当于console.log(obj.a);
console.log(b);
console.log(c); // 会报错
}
9.2 编译模板
- 模板编译为 render 函数,执行 render 函数返回 vnode
- 基于 vnode 再执行 patch 和 diff
- 使用 webpack vue-loader ,会在开发环境下编译模板(生产环境直接使用编译好的)
9.3 vue 组件中使用 render 代替 template
Vue.component('heading', {
// template: 'xxxx',
render: function(createElement) {
return createElement(
'h',
[
createElement('a', {
attrs: {
name: 'headerId',
href: '#' + 'headerId'
}
}, 'this is a tag')
]
)
}
})
10. 组件 渲染/更新 过程(考察流程全面度)
10.1 初次渲染的过程
-
解析模板为 render 函数(或者在开发环境打包已经完成,使用vue-lodaer)
-
触发响应式,监听 data 属性 getter、setter
- 执行 render 函数,生成 vnode ,然后patch(elem, vnode),把vnode挂载到elem上
10.2 更新过程
- 修改 data ,触发 setter (此前在 getter 中已被监听)
- 重新执行 render 函数,生成 newVnode
- 之后执行patch(vnode, newVnode)
初次渲染和更新合并过程:
- 将data里的属性挂载到vue实例上
- 利用getter和setter监听data上的属性
- 将模板编译为render函数,结合data里数据执行render生成vnode
- 执行render函数时,会触发data属性的getter,在getter函数里将这些依赖都收集起来。(watcher)
- 通过patch生成dom节点。
- 修改data时,notify watcher,看修改的data是不是在watcher里。(这是在data里data.age=20新增属性时,无法实现响应式的原因)
- 如果之前被收集起来的依赖有被notify,就重新执行render函数。
10.3 异步渲染
因为数据驱动视图,而修改数据是很容易并且很频繁的,如果频繁更新视图,DOM频繁改变,性能就会下降,因此异步渲染是很重要的。
-
$nextTick
-
汇总 data 的修改,一次性更新视图
大致思路就是:
- 同步修改 data 时,把修改操作汇总到一个队列中
- 在异步时取得这个队列,将其中的修改数据汇总,就像 Object.assign
- 用汇总的结果统一修改 data ,触发试图更新
-
减少 DOM 操作次数,提高性能
11. 前端路由原理
网页 url 组成部分:
11.1 hash 的特点
- hash 变化会触发页面跳转,即浏览器的前进、后退
- hash 变化不会刷新页面,SPA 必需的特点
- hash 永远不会提交到 server 端(前端自生自灭)
11.2 js实现hash路由
<p>hash test</p>
<button id="btn1">修改 hash</button>
<script>
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => { // 监听hash改变
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash:', location.hash)
}
// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
console.log('hash:', location.hash)
})
// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
location.href = '#/user'
})
</script>
11.3 H5 history
- 用 url 规范的路由,但跳转时不刷新页面
- history.pushState
- window.onpopState监听
- 需要服务端配合,无论访问哪个路由,都返回index.html,因为当页面带上路由之后,如果刷新,会出现找不到页面的错误。
<p>history API test</p>
<button id="btn1">修改 url</button>
<script>
// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
console.log('load', location.pathname)
})
// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
const state = { name: 'page1' }
console.log('切换路由到', 'page1')
history.pushState(state, '', 'page1') // 重要!!
})
// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
console.log('onpopstate', event.state, location.pathname)
}
// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
</script>
history接口是HTML5新增的,它有六种模式改变URL而不刷新页面:
- replaceState: 替换原来的路径
- pushState: 使用新的路径
- popState:路径的回退
- go:向前或向后改变路径
- forward:向前改变路径
- back:向后改变路径
11.4 两者选择
- to B 的系统(后台系统)推荐使用 hash,简单易用,对 url 规范不敏感
- to C 的系统,可以考虑选择 H5 history,但需要服务端支持
- 能选简单的,就别用复杂的,要考虑成本和收益
12 面试题
v-show 和 v-if 的区别
- v-show 通过 CSS display 控制显示和隐藏,依然会渲染出来,在DOM树中节点存在
- v-if 组件真正的渲染和销毁,而不是显示隐藏,不满足条件不会渲染,在DOM树中没有该节点
- 频繁切换显示状态使用v-show,否则使用v-if
为何在 v-for 中使用 key
- 必须使用 key,且不能是 index 和 random
- 在 diff 算法中通过标签 tag 和 key 来判断,是否是相同节点sameNode,从而优化 diff 算法时间复杂度
- 因此可以减少渲染次数,提升渲染性能
常见的Vue组件通信方式
- 父子组件 props 和 this.$emit
- 事件总线,自定义事件,event.off(解绑事件)、event.$emit(触发事件)
- 祖先与后代组件,provide 与 inject
- vuex
双向数据绑定 v-model 的实现原理
- input 元素的 value = this.name(假设name就是v-model绑定的变量)
- 绑定 input 事件,然后this.name = $event.target.value,每当input框改变触发input事件,然后将输入框中的值赋值给之前绑定的变量
- data 更新就会触发 re-render,模板重新渲染,就实现了双向数据绑定
computed 有何特点
- 缓存,data 数据不变,就不会重新计算
- 提高性能
为何组件中 data 必须是一个函数?
组件被编译之后,实际上是一个class,在每个地方使用组件实际上是对这个class实例化,实例化的时候执行data,如果不是函数,则每个组件实例的数据都一样(通过原型回溯),一个组件修改data,会导致其他组件的data跟着改变,无法达到复用的目的。如果是一个函数,则data数据存在于闭包中,组件间就不会互相影响。
模拟处理:
const MyComponent = function() {};
MyComponent.prototype.data = {
a: 1,
b: 2
}
const component1 = new MyComponent();
const component2 = new MyComponent();
component1.data.a === component2.data.a; // true
component1.data.b = 5;
component2.data.b //5
如果两个实例同时引用一个对象,那么当你修改其中一个属性的时候,另外一个实例也会跟着改变;
两个实例应该有自己各自的域才对,需要通过下面的方法进行处理
const MyComponent = function() {
this.data = this.data();
};
MyComponent.prototype.data = function() {
return {
a:1,
b:2
}
}
如何将一个组件所有属性props 传递给子组件
- 使用$props
<User v-bind:"$props" />
- 一个细节知识点,优先级不高
多个组件有相同的逻辑,如何抽离?
- 使用mixin
- 以及mixin一些缺点(前面总结过)
- Vue3使用 Composition API
何时要使用异步组件
- 加载大组件
- 路由异步加载
何时需要使用 keep-alivie?
- 缓存组件,不需要重复渲染
- 比如多个静态 tab 页的切换
- 可以优化性能
何时需要使用 beforeDestory
- 解绑自定义事件 event.$off
- 清除定时器
- 解绑自定义的原生DOM事件(使用vue绑定的事件,vue会帮我们解除,但自己绑定的不会),如window.scroll等
- 以上三点不做的话,容易造成内存泄漏
什么是作用域插槽
作用域插槽是为了让插槽将所在组件的data中属性,向外层传递给插槽使用者。
Vuex中action和mutation有何区别
- action 中处理异步,mutation 不可以
- mutation 中做原子操作,即一次只做一个操作
- action 可以整合多个 mutation
vue-router 常用的路由模式
- hash 默认
- H5 history(需要服务端支持)
- 两者比较
Vue 常见性能优化方式
- 合理使用 v-show 和 if
- 合理使用 computed
- 使用 v-for 时加 key,以及避免和 v-if 同时使用(v-for优先级更高,每次v-for都会重新执行v-if计算)
- 自定义事件,DOM事件及时销毁,否则会造成内存泄漏
- 合理使用异步组件
- 合理使用 keep-alive
- data 层级不要太深,vue2深度监听需要一次性遍历完成,这样会导致响应式做监听时递归次数比较多
- 使用 vue-loader 在开发环境做模板编译(预编译)
- webpack 层面的优化:
- 前端通用的性能优化,如图片懒加载
- 使用 SSR
写在最后
如果你觉得我写的还不错,欢迎给我点个赞哦~