阅读 705

关于vue中删除依赖的那些事(vue卸载依赖包)

前言


个人在vue源码的学习过程中,不仅仅觉得数据响应式实现的非常的巧妙,在设计收集依赖的环节中,后续的删除依赖的处理也是非常的细节贴心的,也不得不感叹作者满满的细节。那么vue是在何时删除依赖的?删除依赖的作用及场景又是什么呢?接下来我们带着问题,一步步地分析源码实现,相信这会让你更加深入理解响应式原理。如果还有对响应式原理不大熟悉的小伙伴,可以参考下我的写的数据驱动。

示例

考虑如下代码:

// contentA 和 contentB, isShow都是data中定义的数据  <div>   <div id="A" v-if="isShow">{{ contentA }}</div>     <div id="B" v-else>{{ contentB }}</div>     <button @click="toggle">toggle</button> // toggle用来改变isShow的状态     <button @click="changeA">changeA</button> // 改变contentA     <button @click="changeB">changeB</button> // 改变contentB  </div>  data () {   isShow: true,     contentA: '',     contentB: ''  },  methods: {   toggle () { this.isShow = !this.isShow },     changeA () { this.contentA += 'a' },     changeB () { this.contentB += 'b' }  } 复制代码

我们接下来以元素的ID代指两个元素。 我们知道,在组件初始化时,都会执行mountComponent函数,从而new Watcher(渲染watcher),当生成一个渲染wathcer的时候就会执行render函数,从而访问到组件上的数据,因为当前组件上isShow是true,所以B元素是不会进行渲染的,所以只会访问到contentA的值,所以就只触发了contetA的getter,所以对于contentA所持有的Dep实例就会收集到当前的渲染wathcer。(为了更好地看到效果,小伙伴们可以在源码的defineReactive的set函数里打上断点,进行调试)
此时如果我们点击changeA,会触发对应的setter函数,点击changeB则不会有任何反应,因为根本就没做依赖收集。
接着我们点击toggle,切换isShow的状态,让B显示A消失,此时如果我们在点击changeA,并没有走到setter中,刚刚明明已经做了依赖收集了,为啥不触发了?但是大家想想这是合理的吗?我觉得是很合理的设计,一个元素都已经不渲染了,为啥还需要对相关的watcher进行派发更新呢?这对性能也是一种消耗。接下来我们一起来分析一下,vue是怎么做到的。

删除依赖

