Vue 的生命周期方法有哪些 一般在哪一步发请求
beforeCreate 在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。在当前阶段 data、methods、computed 以及 watch 上的数据和方法都不能被访问
created 实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有el,如果非要想与Dom进行交互,可以通过vm.nextTick 来访问 Dom
beforeMount 在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted 在挂载完成后发生,在当前阶段,真实的 Dom 挂载完毕,数据完成双向绑定,可以访问到 Dom 节点
beforeUpdate 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁(patch)之前。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程
updated 发生在更新完成之后,当前阶段组件 Dom 已完成更新。要注意的是避免在此期间更改数据,因为这可能会导致无限循环的更新,该钩子在服务器端渲染期间不被调用。
beforeDestroy 实例销毁之前调用。在这一步,实例仍然完全可用。我们可以在这时进行善后收尾工作,比如清除计时器。
destroyed Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。
activated keep-alive 专属,组件被激活时调用
deactivated keep-alive 专属,组件被销毁时调用
异步请求在哪一步发起?
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
Vue 中 diff 算法原理
DOM
操作是非常昂贵的,因此我们需要尽量地减少DOM
操作。这就需要找出本次DOM
必须更新的节点来更新,其他的不更新,这个找出的过程,就需要应用 diff 算法
vue
的diff
算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针(头尾都加指针)
的方式进行比较。
简单来说,Diff 算法有以下过程
同级比较,再比较子节点(根据key
和tag
标签名判断)
先判断一方有子节点和一方没有子节点的情况(如果新的children
没有子节点,将旧的子节点移除)
比较都有子节点的情况(核心diff
)
递归比较子节点
正常Diff
两个树的时间复杂度是O(n^3)
,但实际情况下我们很少会进行跨层级的移动DOM
,所以Vue
将Diff
进行了优化,从O(n^3) -> O(n)
,只有当新旧children
都为多个子节点时才需要用核心的Diff
算法进行同层级比较。
Vue2
的核心Diff
算法采用了双端比较
的算法,同时从新旧children
的两端开始进行比较,借助key
值找到可复用的节点,再进行相关操作。相比React
的Diff
算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅
在创建VNode
时就确定其类型,以及在mount/patch
的过程中采用位运算来判断一个VNode
的类型,在这个基础之上再配合核心的Diff
算法,使得性能上较Vue2.x
有了提升
vue3 中采用最长递增子序列来实现diff
优化
回答范例
思路
diff
算法是干什么的
它的必要性
它何时执行
具体执行方式
拔高:说一下vue3
中的优化
回答范例
Vue
中的diff
算法称为patching
算法,它由Snabbdo
m 修改而来,虚拟DOM
要想转化为真实DOM
就需要通过patch
方法转换
最初Vue1.x
视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM
和patching
算法支持,但是这样粒度过细导致Vue1.x
无法承载较大应用;Vue 2.x
中为了降低Watcher
粒度,每个组件只有一个Watcher
与之对应,此时就需要引入patching
算法才能精确找到发生变化的地方并高效更新
vue
中diff
执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render
函数获得最新的虚拟DOM
,然后执行patc
h 函数,并传入新旧两次虚拟 DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM
操作
patch
过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3
的patch
为例
首先判断两个节点是否为相同同类节点,不同则删除重新创建
如果双方都是文本则更新文本内容
如果双方都是元素节点则递归更新子元素,同时更新元素属性
更新子节点时又分了几种情况
新的子节点是文本,老的子节点是数组则清空,并设置文本;
新的子节点是文本,老的子节点是文本则直接更新文本;
新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节 blabla
vue3
中引入的更新策略:静态节点标记等
vdom 中 diff 算法的简易实现
以下代码只是帮助大家理解diff
算法的原理和流程
将vdom
转化为真实dom
:
const createElement = (vnode) => {
let tag = vnode.tag;
let attrs = vnode.attrs || {};
let children = vnode.children || [];
if(!tag) {
return null;
}
//创建元素
let elem = document.createElement(tag);
//属性
let attrName;
for (attrName in attrs) {
if(attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName]);
}
}
//子元素
children.forEach(childVnode => {
//给elem添加子元素
elem.appendChild(createElement(childVnode));
})
//返回真实的dom元素
return elem;
}
复制代码
用简易diff
算法做更新操作
function updateChildren(vnode, newVnode) {
let children = vnode.children || [];
let newChildren = newVnode.children || [];
children.forEach((childVnode, index) => {
let newChildVNode = newChildren[index];
if(childVnode.tag === newChildVNode.tag) {
//深层次对比, 递归过程
updateChildren(childVnode, newChildVNode);
} else {
//替换
replaceNode(childVnode, newChildVNode);
}
})
}
复制代码
</details>
如何定义动态路由?如何获取传过来的动态参数?
(1)param 方式
配置路由格式:/router/:id
传递的方式:在 path 后面跟上对应的值
传递后形成的路径:/router/123
1)路由定义
//在APP.vue中
<router-link :to="'/user/'+userId" replace>用户</router-link>
//在index.js
{
path: '/user/:userid',
component: User,
},
复制代码
2)路由跳转
// 方法1:
<router-link :to="{ name: 'users', params: { uname: wade }}">按钮</router-link
// 方法2:
this.$router.push({name:'users',params:{uname:wade}})
// 方法3:
this.$router.push('/user/' + wade)
复制代码
3)参数获取通过 $route.params.userid
获取传递的值
(2)query 方式
1)路由定义
//方式1:直接在router-link 标签上以对象的形式
<router-link :to="{path:'/profile',query:{name:'why',age:28,height:188}}">档案</router-link>
// 方式2:写成按钮以点击事件形式
<button @click='profileClick'>我的</button>
profileClick(){
this.$router.push({
path: "/profile",
query: {
name: "kobi",
age: "28",
height: 198
}
});
}
复制代码
2)跳转方法
// 方法1:
<router-link :to="{ name: 'users', query: { uname: james }}">按钮</router-link>
// 方法2:
this.$router.push({ name: 'users', query:{ uname:james }})
// 方法3:
<router-link :to="{ path: '/user', query: { uname:james }}">按钮</router-link>
// 方法4:
this.$router.push({ path: '/user', query:{ uname:james }})
// 方法5:
this.$router.push('/user?uname=' + jsmes)
复制代码
3)获取参数
router-link 和 router-view 是如何起作用的
分析
vue-router
中两个重要组件router-link
和router-view
,分别起到导航作用和内容渲染作用,但是回答如何生效还真有一定难度
回答范例
vue-router
中两个重要组件router-link
和router-view
,分别起到路由导航作用和组件内容渲染作用
使用中router-link
默认生成一个a
标签,设置to
属性定义跳转path
。实际上也可以通过custom
和插槽自定义最终的展现形式。router-view
是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name
可以显示具名组件,起到更强的布局作用。
router-link
组件内部根据custom
属性判断如何渲染最终生成节点,内部提供导航方法navigate
,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes
匹配出数组结果,router-view
则根据其所处深度deep
在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。
Watch 中的 deep:true 是如何实现的
当用户指定了 watch
中的 deep 属性为 true
时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher
存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新
源码相关
get () {
pushTarget(this) // 先将当前依赖放到 Dep.target上
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) { // 如果需要深度监控
traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法
}popTarget()
}
复制代码
Vue 组件渲染和更新过程
渲染组件时,会通过 Vue.extend
方法构建子组件的构造函数,并进行实例化。最终手动调用$mount()
进行挂载。更新组件时会进行 patchVnode
流程,核心就是diff
算法
watch 原理
watch
本质上是为每个监听属性 setter
创建了一个 watcher
,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deep
和 immediate
,对应原理如下
deep
:深度监听对象,为对象的每一个属性创建一个 watcher
,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象 setter
,因此引入 deep
能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。
immediate
:在初始化时直接调用回调函数,可以通过在 created
阶段手动调用回调函数实现相同的效果
Vue 是如何实现数据双向绑定的
Vue
数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
Vue 主要通过以下 4 个步骤来实现数据双向绑定的
实现一个监听器 Observer :对数据对象进行遍历,包括子属性对象的属性,利用 Object.defineProperty()
对属性都加上 setter
和 getter
。这样的话,给这个对象的某个值赋值,就会触发 setter
,那么就能监听到了数据变化
实现一个解析器 Compile :解析 Vue
模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新
实现一个订阅者 Watcher :Watcher
订阅者是 Observer
和 Compile
之间通信的桥梁 ,主要的任务是订阅 Observer
中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile
中对应的更新函数
实现一个订阅器 Dep :订阅器采用 发布-订阅 设计模式,用来收集订阅者 Watcher
,对监听器 Observer
和 订阅者 Watcher
进行统一管理
Vue 数据双向绑定原理图
双向绑定的原理是什么
我们都知道 Vue
是数据双向绑定的框架,双向绑定由三个重要部分构成
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM
这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
理解 ViewModel
它的主要职责就是:
当然,它还有两个主要部分组成
监听器(Observer
):对所有数据的属性进行监听
解析器(Compiler
):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
参考:前端vue面试题详细解答
Vue 的事件绑定原理
原生事件绑定是通过 addEventListener
绑定给真实元素的,组件事件绑定是通过 Vue
自定义的$on
实现的。如果要在组件上使用原生事件,需要加.native
修饰符,这样就相当于在父组件中把子组件当做普通 html
标签,然后加上原生事件。
$on
、$emit
是基于发布订阅模式的,维护一个事件中心,on
的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit
将对应的事件进行发布,去执行事件中心里的对应的监听器
EventEmitter(发布订阅模式--简单版)
// 手写发布订阅模式 EventEmitter
class EventEmitter {
constructor() {
this.events = {};
}
// 实现订阅
on(type, callBack) {
if (!this.events) this.events = Object.create(null);
if (!this.events[type]) {
this.events[type] = [callBack];
} else {
this.events[type].push(callBack);
}
}
// 删除订阅
off(type, callBack) {
if (!this.events[type]) return;
this.events[type] = this.events[type].filter(item => {
return item !== callBack;
});
}
// 只执行一次订阅事件
once(type, callBack) {
function fn() {
callBack();
this.off(type, fn);
}
this.on(type, fn);
}
// 触发事件
emit(type, ...rest) {
this.events[type] && this.events[type].forEach(fn => fn.apply(this, rest));
}
}
// 使用如下
const event = new EventEmitter();
const handle = (...rest) => {
console.log(rest);
};
event.on("click", handle);
event.emit("click", 1, 2, 3, 4);
event.off("click", handle);
event.emit("click", 1, 2);
event.once("dbClick", () => {
console.log(123456);
});
event.emit("dbClick");
event.emit("dbClick");
复制代码
源码分析
原生 dom 的绑定
Vue
在创建真是 dom
时会调用 createElm
,默认会调用 invokeCreateHooks
会遍历当前平台下相对的属性处理代码,其中就有 updateDOMListeners
方法,内部会传入 add
方法
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
target = vnode.elm normalizeEvents(on)
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
target = undefined
}
function add ( name: string, handler: Function, capture: boolean, passive: boolean ) {
target.addEventListener( // 给当前的dom添加事件
name,
handler,
supportsPassive ? { capture, passive } : capture
)
}
复制代码
vue
中绑定事件是直接绑定给真实 dom
元素的
组件中绑定事件
export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) {
target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
function add (event, fn) {
target.$on(event, fn)
}
复制代码
组件绑定事件是通过 vue
中自定义的 $on
方法来实现的
什么是作用域插槽
插槽
创建组件虚拟节点时,会将组件儿子的虚拟节点保存起来。当初始化组件时,通过插槽属性将儿子进行分类{a:[vnode],b[vnode]}
渲染组件时会拿对应的 slot
属性的节点进行替换操作。(插槽的作用域为父组件)
<app>
<div slot="a">xxxx</div>
<div slot="b">xxxx</div>
</app>
slot name="a"
slot name="b"
复制代码
作用域插槽
// 插槽
const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
<my-component>
<div slot="header">node</div>
<div>react</div>
<div slot="footer">vue</div>
</my-component> `
)
// with(this) {
// return _c('my-component', [_c('div', {
// attrs: { "slot": "header" },
// slot: "header"
// }, [_v("node")] // _文本及诶点 )
// , _v(" "),
// _c('div', [_v("react")]), _v(" "), _c('div', {
// attrs: { "slot": "footer" },
// slot: "footer" }, [_v("vue")])])
// }
const VueTemplateCompiler = require('vue-template-compiler');
let ele = VueTemplateCompiler.compile(`
<div>
<slot name="header"></slot>
<slot name="footer"></slot>
<slot></slot>
</div> `
);
with(this) {
return _c('div', [_v("node"), _v(" "), _t(_v("vue")])]), _v(" "), _t("default")], 2)
}
// _t定义在 core/instance/render-helpers/index.js
复制代码
// 作用域插槽:
let ele = VueTemplateCompiler.compile(` <app>
<div slot-scope="msg" slot="footer">{{msg.a}}</div>
</app> `
);
// with(this) {
// return _c('app', { scopedSlots: _u([{
// // 作用域插槽的内容会被渲染成一个函数
// key: "footer",
// fn: function (msg) {
// return _c('div', {}, [_v(_s(msg.a))]) } }])
// })
// }
// }
const VueTemplateCompiler = require('vue-template-compiler');
VueTemplateCompiler.compile(` <div><slot name="footer" a="1" b="2"></slot> </div> `);
// with(this) { return _c('div', [_t("footer", null, { "a": "1", "b": "2" })], 2) }
复制代码
什么是递归组件?举个例子说明下?
分析
递归组件我们用的比较少,但是在Tree
、Menu
这类组件中会被用到。
体验
组件通过组件名称引用它自己,这种情况就是递归组件
<template>
<li>
<div> {{ model.name }}</div>
<ul v-show="isOpen" v-if="isFolder">
<!-- 注意这里:组件递归渲染了它自己 -->
<TreeItem
class="item"
v-for="model in model.children"
:model="model">
</TreeItem>
</ul>
</li>
<script>
export default {
name: 'TreeItem',
// ...
}
</script>
复制代码
回答范例
如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
实际开发中类似Tree
、Menu
这类组件,它们的节点往往包含子节点,子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件name
属性,用来查找组件定义,如果使用SFC
,则可以通过SFC
文件名推断。组件内部通常也要有递归结束条件,比如model.children
这样的判断。
查看生成渲染函数可知,递归组件查找时会传递一个布尔值给resolveComponent
,这样实际获取的组件就是当前组件本身
原理
递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)
const _component_Comp = _resolveComponent("Comp", true)
复制代码
就是在传递maybeSelfReference
export function resolveComponent(
name: string,
maybeSelfReference?: boolean
): ConcreteComponent | string {
return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
}
复制代码
resolveAsset
中最终返回的是组件自身:
if (!res && maybeSelfReference) {
// fallback to implicit self-reference
return Component
}
复制代码
为什么 Vue 采用异步渲染
Vue 是组件级更新,如果不采用异步更新,那么每次更新数据都会对当前组件进行重新渲染,所以为了性能, Vue 会在本轮数据更新后,在异步更新视图。核心思想 nextTick
源码相关
dep.notify()
通知 watcher
进行更新, subs[i].update
依次调用 watcher
的 update
, queueWatcher
将watcher
去重放入队列, nextTick
( flushSchedulerQueue
)在下一tick
中刷新watcher
队列(异步)
update () { /* istanbul ignore else */
if (this.lazy) {
this.dirty = true
}
else if (this.sync) {
this.run()
}
else {
queueWatcher(this); // 当数据发生变化时会将watcher放到一个队列中批量更新
}
}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id // 会对相同的watcher进行过滤
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue) // 调用nextTick方法 批量的进行更新
}
}
}
复制代码
子组件可以直接改变父组件的数据么,说明原因
这是一个实践知识点,组件化开发过程中有个单项数据流原则
,不在子组件中修改父组件是个常识问题
思路
讲讲单项数据流原则,表明为何不能这么做
举几个常见场景的例子说说解决方案
结合实践讲讲如果需要修改父组件状态应该如何做
回答范例
所有的 prop
都使得其父子之间形成了一个单向下行绑定:父级 prop
的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的 prop
都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop
。如果你这样做了,Vue
会在浏览器控制台中发出警告
const props = defineProps(['foo'])
// ❌ 下面行为会被警告, props是只读的!
props.foo = 'bar'
复制代码
实际开发过程中有两个场景会想要修改一个属性:
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data
,并将这个 prop
用作其初始值:
const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)
复制代码
这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop
的值来定义一个计算属性:
const props = defineProps(['size'])
// prop变化,计算属性自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
复制代码
实践中如果确实想要改变父组件属性应该emit
一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的prop
,但是我们还是能够直接改内嵌的对象或属性
keep-alive 使用场景和原理
keep-alive
是 Vue
内置的一个组件, 可以实现组件缓存 ,当组件切换时不会对当前组件进行卸载。 一般结合路由和动态组件一起使用 ,用于缓存组件
提供 include
和 exclude
属性, 允许组件有条件的进行缓存 。两者都支持字符串或正则表达式,include
表示只有名称匹配的组件会被缓存,exclude
表示任何名称匹配的组件都不会被缓存 ,其中 exclude
的优先级比 include
高
对应两个钩子函数 activated
和deactivated
,当组件被激活时,触发钩子函数 activated
,当组件被移除时,触发钩子函数 deactivated
keep-alive
的中还运用了 LRU
(最近最少使用) 算法,选择最近最久未使用的组件予以淘汰
<keep-alive></keep-alive>
包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免重新渲染
比如有一个列表和一个详情,那么用户就会经常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就可以对列表组件使用<keep-alive></keep-alive>
进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是重新渲染
关于 keep-alive 的基本用法
<keep-alive>
<component :is="view"></component>
</keep-alive>
复制代码
使用includes
和exclude
:
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- 正则表达式 (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- 数组 (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
复制代码
匹配首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值),匿名组件不能被匹配
设置了 keep-alive
缓存的组件,会多出两个生命周期钩子(activated
与deactivated
):
使用场景
使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用keepalive
举个栗子:
当我们从首页
–>列表页
–>商详页
–>再返回
,这时候列表页应该是需要keep-alive
从首页
–>列表页
–>商详页
–>返回到列表页(需要缓存)
–>返回到首页(需要缓存)
–>再次进入列表页(不需要缓存)
,这时候可以按需来控制页面的keep-alive
在路由中设置keepAlive
属性判断是否需要缓存
{
path: 'list',
name: 'itemList', // 列表页
component (resolve) {
require(['@/pages/item/list'], resolve)
},
meta: {
keepAlive: true,
title: '列表页'
}
}
复制代码
使用<keep-alive>
<div id="app" class='wrapper'>
<keep-alive>
<!-- 需要缓存的视图组件 -->
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<!-- 不需要缓存的视图组件 -->
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
复制代码
思考题:缓存后如何获取数据
解决方案可以有以下两种:
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},
复制代码
// 注意:服务器端渲染期间avtived不被调用
activated(){
this.getData() // 获取数据
},
复制代码
扩展补充:LRU 算法是什么?
LRU
的核心思想是如果数据最近被访问过,那么将来被访问的几率也更高,所以我们将命中缓存的组件 key
重新插入到 this.keys
的尾部,这样一来,this.keys
中越往头部的数据即将来被访问几率越低,所以当缓存数量达到最大值时,我们就删除将来被访问几率最低的数据,即 this.keys
中第一个缓存的组件
相关代码
keep-alive
是vue
中内置的一个组件
源码位置:src/core/components/keep-alive.js
export default {
name: "keep-alive",
abstract: true, //抽象组件
props: {
include: patternTypes, //要缓存的组件
exclude: patternTypes, //要排除的组件
max: [String, Number], //最大缓存数
},
created() {
this.cache = Object.create(null); //缓存对象 {a:vNode,b:vNode}
this.keys = []; //缓存组件的key集合 [a,b]
},
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys);
}
},
mounted() {
//动态监听include exclude
this.$watch("include", (val) => {
pruneCache(this, (name) => matches(val, name));
});
this.$watch("exclude", (val) => {
pruneCache(this, (name) => !matches(val, name));
});
},
render() {
const slot = this.$slots.default; //获取包裹的插槽默认值 获取默认插槽中的第一个组件节点
const vnode: VNode = getFirstComponentChild(slot); //获取第一个子组件
// 获取该组件节点的componentOptions
const componentOptions: ?VNodeComponentOptions =
vnode && vnode.componentOptions;
if (componentOptions) {
// 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag
const name: ?string = getComponentName(componentOptions);
const { include, exclude } = this;
// 不走缓存 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode
if (
// not included 不包含
(include && (!name || !matches(include, name))) ||
// excluded 排除里面
(exclude && name && matches(exclude, name))
) {
//返回虚拟节点
return vnode;
}
const { cache, keys } = this;
// 获取组件的key值
const key: ?string =
vnode.key == null
? // same constructor may get registered as different local components
// so cid alone is not enough (#3269)
componentOptions.Ctor.cid +
(componentOptions.tag ? `::${componentOptions.tag}` : "")
: vnode.key;
// 拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存
if (cache[key]) {
//通过key 找到缓存 获取实例
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
remove(keys, key); //通过LRU算法把数组里面的key删掉
keys.push(key); //把它放在数组末尾
} else {
cache[key] = vnode; //没找到就换存下来
keys.push(key); //把它放在数组末尾
// prune oldest entry //如果超过最大值就把数组第0项删掉
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
vnode.data.keepAlive = true; //标记虚拟节点已经被缓存
}
// 返回虚拟节点
return vnode || (slot && slot[0]);
},
};
复制代码
可以看到该组件没有template
,而是用了render
,在组件渲染的时候会自动执行render
函数
this.cache
是一个对象,用来存储需要缓存的组件,它将以如下形式存储:
this.cache = {
'key1':'组件1',
'key2':'组件2',
// ...
}
复制代码
在组件销毁的时候执行pruneCacheEntry
函数
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
/* 判断当前没有处于被渲染状态的组件,将其销毁*/
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
复制代码
在mounted
钩子函数中观测 include
和 exclude
的变化,如下:
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
复制代码
如果include
或exclude
发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行pruneCache
函数,函数如下
function pruneCache (keepAliveInstance, filter) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode = cache[key]
if (cachedNode) {
const name = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
复制代码
在该函数内对this.cache
对象进行遍历,取出每一项的name
值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用pruneCacheEntry
函数将其从this.cache
对象剔除即可
关于keep-alive
的最强大缓存功能是在render
函数中实现
首先获取组件的key
值:
const key = vnode.key == null?
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
复制代码
拿到key
值后去this.cache
对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存,如下:
/* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
/* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */
remove(keys, key)
keys.push(key)
}
复制代码
直接从缓存中拿 vnode
的组件实例,此时重新调整该组件key
的顺序,将其从原来的地方删掉并重新放在this.keys
中最后一个
this.cache
对象中没有该key
值的情况,如下:
/* 如果没有命中缓存,则将其设置进缓存 */
else {
cache[key] = vnode
keys.push(key)
/* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
复制代码
表明该组件还没有被缓存过,则以该组件的key
为键,组件vnode
为值,将其存入this.cache
中,并且把key
存入this.keys
中
此时再判断this.keys
中缓存组件的数量是否超过了设置的最大缓存数量值this.max
,如果超过了,则把第一个缓存组件删掉
虚拟 DOM 实现原理?
Vue 的 diff 算法详细分析
1. 是什么
diff
算法是一种通过同层的树节点进行比较的高效算法
其有两个特点:
比较只会在同层级进行, 不会跨层级比较
在 diff 比较的过程中,循环从两边向中间比较
diff
算法在很多场景下都有应用,在 vue
中,作用于虚拟 dom
渲染成真实 dom
的新旧 VNode
节点比较
2. 比较方式
diff
整体策略为:深度优先,同层比较
比较只会在同层级进行, 不会跨层级比较
比较的过程中,循环从两边向中间收拢
下面举个vue
通过diff
算法更新的例子:
新旧VNode
节点如下图所示:
第一次循环后,发现旧节点 D 与新节点 D 相同,直接复用旧节点 D 作为diff
后的第一个真实节点,同时旧节点endIndex
移动到 C,新节点的 startIndex
移动到了 C
第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff
后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex
移动到了 B,新节点的 startIndex
移动到了 E
第三次循环中,发现 E 没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex
移动到了 A。旧节点的 startIndex
和 endIndex
都保持不动
第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff
后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex
移动到了 B,新节点的startIndex
移动到了 B
第五次循环中,情形同第四次循环一样,因此 diff
后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex
移动到了 C,新节点的 startIndex 移动到了 F
新节点的 startIndex
已经大于 endIndex
了,需要创建 newStartIdx
和 newEndIdx
之间的所有节点,也就是节点 F,直接创建 F 节点对应的真实节点放到 B 节点后面
3. 原理分析
当数据发生改变时,set
方法会调用Dep.notify
通知所有订阅者Watcher
,订阅者就会调用patch
给真实的DOM
打补丁,更新相应的视图
源码位置:src/core/vdom/patch.js
function patch(oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素
} else {
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// 判断旧节点和新节点自身一样,一致执行patchVnode
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 否则直接销毁及旧节点,根据新节点生成dom元素
if (isRealElement) {
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
}
}
oldVnode = emptyNodeAt(oldVnode)
}
return vnode.elm
}
}
}
复制代码
patch
函数前两个参数位为oldVnode
和 Vnode
,分别代表新的节点和之前的旧节点,主要做了四个判断:
没有新节点,直接触发旧节点的destory
钩子
没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm
旧节点和新节点自身一样,通过 sameVnode
判断节点是否一样,一样时,直接调用 patchVnode
去处理这两个节点
旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点
下面主要讲的是patchVnode
部分
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
// 如果新旧节点一致,什么都不做
if (oldVnode === vnode) {
return
}
// 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
const elm = vnode.elm = oldVnode.elm
// 异步占位符
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 如果新旧都是静态节点,并且具有相同的key
// 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
// 也不用再有其他操作
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果vnode不是文本节点或者注释节点
if (isUndef(vnode.text)) {
// 并且都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 并且子节点不完全一致,则调用updateChildren
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// 如果只有新的vnode有子节点
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// elm已经引用了老的dom节点,在老的dom节点上添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
// 如果老节点是文本节点
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
// 如果新vnode和老vnode是文本节点或注释节点
// 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
复制代码
patchVnode
主要做了几个判断:
新节点是否是文本节点,如果是,则直接更新dom
的文本内容为新节点的文本内容
新节点和旧节点如果都有子节点,则处理比较更新子节点
只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM
,并且添加进父节点
只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM
删除
子节点不完全一致,则调用updateChildren
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // 旧头索引
let newStartIdx = 0 // 新头索引
let oldEndIdx = oldCh.length - 1 // 旧尾索引
let newEndIdx = newCh.length - 1 // 新尾索引
let oldStartVnode = oldCh[0] // oldVnode的第一个child
let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
let newStartVnode = newCh[0] // newVnode的第一个child
let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
// 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 如果oldVnode的第一个child不存在
if (isUndef(oldStartVnode)) {
// oldStart索引右移
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
// 如果oldVnode的最后一个child不存在
} else if (isUndef(oldEndVnode)) {
// oldEnd索引左移
oldEndVnode = oldCh[--oldEndIdx]
// oldStartVnode和newStartVnode是同一个节点
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// patch oldStartVnode和newStartVnode, 索引左移,继续循环
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldEndVnode和newEndVnode是同一个节点
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// patch oldEndVnode和newEndVnode,索引右移,继续循环
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldStartVnode和newEndVnode是同一个节点
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// patch oldStartVnode和newEndVnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// oldStart索引右移,newEnd索引左移
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// 如果oldEndVnode和newStartVnode是同一个节点
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// patch oldEndVnode和newStartVnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
// 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// oldEnd索引左移,newStart索引右移
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 如果都不匹配
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 如果未找到,说明newStartVnode是一个新的节点
if (isUndef(idxInOld)) { // New element
// 创建一个新Vnode
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
// 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
} else {
vnodeToMove = oldCh[idxInOld]
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
warn(
'It seems there are duplicate keys that is causing an update error. ' +
'Make sure each v-for item has a unique key.'
)
}
// 比较两个具有相同的key的新节点是否是同一个节点
//不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
if (sameVnode(vnodeToMove, newStartVnode)) {
// patch vnodeToMove和newStartVnode
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
// 清除
oldCh[idxInOld] = undefined
// 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
// 移动到oldStartVnode.elm之前
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
// 如果key相同,但是节点不相同,则创建一个新的节点
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
}
}
// 右移
newStartVnode = newCh[++newStartIdx]
}
}
复制代码
while
循环主要处理了以下五种情景:
当新老 VNode
节点的 start
相同时,直接 patchVnode
,同时新老 VNode
节点的开始索引都加 1
当新老 VNode
节点的 end
相同时,同样直接 patchVnode
,同时新老 VNode
节点的结束索引都减 1
当老 VNode
节点的 start
和新 VNode
节点的 end
相同时,这时候在 patchVnode
后,还需要将当前真实 dom
节点移动到 oldEndVnode
的后面,同时老 VNode
节点开始索引加 1,新 VNode
节点的结束索引减 1
当老 VNode
节点的 end
和新 VNode
节点的 start
相同时,这时候在 patchVnode
后,还需要将当前真实 dom
节点移动到 oldStartVnode
的前面,同时老 VNode
节点结束索引减 1,新 VNode
节点的开始索引加 1
如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
从旧的 VNode
为 key
值,对应 index
序列为 value
值的哈希表中找到与 newStartVnode
一致 key
的旧的 VNode
节点,再进行patchVnode
,同时将这个真实 dom
移动到 oldStartVnode
对应的真实 dom
的前面
调用 createElm
创建一个新的 dom
节点放到当前 newStartIdx
的位置
小结
当数据发生改变时,订阅者watcher
就会调用patch
给真实的DOM
打补丁
通过isSameVnode
进行判断,相同则调用patchVnode
方法
patchVnode
做了以下操作:
找到对应的真实dom
,称为el
如果都有都有文本节点且不相等,将el
文本节点设置为Vnode
的文本节点
如果oldVnode
有子节点而VNode
没有,则删除el
子节点
如果oldVnode
没有子节点而VNode
有,则将VNode
的子节点真实化后添加到el
如果两者都有子节点,则执行updateChildren
函数比较子节点
updateChildren
主要做了以下操作:
设置新旧VNode
的头尾指针
新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode
进行patch
重复流程、调用createElem
创建一个新节点,从哈希表寻找 key
一致的VNode
节点再分情况操作
v-if 和 v-show 的区别
v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)
vue-router 动态路由是什么
我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User
组件,对于所有 ID
各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router
的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果
const User = {
template: "<div>User</div>",
};
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头
{ path: "/user/:id", component: User },
],
});
复制代码
问题: vue-router
组件复用导致路由参数失效怎么办?
解决方法:
通过 watch
监听路由参数再发请求
watch: { //通过watch来监听路由变化
"$route": function(){
this.getData(this.$route.params.xxx);
}
}
复制代码
用 :key
来阻止“复用”
<router-view :key="$route.fullPath" />
复制代码
回答范例
很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由
例如,我们可能有一个 User
组件,它应该对所有用户进行渲染,但用户 ID
不同。在 Vue Router
中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User }
,其中:id
就是路径参数
路径参数 用冒号 :
表示。当一个路由被匹配时,它的 params
的值将在每个组件中以 this.$route.params
的形式暴露出来。
参数还可以有多个,例如/users/:username/posts/:postId
;除了 $route.params
之外,$route
对象还公开了其他有用的信息,如 $route.query
、$route.hash
等
什么是 mixin ?
Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。
如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
评论