第十四章-内建组件和模块

90 阅读6分钟

一、KeepAlive组件的实现原理

1、组件的失活和激活

keepAlive的本质:缓存管理, 配上特殊的挂载和卸载逻辑,

keepAlive的卸载:keepAlive的组件从原容器搬运到另一个隐藏的容器, 实现假‘卸载’

keepAlive的挂载:不执行真正的挂载逻辑, keepAlive的组件从隐藏容器搬回到原容器

keepAlive的使用:

<template>
  <keepAlive>
    <Tab v-if="currentTab === 1">...</Tab>
    <Tab v-if="currentTab === 2">...</Tab>
  </keepAlive>
</template>

keepAlive的实现

const KeepAlive = {
  name: "KeepAlive",
  _isKeepAlive: true,
  setup(props, {slots}){
    const cache = new Map();
    const instance = currentInstance;      // 当前keepAlive的实例, 对应render中的代码 setCurrentInstance(instance); setup(); setCurrentInstance(null)
    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 () => {
      let rawVnode = slots.default()
      if(typeof  rawVnode.type !== "object") {            // 非组件的虚拟节点无法被keepAlive
        return rawVnode
      }
      const cacheVnode = cache.get(rawVnode.type);
      if(cacheVnode) {
        rawVnode.component = cacheVnode.component;  // 如果已经缓存过, 说明组件已经被挂载过, 直接继承组件实例则可
        rawVnode.keptAlive = true;                  // 进行标注, 避免再次挂载
      } else {
        cache.set(rawVnode.type, rawVnode);
      }
      rawVnode.shouldKeepAlive = true;              // 标注组件已经被缓存,避免进行卸载,为什么需要两个keptAlive和 shouldKeepAlive, 一个是避免挂载, 一个是避免卸载
      rawVnode.keepAliveInstance = instance;        // 把keepAlive组件的实例也添加到vnode上,以便渲染器中访问
      return rawVnode
    }
  }
}

unmounted部分的修改

function unmount(vnode) {
  if(vnode.type === Fragment) {
    vnode.children.forEach(c => unmount(c))
  } else if(typeof  vnode.type === "object") {
    if(vnode.shouldKeepAlive) {
      vnode.keepAliveInstance._deActivate(vnode);
    } else {
      unmount(vnode.component.subTree) // 如果是组件, 卸载的是subTree;
    }
  }
  const parent = vnode.el.parent;
  if(parent) parent.removeChild(vnode.el);
}

patch部分的修改

function patch(n1, n2, container, anchor) {
  if(n1 && n1.type !== n2.type) {
    unmount(n1);
    n1 = null;
  }
  let { type } = n2;
  if(typeof type === "string") {  // 节点是普通标签元素
  } else if(typeof  type === "object" || type === "function"){  // 节点是组件 或者 节点是函数式组件
    if(!n1) {
      if(n2.keptAlive) {
        n2.keepAliveInstance._activate(n2, container, anchor)
      } else {
        mountComponent(n2, container, anchor)
      }
    } else {
      patchComponent()
    }
  } else if(type === Text){
    
  } else if(type === Fragment) {
   
  } else {
   
  }
}

mountComponent中修改

 function mountComponent(vnode, container, anchor) {
    const instance = {
      state,
      props: shallowReactive(props),
      isMounted: false,
      subTree: null,
      mounted: [],
      keepAliveCtx: null,
    }
    const isKeepAlive = vnode.type._isKeepAlive;
    if(isKeepAlive) {
      instance.keepAliveCtx = {
        move(vnode, container, anchor) {
          insert(vnode.component.subTree.el, container, anchor); 
        },
        createElement
      }
    }
 }

vnode.component.subTree.el是怎么来

组件首次加载, 根据patch(null, subTree, container, anchor);, el真实的DOM节点是挂载在subTree上的

在keepAlive组件内部, 如果组件被缓存过,那么会执行 rawVnode.component = cacheVnode.component

2、include和exclude

