一 真实DOM及其解析流程
浏览器渲染引擎工作大致分为五步: 创建 DOM 树——创建 Style Rules ——创建Render树——布局 Layout ——绘制 Painting
- 第一步,创建DOM树:使用HTML解析器,解析HTML元素,构建DOM树
- 第二步,生成样式表:用 CSS 分析器,分析 CSS 文件和元素上的 inline 样式,生成页面的样式表;
- 第三步,创建Render树:DOM树与样式表关联,构建出Render树,每个 DOM 节点都有 attach 方法,接受样式信息,返回一个 render 对象;
- 第四步,确定节点坐标,实现布局:根据Render树的结构,为每个Render树上的节点确定一个在显示屏上出现的精确坐标;
- 第五步,绘制页面:根据Render树和节点的左边,调用节点的paint方法,将其绘制出来
二 虚拟DOM
虚拟DOM,是一个用来表示真实DOM的对象 真实DOM:
<ul id="list">
<li class="item">a</li>
<li class="item">b</li>
<li class="item">c</li>
</ul>
虚拟DOM:
let oldDOM ={
tagName: 'ul',
props: {
id: 'list'
},
children: [
{
tagName: 'li', props: { class: 'item' }, children: ['a']
},
{
tagName: 'li', props: { class: 'item' }, children: ['b']
},
{
tagName: 'li', props: { class: 'item' }, children: ['c']
},
]
}
真实DOM中数据被修改时会生成新的一个虚拟DOM,然后Vue会对新旧的虚拟DOM进行比较
<ul id="list">
<li class="item">a</li>
<li class="item">b</li>
<li class="item">d</li>
</ul>
let newDOM ={
tagName: 'ul',
props: {
id: 'list'
},
children: [
{
tagName: 'li', props: { class: 'item' }, children: ['a']
},
{
tagName: 'li', props: { class: 'item' }, children: ['b']
},
{
tagName: 'li', props: { class: 'item' }, children: ['d']
},
]
}
使用虚拟DOM的好处
-
频繁操作真实DOM会可能会导致浏览器回流和重,导致页面前端性能下降 -
虚拟DOM可以实现跨平台渲染,服务端渲染、小程序、原生应用都使用虚拟DOM -
使用虚拟DOM时,改变了当前的状态不需要立即的去更新DOM,对需要更新的内容进行更新,对于没有改变的内容不做任何操作,通过前后两次差异进行比较 -
虚拟 DOM 可以维护程序的状态,跟踪上一次的状态 -
真实DOM的属性很多,创建DOM节点开销很大,虚拟DOM只是普通JavaScript对象,描述属性并不需要很多,创建开销很小
三 Diff算法
——比较两颗虚拟DOM树差异
-
上述Vue经过对比新旧DOM,发现一个li标签的文本发生了改变,则会去操作修改真实DOM的对应节点,而不是所有的节点都更新一遍。而查找出哪一个标签发生改变使用的算法就是Diff算法,对比旧DOM和新DOM区别。 -
Diff算法在进行对比的时候,Diff算法指在同级比较,不会跨级比较。 -
首先Diff算法会进行进行深度遍历,记录有差异的节点
Diff 对比流程:
页面数据发生改变时会触发 setter,并且通过 Dep.notify 去通知所有的订阅者 watcher,订阅者会调用 patch方法,给真实DOM进行打补丁,更新相应的视图
patch()方法
先判断同层的虚拟节点是否为同一种类型的标签:
- 如果是,需要调用 patchVnode 方法进行深层次对比
- 若不是同一类型标签,直接替换为新的虚拟节点
function patch(oldVnode, newVnode) {
if (sameVnode(oldVnode, newVnode)) {
patchVnode(oldVnode, newVnode)
} else {
const oldEl = oldVnode.el
const parentEle = api.parentNode(oldEl)
createEle(newVnode)
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
api.removeChild(parentEle, oldVnode.el)
oldVnode = null
}
}
return newVnode
}
sameVnode()方法
用于判断是否为同一类型的标签节点
function sameVnode(oldVnode, newVnode) {
return (
oldVnode.key === newVnode.key &&
oldVnode.tagName === newVnode.tagName &&
oldVnode.isComment === newVnode.isComment &&
isDef(oldVnode.data) === isDef(newVnode.data) &&
sameInputType(oldVnode, newVnode)
)
}
patchVnode()方法
若为同一节点,继续比较下去:
第一层判断:是否为同一个对象
? 第二层:都是文本节点且文本不一样
? 第三层判断:都有子节点且子节点不一样
? 新节点有子节点,旧没有
? 旧节点有子节点,新没有
function patchVnode(oldVnode, newVnode) {
const el = newVnode.el = oldVnode.el
const oldCh = oldVnode.children, newCh = newVnode.children
if (oldVnode === newVnode) return
if (oldVnode.text !== null && newVnode.text !== null && oldVnode.text !== newVnode.text) {
api.setTextContent(el, newVnode.text)
} else {
if (oldCh && newCh && oldCh !== newCh) {
updateChildren(el, oldCh, newCh)
} else if (newCh) {
createEle(newVnode)
} else if (oldCh) {
api.removeChild(el)
}
}
}
updateChildren()方法
真实的DOM节点布置变化需要以新节点为基准
patchVnode() 中重要的方法。vue2 中使用了首尾指针的方法,oldS 旧DOM首元素,oldE 旧DOM尾元素;newS 新DOM首元素,newE 新DOM尾元素
核心代码:
function updateChildren(parentElm, oldCh, newCh) {
let oldStartIdx = 0, 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
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
} else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
Diff 掘金
|