vue2核心原理(简易) - diff算法解析

819 阅读3分钟

前言

  • 本章项目地址
  • 为什么用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
}