Vuex 页面刷新数据丢失怎么解决
体验
可以从localStorage
中获取作为状态初始值:
const store = createStore({
state () {
return {
count: localStorage.getItem('count')
}
}
})
复制代码
业务代码中,提交修改状态同时保存最新值:虽说实现了,但是每次还要手动刷新localStorage
不太优雅
store.commit('increment')
localStorage.setItem('count', store.state.count)
复制代码
回答范例
vuex
只是在内存保存状态,刷新之后就会丢失,如果要持久化就要存起来
localStorage
就很合适,提交mutation
的时候同时存入localStorage
,store
中把值取出作为state
的初始值即可。
这里有两个问题,不是所有状态都需要持久化;如果需要保存的状态很多,编写的代码就不够优雅,每个提交的地方都要单独做保存处理。这里就可以利用vuex
提供的subscribe
方法做一个统一的处理。甚至可以封装一个vuex
插件以便复用。
类似的插件有vuex-persist
、vuex-persistedstate
,内部的实现就是通过订阅mutation
变化做统一处理,通过插件的选项控制哪些需要持久化
原理
可以看一下vuex-persist (opens new window)内部确实是利用subscribe
实现的
Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题 ?你能说说如下代码的实现原理么?
1)Vue 为什么要用 vm.$set() 解决对象新增属性不能响应的问题
Vue 使用了 Object.defineProperty 实现双向数据绑定
在初始化实例时对属性执行 getter/setter 转化
属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的(这也就造成了 Vue 无法检测到对象属性的添加或删除)
所以 Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)
2)接下来我们看看框架本身是如何实现的呢?
Vue 源码位置:vue/src/core/instance/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
// target 为数组
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// key 已经存在,直接修改属性值
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
// target 本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val
return val
}
// 对属性进行响应式处理
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
复制代码
我们阅读以上源码可知,vm.$set 的实现原理是:
如果目标是数组,直接使用数组的 splice 方法触发相应式;
如果目标是对象,会先判读属性是否存在、对象是否是响应式,
最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理
defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法
Vue 为什么没有类似于 React 中 shouldComponentUpdate 的生命周期?
考点: Vue 的变化侦测原理
前置知识: 依赖收集、虚拟 DOM、响应式系统
根本原因是 Vue 与 React 的变化侦测方式有所不同
React 是 pull 的方式侦测变化,当 React 知道发生变化后,会使用 Virtual Dom Diff 进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要用 shouldComponentUpdate 进行手动操作来减少 diff,从而提高程序整体的性能.
Vue 是 pull+push 的方式侦测变化的,在一开始就知道那个组件发生了变化,因此在 push 的阶段并不需要手动控制 diff,而组件内部采用的 diff 方式实际上是可以引入类似于 shouldComponentUpdate 相关生命周期的,但是通常合理大小的组件不会有过量的 diff,手动优化的价值有限,因此目前 Vue 并没有考虑引入 shouldComponentUpdate 这种手动优化的生命周期.
vue 中使用了哪些设计模式
1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
组件通信
组件通信的方式如下:
(1) props / $emit
父组件通过props
向子组件传递数据,子组件通过$emit
和父组件通信
1. 父组件向子组件传值
props
只能是父组件向子组件进行传值,props
使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。
props
可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。
props
属性名规则:若在props
中使用驼峰形式,模板中需要使用短横线的形式
// 父组件
<template>
<div id="father">
<son :msg="msgData" :fn="myFunction"></son>
</div>
</template>
<script>
import son from "./son.vue";
export default {
name: father,
data() {
msgData: "父组件数据";
},
methods: {
myFunction() {
console.log("vue");
},
},
components: { son },
};
</script>
复制代码
// 子组件
<template>
<div id="son">
<p>{{ msg }}</p>
<button @click="fn">按钮</button>
</div>
</template>
<script>
export default { name: "son", props: ["msg", "fn"] };
</script>
复制代码
2. 子组件向父组件传值
// 父组件
<template>
<div class="section">
<com-article
:articles="articleList"
@onEmitIndex="onEmitIndex"
></com-article>
<p>{{ currentIndex }}</p>
</div>
</template>
<script>
import comArticle from "./test/article.vue";
export default {
name: "comArticle",
components: { comArticle },
data() {
return { currentIndex: -1, articleList: ["红楼梦", "西游记", "三国演义"] };
},
methods: {
onEmitIndex(idx) {
this.currentIndex = idx;
},
},
};
</script>
复制代码
//子组件
<template>
<div>
<div
v-for="(item, index) in articles"
:key="index"
@click="emitIndex(index)"
>
{{ item }}
</div>
</div>
</template>
<script>
export default {
props: ["articles"],
methods: {
emitIndex(index) {
this.$emit("onEmitIndex", index); // 触发父组件的方法,并传递参数index
},
},
};
</script>
复制代码
(2)eventBus 事件总线($emit / $on
)
eventBus
事件总线适用于父子组件、非父子组件等之间的通信,使用步骤如下: (1)创建事件中心管理组件之间的通信
// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()
复制代码
(2)发送事件 假设有两个兄弟组件firstCom
和secondCom
:
<template>
<div>
<first-com></first-com>
<second-com></second-com>
</div>
</template>
<script>
import firstCom from "./firstCom.vue";
import secondCom from "./secondCom.vue";
export default { components: { firstCom, secondCom } };
</script>
复制代码
在firstCom
组件中发送事件:
<template>
<div>
<button @click="add">加法</button>
</div>
</template>
<script>
import { EventBus } from "./event-bus.js"; // 引入事件中心
export default {
data() {
return { num: 0 };
},
methods: {
add() {
EventBus.$emit("addition", { num: this.num++ });
},
},
};
</script>
复制代码
(3)接收事件 在secondCom
组件中发送事件:
<template>
<div>求和: {{ count }}</div>
</template>
<script>
import { EventBus } from "./event-bus.js";
export default {
data() {
return { count: 0 };
},
mounted() {
EventBus.$on("addition", (param) => {
this.count = this.count + param.num;
});
},
};
</script>
复制代码
在上述代码中,这就相当于将num
值存贮在了事件总线中,在其他组件中可以直接访问。事件总线就相当于一个桥梁,不用组件通过它来通信。
虽然看起来比较简单,但是这种方法也有不变之处,如果项目过大,使用这种方式进行通信,后期维护起来会很困难。
(3)依赖注入(provide / inject)
这种方式就是 Vue 中的依赖注入,该方法用于父子组件之间的通信。当然这里所说的父子不一定是真正的父子,也可以是祖孙组件,在层数很深的情况下,可以使用这种方法来进行传值。就不用一层一层的传递了。
provide / inject
是 Vue 提供的两个钩子,和data
、methods
是同级的。并且provide
的书写形式和data
一样。
provide
钩子用来发送数据或方法
inject
钩子用来接收数据或方法
在父组件中:
provide() {
return {
num: this.num
};
}
复制代码
在子组件中:
还可以这样写,这样写就可以访问父组件中的所有属性:
provide() {
return {
app: this
};
}
data() {
return {
num: 1
};
}
inject: ['app']
console.log(this.app.num)
复制代码
注意: 依赖注入所提供的属性是非响应式的。
(3)ref / $refs
这种方式也是实现父子组件之间的通信。
ref
: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。
在子组件中:
export default {
data () {
return {
name: 'JavaScript'
}
},
methods: {
sayHello () {
console.log('hello')
}
}
}
复制代码
在父组件中:
<template>
<child ref="child"></component-a>
</template>
<script>
import child from "./child.vue";
export default {
components: { child },
mounted() {
console.log(this.$refs.child.name); // JavaScript
this.$refs.child.sayHello(); // hello
},
};
</script>
复制代码
(4)$parent / $children
在子组件中:
<template>
<div>
<span>{{ message }}</span>
<p>获取父组件的值为: {{ parentVal }}</p>
</div>
</template>
<script>
export default {
data() {
return { message: "Vue" };
},
computed: {
parentVal() {
return this.$parent.msg;
},
},
};
</script>
复制代码
在父组件中:
// 父组件中
<template>
<div class="hello_world">
<div>{{ msg }}</div>
<child></child>
<button @click="change">点击改变子组件值</button>
</div>
</template>
<script>
import child from "./child.vue";
export default {
components: { child },
data() {
return { msg: "Welcome" };
},
methods: {
change() {
// 获取到子组件
this.$children[0].message = "JavaScript";
},
},
};
</script>
复制代码
在上面的代码中,子组件获取到了父组件的parentVal
值,父组件改变了子组件中message
的值。 需要注意:
通过$parent
访问到的是上一级父组件的实例,可以使用$root
来访问根组件的实例
在组件中使用$children
拿到的是所有的子组件的实例,它是一个数组,并且是无序的
在根组件#app
上拿$parent
得到的是new Vue()
的实例,在这实例上再拿$parent
得到的是undefined
,而在最底层的子组件拿$children
是个空数组
$children
的值是数组,而$parent
是个对象
(5)$attrs / $listeners
考虑一种场景,如果 A 是 B 组件的父组件,B 是 C 组件的父组件。如果想要组件 A 给组件 C 传递数据,这种隔代的数据,该使用哪种方式呢?
如果是用props/$emit
来一级一级的传递,确实可以完成,但是比较复杂;如果使用事件总线,在多人开发或者项目较大的时候,维护起来很麻烦;如果使用 Vuex,的确也可以,但是如果仅仅是传递数据,那可能就有点浪费了。
针对上述情况,Vue 引入了$attrs / $listeners
,实现组件之间的跨代通信。
先来看一下inheritAttrs
,它的默认值 true,继承所有的父组件属性除props
之外的所有属性;inheritAttrs:false
只继承 class 属性 。
A 组件(APP.vue
):
<template>
<div id="app">
//此处监听了两个事件,可以在B组件或者C组件中直接触发
<child1
:p-child1="child1"
:p-child2="child2"
@test1="onTest1"
@test2="onTest2"
></child1>
</div>
</template>
<script>
import Child1 from "./Child1.vue";
export default {
components: { Child1 },
methods: {
onTest1() {
console.log("test1 running");
},
onTest2() {
console.log("test2 running");
},
},
};
</script>
复制代码
B 组件(Child1.vue
):
<template>
<div class="child-1">
<p>props: {{ pChild1 }}</p>
<p>$attrs: {{ $attrs }}</p>
<child2 v-bind="$attrs" v-on="$listeners"></child2>
</div>
</template>
<script>
import Child2 from "./Child2.vue";
export default {
props: ["pChild1"],
components: { Child2 },
inheritAttrs: false,
mounted() {
this.$emit("test1"); // 触发APP.vue中的test1方法
},
};
</script>
复制代码
C 组件 (Child2.vue
):
<template>
<div class="child-2">
<p>props: {{ pChild2 }}</p>
<p>$attrs: {{ $attrs }}</p>
</div>
</template>
<script>
export default {
props: ["pChild2"],
inheritAttrs: false,
mounted() {
this.$emit("test2"); // 触发APP.vue中的test2方法
},
};
</script>
复制代码
在上述代码中:
(6)总结
(1)父子组件间通信
子组件通过 props 属性来接受父组件的数据,然后父组件在子组件上注册监听事件,子组件通过 emit 触发事件来向父组件发送数据。
通过 ref 属性给子组件设置一个名字。父组件通过 $refs
组件名来获得子组件,子组件通过 $parent
获得父组件,这样也可以实现通信。
使用 provide/inject,在父组件中通过 provide 提供变量,在子组件中通过 inject 来将变量注入到组件中。不论子组件有多深,只要调用了 inject 那么就可以注入 provide 中的数据。
(2)兄弟组件间通信
(3)任意组件之间
如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候采用上面这一些方法可能不利于项目的维护。这个时候可以使用 vuex ,vuex 的思想就是将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。
了解 nextTick 吗?
异步方法,异步渲染最后一步,与 JS 事件循环联系紧密。主要使用了宏任务微任务(setTimeout
、promise
那些),定义了一个异步方法,多次调用nextTick
会将方法存入队列,通过异步方法清空当前队列。
参考 前端进阶面试题详细解答
Vue 的性能优化有哪些
(1)编码阶段
尽量减少 data 中的数据,data 中的数据都会增加 getter 和 setter,会收集对应的 watcher
v-if 和 v-for 不能连用
如果需要使用 v-for 给每项元素绑定事件时使用事件代理
SPA 页面采用 keep-alive 缓存组件
在更多的情况下,使用 v-if 替代 v-show
key 保证唯一
使用路由懒加载、异步组件
防抖、节流
第三方模块按需导入
长列表滚动到可视区域动态加载
图片懒加载
(2)SEO 优化
(3)打包优化
(4)用户体验
什么是 mixin ?
Mixin 使我们能够为 Vue 组件编写可插拔和可重用的功能。
如果希望在多个组件之间重用一组组件选项,例如生命周期 hook、 方法等,则可以将其编写为 mixin,并在组件中简单的引用它。
然后将 mixin 的内容合并到组件中。如果你要在 mixin 中定义生命周期 hook,那么它在执行时将优化于组件自已的 hook。
Vue.mixin 的使用场景和原理
在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过 Vue
的 mixin
功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions
方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”;如果混入的数据和本身组件的数据冲突,会以组件的数据为准
mixin
有很多缺陷如:命名冲突、依赖问题、数据来源问题
基本使用
<script>
// Vue.options
Vue.mixin({ // 如果他是对象 每个组件都用mixin里的对象进行合并
data(){
return {a: 1,b: 2}
}
});
// Vue.extend
Vue.component('my',{ // 组件必须是函数 Vue.extend => render(xxx)
data(){
return {x:1}
}
})
// 没有 new 没有实例 _init()
// const vm = this
new Vue({
el:'#app',
data(){ // 根可以不是函数
return {c:3}
}
})
</script>
复制代码
相关源码
export default function initMixin(Vue){
Vue.mixin = function (mixin) {
// 合并对象
this.options=mergeOptions(this.options,mixin)
};
}
};
// src/util/index.js
// 定义生命周期
export const LIFECYCLE_HOOKS = [
"beforeCreate",
"created",
"beforeMount",
"mounted",
"beforeUpdate",
"updated",
"beforeDestroy",
"destroyed",
];
// 合并策略
const strats = {};
// mixin核心方法
export function mergeOptions(parent, child) {
const options = {};
// 遍历父亲
for (let k in parent) {
mergeFiled(k);
}
// 父亲没有 儿子有
for (let k in child) {
if (!parent.hasOwnProperty(k)) {
mergeFiled(k);
}
}
//真正合并字段方法
function mergeFiled(k) {
// strats合并策略
if (strats[k]) {
options[k] = strats[k](parent[k], child[k]);
} else {
// 默认策略
options[k] = child[k] ? child[k] : parent[k];
}
}
return options;
}
复制代码
你有对 Vue 项目进行哪些优化?
(1)代码层面的优化
(2)Webpack 层面的优化
Webpack 对图片进行压缩
减少 ES6 转为 ES5 的冗余代码
提取公共代码
模板预编译
提取组件的 CSS
优化 SourceMap
构建结果输出分析
Vue 项目的编译优化
(3)基础的 Web 技术的优化
nextTick 在哪里使用?原理是?
nextTick
中的回调是在下次 DOM
更新循环结束之后执行延迟回调,用于获得更新后的 DOM
在修改数据之后立即使用这个方法,获取更新后的 DOM
主要思路就是采用微任务优先
的方式调用异步方法去执行 nextTick
包装的方法
nextTick
方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用 nextTick
会将方法存入队列中,通过这个异步方法清空当前队列。所以这个 nextTick
方法就是异步方法
根据执行环境分别尝试采用
先采用Promise
Promise
不支持,再采用MutationObserver
MutationObserver
不支持,再采用setImmediate
如果以上都不行则采用setTimeout
最后执行flushCallbacks
,把callbacks
里面的数据依次执行
回答范例
nextTick
中的回调是在下次 DOM
更新循环结束之后执行延迟回调,用于获得更新后的 DOM
Vue
有个异步更新策略,意思是如果数据变化,Vue
不会立刻更新 DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在 DOM 上,此时如果想要获取更新后的 DOM 状态,就需要使用nextTick
开发时,有两个场景我们会用到nextTick
nextTick
签名如下:function nextTick(callback?: () => void): Promise<void>
所以我们只需要在传入的回调函数中访问最新 DOM 状态即可,或者我们可以await nextTick()
方法返回的Promise
之后做这件事
在Vue
内部,nextTick
之所以能够让我们看到 DOM 更新后的结果,是因为我们传入的callback
会被添加到队列刷新函数(flushSchedulerQueue
)的后面,这样等队列内部的更新函数都执行完毕,所有 DOM 操作也就结束了,callback
自然能够获取到最新的 DOM 值
基本使用
const vm = new Vue({
el: '#app',
data() {
return { a: 1 }
}
});
// vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue]
// console.log(vm.$el.innerHTML)
// })
// 是将内容维护到一个数组里,最终按照顺序顺序。 第一次会开启一个异步任务
vm.a = 'test'; // 修改了数据后并不会马上更新视图
vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue]
console.log(vm.$el.innerHTML)
})
// nextTick中的方法会被放到 更新页面watcher的后面去
复制代码
相关代码如下
// src/core/utils/nextTick
let callbacks = [];
let pending = false;
function flushCallbacks() {
pending = false; //把标志还原为false
// 依次执行回调
for (let i = 0; i < callbacks.length; i++) {
callbacks[i]();
}
}
let timerFunc; //定义异步方法 采用优雅降级
if (typeof Promise !== "undefined") {
// 如果支持promise
const p = Promise.resolve();
timerFunc = () => {
p.then(flushCallbacks);
};
} else if (typeof MutationObserver !== "undefined") {
// MutationObserver 主要是监听dom变化 也是一个异步方法
let counter = 1;
const observer = new MutationObserver(flushCallbacks);
const textNode = document.createTextNode(String(counter));
observer.observe(textNode, {
characterData: true,
});
timerFunc = () => {
counter = (counter + 1) % 2;
textNode.data = String(counter);
};
} else if (typeof setImmediate !== "undefined") {
// 如果前面都不支持 判断setImmediate
timerFunc = () => {
setImmediate(flushCallbacks);
};
} else {
// 最后降级采用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0);
};
}
export function nextTick(cb) {
// 除了渲染watcher 还有用户自己手动调用的nextTick 一起被收集到数组
callbacks.push(cb);
if (!pending) {
// 如果多次调用nextTick 只会执行一次异步 等异步队列清空之后再把标志变为false
pending = true;
timerFunc();
}
}
复制代码
数据更新的时候内部会调用nextTick
// src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
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]
/**
* vm.a = 'test'; // 修改了数据后并不会马上更新视图
vm.$nextTick(() => {// [fn,更新]
console.log(vm.$el.innerHTML)
})
*/
nextTick(flushSchedulerQueue)
}
}
}
复制代码
Vue 路由 hash 模式和 history 模式
1. hash
模式
早期的前端路由的实现就是基于 location.hash
来实现的。其实现原理很简单,location.hash
的值就是 URL
中 #
后面的内容。比如下面这个网站,它的 location.hash
的值为 '#search'
https://interview2.poetries.top#search
复制代码
hash 路由模式的实现主要是基于下面几个特性
URL
中 hash
值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash
部分不会被发送;
hash
值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash
的切换;
可以通过 a
标签,并设置 href
属性,当用户点击这个标签后,URL
的 hash
值会发生改变;或者使用 JavaScript
来对 loaction.hash
进行赋值,改变 URL
的 hash
值;
我们可以使用 hashchange
事件来监听 hash
值的变化,从而对页面进行跳转(渲染)
window.addEventListener("hashchange", funcRef, false);
复制代码
每一次改变 hash
(window.location.hash
),都会在浏览器的访问历史中增加一个记录利用 hash
的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了
特点 :兼容性好但是不美观
2. history
模式
history
采用HTML5
的新特性;且提供了两个新方法: pushState()
, replaceState()
可以对浏览器历史记录栈进行修改,以及popState
事件的监听到状态变更
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
复制代码
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL
改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
history 路由模式的实现主要基于存在下面几个特性:
pushState
和 repalceState
两个 API
来操作实现 URL
的变化 ;
我们可以使用 popstate
事件来监听 url
的变化,从而对页面进行跳转(渲染);
history.pushState()
或 history.replaceState()
不会触发 popstate
事件,这时我们需要手动触发页面跳转(渲染)。
特点 :虽然美观,但是刷新会出现 404
需要后端进行配置
vue 中使用了哪些设计模式
1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
...其他模式欢迎补充
Proxy 与 Object.defineProperty 优劣对比
Proxy 的优势如下:
Proxy 可以直接监听对象而非属性;
Proxy 可以直接监听数组的变化;
Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利;
Object.defineProperty 的优势如下:
Vue 为什么需要虚拟 DOM?优缺点有哪些
由于在浏览器中操作 DOM
是很昂贵的。频繁的操作 DOM
,会产生一定的性能问题。这就是虚拟 Dom
的产生原因。Vue2
的 Virtual DOM
借鉴了开源库 snabbdom
的实现。Virtual DOM
本质就是用一个原生的 JS
对象去描述一个 DOM
节点,是对真实 DOM
的一层抽象
优点:
保证性能下限 : 框架的虚拟 DOM
需要适配任何上层 API
可能产生的操作,它的一些 DOM
操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM
操作性能要好很多,因此框架的虚拟 DOM
至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
无需手动操作 DOM : 我们不再需要手动去操作 DOM
,只需要写好 View-Model
的代码逻辑,框架会根据虚拟 DOM
和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
跨平台 : 虚拟 DOM
本质上是 JavaScript
对象,而 DOM
与平台强相关,相比之下虚拟 DOM
可以进行更方便地跨平台操作,例如服务器渲染、weex
开发等等。
缺点:
虚拟 DOM 实现原理?
虚拟 DOM
的实现原理主要包括以下 3
部分:
用 JavaScript
对象模拟真实 DOM
树,对真实 DOM
进行抽象;
diff
算法 — 比较两棵虚拟 DOM
树的差异;
pach
算法 — 将两个虚拟 DOM
对象的差异应用到真正的 DOM
树。
说说你对虚拟 DOM 的理解?回答范例
思路
vdom
是什么
引入vdom
的好处
vdom
如何生成,又如何成为dom
在后续的diff
中的作用
回答范例
虚拟dom
顾名思义就是虚拟的dom
对象,它本身就是一个 JavaScript
对象,只不过它是通过不同的属性去描述一个视图结构
通过引入vdom
我们可以获得如下好处:
将真实元素节点抽象成 VNode
,有效减少直接操作 dom
次数,从而提高程序性能
直接操作 dom
是有限制的,比如:diff
、clone
等操作,一个真实元素上有许多的内容,如果直接对其进行 diff
操作,会去额外 diff
一些没有必要的内容;同样的,如果需要进行 clone
那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript
对象上,那么就会变得简单了
操作 dom
是比较昂贵的操作,频繁的dom
操作容易引起页面的重绘和回流,但是通过抽象 VNode
进行中间处理,可以有效减少直接操作dom
的次数,从而减少页面重绘和回流
方便实现跨平台
同一 VNode
节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom
元素节点,渲染在 Native( iOS、Android)
变为对应的控件、可以实现 SSR
、渲染到 WebGL
中等等
Vue3
中允许开发者基于 VNode
实现自定义渲染器(renderer
),以便于针对不同平台进行渲染
vdom
如何生成?在 vue 中我们常常会为组件编写模板 - template
, 这个模板会被编译器 - compiler
编译为渲染函数,在接下来的挂载(mount
)过程中会调用render
函数,返回的对象就是虚拟dom
。但它们还不是真正的dom
,所以会在后续的patch
过程中进一步转化为dom
。
挂载过程结束后,vue
程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render
,此时就会生成新的vdom
,和上一次的渲染结果diff
就能得到变化的地方,从而转换为最小量的dom
操作,高效更新视图
为什么要用 vdom?案例解析
现在有一个场景,实现以下需求:
[
{ name: "张三", age: "20", address: "北京"},
{ name: "李四", age: "21", address: "武汉"},
{ name: "王五", age: "22", address: "杭州"},
]
复制代码
将该数据展示成一个表格,并且随便修改一个信息,表格也跟着修改。 用 jQuery 实现如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">改变</button>
<script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
<script>
const data = [{
name: "张三",
age: "20",
address: "北京"
},
{
name: "李四",
age: "21",
address: "武汉"
},
{
name: "王五",
age: "22",
address: "杭州"
},
];
//渲染函数
function render(data) {
const $container = $('#container');
$container.html('');
const $table = $('<table>');
// 重绘一次
$table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'));
data.forEach(item => {
//每次进入都重绘
$table.append($(`<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`))
})
$container.append($table);
}
$('#btn-change').click(function () {
data[1].age = 30;
data[2].address = '深圳';
render(data);
});
</script>
</body>
</html>
复制代码
这样点击按钮,会有相应的视图变化,但是你审查以下元素,每次改动之后,table
标签都得重新创建,也就是说table
下面的每一个栏目,不管是数据是否和原来一样,都得重新渲染,这并不是理想中的情况,当其中的一栏数据和原来一样,我们希望这一栏不要重新渲染,因为DOM
重绘相当消耗浏览器性能。
因此我们采用 JS 对象模拟的方法,将DOM
的比对操作放在JS
层,减少浏览器不必要的重绘,提高效率。
当然有人说虚拟 DOM 并不比真实的DOM
快,其实也是有道理的。当上述table
中的每一条数据都改变时,显然真实的DOM
操作更快,因为虚拟DOM
还存在js
中diff
算法的比对过程。所以,上述性能优势仅仅适用于大量数据的渲染并且改变的数据只是一小部分的情况。
如下DOM
结构:
<ul id="list">
<li class="item">Item1</li>
<li class="item">Item2</li>
</ul>
复制代码
映射成虚拟DOM
就是这样:
{
tag: "ul",
attrs: {
id: "list"
},
children: [
{
tag: "li",
attrs: { className: "item" },
children: ["Item1"]
}, {
tag: "li",
attrs: { className: "item" },
children: ["Item2"]
}
]
}
复制代码
使用 snabbdom 实现 vdom
这是一个简易的实现vdom
功能的库,相比vue
、react
,对于vdom
这块更加简易,适合我们学习vdom
。vdom
里面有两个核心的api
,一个是h
函数,一个是patch
函数,前者用来生成vdom
对象,后者的功能在于做虚拟dom
的比对和将vdom
挂载到真实DOM
上
简单介绍一下这两个函数的用法:
h('标签名', {属性}, [子元素])
h('标签名', {属性}, [文本])
patch(container, vnode) // container为容器DOM元素
patch(vnode, newVnode)
复制代码
现在我们就来用snabbdom
重写一下刚才的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">改变</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script>
let snabbdom = window.snabbdom;
// 定义patch
let patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
]);
//定义h
let h = snabbdom.h;
const data = [{
name: "张三",
age: "20",
address: "北京"
},
{
name: "李四",
age: "21",
address: "武汉"
},
{
name: "王五",
age: "22",
address: "杭州"
},
];
data.unshift({name: "姓名", age: "年龄", address: "地址"});
let container = document.getElementById('container');
let vnode;
const render = (data) => {
let newVnode = h('table', {}, data.map(item => {
let tds = [];
for(let i in item) {
if(item.hasOwnProperty(i)) {
tds.push(h('td', {}, item[i] + ''));
}
}
return h('tr', {}, tds);
}));
if(vnode) {
patch(vnode, newVnode);
} else {
patch(container, newVnode);
}
vnode = newVnode;
}
render(data);
let btnChnage = document.getElementById('btn-change');
btnChnage.addEventListener('click', function() {
data[1].age = 30;
data[2].address = "深圳";
//re-render
render(data);
})
</script>
</body>
</html>
复制代码
你会发现, 只有改变的栏目才闪烁,也就是进行重绘 ,数据没有改变的栏目还是保持原样,这样就大大节省了浏览器重新渲染的开销
vue 中使用h函数
生成虚拟DOM
返回
const vm = new Vue({
el: '#app',
data: {
user: {name:'poetry'}
},
render(h){
// h()
// h(App)
// h('div',[])
let vnode = h('div',{},'hello world');
return vnode
}
});
复制代码
双向绑定的原理是什么
我们都知道 Vue
是数据双向绑定的框架,双向绑定由三个重要部分构成
而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM
这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
理解 ViewModel
它的主要职责就是:
当然,它还有两个主要部分组成
监听器(Observer
):对所有数据的属性进行监听
解析器(Compiler
):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
v-show 与 v-if 有什么区别?
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
实现双向绑定
我们还是以Vue
为例,先来看看Vue
中的双向绑定流程是什么的
new Vue()
首先执行初始化,对data
执行响应化处理,这个过程发生Observe
中
同时对模板执行编译,找到其中动态绑定的数据,从data
中获取并初始化视图,这个过程发生在Compile
中
同时定义⼀个更新函数和Watcher
,将来对应数据变化时Watcher
会调用更新函数
由于data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep
来管理多个Watcher
将来 data 中数据⼀旦发生变化,会首先找到对应的Dep
,通知所有Watcher
执行更新函数
流程图如下:
先来一个构造函数:执行初始化,对data
执行响应化处理
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 对data选项做响应式处理
observe(this.$data);
// 代理data到vm上
proxy(this);
// 执行编译
new Compile(options.el, this);
}
}
复制代码
对data
选项执行响应化具体操作
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return;
}
new Observer(obj);
}
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
复制代码
编译Compile
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
复制代码
依赖收集
视图中会用到data
中某key
,这称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知
实现思路
defineReactive
时为每⼀个key
创建⼀个Dep
实例
初始化视图时读取某个key
,例如name1
,创建⼀个watcher1
由于触发name1
的getter
方法,便将watcher1
添加到name1
对应的Dep
中
当name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新
// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
// 置空
Dep.target = null
}
// 未来执行dom更新函数,由dep调用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
复制代码
声明Dep
class Dep {
constructor() {
this.deps = []; // 依赖管理
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
复制代码
创建watcher
时触发getter
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
复制代码
依赖收集,创建Dep
实例
function defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例
return val;
},
set(newVal) {
if (newVal === val) return;
dep.notify(); // 通知dep执行更新方法
},
});
}
复制代码
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
,如果超过了,则把第一个缓存组件删掉
computed 和 watch 的区别和运用的场景?
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
评论