前言
本章项目地址- 为什么用diff, 渲染真实DOM的开销是很大的 减少dom树的重绘和重排
- diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较
- 通过比较老的虚拟dom和新的虚拟dom,如果有相同的,通过
oldVnode.el(el对应该虚拟dom的真实节点)属性去修改真实的dom - 本文重点讲如果双方都有
children进行比较 采用的是双指针的方法 - 其他的情况(详细请看注释) 如 文本替换 标签属性比较与生成(
patchProps)...- 标签不一样 直接删除掉 重新生成新元素
- 文本不一样
oldVnode.el.textContent = vnode.text - 标签属性不一样, 新的没有 直接将老的删除掉(
style或者attribute),最后再将新的在节点上重新渲染,覆盖之前的老的
vnode diff 同级比较
children 比较方法
第一排是oldVnode, 第一排是newVnode
1. 头头比较
2.尾尾比较
3.头尾比较(一样移动到后面)
4.尾头比较(一样移动到前面)
5.乱序比较(前面方法都不适用时)
- 这个方法是适用所有情况 上面几种是优化处理
这里的几种dom元素方法
oldnode.parentNode.replaceChild(newnode,oldnode)新节点替换老节点
parentNode.insertBefore(newNode, referenceNode)移动某个节点之前
node.nextSibling 返回某个元素之后紧跟的节点(处于同一树层级中)没有返回null
示例(其中的一种情况)
compileToFunction是模板编译成render函数
render.call(vm)是将render函数变成虚拟dom
详情请看这里
import { compileToFunction } from './compiler/index.js'
import { createElm, patch } from './vdom/patch.js'
// 老的虚拟节点
let oldTemplate = `<div>
<li key="A">A</li>
<li key="B">B</li>
<li key="C">C</li>
<li key="D">D</li>
</div>`
let vm1 = new Vue({ data: { message: 'hello world' } })
const render1 = compileToFunction(oldTemplate)
const oldVnode = render1.call(vm1)
document.body.appendChild(createElm(oldVnode))
// 新的虚拟节点
let newTemplate = `<div>
<li key="A">A</li>
<li key="D">D</li>
<li key="B">B</li>
<li key="C">C</li>
</div>`
let vm2 = new Vue({ data: { message: 'zf' } })
const render2 = compileToFunction(newTemplate)
const newVnode = render2.call(vm2)
setTimeout(() => {
patch(oldVnode, newVnode)
}, 2000)
逻辑区域
vnode diff 方法重点
export function patch(oldVnode, vnode) {
// 组件没有oldVnode
if (!oldVnode) {
return createElm(vnode)
}
if (oldVnode.nodeType == 1) {
const parentElm = oldVnode.parentNode
let elm = createElm(vnode)
parentElm.insertBefore(elm, oldVnode.nextSibling)
parentElm.removeChild(oldVnode);
return elm
} else {
// 从这里开始看
// 如果标签名称不一样 直接删掉老的 换成新的
if (oldVnode.tag !== vnode.tag) {
return oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el)
}
let el = vnode.el = oldVnode.el
// 如果是文本不一样 替换老文本
if (vnode.tag == undefined) {
if (oldVnode.text !== vnode.text) {
el.textContent = vnode.text
}
return
}
// 如果标签一样 比较属性
patchProps(vnode, oldVnode.data)
// 如果有儿子 比较
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
// 双方都有儿子, 这个方法是核心方法
patchChildren(el, oldChildren, newChildren)
} else if (newChildren.length > 0) {
// 老的没儿子 但是新的有儿子
for (let i = 0; i < newChildren.length; i++) {
let child = createElm(newChildren[i])
el.appendChild(child)
}
} else if (oldChildren.length > 0) {
// 老的有儿子 新的没儿子 直接删除老节点
el.innerHTML = ``
}
}
}
/** vnode diff 比较方法 */
/**
* @description 判断标签是否一致
*/
function isSameVnode(oldVnode, newVnode) {
return (oldVnode.tag == newVnode.tag) && (oldVnode.key == newVnode.key);
}
/**
* @description 双方都有儿子 比较children
*/
function patchChildren(el, oldChildren, newChildren) {
// 双指针定义
let oldStartIndex = 0
let oldStartVnode = oldChildren[0]
let oldEndIndex = oldChildren.length - 1
let oldEndVnode = oldChildren[oldEndIndex]
let newStartIndex = 0
let newStartVnode = newChildren[0]
let newEndIndex = newChildren.length - 1
let newEndVnode = newChildren[newEndIndex]
// key的映射 乱序比较时 方便查找
const makeIndexByKey = (children)=>{
return children.reduce((memo,current,index)=>{
if(current.key){
memo[current.key] = index
}
return memo
}, {})
}
const keysMap = makeIndexByKey(oldChildren)
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// 如果指针指向 为 null 就下一个oldVnode
// 乱序比较时 会将执行过的oldVnode 置为null
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex]
} else if (!oldEndVnode) {
oldEndVnode = oldChildren[--oldEndIndex]
}
if (isSameVnode(oldStartVnode, newStartVnode)) {
// 头头比较 发现标签一致
patch(oldStartVnode, newStartVnode)
oldStartVnode = oldChildren[++oldStartIndex]
newStartVnode = newChildren[++newStartIndex]
}else if (isSameVnode(oldEndVnode, newEndVnode)) {
// 尾尾比较 发现标签一致
patch(oldEndVnode,newEndVnode)
oldEndVnode = oldChildren[--oldEndIndex]
newEndVnode = newChildren[--newEndIndex]
} else if (isSameVnode(oldStartVnode, newEndVnode)) {
// 头尾比较 reverse
// 移动老的元素 老的元素就被移动走了 不用删除
patch(oldStartVnode, newEndVnode)
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)
oldStartVnode = oldChildren[++oldStartIndex]
newEndVnode = newChildren[--newEndIndex]
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
// 尾头比较
patch(oldEndVnode, newStartVnode)
el.insertBefore(oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldChildren[--oldEndIndex]
newStartVnode = newChildren[++newStartIndex]
} else {
// 乱序比较 核心diff 适用所有方法
// 需要根据key 和 对应的索引将老的内容生成程映射表
let moveIndex = keysMap[newStartVnode.key]
if(moveIndex == undefined){
el.insertBefore(createElm(newStartVnode),oldStartVnode.el)
}else{
let moveNode = oldChildren[moveIndex]
oldChildren[moveIndex] = null
el.insertBefore(moveNode.el,oldStartVnode.el)
patch(moveNode,newStartVnode)
}
newStartVnode = newChildren[++newStartIndex]
}
}
// 没有比对完的 新的新增 老的删除
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
// 看一下为指针的下一个元素是否存在
let anchor = newChildren[newEndIndex + 1] == null ? null :newChildren[newEndIndex + 1].el
el.insertBefore(createElm(newChildren[i]), anchor)
}
}
if(oldStartIndex <= oldEndIndex){
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
// 如果老的多 将老节点删除 但是可能里面考虑有null的情况
if(oldChildren[i] !== null) el.removeChild(oldChildren[i].el)
}
}
}
/**
* @description 标签属性的比较与生成
* @description 初次渲染时可以调用此方法,后续更新也可以调用此方法
*/
function patchProps(vnode, oldProps = {}) {
let newProps = vnode.data || {}
let el = vnode.el
let newStyle = newProps.style || {}
let oldStyle = oldProps.style || {}
// 老的vnode.data有 而新的没有 dom上进行删除样式 和 移除属性
for (let key in oldStyle) {
if (!newStyle[key]) {
el.style[key] = ''
}
}
for (let key in oldProps) {
if (!newProps[key]) {
el.removeAttribute(key)
}
}
// 第一次渲染 直接将新的生产到元素上
// 比对完之后 直接用新的生成到元素上 覆盖之前的老的
for (let key in newProps) {
if (key === 'style') {
for (let styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName]
}
} else {
el.setAttribute(key, newProps[key])
}
}
}
/** 核心方法 */
/**
* @description 创建组件的真实节点
*/
function createComponent(vnode) {
let i = vnode.data
if ((i = i.hook) && (i = i.init)) {
i(vnode)
}
if (vnode.componentInstance) {
return true
}
}
/**
* @description 创建真实的节点元素 并赋值与vnode上el
*/
export function createElm(vnode) {
let { tag, data, children, text, vm } = vnode
if (typeof tag === 'string') {
// 是不是组件
if (createComponent(vnode)) {
return vnode.componentInstance.$el
}
vnode.el = document.createElement(tag)
// 看这里 元素属性
patchProps(vnode)
children.forEach(child => {
vnode.el.appendChild(createElm(child))
})
} else {
vnode.el = document.createTextNode(text)
}
return vnode.el
}