keywords: 双向数据绑定、Object.defineProperty、 Dep、 Watcher、 Compiler、 keep-alive …
Frankly Speaking, 我并专攻vue技术栈,但是简单了学习了vue的核心原理和源码之后,感觉很有可借鉴的地方,那么很功夫一定不会白费。
vue2的代码结构:
- /compiler ⽬录是编译模版;vue这套静态模版分析是运行在浏览器端的还是打包的时候执行的?答案是都行,离线模式 和 在线模式;
- /core ⽬录是 Vue.js 的核⼼(是重点);
- /entries ⽬录是⽣产打包的⼊⼝;
- /platforms ⽬录是针对核⼼模块的 ‘平台’ 模块,platforms ⽬录下暂时只有 web ⽬录(在最新的开发⽬录⾥⾯已经有 weex ⽬录了)。web ⽬录下有对应的 /compiler、/runtime、/server、/util⽬录;
- /server ⽬录是处理服务端渲染;
- /sfc ⽬录处理单⽂件 .vue;
- /shared ⽬录提供全局⽤到的⼯具函数。
Vue.js 的组成是由 core + 对应的 ‘平台’ 补充代码构成 。
core中文件:
- compents 模板编译的代码
- global-api 最上层的⽂件接⼝
- instance ⽣命周期->init.js
- observer 数据收集与订阅
- util 常⽤⼯具⽅法类
- vdom 虚拟dom
vue的流程模型
vue的动态数据和静态模版之间是通过指令关联的。
静态模版,结合关联的动态数据,生成真实DOM的映射对象,VDOM,VDOM当然是由Vnode构成的。每一个Vnode都是经由render方法生成的。
那么它的核心流程就是,扫描模版中的动态数据,将这些数据的依赖关系收集起来,当数据被修改,查找这些依赖,根据依赖关系通知这些view层来触发render。
DEP —- 维护数据和view依赖的模块;
Watcher —- Watcher就是View层的关联对象,由此对象来调用更新方法;
下面我们就逐一审视下这些重要的模型角色,首先看看双向数据绑定。
vue2的双向数据绑定
核心API:Object.defineProperty()
let person = {}
let temp = null
Object.defineProperty(person, 'name', {
get: function () {
console.log('getting Name:>>', temp)
return temp
},
set: function (val) {
console.log('setting Name:>>', val)
temp = val
}
})
这样我们每次对o对象设置b属性的值,或者访问b比如o.b的时候,都会调用上述demo的get和set方法。
基于这个特性,我们就可以在这些属性的访问器中实现所谓‘双向数据绑定’了:当get方法触发的时候,就说明view层中有对这个数据的依赖,那么在此时收集这些依赖关系;当set方法触发的时候,需要通知所有我们此前收集的依赖关系,通知他们需要更新view了。
这个过程对应上面看到的流程模型中就是:
- Dep:get方法中维护的数据和依赖的关系,就像一个电话本一样;当某个数据的set方法被触发,需要在Dep中找到这数据的依赖关系,然后需要根据这些关系通知对应的view(指令)该更新了;
- Watcher:Watcher就是,当dep的更新的时候,调用每一个watcher的update的方法,其实就是再调用render。早期的vue大致是每一个指令对应一个watcher。具体的话大致是:vue1以指令为单位建立一一对应关系;vue2中 以component为维度对应一个watcher,watcher对应多个dep;
这不就是发布订阅,或者观察者模式么:一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。
Object.defineProperty存在问题
大家都知道,vue3中已经废弃了在Object.defineProperty的做法,而是采用了Proxy来拦截对象。
那么为什么要废弃呢,那肯定是Object.defineProperty存在问题了呗:
- 对象
最基本的Object.defineProperty只能监听一层数据,比如上述person demo,如果name的值是一个对象:
// 属性是数组的时候
person.name = { familyName: 'zhou', nickName : 'enen' };
// 当设置
person.name.familyName = 'Zhou';
这种情况下设置person.name.familyName = ‘Zhou’就不能触发set方法了,只能触发访问name的get方法:
// in console
getting Name:>> {familyName: "zhou", nickName: "enen"}
不过这个问题当然不是无解的,可以在定义的时候,遍历数据对象,如果对象的key不是基本类型,就再遍历这个key的数据,如此递归下去,就能对所有定义阶段的数据进行监听…
不过这里还有一个问题,就是无法对新数据监听:
person.age = 27;
- 数组
js中的数组,本质上其实也是有key和value的。只不过其key就是index。数组并不是不能监听啊,很多人说不能被监听,是不对的
那么从这个角度来看,数组也涉及上述问题,那么针对这个问题,在解法上来说也是一样的—递归遍历。但是数组当然还涉及其它的问题:
function defineObserverPerperty(data, key, value){
Object.defineProperty(data, key, {
get: function () {
console.log('getting Name:>>', value)
return value
},
set: function (new_val) {
console.log('setting Name:>>', new_val)
value = new_val
}
})
}
// 遍历对象
function mapObj(o) {
Object.keys(o).forEach( k => {
defineObserverPerperty(o, k, o[k]);
})
}
比如上面这段代码,我们去访问list[3]或者设置list[3]显然是不能触发监听,这是老问题:
const list = ['a', 'b', 'c'];
list[3] = 'x';
不过这个问题不至于导致重写数组,真正的问题在于多次触发,比如我们向首位塞入数据:
list.unshift('zero')
这种情况下,unshift本来数组的一个正常操作,我们期望是只涉及一个元素的读写,但是,控制台的打印明显是不符合预期的:
getting Name:>> c
getting Name:>> b
setting Name:>> b
getting Name:>> a
setting Name:>> a
setting Name:>> zero
因为对数组的操作,可能涉及到数组内部其它元素,其index对应的key的值发生变化。
这就要导致vue2对数组重写了。
Observer 处理成响应式数据
- class Observer
简单看下src/core/observer/index.js
这是这个模块的主入口。在index中导出了Observer类,重点关注下构造函数中的流程:构造函数中分别对数组和非数组进行了判断,非数组的话直接走walk方法,数组的话,最终统一走了observeArray方法。
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor(value: any) {
this.value = value
this.dep = new Dep(); // 依赖关系(电话本)
this.vmCount = 0
def(value, '__ob__', this); // 表示是响应式数据
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through all properties and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk(obj: Object) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
/**
* Observe a list of Array items.
*/
observeArray(items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
非数组处理
非数组处理被分发到oberver.walk中,我们看到walk中遍历非数组对象的key,把每一个key都调用了defineReactive:/**
* Define a reactive property on an Object.
*/
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
//使用数据的东西添加到dep
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 打电话通知,数据被修改
dep.notify();
}
})
}
数组处理
数组的在observer中其实是被重写了:
刚才在Observer的构造函数中存在这样的逻辑:if (Array.isArray(value)) {
// 这里判断的是浏览器支持不支持__proto__
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
}
hasProto是import的进来的一个工具方法,其目的就是在判断的是浏览器支持不支持proto。protoAugment、copyAugment这两个方法是重写原型方法, protoAugment的代码也没啥,protoAugment的代码就是把value的proto赋值为arrayMethods。
function protoAugment(target, src: Object) {
target.__proto__ = src
}
现在看下arrayMethods是什么东西:
/*
* not type checking this file because flow doesn't play well with
* dynamically accessing methods on Array prototype
*/
import {
/**
def其实就是defineProperty
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
*/
def
} from '../util/index'
const arrayProto = Array.prototype // Array构造函数的原型对象
export const arrayMethods = Object.create(arrayProto) // 新对象继承了数组构造方法的原型对象
// 对数组需要重写的方法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
//遍历上述方法并重写
methodsToPatch.forEach(function (method) {
const original = arrayProto[method] // 获取原来最初始的方法
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args) // 拿到结果 1213
const ob = this.__ob__ // 当前observer
let inserted // 新增项
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 新增索引,才会重新处理响应数据
if (inserted) ob.observeArray(inserted)
// 触发视图更新,打电话
ob.dep.notify();
return result
})
})
上面的代码中清晰可见vue重写了那些数组方法——methodsToPatch里面的值:’push’、’pop’、’shift’、’unshift’、’splice’, 、’sort’、’reverse’这些。
重写的方法其实逻辑是统一的:先调用原始方法完成操作后,再做处理,最后调用dep去通知。
先调用原始方法, 这里拿到result就是结果:
const result = original.apply(this, args)
判断这个方法是不是对数组产生了新增项,若有新增项赋值给inserted
if (inserted) ob.observeArray(inserted)
调用 ob.observeArray(inserted) ,是因为新增项可能也是数组或者对象这种非基本类型,如果是非基本类型,最终会newOb = new Observer(inserted),即对非基本类型设置监听。
- 由通知dep
ob.dep.notify();
可以看到,重写的这些数组的方法,其实是在原属的数组方法上做了一层拦截。
Dep
Dep就是维护依赖关系的‘电话本’。代码是在:src/core/observer/dep.js
这个方法是在响应式的过程中调用的,用户修改数据触发 setter 函数,函数的最后一行就是调用 dep.notify 去通知订阅者更新视图。
源码倒是很简单:
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
// 依赖关系集合,订阅者
subs: Array<Watcher>;
constructor() {
this.id = uid++
this.subs = [];
}
addSub(sub: Watcher) {
this.subs.push(sub)
}
removeSub(sub: Watcher) {
remove(this.subs, sub)
}
depend() {
if (Dep.target) {
//wathcer.appDep(dep)
Dep.target.addDep(this)
}
}
notify() {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()//Watcher
}
}
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []
export function pushTarget(target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
看下notify方法,其实就是遍历subs,分别调用subs的update方法。subs是 Array,这和我们之前说的,借助Watcher去更新数据的。另外,在更新之前还调用了subs.sort()做优先级的保证?但是这里只是更具id sort( (a, b) => a.id - b.id) ??
Watcher
上面简单的讨论中涉及了watcher的角色,现在可以更加仔细的审视下这个模型对象:
src/core/observer/watcher.js
Watcher是将模板和 Observer 对象结合在一起的纽带。
Watcher是订阅者模式中的订阅者。
Watcher是什么时候被构建的?
这个问题需要再从流程的角度先阐述下。As we know,在vue1时代,一个模版指令对应一个watcher,试想现在是程序初始化阶段,所有的模版要先经过编译(当然是要编译,否则v-model这种东西不编译,浏览器也不认识),编译的过程中,遇到需要建立数据绑定的变量,这时候就会new Watcher()。
new Watcher的话那就先看下 Watcher constructor:
// Watcher constructor
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm.push(this)
// options
if (options) {
// ...一通赋值的
} else {
// ...
}
// ...一通赋值的
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
// 这里是重点
this.value = this.lazy
? undefined
: this.get()
}
这一堆代码,真正核心的地方是最后一句,走到了:this.get():
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
// 1
pushTarget(this)
let value
const vm = this.vm
try {
// 2
// this.getter 可以理解为去vm对象上拿值
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
// 3
popTarget()
this.cleanupDeps()
}
return value
}
上面的代码实际上干了三件事:
- pushTarget: Dep的方法,实际上是向targetStack.push(target),同时设置标志位Dep.target = target;
- value = this.getter.call(vm, vm);可以理解为去vm对象上拿值,这个过程就要触发双向数据绑定中的的get方法了;这个时候再看下Observer中的get:
get: function reactiveGetter() {
//使用数据的东西添加到电话本
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
}
可以看到,这里检查了我们刚才设置的标志位置:Dep.target,这个静态属性其实是用来表征第一次时候vm中数据的;因为下一步就可能要改变这个标志了;
3. popTarget:改变这个标志Dep.target
watcher是怎样更新?待后文分解…
更新调度
watcher是view层到数据层的纽带,是view层沟通数据的信使,是数据层更新view的代理。前文说了,view层和watcher是一一对应的,或指令对应watcher,或组件对应watcher,而且分析了watcher是如何在响应式数据的建立中发挥作用的。
那么,当vm的数据要更新了,wathcer是怎样更新view层呢?
看下watcher.update
update() {
/* istanbul ignore else */
if (this.lazy) {// 懒
this.dirty = true
} else if (this.sync) {// 同步
this.run()
} else {// 批量跟新
queueWatcher(this)
}
}
忽略代码中lazy和sync,直接看queueWatcher这边, 代码来到了core / oberver / scheduler:
export function queueWatcher(watcher: Watcher) {
const id = watcher.id
// has 是这个东西:let has: { [key: number]: ?true } = {}
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)
}
}
}
这里其实涉及一个比较重要的更新机制。
我们知道,每一个watcher都和view层的指令或者component关联,会有一个id,假设现在出现了这样的情况:
for(let i = 0; i < 100; i+= 1 ) {
// 更新vm数据
app.data.name = `name: ${i}`
}
向上面一样:连续触发100次同一个数据更新,按照道理来说,watcher应该也会触发100次更新。但这样显然是没有必要的,这样会导致大量的不必要的DOM更新操作。
那么代码中这里的逻辑是怎样的呢?
简单先用文字描述下:watcher会调用scheduler的queueWatcher,在scheduler中会维护一个queue,里面放的都是即将被触发更新的watcher,另外,还有一个has,let has: { [key: number]: ?true } = {},其作用了set类似,用来标示当前的queue有没有某一个id的watcher。
如果没有,就把这个watcher加到queue中。
这个queue中的东西,就是每一次需要执行的更新。当具体执行的时候,watcher再去vm中得到那一时刻最新的值。
在这个简单原理描述中,还有一个点很重要,就是所谓“每一次需要执行的更新”,这个每一次?是怎么来的,答案很简单,就是异步任务机制,源码中这里还有一段降级处理,拉胯兜底的自然是setTimeout。
结合上面for(let i = 0; i < 100; i+= 1 ) 的这个例子,再看下整个过程,我们要更新的name,ok,和name关联的watcher假设其id是99,第一次,queue中没有id是99的watcher,ok,把watcher增加进queue来,for循环还在继续,但是真正需要执行watcher.update的时候,name已经是最终值了name: 99。
根据上面分析,结合代码,我们先看下:nextTick(flushSchedulerQueue):
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve
// 核心就是callbacks中加了一个函数
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
上面代码没啥好看,核心就是callbacks中加了一个函数,不过这个cb是啥?自然是nextTick(flushSchedulerQueue)传进去flushSchedulerQueue,flushSchedulerQueue就有必要看看:
function flushSchedulerQueue() {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
// ....环境 警告, 不看
}
}
// keep copies of post queues before resetting state
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
resetSchedulerState()
// call component updated and activated hooks
callActivatedHooks(activatedQueue)
callUpdatedHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}
上面这个函数,先上了搞了一堆自己的queue的排序,其实就是优先更新的策略,具体什么策略见注释中英文源代码吧😭~
后面部分的逻辑我也不懂,不过核心逻辑是,遍历queue中的watcher,而且调用了watcher.run():
理应当看看watcher.run的内容了:
run() {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
其核心,发现是调用了this.cb:this.cb.call(this.vm, value, oldValue)。
这是干嘛呢?
猜也知道,要更新view,触发render呗~
留着这个问题,我们看下view的编译和render过程。
view的编译和render
vue的编译可以分为离线编译和在线编译,编译的目的都是把.vue变成js,那么所谓离线编译就是在发布之前就编译好了(webpack + vue loader),而所谓的在线编译就是用户在前端执行代码,编译模版。
编译的过程一般是:1. 分析模版; 2. 生成树(AST); 3. 产生js;
编译的代码在compiler中,我们看下index.js:
export const createCompiler = createCompilerCreator(function baseCompile(
template: string,
options: CompilerOptions
): CompiledResult {
// 1. 转义html==>AST
const ast = parse(template.trim(), options);
// 不等于false,默认都优化
if (options.optimize !== false) {
// 2. 优化, 优化AST,标记静态节点
optimize(ast, options);
}
// 3. 把AST,转换为可以执行的代码
const code = generate(ast, options)
/**
* 生成with 方法,内部方法==>core/instance/render-helpers
*/
return {
ast,// 返回的AST
render: code.render,// 返回的render,执行生成VNode
staticRenderFns: code.staticRenderFns
}
})
- const ast = parse(template.trim(), options); 这一句话template就是我们的vue模版,比如
<button @click="show">show </button>
通过parse函数,生成AST。这里注意:
- AST的节点可不是VNode哈~AST是解析语法用的~
- 整个解析的过程vue2是用正则做的;
- parse的时候标记出静态节点(static),static就是没有那些vm数据的节点,这样标记出来以后,做diff的时候可以节省算力;
optimize(ast, options);
如何分辨出此节点是一个静态节点?
这里是一个分治(递归):自己和子节点都是静态节点,那就是静态节点。
- 把AST, 转换为可以执行的代码。这样生成了render函数的代码, 转化成的代码大致是这样的:
// render
with(this){
return _c('div', {
attrs: { 'id': 'app' },
}, [
(name)?_c('h2', [ _v(_s(name))]) : _e(),
// ....
])
}
with是什么作用,在代码块中强行引入对象追加到当前作用域链中。不过,通常认为with有比较严重的性能问题,浅谈 js 下 with 对性能的影响。
上面代码中的,with(this)就是vm中的data。
这些_c、_v之类的方法,就是创建Vnode的方法。
这个render,其实就是watch的cb,也就是说,watcher在遇到更新数据的时候,最终会调用render,生成Vnode。
vue2特点
优点:
- vue有静态编译优化,那些生成静态节点的过程,打标static的后面就不做diff了;
- 批处理机制,避免过多更新;
- 双向数据绑定,本身就是架构层面的一种优点,可以实现view层的靶向更新;
缺点:
keep-alive 是保存失活组件状态的东西,外头包裹下keep-alive,内部组件的状态就得以保存。
keep-alive 保存的是Vnode,所以,可能影响内存空间导致性能问题。
keep-alive 的代码在:components / keep-alive.js
所以这里牵扯LRU算法:移除最久没有用到的。LRU算法