1. KeepAlive
前言:KeepAlive 这个词相信对你并不陌生,没错,正是借鉴了HTTP协议,在HTTP中的keep-alive又称HTTP持久连接(HTTP persistent connection),作用是允许多个请求或响应共用一个TCP链接。 http中的KeepaAlive可以避免链接频繁创建/销毁,在Vue中的keepalive同样也是为了避免一个组件频繁创建/重建。 比如开发中我们编写了如下代码:
<template>
<tab v-if="currTab==='1'" />
<tab v-if="currTab==='2'" />
<tab v-if="currTab==='3'" />
</template>
1.1 解决了什么问题?
可以看到,如果频繁的切换 currTab ,会导致不停地卸载并重建对应的tab组件,为了避免不必要的开销,可以使用keepalive包裹,同时还可以通过 include 和 exclude prop 来定制缓存行为,会根据组件的 name 选项进行匹配,另外,max 最大缓存实例数,缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁,以便为新的实例腾出空间。
1.2 实现原理
其实keepAlive的本质是缓存管理,再加上特殊的挂载/卸载逻辑。首先,KeepAlive 组件的实现需要渲染层的支持,而KeepAlive的组件在卸载时,我们也不能真正的将其卸载否则无法维持组件当前的状态了。通过观察源码发现,被KeepAlive的组件从原容器搬运到了另一个隐藏的容器中,实现了“假卸载”;当隐藏容器中的组件被”挂载“的时候,再从隐藏容器中搬运到原容器。这个过程就是 activated 和 deactivated。
1.3 include 和 exclude
默认情况下,KeepAlive组件会对所有”内部组件“进行缓存,但有时候期望只缓存特定组件,include 用来显示的配置应该被缓存的组件,exclude用来显示的配置不应该缓存的组件。源码中会根据”内部组件“的名称name进行匹配,判断是否进行缓存。
1.4 缓存管理
这也是KeepAlive的核心,其内部通过一个Map对象来实现对组件的缓存,该Map的key是组件选项对象,即 vnode.type属性的值,而Map的Value是用于描述组件的vnode对象。
- 缓存管理的实现
// 使用组件选项对象 rawVNode.type 作为键,去缓存中查找
const cacheVNode = cachce.get(rawVNode.type);
if(cacheVNode){
// 如果缓存存在,则无须重新创建组件实例,只需要继承即可。
rawVNode.component = cacheVNode.component;
rawVNode.keptAlive = true;
} else {
// 如果缓存不存在,则设置缓存
cache.set(rawVNode.type, rawVNode);
}
1.5 可能出现的问题
上面代码看似没毛病,但是有个细节,如果缓存不存在的时候,总是会设置新的缓存,这会导致缓存不断地增加。为了解决这个问题,必须设置一个缓存阈值,当缓存数量超过指定阈值时将其进行修剪。那么你可能会问,进行修剪的依据是什么呢?vue中当前采用的策略是 ”最新一次访问“,结合max最大缓存实例数,把当前访问(或修剪)的组件作为最新一次渲染的组件,并且该组件在缓存修剪过程中是安全且不会被修剪的。
2 Teleport
2.1 解决了什么问题?
通常情况下,在将虚拟dom渲染成真实dom时,最终渲染出来的真实dom层级结果与虚拟dom层级结构一致。
<template>
<div id="main" style="z-index:-1;">
<Overlay />
</div>
</template>
上面这段代码中,<Overlay />组件的内容会被渲染到id位main的div标签下,假设<Overlay />是一个”蒙层“组件,并要求蒙层能遮挡页面上的任何元素,也就是要求<Overlay />的z-index层级最高来实现。但问题是,如果<Overlay />组件的内容无法跨域dom层级渲染,就无法实现这个目标。
于是就出现了Teleport内置组件,该组件可以将指定的内容渲染到特定容器中,并且不受dom层级限制。
2.2 实现原理
跟KeepAlive组件一样,需要渲染器的支持,通过判断就得虚拟节点是否存在,源码中通过组件选项的 _isTeleport 标识判断是否为 Teleport组件,如果是,则直接调用 process 函数,将渲染控制权完全交接出去。通过判断分离出来的dom是否存在,来决定执行挂载还是更新,如果是挂载,则根据props.to的属性来去的真正的挂载点。to 的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。
- 具体实现
const Teleport = {
_isTeleport: true,
process(n1. n2, container, anchor, internals){
// 通过 internals 参数取的渲染器的内部方法,
const { path } = internale'
// 如果旧的 VNode na 不存在,则是全新的挂载,否则执行更新
if(!n1){
// 挂载,获取容器
const tatget = typeof n2.props.to === 'string' ? document.querySelector(n2.props.to) : n2.props.to
// 将n2.children渲染到执行挂载点
n2.children.forEach(c => patch(null, c, target, anchor))
} else {
// 更新
patchChildren(n1, n2, container)
}
}
}
3. Transition
3.1 解决了什么问题?
transition你是不是感觉很熟悉,没错,就是css中的那个 transition过渡,原生DOM的过渡本质是将一个DOM与元素在两种状态间的切换,浏览器会根据过渡效果自行完成DOM元素的过渡,其中包括了持续时间、远动曲线、过渡的属性等。
通过一段代码,一起来回忆一下css中如何实现过渡的:
<html>
<body>
<div class="main"></div>
</body>
</html>
<script>
// 创建class为main的元素,
const el = document.createElement('div');
el.classList.add('main');
// 在DOM元素被添加到页面时,将初始状态和远动过程定义到元素上
el.classList.add('enter-form');
el.classList.add('enter-active');
// 将元素添加到页面
document.body.appendChild(el);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// 切换元素的状态
el.classList.remove('enter-form');
el.classList.add('enter-to');
// 监听 transitioned 事件完成收尾工作
el.addEventlstener('transitioned', () => {
el.classList.remove('enter-to');
el.classLsit.remove('enter-active');
// 过渡完成后,调用 performRemve 函数将dom元素移除;
performRemove();
})
})
})
</script>
<style>
.main{
width: 100px;
height: 100px;
background-color: red:
}
.enter-form{
transform: translateX(200px);
}
.enter-to{
transform: transitionX(0);
}
.enter-active{
transition: transform 1s ease-in-out;
}
</style>
上面css指定了远动属性是transform,持续时间是1s,运动曲线是 ease-in-out,离场过渡的处理和进程过渡的处理方式很相似,首先设置初始状态,然后在下一帧中切换为结束状态,从而实现过渡效果。
3.2 实现原理
实现逻辑其实和元素DOM很相似,不过Transition内置组件是基于虚拟dom实现的,整个过渡过程可以抽象的分为几个阶段,beforeEnter,enter, leave等。
// 在元素被插入到 DOM 之前被调用
// 用这个来设置元素的 "enter-from" 状态
function onBeforeEnter(el) {}
// 在元素被插入到 DOM 之后的下一帧被调用
// 用这个来开始进入动画
function onEnter(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 当进入过渡完成时调用。
function onAfterEnter(el) {}
// 当进入过渡在完成之前被取消时调用
function onEnterCancelled(el) {}
// 在 leave 钩子之前调用
// 大多数时候,你应该只会用到 leave 钩子
function onBeforeLeave(el) {}
// 在离开过渡开始时调用
// 用这个来开始离开动画
function onLeave(el, done) {
// 调用回调函数 done 表示过渡结束
// 如果与 CSS 结合使用,则这个回调是可选参数
done()
}
// 在离开过渡完成、
// 且元素已从 DOM 中移除时调用
function onAfterLeave(el) {}
// 仅在 v-show 过渡中可用
function onLeaveCancelled(el) {}
在dom挂载之前,调用 transition.beforeEnter钩子;挂载之后,调用 transition.enter,并且两个钩子函数都接收需要过渡的dom元素对象作为的个参数;卸载元素则调用 transition.leave;通过这些钩子,灵活的实现了Transition内置组件。与原生实现方式对比,增加了对节点过渡时机的控制,将卸载动作封装到performRemove函数中,只需要在具体的时机以回调的方式将控制权交接出去即可。
总结
这三个内置组件的共同特点是与渲染器的结合非常紧密,KeepAlive通过缓存管理实现了频繁切换下带来渲染消耗问题;Teleprot通过对DOM层级的控制实现了将指定的内容渲染到特定容器;Transition通过钩子函数实现了过渡效果。下课……