标签间通信

两个标签之间的通信可以通过BroadcastChannel来实现

// 创建一个广播频道
const channel = new BroadcastChannel('channelName')

// 监听消息
channel.onmessage = (event) => {
  console.log(event.data)
}

在另一个浏览器标签中,我们可以通过以下代码向频道发送消息:

// 创建一个广播频道
const channel = new BroadcastChannel('channelName')

// 发送消息
channel.postMessage('Hello, World!')

以上两个信道之间的通信是双向的,也就是说,一个信道可以发送消息,另一个信道可以接收消息

但是两个信道的信道名称必须相同,否则无法通信

拖拽API

先看一下效果吧

课程表

  • 语文
  • 数学
  • 英语
  • 物理
  • 化学
  • 生物
  • 历史
  • 地理
  • 政治
  • 体育
  • 音乐
上午
星期一星期二星期三星期四星期五星期六星期天
上午

API

在HTML5的规范中,我们可以通过为元素增加 draggable="true" 来设置此元素是否可以进行拖拽操作,其中图片、链接默认是开启拖拽的。

涉及到拖拽的api如下

  1. 拖拽元素的事件监听:(应用于拖拽元素)
事件名说明
ondragstart拖拽开始时触发
ondragleave当鼠标离开拖拽元素时调用
ondragend拖拽结束时触发
ondrag整个拖拽过程都会调用
  1. 拖拽目标元素的事件监听:(应用于目标元素)
事件名说明
ondragenter拖拽元素进入目标元素时触发
ondragover拖拽元素在目标元素上移动时触发
ondrop拖拽元素在目标元素上释放时触发

源码

<template>
  <div class="container">
    <p class="title">课程表</p>
    <div
      class="main"
      @dragstart="dragStart"
      @dragover="dragOver"
      @drop="dragDrop"
      @dragenter="dragEnter"
    >
      <div class="slide">
        <ul data-allow="true" data-warp="true">
          <li style="background-color: #3eaf7c" data-effect="copy" draggable="true">语文</li>
          <li style="background-color: #62a238" data-effect="copy" draggable="true">数学</li>
          <li style="background-color: #f80000" data-effect="copy" draggable="true">英语</li>
          <li style="background-color: #768e9d" data-effect="copy" draggable="true">物理</li>
          <li style="background-color: #e7c837" data-effect="copy" draggable="true">化学</li>
          <li style="background-color: #ad21b2" data-effect="copy" draggable="true">生物</li>
          <li style="background-color: #3eaf7c" data-effect="copy" draggable="true">历史</li>
          <li style="background-color: #9f13a4" data-effect="copy" draggable="true">地理</li>
          <li style="background-color: #12e122" data-effect="copy" draggable="true">政治</li>
          <li style="background-color: #19968c" data-effect="copy" draggable="true">体育</li>
          <li style="background-color: #af3e3e" data-effect="copy" draggable="true">音乐</li>
        </ul>
      </div>
      <div class="right">
        <div class="top">
          <div class="am">
            上午
          </div>
          <div class="table">
            <table>
              <tr>
                <th>星期一</th>
                <th>星期二</th>
                <th>星期三</th>
                <th>星期四</th>
                <th>星期五</th>
                <th>星期六</th>
                <th>星期天</th>
              </tr>
              <tr v-for="(_item, index) in 4" :key="index">
                <td data-allow="true" v-for="_ in 7"></td>
              </tr>
            </table>
          </div>
        </div>
        <div class="bottom">
          <div class="pm">
            上午
          </div>
          <div class="table">
            <table>
              <tr v-for="(_item, index) in 4" :key="index">
                <td data-allow="true" v-for="_ in 7"></td>
              </tr>
            </table>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script>
