u-lazy-load.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <template>
  2. <view class="u-wrap" :style="{
  3. opacity: Number(opacity),
  4. borderRadius: $u.addUnit(round),
  5. transition: `opacity ${time / 1000}s ease-in-out`
  6. }" :class="'u-lazy-item-' + elIndex">
  7. <view :class="'u-lazy-item-' + elIndex">
  8. <image :style="imgStyle" v-if="!isError" class="u-lazy-item"
  9. :src="isShow ? image : loadingImg" :mode="mode" @load="imgLoaded" @error="loadError" @tap="clickImg">
  10. </image>
  11. <image :style="imgStyle" class="u-lazy-item error" v-else
  12. :src="errorImg" :mode="mode" @load="errorImgLoaded" @tap="clickImg"></image>
  13. </view>
  14. </view>
  15. </template>
  16. <script>
  17. import props from './props.js';
  18. import mixin from '../../libs/mixin/mixin'
  19. import mpMixin from '../../libs/mixin/mpMixin';
  20. /**
  21. * lazyLoad 懒加载
  22. * @description 懒加载使用的场景为:页面有很多图片时,APP会同时加载所有的图片,导致页面卡顿,各个位置的图片出现前后不一致等.
  23. * @tutorial https://uview.d3u.cn/components/lazyLoad.html
  24. * @property {String Number} index 用户自定义值,在事件触发时回调,用以区分是哪个图片
  25. * @property {String} image 图片路径
  26. * @property {String} loading-img 预加载时的占位图
  27. * @property {String} error-img 图片加载出错时的占位图
  28. * @property {String} threshold 触发加载时的位置,见上方说明,单位 px(默认300)
  29. * @property {String Number} duration 图片加载成功时,淡入淡出时间,单位ms(默认)
  30. * @property {String} effect 图片加载成功时,淡入淡出的css动画效果(默认ease-in-out)
  31. * @property {Boolean} is-effect 图片加载成功时,是否启用淡入淡出效果(默认true)
  32. * @property {String Number} round 图片圆角值,单位px(默认0)
  33. * @property {String Number} height 图片高度,注意:实际高度可能受mode参数影响(默认450)
  34. * @property {String Number} mode 图片的裁剪模式,详见image组件裁剪模式(默认widthFix)
  35. * @event {Function} click 点击图片时触发
  36. * @event {Function} load 图片加载成功时触发
  37. * @event {Function} error 图片加载失败时触发
  38. * @example <u-lazy-load :image="image" :loading-img="loadingImg" :error-img="errorImg"></u-lazy-load>
  39. */
  40. export default {
  41. name: 'u-lazy-load',
  42. mixins: [mpMixin, mixin, props],
  43. data() {
  44. return {
  45. isShow: false,
  46. opacity: 1,
  47. time: this.duration,
  48. loadStatus: '', // 默认是懒加载中的状态
  49. isError: false, // 图片加载失败
  50. }
  51. },
  52. computed: {
  53. elIndex(){
  54. return this.$u.guid()
  55. },
  56. getThreshold() {
  57. // 先取绝对值,因为threshold可能是负数,最后根据this.threshold是正数或者负数,重新还原
  58. let thresholdPx = Math.abs(this.threshold);
  59. return this.threshold < 0 ? -thresholdPx : thresholdPx;
  60. },
  61. imgStyle() {
  62. return {
  63. borderRadius: uni.$u.addUnit(this.round),
  64. height: uni.$u.addUnit(this.height)
  65. }
  66. }
  67. },
  68. created() {
  69. // 由于一些特殊原因,不能将此变量放到data中定义
  70. this.observer = {};
  71. // 初始化observers对象来存储所有的observer实例
  72. this.observers = {};
  73. },
  74. watch: {
  75. isShow(nVal) {
  76. // 如果是不开启过渡效果,直接返回
  77. if (!this.isEffect) return;
  78. this.time = 0;
  79. // 原来opacity为1(不透明,是为了显示占位图),改成0(透明,意味着该元素显示的是背景颜色,默认的白色),再改成1,是为了获得过渡效果
  80. this.opacity = 0;
  81. // 延时30ms,否则在浏览器H5,过渡效果无效
  82. setTimeout(() => {
  83. this.time = this.duration;
  84. this.opacity = 1;
  85. }, 30)
  86. },
  87. // 图片路径发生变化时,需要重新标记一些变量,否则会一直卡在某一个状态,比如isError
  88. image(n) {
  89. if (!n) {
  90. // 如果传入null或者'',或者undefined,标记为错误状态
  91. this.isError = true;
  92. } else {
  93. this.init();
  94. this.isError = false;
  95. }
  96. }
  97. },
  98. // #ifdef VUE3
  99. emits: ['click','load','error'],
  100. // #endif
  101. methods: {
  102. // 用于重新初始化
  103. init() {
  104. this.isError = false;
  105. this.loadStatus = '';
  106. },
  107. // 点击图片触发的事件,loadlazy-还是懒加载中状态,loading-图片正在加载,loaded-图片加加载完成
  108. clickImg() {
  109. let whichImg = '';
  110. // 如果isShow为false,意味着图片还没开始加载,点击的只能是最开始的占位图
  111. if (this.isShow == false) whichImg = 'lazyImg';
  112. // 如果isError为true,意味着图片加载失败,这是只剩下错误的占位图,所以点击的只能是错误占位图
  113. // 当然,也可以给错误的占位图元素绑定点击事件,看你喜欢~
  114. else if (this.isError == true) whichImg = 'errorImg';
  115. // 总共三张图片,除了两个占位图,剩下的只能是正常的那张图片了
  116. else whichImg = 'realImg';
  117. // 只通知当前图片的index
  118. this.$emit('click', this.index);
  119. },
  120. // 图片加载完成事件,可能是加载占位图时触发,也可能是加载真正的图片完成时触发,通过isShow区分
  121. imgLoaded() {
  122. // 占位图加载完成
  123. if (this.loadStatus == '') {
  124. this.loadStatus = 'lazyed';
  125. }
  126. // 真正的图片加载完成
  127. else if (this.loadStatus == 'lazyed') {
  128. this.loadStatus = 'loaded';
  129. this.$emit('load', this.index);
  130. }
  131. },
  132. // 错误的图片加载完成
  133. errorImgLoaded() {
  134. this.$emit('error', this.index);
  135. },
  136. // 图片加载失败
  137. loadError() {
  138. this.isError = true;
  139. },
  140. disconnectObserver(observerName) {
  141. // 使用observers对象替代动态属性访问,避免生产环境警告
  142. const observer = this.observers[observerName];
  143. observer && observer.disconnect();
  144. },
  145. },
  146. // #ifdef VUE2
  147. beforeDestroy() {
  148. // 销毁页面时,可能还没触发某张很底部的懒加载图片,所以把这个事件给去掉
  149. // 清理所有的observer实例
  150. this.disconnectObserver('contentObserver');
  151. },
  152. // #endif
  153. // #ifdef VUE3
  154. beforeUnmount() {
  155. // 销毁页面时,可能还没触发某张很底部的懒加载图片,所以把这个事件给去掉
  156. // 清理所有的observer实例
  157. this.disconnectObserver('contentObserver');
  158. },
  159. // #endif
  160. mounted() {
  161. // 此uOnReachBottom事件由mixin.js发出,目的是让页面到底时,保证所有图片都进行加载,做到绝对稳定且可靠
  162. this.$nextTick(() => {
  163. uni.$once('uOnReachBottom', () => {
  164. if (!this.isShow) this.isShow = true;
  165. });
  166. // mounted的时候,不一定挂载了这个元素,延时30ms,否则会报错或者不报错,但是也没有效果
  167. uni.$u.sleep(10).then(() => {
  168. // 使用组件实例方法创建observer,避免传递this参数
  169. this.disconnectObserver('contentObserver');
  170. let contentObserver;
  171. if(this.createIntersectionObserver) {
  172. contentObserver = this.createIntersectionObserver();
  173. }else{
  174. contentObserver = uni.createIntersectionObserver(this);
  175. }
  176. // 要理解这里怎么计算的,请看这个:
  177. // https://blog.csdn.net/qq_25324335/article/details/83687695
  178. contentObserver.relativeToViewport({
  179. bottom: this.getThreshold,
  180. }).observe('.u-lazy-item-' + this.elIndex, (res) => {
  181. if (res.intersectionRatio > 0) {
  182. // 懒加载状态改变
  183. this.isShow = true;
  184. // 如果图片已经加载,去掉监听,减少性能的消耗
  185. this.disconnectObserver('contentObserver');
  186. }
  187. })
  188. // 将observer实例保存到observers对象中
  189. this.observers.contentObserver = contentObserver;
  190. })
  191. })
  192. }
  193. }
  194. </script>
  195. <style scoped lang="scss">
  196. @import '../../libs/css/components.scss';
  197. .u-wrap {
  198. background-color: #eee;
  199. overflow: hidden;
  200. }
  201. .u-lazy-item {
  202. width: 100%;
  203. // 骗系统开启硬件加速
  204. transform: transition3d(0, 0, 0);
  205. // 防止图片加载"闪一下"
  206. will-change: transform;
  207. /* #ifndef APP-NVUE */
  208. display: block;
  209. /* #endif */
  210. }
  211. </style>