從0行代碼開始學習Vue2.x的虛擬DOM diff原理

前言

常常看到講解Vue2的虛擬Dom diff原理的,但不少都是在原代碼的基礎上添加些註釋等等,這裏從0行代碼開始實現一個
Vue2的虛擬DOMnode

實現VNode

src/core/vdom/Vnode.js

export class VNode{
    constructor (
        tag, //標籤名
        children,//孩子[VNode,VNode],
        text, //文本節點
        elm //對應的真實dom對象
    ){
        this.tag = tag;
        this.children = children
        this.text = text;
        this.elm = elm;
    }
}
export function createTextNode(val){
    //爲何這裏默認把elm置爲undefined,不直接根據tag 用document.createElement(tagName)把elm賦值?而要等後面createElm時候再賦值呢?
    return new VNode(undefined,undefined,String(val),undefined)
}
export function createCommentNode(tag,children){
    if(children){
        for(var i=0;i<children.length;i++){
            var child = children[i];
            if(typeof child == 'string'){
                children[i] = createTextNode(child)
            }
        }
    }
    return new VNode(tag,children,undefined,null)
}

定義一個Vnode類, 建立節點分爲兩類,一類爲text節點,一類非text節點app

src/main.js

import {VNode,createCommentNode} from './core/vdom/vnode'
var newVonde = createCommentNode('ul',[createCommentNode('li',['item 1']),createCommentNode('li',['item 2']),createCommentNode('li',['item 3'])])

在main.js就能夠根據Vnode 生成對應的Vnode對象,上述代碼對應的dom表示
<ul>dom

<li>item1</li>
<li>item2</li>
<li>item3</li>

</ul>函數

先實現不用diff把Vnode渲染到頁面中來

爲何先來實現不用diff渲染Vnode的部分,這裏也是爲了統計渲染的時間,來代表一個道理
並非diff就比非diff要開,虛擬DOM並非任什麼時候候性能都比非虛擬DOM 要快工具

先來實現一個工具函數,不熟悉的人能夠手工敲下代碼 熟悉下性能

// 真實的dom操做
src/core/vdom/node-ops.js

export function createElement (tagName) {
  return document.createElement(tagName)
}

export function createTextNode (text) {
  return document.createTextNode(text)
}

export function createComment (text) {
  return document.createComment(text)
}

export function insertBefore (parentNode, newNode, referenceNode) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function removeChild (node, child) {
  node.removeChild(child)
}

export function appendChild (node, child) {
  node.appendChild(child)
}

export function parentNode (node) {
  return node.parentNode
}

export function nextSibling (node) {
  return node.nextSibling
}

export function tagName (node) {
  return node.tagName
}

export function setTextContent (node, text) {
  node.textContent = text
}

export function setAttribute (node, key, val) {
  node.setAttribute(key, val)
}
src/main.js

import {VNode,createCommentNode} from './core/vdom/vnode'
import patch from './core/vdom/patch'


var container = document.getElementById("app");
var oldVnode = new VNode(container.tagName,[],undefined,container);
var newVonde = createCommentNode('ul',[createCommentNode('li',['item 1']),createCommentNode('li',['item 2']),createCommentNode('li',['item 3'])])


console.time('start');
patch(oldVnode,newVonde); //渲染頁面
console.timeEnd('start');

這裏咱們要實現一個patch方法,把Vnode渲染到頁面中this

src/core/vdom/patch.js

import * as nodeOps from './node-ops'
import VNode from './vnode'