const KeepAlive = {
  name: "KeepAlive",
  _isKeepAlive: true,
  props: {
    include: RegExp,
    exclude: RegExp,
  },
  setup(props, {slots}){
    const cache = new Map();
    const instance = currentInstance;      // 当前keepAlive的实例, 对应render中的代码 setCurrentInstance(instance); setup(); setCurrentInstance(null)
    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 () => {
      let rawVnode = slots.default()
      if(typeof  rawVnode.type !== "object") {            // 非组件的虚拟节点无法被keepAlive
        return rawVnode
      }
      // 在keep
      const name = rawVnode.type.name;
      if(name &&( props.include && !props.include.test(name) || props.exclude && props.exclude.test(name))) {
        return rawVnode;
      }
      const cacheVnode = cache.get(rawVnode.type);
      if(cacheVnode) {
        rawVnode.component = cacheVnode.component;  // 如果已经缓存过, 说明组件已经被挂载过, 直接继承组件实例则可
        rawVnode.keptAlive = true;                  // 进行标注, 避免再次挂载
      } else {
        cache.set(rawVnode.type, rawVnode);
      }
      rawVnode.shouldKeepAlive = true;              // 标注组件已经被缓存,避免进行卸载,为什么需要两个keptAlive和 shouldKeepAlive, 一个是避免挂载, 一个是避免卸载
      rawVnode.keepAliveInstance = instance;        // 把keepAlive组件的实例也添加到vnode上,以便渲染器中访问
      return rawVnode
    }
  }
}

3、缓存管理

问题: 当缓存不存在, cache总是会设置新的缓存, 这导致缓存不断增加, 极端情况下会占用大量内存

解决: 设置缓存阈值, 当缓存数量超于阈值时对缓存进行修剪

修剪策略: 最新一次访问, 维持一个栈, 最新访问组件压入栈头后, 按照阈值修剪栈尾的组件

二、Teleport组件的实现原理

1、Teleport组件要解决的问题

问题:

一般情况下, 将虚拟DOM渲染为真实DOM时, 最终渲染出来的真实DOM层级结构和虚拟DOM的层级结构一致

<template>
  <div  id="box" style = “z-index : -1”>
     <Overlay/>
  </div>   
</template>  

上面代码中Overlay无法实现跨越DOM层级渲染

解决:

// overlay.vue
<template>
  <Teleport to="body">
    <div class="overlay"></div>
  </Teleport>
</template>
<style scoped>
 .overlay{
  overlay: 9999;
 }
</style>

通过为Teleport组件指定渲染目标body, 即to属性的值, Teleport就把插槽内容渲染到body下, 而不会按照模板的DOM来渲染, 实现了跨DOM层级的渲染

2、实现Teleport组件

Teleport组件的渲染逻辑需要从渲染器中分离出来

  • 可以避免渲染器代码膨胀
  • 当用户没使用Teleport组件时, 由于Teleport的渲染逻辑被分离, 因此可以利用TreeShaking机制在最终的bundle中删除Teleport相关的代码

patch函数的更改

  function patch(n1, n2, container, anchor) {
    let { type } = n2;
    if(typeof type === "string") {  // 节点是普通标签元素
     
    } else if (typeof type === "object" && type._isTeleport) {  // 节点是Teleport
       type.process(n1, n2, container, anchor, {
         patch,
         patchChildren,
         unmount,
         move(vnode, container, anchor) {
          insert(vnode.component ? vnode.component.subTree.el : vnode.el, container, anchor) 
         }
       })
    } else if(typeof  type === "object" || type === "function"){  // 节点是组件 或者 节点是函数式组件
    
    } else if(type === Text){
     
    } else if(type === Fragment) {
     
    } else {
      // 省略了其他类型的vnode
    }
  }

teleport组件的实现

const Teleport = {
  _isTeleport: true,
  process(n1, n2, container, anchor, internals) {
     const { patch, patchChildren, move } = internals;
     if(!n1) {
       const target = typeof n2.props.to === "string" ?  document.querySelector(n2.props.to) : n2.props.to;
       n2.children.forEach(c => patch(c, null, target))
     } else {
       patchChildren(n1, n2, container) // 很类似Fragment, 主要处理Teleport下面的children
       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))  
       }
     }
  }
}

三、Transistion组件实现原理

1、原生DOM的过渡

1.1核心原理

  • 当DOM元素被挂载时, 将动效附加到该DOM元素上
  • 当DOM元素被卸载时, 不要立刻卸载DOM元素, 而是等到附加到该DOM元素上的动效执行完成后再卸载它