export default {
  name: 'Drag',
  data() {
    return {
      // 被拖拽的元素
      dragElement: null,
    }
  },
  methods: {
    // 拖拽开始
    dragStart(e) {
      // 判断是否允许拖拽
      if (!this.isAllowDrag(e.target)) {
        e.preventDefault()
        return
      }

      // 设置被拖拽的元素
      this.dragElement = e.target
      // 设置拖拽的状态为move
      e.dataTransfer.effectAllowed = e.target.dataset.effect
    },
    // 拖拽移动
    dragOver(e) {
      // 阻止默认事件 使tr和td可以接收拖拽
      e.preventDefault()
    },
    // 拖拽进入
    dragEnter(e) {
      // 移除所有元素的背景颜色
      this.removeDropHover()
      // 改变被进入元素的背景颜色
      if (e.target.dataset.allow === 'true' && this.isAllowDrop(e.target)) {
        e.target.classList.add('drop-hover')
      }
    },
    // 拖拽放置
    dragDrop(e) {
      // 移除所有元素的背景颜色
      this.removeDropHover()
      // 复制拖拽元素 如果是包裹元素则不复制
      if (this.isAllowDrop(e.target) && this.dragElement && !e.target.dataset.warp) {
        e.target.appendChild(this.dragElement.cloneNode(true))
        this.dragElement = null
      }
      // 如果是包裹元素, 则移除原来的元素
      if (this.isAllowDrop(e.target) && this.dragElement && e.target.dataset.warp) {
        // 将原来的元素移除
        this.dragElement.remove()
      }
    },
    // 移除拖拽元素的背景颜色
    removeDropHover() {
      document.querySelectorAll('.drop-hover').forEach((item) => {
        item.classList.remove('drop-hover')
      })
    },
    // 判断是否允许被放置
    isAllowDrop(target) {
      if (!target) return false
      // 是否设置了允许
      const isAllow = target.dataset.allow === 'true'
      // 是否允许包裹
      const isWarp = target.dataset.warp === 'true'
      // 是否有子元素 但是允许包裹的元素不算
      const hasChild = target.children.length === 0 || isWarp

      return isAllow && hasChild
    },
    // 元素是否可以被拖拽
    isAllowDrag(target) {
      // 判断有没有draggable属性
      return target && target?.getAttribute?.('draggable')
    },
  },
}
</script>
<style scoped>
  .title {
    text-align: center;
    font-size: 22px;
  }

  .drop-hover {
    background-color: #1dbb27;
  }

  .main {
    display: flex;
    justify-content: space-between;
    gap: 15px;

    --main-height: 470px;

    .slide {
      width: 10%;
      height: var(--main-height);
      background-color: #f0f0f0;

      ul {
        margin: 0;
        height: var(--main-height);
        padding: 5px;

        li {
          text-align: center;
          list-style: none;
          background-color: #fff;
          margin-bottom: 10px;
          cursor: pointer;
          user-select: none;
        }
      }
    }

    .right {
      width: 90%;
      height: var(--main-height);
      box-sizing: border-box;
      padding: 10px;
      display: flex;
      justify-content: space-between;
      flex-direction: column;
      background-color: #f0f0f0;

      .table {
        width: 92%;

        table {
          width: 100%;

          li {
            list-style: none;
            text-align: center;
            height: 30px;
            line-height: 30px;
          }

          th {
            padding: 5px;
            width: 80px;
            height: 40px;
            font-size: 14px;
          }

          td {
            box-sizing: border-box;
            padding: 5px;
            height: 45px;
          }
        }
      }

      .top {
        width: 100%;
        height: 250px;
        display: flex;
        justify-content: space-between;

        .am {
          width: 8%;
          height: 180px;
          background-color: #9ea9b4;
          display: flex;
          justify-content: center;
          align-items: center;
          writing-mode: vertical-rl;
          letter-spacing: 15px;
          align-self: flex-end;
        }
      }

      .bottom {
        display: flex;
        justify-content: space-between;

        .pm {
          width: 8%;
          height: 180px;
          background-color: #9ea9b4;
          display: flex;
          justify-content: center;
          align-items: center;
          writing-mode: vertical-rl;
          letter-spacing: 15px;
          align-self: flex-end;
        }

        .table {
          width: 92%;

          table {
            width: 100%;
            margin: 0;

            td {
              width: 80px;
            }
          }
        }
      }
    }
  }
</style>

IntersectionObserver API

这个api可以用来监听元素是否进入视口, 用来实现懒加载图片, 无限滚动等效果

使用方法

通过IntersectionObserver构造函数创建一个新的观察者实例, 并传入一个回调函数, 这个回调函数会在被观察的元素进入或者离开视口时被调用

const observer = new IntersectionObserver(callback, options)

其中

  • callback: 当被观察的元素进入或者离开视口时被调用的回调函数
  • options: 一个配置对象, 用来配置观察者实例的一些选项

options的属性如下:

属性名说明默认值
root用来指定根元素, 如果不指定, 则默认为视口null
rootMargin用来指定根元素的边界, 可以是一个字符串或者一个数组'0px'
threshold一个观察目标的可见比例, 一个数组, 可以是0-1之间的任意值0

