vuejs设计与实现-内建组件和模块

79 阅读4分钟

KeepAlive组件的实现原理

KeepAlive组件在卸载时, 是将被KeepAlive的组件从原容器搬运到另外一个隐藏的容器当中, 实现“假卸载”. 再次挂载时, 也是将该组件从隐藏容器中搬运到原容器. 此过程对应生命周期activateddeactivated.

const KeepAlive = {
    __isKeepAlive: true,
    props: {
        include: RegExp,
        exclude: RegExp
    },
    setup(props, { slots }){
    
        const cache = new Map() // Map<vnode.type, vnode>
        const instance = currentInstance
        // keepAlive组件特殊属性, 由渲染器注入
        const { move, createElement } = instance.keepAliveCtx
        
        const storageContainer = createElement('div')
        // 失活。搬运至隐藏容器
        instance._deActivate = vnode => {
            move(vnode, storageContainer)
        }
        // 激活。搬运至原容器
        instance._activate = (vnode, container, anchor) => {
            move(vnode, container, anchor)
        }
        
        return () => {
            // 被keepAlive的原组件
            let rawVnode = slots.default()
            // 如果不是组件, 直接return (只有组件才可以被 keepAlive)
            if(typeof rawVnode.type !== 'object') {
                return rawVnode
            }
            
            const name = vnode.type.name
            // 不符合条件的不进行缓存
            if(name && (!props?.include.test(name)||props?.exclude.test(name))) {
                return rawVnode
            }
            
            // 对 include 和 exclude 进行处理
            const name = vnode.type.name
            if(name && (!props?.include.test(name) || props?.exclude.test(name))) {
                // 如果组件不符合条件, 则不进行keepAlive
                return rawVnode
            }
            
            
            const cachedVnode = cache.get(rawVnode.type)
            if(cachedVnode) {
                // 获取缓存的组件实例
                rawVnode.component = cachedVnode.component
                // 标记此组件已被keepAlive
                rawVnode.keptAlive = true
            } else {
                // 添加至缓存。
                cache.set(rawVnode.type, rawVnode)
            }
            // 打上标记, 避免被真的卸载
            rawVnode.shouldKeepAlive = true
            // KeepAlive 组件实例
            rawVnode.keepAliveInstance = instance
            return rawVnode
        }
    }
}


// patch函数  增加 KeepAlive 组件的渲染逻辑
function patch(n1, n2, container, anchor){
    // ...
    // else if
    if(typeof n2.type === 'object' || n2.type === 'function') {
        if(!n1) {
            // 如果已被keepAlive 则激活
            if(n2.keptAlive){
                n2.keepAliveInstance._activate(n2, container, anchor)
            } esle {
                mountComponent(n2, container, anchor)
            }
        } else {
            patchComponent(n2, container, anchor)
        }
    }
    // ...
}

// keepAlive 挂载(激活)操作
function mountComponent(){
    // ...
    const instance = {
        // ...
        keepAliveCtx: null
    }
    const isKeepAlive = vnode.type.__isKeepAlive
    // 为KeepAlive组件添加 keepAliveCtx 对象, 提供两个方法
    if(isKeepAlive) {
        instance.keepAliveCtx = {
            move(vnode, container, anchor) {
                // 将组件渲染的内容移动到指定容器
                insert(vnode.component.subTree.el, container, anchor)
            },
            createElement
        }
    }
}

// keepAlive 假卸载操作
function unmount(vnode){
    if(vnode.type === Fragment) {
        vnode.children.forEach(c => unmount(c))
        return
    } else if (typeof vnode.type === 'object') {
        // keepAlive 执行假卸载
        if(vnode.shouldKeepAlive) {
            vnode.keepAliveInstance._deActivate(vnode)
        } else {
            unmount(vnode.component.subTree)
        }
        return
    }
    // 卸载节点
    const parent = vnode.el.parentNode
    if(parent) {
        parent.removeChild(vnode.el)
    }
}

KeepAlive组件的缓存策略默认为最新一次访问. (超出限制时, 优先裁剪最久未访问的组件实例.)除此之外, 也应该允许用户自定义缓存策略. 在用户接口层面, 增加cache接口, 允许用户指定缓存实例:

<KeepAlive :cache="cache">
    <Comp />
</KeepAlive>
// 一个基本的缓存实例
const _cache = new Map()
const cache = {
    get(key){
        _cache.get(key)
    },
    set(key, val){
        _cache.set(key, val)
    },
    delete(key){
        _cache.delete(key)
    },
    forEach(fn){
         _cache.forEach(fn)
    }
}

Teleport组件的实现原理

