观察者模式实现图片预加载,并开放事件监听接口

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 =

以上是关于观察者模式实现图片预加载,并开放事件监听接口的主要内容,如果未能解决你的问题,请参考以下文章

Spring事件监听机制及观察者模式

Spring事件监听机制及观察者模式

Delphi的基于接口(IInterface)的多播监听器模式(观察者模式 ),利用RTTI实现Delphi的多播事件代理研究

JS代理模式实现图片预加载

图片懒加载和预加载

java中jna使用回调实现事件监听器——观察者模式