前言
Vue-lazy学习总结,学无止境,每天进步一点点,加油
应用层
- 入口文件main.js
// vue-lazyload 图片懒加载 v-lazy
import Vue from 'vue';
import VuelazyLoad from './vue-lazyload';
import loading from './loading.jpg'
import App from './App.vue'
// use方法是一个全局的api 会调用 VuelazyLoad install
Vue.use(VuelazyLoad,{
preLoad: 1.3, // 可见区域的1.3倍
loading, // loading图
}); // use的默认调用就会执行VuelazyLoad的install方法
new Vue({
el:'#app',
render:h=>h(App)
})
- 根组件App.vue
<template>
<div class="box">
<li v-for="img in imgs" :key="img">
<img v-lazy="img">
</li>
</div>
</template>
<script>
import axios from 'axios'; // 基于promise async + await
export default {
data(){
return {imgs:[]}
},
created(){
axios.get('图片请求地址').then(({data})=>{
this.imgs = data;
})
}
}
</script>
<style>
.box {
height:300px;
overflow: scroll;
width: 200px;
}
img{
width: 100px; height:100px;
}
</style>
可以看到,其实用法挺简单的,常规的插件use后,直接在img标签上加v-lazy就可以了,由此我们也可以猜测,其实v-lazy是一个“包着插件的皮,有着自定义指令实现的心”的存在;
个人梳理xmind
实现逻辑
前备知识
- jsApi
- getComputedStyle(dom) 获取dom的属性对象,本文用以得到overflow:scroll的节点
- dom.getBoundingClientRect() 获取距离信息,本文用以获取img距离设备可视区顶部的高度值
- Vue中use插件时,会默认执行插件的istall方法
- 自定义指令的bind生命周期函数会被默认传递三个参数el、binging和vnode,建议官网了解
实现思路(注释是重点)
- 插件的index入口文件简单的做下初始化调用add方法
- vue-lazy默认导出一个高阶函数,调用返回一个类LazyClass,实例的add方法时重点逻辑
export let _Vue;
import Lazy from './lazy';
export default {
<!--vue默认传递参数-->
install(Vue,options){
_Vue = Vue;
const LazyClass = Lazy(Vue);
const lazy = new LazyClass(options)
Vue.directive('lazy',{
bind: lazy.add.bind(lazy)
})
}
}
- Vue中use插件时,会默认执行插件的istall方法,所以我们可以在istall中自定义全局指令,然后在指令的生命周期函数bind中写渲染逻辑
- 注意:bind生命周期中dom还未挂载,所以有获取dom的操作必须包裹在nextTick里
add(el, binding) {
Vue.nextTick(() => {
// 找到带有overflow属性的祖先元素
function scrollParent() {
let parent = el.parentNode;
while (parent) {
if (/scroll/.test(getComputedStyle(parent)["overflow"]))
return parent;
parent = parent.parentNode;
}
}
let parent = scrollParent();
let src = binding.value;
// 监控带有overflow属性的祖先元素的滚动事件;滚动时则调用listener的校验状态等默认事件,是为发布
let listener = new ReactiveListener({
el,
src,
elRenderer: this.elRenderer.bind(this),
options: this.options
});
// 将当前绑定的img对应的观测者保存到持有的数组中,是为订阅;
this.listenerQueue.push(listener);
// 因为每个img上的v-lazy都会执行add方法,所以都会尝试挂载scroll事件,但LazyClass实例的单例的,所以加这个参数做一个优化
if(!this.isBinded){
// 监控带有overflow属性的祖先元素的滚动事件
parent.addEventListener("scroll", this.lazyLoadHandler);
}
// 执行add方法时默认调用一次lazyLoadHandler方法
this.lazyLoadHandler()
});
}
- 滚动时,会触发lazyLoadHandler,是LazyClass上的属性,滚动事件处理函数
- 只做一件事,遍历listenQueue,判断哪些img需要渲染
// throttle是loadsh的节流方法
this.lazyLoadHandler = throttle(() => {
// 用以判断img是否在可渲染区域内
let catIn = false;
this.listenerQueue.forEach(listener => {
// 如果已经在加载,则不作处理
if(listener.state.loading) return
// 判断img是否在可渲染区域内
catIn = listener.checkInView();
// 如在,则执行加载
catIn && listener.load();
});
})
- img的观测者类的结构
class ReactiveListener {
constructor({ el, src, elRenderer, options }) {
this.el = el;
this.src = src;
this.elRenderer = elRenderer;
this.options = options;
this.state = {};
}
// 判断是否渲染
checkInView() {
let {top} = this.el.getBoundingClientRect();
console.log(top);
return top < window.innerHeight * this.options.preLoad
}
load() {
this.elRenderer(this,'loading');
loadImageAsync(this.src,()=>{
this.state.loading = true;
this.elRenderer(this,'loading')
},()=>{
this.elRenderer(this,'error')
});
}
- 实际渲染方法
elRenderer(listener, state) {
let { el } = listener;
let src = "";
switch (state) {
case "loading":
src = listener.options.loading || "";
break;
case "error":
src = listener.options.error || "";
break;
default:
src = listener.src;
break;
}
el.setAttribute("src", src);
}
整体实现思路
后语
看人家源码写的真是巧妙,自认菜鸟一枚,望掘金大神有错直指,虚心求教,有错必改; --19年毕业小菜语
个人简易版源码
# index.js
export let _Vue;
import Lazy from './lazy';
export default {
install(Vue,options){
_Vue = Vue;
const LazyClass = Lazy(Vue);
const lazy = new LazyClass(options)
Vue.directive('lazy',{
bind: lazy.add.bind(lazy)
})
}
}
# lazy.js
import {throttle} from 'lodash';
export default Vue => {
class ReactiveListener {
constructor({ el, src, elRenderer, options }) {
this.el = el;
this.src = src;
this.elRenderer = elRenderer;
this.options = options;
this.state = {};
}
// 判断是否渲染
checkInView() {
let {top} = this.el.getBoundingClientRect();
console.log(top);
return top < window.innerHeight * this.options.preLoad
}
load() {
this.elRenderer(this,'loading');
loadImageAsync(this.src,()=>{
this.state.loading = true;
this.elRenderer(this,'loading')
},()=>{
this.elRenderer(this,'error')
});
}
}
function loadImageAsync(src,resolve,reject) {
let image = new Image();
image.src = src;
image.onload = resolve;
image.onerror = reject;
}
return class LazyClass {
constructor(options) {
this.options = options;
this.listenerQueue = [];
this.bindHandler = false;
// throttle是loadsh的节流方法
this.lazyLoadHandler = throttle(() => {
// 用以判断img是否在可渲染区域内
let catIn = false;
this.listenerQueue.forEach(listener => {
// 如果已经在加载,则不作处理
if(listener.state.loading) return
// 判断img是否在可渲染区域内
catIn = listener.checkInView();
// 如在,则执行加载
catIn && listener.load();
});
})
}
add(el, binding) {
Vue.nextTick(() => {
// 找到带有overflow属性的祖先元素
function scrollParent() {
let parent = el.parentNode;
while (parent) {
if (/scroll/.test(getComputedStyle(parent)["overflow"]))
return parent;
parent = parent.parentNode;
}
}
let parent = scrollParent();
let src = binding.value;
// 监控带有overflow属性的祖先元素的滚动事件;滚动时则调用listener的校验状态等默认事件,是为发布
let listener = new ReactiveListener({
el,
src,
elRenderer: this.elRenderer.bind(this),
options: this.options
});
// 将当前绑定的img对应的观测者保存到持有的数组中,是为订阅;
this.listenerQueue.push(listener);
// 因为每个img上的v-lazy都会执行add方法,所以都会尝试挂载scroll事件,但LazyClass实例的单例的,所以加这个参数做一个优化
if(!this.isBinded){
// 监控带有overflow属性的祖先元素的滚动事件
parent.addEventListener("scroll", this.lazyLoadHandler);
}
// 执行add方法时默认调用一次lazyLoadHandler方法
this.lazyLoadHandler()
});
}
elRenderer(listener, state) {
let { el } = listener;
let src = "";
switch (state) {
case "loading":
src = listener.options.loading || "";
break;
case "error":
src = listener.options.error || "";
break;
default:
src = listener.src;
break;
}
el.setAttribute("src", src);
}
};
};