探索Vue渲染器的内部机制-卸载元素

976 阅读2分钟

在上一节说了解了一下元素属性的挂载,主要内容就是如何正确的挂载元素的属性,HTML AttributesDOM Properties两者之间的区别以及应用,还有在Vue中是如何对class、style属性进行增强的,前面说完了挂载阶段下面再说下卸载阶段。

首先回顾一下在渲染器章节中的代码,在初次挂载完成之后,后续渲染就会触发更新,所以卸载操作时发生在更新阶段的,在卸载阶段也就是给renderer函数传递null参数时候,在patch函数中不能通过使用container.innerHTML=""方式来清空。

  function createRender() {
    function render(vnode, container) {
      if (vnode) { // 挂载或更新阶段
        patch(container._vnode, vnode, container)
      } else {
        // 旧的vnode存在,新的vnode不存在 卸载阶段
        if (container._vnode) {
          container.innerHTML = "";
        }
      }
      container._vnode = vnode;
    }
    return {
      render
    }
  }
  
  const renderer = createRenderer()
  // 首次渲染 挂载阶段 
  renderer(vnode1, document.getElementById("#app"))
  // 第二次渲染 更新阶段 
  renderer(vnode2, document.getElementById("#app"))
  // 卸载阶段
  renderer(null, document.getElementById("#app"))

这样做有以下几个缺陷:

  1. 使用innerHTML虽然可以清空元素内容,但是不会清除绑定在DOM元素上的事件处理函数;
  2. 容器中的元素可能是某个或多个组件渲染的,当需要进行卸载操作时应该正确调用组件对应卸载的生命周期函数;
  3. 若元素中存在自定义指令在卸载时也应该执行指令对应的钩子函数;

那么正确的做法应该是使用原生DOM操作方法将真实DOM元素移除,但是在卸载阶段并没有办法获取到真实DOM,所以我们可以根据vnode对象获取与其相关联的真实DOM元素,关键的一步就是怎么在vnode对象与真实DOM元素之间建立联系。

function mountElement(vnode,container){
// 在此处建立联系
    const el = vnode.el = document.createElement(vnode.type);
    // ...
}

function render(vnode, container) {
  if (vnode) { // 挂载或更新阶段
    patch(container._vnode, vnode, container)
  } else {
    if (container._vnode) {
        const el = container._vnode.el;
        const parent = el.parentNode;
        if(parent) parent.removeChild(el);
    }
  }
  container._vnode = vnode;
}

可以在挂载阶段将创建的真实DOM元素直接挂载到vnode虚拟节点中进行关联,在挂载阶段建立起联系之后,在卸载的时候就可以从旧vnode中获取到关联的真实DOM,通过调用removeChild函数将其从父元素中删除。由于卸载操作在后续可能会被复用所以可以将其封装成一个函数。

function unmount(vnode){
    const parent = vnode.el.parentNode;
    if(parent) parent.removeChild(vnode.el);
}

funciton render(vnode,container){
    // ...
    if (container._vnode) {
        unmount(container._vnode);
    }
}

从整体或者说宏观角度来说这就是卸载阶段要做的所有事情。