get () {     pushTarget(this)     let value     const vm = this.vm     try {       value = this.getter.call(vm, vm)     } catch (e) {       if (this.user) {         handleError(e, vm, `getter for watcher "${this.expression}"`)       } else {         throw e       }     } finally {       // "touch" every property so they are all tracked as       // dependencies for deep watching       if (this.deep) {         traverse(value)       }       popTarget()       this.cleanupDeps() // 进行依赖的删除     }     return value   }   /**    * Add a dependency to this directive.    */   addDep (dep: Dep) {     const id = dep.id     if (!this.newDepIds.has(id)) {       this.newDepIds.add(id)       this.newDeps.push(dep)       if (!this.depIds.has(id)) {         dep.addSub(this)       }     }   } 复制代码

在分析删除依赖收集之前,我们还是先看看addDep函数具体逻辑,这跟后面分析的依赖删除有着密切的关联。
addDep的时机就是依赖收集的时候,当执行dep.depend()的时候,实际上就是执行了watcher.addDep逻辑

  • 首先会判断当前的dep实例的id是否存在于watcher.newDepIds中,不存在则将dep添加到newDeps中,dep.id添加到newDepsIds中

  • 接着会继续判断id是否存在于depIds中,如果不存在,则执行dep.addSub(this),就是把当前的watcher添加到dep.subs数组中

对应我们示例代码的首次渲染而言,这两个判断逻辑都会进入,所以此时会有下面两个结果。

  • 渲染watcher的newsDep数组会存放了contentA所持有的Dep实例,newsIds则存放了对应的dep.id

  • contentA所持有的Dep实例的subs数组中也存有渲染Watcher。

对于渲染Wachter而言,进行new操作的时候,就会执行this.get方法,从而执行了render函数,触发了依赖收集,最后走到了this.cleanupDeps函数,具体代码如下:

/**    * Clean up for dependency collection.    */ cleanupDeps () {   let i = this.deps.length // this.deps 存放旧的dep实例数组   while (i--) {     const dep = this.deps[i]     if (!this.newDepIds.has(dep.id)) {       dep.removeSub(this)     }   }   // 每次收集完依赖之后将 newDeps 和deps交换 newDepIds跟depIds交换, 接着对newDeps和newDepIds清空   let tmp = this.depIds   this.depIds = this.newDepIds   this.newDepIds = tmp   this.newDepIds.clear()   tmp = this.deps   this.deps = this.newDeps   this.newDeps = tmp   this.newDeps.length = 0 } 复制代码

对于示例代码首次依赖收集的时候,会走以下流程

  • this.deps一开始是一个空数组,所以直接跳过while语句

  • 之后就会将newDeps跟deps, newDepIds 和depIds进行交换,从而保证了本次的newDeps跟newDepIds是下次依赖收集时,作为新的deps跟depIds,从而比较两次依赖收集时的差异,进行删除依赖的操作。

接着当我点击toggle按钮,进行切换的时候,这个时候A消失,B显示了出来,因为在切换的时候isShow的发生了变化,所以就会触发对应的setter函数,因为dep实例中订阅了渲染wather,所以执行dep.notify(),wathcer.update(),页面又会重新进行渲染,进而又执行了render函数。此时A是消失的,B是显示,在render的过程中,只能访问到B上的数据,A的数据没访问到,又重新走了一遍依赖收集的过程。

toggle --> setter --> dep.notify --> wather.update --> render --> 依赖收集(getter) --> watcher.addDep 复制代码

对于我们示例代码的切换更新阶段addDep具体逻辑如下:

  • 因为当前的dep实例是对应B的contentB所持有的,所以是一个新的dep,this.newDepIds是一个空数组,所以会将当前的dep实例跟id都添加到newDepIds跟newDeps中

  • this.depIds此时存放的只有上次A的contentA所持有的depId,所以也会将当前的渲染wathcer添加到dep.subs数组中

接着执行cleanupDeps函数,流程如下:

  • this.deps中存放了上次A中contentA所持有的dep实例,执行while语句,因为this.newDepIds中现在只有B中contentB所持有的depId,所以会执行到dep.removeSub(this)函数,removeSub的作用就把watcher从dep.subs数组中删除

  • 接下来还是将newDeps和deps交换 newDepIds跟depIds交换,接着对newDeps和newDepIds清空。

此时我们在点击changeA按钮,执行代码

this.contentA += 'a' 复制代码

因为contentA被改变了,所以会触发setter函数,进而执行dep.notify()函数

notify () {   // stabilize the subscriber list first   const subs = this.subs.slice()   if (process.env.NODE_ENV !== 'production' && !config.async) {     // subs aren't sorted in scheduler if not running async     // we need to sort them now to make sure they fire in correct     // order     subs.sort((a, b) => a.id - b.id)   }   for (let i = 0, l = subs.length; i < l; i++) {     subs[i].update()   } } 复制代码

但是此时dep.subs数组为空,因为在cleanupDeps函数中,我们执行了dep.removeSub(this),把渲染wathcer从contentA的dep.subs中删除掉,所以执行notify时相当于一个空函数,并不会触发视图的渲染,从而达到了性能优化的细节处理。

总结


主要逻辑在于clearupDeps函数,每次进行收集依赖的时候,都会将当前watcher中新的newDeps跟旧的deps作对比,比较出两者之间的差异之后,如果旧的Dep不在新的newDeps数组中,就把dep所订阅的watcher从subs数组中删除掉,从而删除掉依赖。至此,删除依赖的逻辑我们就分析完了,希望对大家有所帮助。文中有不对的地方,还望指出!!


作者:卑微前端
链接:https://juejin.cn/post/6878846459765063687


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