3.3 vm.$nextTick

nextTick接收一个回调函数作为参数,作用是将回调延迟至下次DOM更新之后执行,它与全局方法Vue.nextTick相同,不同的是回调的this自动绑定到调用它的实例上。如果没有提供回调且在支持promise的环境中,则会返回一个promise。
需要用到nextTick的开发场景:当更新了状态后,需要对新DOM做一些操作,但是这时其实获取不到更新后的DOM,因为还没有重新渲染,所以这时需要使用nextTick方法。
当状态发生变化时,会通知watcher,然后触发虚拟DOM的渲染流程,但是触发渲染这个操作不是同步的,而是异步进行的。Vue.js中有一个队列,每当需要重新渲染时,会将watcher推送到这个队列中,在下次事件循环event loop中再让watcher触发渲染的流程。

1.为什么Vue.js使用异步更新队列?
Vue.js2.0使用虚拟DOM进行渲染,变化侦测的通知只发送到组件,用到的所有状态的变化会通知到一个watcher,然后虚拟DOM在组件内进行比对并更改DOM。如果同一轮事件循环有两个数据发生了变化,watcher会有两个通知,会进行两次渲染,但是并不需要渲染两次,虚拟DOM会对整个组件进行渲染,只需要等所有状态都修改完毕后,一次性将整个组件的DOM渲染成最新的即可。为了解决这个问题,vue将受到通知的watcher添加到队列中缓存起来,并且检查之前的队列是否有存在相同的watcher,只有不存在的时候,才将其添加到队列中,然后在下一次事件循环中,让队列中的watcher触发渲染流程并清空队列,这样就能保证在同一事件循环中有多个状态发生改变,watcher最后也只是执行一次渲染流程。

2.什么是事件循环?
js是一门单线程且非阻塞的脚本语言,说明js在执行的时候只有一个主线程来处理所有任务。非阻塞指的是当代码需要处理异步任务的时候,主线程会将其先挂起(pending),当异步任务处理完毕后,在根据一定规则去执行响应回调。
当所有任务被处理完毕后,js会将这个事件加入到一个事件队列中,被放入队列的事件不会立即执行其回调,而是等待当前执行栈中的所有任务执行完毕后,再去检查事件队列中是否存在任务。
异步任务有两种类型:微任务(microtask)和宏任务(macrotask),不同的任务会分配到不同的任务队列中。
当执行栈中所有任务都执行完毕后,会检查微任务队列是否有事件存在,如果存在,会依次执行微任务队列中的事件对应的回调直到为空时。然后从宏任务队列中取出一个事件,把对应的回调加入当前执行栈,当执行栈的任务执行完毕后,再来检查微任务队列中是否有事件存在。无限重复此过程直到所有事件都执行完,这个循环的过程叫做事件循环。
微任务:Promise.then/MutationObserver/Object.observe/process.nextTick
宏任务:setTimeout/setInterval/setImmediate/MessageChannel/requestAnimationFrame/ I/O /UI交互事件

3.什么是执行栈?
当执行一个方法时,js会生成一个与这个方法对应的执行环境,又叫做执行上下文。这个执行环境中有这个方法的私有作用域、上层作用域的指向、方法的参数、私有作用域定义的变量以及this对象。这个执行环境会被添加到一个栈中,这个栈就是执行栈。
如果在这个方法的代码中执行一行函数调用的语句,那么js会生成这个函数的执行环境并将其添加到执行栈中,然后进入这个执行环境继续执行其中的代码。执行完毕并返回结果后,js会退出执行环境并把这个执行环境从栈中销毁,回到上一个方法的执行环境。这个过程反复进行,直到执行栈中的代码全部执行完毕。

"下次DOM更新周期"就是下次微任务执行时更新DOM。vm.$nextTick是将回调添加到微任务中。特殊情况下才会降级成宏任务,一般都是微任务。不论是更新DOM的回调还是nextTick注册的回调,都是向微任务队列中添加任务,所以哪个先被添加进去,哪个就先执行。

如果想在nextTick中获取更新后的DOM,则一定要在更改数据之后再使用vm.$nextTick注册回调。

new Vue({
    //······
    methods:{
        //······
        example:function(){
            //先修改数据
            this.message = 'changed'
            //然后使用nextTick注册回调
            this.$nextTick(function(){ ... })
        }
    }
})

如果先注册回调,然后修改数据,则在微任务队列中先执行使用nextTick注册的回调,然后在执行更新DOM的回调,在回调中得不到最新的DOM,因为DOM还没有更新。

Vue原型上的$nextTick方法只是调用了nextTick方法(Vue.nextTick),具体实现是在nextTick中。

Vue.js内部有一个列表来存储vm.$nextTick参数中提供的回调,在一轮事件循环中,nextTick只会向任务队列添加一个任务,多次使用注册回调只会将回调添加到回调列表中缓存起来,当任务触发时,依次执行列表中的所有回调并清空列表。

当环境不支持Promise的时候,降级使用宏任务队列。使用宏任务时优先使用setImmediate,然后使用MessageChannel作为备选方案,最后都不能使用的话使用setTimeout来将回调添加到宏任务队列中。

3.4 vm.$mount

如果在实例化Vue.js时设置了el选项,会自动把Vue.js实例挂载到DOM元素上。如果想要Vue.js具有关联的DOM元素,只有使用mount方法这个途径。

vm.$mount([elementOrSelector])

【参数】{Element|String} [elementOrSelector]
【返回值】vm,实例自身
【用法】如果Vue.js在实例化时没有收到el选项,则它处于“未挂载”状态,没有关联的DOM元素。可以用vm.$mount手动挂载一个未挂载的实例。如果没有提供elementOrSelector参数,模板将被渲染成文档之外的元素,并且必须使用原生DOM的API把它插入文档中。这个方法返回实例自身,所以可以链式调用其他实例方法。
【示例】

var MyComponent = Vue.extend({ 
    template:`<div>Hello</div>`
})

//el选项挂载 (会替换#app)
new MyComponent({ el:'#app' })
//vm.$mount挂载 (会替换#app)
new MyComponent().$mount('#app')
//在文档之外渲染,随后使用原生DOM挂载
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)

在不同的构建版本中,vm.$mount表现都不一样,完整版(vue.js)和只包含运行时版本(vue.runtime.js)之间的差异在于是否有编译器。在完整的构建版本中,mount首先会检查template和el选项所提供的模板是否已经转换成渲染函数,如果没有,则立即进入编译过程,将模板编译成渲染函数,完成后在进入挂载和渲染的流程中。只包含运行的版本中没有编译步骤,它会默认实例上已经存在渲染函数,如果不存在,会设置一个,返回一个空节点的vnode,以保证不会因为不存在而报错,开发环境下会有触发警告,提示用户必须提供渲染函数。
挂载操作与渲染类似,不同的是渲染指的是渲染一次,而挂载是持续性渲染。挂在之后,每当状态发生变化时,都会进行重新渲染的操作。挂载完毕后,会触发mounted钩子函数