1.2enter动效

const el = document.createElement("div")
el.classList.add("box");
el.classList.add("enter-active");
el.classList.add("enter-from");
document.body.appendChild(el);

// 切换元素状态, 以下代码不起效 因为浏览器会在当前帧绘制DOM元素, 类没有切换, 绘制到屏幕的结果已经是enter-to
// el.classList.remove("enter-from");
// el.classList.add("enter-to")

// 理论上应该下一帧进行类的切换
requestAnimationFrame(() => {
  requestAnimationFrame(() => {
    el.classList.remove("enter-from");
    el.classList.add("enter-to");
    // 过渡完成后需要将enter-to和enter-active这两个类从DOM元素中去掉
    el.addEventListener("transitionend", () => {
      el.classList.remove("enter-to");
      el.classList.remove("enter-active")
    })
  })
})

使用两次requestAnimationFrame的原因: 浏览器的bug, chrome或者Safari使用requestAnimation函数注册回调会在当前帧执行, 除非其他代码已经调用了一次requestAnimationFrame, 实际测试在108版本不会出现

image-20231123170626393

1.3leave动效

el.addEventListener("click", () => {
  // 重点: 封装卸载动作
  const performRemove = () => el.parentNode.removeChild(el);
  el.classList.add("leave-from");
  el.classList.add("leave-active");
  
  // 强制reflow: 使初始状态失效
  document.body.offsetHeight;
  
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      el.classList.remove("leave-from");
      el.classList.add("leave-to");
      el.addEventListener("transitionend", () => {
        el.classList.remove("leave-to");
        el.classList.remove("leave-active");
        performRemove();
      })
    })
  })
})
  • 需要对卸载行为进行封装, 让动效执行完成之后才进行卸载
  • 强制reflow, 获取offsetHeight可以使页面重排, 但是108版本的浏览器去掉强制reflow也能起效

2、实现Transition的组件

虚拟DOM层面的表现形式

<template>
  <Transition>
     <div>我是需要过渡的元素</div>
  </Transition>
</template>

虚拟DOM的实现

function render() {
  return {
    type: "Transition",
    children: {
       default() {
         return {type: "div", children: "我是需要过渡的元素"}
       }
    }
  }
}

transition的实现

const Transition = {
  name: "Transition",
  setup(props, {slots}){
    const nextFrame = (cb) => {
      requestAnimationFrame(() => {
        requestAnimationFrame(() => {
          cb()
        })
      })
    }
    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");
            // 过渡完成后需要将enter-to和enter-active这两个类从DOM元素中去掉
            el.addEventListener("transitionend", () => {
              el.classList.remove("enter-to");
              el.classList.remove("enter-active")
            })
          })
        },
        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-to");
              el.classList.remove("leave-active");
              performRemove();
            })
          })
        }
      }
      return innerVnode;
    }
  }
}

mountElement中

 function mountElement(vnode, container, anchor) {
    const el = vnode.el =  createElement(vnode.type);
    if(typeof vnode.children === "string") {
      setElementText(el, vnode.children)
    } else if(Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        patch(null, child, el, anchor)
      })
    }
    if(vnode.props) {
      for(let key in vnode.props) {
        patchProps(el, key, null, vnode.props[key])
      }
    }
    let needTransition = vnode.transition;
    if(needTransition) {
      vnode.transition.beforeEnter(el);
    }
    insert(el, container);
    if(needTransition) {
      vnode.transition.enter(el);
    }
  }

unmount中

function unmount(vnode) {
  if(vnode.type === Fragment) {
    vnode.children.forEach(c => unmount(c))
  } else if(typeof  vnode.type === "object") {
    if(vnode.shouldKeepAlive) {
      vnode.keepAliveInstance._deActivate(vnode);
    } else {
      unmount(vnode.component.subTree) // 如果是组件, 卸载的是subTree;
    }
  }
  const parent = vnode.el.parent;
  if(parent) {
    const performRemove = () => parent.removeChild(vnode.el);
    const needTransition = vnode.transition;
    if(needTransition) {
      vnode.transition.leave(vnode.el, performRemove)
    } else {
      performRemove();
    }
  }
}