Teleport组件是新增的内置组件. 以前需要通过原生dom操作来移动元素节点, 现在Teleport组件可以将插槽内容渲染在任何期望的地方.

// ./Overlay.vue
<template>
    <Teleport to="body">
        <div class="overlay"></div>
    </Teleport>
</template>

// 将<Overlay />的内容通过 Teleport组件 渲染在body上
const Teleport = {
    // Teleport组件的标识
    __isTeleport: true,
    // 通过process函数实现渲染
    // 1. 缩减渲染器代码体积。  2.更好地利用Tree-shaking
    process(n1, n2, container, anchor, internals) {
        const { patch, patchChildren, move } = internals
        if(!n1) {
            // 挂载至指定节点target
            const target = typeof n2.props.to === 'string' ? document.querySelector(n2.props.to) : n2.props.to
            n2.children.forEach(c => patch(null, c, target, anchor))
        } else {
            // 更新
            patchChildren(n1, n2, container)
            // props.to 变化则进行移动
            if(n2.props.to !== n1.props.to) {
                const newTarget = typeof n2.props.to === 'string' ? document.querySelector(n2.props.to) : n2.props.to
                n2.children.forEach(c => move(c, newTarget))
            }
        }
    }
}

// 修改patch函数  抽离Teleport组件的渲染逻辑
function patch(n1, n2, container, anchor){
    // ...
    // else if
    if(typeof n2.type === 'object' && n2.type.__isTeleport) {
        // 如果是Teleport 则执行组件的process方法
        n2.type.process(n1, n2, container, anchor, {
            patch, 
            patchChildren,
            unmount,
            move(){
                // 简易实现. 实际上虚拟节点的类型有很多种, 不止组件和普通元素
                insert(vnode.component ? vnode.component.subTree.el : vnode.el, container, anchor)
            }
        })
    }
    // ...
}

Transition组件饿实现原理

  1. 当dom元素被挂载时, 将动效附加到该dom元素上
  2. 当dom元素被卸载时, 等到附加到该元素上的动效执行完毕后再卸载

Transition不会渲染任务内容, 只是读取默认插槽内容, 渲染要过渡的元素. 并在过渡元素的虚拟节点上添加相关的钩子函数.

const Transition = {
    name: 'Transition',
    props: {
        // 简易实现 还需要处理name、mode等props
        // ... name  mode
    },
    setup(props, { slots }){
        return () => {
            const innerVnode = slots.default()
            // 相关钩子。
            innerVnode.transition = {
                beforeEnter(el){
                    el.classList.add('enter-from')
                    el.classList.add('enter-active')
                },
                enter(el){
                    // 下一帧切换到入场结束状态
                    nextFrame(() => {
                        el.classList.remove('enter-from')
                        el.classList.add('enter-to')
                        el.addEventListener('transitionend', () => {
                            el.classList.remove('enter-active')
                            el.classList.remove('enter-to')
                        })
                    })
                },
                leave(el, performRemove){
                    el.classList.add('leave-from')
                    el.classList.add('leave-active')
                    
                    // 强制reflow 使初始状态生效
                    document.body.offsetHeight
                    // 下一帧切换到离场结束状态
                    nextFrame(() => {
                        el.classList.remove('leave-from')
                        el.classList.add('leave-to')
                        el.addEventListener('transitionend', () => {
                            el.classList.remove('leave-active')
                            el.classList.remove('leave-to')
                            performRemove()
                        })
                    })
                }
            }
            
            return innerVnode
        }
    }
}

// 挂载前后执行对应钩子函数
function mountComponent(vnode, container, anchor){
     const needTransition = vnode.transition
     if(needTransition) {
         vnode.transition.beforeEnter(el)
     }
     
     insert(el, container, anchor)
     
      if(needTransition) {
         vnode.transition.enter(el)
     }
}

// Transition组件的卸载
function unmount(vnode){
    const needTransition = vnode.transition
    if(vnode.type === Fragment) {
        vnode.children.forEach(c => unmount(c))
        return
    } else if (typeof vnode.type === 'object') {
        if(vnode.shouldKeepAlive) {
            vnode.keepAliveInstance._deActivate(vnode)
        } else {
            unmount(vnode.component.subTree)
        }
        return
    }
    
    const parent = vnode.el.parentNode
    if(parent) {
        // 封装卸载动作
        const performRemove = () => parent.removeChild(vnode.el)
        
        if(needTransition) {
            // 如果需要过渡处理
            // 则先调用leave钩子, 并将卸载操作作为参数传递
            vnode.transition.leave(vnode.el, performRemove)
        } else {
            performRemove()
        }
    }
    
    

}

总结

内建组件KeepAliveTeleportTransition与渲染器结合紧密, 都需要框架提供底层的实现与支持.