Browse Source

fix: group rendering powered by Vue

Gerald 6 years ago
parent
commit
b3ae319e65

+ 0 - 3
src/options/utils/index.js

@@ -1,9 +1,6 @@
 import Modal from 'vueleton/lib/modal/bundle';
 import { route } from '#/common/router';
 import Message from '../views/message';
-import * as throttledRender from './throttled-render';
-
-export { throttledRender };
 
 export const store = {
   route,

+ 0 - 59
src/options/utils/throttled-render.js

@@ -1,59 +0,0 @@
-import { route } from '#/common/router';
-
-const MAX_BATCH_DURATION = 150;
-/** @type ThrottledVue[] */
-const queue = [];
-// When script list is the initial navigation of this tab, startTime should start now
-// so that the first batch is rendered earlier to compensate for main app init
-let startTime = !route.pathname || route.pathname === 'scripts' ? performance.now() : 0;
-let batchSize = 0;
-let maxBatchSize = 0;
-let timer = 0;
-
-/** @param { ThrottledVue } component */
-export function register(component) {
-  // first run: calculate maxBatchSize as a number of items rendered within MAX_BATCH_DURATION
-  if (!maxBatchSize && !startTime) startTime = performance.now();
-  if (component.renderStage === 'check') {
-    const show = maxBatchSize
-      ? batchSize < maxBatchSize
-      : performance.now() - startTime < MAX_BATCH_DURATION;
-    if (show) {
-      batchSize += 1;
-      component.renderStage = 'show';
-      return false;
-    }
-    component.renderStage = 'hide';
-  }
-  queue.push(component);
-  if (!timer) timer = setTimeout(renderNextBatch);
-  return true;
-}
-
-export function unregister(component) {
-  const i = queue.indexOf(component);
-  if (i >= 0) queue.splice(i, 1);
-}
-
-function renderNextBatch() {
-  // render at least 10 items in a theoretically possible case the main app init took more
-  // than MAX_BATCH_DURATION and the batchSize is 0
-  const count = Math.min(queue.length, Math.max(10, batchSize));
-  for (let i = 0; i < count; i += 1) {
-    queue[i].renderStage = 'check';
-  }
-  queue.splice(0, count);
-  timer = queue.length && setTimeout(renderNextBatch);
-  maxBatchSize = Math.max(10, batchSize);
-  batchSize = 0;
-  startTime = 0;
-}
-
-/**
- * @typedef { Vue } ThrottledVue
- * @property { ThrottledRenderStage } renderStage
- */
-
-/**
- * @typedef { 'hide' | 'check' | 'show' } ThrottledRenderStage
- */

+ 13 - 10
src/options/views/script-item.vue

@@ -97,7 +97,7 @@ import Tooltip from 'vueleton/lib/tooltip/bundle';
 import { sendCmd, getLocaleString, formatTime } from '#/common';
 import { objectGet } from '#/common/object';
 import Icon from '#/common/ui/icon';
-import { store, throttledRender } from '../utils';
+import { store } from '../utils';
 
 const DEFAULT_ICON = '/public/images/icon48.png';
 const PADDING = 10;
@@ -123,7 +123,11 @@ function loadImage(url) {
 }
 
 export default {
-  props: ['script', 'draggable'],
+  props: [
+    'script',
+    'draggable',
+    'visible',
+  ],
   components: {
     Icon,
     Tooltip,
@@ -131,8 +135,7 @@ export default {
   data() {
     return {
       safeIcon: DEFAULT_ICON,
-      /** @type ThrottledRenderStage */
-      renderStage: 'check',
+      canRender: this.visible,
     };
   },
   computed: {
@@ -146,9 +149,6 @@ export default {
         || script.custom.lastInstallURL
       );
     },
-    canRender() {
-      return this.renderStage === 'show' || !throttledRender.register(this);
-    },
     homepageURL() {
       const { script } = this;
       return script.custom.homepageURL || script.meta.homepageURL || script.meta.homepage;
@@ -190,6 +190,12 @@ export default {
       return ret;
     },
   },
+  watch: {
+    visible(visible) {
+      // Leave it if the element is already rendered
+      if (visible) this.canRender = true;
+    },
+  },
   mounted() {
     const { icon } = this.script.meta;
     if (icon && icon !== this.safeIcon) {
@@ -203,9 +209,6 @@ export default {
       });
     }
   },
-  destroyed() {
-    throttledRender.unregister(this);
-  },
   methods: {
     onEdit() {
       this.$emit('edit', this.script.props.id);

+ 33 - 1
src/options/views/tab-installed.vue

@@ -75,12 +75,13 @@
       <div class="flex-auto pos-rel">
         <div class="scripts abs-full">
           <script-item
-            v-for="script in sortedScripts"
+            v-for="(script, index) in sortedScripts"
             v-show="!search || script.$cache.show !== false"
             :key="script.props.id"
             :class="{ removing: removing && removing.id === script.props.id }"
             :script="script"
             :draggable="filters.sort.value === 'exec' && !script.config.removed"
+            :visible="index < batchRender.limit"
             @edit="onEditScript"
             @move="moveScript"
             @remove="onRemove"
@@ -148,6 +149,9 @@ options.ready.then(() => {
   filters.sort.set(options.get('filters.sort'));
 });
 
+const MAX_BATCH_DURATION = 100;
+let step = 0;
+
 export default {
   components: {
     ScriptItem,
@@ -173,6 +177,9 @@ export default {
       // Speedup and deflicker for initial page load:
       // skip rendering the script list when starting in the editor.
       canRenderScripts: !store.route.paths[1],
+      batchRender: {
+        limit: step,
+      },
     };
   },
   watch: {
@@ -237,6 +244,7 @@ export default {
         sortedScripts.sort((a, b) => getSortKey(b) - getSortKey(a));
       }
       this.sortedScripts = sortedScripts;
+      this.debouncedRender();
     },
     updateLater() {
       this.debouncedUpdate();
@@ -316,6 +324,7 @@ export default {
         if (!this.script) {
           // First time showing the list we need to tell v-if to keep it forever
           this.canRenderScripts = true;
+          this.debouncedRender();
           // Strip the invalid id from the URL so |App| can render the aside,
           // which was hidden to avoid flicker on initial page load directly into the editor.
           if (id) setRoute(tab, true);
@@ -347,9 +356,32 @@ export default {
         }, 300);
       });
     },
+    async renderScripts() {
+      if (!this.canRenderScripts) return;
+      const { length } = this.sortedScripts;
+      let limit = 9;
+      const batchRender = { limit };
+      this.batchRender = batchRender;
+      const startTime = performance.now();
+      // If we entered a new loop of rendering, this.batchRender will no longer be batchRender
+      while (limit < length && batchRender === this.batchRender) {
+        if (step) {
+          limit += step;
+        } else {
+          limit += 1;
+        }
+        batchRender.limit = limit;
+        await new Promise(resolve => this.$nextTick(resolve));
+        if (!step && performance.now() - startTime >= MAX_BATCH_DURATION) {
+          step = limit;
+        }
+        if (step) await new Promise(resolve => setTimeout(resolve));
+      }
+    },
   },
   created() {
     this.debouncedUpdate = debounce(this.onUpdate, 100);
+    this.debouncedRender = debounce(this.renderScripts);
   },
   mounted() {
     // Ensure the correct UI is shown when mounted: