vue this.$nextTick

js事件循环

js处理异步主要有微任务(microTask)和 宏任务 (macroTask),而从开始执行一个宏任务–>执行完这个宏任务中所有同步代码—>清空当前微任务队列中所有微任务—> UI渲染 。 这便是完成了一个事件循环(Tick), 然后开始执行下一个宏任务(相当于下一轮循环)。

简单来说,Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新。

Vue异步更新

<body>
    <div id="app">
        <p ref="dom">{{message}}</p> <button @click="changeValue">改变值</button>
    </div>
</body>
<script>
    new Vue( {
        el: '#app',
        data: {
            message: 'hello world'
        },
        methods: {
            changeValue() {
                this.message = 'hello zhangShan'
                console.log( this.$refs.dom.innerText )
            }
        }
    } )

</script>
//输出值为
//hello world

从上图中,我们可以看出,我们改变了message后,立马去输出p标签的text值,发现还是原来的值。这就很明显了,vue的dom更新,并不是同步的。而是异步的,所以在输出时,实际dom还并没有更新。
那么,为什么要设计成异步的,其实很好理解,如果是同步的,当我们频繁的去改变状态值时,是不是会频繁的导致我们的dom更新啊。这很显然是不行的。

<body>
    <div id="app">
        <p ref="dom">{{message}}</p> <button @click="changeValue">改变值</button>
    </div>
</body>
<script>
    new Vue( {
        el: '#app',
        data: {
            message: 'hello world'
        },
        methods: {
            changeValue() {
                this.message = 'hello zhangShan'
                this.message = 'hello liShi'
                this.message = 'hello wangWu'
                this.message = 'hello chenLiu'
                console.log( this.$refs.dom.innerText )
            }
        }
    } )

</script>

像上图这样,如果vue同步更新的话,将会造成4次dom更新。故vue是异步dom更新的,且更新原理如下(借用官网的话):

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

这句话大部分地方其实都很好理解,我也不做过多的说明,我只说明下这句话中(在下一个的事件循环"tick"中,vue刷新队列并执行实际工作),按理的理解,这个下一个事件循环"tick"其实是个泛指,他并不是指下一个事件循环,才去刷新队列。实际刷新队列是有可能在本次事件循环的微任务中刷新的,也可能是在下一个事件循环中刷新的。这取决于代码当前执行的环境,如若当前执行环境支持promise,那么nextTick内部实际会用Promise去执行,那么队列刷新就会在本次事件循环的微任务中去执行。
也就是说,如果当前环境支持promise,那么nextTick内部会使用promise.then去执行,否则,如果支持mutationObserver,那么会用mutationObserver(什么是mutationObserver),不过mutationObserver在vue2.5以后被弃用了。如果这两种都不支持,才会使用setImmediate,MessageChannel(vue2.5以后才有),或者setTimeout(按顺序,支持哪个优先用哪个)。

这也就是vue的降级策略
优先选择微任务microtask(promise和mutationObserver),不支持的情况下,才不得不降级选用宏任务macrotask(setImmediate, MessageChannel, setTimeout)。

那么,为什么优先选择微任务呢
微任务执行期在 本次宏任务执行完之后,下个宏任务执行之前,并且在UI渲染之前
所以在微任务中更新队列是会比在宏任务中更新少一次UI渲染的。

下面我们来证实下我们的猜想,请看下面一段代码

<body>
    <div id="app">
        <p ref="dom">{{message}}</p>
    </div>
</body>
<script>
    new Vue( {
                el: '#app',
                data: {
                    message: 'hello world'
                },
                mounted() {
                     //第一步 
                     this.message = 'aaa' 
                     // 第二步 
                     setTimeout(() => { console.log('222') }) 
                     // 第三步 
                     Promise.resolve().then((res) => { console.log('333') }) 
                     // 第四步
                      this.$nextTick(() => { 
                          console.log('444'),
                          consol.log(this.$refs.dom) 
                        })
                       // 第五步
                    Promise.resolve().then((res) => { console.log('555') }) } })

</script>
</script>
image.png

首先,从上图中,我们可以看出

第四步优先第二步输出了 444 和 p标签,从这里我们可以看出,chrome浏览器环境下 nextTick内部是执行了微任务的,所以优先setTimeout输出了。从这点上是可以验证我们上面的说法的(至于其他环境下的,我这里就不测试了)
但是,我们还有个疑问,同样是微任务,为什么第三步的promise会晚于第四步输出呢。按照我们js事件循环来看,第三步第四步都是微任务的话,第三步肯定会优先第四步输出的,但是我们看到的结果却是第四步优于第三步输出了,这是为什么呢。其实这个跟我们改变数据触发watcher更新的先后有关,我们先看下面一段代码验证一下是不是跟数据改变触发watcher更新的顺序有关,然后我们再来看为什么跟触发watcher更新的顺序有关。

<body>
    <div id="app">
        <p ref="dom">{{message}}</p>
    </div>
</body>
<script>
    new Vue( {
                el: '#app',
                data: {
                    message: 'hello world'
                },
                mounted() { 
                    // 第二步 
                    setTimeout(() => { console.log('222') }) 
                    // 第三步 
                    Promise.resolve().then((res) => { 
                        console.log('333')
                     })
                     // 第一步 
                     this.message = 'aaa' 
                     // 第四步
                      this.$nextTick(() => { 
                          console.log('444') 
                          console.log(this.$refs.dom) }) 
                    // 第五步 
                    Promise.resolve().then((res) => { 
                        console.log('555') }) } 
                    })

</script>
image.png

大家发现没有,这个时候,第三步的微任务是优先执行了的。是不是说明了,nextTick中的callback啥时候执行,取决于数据是在什么时候发生了改变的啊。那么为什么会这样呢。这我们就要从nextTick源码来看看到底是怎么回事了。我们先来看看源码

首先,我们知道(响应式原理请自行查看MVVM响应式原理或者vue源码解析),当响应式数据发生变化时,是不是会触发它的setter,从而通知Dep去调用相关的watch对象,从而触发watch的update函数进行视图更新。那我们先看看update函数做了啥

update() {
    /* istanbul ignore else */
    if ( this.lazy ) {
        this.dirty = true
    } else if ( this.sync ) {
        /*同步则执行run直接渲染视图*/
        this.run()
    } else {
        /*异步推送到观察者队列中,下一个tick时调用。*/
        queueWatcher( this )
    }
}

update中是不是调用了一个queueWatcher方法啊(我们先将update的调用称作第一步,将queueWatcher函数的调用称作第二步,后面用的上),我们再看这个方法做了什么

 /*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
 export function queueWatcher( watcher: Watcher ) {
     /*获取watcher的id*/
     const id = watcher.id
     /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
     if ( has[ id ] == null ) {
         has[ id ] = true
         if ( !flushing ) {
             /*如果没有flush掉,直接push到队列中即可*/
             queue.push( watcher )
         } else { 
             // if already flushing, splice the watcher based on its id
              // if already past its id, it will be run next immediately. 
              let i = queue.length - 1 
              while (i >= 0 && queue[i].id > watcher.id) { i-- }
               queue.splice(Math.max(i, index) + 1, 0, watcher) } 
              // queue the flush 
              if (!waiting) { 
                  waiting = true 
                  nextTick(flushSchedulerQueue) 
                }
         }
     }

可以看出,queueWatcher方法内部主要做的就是将watcher push到了queue队列当中。
同时当waiting为false时,调用了一次 nextTick方法, 同时传入了一个参数 flushSchedulerQueue,其实这个参数,就是具体的队列更新函数,也就是说更新dom操作就是在这里面做的。
而这个waiting状态的作用,很明显是为了保证nextTick(flushSchedulerQueue)只会执行一次。后续再通过this.xxx改变数据,只会加入将相关的watcher加入到队列中,而不会再次执行nextTick(flushSchedulerQueue)。
现在我们将nextTick(flushSchedulerQueue) 称作第三步

function flushSchedulerQueue () {
    currentFlushTimestamp = getNow()
    flushing = true
    let watcher, id 
    // Sort queue before flush.
    // This ensures that:
    // 1. Components are updated from parent to child. 
    //(because parent is always created before the child)
    // 2. A component's user watchers are run before its render watcher
     //(because user watchers are created before the render watcher)
    // 3. If a component is destroyed during a parent component's watcher run,
    // its watchers can be skipped.
    queue.sort((a, b) => a.id - b.id) 
    // do not cache length because more watchers might be pushed
    // as we run existing watchers
    for (index = 0; index < queue.length; index++) 
    { 
        watcher = queue[index] 
        if (watcher.before) { 
            watcher.before() 
        } 
        id = watcher.id 
        has[id] = null 
        watcher.run() 
        // in dev build, check and stop circular updates. 
        if (process.env.NODE_ENV !== 'production' && has[id] != null) 
        { circular[id] = (circular[id] || 0) + 1 
        if (circular[id] > MAX_UPDATE_COUNT) 
        { warn( 'You may have an infinite update loop '
         + ( watcher.user ? `in watcher with expression "${watcher.expression}"` 
         : `in a component render function.` ), watcher.vm ) 
        break } 
    }
}
}

我们再来看看nextTick内部,做了些啥

