最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Javascript深入系列(五):EventLoop事件循环

    正文概述 掘金(宇智波胖虎)   2021-01-15   548

    一、单线程的JavaScript

    JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事
    JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

    1.1 单线程代码示例

    例子1

    function foo() {
        bar()
        console.log('foo')
    }
    function bar() {
        baz()
        console.log('bar')
    }
    function baz() {
        console.log('baz')
    }
    
    foo()
    //baz、bar、foo
    

    Javascript深入系列(五):EventLoop事件循环

    1.2 浏览器的渲染进程是提供多个线程

    线程名作用
    1. JS引擎线程也称为JS内核,负责处理JavaScript脚本。(例如V8引擎)
    ①JS引擎线程负责解析JS脚本,运行代码。
    ②JS引擎一直等待着任务队列中的任务的到来,然后加以处理。
    ③一个Tab页(renderer进程)中无论什么时候都只有一个JS线程运行JS程序。
    2. 事件触发线程归属于渲染进程而不是JS引擎,用来控制事件循环
    ①当JS引擎执行代码块如setTimeout时(也可来自浏览器内核的其他线程,如鼠标点击、Ajax异步请求等),会将对应任务添加到事件线程中。
    ②当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。
    注意:由于JS的单线程关系,所以这些待处理队列中的事件都是排队等待JS引擎处理,JS引擎空闲时才会执行。
    3. 定时触发器线程setIntervalsetTimeout所在的线程
    ①浏览器定时计数器并不是由JS引擎计数的。
    ②JS引擎时单线程的,如果处于阻塞线程状态就会影响计时的准确,因此,通过单独的线程来计时并触发定时。
    ③计时完毕后,添加到事件队列中,等待JS引擎空闲后执行。
    注意:W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
    4. 异步http请求线程XMLHttpRequest在连接后通过浏览器新开一个线程请求
    将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调放入事件队列中,再由JS引擎执行。
    5. GUI渲染线程负责渲染浏览器界面,包括:
    ①解析HTML、CSS,构建DOM树和RenderObject树,布局和绘制等。
    ②重绘(Repaint)以及回流(Reflow)处理。

    1.3 进程和线程

    1. 一个进程包含一个或多个线程
    2. chrome为例,打开一个Tab(创建一个进程),又会包含多个线程(JS引擎线程、渲染线程)

    二、任务队列

    任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)
    同步任务 指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务 指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
    “任务队列”是一个先进先出(FIFO)的数据结构,排在前面的事件,优先被主线程读取。主线程的读取过程基本上是自动的,只要执行栈一清空,“任务队列”上第一位的事件就自动进入主线程。但是,由于存在后文提到的定时器功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

    2.1 运行机制

    1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)
    2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
    3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
    4. 主线程不断重复上面的第三步。

    三、Event Loop(事件循环)

    如果执行栈里的任务执行完成,即执行栈为空的时候(即JS引擎线程空闲)事件触发线程才会从消息队列取出一个任务(即异步的回调函数)放入执行栈中执行。
    Javascript深入系列(五):EventLoop事件循环

    例子2

    function foo() {
        bar()
        console.log('foo')
    }
    function bar() {
        baz()
        console.log('bar')
    }
    function baz() { 
        setTimeout(() => {
            console.log('setTimeout: 2s')
        }, 2000)
        console.log('baz') 
    }
    
    foo()
    

    Javascript深入系列(五):EventLoop事件循环

    3.1 执行栈和事件队列

    function a(){
        b();
        console.log('a');
    }
    
    function b(){
        console.log('b');
    }
    a();
    
    // b、a
    

    可以用Loupe可视化工具模拟

    Javascript深入系列(五):EventLoop事件循环

    function a(){
        b();
        console.log('a');
    }
    
    function b(){
    	console.log('b');
        setTimeout(function(){
        	console.log('c');
        },2000)
    }
    a();
    
    //b、a、c
    

    Javascript深入系列(五):EventLoop事件循环

    console.log('script start')
    
    setTimeout(() => {
      console.log('timer 1 over')
    }, 1000)
    
    setTimeout(() => {
      console.log('timer 2 over')
    }, 0)
    
    console.log('script end')
    
    // script start
    // script end
    // timer 2 over
    // timer 1 over
    

    timer 2 的延时为 0ms,HTML5标准规定 setTimeout 第二个参数不得小于4(不同浏览器最小值会不一样),不足会自动增加,所以 "timer 2 over" 还是会在 “script end” 之后。
    就算延时为 0ms,只是 timer 2 的回调函数会立即加入消息队列而已,回调的执行还是得等执行栈为空(JS引擎线程空闲)时执行。

    3.2 宏任务Micro-Task与微任务Macro-Task

    浏览器端事件循环中的异步队列有两种:macro(宏任务)队列micro(微任务)队列宏任务队列可以有多个,微任务队列只有一个。

    3.2.1 宏任务 macrotask

    #浏览器Node
    I/OsetTimeoutsetIntervalsetImmediaterequestAnimationFrameUI rendering渲染有些地方会列出来UI Rendering,说这个也是宏任务
    可是在读了HTML规范文档以后,发现这很显然是和微任务平行的一个操作步骤
    run <script>(同步的代码执行)

    requestAnimationFrame姑且也算是宏任务吧,requestAnimationFrame在MDN的定义为,下次页面重绘前所执行的操作,而重绘也是作为宏任务的一个步骤来存在的,且该步骤晚于微任务的执行

    3.2.2 微任务 microtask

    #浏览器Node
    process.nextTickMutationObserverPromise.then catch finally 回调

    Promise中注意是回调,而new Promise在实例化的过程中所执行的代码都是同步进行的

    3.2.3 代码1

    绿色部分表示同步执行的代码
    
    +setTimeout(_ => {
    -  console.log(4)
    +})
    
    +new Promise(resolve => {
    +  resolve()
    +  console.log(1)
    +}).then(_ => {
    -  console.log(3)
    +})
    
    +console.log(2)
    
    //1、2、3、4
    

    3.2.4 代码2

    setTimeout(_ => console.log(4))
    
    new Promise(resolve => {
        resolve()
        console.log(1)
    }).then(_ => {
        console.log(3)
        Promise.resolve().then(_ => {
            console.log('before timeout')
      }).then(_ => {
        Promise.resolve().then(_ => {
            console.log('also before timeout')
        })
      })
    })
    
    console.log(2)
    //1、 2、3、before timeout、also before timeout、4
    

    3.3 Event Loop 过程解析

    1. 一开始执行栈空,我们可以把执行栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。micro 队列空,macro 队列里有且只有一个 script 脚本(整体代码)。
    2. 全局上下文(script 标签)被推入执行栈,同步代码执行。在执行的过程中,会判断是同步任务还是异步任务,通过对一些接口的调用,可以产生新的 macro-task 与 micro-task,它们会分别被推入各自的任务队列里。同步代码执行完了,script 脚本会被移出 macro 队列,这个过程本质上是队列的 macro-task 的执行和出队的过程。
    3. 上一步我们出队的是一个 macro-task,这一步我们处理的是 micro-task。但需要注意的是:当 macro-task 出队时,任务是一个一个执行的;而 micro-task 出队时,任务是一队一队执行的。因此,我们处理 micro 队列这一步,会逐个执行队列中的任务并把它出队,直到队列被清空。
    4. 执行渲染操作,更新界面
    5. 检查是否存在 Web worker 任务,如果有,则对其进行处理
    6. 上述过程循环往复,直到两个队列都清空
    console.log('script start')
    
    setTimeout(function() {
        console.log('timer over')
    }, 0)
    
    Promise.resolve().then(function() {
        console.log('promise1')
    }).then(function() {
        console.log('promise2')
    })
    
    console.log('script end')
    
    // script start
    // script end
    // promise1
    // promise2
    // timer over
    

    3.4 事件循环伪代码

    const macroTaskList = [
      ['task1'],
      ['task2', 'task3'],
      ['task4'],
    ]
    
    for (let macroIndex = 0; macroIndex < macroTaskList.length; macroIndex++) {
      const microTaskList = macroTaskList[macroIndex]
      
      for (let microIndex = 0; microIndex < microTaskList.length; microIndex++) {
        const microTask = microTaskList[microIndex]
    
        // 添加一个微任务
        if (microIndex === 1) microTaskList.push('special micro task')
        
        // 执行任务
        console.log(microTask)
      }
    
      // 添加一个宏任务
      if (macroIndex === 2) macroTaskList.push(['special macro task'])
    }
    
    // > task1
    // > task2
    // > task3
    // > special micro task
    // > task4
    // > special macro task
    

    四、事件循环(进阶)与异步

    4.1 定时器问题:setTimeout/setInterval

    const s = new Date().getSeconds();
    
    setTimeout(function() {
      // 输出 "2",表示回调函数并没有在 500 毫秒之后立即执行
      console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
    }, 500);
    
    while(true) {
      if(new Date().getSeconds() - s >= 2) {
        console.log("Good, looped for 2 seconds");
        break;
      }
    }
    //Good, looped for 2 seconds
    //Ran after 2 seconds
    

    直到 2 秒后,主线程中的任务才执行完成,这才去执行 macrotask 中的 setTimeout 回调任务。 setTimeout实际延时比设定值更久的原因:最小延迟时间

    4.2 Promise

    function foo() {
        console.log('foo')
    }
    
    console.log('global start')
    
    new Promise((resolve) => {
        console.log('promise')
        resolve()
    }).then(() => {
        console.log('promise then')
    })
    
    foo()
    
    console.log('global end')
    
    //global start
    //promise
    //foo
    //global end
    //promise then
    
    function foo() {
        console.log('foo')
    }
    
    console.log('global start')
    
    setTimeout(() => {
        console.log('setTimeout: 0s')
    }, 0)
    
    new Promise((resolve) => {
        console.log('promise')
        resolve()
    }).then(() => {
        console.log('promise then')
    })
    
    foo()
    
    console.log('global end')
    
    //global start
    //promise
    //foo
    //global end
    //promise then
    //setTimeout: 0S
    

    4.3 async/await

    1. 函数前面async关键字的作用就2点:①这个函数总是返回一个promise。②允许函数内使用await关键字。
    2. 关键字await使async函数一直等待(执行栈当然不可能停下来等待的,await将其后面的内容包装成promise交给Web APIs后,执行栈会跳出async函数继续执行),直到promise执行完并返回结果。await只在async函数函数里面奏效。
    3. async函数只是一种比promise更优雅得获取promise结果(promise链式调用时)的一种语法而已。
    async function async1() {
        console.log('async1 start');
        await async2();
        console.log('async1 end');
    }
    async function async2() {
        console.log('async2');
    }
    async1();
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
    console.log('script end');
    
    /** 
     * async1 start
     * async2
     * promise1
     * script end
     * async1 end
     * promise2
     * */
    

    async/await其实是 PromiseGenerator 的语法糖,如下代码可以转成Promise

    async function async1() {
        console.log('async1 start');
        Promise.resolve(async2()).then(()=>console.log('async1 end'))
    }
    async function async2() {
        console.log('async2');
    }
    async1();
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
    console.log('script end');
    

    五、Node.js的Event Loop

    Javascript深入系列(五):EventLoop事件循环

    JS的运行环境主要有两个:浏览器、Node。
    在两个环境下的Event Loop实现是不一样的,在浏览器中基于规范来实现,不同浏览器可能有小小区别。在Node中基于libuv这个库来实现。

    5.1 Node.js的运行机制

    1. V8引擎解析JavaScript脚本
    2. 解析后的代码,调用Node API
    3. libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎
    4. V8引擎再将结果返回给用户

    5.2 Node.js event loop 和 JS 浏览器环境下的事件循环的区别

    浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。

    浏览器和Node 环境下,microtask 任务队列的执行时机不同:

    1. Node端,microtask 在事件循环的各个阶段之间执行
    2. 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

    六、题目

    案例 6.1.1

    async function async1() {
        console.log('async1 start');
        await async2();
        console.log('async1 end');
    }
    async function async2() {
        console.log('async2');
    }
    console.log('script start');
    setTimeout(function() {
        console.log('setTimeout');
    }, 0)
    async1();
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
    console.log('script end');
    //script start、async1 start、async2、promise1、script end、async1 end、promise2、setTimeout
    

    先执行宏任务(当前代码块也算是宏任务),然后执行当前宏任务产生的微任务,然后接着执行宏任务

    1. 从上往下执行代码,先执行同步代码,输出 script start
    2. 遇到setTimeout,现把 setTimeout 的代码放到宏任务队列中
    3. 执行 async1(),输出 async1 start, 然后执行 async2(), 输出 async2,把 async2() 后面的代码 console.log('async1 end')放到微任务队列中
    4. 接着往下执行,输出 promise1,把 .then()放到微任务队列中。(注意Promise本身是同步的立即执行函数,.then是异步执行函数!)
    5. 接着往下执行, 输出 script end。同步代码(同时也是宏任务)执行完成,接下来开始执行刚才放到微任务中的代码
    6. 依次执行微任务中的代码,依次输出 async1 end、 promise2, 微任务中的代码执行完成后,开始执行宏任务中的代码,输出 setTimeout

    案例 6.1.2

    console.log('start');
    setTimeout(() => {
        console.log('children2');
        Promise.resolve().then(() => {
            console.log('children3');
        })
    }, 0);
    
    new Promise(function(resolve, reject) {
        console.log('children4');
        setTimeout(function() {
            console.log('children5');
            resolve('children6')
        }, 0)
    }).then((res) => {
        console.log('children7');
        setTimeout(() => {
            console.log(res);
        }, 0)
    })
    //start、children4、children2、children3、children5、children7、children6
    

    这道题跟上面题目不同之处在于,执行代码会产生很多个宏任务,每个宏任务中又会产生微任务

    1. 从上往下执行代码,先执行同步代码,输出 start
    2. 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列中
    3. 接着往下执行,输出 children4, 遇到setTimeout,先把 setTimeout 的代码放到宏任务队列②中,此时.then并不会被放到微任务队列中,因为 resolve是放到 setTimeout中执行的
    4. 代码执行完成之后,会查找微任务队列中的事件,发现并没有,于是开始执行宏任务①,即第一个 setTimeout, 输出 children2,此时,会把 Promise.resolve().then放到微任务队列中。
    5. 宏任务①中的代码执行完成后,会查找微任务队列,于是输出 children3;然后开始执行宏任务②,即第二个 setTimeout,输出 children5,此时将.then放到微任务队列中。
    6. 宏任务②中的代码执行完成后,会查找微任务队列,于是输出 children7,遇到 setTimeout,放到宏任务队列中。此时微任务执行完成,开始执行宏任务,输出 children6

    案例 6.1.3

    const p = function() {
        return new Promise((resolve, reject) => {
            const p1 = new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve(1)
                }, 0)
                resolve(2)
            })
            p1.then((res) => {
                console.log(res);
            })
            console.log(3);
            resolve(4);
        })
    }
    
    
    p().then((res) => {
        console.log(res);
    })
    console.log('end');
    //3、end、2、4
    
    1. 执行代码,Promise本身是同步的立即执行函数,.then是异步执行函数。遇到setTimeout,先把其放入宏任务队列中,遇到p1.then会先放到微任务队列中,接着往下执行,输出 3
    2. 遇到 p().then 会先放到微任务队列中,接着往下执行,输出 end
    3. 同步代码块执行完成后,开始执行微任务队列中的任务,首先执行 p1.then,输出 2, 接着执行p().then, 输出 4
    4. 微任务执行完成后,开始执行宏任务setTimeout, resolve(1),但是此时 p1.then已经执行完成,此时1不会输出。

    将上述代码中的 resolve(2)注释掉, 此时 1才会输出,输出结果为 3 end 4 1。

    案例 6.1.4

    Promise.resolve().then(()=>{
      console.log('Promise1')  
      setTimeout(()=>{
        console.log('setTimeout2')
      },0)
    })
    setTimeout(()=>{
      console.log('setTimeout1')
      Promise.resolve().then(()=>{
        console.log('Promise2')    
      })
    },0)
    //Promise1,setTimeout1,Promise2,setTimeout2
    
    1. 一开始执行栈的同步任务(这属于宏任务) 执行完毕,会去查看是否有微任务队列,上题中存在(有且只有一个),然后执行微任务队列中的所有任务输出Promise1,同时会生成一个宏任务 setTimeout2
    2. 然后去查看宏任务队列,宏任务 setTimeout1setTimeout2 之前,先执行宏任务 setTimeout1,输出 setTimeout1
    3. 在执行宏任务setTimeout1时会生成微任务Promise2 ,放入微任务队列中,接着先去清空微任务队列中的所有任务,输出 Promise2
    4. 清空完微任务队列中的所有任务后,就又会去宏任务队列取一个,这回执行的是setTimeout2

    案例 6.1.5

    console.log('1');
    
    setTimeout(function() {
        console.log('2');
        process.nextTick(function() {
            console.log('3');
        })
        new Promise(function(resolve) {
            console.log('4');
            resolve();
        }).then(function() {
            console.log('5')
        })
    })
    process.nextTick(function() {
        console.log('6');
    })
    new Promise(function(resolve) {
        console.log('7');
        resolve();
    }).then(function() {
        console.log('8')
    })
    
    setTimeout(function() {
        console.log('9');
        process.nextTick(function() {
            console.log('10');
        })
        new Promise(function(resolve) {
            console.log('11');
            resolve();
        }).then(function() {
            console.log('12')
        })
    })
    //1,7,6,8,2,4,3,5,9,11,10,12
    

    案例 6.1.6

    console.log(1)
    setTimeout(function() {
      //settimeout1
      console.log(2)
    }, 0);
    const intervalId = setInterval(function() {
      //setinterval1
      console.log(3)
    }, 0)
    setTimeout(function() {
      //settimeout2
      console.log(10)
      new Promise(function(resolve) {
        //promise1
        console.log(11)
        resolve()
      })
      .then(function() {
        console.log(12)
      })
      .then(function() {
        console.log(13)
        clearInterval(intervalId)
      })
    }, 0);
    
    //promise2
    Promise.resolve()
      .then(function() {
        console.log(7)
      })
      .then(function() {
        console.log(8)
      })
    console.log(9)
    
    //1、9、7、8、2、3、10、11、12、13
    //注释clearInterval(intervalId),则最后不会不断输出3
    

    注意:
    由于在执行microtask任务的时候,只有当microtask队列为空的时候,它才会进入下一个事件循环,因此,如果它源源不断地产生新的microtask任务,就会导致主线程一直在执行microtask任务,而没有办法执行macrotask任务,这样我们就无法进行UI渲染/IO操作/ajax请求了,因此,我们应该避免这种情况发生。在nodejs里的process.nexttick里,就可以设置最大的调用次数,以此来防止阻塞主线程。

    案例 6.2.1

    async function async1() {
        console.log('async1 start');
        await async2();
        console.log('async1 end');
    }
    async function async2() {
    	console.log('async2');
    }
    
    console.log('script start');
    
    setTimeout(function() {
        console.log('setTimeout');
    }, 0)
    
    async1();
    
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
    
    console.log('script end');
    
    //script start
    //async1 start
    //async2
    //promise1
    //script end
    //async1 end
    //promise2
    //setTimeout
    

    案例 6.2.2

    async function async1() {
        console.log('async1 start');
        await async2();
        console.log('async1 end');
    }
    async function async2() {
        //async2做出如下更改:
        new Promise(function(resolve) {
    	    console.log('promise1');
    	    resolve();
    	}).then(function() {
    	    console.log('promise2');
        });
    }
    
    console.log('script start');
    
    setTimeout(function() {
        console.log('setTimeout');
    }, 0)
    
    async1();
    
    new Promise(function(resolve) {
        console.log('promise3');
        resolve();
    }).then(function() {
        console.log('promise4');
    });
    
    console.log('script end');
    
    //script start
    //async1 start
    //promise1
    //promise3
    //script end
    //promise2
    //async1 end
    //promise4
    //setTimeout
    

    案例 6.2.3

    async function async1() {
        console.log('async1 start');
        await async2();
        //更改如下:
        setTimeout(function() {
            console.log('setTimeout1')
        },0)
    }
    async function async2() {
        //更改如下:
    	setTimeout(function() {
    		console.log('setTimeout2')
    	},0)
    }
    
    console.log('script start');
    
    setTimeout(function() {
        console.log('setTimeout3');
    }, 0)
    
    async1();
    
    new Promise(function(resolve) {
        console.log('promise1');
        resolve();
    }).then(function() {
        console.log('promise2');
    });
    
    console.log('script end');
    
    //script start
    //async1 start
    //promise1
    //script end
    //promise2
    //setTimeout3
    //setTimeout2
    //setTimeout1
    

    案例 6.2.4

    async function a1 () {
        console.log('a1 start')
        await a2()
        console.log('a1 end')
    }
    async function a2 () {
        console.log('a2')
    }
    
    console.log('script start')
    
    setTimeout(() => {
        console.log('setTimeout')
    }, 0)
    
    Promise.resolve().then(() => {
        console.log('promise1')
    })
    
    a1()
    
    let promise2 = new Promise((resolve) => {
        resolve('promise2.then')
        console.log('promise2')
    })
    
    promise2.then((res) => {
        console.log(res)
        Promise.resolve().then(() => {
            console.log('promise3')
        })
    })
    console.log('script end')
    
    //script start
    //a1 start
    //a2
    //promise2
    //script end
    //promise1
    //a1 end
    //promise2.then
    //promise3
    //setTimeout
    

    参考文章

    1. JavaScript 运行机制详解:再谈Event Loop
    2. 浏览器工作原理与实践
    3. 微任务、宏任务与Event-Loop
    4. 图解搞懂JavaScript引擎Event Loop
    5. 浏览器与Node的事件循环(Event Loop)有何区别?
    6. 《一文看懂浏览器事件循环》是个大佬
    7. 这一次,彻底弄懂 JavaScript 执行机制
    8. 前端开发都应该懂的事件循环(event loop)以及异步执行顺序(setTimeout、promise和async/await)

    视频讲解

    1. 菲利普·罗伯茨:到底什么是Event Loop呢?
    2. Jake Archibald: In The Loop - JSConf.Asia
    3. 《Help, I’m stuck in an event-loop》(bilibili版)

    起源地下载网 » Javascript深入系列(五):EventLoop事件循环

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元