Vue3源码分析(12)-组件卸载流程

2,302 阅读5分钟

本文介绍

  • 前面我们讲述了组件的挂载、更新等流程。本文继续讲解组件挂载流程。挂载流程主要由unmountunmountComponentunmountChildren三个函数控制。下面主要以unmountComponent为起点剖析组件的卸载流程。

卸载流程

1.卸载组件(unmountComponent)

const unmountComponent = (instance, parentSuspense, doRemove) => {
  const { bum, update, subTree, um } = instance;
  //调用beforeUnmount
  if (bum) {
    shared.invokeArrayFns(bum);
  }
  if (update) {
    //让更新任务失活,即使当前组件的update任务
    //在调度器队列当中也不会在执行。
    update.active = false;
    //卸载组件的子节点
    unmount(subTree, instance, parentSuspense, doRemove);
  }
  //调用unmounted钩子
  if (um) {
    queuePostRenderEffect(um, parentSuspense);
  }
  //设置组件为已经卸载
  queuePostRenderEffect(() => {
    instance.isUnmounted = true;
  }, parentSuspense);
};
  • 立即调用当前组件所有的beforeUnmount钩子
  • 让组件实例的更新任务立即失活,即使当前更新任务还在调度器队列当中也不会执行。
  • 卸载组件的子节点。
  • unmount钩子函数放入调度器后置队列中,后置队列没有优先级,先放入先执行。当时子组件的卸载在父组件卸载之前,所以子组件的unmount钩子函数会先放入调度器后置队列当中,保证执行钩子的执行顺序不会错误
  • 在完成卸载后标识当前组件已经被卸载。

2.unmount

const unmount = (
  vnode,
  parentComponent,
  parentSuspense,
  doRemove = false,
  optimized = false
) => {
  const {
    type,//实例的类型
    props,//实例接受的props
    ref,//ref属性
    children,//子节点
    dynamicChildren,//带有变量的子节点
    shapeFlag,//当前vnode类型
    patchFlag,
    dirs,//自定义指令
  } = vnode;
  //清空ref
  if (ref != null) {
    setRef(ref, null, parentSuspense, vnode, true);
  }
  //是否执行自定义指令的钩子
  const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs;
  //如果当前是一个组件,卸载
  if (shapeFlag & ShapeFlags.COMPONENT) {
    unmountComponent(vnode.component, parentSuspense, doRemove);
  } else {
   //省略第二部分代码
  }
};
  • vnode中获取必要的属性。
  • 将所有的ref设置为nullsetRefVue3源码分析(4)已经详细讲解过了。
  • 根据shapeFlag判断当前节点是否是组件,如果是组件则卸载组件。
//调用自定义指令的钩子
if (shouldInvokeDirs) {
  invokeDirectiveHook(vnode, null, parentComponent, "beforeUnmount");
}
//处理dynamicChildren
else if (
  dynamicChildren &&
  //非稳定的fragments不应该采用fast path
  (type !== Fragment ||
    //必须是稳定的fragment
    (patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))
) {
  unmountChildren(
    dynamicChildren,
    parentComponent,
    parentSuspense,
    false,
    true
  );
}
//处理fragment是keyed和unkeyed的情况以及children是数组的情况
else if (
  (type === Fragment &&
    patchFlag &
      (PatchFlags.KEYED_FRAGMENT | PatchFlags.UNKEYED_FRAGMENT)) ||
  (!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN)
) {
  //卸载children
  unmountChildren(children, parentComponent, parentSuspense);
}
//真实卸载DOM的操作
if (doRemove) {
  remove(vnode);
}
  • 处理当前节点不是组件的情况。
  • 调用当前节点自定义指令的beforeUnmount钩子
  • STABLE_FRAGMENT:稳定的Fragment节点,我们知道在Vue2template中需要返回一个根节点,但是Vue3则不需要返回一个根节点,这是因为Vue3会自动给组件包裹一层Fragment类型节点
//vue2写多个节点的写法
<template>
  <div>
    <p></p>
    <p></p>
  </div>
</template>

//vue3写多个节点的写法
<template>
  <p></p>
  <p></p>
