手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理

目录

一、首先我们自己搭建一个后端服务

1. 根目录下新建一个 server 目录

2. 在 package.json 中增加命令

3. 在 server 目录下新建 index.js

4. 启动服务,查看效果

二、使用 vue-lazyload 插件完成图片懒加载

1. 安装并引入 vue-lazyload

2. 通过 axios 获取后端数据

3. 编写视图模板

三、自定义指令的写法与 v-lazy 基本设计

1. 自定义指令的写法

2. v-lazy 基本设计

四、完成图片实例创建与 loading 状态显示

五、整体思路概括


一、首先我们自己搭建一个后端服务

1. 根目录下新建一个 server 目录

值得注意的是我们在创建项目时,要选择 vue2 版本,因为 vue-lazyload 现在还不支持 vue3,如果在做 vue3 项目时需要用图片懒加载可以下载黄轶老师的 vue3-lazy。

在 server 目录下执行命令:

npm init-y
yarn add express -S
yarn add nodemon global

nodemon 是用来监听我们的 node 服务

2. 在 package.json 中增加命令

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第1张图片

3. 在 server 目录下新建 index.js

编写如下代码:

const express = require('express')
const { resolve } = require('path')
const { readFileSync } = require('fs')

const app = express()

//解决跨域
app.all('*',(req, res, next) => {
    res.header('Access-Control-Allow-Origin', '*')
    res.header('Access-Control-Allow-methods', 'POST,GET')
    next()
})
app.get('/images/:filename',(req, res) => { //通过这个api访问对应图片
    res.sendFile(resolve(__dirname,'./images/' + req.params.filename))
})

app.get('/imgs', (req, res) => { // 通过 imgs api 访问JSON数据
    const imageData = JSON.parse(readFileSync(resolve(__dirname,'./data/images.json'),'utf-8'))
    res.send(imageData)
})
app.listen(3000,() => { //监听3000端口
    console.log('Welcome to use Express on 3000!!!');
})

在 index.js 中我们引入 express,然后在 3000 端口监听它,在 images 目录下是我们的图片,data 目录下有一个 JSON 文件里面存储着图片相关的信息

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第2张图片

JSON文件的格式是这样的:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第3张图片

然后总共写了十一条数据

4. 启动服务,查看效果

运行 npm run dev,启动服务,在浏览器中输入 localhost:3000/images/1.png

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第4张图片

这样我们就接收到了后端的图片

输入 localhost:3000/imgs

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第5张图片

这样我们的JSON数据也能正常接收到,现在后端服务就搭建好了

二、使用 vue-lazyload 插件完成图片懒加载

1. 安装并引入 vue-lazyload

yarn add vue-lazyload axios -S

然后我们把 项目原本的 components 目录下的 hellohowld.vue 删除,然后把 APP.vue 也做对应的清理,来到 main.js:

import VueLazyload from 'vue-lazyload'

先把他引进来,然后全局注册:

Vue.use(VueLazyload, {
  loading: 'http://localhost:3000/images/default.png',
  preload: '1'
})

这里 loading 就是我们加载时候显示的图片,preload 就是预加载的范围,默认是 1.3,这个可以自己设定

2. 通过 axios 获取后端数据

在 APP.vue 中,我们引入 axios ,然后向后端的 JSON 数据的地址发送请求,然后把返回的结果做一个打印

import axios from 'axios'

export default {
  name: 'App',
  async mounted() {
    const res = await axios('http://localhost:3000/imgs')
    console.log(res);
  },
}

在项目根目录下执行 npm run serve,然后打开控制台:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第6张图片

这样我们 JSON 里的数据就被成功获取到了

3. 编写视图模板

APP.vue 如下:





这里 img 中用的就不是 src 了,而是 v-lazy 指令。

注意:如果有同学运行后报错:Failed to resolve directive: lazy 

这其实是安装的 vue-lazyload 的版本不对,请卸载后重新安装 1.3.4 的版本:

yarn add [email protected]

现在启动项目,页面中就能看到我们的图片懒加载的效果了:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第7张图片

三、自定义指令的写法与 v-lazy 基本设计

1. 自定义指令的写法