threshold 设为0时, 被观察的元素冒头就会执行回调的代码, 设为1时, 当该元素完全可见时才执行

图片懒加载 + 触底加载

现在来看一个懒加载拖的例子:

加载中...

当图片进入视口范围时, 将data-src的值赋给src, 实现图片懒加载

当然了, vue提供了v-lazy指令, 也可以实现图片懒加载😂🤣😅😅😅😅😅😅

代码:

<template>
  <div class="container">
    <div class="tool">
      <div class="top-line"></div>
      <label for="threshold">threshold: </label>
      <input id="threshold" v-model="threshold" />
      <label for="rootMargin">rootMargin: </label>
      <input id="rootMargin" v-model="rootMargin" />
      <button @click="refresh">刷新内容</button>
    </div>
    <div :key="mainKey" class="main">
      <img
        v-for="item in 100"
        src="./images/wall.jpg"
        :data-src="`https://picsum.photos/200/300?random=${Math.random()}`"
        alt=""
        :key="item"
      >
    </div>
    <div class="loading">
      <span>加载中...</span>
    </div>
  </div>
</template>
<script>
export default {
  name: 'IntersectionObserver',
  data() {
    return {
      mainKey: 100,
      imageCOunt: 100,
      threshold: 0.1,
      rootMargin: 0,
    }
  },
  mounted() {
    this.handleObserve()
    this.handleLoad().observe(document.querySelector('.loading'))
  },
  methods: {
    // 创建一个 IntersectionObserver 实例
    createObserver() {
      const observer = new IntersectionObserver((entries) => {
        // entries 是一个数组,包含所有被观察的元素
        entries.forEach((entry) => {
          // 当图片进入视口时,加载图片
          if (entry.isIntersecting) {
            const img = entry.target
            img.src = img.dataset.src
            // 加载完成后,取消观察
            observer.unobserve(img)
          }
        })
      }, {
        // 这个属性用来配置交叉观察器的根元素,如果不设置,默认为视口
        root: null,
        // 这个属性用来配置根元素的边界区域, 相当于扩大或缩小视口的大小
        rootMargin: this.rootMargin + 'px',
        // 这个属性用来配置目标元素与根元素相交时的交叉区域的比例
        threshold: this.threshold,
      })

      return observer
    },
    // 监听图片
    observeImages(observer) {
      const images = document.querySelectorAll('img')
      images.forEach((img) => {
        observer.observe(img)
      })
    },
    // 刷新列表
    refresh() {
      this.mainKey += 1
      // 等待页面渲染完成后再执行
      this.$nextTick(() => {
        this.handleObserve()
      })
    },
    // 监听加载
    handleLoad() {
      return new IntersectionObserver((entries) => {
        entries.forEach((entry) => {
          this.handleScroll()
          // 重新监听新增的图片
          this.handleObserve()
        })
      }, {
        root: null,
        rootMargin: '0px',
        threshold: 0,
      })
    },
    // 触底加载
    async handleScroll() {
      await new Promise((resolve) => {
        setTimeout(() => {
          resolve()
        }, 2000)
      })
      this.imageCOunt += 10
    },
    // 监听
    handleObserve() {
      const observer = this.createObserver()
      this.observeImages(observer)
    },
  }
}
</script>

<style scoped>
@keyframes line {
  from {
    width: 0;
  }
  to {
    width: 100%;
  }
}

.top-line {
  position: sticky;
  top: 0;
  height: 5px;
  background-color: #e1732e;
  border-radius: 5px;
  z-index: 999;
  animation: line 5s linear;
  animation-timeline: scroll(y);
}

.container {
  width: 100%;
  height: 400px;
  overflow: auto;
  position: relative;

  .tool {
    top: 0;
    background-color: wheat;
    padding: 5px 0;
    position: sticky;
  }

  .main {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    align-items: center;
    gap: 10px;

    img {
      width: 120px;
      height: 120px;
    }
  }
  
  .loading {
    display: flex;
    justify-content: center;
    align-items: center;
  }
}
</style>

触底加载原理差不多, 观察loading元素, 当loading元素进入视口时, 触发加载事件, 加载完成后, 重新监听新增的图片

RequestIdleCallback API

RequestIdleCallback 适用于一些非急迫的任务,这些任务通常不会影响用户的交互体验或页面的渲染。

当浏览器没有执行渲染、事件处理等关键任务时, 可以将回调函数加入事件队列执行

