前言
通过手写Vue2源码,更深入了解Vue;
在项目开发过程,一步一步实现Vue核心功能,我会将不同功能放到不同分支,方便查阅;
另外我会编写一些开发文档,阐述编码细节及实现思路;
源码地址:手写Vue2源码
使用场景
频繁的组件切换,或者路由切换,需要对组件进行缓存,避免组件的重复创建。
<keep-alive :include='whiteList' :exclude='blackList' :max='count'>
<component :is='component'></component>
</keep-alive>
<keep-alive :include='whiteList' :exclude='blackList' :max='count'>
<router-view></router-view>
</keep-alive>
keep-alive注册流程
在定义Vue时,调用initGlobalAPI(Vue)
:
// src/core/index.js
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
initGlobalAPI(Vue)
export default Vue
在initGlobalAPI()
中定义了很多全局的方法和数据(例如:Vue.options
、Vue.config
、Vue.util
、Vue.set
、Vue.delete
、Vue.nextTick
、Vue.observable
、Vue.use
、Vue.mixin
、Vue.extend
等),并注册了全局组件 keep-alive
// src/core/global-api/index.js
import builtInComponents from '../components/index'
import {
extend
} from '../util/index'
export function initGlobalAPI (Vue: GlobalAPI) {
// ...略
// extend定义在 src/shared/util.js中
// 作用是将builtInComponents合并到Vue.options.components
extend(Vue.options.components, builtInComponents)
}
在builtInComponents中引入需要定义在全局的组件:
// src/core/components/index.js
import KeepAlive from './keep-alive'
export default {
KeepAlive
}
小结:
keep-alive
是一个全局组件- 在Vue构造函数的
initGlobalAPI(Vue)
方法中,将keep-alive
组件置入Vue.options.components
中; - 组件实例化时会将
Vue.options
与组件实例的vm.options
合并;components的合并采用的是原型继承,所以可以在任意组件中使用keep-alive
keep-alive的实现原理
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 不会放到对应的lifecycle,也不会创建dom
props: {
include, // 白名单
exclude, // 黑名单
max: [String, Number] // 缓存的最大个数
},
methods: {
// 往cache中缓存vnode,往keys中缓存vnode的key
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache)
// 超出长度则删除第一项
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
created () {
this.cache = Object.create(null) // 缓存组件vnode的对象
this.keys = [] // 缓存的key列表
},
// 删除缓存的cache列表和keys列表
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
// 监控include/exclude,动态更新缓存列表
mounted () {
this.cacheVNode() // 缓存组件
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
updated () {
this.cacheVNode()
},
render () {
const slot = this.$slots.default // 获取包裹的默认插槽
const vnode: VNode = getFirstComponentChild(slot) // 获取第一个组件
const componentOptions = vnode && vnode.componentOptions // 获取组件的componentOptions:{Ctor,children,tag,name...}
if (componentOptions) {
const name = getComponentName(componentOptions)
const { include, exclude } = this
// 如果不走缓存
if (
// not included 不包含
(include && (!name || !matches(include, name))) ||
// excluded 排除
(exclude && name && matches(exclude, name))
) {
return vnode // 不需要缓存则直接返回虚拟节点即可
}
const { cache, keys } = this
const key = vnode.key == null
// same constructor may get registered as different local components
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
// 如果在cache中缓存过该组件
if (cache[key]) {
// 通过key,找到缓存,获取实例
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key) // 把keys数组里面的key删掉
keys.push(key) // 把它放在数组末尾
} else {
cache[key] = vnode; // 没找到就换存下来
keys.push(key); // 把它放在数组末尾
// prune oldest entry // 如果超过最大值就把数组第0项删掉
if (this.max && keys.length > parseInt(this.max)) {
// LRU算法:删除缓存第0项的组件和key
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
vnode.data.keepAlive = true // 标记虚拟节点已经被缓存
}
// 返回虚拟节点
return vnode || (slot && slot[0])
}
}
小结:
- keep-alive缓存的是组件的vnode
- 通过 LRU 算法对组件进行缓存
缓存中的LRU算法
- LRU全称:Least Recently Used
- 将新数据从尾部插入到
this.keys
中 - 每当缓存命中时(即缓存数据被访问时),则将其移到this.keys尾部
- 当
this.keys
满的时候,将头部数据丢弃
keep-alive
中的LRU算法:
- 当没有缓存时,将vnode的key推进
this.keys
中的末尾,并在cache中设置vnode.key
属性及其对应的vnode - 设置缓存时,如果长度大于max,则删除
this.keys
中的第0项,并删除cache中key对应的vnode - 当有缓存时,获取缓存的数据,并将该缓存组件的key放到
this.keys
中的末尾
keep-alive组件的渲染
首次渲染
- keep-alive组件执行
render()
函数,可以看到首次执行render()
函数的时候,没有缓存任何子组件,keep-alive缓存的子组件的vnode.componentInstance
为null,且vnode.data.keepAlive
为true。 - keep-alive组件的
patch()
过程,对于内部缓存的组件会调用createComponent方法,实际上执行的是缓存组件的init()
函数function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { // 初次渲染时vnode.componentInstance为null,则isReactivated为false const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) } // after calling the init hook, if the vnode is a child component // it should've created a child instance and mounted it. the child // component also has set the placeholder vnode's elm. // in that case we can just return the element and be done. if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
- 我们来看下
init()
函数,在init()
函数里面会对vnode.componentInstance
和vnode.data.keepAlive
的值做判断。由第一步的值可知,首次渲染的时候会走else里面的逻辑,执行createComponentInstanceForVnode()
创建新的子组件,接下来会去执行子组件的$mount
过程(会调用正常组件的所有生命周期),去创建子组件的虚拟Vnode。const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, // prepatch // insert // destroy }
非首次渲染
会触发keep-alive的缓存机制
- 触发keep-alive的
render()
函数,此时会命中缓存,所以keep-alive缓存的子组件的vnode.componentInstance
为组件A,且vnode.data.keepAlive
为true。 - 执行keep-alive组件的
patch()
过程,实际上执行的是init()
函数,这时vnode.componentInstance
和vnode.data.keepAlive
都为true,所以不会执行createComponentInstanceForVnode()
创建新的子组件,而是直接去缓存中取子组件。并执行componentVNodeHooks.prepatch(mountedNode, mountedNode)
- 当我们的代码重新执行到
createComponent
时,此时isReactivated
为true,会执行reactivateComponent()
方法function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i = vnode.data if (isDef(i)) { const isReactivated = isDef(vnode.componentInstance) && i.keepAlive if (isDef(i = i.hook) && isDef(i = i.init)) { i(vnode, false /* hydrating */) } if (isDef(vnode.componentInstance)) { initComponent(vnode, insertedVnodeQueue) insert(parentElm, vnode.elm, refElm) if (isTrue(isReactivated)) { // 非首次渲染会执行到这一步 reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) } return true } } }
- 执行
reactivateComponent()
函数,最后通过执行insert(parentElm, vnode.elm, refElm)
就把缓存的DOM对象直接插入到目标元素中,这样就完成了在数据更新的情况下的渲染过程。function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) { let i // hack for #4339: a reactivated component with inner transition // does not trigger because the inner node's created hooks are not called // again. It's not ideal to involve module-specific logic in here but // there doesn't seem to be a better way to do it. let innerNode = vnode while (innerNode.componentInstance) { innerNode = innerNode.componentInstance._vnode if (isDef(i = innerNode.data) && isDef(i = i.transition)) { for (i = 0; i < cbs.activate.length; ++i) { cbs.activate[i](emptyNode, innerNode) } insertedVnodeQueue.push(innerNode) break } } // unlike a newly created component, // a reactivated keep-alive component doesn't insert itself insert(parentElm, vnode.elm, refElm) }
抽象组件
定义
不会在DOM树中渲染(真实或者虚拟都不会),不会渲染为一个DOM元素,也不会出现在父组件链中——你永远在 this.$parent
中找不到。它有一个属性 abstract 为 true,表明是它一个抽象组件。
特点:抽象组件的子组件,会选取抽象组件的上一级非抽象组件作为父级
常见的抽象组件:<keep-alive>
、<transition>
、<transition-group>
等.
抽象组件是如何忽略掉父子关系
// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options= vm.$options
// 找到第一个非abstract父组件实例
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
// ...
}
Vue在初始化生命周期的时候,为组件实例建立父子关系会根据abstract属性决定是否忽略某个组件。在抽象组件中,设置了abstract:true
,那Vue就会跳过该组件实例。
最后构建的组件树中就不会包含keep-alive组件,那么由组件树渲染成的DOM树自然也不会有keep-alive相关的节点了。
缓存组件的生命周期钩子
只执行一次的钩子——created、mounted、destroyed等
被缓存的组件实例会为其设置keepAlive= true
,而在初始化组件钩子函数中:
// src/core/vdom/create-component.js
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean{
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// keep-alive components, treat as a patch
const mountedNode:any = vnode
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode (vnode, activeInstance)
}
}
}
初次渲染时,vnode.componentInstance
为null,会创建缓存组件的vnode,执行完整的$mount
流程,期间会调用非缓存组件的所有生命周期方法,如created、mounted、updated等。
当非首次渲染,vnode.componentInstance
为缓存的vnode、keepAlive为true,此时不再进入$mount
过程,那mounted之前的所有钩子函数(beforeCreate、created、mounted)都不再执行。只会执行componentVNodeHooks.prepatch(mountedNode, mountedNode)
,直接走patch流程。
可重复执行的钩子——activated、deactivated
在patch的阶段,最后会执行invokeInsertHook函数,而这个函数就是去调用组件实例(VNode)自身的insert钩子:
// src/core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
if (isTrue(initial) && isDef(vnode.parent)) {
vnode.parent.data,pendingInsert = queue
} else {
for(let i =0; i<queue.length; ++i) {
queue[i].data.hook.insert(queue[i]) // 调用VNode自身的insert钩子函数
}
}
}
再看insert钩子:
// src/core/vdom/create-component.js
const componentVNodeHooks = {
// init()
insert (vnode: MountedComponentVNode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true/* direct */)
}
}
// ...
}
// destroy()
}
在这个钩子里面,调用了activateChildComponent
函数递归地去执行所有子组件的 activated
钩子函数:
// src/core/instance/lifecycle.js
export function activateChildComponent (vm: Component, direct?: boolean) {
if (direct) {
vm._directInactive = false
if (isInInactiveTree(vm)) {
return
}
} else if (vm._directInactive) {
return
}
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
相反地,deactivated
钩子函数也是一样的原理,在组件实例(VNode)的destroy
钩子函数中调用deactivateChildComponent
函数。
系列文章
- 手写Vue2源码(一)—— 环境搭建
- 手写Vue2源码(二)—— 数据劫持
- 手写Vue2源码(三)—— 模板编译
- 手写Vue2源码(四)—— 初次渲染
- 手写Vue2源码(五)—— 观察者模式
- 手写Vue2源码(六)—— 异步更新及nextTick
- 手写Vue2源码(七)—— 侦听属性
- 手写Vue2源码(八)—— 计算属性
- 手写Vue2源码(九)—— 混入原理与生命周期
- 手写Vue2源码(十)—— 组件原理
- 手写Vue2源码(十一)—— diff算法
- 手写Vue2源码(十二)—— keep-alive
- 手写Vue2源码(十三)—— 全局API
- vue-router原理解析
- vuex原理解析
- vue3原理解析