我们可以在 main.js 中通过 directive 自定义一个指令:

Vue.directive('lazy', {
  bind (el, binding, vnode) {
    
  }
})

如果采用插件方式注册自定义指令是这样编写的:

const VueLazyload = {
  //options就是use后面的配置
  install (Vue, options) {
    Vue.directive('lazy', {
      bind
    })
  }
}
app.use(VueLazyload)

这两种方式效果是一样的,只不过后一种是插件形式的,在 main.js 中我们通过 app.use() 来注册

2. v-lazy 基本设计

在 src 目录下新建 modules 文件夹,在里面的 index.js 中来写我们的指令:

然后再去 mian.js 中引入我们的插件:

import VueLazyload from './modules/vue-lazy-load'

现在我们就可以在 index.js 中开始编写属于自己的 vue-layload 了

基本模板:

const VueLazyload = {
    install (Vue, options) {
        Vue.directive('lazy', {
            bind (el, bindings, vnode) {
                console.log(el ,bindings, vnode);
            }
        })
    }
}

export default VueLazyload

我们先做一个简单的打印,启动项目看看我们这一套流程对不对:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第8张图片

 从控制台的输出我们就可以看出,el 代表的就是你的自定义指令绑定到谁身上,谁就是这个 el。

我们再看一下 bindings :

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第9张图片

这里有很多属性,比如通过 bindings.value 我们就可以拿到图片的 src。

再看一下 vnode 是个什么东西:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第10张图片

他指的就是 tag 对应的虚拟节点,这个我们基本用不到

现在的重点就是 bind 中我们应该做些什么,在 bind 中我们要做的事情特别多,比如给外层元素绑定滚动事件,把 options 中的信息保存起来等等。所以我们写一个类,这样他的扩展性就非常的好

那么现在我们就改写一下 index.js :

import Lazyload from './Lazyload'

const VueLazyload = {
    install (Vue, options) {

        const LazyClass = Lazyload(Vue)
        const lazyload = new LazyClass(options)

        Vue.directive('lazy', {
            bind: lazyload.xxx
        })
    }
}

export default VueLazyload

我们在当前目录下创建一个 Lazyload.js ,这里面就用来创建这个类,因为这个类肯定要用到 Vue 参数,所以我们定义一个函数 Lazyload ,在这个函数中我们能 return 这个类,这样类就能访问到外层函数的参数了,也是用到了函数柯里化的知识,在 index.js 中定义类 LazyClass 后就生成他的实例对象 lazyload,然后传入 options,在这个实例里有个方法也就是 xxx 他干的就是以前 bind(el, bindings, vnode){...} 这个活 。

那我们这样写正确吗?在 bind 那里还需要调整下 this 指向:

Vue.directive('lazy', {
      bind: lazyload.bindLazy.bind(lazyload)
})

这样的话 bindLazy 里的 this 指向的就是 lazyload,如果不邦定一下那指向的就是当前执行上下文

现在我们要明确的是,bindLazy 这个函数同样要拥有 el,bindings,vnode 这三个参数,然后 Lazyload 这个类的 constructor 里要传入的是 options。下面就来编写 Lazyload.js:

export default function Lazyload (Vue) {

    return class Lazy {
        constructor(options) {
            this.options = options
        }
        bindLazy (el, bindings, vnode) {
            console.log(el, bindings, vnode);
        }
    }
}

可以看到我们在 Lazyload 中返回了一个类,这样写我们整个逻辑就有很强的可扩展性了。我们看一下现在能否正常输出:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第11张图片

 现在我们再输出一下 options:

return class Lazy {
        constructor(options) {
            this.options = options
        }
        bindLazy (el, bindings, vnode) {
            console.log(this.options);
        }
    }

现在就能把我们在 main.js 中传入的配置项打印出来了: 

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第12张图片

现在我们要做的就是在 bindLazy 中绑定事件处理函数:

因为在我们的模板中滚动的盒子是 container ,所以我们要给他添加这个 scroll 的事件

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第13张图片

那现在我们的问题就是如何才能在 bindLazy 中找到这个 container 呢?思路就是去找添加了我们的自定义指令的父级,一级一级向上找,如果哪一个父级的样式里有 overflow:auto | scroll,那就代表是这个 container。

