lazy.mjs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import { nextTick } from "vue";
  2. import { inBrowser, getScrollParent } from "@vant/use";
  3. import {
  4. remove,
  5. on,
  6. off,
  7. throttle,
  8. supportWebp,
  9. getDPR,
  10. getBestSelectionFromSrcset,
  11. hasIntersectionObserver,
  12. modeType,
  13. ImageCache
  14. } from "./util.mjs";
  15. import { isObject } from "../../utils/index.mjs";
  16. import ReactiveListener from "./listener.mjs";
  17. const DEFAULT_URL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
  18. const DEFAULT_EVENTS = [
  19. "scroll",
  20. "wheel",
  21. "mousewheel",
  22. "resize",
  23. "animationend",
  24. "transitionend",
  25. "touchmove"
  26. ];
  27. const DEFAULT_OBSERVER_OPTIONS = {
  28. rootMargin: "0px",
  29. threshold: 0
  30. };
  31. function stdin_default() {
  32. return class Lazy {
  33. constructor({
  34. preLoad,
  35. error,
  36. throttleWait,
  37. preLoadTop,
  38. dispatchEvent,
  39. loading,
  40. attempt,
  41. silent = true,
  42. scale,
  43. listenEvents,
  44. filter,
  45. adapter,
  46. observer,
  47. observerOptions
  48. }) {
  49. this.mode = modeType.event;
  50. this.listeners = [];
  51. this.targetIndex = 0;
  52. this.targets = [];
  53. this.options = {
  54. silent,
  55. dispatchEvent: !!dispatchEvent,
  56. throttleWait: throttleWait || 200,
  57. preLoad: preLoad || 1.3,
  58. preLoadTop: preLoadTop || 0,
  59. error: error || DEFAULT_URL,
  60. loading: loading || DEFAULT_URL,
  61. attempt: attempt || 3,
  62. scale: scale || getDPR(scale),
  63. ListenEvents: listenEvents || DEFAULT_EVENTS,
  64. supportWebp: supportWebp(),
  65. filter: filter || {},
  66. adapter: adapter || {},
  67. observer: !!observer,
  68. observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS
  69. };
  70. this.initEvent();
  71. this.imageCache = new ImageCache({ max: 200 });
  72. this.lazyLoadHandler = throttle(
  73. this.lazyLoadHandler.bind(this),
  74. this.options.throttleWait
  75. );
  76. this.setMode(this.options.observer ? modeType.observer : modeType.event);
  77. }
  78. /**
  79. * update config
  80. * @param {Object} config params
  81. * @return
  82. */
  83. config(options = {}) {
  84. Object.assign(this.options, options);
  85. }
  86. /**
  87. * output listener's load performance
  88. * @return {Array}
  89. */
  90. performance() {
  91. return this.listeners.map((item) => item.performance());
  92. }
  93. /*
  94. * add lazy component to queue
  95. * @param {Vue} vm lazy component instance
  96. * @return
  97. */
  98. addLazyBox(vm) {
  99. this.listeners.push(vm);
  100. if (inBrowser) {
  101. this.addListenerTarget(window);
  102. this.observer && this.observer.observe(vm.el);
  103. if (vm.$el && vm.$el.parentNode) {
  104. this.addListenerTarget(vm.$el.parentNode);
  105. }
  106. }
  107. }
  108. /*
  109. * add image listener to queue
  110. * @param {DOM} el
  111. * @param {object} binding vue directive binding
  112. * @param {vnode} vnode vue directive vnode
  113. * @return
  114. */
  115. add(el, binding, vnode) {
  116. if (this.listeners.some((item) => item.el === el)) {
  117. this.update(el, binding);
  118. return nextTick(this.lazyLoadHandler);
  119. }
  120. const value = this.valueFormatter(binding.value);
  121. let { src } = value;
  122. nextTick(() => {
  123. src = getBestSelectionFromSrcset(el, this.options.scale) || src;
  124. this.observer && this.observer.observe(el);
  125. const container = Object.keys(binding.modifiers)[0];
  126. let $parent;
  127. if (container) {
  128. $parent = vnode.context.$refs[container];
  129. $parent = $parent ? $parent.$el || $parent : document.getElementById(container);
  130. }
  131. if (!$parent) {
  132. $parent = getScrollParent(el);
  133. }
  134. const newListener = new ReactiveListener({
  135. bindType: binding.arg,
  136. $parent,
  137. el,
  138. src,
  139. loading: value.loading,
  140. error: value.error,
  141. cors: value.cors,
  142. elRenderer: this.elRenderer.bind(this),
  143. options: this.options,
  144. imageCache: this.imageCache
  145. });
  146. this.listeners.push(newListener);
  147. if (inBrowser) {
  148. this.addListenerTarget(window);
  149. this.addListenerTarget($parent);
  150. }
  151. this.lazyLoadHandler();
  152. nextTick(() => this.lazyLoadHandler());
  153. });
  154. }
  155. /**
  156. * update image src
  157. * @param {DOM} el
  158. * @param {object} vue directive binding
  159. * @return
  160. */
  161. update(el, binding, vnode) {
  162. const value = this.valueFormatter(binding.value);
  163. let { src } = value;
  164. src = getBestSelectionFromSrcset(el, this.options.scale) || src;
  165. const exist = this.listeners.find((item) => item.el === el);
  166. if (!exist) {
  167. this.add(el, binding, vnode);
  168. } else {
  169. exist.update({
  170. src,
  171. error: value.error,
  172. loading: value.loading
  173. });
  174. }
  175. if (this.observer) {
  176. this.observer.unobserve(el);
  177. this.observer.observe(el);
  178. }
  179. this.lazyLoadHandler();
  180. nextTick(() => this.lazyLoadHandler());
  181. }
  182. /**
  183. * remove listener form list
  184. * @param {DOM} el
  185. * @return
  186. */
  187. remove(el) {
  188. if (!el) return;
  189. this.observer && this.observer.unobserve(el);
  190. const existItem = this.listeners.find((item) => item.el === el);
  191. if (existItem) {
  192. this.removeListenerTarget(existItem.$parent);
  193. this.removeListenerTarget(window);
  194. remove(this.listeners, existItem);
  195. existItem.$destroy();
  196. }
  197. }
  198. /*
  199. * remove lazy components form list
  200. * @param {Vue} vm Vue instance
  201. * @return
  202. */
  203. removeComponent(vm) {
  204. if (!vm) return;
  205. remove(this.listeners, vm);
  206. this.observer && this.observer.unobserve(vm.el);
  207. if (vm.$parent && vm.$el.parentNode) {
  208. this.removeListenerTarget(vm.$el.parentNode);
  209. }
  210. this.removeListenerTarget(window);
  211. }
  212. setMode(mode) {
  213. if (!hasIntersectionObserver && mode === modeType.observer) {
  214. mode = modeType.event;
  215. }
  216. this.mode = mode;
  217. if (mode === modeType.event) {
  218. if (this.observer) {
  219. this.listeners.forEach((listener) => {
  220. this.observer.unobserve(listener.el);
  221. });
  222. this.observer = null;
  223. }
  224. this.targets.forEach((target) => {
  225. this.initListen(target.el, true);
  226. });
  227. } else {
  228. this.targets.forEach((target) => {
  229. this.initListen(target.el, false);
  230. });
  231. this.initIntersectionObserver();
  232. }
  233. }
  234. /*
  235. *** Private functions ***
  236. */
  237. /*
  238. * add listener target
  239. * @param {DOM} el listener target
  240. * @return
  241. */
  242. addListenerTarget(el) {
  243. if (!el) return;
  244. let target = this.targets.find((target2) => target2.el === el);
  245. if (!target) {
  246. target = {
  247. el,
  248. id: ++this.targetIndex,
  249. childrenCount: 1,
  250. listened: true
  251. };
  252. this.mode === modeType.event && this.initListen(target.el, true);
  253. this.targets.push(target);
  254. } else {
  255. target.childrenCount++;
  256. }
  257. return this.targetIndex;
  258. }
  259. /*
  260. * remove listener target or reduce target childrenCount
  261. * @param {DOM} el or window
  262. * @return
  263. */
  264. removeListenerTarget(el) {
  265. this.targets.forEach((target, index) => {
  266. if (target.el === el) {
  267. target.childrenCount--;
  268. if (!target.childrenCount) {
  269. this.initListen(target.el, false);
  270. this.targets.splice(index, 1);
  271. target = null;
  272. }
  273. }
  274. });
  275. }
  276. /*
  277. * add or remove eventlistener
  278. * @param {DOM} el DOM or Window
  279. * @param {boolean} start flag
  280. * @return
  281. */
  282. initListen(el, start) {
  283. this.options.ListenEvents.forEach(
  284. (evt) => (start ? on : off)(el, evt, this.lazyLoadHandler)
  285. );
  286. }
  287. initEvent() {
  288. this.Event = {
  289. listeners: {
  290. loading: [],
  291. loaded: [],
  292. error: []
  293. }
  294. };
  295. this.$on = (event, func) => {
  296. if (!this.Event.listeners[event]) this.Event.listeners[event] = [];
  297. this.Event.listeners[event].push(func);
  298. };
  299. this.$once = (event, func) => {
  300. const on2 = (...args) => {
  301. this.$off(event, on2);
  302. func.apply(this, args);
  303. };
  304. this.$on(event, on2);
  305. };
  306. this.$off = (event, func) => {
  307. if (!func) {
  308. if (!this.Event.listeners[event]) return;
  309. this.Event.listeners[event].length = 0;
  310. return;
  311. }
  312. remove(this.Event.listeners[event], func);
  313. };
  314. this.$emit = (event, context, inCache) => {
  315. if (!this.Event.listeners[event]) return;
  316. this.Event.listeners[event].forEach((func) => func(context, inCache));
  317. };
  318. }
  319. /**
  320. * find nodes which in viewport and trigger load
  321. * @return
  322. */
  323. lazyLoadHandler() {
  324. const freeList = [];
  325. this.listeners.forEach((listener) => {
  326. if (!listener.el || !listener.el.parentNode) {
  327. freeList.push(listener);
  328. }
  329. const catIn = listener.checkInView();
  330. if (!catIn) return;
  331. listener.load();
  332. });
  333. freeList.forEach((item) => {
  334. remove(this.listeners, item);
  335. item.$destroy();
  336. });
  337. }
  338. /**
  339. * init IntersectionObserver
  340. * set mode to observer
  341. * @return
  342. */
  343. initIntersectionObserver() {
  344. if (!hasIntersectionObserver) {
  345. return;
  346. }
  347. this.observer = new IntersectionObserver(
  348. this.observerHandler.bind(this),
  349. this.options.observerOptions
  350. );
  351. if (this.listeners.length) {
  352. this.listeners.forEach((listener) => {
  353. this.observer.observe(listener.el);
  354. });
  355. }
  356. }
  357. /**
  358. * init IntersectionObserver
  359. * @return
  360. */
  361. observerHandler(entries) {
  362. entries.forEach((entry) => {
  363. if (entry.isIntersecting) {
  364. this.listeners.forEach((listener) => {
  365. if (listener.el === entry.target) {
  366. if (listener.state.loaded)
  367. return this.observer.unobserve(listener.el);
  368. listener.load();
  369. }
  370. });
  371. }
  372. });
  373. }
  374. /**
  375. * set element attribute with image'url and state
  376. * @param {object} lazyload listener object
  377. * @param {string} state will be rendered
  378. * @param {bool} inCache is rendered from cache
  379. * @return
  380. */
  381. elRenderer(listener, state, cache) {
  382. if (!listener.el) return;
  383. const { el, bindType } = listener;
  384. let src;
  385. switch (state) {
  386. case "loading":
  387. src = listener.loading;
  388. break;
  389. case "error":
  390. src = listener.error;
  391. break;
  392. default:
  393. ({ src } = listener);
  394. break;
  395. }
  396. if (bindType) {
  397. el.style[bindType] = 'url("' + src + '")';
  398. } else if (el.getAttribute("src") !== src) {
  399. el.setAttribute("src", src);
  400. }
  401. el.setAttribute("lazy", state);
  402. this.$emit(state, listener, cache);
  403. this.options.adapter[state] && this.options.adapter[state](listener, this.options);
  404. if (this.options.dispatchEvent) {
  405. const event = new CustomEvent(state, {
  406. detail: listener
  407. });
  408. el.dispatchEvent(event);
  409. }
  410. }
  411. /**
  412. * generate loading loaded error image url
  413. * @param {string} image's src
  414. * @return {object} image's loading, loaded, error url
  415. */
  416. valueFormatter(value) {
  417. let src = value;
  418. let { loading, error } = this.options;
  419. if (isObject(value)) {
  420. if (process.env.NODE_ENV !== "production" && !value.src && !this.options.silent) {
  421. console.error("[@vant/lazyload] miss src with " + value);
  422. }
  423. ({ src } = value);
  424. loading = value.loading || this.options.loading;
  425. error = value.error || this.options.error;
  426. }
  427. return {
  428. src,
  429. loading,
  430. error
  431. };
  432. }
  433. };
  434. }
  435. export {
  436. stdin_default as default
  437. };