/**
 1. Defer a task to execute it asynchronously.
 */
 /* 延迟一个任务使其异步执行,
 在下一个tick时执行,
 一个立即执行函数,
 返回一个function 
 这个函数的作用是在task或者microtask中推入一个timerFunc,
 在当前调用栈执行完以后以此执行直到执行到timerFunc 
 目的是延迟到当前调用栈执行完以后执行
*/
export const nextTick = (function () {
    /*存放异步执行的回调*/
    const callbacks = []
    /*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
    let pending = false
    /*一个函数指针,
    指向函数将被推送到任务队列中,
    等到主线程任务执行完时,
    任务队列中的timerFunc被调用*/
    let timerFunc
     /*下一个tick时的回调*/
    function nextTickHandler () 
    { /*一个标记位,标记等待状态(
        即函数已经被推入任务队列或者主线程,
        已经在等待当前栈执行完毕去执行),
        这样就不需要在push多个回调到callbacks
        时将timerFunc多次推入任务队列或者主线程*/ 
        pending = false 
        /*执行所有callback*/
         const copies = callbacks.slice(0) 
         callbacks.length = 0 
         for (let i = 0; i < copies.length; i++) 
         { copies[i]() }
    } 
    // the nextTick behavior leverages the microtask queue, which can be accessed
    // via either native Promise.then or MutationObserver.
    // MutationObserver has wider support, however it is seriously bugged in
    // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
    // completely stops working after triggering a few times... so, if native
    // Promise is available, we will use it:
    /* istanbul ignore if 
    */ /* 这里解释一下,一共有Promise、MutationObserver以及setTimeout
    三种尝试得到timerFunc的方法 优先使用Promise,
    在Promise不存在的情况下使用MutationObserver,
    这两个方法都会在microtask中执行,
    会比setTimeout更早执行,所以优先使用。 
    如果上述两种方法都不支持的环境则会使用setTimeout,
    在task尾部推入这个函数,等待调用执行。 
    参考:https://www.zhihu.com/question/55364497
    */
    if (typeof Promise !== 'undefined' && isNative(Promise)) 
    {
         /*使用Promise*/ 
         var p = Promise.resolve() 
         var logError = err => { console.error(err) } 
         timerFunc = () => { p.then(nextTickHandler).catch(logError) 
            // in problematic UIWebViews, Promise.then doesn't completely break, but 
            // it can get stuck in a weird state where callbacks are pushed into the 
            // microtask queue but the queue isn't being flushed, until the browser 
            // needs to do some other work, e.g. handle a timer. Therefore we can 
            // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) setTimeout(noop) }
    } else if (typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || 
    // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) { // use MutationObserver where native Promise is not available, 
        // e.g. PhantomJS IE11, iOS7, Android 4.4
         /*新建一个textNode的DOM对象,
         用MutationObserver绑定该DOM并指定回调函数,
         在DOM变化的时候则会触发回调,
         该回调会进入主线程(比任务队列优先执行),
         即textNode.data = String(counter)时便会触发回调*/ 
         var counter = 1 
         var observer = new MutationObserver(nextTickHandler) 
         var textNode = document.createTextNode(String(counter)) 
         observer.observe(textNode, { characterData: true }) 
         timerFunc = () => { counter = (counter + 1) % 2 
            textNode.data = String(counter) }
    } else { 
        // fallback to setTimeout 
        /* istanbul ignore next 
        */ /*使用setTimeout将回调推入任务队列尾部*/
         timerFunc = () => {
              setTimeout(nextTickHandler, 0) }
    } /* 推送到队列中下一个tick时执行 cb 回调函数 ctx 上下文
    */
    return function queueNextTick (cb?: Function, ctx?: Object) 
    { 
        let _resolve 
        /*cb存到callbacks中*/ 
        callbacks.push(() => { if (cb) 
            { try { cb.call(ctx) } catch (e) {
                 handleError(e, ctx, 'nextTick') } } 
                 else if (_resolve) { _resolve(ctx) } }) 
                 if (!pending) {
                      pending = true 
                      timerFunc() } 
                 if (!cb && typeof Promise !== 'undefined') 
                 {
                    return new Promise((resolve, reject) => { 
                     _resolve = resolve })
                 }
    }
  })()

在这个函数内,我们可以看到

首先可以看出,nextTick是一个立即执行函数,也就是说这个函数在定义的时候就已经自动执行一次了,而自动执行时,return function queueNextTick前面的代码是不是就已经执行了啊。这也是nextTick第一次执行
定义了一个函数timerFunc,这是个关键函数,因为这个函数是怎样的,决定了我们的nextTick内部最终是执行了微任务,还是执行了宏任务。(定义nextTick函数时就定义了)
定义了一个nextTickHandler函数,这个函数作用很明显,就是执行我们调用nextTick时,所传进来的callback回调函数,也就是说当我们执行

this.$nextTick(()=> {})

时,内部传递进来的这个函数,就是在nextTickHandler内被执行的。(定义nextTick函数时就定义了))
return了一个函数queueNextTick,所以我们可以看出,当我们平常调用this.$nextTick(cb)时以及上面调用nextTick(flushSchedulerQueue),实际上,是不是调用了这个queueNextTick啊, 此时,我们将queueNextTick称为第四步。
这个时候,我们继续看queueNextTick,这里做了什么啊

将传入进来的callback回调函数,push到了callbacks数组中,为后面nextTickHandler函数执行callback做准备
当pending为false时,调用了timerFunc函数,此时我们将timerFunc函数的执行,称为第五步
大家发现没有,这个pending其实就是解开我们问题的关键啊,为什么这么说呢。我们先看timerFunc内做了啥,再回过头来解释
那么timerFunc做啥了

