Vue3中KeepAlive、Teleport、Transition 内置组件是怎么实现的?

388 阅读7分钟

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的组件从原容器搬运到了另一个隐藏的容器中,实现了“假卸载”;当隐藏容器中的组件被”挂载“的时候,再从隐藏容器中搬运到原容器。这个过程就是 activateddeactivated

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通过钩子函数实现了过渡效果。下课……