批量异步更新策略
- 由于Vue一个组件渲染对应一个Watcher,这个Wathcer 被很多属性绑定,每个属性更改都会触发Watcher的更新函数,所以为了避免重复的更新Wathcher ,Vue使用了批量异步的更新;简单来说,每次属性发生变化会判单异步更新队列中是否存在需要触发的Watcher,不存在才会将Wathcer 追加到队列中。通知在下一次渲染之前,将队列中所有Watcher都触发一次(一般是在微任务当中)。从而实现Vue高效的渲染
1.src\core\observer\watcher.js
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
- 从响应试原理中,我们知道当依赖项发生变化时,会执行
Dep.notify
,从而会执行Watcher.update
.此时并没有直接进行更新,然后将自己放进了queueWatcher
队列中
2.src\core\observer\scheduler.js
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
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 > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {
flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue)
}
}
}
- 此处将watcher去重后放入队列中,并在
nextTick(flushSchedulerQueue)
中调用,一般nextTick
会在微任务中,也就是在Js Task清空后,第二次渲染之前调用.
3.src\core\observer\scheduler.js
watcher.run()
-
flushSchedulerQueue
中会执行所有Watcher里面的run,从而会调用当初,定义Watcher时,传入的更新方法
4.src\core\util\next-tick.js
看下nextTick
的实现
typeof Promise !== 'undefined'
typeof MutationObserver !== 'undefined'
typeof setImmediate !== 'undefined'
setTimeout(flushCallbacks, 0)
从源码可以看出,依次判断Promise ,MutationObserver , setImmediate ,setTimeout 谁存在就是用谁,Promise 任务调用在下一次渲染之前,setTimeout 发生在下一次渲染之后
虚拟Dom
- 虚拟Dom本质就是Js对象,他是对DOM的抽象表示
体验虚拟Dom
npm i snabbdom
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script type="module">
import { init } from './node_modules/snabbdom/build/package/init.js'
import { h } from './node_modules/snabbdom/build/package/h.js'
const patch = init([])
let vnode; // 保存旧的vnode
const app = document.querySelector("#app")
const obj = {};
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log('get' + key);
return val;
},
set(newVal) {
if (newVal !== val) {
console.log('set' + key + ':' + newVal);
val = newVal;
}
update()
}
})
}
function update() {
// app.innerText = obj.foo;
vnode = patch(vnode, h('div#app', obj.foo))
}
defineReactive(obj, 'foo', '')
// 初始化
vnode = h('app', { on: { click: () => console.log('click') } }, [
h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
' and this is just normal text',
h('a', { props: { href: '/foo' } }, 'I\'ll take you places!')
])
patch(app, vnode)
setInterval(() => {
obj.foo = new Date().toLocaleTimeString()
}, 1000)
</script>
</body>
</html>
查看Vue使用虚拟Dom流程
1.src\core\instance\lifecycle.js
已知组件是在实例化Watcher,传入更新函数updateComponent
,从而刷新页面,以及后续响应化页面
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
-
vm._update(vm._render(), hydrating)
是实现组件更新,但是_update
和_render
不知什么时候挂载的,但是从初始化流程中可知Vue构造函数
的文件
3. src\core\instance\index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
-
renderMixin
应该就是_render
的实现
2.src\core\instance\render.js
export function initRender(vm: Component) {
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
}
export function renderMixin(Vue: Class<Component>) {
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
vnode = render.call(vm._renderProxy, vm.$createElement)
}
}
- 内部拿到运行传入的render函数,执行并返回虚拟Dom,也就是我们平时调用的
render(h){ return h('div',children) }
里面的h
就是createElement
- 步骤1中的
vm._update(vm._render(), hydrating)
,_render
已经弄清楚了,返回的是一个虚拟Dom,传入_update
中
3.继续看src\core\instance\lifecycle.js
export function lifecycleMixin (Vue: Class<Component>) {
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
}
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
-
_update
通过传入虚拟Dom,判断是否有上一次的虚拟Dom运行+__patch__
,patch就是将虚拟Dom转换为真实Dom - 根据上述注释
Vue.prototype.__patch__
全局搜索,找到src\platforms\web\runtime\index.js
patch位置
4.src\platforms\web\runtime\index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
5.src\platforms\web\runtime\patch.js
import { createPatchFunction } from 'core/vdom/patch'
export const patch: Function = createPatchFunction({ nodeOps, modules })
6.src\core\vdom\patch.js
export function createPatchFunction (backend) {
return function patch (oldVnode, vnode, hydrating, removeOnly) {
if (isUndef(oldVnode)) {
createElm(vnode, insertedVnodeQueue)
}else{
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}else{
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
}
}
}
return vnode.elm
}
}
- createPatchFunction 会返回一个patch函数
- 不存在,则
createElm
- 存在虚拟Dom,
patchVnode
- 最后旧节点不存在,就
removeVnodes
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
nodeOps.setTextContent(elm, vnode.text)
}
}
- 新老节点均有children子节点,则对子节点进行diff操作,调用upateChildren
- 如果老节点没有子节点,而新节点有子节点,先清空老节点的文本内容,然后为其新增子节点
- 当新节点没有子节点而老节点有子节点的时候,则移除该节点的所有子节点
- 当新老节点都无子节点的时候,只是文本替换
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
// removeOnly is a special flag used only by <transition-group>
// to ensure removed elements stay in correct relative positions
// during leaving transitions
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
if (isUndef(idxInOld)) { // New element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
updateChildren
就是网上写的虚拟Dom,Diff 算法
- 同层比较,深度优先
- 主要作用是用一种高效的方式对比新旧两个VNode的children得出最小得操作补丁。头部四个比较,尾部四个比较,头尾四个比较,尾头四个比较,都没找到会执行一个双循环.
Vue虚拟Dom调试
1.src\core\vdom\patch.js
- createElem 执行玩后界面会出现Title
- 当数据发生变化后第二次进入
patch.js
,会运行patchVnode