浅尝MutationObserver|Vue自定义指令

1,949 阅读3分钟

源码之旅

Vue 的自定义指令,用来解决需要对普通 DOM 元素进行操作的问题。

看Element UI库的源码,其中 v-infinite-scroll无限滚动可配置的属性:

image.png

源码传送门:Element & Element-plus

按照自己的理解,截取了基于Vue2.x的部分源码,添加了一些注释,如下:

export default {
  name: 'InfiniteScroll',
   /**
   * @description: 被绑定元素插入父节点时调用inserted
   * @param {*} el 指令所绑定的元素,可以用来直接操作 DOM
   * @param {*} binding 指令属性对象
   * @param {*} vnode Vue 编译生成的虚拟节点
   */  
  inserted(el, binding, vnode) {
    // 该指令的绑定值,则该指令的滚动回调
    const cb = binding.value;  

    const vm = vnode.context;
    // 针对垂直滚动,获取目标节点
    const container = getScrollContainer(el, true);
    // 获取配置的属性值
    const { delay, immediate } = getScrollOptions(el, vm);
    // 监听滚动事件当然需要配合节流呀~
    const onScroll = throttle(delay, handleScroll.bind(el, cb));
    
    // 定义目标DOM
    el[scope] = { el, vm, container, onScroll };

    if (container) {
      // 监听滚动事件
      container.addEventListener('scroll', onScroll);
       
      // 若立即执行加载(默认为true)
      if (immediate) {
         // 为观察DOM树结构发生变化:创建并返回一个观察器实例 MutationObserver
         // 当指定的DOM发生变化时,会执行回调函数 onScroll
        const observer = el[scope].observer = new MutationObserver(onScroll);
        // 配置MutationObserver,开始观察目标节点
        observer.observe(container, { childList: true, subtree: true });
        // 立即执行:当然要执行一次onScroll,不然哪来的变化观察呢
        onScroll();
      }
    }
  },
  unbind(el) {
    const { container, onScroll } = el[scope];
    if (container) {
      // 指令与元素解绑时调用时,移除监听
      container.removeEventListener('scroll', onScroll);
    }
  }
};

可见,针对infinite-scroll-immediate的实现,使用到了MutationObserver()这个构造函数 (补充:Element-plus该属性的实现基本一致)

为什么用MutationObserver

Element官方文档的说明:

infinite-scroll-immediate:是否立即执行加载方法,以防初始状态下内容无法撑满容器。默认为true

无限滚动这个指令,为了解决滚动加载的问题,v-infinite-scroll的绑定值,就是数据的加载回调函数

官方实例:

 <template>
    <ul class="infinite-list" v-infinite-scroll="load" style="overflow:auto">
      <li v-for="i in count" class="infinite-list-item">{{ i }}</li>
    </ul>
</template>
<script>
  import { defineComponent, ref } from 'vue';
  export default defineComponent({
    setup() {
      const count = ref(0);
      // 用于加载滚动数据
      const load = () => {
        count.value += 2;
      };
      return {
        count,
        load,
      };
    },
  });
</script>

立即执行是针对初始状态下内容撑满容器的需求,必然要知道内容DOM的变化,以确认需要执行多少次回调函数:onScroll

MutationObserver是什么

MutationObserver可监视对DOM树所做的更改,作为一个观察者对象,DOM变化执行触发回调,提供了一个接口操作DOM。

废话不多说,链接传送门:

MutationObserver - Web API 接口参考

MutationObserver 监听DOM树变化

  • observer.observe(target[, options])

若源码进行如下改动:会报错

// childList:观察目标节点的子节点的新增和删除
// subtree:观察目标节点的所有后代节点
observer.observe(container, { childList: false, subtree: true });

[Vue warn]: Error in directive infinite-scroll inserted hook: "TypeError: Failed to execute 'observe' on 'MutationObserver': The options object must set at least one of 'attributes', 'characterData', or 'childList' to true."

可知,options对象必须将"attributes"、"characterData"或"childList"中的至少一个设置为true。

  • observer.disconnect()

可停止观察DOM变化,回调函数不再执行,直至再次调用其observe()方法

  • 异步执行

Mutation Observer 是在DOM4中定义的,用于替代 mutation events 的新API,它的不同于events的是,所有监听操作以及相应处理都是在其他脚本执行完成之后异步执行的,并且是所以变动触发之后,将变得记录在数组中,统一进行回调的,也就是说,当你使用observer监听多个DOM变化时,并且这若干个DOM发生了变化,那么observer会将变化记录到变化数组中,等待一起都结束了,然后一次性的从变化数组中执行其对应的回调函数

其他应用

Vue 2.x有关异步更新队列的说明,提到了MutationObserver

Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替

提供了Vue.nextTick(callback),保证在数据变化之后,DOM的更新完成,再执行后续操作:callback

有关$nextTick源码可阅读 Vue 2.x / nextTick

Last but not least

如有不妥,请多指教~