if ( 
    typeof Promise !== 'undefined' && isNative( Promise ) ) {
    /*使用Promise*/
    var p = Promise.resolve() 
    var logError = err => {
        console.error( err )
    }
    timerFunc = () => {
        p.then( nextTickHandler ).catch( logError ) 
        // in problematic UIWebViews, Promise.then doesn't completely break, but 
        // it can get stuck in a weird state where callbacks are pushed into the 
        // microtask queue but the queue isn't being flushed, until the browser 
        // needs to do some other work, e.g. handle a timer. Therefore we can
         // "force" the microtask queue to be flushed by adding an empty timer. 
         if (isIOS) 
         setTimeout(noop) 
        }
    } else if (
         typeof MutationObserver !== 'undefined' && ( 
             isNative( MutationObserver ) || 
             // PhantomJS and iOS 7.x 
             MutationObserver.toString() === '[object MutationObserverConstructor]'
        ) ) { // use MutationObserver where native Promise is not available, 
            // e.g. PhantomJS IE11, iOS7, Android 4.4
             /*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,
             该回调会进入主线程(比任务队列优先执行),
             即textNode.data = String(counter)时便会触发回调*/ 
             var counter = 1 
             var observer = new MutationObserver(nextTickHandler) 
             var textNode = document.createTextNode(String(counter)) 
             observer.observe(textNode, { characterData: true }) 
             timerFunc = () => { counter = (counter + 1) % 2 
                textNode.data = String(counter) }
    } else { 
        // fallback to setTimeout
         /* istanbul ignore next */ 
         /*使用setTimeout将回调推入任务队列尾部*/ 
         timerFunc = () => { setTimeout(nextTickHandler, 0) }
    }

可以看出,timerFunc内部定义了一些异步函数,视当前执行环境的不同,timerFunc内部执行的异步函数不同,他内部可能是promise, 可能是mutationObserver, 可能是setTimeout。(我们当前例子是在chrome浏览器下,timerFunc内部是Promise无疑)。但可以看出,不管内部是什么异步函数,它都在异步的回调中执行了nextTickHandler,而nextTickHandler是决定我们调用

this.$nextTick(() => {})

时,内部回调函数啥时候执行的关键。
故可以得出结论,timerFunc内部的异步函数的回调啥时候执行,我们this.$nextTick()内的回调就啥时候执行

好,到了这一步,我们就可以来重新梳理下,代码是怎么走的啦。

栗子1:

mounted() { 
    // 第一步
     this.message = 'aaa' 
    // 第二步
     setTimeout(() => { console.log('222') })
      // 第三步 
      Promise.resolve().then((res) => { console.log('333') }) 
      // 第四步 
      this.$nextTick(() => { 
          console.log('444') 
      console.log(this.$refs.dom) }) 
      // 第五步 
      Promise.resolve().then((res) => { console.log('555') })
}

1.this.message = ‘aaa’ 执行,响应式数据发生变化,是不是会触发setter, 从而进一步触发watcher的update方法,也就是我们前面说的第一步
2.update方法内执行了queueWatcher函数(也就是我们上面说的第二步),将相关watcher push到queue队列中。并执行了nextTick(flushSchedulerQueue) ,也就是我们上面说的第三步。此时,记住了,我们这里是第一次执行了nextTick方法。此时,我们代码中的

this.$nextTick()

还并没有执行,只执行了this.message = ‘aaa’ , 但是vue内部自动执行了一次nextTick方法,并将flushSchedulerQueue当作参数传入了
3.nexTick内部代码执行,实际上是执行了queueNextTick,传入了一个flushSchedulerQueue函数,将这个函数加入到了callbacks数组中,此时数组中只有一个cb函数flushSchedulerQueue。
4.pending状态初始为false,故执行了timerFunc,


image.png

5.timerFunc一旦执行,发现内部是一个promise异步回调,是不是就加入到微任务队列了,此时,是不是微任务队列中的第一个任务啊。但注意,此时,callbacks内的回调函数还并没有执行,是不是要等这个微任务执行的时候,callbcaks内的回调函数才会执行啊
6.此时,跳出源码,继续向下执行我们写的代码

栗子2:

···
mounted() {
// 第一步
this.message = 'aaa'
// 第二步
setTimeout(() => { console.log('222') })
// 第三步
Promise.resolve().then((res) => { console.log('333') })
// 第四步
this.nextTick(() => { console.log('444') console.log(this.refs.dom) })
// 第五步
Promise.resolve().then((res) => {
console.log('555') })
}
···
1.碰到setTimeout,加入宏任务队列,
2.碰到第一个Promise(console.log(333)的这个), 加入微任务队列,此时微任务队列中,是不是就有两个微任务啦。我们现在加入的这个是第二个
3.此时

this.$nextTick()

执行,相当于就是调用了queueNextTick,并传入了一个回调函数。此时注意了


image.png

之前,我们是不是执行过一次queueNextTick啊,那么pending状态是不是变为true了,那么timerFunc是不是这个时候不会再执行了,而此时唯一做的操作就是将传入的回调函数加入到了callbacks数组当中。
所以,实际timerFunc这个函数的执行,是在this.message = ‘aaa’ 执行的时候调用的,也就意味着,timerFunc内的异步回调, 是在 this.message = ‘aaa’ 时被加入到了微任务队列当中,而不是this.$nextTick()执行时被加入到微任务队列的。
所以这也就是前面我们为什么说pending状态是解决问题的关键,因为他决定了,异步回调啥时候加入到微任务队列

this.$nextTick(cb)

执行时,唯一的作用就是将cb回调函数加入到了callbacks数组当中,那么在微任务队列被执行的时候,去调用callbacks中的回调函数时,是不是就会调用到我们现在加入的这个回调函数啊
4.继续,碰到第二个promise(console.log(555)的这个),又加入到微任务队列中。
5.此时,微任务队列中存在3个任务,第一个是timerFunc中的promise回调,第二个是console.log(333)的那个promise回调,第三个是console.log(555)的那个promise回调。
6.故,同步代码执行完成后,优先清空微任务队列,那么是不是先执行了第一个微任务啊,也就是timeFunc内的那个微任务


image.png

而这个微任务一执行,是不是调用了nextTickHandler, nextTickHandler是不是就依次执行了callbacks中的回调函数啊,此时callbacks中有两个回调函数,第一个就是flushSchedulerQueue,用于更新dom,第二个就是我们传进来的这个



所以,我们第二个回调函数执行时,dom是不是已经更新了啊。然后才输出 444 和 p标签
7.然后再取出第二个微任务去执行,就输出了333
8.再取出第三个微任务去执行,就输出了555
9.再之后,微任务队列清空,开始下一轮循环,取出宏任务队列中的setTimeout的回调并执行,输出222。
这也就是所有的一个执行过程了

总结

nextTick流程总结:
1、将回调放到callbacks里等待执行;
2、将执行函数(flushCallbacks)放到微任务或宏任务里;原码里按照是否原生支持Promise.then、MutationObserver和setImmediate的顺序决策,都不支持则使用setTimeout
3、等到事件循环执行到微任务或者宏任务时,执行函数依次执行callbacks里的回调;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 弄懂js异步 讲异步之前,我们必须掌握一个基础知识-event-loop。 我们知道JavaScript的一大特点...
    DCbryant阅读 2,693评论 0 5
  • 静下心学了一波事件循环机制,好开心,我学会了,首先还是得感谢作者写的笔记特别详细 链接: http://www.c...
    Dianaou阅读 519评论 0 0
  • 第十三章:事件 本章内容: 理解事件流 使用事件处理程序 不同的事件类型 13.1 事件流 页面的那个部分拥有特定...
    穿牛仔裤的蚊子阅读 988评论 0 1
  • 事件循环(evenloop) 事件循环机制是宿主环境提供的。js中处理异步,增加了任务队列的概念(你不知道的js中...
    209bd3bc6844阅读 2,803评论 0 1
  • 惊觉忘了时间 无序生活 便问道 我和他分手时间 他随口淡淡一句 六月 我心口突突一下 六月 如同这前任 便没有下文
    许蜜蜜阅读 118评论 0 0