</template>
  • 对于Vue3写法的节点就需要包裹一层稳定的Fragment
  • UNKEYED_FRAGMENT、KEYED_FRAGMENT:不稳定的Fragment。首先只要使用了v-for指令那么就会包裹一层Fragment类型节点,当我们写出<div v-for="a in b"></div>这样的代码,这就是UNKEYED_FRAGMENT类型,当我们写出<div v-for="a in b" :key="a"></div>这样的代码,这就是KEYED_FRAGMENT类型,他们都是不稳定的Fragment类型节点
  • 这里需要详细讲解一下为什么稳定的Fragment类型节点是卸载dynamicChildren,而其他的Fragment类型节点需要卸载所有的children对于不稳定的Fragment节点,无法收集dynamicHildren属性,这是因为使用了v-for渲染列表之后,新旧节点在数组中的顺序可能发生了改变违背了dynamicChilren一一比较的原则,所以对于使用v-for渲染的Fragment类型节点禁止收集dynamicChilren。那么对于稳定的Fragment节点则允许收集所以可以直接卸载dynamicChildren即可
<template>
  <div v-for="a in b" :key="a"></div>
  <div></div>
</template>

//编译后
const _hoisted_1 = _createElementVNode("div")
function render(ctx, cache) {
  return (openBlock(), createElementBlock(Fragment, null, [
    (openBlock(true), createElementBlock(Fragment, null, renderList(ctx.b, (a) => {
      return (openBlock(), createElementBlock("div", { key: a }))
    }), 128 /* KEYED_FRAGMENT */)),
    hoisted_1
  ], 64 /* STABLE_FRAGMENT */))
  • 我们可以发现STABLE_FRAGMENTopenBlock没有传递true参数表示允许收集dynamicChildren,对于KEYED_FRAGMENTopenBlock则传递了true参数表示不允许收集dynamicChildren。具体大家也可以看看Vue3源码分析(10)详细讲解了如何收集动态节点。
if (shouldInvokeDirs) {
  queuePostRenderEffect(() => {
    //调用组件自定义指令的unmounted钩子  
    invokeDirectiveHook(vnode, null, parentComponent, "unmounted");
  }, parentSuspense);
}
  • 最后把节点自定义指令的unmounted钩子函数放入调度器后置队列中等待执行。

3.卸载DOM(remove)

const remove = (vnode) => {
  const { type, el, anchor } = vnode;
  if (type === Fragment) {
    //el代表开始的空文本节点 anchor代表最后的空文本节点
    //删除el anchor在内的所有节点
    removeFragment(el, anchor);
    return;
  }
  //移除静态节点
  if (type === Static) {
    removeStaticNode(vnode);
    return;
  }
  //普通DOM直接移除
  hostRemove(el);
};
  • 判断当前需要移除节点的类型,调用不同的移除DOM函数处理。
const hostRemove = function(){
  const parent = child.parentNode;
  if (parent) parent.removeChild(child);
}

//依次移除所有的兄弟节点
const removeFragment = (cur, end) => {
  let next;
  while (cur !== end) {
    //cur.nextSibling
    next = hostNextSibling(cur);
    hostRemove(cur);
    cur = next;
  }
  hostRemove(end);
};
  • hostRemovereact-dom实现,实际就是调用浏览器的Api实现对DOM的移除
const removeStaticNode = ({ el, anchor }) => {
  let next;
  while (el && el !== anchor) {
    next = hostNextSibling(el);
    hostRemove(el);
    el = next;
  }
  hostRemove(anchor);
};

4.unmountChildren

const unmountChildren = (
  children,
  parentComponent,
  parentSuspense,
  doRemove = false,
  optimized = false,
  start = 0
) => {
  for (let i = start; i < children.length; i++) {
    unmount(
      children[i],
      parentComponent,
      parentSuspense,
      doRemove,
      optimized
    );
  }
};
  • for循环依次删除所有的child

总结

  • 本章主要分析了组件的卸载流程。到此为止,组件的挂载、更新、卸载流程已经全部分析完毕。下面我们还会详细讲解内置组件Teleport、Suspense的实现原理。