一些原理汇总
整体流程
- 创建
Vue
构造函数:在构造函数上添加一些静态属性方法,并子啊原型上添加一些方法属性。 - 实例化
Vue
形成组件实例:首先会将Vue.options
与传入的组件配置进行合并,得到最终的组件options
。 - 继续在原型上添加一些属性方法,如事件方法,生命周期方法等等。与此同时,还会将
options
里的data,method,props
等等全部添加到vue
实例上,并对他们进行响应式处理。 - 随后开始对模板进行编译。
- 首先解析
template
字符串,形成ast
树。 - 其次对
ast
进行优化,将一些静态节点做标记。 ast
生成新的代码,这些代码执行后可以生成Vnode
。
- 首先解析
- 实例化
renderWatcher
,开始更新:- 首先执行生成的代码,得到
vnode
。 - 然后将
vnode
与老的vm._vnode
进行diff
。 - 最后根据
diff
结果进行增删节点、更新属性等操作。
- 首先执行生成的代码,得到
diff算法
diff
算法处于patch
阶段,此时已经生成了新的Vnode
。如果新老Vnode
处于同层级,并且Vnode
都存在,那么需要将新老Vnode
进行对比。过程如下:
新节点第一个节点 vs 老节点第一个节点:
- 如果节点类型相同,调用
patchVnode
递归更新。新老节点index
移动(后续也会移动)。
- 如果节点类型相同,调用
新节点最后一个节点 vs 老节点最后一个节点:
- 如果节点类型相同,调用
patchVnode
递归更新。
- 如果节点类型相同,调用
新节点最后一个节点 vs 老节点第一个节点:
- 如果节点类型相同,调用
patchVnode
递归更新。并且将老Vnode
对应的真实节点插入到最后一个新节点的下一个兄弟节点之前。
- 如果节点类型相同,调用
新节点第一个节点 vs 老节点最后一个节点:
- 如果节点类型相同,调用
patchVnode
递归更新。并且将老Vnode
对应的真实节点插入到第一个老节点的下一个兄弟节点之前。
- 如果节点类型相同,调用
如果以上都不符合:
将老节点的
key
与节点的index
形成键值对形式{ key: index }
。通过新节点的
key
去查找是否存在对应的老节点:- 如果存在,且类型相同,将对应的老节点(真实节点)插入到第一个老节点之前。
- 否则直接创建新的真实节点。
最后新节点
index
向后移动一步。
对比完成后,如果老节点还有剩余,删除对应的真实节点。如果新节点还有没有复用的,创建新的真实节点。
Vnode
之所以提升性能,是因为它的计算相较于直接对DOM
的操作的开销要小得多:
javascript
不使用虚拟DOM的开销 = 所有真实节点的操作
使用虚拟DOM的开销 = Vnode的创建 + diff开销 + 必要的真实节点操作
$nextTick原理
$nextTick
原理比较简单,实际上是将当前的callback
收集起来,然后利用微任务/宏任务在下一轮事件循环时执行。
javascript
// timerFunc 其实是 微任务 or 宏任务,这里做了兼容处理
// Promise =》 MutationObserver =》 setImmediate =》 setTimeout
const p = Promise.resolve()
timerFunc = () => {
// 在下一轮事件循环时,执行所有的 callback
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// 收集 callback
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
// 如果已经启动执行,那么就不再启动。
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
批量更新(queueWatcher)原理
javascript
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
// 当前已经在下一轮事件循环了
flushing = true
let watcher, id
// 1. 父组件 优先于 子组件
// 2. $watch 优先于 renderWatcher
// 3. 如果子组件被销毁了,它的watcher会被跳过
queue.sort((a, b) => a.id - b.id)
// 遍历执行
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
// 执行 watcher
watcher.run()
}
}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
// 如果 watcher 已经存在,那么不再添加
if (has[id] == null) {
has[id] = true
// 如果还未启动执行,直接追加 watcher
if (!flushing) {
queue.push(watcher)
} else {
// 如果在下一轮事件循环中,此时watcher正在执行。
// 如果过了这个id应该早就执行,但是错过了,
// 此时会通过 while 循环相当于插队,将它置于最前面了。
// 下一个就会执行它
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 如果没有开始执行,那么启动执行
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
批量更新的实质与$nextTick
一致,首先收集watchers
,然后在下一轮事件循环时批量run
。