This's FE Life
  • Home
  • Categories
    • LifeStyle
    • Travel
  • Tags
    • 冰山工作室
    • 前端
    • JavaScript
    • Web

© Carlo All Rights Reserved 2021 . 辽ICP备15007588号

关于VUE+element组件销毁思考和源码分析

WEB基础 6月 22, 2020

「吾日三省吾身。」

问题的复盘

上午同事提出一个问题,大概的意思是:弹窗组件在关闭重新打开之后,并没有触发数据的更新。

处理这个问题的时候,我的反应是抽象出来看,看似是数据绑定的问题,实际上是组件渲染的问题。
简单点来说就是如何重置组件,或者强制刷新组件

找到问题的根源当时想到了三种可行的解决办法:

  • v-if
  • 强制更新
  • destroy

先说第一种,简单粗暴好理解,但是要频繁切换状态,不细说了。

第二种,其实我忘记具体的指令名字了,后来才查到了,当时就说了个$set,当然原理是一样的,这个也不细说,可以看vue的文档

第三种,再次原谅我记不住方法名,只记得element有个destroy,现成的。同事提到了具体的方法名destroyOnClose

事情到此应该已经得到了解决,超哥@Alex提出了一个问题:在Element UI中这个方法是怎么实现的?

源码分析

先看源码
源码1
源码2

看到这似乎有种似曾相识的感觉,又有点困惑,:key这个经常用,在v-for中。在这里是什么意思?怎么++就能触发销毁和更新?

Key的说明

Vue对于Key的说明:

key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。

React对于Key的说明:

当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。

总结:

新子节点根据自己的key找到对应的旧子节点。

通俗的讲:

给子组件或者元素标签加上一个记号,方便vue根据记号对子组件或者元素标签做排查。一旦某个加上记号的子组件或者元素标签其上的记号进行了改变,vue就会对其做出一些相关的操作。

执行什么操作呢:

使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。

使用场景:

它也可以用于强制替换元素/组件而不是重复使用它。

  • 完整地触发组件的生命周期钩子
  • 触发过渡

这样就很好理解了,我们给每一个弹窗组件绑定了唯一key,当我们需要销毁这个弹窗的时候只需要修改组件的key,就可以让vue帮我们回收掉它,最简单的改值方式当然就是++(当然你也可以--,其实这有bug,有数值越界的问题)

VNode的说明

可能有人会问,vue底层是如何通过key干掉这个DOM的呢?这里就不得不提两个东西,一个叫VNode,一个叫diff

这里简单的说一下有key的虚拟DOM更新,如果感兴趣的话,可以找我讨论。

VNode

虚拟DOM简而言之就是,用JS去按照DOM结构来实现的树形结构对象,一般称之为虚拟节点(VNode)

简单的diff算法

加key的h函数,

// 旧 children
[
  h('li', { key: 'a' }, 1),
  h('li', { key: 'b' }, 2),
  h('li', { key: 'c' }, 3)
]

// 新 children
[
  h('li', { key: 'c' }, 3)
  h('li', { key: 'a' }, 1),
  h('li', { key: 'b' }, 2)
]

export function h(tag, data = null, children = null) {
  // 省略...

  // 返回 VNode 对象
  return {
    // 省略...
    key: data && data.key ? data.key : null
    // 省略...
  }
}

有了key,我们就可以找到相同可复用的DOM
复用

// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {
  const nextVNode = nextChildren[i]
  // 遍历旧的 children
  for (let j = 0; j < prevChildren.length; j++) {
    const prevVNode = prevChildren[j]
    // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
    if (nextVNode.key === prevVNode.key) {
      patch(prevVNode, nextVNode, container)
      break // 这里需要 break
    }
  }
}

这段代码中有两层嵌套的 for 循环语句,外层循环用于遍历新 children,内层循环用于遍历旧 children,其目的是尝试寻找具有相同 key 值的两个节点,如果找到了,则认为新 children 中的节点可以复用旧 children 中已存在的节点,这时我们仍然需要调用 patch 函数对节点进行更新,如果新节点相对于旧节点的 VNodeData 和子节点都没有变化,则 patch 函数什么都不会做

找到不动的,那么要动的怎么移动呢?
移动
移动1

// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0
// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {
  const nextVNode = nextChildren[i]
  // 遍历旧的 children
  for (let j = 0; j < prevChildren.length; j++) {
    const prevVNode = prevChildren[j]
    // 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
    if (nextVNode.key === prevVNode.key) {
      patch(prevVNode, nextVNode, container)
      if (j < lastIndex) {
        // 需要移动
        // refNode 是为了下面调用 insertBefore 函数准备的
        const refNode = nextChildren[i - 1].el.nextSibling
        // 调用 insertBefore 函数移动 DOM
        container.insertBefore(prevVNode.el, refNode)
      } else {
        // 更新 lastIndex
        lastIndex = j
      }
      break // 这里需要 break
    }
  }
}

如果我要添加怎么办呢?
添加

for (let i = 0; i < nextChildren.length; i++) {
  const nextVNode = nextChildren[i]
  let find = false
  for (let j = 0; j < prevChildren.length; j++) {
    const prevVNode = prevChildren[j]
    if (nextVNode.key === prevVNode.key) {
      find = true
      patch(prevVNode, nextVNode, container)
      if (j < lastIndex) {
        // 需要移动
        const refNode = nextChildren[i - 1].el.nextSibling
        container.insertBefore(prevVNode.el, refNode)
        break
      } else {
        // 更新 lastIndex
        lastIndex = j
      }
    }
  }
  if (!find) {
    // 挂载新节点
    // 找到 refNode
    const refNode =
      i - 1 < 0
        ? prevChildren[0].el
        : nextChildren[i - 1].el.nextSibling
    mount(nextVNode, container, false, refNode)
  }
}

节点li-d在旧的children中是不存在的,所以当我们尝试在旧的children中寻找li-d节点时,是找不到可复用节点的,这时就没办法通过移动节点来完成更新操作,所以我们应该使用mount函数将li-d节点作为全新的VNode挂载到合适的位置。

那么最后我们再来实现一下删除

删除

// 移除已经不存在的节点
// 遍历旧的节点
for (let i = 0; i < prevChildren.length; i++) {
  const prevVNode = prevChildren[i]
  // 拿着旧 VNode 去新 children 中寻找相同的节点
  const has = nextChildren.find(
    nextVNode => nextVNode.key === prevVNode.key
  )
  if (!has) {
    // 如果没有找到相同的节点,则移除
    container.removeChild(prevVNode.el)
  }
}
Share
Facebook Twitter Linkedin Google+
Newer Older