观察者模式实现图片预加载,并开放事件监听接口
Posted hans774882968
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了观察者模式实现图片预加载,并开放事件监听接口相关的知识,希望对你有一定的参考价值。
需求
实现图片预加载功能。在每张图片加载成功和加载失败时,分别需要调用loadProgress和loadError函数;图片加载完毕后需要调用loadComplete函数。
可选:
- 以上3个接口函数可以随时切换,并支持多次预加载。
- 以上3个接口函数拓展为”事件“,可添加任意多个事件监听器。
技术栈:vue3。一开始之所以选vue3,是期望vue能比较方便地把预加载所得Image对象插入到DOM。我探究了许久,最后宣布期望落空。下文会探讨这一问题。
我们实现一个loader,并期望它可以这么用:
let loader = new Loader()
loader.addEvent(Loader.LOAD_PROGRESS, loadProgress1)
loader.addEvent(Loader.LOAD_PROGRESS, loadProgress2)
loader.addEvent(Loader.LOAD_COMPLETE, loadComplete)
loader.addEvent(Loader.LOAD_ERROR, loadError)
loader
.load(['imgs/1.png', 'imgs/2.png'])
.then(() =>
// 第2次预加载
loader.setEvent(Loader.LOAD_COMPLETE, loadComplete2)
return loader.load(['imgs/4.png', 'imgs/3.png'])
)
.then(() =>
// 第3次预加载
return loader.load(['imgs/4.png', 'imgs/5.png'])
)
.then(() =>
// 第4次预加载
return loader.load(['imgs/1.png'])
)
监听器函数大概长这样
let loadComplete = resp =>
Vue.nextTick(() =>
console.log(resp.msg, `加载成功图片数:$resp.sucCount,失败数:$resp.failCount`)
this.imgData = resp.data
this.drawCanvas()
)
所有监听器函数都有且只有1个参数:resp
。这个参数包含所有相关的数据。resp
的格式:
URL: url, progress: 加载成功图片数占图片总数的比例//单张图片加载成功
errURL: url, msg: '加载失败!'//单张图片加载失败
data: [
img: Image实例1, url: 图片url1, succeed: 是否加载成功1
,
img: Image实例2, url: 图片url2, succeed: 是否加载成功2
],
msg: '加载完成!',
sucCount: 加载成功图片数,
failCount: 加载失败图片数
//所有图片加载完成
我们写2个js文件,img_loader.js
是Loader的实现,观察者模式实现图片预加载.js
是Loader的使用。
Loader的实现
首先实现一个EventListener,就是标准的观察者模式。Loader则是使用一个EventListener对象来进行事件触发。我的观点和参考链接是不同的,参考链接在Loader里直接实现观察者模式,而我认为分离出来比较好。
load函数的整体框架:
imgs.forEach((url) =>
let im = new Image()
im.onload = () => ...
im.onerror = () => ...
im.src = url
)
因为是异步操作,所以我们需要用Promise封装一下,因此load函数应该返回一个Promise对象。
一开始我写得很丑很丑(被注释的那段代码),后来发现用Promise.all
就可以写出画风正常的代码了。
load(imgs)
let sucCount = 0
return Promise.all(imgs.map(url =>
return new Promise((resolve, reject) =>
let img = new Image()
img.onload = () => resolve(img, URL: url, progress: (++sucCount) / imgs.length)
img.onerror = () => reject(img, errURL: url, msg: '加载失败!')
img.src = url
).then(res =>
this.e.trigger(Loader.LOAD_PROGRESS, null, res)
return img: res.img, url, succeed: true
, err =>
this.e.trigger(Loader.LOAD_ERROR, null, err)
return img: err.img, url, succeed: false
)
)).then(imgData =>
this.e.trigger(Loader.LOAD_COMPLETE, null,
data: imgData,
msg: '加载完成qwq!',
sucCount,
failCount: imgs.length - sucCount
)
)
加载成功走到fulfilled分支,失败则走到rejected分支,如此保证Promise数组每个元素都是fulfilled的Promise对象。你看,相比于参考链接的代码,是不是简洁多了(逃)~
Image对象插入到DOM
我找了很久,vue似乎没有把htmlElement和HTML绑定起来的办法。所以最后就直接操作DOM了……
网上各种劣质资料(别问,问就是csdn无🐎)只展示了一个办法:使用canvas把图片转Base64。这个办法需要克服跨域问题,太麻烦了,还是算了……
输出(部分)
图片加载失败:imgs/4.png,消息:加载失败!
当前加载成功图片:imgs/3.png
当前加载进度:50%
加载完成qwq! 加载成功图片数:1,失败数:1
效果:把某张预加载好的图片画进canvas;点击按钮,则把预加载好的HTMLElement插入DOM。
代码
HTML
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>观察者模式实现图片预加载</title>
<!--<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">-->
<!--<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">-->
<link rel="stylesheet" type="text/css" href = "./观察者模式实现图片预加载.css" />
<!--<script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js"></script>-->
<script src="https://unpkg.com/vue@3.0.5/dist/vue.global.js"></script>
<!--<script src="https://unpkg.com/element-ui/lib/index.js"></script>-->
</head>
<body>
<div id="app">
<canvas id="canvas" ref="canvas" width="400" height="400"></canvas>
<div class="right">
<div><button @click="showImgs">点击展示图片</button></div>
<div class="container" ref="imgContainer"></div>
</div>
</div>
<script src="./img_loader.js"></script>
<script src="./观察者模式实现图片预加载.js"></script>
</body>
</html>
CSS
body
margin: 0;
background-color: wheat;
div
box-sizing: border-box;
#app
display: flex;
align-items: flex-start;
#canvas
border: 1px solid blue;
.container
border: 1px solid red;
display: grid;
grid-template-columns: repeat(2,1fr);
img_loader.js
"use strict";
class EventListener
constructor()
this.listener =
addEvent(type, callback)
if (!this.listener[type])
this.listener[type] = []
this.listener[type].push(callback)
removeEvent(type, callback)
if (!this.listener[type])
this.listener[type] = []
let idx = this.listener[type].indexOf(callback)
if (~idx) this.listener[type].splice(idx, 1)
clearEvents(type)
this.listener[type] = []
trigger(type, context, ...args)
for (let cb of this.listener[type]) cb.apply(context, args)
class Loader
static LOAD_PROGRESS = Symbol()
static LOAD_COMPLETE = Symbol()
static LOAD_ERROR = Symbol()
constructor()
this.e = new EventListener()
addEvent(type, callback)
this.e.addEvent(type, callback)
setEvent(type, callback)
this.e.clearEvents(type)
this.e.addEvent(type, callback)
// load(imgs)
// return new Promise(resolve =>
// let singleLoadedHandle = (succeed, i) =>
// if (succeed) this.sucCount++
// this.loadStates[i] = succeed
// if ((++finished) >= imgs.length) resolve()
//
// this.sucCount = 0
// let finished = 0
// this.loadStates = []
// this.imgData = imgs.map((url, i) =>
// let img = new Image()
// img.onload = () =>
// singleLoadedHandle(true, i)
// this.e.trigger(Loader.LOAD_PROGRESS, null,
// URL: url, progress: this.sucCount / imgs.length
// )
//
// img.onerror = () =>
// singleLoadedHandle(false, i)
// this.e.trigger(Loader.LOAD_ERROR, null, errURL: url, msg: '加载失败!')
//
// img.src = url
// return img
// )
// ).then(() =>
// this.e.trigger(Loader.LOAD_COMPLETE, null,
// data: this.imgData.map((img, i) => (
// img, url: imgs[i], succeed: this.loadStates[i]
// )),
// msg: '加载完成!',
// sucCount: this.sucCount,
// failCount: imgs.length - this.sucCount
// )
// )
//
load(imgs)
let sucCount = 0
return Promise.all(imgs.map(url =>
return new Promise((resolve, reject) =>
let img = new Image()
img.onload = () => resolve(img, URL: url, progress: (++sucCount) / imgs.length)
img.onerror = () => reject(img, errURL: url, msg: '加载失败!')
img.src = url
).then(res =>
this.e.trigger(Loader.LOAD_PROGRESS, null, res)
return img: res.img, url, succeed: true
, err =>
this.e.trigger(Loader.LOAD_ERROR, null, err)
return img: err.img, url, succeed: false
)
)).then(imgData =>
this.e.trigger(Loader.LOAD_COMPLETE, null,
data: imgData,
msg: '加载完成qwq!',
sucCount,
failCount: imgs.length - sucCount
)
)
观察者模式实现图片预加载.js
"use strict";
function main()
let app =
data()
return
imgData: null,
inserted: false
,
methods:
drawCanvas()
let canvas = this.$refs.canvas
let ctx = canvas.getContext('2d')
let img = this.imgData[1].img
let x = (canvas.width - img.width) / 2, y = (canvas.height - img.height) / 2
ctx.drawImage(img, x, y)
,
// 在适当时机展示出预加载好的图片
showImgs()
if (this.inserted) return
this.inserted = true
let container = this.$refs.imgContainer
let elements = this.imgData.map(dat =>
if (dat.succeed)
dat.img.title = `$dat.url`
return dat.img
let p = document.createElement('p')
p.innerText = `$dat.url加载失败QAQ`
return p
)
elements.forEach(ele => container.appendChild(ele))
,
created()
let loadProgress1 = resp =>
console.log(`当前加载成功图片:$resp.URL`)
let loadProgress2 = resp =>
// 可以修改为与进度条更新有关的代码
let prog = Math.round(resp.progress * 100)
console.log(`当前加载进度:$prog%`)
let loadComplete = resp =>
Vue.nextTick(() =>
console.log(resp.msg, `加载成功图片数:$resp.sucCount,失败数:$resp.failCount`)
this.imgData = resp.data
this.drawCanvas()
)
let loadError = resp =>
// 可以修改为与用户提示有关的代码
console.log(`图片加载失败:$resp.errURL,消息:$resp.msg`)
let loadComplete2 = resp =以上是关于观察者模式实现图片预加载,并开放事件监听接口的主要内容,如果未能解决你的问题,请参考以下文章
Delphi的基于接口(IInterface)的多播监听器模式(观察者模式 ),利用RTTI实现Delphi的多播事件代理研究