RequestIdleCallback 的最大优势是它能够确保浏览器优先处理渲染和用户交互的任务,避免阻塞用户的操作或页面的渲染。

RequestIdleCallback 的基本语法

window.requestIdleCallback(callback, options)

  • callback: 一个回调函数,表示在浏览器空闲时执行的任务。
  • options(可选):一个配置对象,可以设置超时时间。

参数详解:

  • callback

这个回调函数会在浏览器空闲时执行。该回调函数有一个参数 deadline,这是一个 IdleDeadline 对象,它提供了一些信息来帮助了解当前的空闲时间和执行回调的限制。

callback(deadline), deadline 对象有以下属性:

timeRemaining(): 返回当前空闲时间剩余的毫秒数。浏览器根据这个时间判断是否继续执行任务,避免占用过多的资源。返回一个浮动的值,单位为毫秒。

timeRemaining() > 0 时,表示还有空闲时间,可以继续执行任务。

timeRemaining() <= 0 时,表示没有空闲时间,需要停止执行任务。

didTimeout: 如果回调因为超时而被执行,返回 true;否则为 false。

  • options

options 是一个可选的配置对象,包含以下属性:

timeout: 设置回调的超时值,单位为毫秒。如果在该时间内没有空闲时间,回调函数会被强制执行。

简单的例子

window.requestIdleCallback((deadline) => {
  // 只有空闲时间,才会执行这个任务
  while (deadline.timeRemaining() > 0) {
    // 执行某个任务(例如懒加载图片或数据处理)
    console.log('任务执行中...');
  }

  if (deadline.timeRemaining() === 0) {
    console.log('没有足够空闲时间,稍后重试...');
  }
}, { timeout: 1000 });

RequestIdleCallback 与 setTimeout 和 setInterval 的对比

特性requestIdleCallbacksetTimeout/setInterval
执行时间空闲时执行,可能会延迟执行按照指定的延迟或周期执行
任务优先级低优先级任务(不会影响渲染或用户输入)不关心渲染或交互任务优先级
适用场景后台任务、懒加载、优化性能定时执行、周期性任务
是否被阻塞只有在浏览器空闲时执行,不会阻塞渲染任务会阻塞当前线程(尤其是同步任务)

使用体验

这个是没有优化的

这个是优化后的

代码:

<template>
<div class="container">
  <button @click="render">{{ startButtonText }} - 点击渲染 {{ max }} 个div</button>
  <input type="text" v-model.number="max" max="9999999">
  <button @click="clear">清空</button>
  <div ref="main" class="main" />
</div>
</template>

<script>
export default {
  name: 'RequestIdleCallback',
  props: {
    type: {
      type: String,
      default: 'unoptimized',
    },
  },
  data() {
    return {
      count: 0,
      max: 20000,
    }
  },
  computed: {
    startButtonText() {
      return this.type === 'unoptimized' ? '未优化前' : '优化后'
    },
  },
  methods: {
    render() {
      if (this.type === 'unoptimized') {
        this.handleRenderWithoutOptimization()
      } else {
        this.handleRenderWithOptimization()
      }
    },
    // 未优化前
    handleRenderWithoutOptimization() {
      Array.from({ length: this.max }).forEach((_, index) => {
        const div = document.createElement('div')
        div.innerText = (index + 1).toString()
        this.$refs.main.appendChild(div)
      })
    },
    // 优化后
    handleRenderWithOptimization() {
      requestIdleCallback((deadline) => {
        // 每帧最多渲染 100 个 div
        const maxTasksPerFrame = 100
        // 当前帧渲染的任务数
        let tasksRendered = 0

        // 只有空闲时间 并且 未渲染完成 ,才会执行这个任务
        while ((deadline.timeRemaining() > 0) && this.count < this.max && tasksRendered < maxTasksPerFrame) {
          const div = document.createElement('div')
          div.innerText = (this.count).toString()
          this.$refs.main.appendChild(div)
          this.count++
          tasksRendered++
        }
        if (this.count < this.max) {
          requestIdleCallback(this.handleRenderWithOptimization)
        }
      })
    },
    // 清空
    clear() {
      this.$refs.main.innerHTML = ''
      this.count = 0
    }
  },
}
</script>

<style scoped>
.container {
  width: 100%;
  height: 350px;
  border: 1px solid #000;
  overflow-y: scroll;

  input {
    width: 60px;
  }
}
</style>
Last Updated:
Contributors: huangdingxin