├── .browserslistrc ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── generate.js ├── package.json ├── postcss.config.js ├── public ├── _redirects ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ └── logo.png ├── components │ ├── Benchmark.vue │ ├── HeavyComponent.vue │ ├── Home.vue │ ├── PlayToggle.vue │ ├── benchmarks │ │ ├── child │ │ │ ├── Child.vue │ │ │ ├── ChildOff.vue │ │ │ └── ChildOn.vue │ │ ├── deferred │ │ │ ├── Deferred.vue │ │ │ ├── DeferredOff.vue │ │ │ ├── DeferredOn.vue │ │ │ └── SimplePage.vue │ │ ├── fetch-items │ │ │ ├── FetchItemViewFunctional.vue │ │ │ ├── FetchItemViewNormal.vue │ │ │ ├── FetchItems.vue │ │ │ └── Particles.vue │ │ ├── functional │ │ │ ├── Functional.vue │ │ │ ├── FunctionalOff.vue │ │ │ └── FunctionalOn.vue │ │ ├── hide │ │ │ ├── Hide.vue │ │ │ ├── HideOff.vue │ │ │ └── HideOn.vue │ │ ├── keep-alive │ │ │ └── KeepAlive.vue │ │ ├── local-var │ │ │ ├── LocalVar.vue │ │ │ ├── LocalVarOff.vue │ │ │ └── LocalVarOn.vue │ │ └── static │ │ │ ├── Static.vue │ │ │ ├── StaticOff.vue │ │ │ ├── StaticOn.vue │ │ │ └── colors.js │ └── index.js ├── generate.js ├── main.js ├── mixins │ └── Defer.js ├── particlesjs-config.json ├── plugins.js ├── router.js ├── store.js └── utils.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not ie <= 8 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | '@vue/standard', 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'comma-dangle': ['error', 'always-multiline'], 14 | 'vue/valid-v-for': 'off', 15 | }, 16 | parserOptions: { 17 | parser: 'babel-eslint', 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vueconfus-perf-secrets 2 | 3 | [Slides](https://slides.com/akryum/vueconfus-2019) 4 | 5 | ## Project setup 6 | ``` 7 | yarn install 8 | ``` 9 | 10 | ### Compiles and hot-reloads for development 11 | ``` 12 | yarn run serve 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | ``` 17 | yarn run build 18 | ``` 19 | 20 | ### Run your tests 21 | ``` 22 | yarn run test 23 | ``` 24 | 25 | ### Lints and fixes files 26 | ``` 27 | yarn run lint 28 | ``` 29 | 30 | ### Customize configuration 31 | See [Configuration Reference](https://cli.vuejs.org/config/). 32 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/app', 4 | ], 5 | } 6 | -------------------------------------------------------------------------------- /generate.js: -------------------------------------------------------------------------------- 1 | const { writeFileSync } = require('fs') 2 | const faker = require('faker') 3 | const CliProgress = require('cli-progress') 4 | 5 | const bar = new CliProgress.Bar({}, CliProgress.Presets.shades_classic) 6 | 7 | const count = 4000 8 | 9 | bar.start(count, 0) 10 | 11 | const items = [] 12 | for (let i = 0; i < count; i++) { 13 | const posts = [] 14 | for (let p = 0; p < 10; p++) { 15 | posts.push(faker.lorem.paragraphs()) 16 | } 17 | items.push(Object.assign( 18 | {}, 19 | faker.helpers.createCard(), 20 | { 21 | avatar: faker.image.avatar(), 22 | posts, 23 | }, 24 | )) 25 | bar.update(i + 1) 26 | } 27 | 28 | writeFileSync('./src/items.json', JSON.stringify(items, null, 2), { 29 | encoding: 'utf8', 30 | }) 31 | 32 | bar.stop() 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vueconfus-perf-secrets", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint", 9 | "start": "serve ./dist -s" 10 | }, 11 | "dependencies": { 12 | "@vue/ui": "^0.5.6", 13 | "faker": "^4.1.0", 14 | "fps-indicator": "^1.3.0", 15 | "lodash.clone": "^4.5.0", 16 | "particles.js": "^2.0.0", 17 | "vue": "^2.6.6", 18 | "vue-router": "^3.0.1", 19 | "vue-virtual-scroller": "^1.0.0-rc.2", 20 | "vuex": "^3.0.1" 21 | }, 22 | "devDependencies": { 23 | "@vue/cli-plugin-babel": "^3.5.0", 24 | "@vue/cli-plugin-eslint": "^3.5.0", 25 | "@vue/cli-service": "^3.5.0", 26 | "@vue/eslint-config-standard": "^4.0.0", 27 | "babel-eslint": "^10.0.1", 28 | "cli-progress": "^2.1.1", 29 | "eslint": "^5.8.0", 30 | "eslint-plugin-vue": "^5.0.0", 31 | "serve": "^10.1.2", 32 | "stylus": "^0.54.5", 33 | "stylus-loader": "^3.0.2", 34 | "vue-template-compiler": "^2.5.21" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akryum/vue-9-perf-secrets/46483233d0edb80c511eb899776268c7f7768220/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vueconfus-perf-secrets 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 42 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Akryum/vue-9-perf-secrets/46483233d0edb80c511eb899776268c7f7768220/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Benchmark.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 50 | 51 | 84 | -------------------------------------------------------------------------------- /src/components/HeavyComponent.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 39 | 40 | 50 | -------------------------------------------------------------------------------- /src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/components/PlayToggle.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 13 | 14 | 18 | -------------------------------------------------------------------------------- /src/components/benchmarks/child/Child.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 71 | 72 | 86 | -------------------------------------------------------------------------------- /src/components/benchmarks/child/ChildOff.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 22 | -------------------------------------------------------------------------------- /src/components/benchmarks/child/ChildOn.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | -------------------------------------------------------------------------------- /src/components/benchmarks/deferred/Deferred.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 67 | 68 | 80 | -------------------------------------------------------------------------------- /src/components/benchmarks/deferred/DeferredOff.vue: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /src/components/benchmarks/deferred/DeferredOn.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 24 | -------------------------------------------------------------------------------- /src/components/benchmarks/deferred/SimplePage.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/components/benchmarks/fetch-items/FetchItemViewFunctional.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 44 | -------------------------------------------------------------------------------- /src/components/benchmarks/fetch-items/FetchItemViewNormal.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 21 | 22 | 50 | -------------------------------------------------------------------------------- /src/components/benchmarks/fetch-items/FetchItems.vue: -------------------------------------------------------------------------------- 1 | 120 | 121 | 180 | 181 | 208 | -------------------------------------------------------------------------------- /src/components/benchmarks/fetch-items/Particles.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/components/benchmarks/functional/Functional.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 75 | 76 | 95 | -------------------------------------------------------------------------------- /src/components/benchmarks/functional/FunctionalOff.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /src/components/benchmarks/functional/FunctionalOn.vue: -------------------------------------------------------------------------------- 1 | 7 | -------------------------------------------------------------------------------- /src/components/benchmarks/hide/Hide.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 69 | 70 | 96 | -------------------------------------------------------------------------------- /src/components/benchmarks/hide/HideOff.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/components/benchmarks/hide/HideOn.vue: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/components/benchmarks/keep-alive/KeepAlive.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 72 | 73 | 91 | -------------------------------------------------------------------------------- /src/components/benchmarks/local-var/LocalVar.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 71 | 72 | 86 | -------------------------------------------------------------------------------- /src/components/benchmarks/local-var/LocalVarOff.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/components/benchmarks/local-var/LocalVarOn.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | -------------------------------------------------------------------------------- /src/components/benchmarks/static/Static.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 64 | -------------------------------------------------------------------------------- /src/components/benchmarks/static/StaticOff.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 27 | -------------------------------------------------------------------------------- /src/components/benchmarks/static/StaticOn.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 25 | -------------------------------------------------------------------------------- /src/components/benchmarks/static/colors.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | '2c3e50', 3 | '4f6f7f', 4 | '1d2935', 5 | '42b983', 6 | '6806c1', 7 | 'e83030', 8 | 'ea6e00', 9 | ] 10 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Benchmark from './Benchmark.vue' 3 | import PlayToggle from './PlayToggle.vue' 4 | import Heavy from './HeavyComponent.vue' 5 | 6 | Vue.component('Benchmark', Benchmark) 7 | Vue.component('PlayToggle', PlayToggle) 8 | Vue.component('Heavy', Heavy) 9 | -------------------------------------------------------------------------------- /src/generate.js: -------------------------------------------------------------------------------- 1 | export async function generate (count) { 2 | const faker = (await import(/* webpackChunkName: 'faker' */ 'faker')).default 3 | const items = [] 4 | for (let i = 0; i < count; i++) { 5 | // const posts = [] 6 | // for (let p = 0; p < 10; p++) { 7 | // posts.push(faker.lorem.paragraphs()) 8 | // } 9 | items.push(Object.assign( 10 | {}, 11 | faker.helpers.createCard(), 12 | { 13 | avatar: faker.image.avatar(), 14 | // posts, 15 | }, 16 | )) 17 | } 18 | return items 19 | } 20 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import './plugins' 2 | import './components' 3 | import Vue from 'vue' 4 | import fps from 'fps-indicator' 5 | import App from './App.vue' 6 | import router from './router' 7 | import store from './store' 8 | 9 | Vue.config.productionTip = false 10 | 11 | fps({ 12 | position: 'top-right', 13 | style: ` 14 | font-size: 24px; 15 | `, 16 | }) 17 | 18 | new Vue({ 19 | router, 20 | store, 21 | ...App, 22 | }).$mount('#app') 23 | -------------------------------------------------------------------------------- /src/mixins/Defer.js: -------------------------------------------------------------------------------- 1 | export default function (count = 10) { 2 | // @vue/component 3 | return { 4 | data () { 5 | return { 6 | displayPriority: 0, 7 | } 8 | }, 9 | 10 | mounted () { 11 | this.runDisplayPriority() 12 | }, 13 | 14 | methods: { 15 | runDisplayPriority () { 16 | const step = () => { 17 | requestAnimationFrame(() => { 18 | this.displayPriority++ 19 | if (this.displayPriority < count) { 20 | step() 21 | } 22 | }) 23 | } 24 | step() 25 | }, 26 | 27 | defer (priority) { 28 | return this.displayPriority >= priority 29 | }, 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/particlesjs-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "particles": { 3 | "number": { 4 | "value": 50, 5 | "density": { 6 | "enable": true, 7 | "value_area": 800 8 | } 9 | }, 10 | "color": { 11 | "value": "#ffffff" 12 | }, 13 | "shape": { 14 | "type": "circle", 15 | "stroke": { 16 | "width": 0, 17 | "color": "#000000" 18 | }, 19 | "polygon": { 20 | "nb_sides": 5 21 | }, 22 | "image": { 23 | "src": "img/github.svg", 24 | "width": 100, 25 | "height": 100 26 | } 27 | }, 28 | "opacity": { 29 | "value": 1, 30 | "random": false, 31 | "anim": { 32 | "enable": false, 33 | "speed": 1, 34 | "opacity_min": 0.1, 35 | "sync": false 36 | } 37 | }, 38 | "size": { 39 | "value": 5, 40 | "random": true, 41 | "anim": { 42 | "enable": false, 43 | "speed": 40, 44 | "size_min": 0.1, 45 | "sync": false 46 | } 47 | }, 48 | "line_linked": { 49 | "enable": true, 50 | "distance": 150, 51 | "color": "#ffffff", 52 | "opacity": 0.6, 53 | "width": 2 54 | }, 55 | "move": { 56 | "enable": true, 57 | "speed": 6, 58 | "direction": "none", 59 | "random": false, 60 | "straight": false, 61 | "out_mode": "out", 62 | "bounce": false, 63 | "attract": { 64 | "enable": false, 65 | "rotateX": 600, 66 | "rotateY": 1200 67 | } 68 | } 69 | }, 70 | "interactivity": { 71 | "detect_on": "canvas", 72 | "events": { 73 | "onhover": { 74 | "enable": true, 75 | "mode": "repulse" 76 | }, 77 | "onclick": { 78 | "enable": true, 79 | "mode": "push" 80 | }, 81 | "resize": true 82 | }, 83 | "modes": { 84 | "grab": { 85 | "distance": 400, 86 | "line_linked": { 87 | "opacity": 1 88 | } 89 | }, 90 | "bubble": { 91 | "distance": 400, 92 | "size": 40, 93 | "duration": 2, 94 | "opacity": 8, 95 | "speed": 3 96 | }, 97 | "repulse": { 98 | "distance": 200, 99 | "duration": 0.4 100 | }, 101 | "push": { 102 | "particles_nb": 4 103 | }, 104 | "remove": { 105 | "particles_nb": 2 106 | } 107 | } 108 | }, 109 | "retina_detect": true 110 | } -------------------------------------------------------------------------------- /src/plugins.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueUi from '@vue/ui' 3 | import '@vue/ui/dist/vue-ui.css' 4 | import VueVirtualScroller from 'vue-virtual-scroller' 5 | import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' 6 | 7 | Vue.use(VueUi) 8 | Vue.use(VueVirtualScroller) 9 | -------------------------------------------------------------------------------- /src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './components/Home.vue' 4 | 5 | Vue.use(Router) 6 | 7 | export default new Router({ 8 | mode: 'history', 9 | base: process.env.BASE_URL, 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | component: Home, 15 | }, 16 | { 17 | path: '/bench/static', 18 | name: 'bench-static', 19 | component: () => import(/* webpackChunkName: "bench-static" */ './components/benchmarks/static/Static.vue'), 20 | }, 21 | { 22 | path: '/bench/child', 23 | name: 'bench-child', 24 | component: () => import(/* webpackChunkName: "bench-child" */ './components/benchmarks/child/Child.vue'), 25 | }, 26 | { 27 | path: '/bench/local-var', 28 | name: 'bench-local-var', 29 | component: () => import(/* webpackChunkName: "bench-local-var" */ './components/benchmarks/local-var/LocalVar.vue'), 30 | }, 31 | { 32 | path: '/bench/fetch-items', 33 | name: 'bench-fetch-items', 34 | component: () => import(/* webpackChunkName: "bench-fetch-items" */ './components/benchmarks/fetch-items/FetchItems.vue'), 35 | }, 36 | { 37 | path: '/bench/deferred', 38 | name: 'bench-deferred', 39 | component: () => import(/* webpackChunkName: "bench-deferred" */ './components/benchmarks/deferred/Deferred.vue'), 40 | }, 41 | { 42 | path: '/bench/hide', 43 | name: 'bench-hide', 44 | component: () => import(/* webpackChunkName: "bench-hide" */ './components/benchmarks/hide/Hide.vue'), 45 | }, 46 | { 47 | path: '/bench/keep-alive', 48 | component: () => import(/* webpackChunkName: "bench-keep-alive" */ './components/benchmarks/keep-alive/KeepAlive.vue'), 49 | children: [ 50 | { 51 | path: '', 52 | name: 'bench-keep-alive', 53 | component: () => import(/* webpackChunkName: "bench-keep-alive-simple" */ './components/benchmarks/deferred/SimplePage.vue'), 54 | }, 55 | { 56 | path: 'heavy', 57 | name: 'bench-keep-alive-heavy', 58 | component: () => import(/* webpackChunkName: "bench-keep-alive-heavy" */ './components/benchmarks/deferred/DeferredOff.vue'), 59 | }, 60 | ], 61 | }, 62 | { 63 | path: '/bench/functional', 64 | name: 'bench-functional', 65 | component: () => import(/* webpackChunkName: "bench-functional" */ './components/benchmarks/functional/Functional.vue'), 66 | }, 67 | ], 68 | }) 69 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { generate } from './generate' 4 | import { JobQueue, splitArray } from './utils' 5 | 6 | let items = [] 7 | 8 | function optimizeItem (item) { 9 | const data = { 10 | id: uid++, 11 | vote: 0, 12 | } 13 | Object.defineProperty(data, 'data', { 14 | configurable: false, 15 | value: item, 16 | }) 17 | return data 18 | } 19 | 20 | Vue.use(Vuex) 21 | 22 | let uid = 0 23 | 24 | const store = new Vuex.Store({ 25 | state () { 26 | return { 27 | items: [], 28 | loading: false, 29 | progress: 0, 30 | generatedCount: 0, 31 | } 32 | }, 33 | getters: { 34 | itemCount: state => state.items.length, 35 | }, 36 | mutations: { 37 | clearItems (state) { 38 | state.items = [] 39 | }, 40 | addItems (state, items) { 41 | state.items.push(...items) 42 | }, 43 | loading (state, value) { 44 | state.loading = value 45 | }, 46 | progress (state, value) { 47 | state.progress = value 48 | }, 49 | voteItem (state, item) { 50 | item.vote++ 51 | }, 52 | generatedCount (state, value) { 53 | state.generatedCount = value 54 | }, 55 | }, 56 | actions: { 57 | async generateItems ({ commit }, count) { 58 | items = await generate(count) 59 | commit('generatedCount', count) 60 | }, 61 | 62 | commitItems ({ commit }, { splitCount, split, optimize }) { 63 | commit('clearItems') 64 | commit('loading', true) 65 | requestAnimationFrame(async resolve => { 66 | const data = JSON.parse(JSON.stringify(items)).map( 67 | item => optimize ? optimizeItem(item) : { 68 | id: uid++, 69 | data: item, 70 | vote: 0, 71 | } 72 | ) 73 | const timeStart = performance.now() 74 | if (split) { 75 | const queue = new JobQueue() 76 | splitArray(data, splitCount).forEach( 77 | chunk => queue.addJob(done => requestAnimationFrame(() => { 78 | commit('addItems', chunk) 79 | done() 80 | })) 81 | ) 82 | await queue.start() 83 | } else { 84 | commit('addItems', data) 85 | } 86 | const timeEnd = performance.now() 87 | console.log('time', Math.round(timeEnd - timeStart), 'ms', 'split', split, 'splitCount', splitCount, 'optimize', optimize) 88 | commit('loading', false) 89 | }) 90 | }, 91 | }, 92 | }) 93 | 94 | export default store 95 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function splitArray (list, chunkLength) { 2 | const chunks = [] 3 | let chunk = [] 4 | let i = 0 5 | let l = 0 6 | let n = list.length 7 | while (i < n) { 8 | chunk.push(list[i]) 9 | l++ 10 | if (l === chunkLength) { 11 | chunks.push(chunk) 12 | chunk = [] 13 | l = 0 14 | } 15 | i++ 16 | } 17 | chunk.length && chunks.push(chunk) 18 | return chunks 19 | } 20 | 21 | export class JobQueue { 22 | constructor ({ autoStart = false } = {}) { 23 | this.autoStart = autoStart 24 | 25 | this._queue = [] 26 | this._running = false 27 | this._results = [] 28 | this._resolves = [] 29 | this._rejects = [] 30 | this._runId = 0 31 | } 32 | 33 | get length () { 34 | return this._queue.length 35 | } 36 | 37 | addJob (func) { 38 | this._queue.push(async () => { 39 | try { 40 | const runId = this._runId 41 | const result = func(() => { 42 | // Run not cancelled 43 | if (runId === this._runId) { 44 | this._results.push(result) 45 | this._next() 46 | } 47 | }) 48 | } catch (error) { 49 | this._reject(error) 50 | } 51 | }) 52 | 53 | if (this.autoStart && this.length === 1) { 54 | this.start() 55 | } 56 | } 57 | 58 | clear () { 59 | this._running = false 60 | this._queue.length = 0 61 | this._resolves.length = 0 62 | this._rejects.length = 0 63 | this._results.length = 0 64 | this._runId++ 65 | } 66 | 67 | cancel () { 68 | this._resolve() 69 | this.clear() 70 | } 71 | 72 | start () { 73 | return new Promise((resolve, reject) => { 74 | if (!this._running && this.length > 0) { 75 | this._running = true 76 | this._queue[0]() 77 | this._resolves.push(resolve) 78 | this._rejects.push(reject) 79 | } else { 80 | resolve() 81 | } 82 | }) 83 | } 84 | 85 | _next () { 86 | if (this._running && this.length > 0) { 87 | this._queue.shift() 88 | 89 | if (this.length === 0) { 90 | this._resolve() 91 | } else { 92 | this._queue[0]() 93 | } 94 | } 95 | } 96 | 97 | _resolve () { 98 | this._resolves.forEach(f => f(this._results)) 99 | this.clear() 100 | } 101 | 102 | _reject (error) { 103 | this._rejects.forEach(f => f(error)) 104 | this.clear() 105 | } 106 | } 107 | --------------------------------------------------------------------------------