由于对Vue.js很感兴趣,并且平时工做的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并作了总结与输出。
文章的原地址:github.com/answershuto…。
在学习过程当中,为Vue加上了中文的注释github.com/answershuto…,但愿能够对其余想学习Vue源码的小伙伴有所帮助。
可能会有理解存在误差的地方,欢迎提issue指出,共同窗习,共同进步。javascript
在使用vue.js的时候,有时候由于一些特定的业务场景,不得不去操做DOM,好比这样:html
<template>
<div>
<div ref="test">{{test}}</div>
<button @click="handleClick">tet</button>
</div>
</template>复制代码
export default {
data () {
return {
test: 'begin'
};
},
methods () {
handleClick () {
this.test = 'end';
console.log(this.$refs.test.innerText);//打印“begin”
}
}
}复制代码
打印的结果是begin,为何咱们明明已经将test设置成了“end”,获取真实DOM节点的innerText却没有获得咱们预期中的“end”,而是获得以前的值“begin”呢?vue
带着疑问,咱们找到了Vue.js源码的Watch实现。当某个响应式数据发生变化的时候,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的全部Watch对象。触发Watch对象的update实现。咱们来看一下update的实现。java
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
/*同步则执行run直接渲染视图*/
this.run()
} else {
/*异步推送到观察者队列中,下一个tick时调用。*/
queueWatcher(this)
}
}复制代码
咱们发现Vue.js默认是使用异步执行DOM更新。
当异步执行update的时候,会调用queueWatcher函数。react
/*将一个观察者对象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的源码咱们发现,Watch对象并非当即更新视图,而是被push进了一个队列queue,此时状态处于waiting的状态,这时候会继续会有Watch对象被push进这个队列queue,等待下一个tick时,这些Watch对象才会被遍历取出,更新视图。同时,id重复的Watcher不会被屡次加入到queue中去,由于在最终渲染时,咱们只须要关心数据的最终结果。git
那么,什么是下一个tick?github
vue.js提供了一个nextTick函数,其实也就是上面调用的nextTick。express
nextTick的实现比较简单,执行的目的是在microtask或者task中推入一个funtion,在当前栈执行完毕(也行还会有一些排在前面的须要执行的任务)之后执行nextTick传入的funtion,看一下源码:api
/** * 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
})
}
}
})()复制代码
它是一个当即执行函数,返回一个queueNextTick接口。浏览器
传入的cb会被push进callbacks中存放起来,而后执行timerFunc(pending是一个状态标记,保证timerFunc在下一个tick以前只执行一次)。
timerFunc是什么?
看了源码发现timerFunc会检测当前环境而不一样实现,其实就是按照Promise,MutationObserver,setTimeout优先级,哪一个存在使用哪一个,最不济的环境下使用setTimeout。
这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试获得timerFunc的方法。
优先使用Promise,在Promise不存在的状况下使用MutationObserver,这两个方法的回调函数都会在microtask中执行,它们会比setTimeout更早执行,因此优先使用。
若是上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。
为何要优先使用microtask?我在顾轶灵在知乎的回答中学习到:
JS 的 event loop 执行时会区分 task 和 microtask,引擎在每一个 task 执行完毕,从队列中取下一个 task 来执行以前,会先执行完全部 microtask 队列中的 microtask。
setTimeout 回调会被分配到一个新的 task 中执行,而 Promise 的 resolver、MutationObserver 的回调都会被安排到一个新的 microtask 中执行,会比 setTimeout 产生的 task 先执行。
要建立一个新的 microtask,优先使用 Promise,若是浏览器不支持,再尝试 MutationObserver。
实在不行,只能用 setTimeout 建立 task 了。
为啥要用 microtask?
根据 HTML Standard,在每一个 task 运行完之后,UI 都会重渲染,那么在 microtask 中就完成数据更新,当前 task 结束就能够获得最新的 UI 了。
反之若是新建一个 task 来作数据更新,那么渲染就会进行两次。
参考顾轶灵知乎的回答:https://www.zhihu.com/question/55364497/answer/144215284复制代码
首先是Promise,(Promise.resolve()).then()能够在microtask中加入它的回调,
MutationObserver新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入microtask,即textNode.data = String(counter)时便会加入该回调。
setTimeout是最后的一种备选方案,它会将回调函数加入task中,等到执行。
综上,nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完之后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。
/*Github:https://github.com/answershuto*/
/** * Flush both queues and run the watchers. */
/*nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers*/
function flushSchedulerQueue () {
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排序,这样作能够保证: 1.组件更新的顺序是从父组件到子组件的顺序,由于父组件老是比子组件先建立。 2.一个组件的user watchers比render watcher先运行,由于user watchers每每比render watcher更早建立 3.若是一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过。 */
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
/*这里不用index = queue.length;index > 0; index--的方式写是由于不要将length进行缓存,由于在执行处理现有watcher对象期间,更多的watcher对象可能会被push进queue*/
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
/*将has的标记删除*/
has[id] = null
/*执行watcher*/
watcher.run()
// in dev build, check and stop circular updates.
/* 在测试环境中,检测watch是否在死循环中 好比这样一种状况 watch: { test () { this.test++; } } 持续执行了一百次watch表明可能存在死循环 */
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
}
}
}
// keep copies of post queues before resetting state
/**/
/*获得队列的拷贝*/
const activatedQueue = activatedChildren.slice()
const updatedQueue = queue.slice()
/*重置调度者的状态*/
resetSchedulerState()
// call component updated and activated hooks
/*使子组件状态都改编成active同时调用activated钩子*/
callActivatedHooks(activatedQueue)
/*调用updated钩子*/
callUpdateHooks(updatedQueue)
// devtool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
}复制代码
flushSchedulerQueue是下一个tick时的回调函数,主要目的是执行Watcher的run函数,用来更新视图
来看一下下面这一段代码
<template>
<div>
<div>{{test}}</div>
</div>
</template>复制代码
export default {
data () {
return {
test: 0
};
},
created () {
for(let i = 0; i < 1000; i++) {
this.test++;
}
}
}复制代码
如今有这样的一种状况,created的时候test的值会被++循环执行1000次。
每次++时,都会根据响应式触发setter->Dep->Watcher->update->patch。
若是这时候没有异步更新视图,那么每次++都会直接操做DOM更新视图,这是很是消耗性能的。
因此Vue.js实现了一个queue队列,在下一个tick的时候会统一执行queue中Watcher的run。同时,拥有相同id的Watcher不会被重复加入到该queue中去,因此不会执行1000次Watcher的run。最终更新视图只会直接将test对应的DOM的0变成1000。
保证更新视图操做DOM的动做是在当前栈执行完之后下一个tick的时候调用,大大优化了性能。
因此咱们须要在修改data中的数据后访问真实的DOM节点更新后的数据,只须要这样,咱们把文章第一个例子进行修改。
<template>
<div>
<div ref="test">{{test}}</div>
<button @click="handleClick">tet</button>
</div>
</template>复制代码
export default {
data () {
return {
test: 'begin'
};
},
methods () {
handleClick () {
this.test = 'end';
this.$nextTick(() => {
console.log(this.$refs.test.innerText);//打印"end"
});
console.log(this.$refs.test.innerText);//打印“begin”
}
}
}复制代码
使用Vue.js的global API的$nextTick方法,便可在回调中获取已经更新好的DOM实例了。
做者:染陌
Email:answershuto@gmail.com or answershuto@126.com
Github: github.com/answershuto
知乎主页:www.zhihu.com/people/cao-…
osChina:my.oschina.net/u/3161824/b…
转载请注明出处,谢谢。
欢迎关注个人公众号