我们新建一个 utils.js ,专门负责去找这个父节点:

export function getScrollParent (el) {
    let parent = el.parentNode
    while(parent) {
        styleOverflow = getComputedStyle(parent,'overflow')
        if (/(scroll) | (auto)/.test(styleOverflow)) {
            return parent
        }
        parent = parent.parentNode
    }
}

现在就来了另一个问题就是我们在自定义指令中执行 bind 的时候,页面的 dom 还没有挂载,我们可以试一下:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第14张图片

在 bindLazy 中我们打印111,然后在 APP.vue 的 mounted 中我们打印222,看一下控制台哪个先输出:

所以我们得在 lazyLoad 前先执行一个 nextTick:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第15张图片

这样再看一下控制台的输出,就是先打印222再打印111了,下面我们在 bindLazy 中监听滚动事件:

bindLazy (el, bindings, vnode) {
            Vue.nextTick().then(function() {
                const scrollParent = getScrollParent(el)
                console.log(scrollParent);
            })
        }

启动项目看一下控制台输出:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第16张图片

控制台打印了很多次,我们不能每一次都去绑定,只需要绑定一次就行,所以再做一下条件判断,然后给父节点添加 scroll 事件:

import { getScrollParent } from "./utils"

export default function Lazyload (Vue) {

    return class Lazy {
        constructor(options) {
            this.options = options
            this.isAddScrollListener = false
        }
        bindLazy (el, bindings, vnode) {
            const self = this
            Vue.nextTick().then(function() {
                const scrollParent = getScrollParent(el)
                if (scrollParent && !self.isAddScrollListener) {
                    scrollParent.addEventListener('scroll',self.handleScroll.bind(self),false)
                }
            })
        }
        handleScroll () {
            
        }
    }
}

注意这里要保存 this 指向,否则在 nextTick 的回调中 this 指向的就是 undefined。

四、完成图片实例创建与 loading 状态显示

因为每张图片都有三种状态,把每张图片都保存为一个实例方便统一管理,所以我们再写一个类专门去处理每一张图片的实例,新建 Lazyimg.js :

export default class Lazyimg {
    constructor({el, src, options, imgRender}) {
        this.el = el
        this.src = src
        this.options = options
        this.imgRender = imgRender
        this.loaded = false
    }
}

每一张图片的实例都需要 el ,也就是绑定的元素,src 就是当前图片的地址,options 就是用户传进来的 loading 和 error 状态下的图片地址,imgRender 是处理图片状态切换的函数,loaded 是有没有加载完毕,如果一张图片加载完了就把这个状态设为 true。

现在我们回到 Lazyload 里,在 Lazy 类中定义一个数组来保存所有图片的实例:

import { getScrollParent } from "./utils"
import LazyImg from "./Lazyimg"

export default function Lazyload (Vue) {

    return class Lazy {
        constructor(options) {
            this.options = options
            this.isAddScrollListener = false
            this.lazyImgpool = []
        }
        bindLazy (el, bindings, vnode) {
            const self = this
            Vue.nextTick().then(function() {
                const scrollParent = getScrollParent(el)
                if (scrollParent && !self.isAddScrollListener) {
                    scrollParent.addEventListener('scroll',self.handleScroll.bind(self),false)
                }

                const lazyImg = new LazyImg({
                    el,
                    src: bindings.value,
                    options: self.options,
                    imgRender: self.imgRender.bind(self)
                })
                self.lazyImgpool.push(lazyImg)
                console.log(self.lazyImgpool);
            })
        }
        handleScroll () {
            
        }

        imgRender () {

        }
    }
}

我们在给对应元素的父节点监听完滚动事件后,就创建这个图片的实例对象,把挂载的元素,图片地址,输入的配置和处理函数都传进去,最后要不这个新创建好的实例放到总的实例池子中。

现在打印一下 lazyImgpool :

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第17张图片

这样生成的所有实例就都在这里面了 

现在我们就去实现 handleScroll 里的逻辑,他就是判断图片是不是出现在了滚动区域内,如果出现在了滚动区域内就让他加载,所以我们在第一次的时候就要执行一下 handleScroll:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第18张图片

