标签间通信
两个标签之间的通信可以通过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如下
- 拖拽元素的事件监听:(应用于拖拽元素)
事件名 | 说明 |
---|---|
ondragstart | 拖拽开始时触发 |
ondragleave | 当鼠标离开拖拽元素时调用 |
ondragend | 拖拽结束时触发 |
ondrag | 整个拖拽过程都会调用 |
- 拖拽目标元素的事件监听:(应用于目标元素)
事件名 | 说明 |
---|---|
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 的对比
特性 | requestIdleCallback | setTimeout /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>