export default function patch(oldVnode,vnode){
    let isInitialPatch = false;
    if(sameVnode(oldVnode,vnode)){
        //若是兩個Vnode節點的根一致  開始diff
        patchVnode(oldVnode,vnode)
    }else{
        //這裏就是不借助diff的實現
        const oldElm = oldVnode.elm;
        const parentElm = nodeOps.parentNode(oldElm);
        createElm(
            vnode,
            parentElm,
            nodeOps.nextSibling(oldElm)
        )
        if(parentElm != null){
            removeVnodes(parentElm,[oldVnode],0,0)
        }
    }
    return vnode.elm;
}
function patchVnode(oldVnode,vnode,removeOnly){
    if(oldVnode === vnode){
        return
    }
    const elm = vnode.elm = oldVnode.elm
    const oldCh = oldVnode.children;
    const ch = vnode.children

    if(isUndef(vnode.text)){
        //非文本節點
        if(isDef(oldCh) && isDef(ch)){
            //都有字節點
            if(oldCh !== ch){
                //更新children
                updateChildren(elm,oldCh,ch,removeOnly);
            }
        }else if(isDef(ch)){
            //新的有子節點,老的沒有
            if(isDef(oldVnode.text)){
                nodeOps.setTextContent(elm,'');
            }
            //添加子節點
            addVnodes(elm,null,ch,0,ch.length-1)
        }else if(isDef(oldCh)){
            //老的有子節點,新的沒有
            removeVnodes(elm,oldCh,0,oldCh.length-1)
        }else if(isDef(oldVnode.text)){
            //不然老的有文本內容 直接置空就行
            nodeOps.setTextContent(elm,'');
        }
    }else if(oldVnode.text !== vnode.text){
        //直接修改文本
        nodeOps.setTextContent(elm,vnode.text);
    }
}

function updateChildren(parentElm,oldCh,newCh,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 refElm;
    const canMove = !removeOnly
    while(oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx){
        if(isUndef(oldStartVnode)){
            oldStartVnode = oldCh[++oldStartIdx]
        }else if(isUndef(oldEndVnode)){
            oldEndVnode = oldCh[--oldEndIdx]
        }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);
            //更換順序
            canMove && nodeOps.insertBefore(parentElm,oldStartVnode.elm,nodeOps.nextSibling(oldEndVnode.elm))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if(sameVnode(oldEndVnode,newStartVnode)){
            patchVnode(oldEndVnode,newStartVnode)
            canMove && nodeOps.insertBefore(parentElm,oldEndVnode.elm,oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        }else{
            createElm(newStartVnode,parentElm,oldStartVnode.elm)
            newStartVnode = newCh[++newStartIdx];
        }
    }

    if(oldStartIdx > oldEndIdx){
        //老的提早相遇,添加新節點中沒有比較的節點
        refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx+1].elm
        addVnodes(parentElm,refElm,newCh,newStartIdx,newEndIdx)
    }else{
        //新的提早相遇  刪除多餘的節點
        removeVnodes(parentElm,oldCh,oldStartIdx,oldEndIdx)
    }
}
function removeVnodes(parentElm,vnodes,startIdx,endIdx){
    for(;startIdx<=endIdx;++startIdx){
        const ch = vnodes[startIdx];
        if(isDef(ch)){
            removeNode(ch.elm)
        }
    }
}

function addVnodes(parentElm,refElm,vnodes,startIdx,endIdx){
    for(;startIdx <=endIdx;++startIdx ){
        createElm(vnodes[startIdx],parentElm,refElm)
    }
}

function sameVnode(vnode1,vnode2){
    return vnode1.tag === vnode2.tag
}
function removeNode(el){
    const parent = nodeOps.parentNode(el)
    if(parent){
        nodeOps.removeChild(parent,el)
    }
}
function removeVnodes(parentElm,vnodes,startIdx,endIdx){
    for(;startIdx<=endIdx;++startIdx){
        const ch = vnodes[startIdx]
        if(isDef(ch)){
            removeNode(ch.elm)
        }
    }
}
function isDef (s){
    return s != null
}
function isUndef(s){
    return s == null
}
function createChildren(vnode,children){
    if(Array.isArray(children)){
        for(let i=0;i<children.length;i++){
            createElm(children[i],vnode.elm,null)
        }
    }
}
function createElm(vnode,parentElm,refElm){
    const children = vnode.children
    const tag = vnode.tag
    if(isDef(tag)){
        // 非文本節點
        vnode.elm = nodeOps.createElement(tag); // 其實能夠初始化的時候就賦予
        createChildren(vnode,children);
        insert(parentElm,vnode.elm,refElm)
    }else{
        vnode.elm = nodeOps.createTextNode(vnode.text)
        insert(parentElm,vnode.elm,refElm)
    }
}
function insert(parent,elm,ref){
    if(parent){
        if(ref){
            nodeOps.insertBefore(parent,elm,ref)
        }else{
            nodeOps.appendChild(parent,elm)
        }
    }
}

這就是完整實現了code

相關文章
相關標籤/搜索