lazy.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364
  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. config(options = {}) {
  79. Object.assign(this.options, options);
  80. }
  81. performance() {
  82. return this.listeners.map((item) => item.performance());
  83. }
  84. addLazyBox(vm) {
  85. this.listeners.push(vm);
  86. if (inBrowser) {
  87. this.addListenerTarget(window);
  88. this.observer && this.observer.observe(vm.el);
  89. if (vm.$el && vm.$el.parentNode) {
  90. this.addListenerTarget(vm.$el.parentNode);
  91. }
  92. }
  93. }
  94. add(el, binding, vnode) {
  95. if (this.listeners.some((item) => item.el === el)) {
  96. this.update(el, binding);
  97. return nextTick(this.lazyLoadHandler);
  98. }
  99. const value = this.valueFormatter(binding.value);
  100. let { src } = value;
  101. nextTick(() => {
  102. src = getBestSelectionFromSrcset(el, this.options.scale) || src;
  103. this.observer && this.observer.observe(el);
  104. const container = Object.keys(binding.modifiers)[0];
  105. let $parent;
  106. if (container) {
  107. $parent = vnode.context.$refs[container];
  108. $parent = $parent ? $parent.$el || $parent : document.getElementById(container);
  109. }
  110. if (!$parent) {
  111. $parent = getScrollParent(el);
  112. }
  113. const newListener = new ReactiveListener({
  114. bindType: binding.arg,
  115. $parent,
  116. el,
  117. src,
  118. loading: value.loading,
  119. error: value.error,
  120. cors: value.cors,
  121. elRenderer: this.elRenderer.bind(this),
  122. options: this.options,
  123. imageCache: this.imageCache
  124. });
  125. this.listeners.push(newListener);
  126. if (inBrowser) {
  127. this.addListenerTarget(window);
  128. this.addListenerTarget($parent);
  129. }
  130. this.lazyLoadHandler();
  131. nextTick(() => this.lazyLoadHandler());
  132. });
  133. }
  134. update(el, binding, vnode) {
  135. const value = this.valueFormatter(binding.value);
  136. let { src } = value;
  137. src = getBestSelectionFromSrcset(el, this.options.scale) || src;
  138. const exist = this.listeners.find((item) => item.el === el);
  139. if (!exist) {
  140. this.add(el, binding, vnode);
  141. } else {
  142. exist.update({
  143. src,
  144. error: value.error,
  145. loading: value.loading
  146. });
  147. }
  148. if (this.observer) {
  149. this.observer.unobserve(el);
  150. this.observer.observe(el);
  151. }
  152. this.lazyLoadHandler();
  153. nextTick(() => this.lazyLoadHandler());
  154. }
  155. remove(el) {
  156. if (!el)
  157. return;
  158. this.observer && this.observer.unobserve(el);
  159. const existItem = this.listeners.find((item) => item.el === el);
  160. if (existItem) {
  161. this.removeListenerTarget(existItem.$parent);
  162. this.removeListenerTarget(window);
  163. remove(this.listeners, existItem);
  164. existItem.$destroy();
  165. }
  166. }
  167. removeComponent(vm) {
  168. if (!vm)
  169. return;
  170. remove(this.listeners, vm);
  171. this.observer && this.observer.unobserve(vm.el);
  172. if (vm.$parent && vm.$el.parentNode) {
  173. this.removeListenerTarget(vm.$el.parentNode);
  174. }
  175. this.removeListenerTarget(window);
  176. }
  177. setMode(mode) {
  178. if (!hasIntersectionObserver && mode === modeType.observer) {
  179. mode = modeType.event;
  180. }
  181. this.mode = mode;
  182. if (mode === modeType.event) {
  183. if (this.observer) {
  184. this.listeners.forEach((listener) => {
  185. this.observer.unobserve(listener.el);
  186. });
  187. this.observer = null;
  188. }
  189. this.targets.forEach((target) => {
  190. this.initListen(target.el, true);
  191. });
  192. } else {
  193. this.targets.forEach((target) => {
  194. this.initListen(target.el, false);
  195. });
  196. this.initIntersectionObserver();
  197. }
  198. }
  199. addListenerTarget(el) {
  200. if (!el)
  201. return;
  202. let target = this.targets.find((target2) => target2.el === el);
  203. if (!target) {
  204. target = {
  205. el,
  206. id: ++this.targetIndex,
  207. childrenCount: 1,
  208. listened: true
  209. };
  210. this.mode === modeType.event && this.initListen(target.el, true);
  211. this.targets.push(target);
  212. } else {
  213. target.childrenCount++;
  214. }
  215. return this.targetIndex;
  216. }
  217. removeListenerTarget(el) {
  218. this.targets.forEach((target, index) => {
  219. if (target.el === el) {
  220. target.childrenCount--;
  221. if (!target.childrenCount) {
  222. this.initListen(target.el, false);
  223. this.targets.splice(index, 1);
  224. target = null;
  225. }
  226. }
  227. });
  228. }
  229. initListen(el, start) {
  230. this.options.ListenEvents.forEach(
  231. (evt) => (start ? on : off)(el, evt, this.lazyLoadHandler)
  232. );
  233. }
  234. initEvent() {
  235. this.Event = {
  236. listeners: {
  237. loading: [],
  238. loaded: [],
  239. error: []
  240. }
  241. };
  242. this.$on = (event, func) => {
  243. if (!this.Event.listeners[event])
  244. this.Event.listeners[event] = [];
  245. this.Event.listeners[event].push(func);
  246. };
  247. this.$once = (event, func) => {
  248. const on2 = (...args) => {
  249. this.$off(event, on2);
  250. func.apply(this, args);
  251. };
  252. this.$on(event, on2);
  253. };
  254. this.$off = (event, func) => {
  255. if (!func) {
  256. if (!this.Event.listeners[event])
  257. return;
  258. this.Event.listeners[event].length = 0;
  259. return;
  260. }
  261. remove(this.Event.listeners[event], func);
  262. };
  263. this.$emit = (event, context, inCache) => {
  264. if (!this.Event.listeners[event])
  265. return;
  266. this.Event.listeners[event].forEach((func) => func(context, inCache));
  267. };
  268. }
  269. lazyLoadHandler() {
  270. const freeList = [];
  271. this.listeners.forEach((listener) => {
  272. if (!listener.el || !listener.el.parentNode) {
  273. freeList.push(listener);
  274. }
  275. const catIn = listener.checkInView();
  276. if (!catIn)
  277. return;
  278. listener.load();
  279. });
  280. freeList.forEach((item) => {
  281. remove(this.listeners, item);
  282. item.$destroy();
  283. });
  284. }
  285. initIntersectionObserver() {
  286. if (!hasIntersectionObserver) {
  287. return;
  288. }
  289. this.observer = new IntersectionObserver(
  290. this.observerHandler.bind(this),
  291. this.options.observerOptions
  292. );
  293. if (this.listeners.length) {
  294. this.listeners.forEach((listener) => {
  295. this.observer.observe(listener.el);
  296. });
  297. }
  298. }
  299. observerHandler(entries) {
  300. entries.forEach((entry) => {
  301. if (entry.isIntersecting) {
  302. this.listeners.forEach((listener) => {
  303. if (listener.el === entry.target) {
  304. if (listener.state.loaded)
  305. return this.observer.unobserve(listener.el);
  306. listener.load();
  307. }
  308. });
  309. }
  310. });
  311. }
  312. elRenderer(listener, state, cache) {
  313. if (!listener.el)
  314. return;
  315. const { el, bindType } = listener;
  316. let src;
  317. switch (state) {
  318. case "loading":
  319. src = listener.loading;
  320. break;
  321. case "error":
  322. src = listener.error;
  323. break;
  324. default:
  325. ({ src } = listener);
  326. break;
  327. }
  328. if (bindType) {
  329. el.style[bindType] = 'url("' + src + '")';
  330. } else if (el.getAttribute("src") !== src) {
  331. el.setAttribute("src", src);
  332. }
  333. el.setAttribute("lazy", state);
  334. this.$emit(state, listener, cache);
  335. this.options.adapter[state] && this.options.adapter[state](listener, this.options);
  336. if (this.options.dispatchEvent) {
  337. const event = new CustomEvent(state, {
  338. detail: listener
  339. });
  340. el.dispatchEvent(event);
  341. }
  342. }
  343. valueFormatter(value) {
  344. let src = value;
  345. let { loading, error } = this.options;
  346. if (isObject(value)) {
  347. if (process.env.NODE_ENV !== "production" && !value.src && !this.options.silent) {
  348. console.error("[@vant/lazyload] miss src with " + value);
  349. }
  350. ({ src } = value);
  351. loading = value.loading || this.options.loading;
  352. error = value.error || this.options.error;
  353. }
  354. return {
  355. src,
  356. loading,
  357. error
  358. };
  359. }
  360. };
  361. }
  362. export {
  363. stdin_default as default
  364. };