我们先让所有图片在可视区间的状态为 false,然后遍历实例数组,如果实例的状态 loaded 为 true,代表他加载过了,那就不执行下面的逻辑,如果没加载那么就判断当前实例位于屏幕的位置,看看他在不在可视区间内,这一步判断是否在可视区间内的函数是图片实例这个类里应该定义的,然后如果他是在可视区间内,就把代表可视区间的状态设为 true,再去执行图片实例加载图片的函数:

handleScroll () {
            let isVisible = false
            this.lazyImgpool.forEach(item => {
                if (!item.loaded) {
                    isVisible = item.checkIsVisible()
                    isVisible && item.loadImg()
                }
            })
        }

那么下一步我们就去图片实例这个类中完成 checkIsVisible 和 loadImg 这两个方法:

checkIsVisible () {
        const { top } = this.el.getBoundingClientRect()
        return top < window.innerHeight * (this.options.preload || 1.3)
    }

checkIsVisible 中的代码不难理解,就是看他在不在屏幕范围内,在的话就返回 true。

下面再看一下 loadImg 的代码:

loadImg () {
        this.imgRender(this, 'loading')
    }

这里我们就调用 imgRender 处理函数,第一个参数就是 LazyImg 这个类,第二个参数就代表想要设置的状态,我们先做个小 demo,先统一把状态设置为 loading 加载中。

现在我们回到 Lazyload 中,编写 imgRender 的代码,这是一个逻辑处理函数,目的是根据传进来的状态同步对应的图片实例的 src 属性:

imgRender (lazyImg, state) {
    const { el, options } = lazyImg
    const { loading, error } = options
    let src = ''
    switch (state) {
    case 'loading':
        src = loading || ''
        break
    case 'error':
        src = error || ''
        break
    default:
        src = lazyImg.src
        break    
    }
    el.setAttribute('src',src)
} 

启动项目:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第19张图片

现在页面上显示的就是 loading 状态下的图片了

现在要实现的就是一个方法,我们叫它 imgLoad ,然后把图片 src 作为参数传进去,如果没问题就执行成功的回调,有问题就执行失败的回调:

loadImg () {
        this.imgRender(this, 'loading')
        
        imgLoad(this.src).then(() => {
            this.imgRender(this, 'ok')
            this.loaded = true
        }, () => {
            this.imgRender(this, 'error')
            this.loaded = true
        })
    }

我们在 utils.js 下定义这个 imgLoad :

export function imgLoad (src) {
    return new Promise ((resolve, reject) => {
        const oImg = new Image()
        oImg.src = src
        oImg.onload = resolve
        oImg.onerror = reject
    })
}

在这里我们返回一个 promise 对象,如果图片 onload 了就执行 resolve,如果图片 error 了就执行 reject。因为我们在 imgRender 中规定如果传入的 state 不是 loading 或者 error 就返回 src 属性,所以这里传入 ok 就直接传入 src。

现在启动项目,查看效果:

手写 vue-lazyload 源码,带你掌握高阶自定义指令的内部实现原理_第20张图片

五、整体思路概括

在 bind 这个函数中我们首先去找滚动的父级元素,然后给这个父级元素绑定事件处理函数,再给每一张图片进行类实例包装,把有用的信息全都放在图片实例里,所有图片的信息组成了一个图片池。当第一次加载的时候手动执行 handleScroll 事件,当我们滚动页面的时候执行的也是 handleScroll ,那他都做了什么事呢?看每一张图片在不在我们可视化区域内,在的话就 load,前提是这张图片没有 load 过。在 imgRender 中做的是对图片渲染的过程,其实就是添加 src 属性。

为什么我们要封装成类呢?

因为每张图片都有很多信息,它的实例,地址,状态,等等。而且每个实例都有它的方法,通过这些方法我们才能判定这个实例在不在可视化区间内,这张图片又怎么去渲染,这些都得放在图片实例里,所以我们才要用到类做一个封装。而 Lazy 类负责的主要是整体的一个滚动和渲染。

你可能感兴趣的:(vue,vue.js,前端,javascript)