前言
要来力! diff算法,以前只是知其然而不知其所以然,接下来将完成diff算法原理的学习,至于为什么把他和异步更新原理一起学习,是我认为它们都是性能优化方面的,异步更新是对视图更新的性能优化,而diff是对渲染更新的性能优化。同样还是跟着鲨鱼大佬的源码学习,后面有大佬文章链接。
异步更新原理
异步更新原理主要是对视图更新的性能优化,总结:
在Vue 2中,异步更新是通过事件循环机制实现的。当Vue组件的响应式状态发生变化时,Vue会将更新操作推入一个队列中,而不是立即执行更新。然后,在下一个事件循环周期中,Vue会遍历队列并执行更新操作。
这种异步更新的机制有以下几个好处:
- 提高性能:将多个状态变化合并为一个更新操作,避免了频繁的DOM操作,从而提高了性能。
- 避免重复更新:如果在同一个事件循环周期内多次修改了同一个状态,Vue只会执行一次更新操作,避免了重复更新。
- 避免阻塞UI线程:由于更新操作是在下一个事件循环周期中执行的,所以不会阻塞UI线程,保持了页面的流畅性。
1.watcher
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import { queueWatcher } from "./scheduler"; export default class Watcher { update() { queueWatcher(this); } run() { this.get(); } }
|
2.queueWatcher 实现队列机制
queueWatcher函数是用于实现异步更新队列机制的关键函数之一。它的作用是将Watcher对象推入更新队列中,以便在下一个事件循环周期中执行更新操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
import { nextTick } from "../util/next-tick";
const queue = [];
let isUpdating = false;
function flushSchedulerQueue() { isUpdating = true;
for (let i = 0; i < queue.length; i++) { const watcher = queue[i]; watcher.run(); }
queue.length = 0;
isUpdating = false; }
function queueWatcher(watcher) { if (!queue.includes(watcher)) { queue.push(watcher); }
if (queue.length === 1 && !isUpdating) { nextTick(flushSchedulerQueue); } }
|
3.nextTick的实现原理
可以看到在执行异步渲染时,我们使用nexTick,nexTick是在下次Dom更新执行延迟回调,那它的具体实现方式是什么呢?
简单版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| const callbacks = [];
let pending = false;
function flushCallbacks() { pending = true;
for (let i = 0; i < callbacks.length; i++) { callbacks[i](); }
callbacks.length = 0;
pending = false; }
function nextTick(callback) { callbacks.push(callback);
if (!pending) { setTimeout(flushCallbacks, 0); } }
|
采用优雅降级后的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
|
let callbacks = []; let pending = false; function flushCallbacks() { pending = false; for (let i = 0; i < callbacks.length; i++) { callbacks[i](); } } let timerFunc; if (typeof Promise !== "undefined") { const p = Promise.resolve(); timerFunc = () => { p.then(flushCallbacks); }; } else if (typeof MutationObserver !== "undefined") { let counter = 1; const observer = new MutationObserver(flushCallbacks); const textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true, }); timerFunc = () => { counter = (counter + 1) % 2; textNode.data = String(counter); }; } else if (typeof setImmediate !== "undefined") { timerFunc = () => { setImmediate(flushCallbacks); }; } else { timerFunc = () => { setTimeout(flushCallbacks, 0); }; }
export function nextTick(cb) { callbacks.push(cb); if (!pending) { pending = true; timerFunc(); } }
|
3.nextTick原理总结
因此nextTcik
说白了就是通过事件循环的机制(每次当一次事件循环结束后,即一个宏任务执行完成后以及微任务队列被清空后,浏览器就会进行一次页面更新渲染。),利用微任务和宏任务结合优雅降级的方式来执行回调函数,从而实现在下次Dom
更新完后执行的回调函数。当某个由watcter
监听观察的值发生,并通知相应的DOM
更新,这时dom
更新同步执行发现nextTcik
是个微任务或宏任务,放在队列中,同步任务执行完了,浏览器就会进行一次页面更新渲染,再取出nextTick的队列,执行了nextTick
的回调函数。
最后将其挂载到vue
的原型方法上,就是我们经常使用的$nextTick()
方法了
diff算法原理
前提纪要:Vue2源码学习笔记(三)——初次渲染原理 - 掘金 (juejin.cn)
Vue
在初始化页面后,会将当前的真实DOM
转换为虚拟DOM
(Virtual DOM),并将其保存起来,这里称为oldVnode
。然后当某个数据发变化后,Vue
会先生成一个新的虚拟DOM
——vnode
,然后将vnode
和oldVnode
进行比较,找出需要更新的地方,然后直接在对应的真实DOM
上进行修改。当修改结束后,就将vnode
赋值给oldVnode
存起来,作为下次更新比较的参照物。其中新旧vnode
的比较,也就是我们常说的Diff
算法
复用 DOM 比直接替换(移除旧 DOM,创建新 DOM )性能好的多。
原地复用 > 移动后复用 >> 暴力替换
1.patch 核心渲染方法
patch()
方法,该方法接收新旧虚拟Dom,即oldVnode
,vnode
.
1.首先,检查oldVnode
是否是一个真实的DOM
元素,如果是,则表示这是初次渲染,不需要进行Diff算法的比较。
2.如果oldVnode
是一个虚拟DOM
节点,那么就需要使用Diff算法进行更新过程。首先,代码检查新旧虚拟DOM
节点的标签是否一致,如果不一致,则直接用新的虚拟DOM
节点替换旧的真实DOM
节点。
3.接下来,代码检查旧虚拟DOM
节点是否是一个文本节点,如果是,则比较新旧文本内容是否一致,如果不一致,则更新旧文本节点的textContent
。
4.如果既不是标签不一致的情况,也不是文本节点的情况,那么说明标签一致且不是文本节点。为了节点复用,代码将旧虚拟DOM
节点对应的真实DOM
节点赋值给新虚拟DOM
节点的el属性
。
5.然后,代码调用updateProperties
函数来更新新虚拟DOM
节点的属性。
6.接下来,代码获取旧虚拟DOM节点的子节点和新虚拟DOM
节点的子节点,并进行比较和更新。
- 如果旧节点和新节点都有子节点,代码调用
updateChildren
函数来比较和更新子节点。 - 如果只有旧节点有子节点,代码将旧节点的innerHTML清空,相当于移除所有旧子节点。
- 如果只有新节点有子节点,代码遍历新子节点,并将它们创建为真实
DOM
节点,并添加到旧节点的el
中。
这样,根据新旧虚拟DOM
节点的差异,代码完成了相应的更新操作。以下是简单的实现方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
export function patch(oldVnode, vnode) { const isRealElement = oldVnode.nodeType; if (isRealElement) { } else { if (oldVnode.tag !== vnode.tag) { oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el); } if (!oldVnode.tag) { if (oldVnode.text !== vnode.text) { oldVnode.el.textContent = vnode.text; } } const el = (vnode.el = oldVnode.el); updateProperties(vnode, oldVnode.data); const oldCh = oldVnode.children || []; const newCh = vnode.children || []; if (oldCh.length > 0 && newCh.length > 0) { updateChildren(el, oldCh, newCh); } else if (oldCh.length) { el.innerHTML = ""; } else if (newCh.length) { for (let i = 0; i < newCh.length; i++) { const child = newCh[i]; el.appendChild(createElm(child)); } } } }
|
2.调用updateProperties函数来更新新虚拟DOM节点的属性
updateProperties
函数的主要作用就是将新虚拟DOM
节点的属性映射到对应的真实DOM
节点上,是根据新旧虚拟DOM
节点的属性差异,代码完成了对真实DOM
节点属性的更新操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
function updateProperties(vnode, oldProps = {}) { const newProps = vnode.data || {}; const el = vnode.el; for (const k in oldProps) { if (!newProps[k]) { el.removeAttribute(k); } } const newStyle = newProps.style || {}; const oldStyle = oldProps.style || {}; for (const key in oldStyle) { if (!newStyle[key]) { el.style[key] = ""; } } for (const key in newProps) { if (key === "style") { for (const styleName in newProps.style) { el.style[styleName] = newProps.style[styleName]; } } else if (key === "class") { el.className = newProps.class; } else { el.setAttribute(key, newProps[key]); } } }
|
3.updateChildren 更新子节点-diff 核心方法
updateChildren
函数通过双指针的方式,新旧头尾指针进行比较,循环向中间靠拢,对比新旧子节点数组的差异,并根据差异进行相应的更新操作,实现了虚拟DOM的高效更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92
|
function isSameVnode(oldVnode, newVnode) { return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key; }
function updateChildren(parent, oldCh, newCh) { let oldStartIndex = 0; let oldStartVnode = oldCh[0]; let oldEndIndex = oldCh.length - 1; let oldEndVnode = oldCh[oldEndIndex];
let newStartIndex = 0; let newStartVnode = newCh[0]; let newEndIndex = newCh.length - 1; let newEndVnode = newCh[newEndIndex];
function makeIndexByKey(children) { let map = {}; children.forEach((item, index) => { map[item.key] = index; }); return map; } let map = makeIndexByKey(oldCh);
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) { if (!oldStartVnode) { oldStartVnode = oldCh[++oldStartIndex]; } else if (!oldEndVnode) { oldEndVnode = oldCh[--oldEndIndex]; } else if (isSameVnode(oldStartVnode, newStartVnode)) { patch(oldStartVnode, newStartVnode); oldStartVnode = oldCh[++oldStartIndex]; newStartVnode = newCh[++newStartIndex]; } else if (isSameVnode(oldEndVnode, newEndVnode)) { patch(oldEndVnode, newEndVnode); oldEndVnode = oldCh[--oldEndIndex]; newEndVnode = newCh[--newEndIndex]; } else if (isSameVnode(oldStartVnode, newEndVnode)) { patch(oldStartVnode, newEndVnode); parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling); oldStartVnode = oldCh[++oldStartIndex]; newEndVnode = newCh[--newEndIndex]; } else if (isSameVnode(oldEndVnode, newStartVnode)) { patch(oldEndVnode, newStartVnode); parent.insertBefore(oldEndVnode.el, oldStartVnode.el); oldEndVnode = oldCh[--oldEndIndex]; newStartVnode = newCh[++newStartIndex]; } else { let moveIndex = map[newStartVnode.key]; if (!moveIndex) { parent.insertBefore(createElm(newStartVnode), oldStartVnode.el); } else { let moveVnode = oldCh[moveIndex]; oldCh[moveIndex] = undefined; parent.insertBefore(moveVnode.el, oldStartVnode.el); patch(moveVnode, newStartVnode); } } } if (newStartIndex <= newEndIndex) { for (let i = newStartIndex; i <= newEndIndex; i++) { const ele = newCh[newEndIndex + 1] == null ? null : newCh[newEndIndex + 1].el; parent.insertBefore(createElm(newCh[i]), ele); } } if (oldStartIndex <= oldEndIndex) { for (let i = oldStartIndex; i <= oldEndIndex; i++) { let child = oldCh[i]; if (child != undefined) { parent.removeChild(child.el); } } } }
|
4.改造原型渲染更新方法_update
当数据发生变化时订阅者watcher
就会调用_update
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
export function lifecycleMixin(Vue) { Vue.prototype._update = function (vnode) { const vm = this; const prevVnode = vm._vnode; vm._vnode = vnode; if (!prevVnode) { vm.$el = patch(vm.$el, vnode); } else { vm.$el = patch(prevVnode, vnode); } }; }
|
5.diff算法总结
updateChildren
主要做了以下操作:- 设置新旧
VNode
的头尾指针 - 新旧头尾指针进行比较,循环向中间靠拢,根据情况
patch
重复流程或调用createElem
创建一个新节点 - 从哈希表寻找
key
一致的VNode
节点再分情况操作,如果标签,key值,属性都相同就可以就地复用
个人博客
耀耀切克闹 (yaoyaoqiekenao.com)
参考文章
手写Vue2.0源码(六)-diff算法原理 - 掘金 (juejin.cn)