Vue
概念篇
Vue
的优缺点
优点:
- 数据驱动试图:虚拟
DOM
、diff
算法、响应式、观察者、异步队列,最小代码更新DOM
,渲染视图 - 组件化
- 丰富的
API
Vue ssr
服务端渲染- 生命周期
- 社区、生态 缺点:
- 不支持
IE8
及以下浏览器(Object.defineProperty
实现响应式) - 不利于
seo
优化
Vue
基础篇
watch
和计算属性有什么区别?
computed
计算属性是基于它们的响应式依赖进行缓存的,响应式依赖变化时才会重新求值。性能得到很大的提升。watch
侦听器监听数据变化执行异步或开销较大的操作。watch
详细用法
v-show
和 v-if
的区别
v-show
通过css display
属性控制元素的显示与隐藏v-if
组件和元素真正的渲染和销毁- 频繁切换使用
v-show
,初始化后状态不再改变用v-if
v-for
为什么要使用key
key
必须存在,并且不能使用下标和随机数diff
算法时,通过标签tag
和key
对比识别VNode
- 减少渲染次数,提升渲染性能
v-for
和v-if
不能同时使用
v-for
的优先级比v-if
更高,先循环渲染,再判断销毁,增加了不必要的渲染- 可以使用计算属性过滤,再循环
- 可以在循环外层,根据数据的长度判断整体的显示与隐藏
监听键盘事件
- 按键修饰符
@keyup
addEventListener
与removeEventListener
监听和移除keyup
事件
组件内 data
为什么必须是一个函数?
每个实例可以维护一份被返回对象的独立的拷贝,否则相同数据会共用
生命周期
所有生命周期钩子的
this
上下文将自动绑定至实例中,因此你可以访问 data、computed 和 methods。这意味着你不应该使用箭头函数来定义一个生命周期方法 分为四个过程:创建、挂载、更新、销毁
创建
beforeCreate
初始化实例,实例上只有一些默认的生命周期和默认的事件,其他均为创建created
实例创建完成,以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数
挂载
beforeMount render
函数被调用,生成虚拟DOM
mounted
是在模板渲染成HTML之后调用的,此时data
,el
都已准备好,可以操作html
的dom
节点
更新
数据更新调用beforeUpdate
和updated
。
beforeUpdate
数据更新后,新的虚拟DOM
生成,还没和旧的虚拟DOM
对比打补丁,适合在现有DOM
将要被更新之前访问它,比如移除手动添加的事件监听器。updated
数据更改后,虚拟DOM
重新渲染和更新完毕,可以进行DOM
操作
销毁
beforeDestroy
实例销毁之前调用。在这一步,实例仍然完全可用。可清除定时器和手动添加的事件监听器。destroyed
实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
异步请求应该在那个生命周期访问?
created
钩子函数data
就已经创建,所以created
、beforeMount
、mounted
都可以进行异步请求,主要看异步请求后是否有DOM
操作,需要DOM
操作在mounted
钩子函数异步请求。
父子组件生命周期更新过程
渲染过程
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
父子组件实现双向数据流更新过程
父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
销毁过程
父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
$nextTick
是什么?
Vue
是异步渲染data
改变之后,DOM
不会立即渲染$nextTick
会在DOM
渲染之后触发,以获取最新的DOM
节点
Vue
组件的通讯方式有哪些?
Vue
进阶篇
自定义组件的 v-model
- 一个组件上的
v-model
默认会利用名为value
的prop
和名为input
的事件。 - 像单选框、复选框等类型的输入控件可能会将
value attribute
用于不同的目的,子组件种可以使用model.prop
更改默认的prop
名,model.event
更改默认的事件名。
// 父组件
// 默认形式
<VmodelComponent :value="cval" @input="cval = $event"></VmodelComponent>
// v-model形式
<VmodelComponent v-model="cval"></VmodelComponent>
// 子组件
// 注:input要使用:value,不能用v-model
<template>
<div>
<input type="text" :value="value" @input="$emit('input', $event.target.value)"/>
</div>
</template>
<script>
export default {
props: ["value"]
}
</script>
插槽 slot
具名插槽
组件内有多个插槽时,使用具名插槽。
// 子组件
<slot name="default"></slot>
// 父组件
<template v-slot:default>
父组件给子组件插入的内容
</template>
// 简写
<template #default></template>
作用域插槽
插槽内容使用子组件内的数据。
// 子组件
<slot name="default" :child="child"></slot>
// 父组件
<template v-slot:default="slotProps">
{{ slotProps.child }}
</template>
// 解构
<template v-slot:default="{child}">
{{ child }}
</template>
// 解构并重新命名
<template v-slot:default="{child: childRename}">
{{ childRename }}
</template>
// 当子组件没有传值时,添加后备内容
<template v-slot:default="{child='后备内容'}">
{{ child }}
</template>
keep-alive
的作用
- 缓存组件
- 频繁切换,不需要重新渲染(tab页切换)
混入 mixin
- 多个组件有相同的逻辑,抽离出来
mixin
和组件有冲突时:
data
、methods
、components
和directives
:组件覆盖混入的- 生命周期:混入在组件之前执行
mixin
并不是完美的解决方案,会有一些问题
- 变量来源不明确,不便于理解
- 多
mixin
会造成命名冲突 mixin
和组件可能出现多对多的关系,复杂度较高
自定义指令
指令本质上是装饰器,是 vue
对 HTML
元素的扩展,给 HTML
元素增加自定义功能。vue
编译 DOM
时,会找到指令对象,执行指令的相关方法。
// 复制指令
Vue.directive('copy', {
bind (el, { value }) {
el.$value = value
el.handler = () => {
debugger
// 创建input标签
const input = document.createElement('input')
// 将input的值设置为需要复制的内容
input.value = el.$value
// 添加input标签
document.body.appendChild(input)
// 选中input标签
input.select()
// 执行复制
document.execCommand('copy')
// 移除input标签
document.body.removeChild(input)
}
// 绑定点击事件,就是所谓的一键 copy 啦
el.addEventListener('click', el.handler)
},
// 当传进来的值更新的时候触发
componentUpdated (el, { value }) {
el.$value = value
},
// 指令与元素解绑的时候,移除事件绑定
unbind (el) {
el.removeEventListener('click', el.handler)
}
})
// 应用
<template>
<div>
<input v-model="inputVal" />
<div v-copy="inputVal">复制</div>
</div>
</template>
<script>
export default {
data () {
return {
inputVal: ""
}
}
}
</script>
Vue.use()
做了什么?
Vue.use()
执行插件的install
方法,它需要在你调用 new Vue()
启动应用之前完成。比如路由的引用。
Vue
高级篇
MVVM的理解 Model-View-ViewModel
核心概念:数据驱动视图;View
视图层,Model
数据层,ViewModel
数据和视图之间的连接层。
图中的DOM Listeners
和Data Bindings
看作两个工具,它们是实现双向绑定的关键。
从View
侧看,ViewModel
中的DOM Listeners
工具会帮我们监测页面上DOM
元素的变化,如果有变化,则更改Model
中的数据;
从Model
侧看,当我们更新Model
中的数据时,Data Bindings
工具会帮我们更新页面中的DOM
元素。
响应式原理
- 数据劫持:
Vue
将遍历data
选项种所有的property
,并使用Object.defineProperty
把这些property
全部转为getter/setter
。 - 观察者模式:每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据
property
记录为依赖。之后当依赖项的setter
触发时,会通知watcher
,从而使它关联的组件重新渲染。 - 异步更新队列:
Vue
在更新DOM
时是异步执行的。只要侦听到数据变化,Vue
将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher
被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
Object.defineProperty
缺点:
- 深度监听,需要深度递归遍历
- 无法监测新增和删除属性(
Vue.set Vue.delete
) - 无法原生监听数组,需要特殊处理(
Object.create
重写数组方法'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
) - 不支持
IE8
以及更低版本浏览器
是么是虚拟 DOM
和 diff
算法
虚拟 DOM
Vue
通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM
,通过 JS
模拟 DOM
结构,计算出最小的变更,操作 DOM
;数据驱动试图的模式下,有效控制 DOM
操作。
Vue
通过 createElement
来创建虚拟节点,即 VNode
,虚拟 DOM
是我们对由 Vue 组件树建立起来的整个 VNode
树的称呼。
// createElement参数
createElement(
// {String | Object | Function}
// 一个 HTML 标签名、组件选项对象,或者
// resolve 了上述任何一种的一个 async 函数。必填项。
'div',
// {Object}
// 一个与模板中 attribute 对应的数据对象。可选。
{
// (详情见下一节)
},
// {String | Array}
// 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)
diff
算法 - 双端比较
新旧 vnode
都包含 children
子元素且不相等的情况下,diff
算法计算出最优的更新方案。
双端比较:旧列表和新列表头头、尾尾、头尾、尾头作比较,对比个过程中指针不断向内靠拢,直到循环结束
// 来源 snabbdom 源码
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
let oldStartIdx = 0, newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
// 开始和开始对比
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
// 结束和结束对比
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
// 开始和结束对比
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
// 结束和开始对比
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
// 以上四个都未命中
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 拿新节点 key ,能否对应上 oldCh 中的某个节点的 key
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 没对应上
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
newStartVnode = newCh[++newStartIdx];
// 对应上了
} else {
// 对应上 key 的节点
elmToMove = oldCh[idxInOld];
// sel 是否相等(sameVnode 的条件)
if (elmToMove.sel !== newStartVnode.sel) {
// New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!);
// sel 相等,key 相等
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
newStartVnode = newCh[++newStartIdx];
}
}
}
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
模板编译
vue-template-compiler
将模板编译为 render
函数的过程:
- parse函数的作用就是把字符串型的
template
转化为AST
抽象语法树结构 - optimize优化,对静态节点做标记。减少了
diff
算法比较的过程 - generate将
AST
生成render
函数(with
语法)
渲染过程
vue-template-compiler
将模板编译为 render
函数,执行 render
函数生成vnode
,通过vnode
组件成的树称为虚拟 DOM
,虚拟 DOM
通过 diff
算法更新为真实 DOM
结构。
初次渲染
- 解析模板为
render
函数 - 触发响应式,监听
data
属性的getter
和setter
- 执行
render
函数,生成vnode
, 通过diff
算法生成真实DOM
更新过程
- 修改
data
,触发setter
(初次渲染时已经监听) - 重新执行
render
函数,生成newVnode
- 通过
diff
算法生成真实DOM
vue-router
路由管理器
常用的路由钩子函数
全局守卫
beforeEach
全局前置守卫
- 判断是否有
token
,跳转登录页 - 通过角色权限动态添加路由
- 修改页面的
title
- 判断当前所处浏览器,跳转非微信环境页面
beforeResolve
全局解析守卫afterEach
全局后置守卫
- 第三方埋点的注册
路由守卫
beforeEnter
路由独享守卫
组件守卫
beforeRouteEnter
- 守卫执行前,组件实例还没创建,不能使用
this
- 可以在
next
回调函数种访问组件实例
beforeRouteUpdate
- 动态路由组件复用时调用
beforeRouteLeave
- 禁止未保存修改前离开
- 清除定时器和事件监听
组件复用导致动态参数失效?
路由参数更改时,组件实例被复用,但组件的生命周期钩子不会再被调用。那怎么实时获取动态参数呢?
- 监听路由的变化
- 监听路由
immediate
为true
时,首次也会进行监听,不需要在生命周期内获取动态参数 - 监听路由
immediate
为false
时,需要在mounted
生命周期进行首次获取动态参数
mounted () {
console.log("mouted", this.$route.params.id)
},
watch: {
$route: {
handler: function (to, from) {
// 当前的动态参数 to.params.id
console.log("watch", to.params.id)
},
immediate: true // 立即执行
}
}
- 组件内路由
beforeRouteUpdate
,组件复用时调用,需要在mounted
生命周期进行首次获取动态参数
mounted () {
console.log("mouted", this.$route.params.id)
},
// 在当前路由改变,但是该组件被复用时调用
beforeRouteUpdate (to, from, next) {
console.log("beforeRouteUpdate", to.params.id)
}
- 添加
key
阻止组件复用,不建议使用,组件销毁再创建,增加开销
<router-view :key="$route.fullPath" />
$router
和 $route
的区别?
路由全局注入后,组件内可以通过 this.$router
访问 router
的实例,就可以调用实例方法 push
、go
等;this.$route
获取当前路由信息对象,获取路由上的 params
、query
等
路由模式
hash
- 通过
location.hash
获取url
的hash
值(从#
号开始的部分) hash
的变化会触发网页的跳转,即浏览器的前进和后退hash
的变化不会触发页面刷新window.onhashchange
监听hash
的变化
history
- 依赖
HTML5 History API
和服务器配置 history.pushState
路由的跳转,不刷新页面window.onpopstate
监听路由的前进和后退
路由懒加载
打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
const router = new VueRouter({
routes: [{ path: '/foo', component: () => import(/* webpackChunkName: "group-foo" */ './Foo.vue'}]
})
Vuex
Vuex
是什么?
Vuex
是一个专为 Vue.js
应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
应用场景:
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态
基本概念:
State
:单一的状态数,存储在Vuex
中的数据和Vue
实例中的data
遵循相同的规则Getters
:store
的计算属性,跟计算属性一样也会有缓存Mutations
:更改state
的唯一方法,通过store.commit
函数触发,必须是同步函数Actions
:不能直接更改state
,通过store.dispatch
函数触发,可以包含异步函数Modules
: 模块,将store
分割成模块,根据不同的功能分割模块,便于状态管理
欢迎大家补充,让更多的XDJM学习并传播。码字不易,评论、点赞、收藏三连哟。