瞭解dom-diff算法

虛擬DOM

1、新建react項目

//全局安裝腳手架工具
$ npm install create-react-app -g
//創建項目
$ create-react-app dom-diff
// 進入項目目錄
$ cd dom-diff
// 編譯
$ npm run start

2、定義虛擬DOM類

什麼是虛擬DOM? 虛擬DOM就是使用javascript對象來表示真實DOM,是一個樹形結構。
這個對象結構如下:

const virtualDom = {
    type: 'ul',
    props: {
        class: 'lists'
    },
    children: [{
        type: 'li',
        props: {},
        children: [1]
    },
    {
        type: 'li',
        props: {},
        children: [2]
    },
    {
        type: 'li',
        props: {},
        children: [3]
    }
 ]
}

爲了構建這樣的對象,我們需要新建一個 element.js 文件。

  1. 需要定義對象,用來描述虛擬dom
class Element{
    constructor(type,props,childrens){
        this.type= type
        this.props = props
        this.childrens= childrens
    }
  1. }

提供個方法,生成element對象

function createElement(type,props,children){
    return new Element(type,props,children)
}

element.js
全部代碼

/**
 * 定義虛擬dom對象
 */
class Element{
    constructor(type,props,childrens){
        this.type= type
        this.props = props
        this.childrens= childrens
    }
}
/**
 * 生成虛擬dom
 * @param {*} type 元素類型
 * @param {*} props 元素屬性
 * @param {*} children 子元素
 */
function createElement(type,props,children){
    return new Element(type,props,children)
}

3、構建虛擬DOM

在 index.js 文件中

import {createElement} from './element'

let virtualDom  = createElement('ul',{className:'lists'},[
    createElement('li',{},[1]),
    createElement('li',{},[2]),
    createElement('li',{},[3])
])

console.log(virtualDom)

變量 virtualDom 在控制檯的打印結果:
圖片

4、虛擬DOM生成真實DOM

我們已經生成了構建出了虛擬dom對象,下一步就是將這個對象渲染成真實的dom對象。
我們在 element.js 文件中新增一個render方法。

/**
 * 將虛擬dom轉化成真實dom
 * @param {Element} virtualDom 虛擬dom
 */
function render(virtualDom){
    if(!virtualDom) throw new Error('傳入的虛擬dom爲空')
    // 根據type類型來創建對應的元素
    let el = document.createElement(virtualDom.type)
    //遍歷虛擬dom的屬性
    for(let attrKey in virtualDom.props){
        const attrValue = virtualDom.props[attrKey]
        el.setAttribute(attrKey,attrValue)
    }
    //處理子元素
    if(virtualDom.childrens){
        for(let child of virtualDom.childrens){
            //判斷是不是element類型。
            if(child instanceof Element){
                //遞歸
                el.appendChild(render(child)) 
            }else{
                //如果不是element,則證明是文本節點
                el.appendChild(document.createTextNode(child))
           }
       }
    }
    return el
}

5、將DOM元素渲染到頁面上

在element.js 中新增一個方法:renderElement(node,element)

//...省略其他代碼
/**
 * 將構建好的真實dom插入到目標元素裏
 * @param {*} el 目標元素
 * @param {*} dom 真實dom
 */
function renderElement(el,dom){
    el.appendChild(dom)
}
export {Element,createElement,render,renderElement}

在 index.js 頁面操作,將虛擬dom轉成真實dom,再添加到id爲root的節點下面

import {createElement,render,renderElement} from './element'

let virtualDom  = createElement('ul',{class:'lists'},[
    createElement('li',{},[1]),
    createElement('li',{},[2]),
    createElement('li',{},[3])
])

//將虛擬dom轉成真實dom
let actualDom = render(virtualDom)
//將真實dom渲染到頁面上

renderElement(document.getElementById(‘root’),actualDom)

界面效果
圖片

DOM-DIFF

  1. dom-diff 三種優化策略
  • 更新的時候只比較平級虛擬節點,依次進行比較並不會跨級比較,比較差異部分,生成對應補丁包,根據補丁包改變的內容更新差異的dom。
  • 更新的時候平級比較兩個虛擬DOM,當發現節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用於進一步的比較。如果該刪除的節點之下有子節點,那麼這些子節點也會被完全刪除,它們也不會用於後面的比較。這樣只需要對樹進行一次遍歷,便能完成整個DOM樹的比較。新增的節點,會對應的創建一個新的節點。
  • 第三種更新的時候平級比較兩個虛擬DOM,如果只是一層變了,互換了位置,那麼它會複用此虛擬節點,把對應的位置互換一下即可,這個是通過給對應元素添加的key不同來實現的。
  1. 前置知識
    樹的先序深度遍歷
    按照 “根-左-右” 的順序進行遍歷

  2. 代碼實現
    3.1 根據虛擬dom差異得到補丁對象

import {
    isString
}
from './utils/types.js'

const ATTRS = 'ATTRS'const REPLACE = 'REPLACE'const REMOVE = 'REMOVE'const TEXT = 'TEXT'
//todo
//const ADD ='ADD'
/**
 * 
 * @param {*} newDom 新節點 
 * @param {*} oldDom 老節點
 */
// 所有都基於一個序號來實現
let num = 0
function diff(oldDom, newDom) {
    const patches = {}
    num = 0 walk(oldDom, newDom, num, patches) return patches
}
/**
 * 比較子節點
 * @param {*} oldChilds 
 * @param {*} newChilds 
 */
function diffChildren(oldChilds, newChilds, patches) {
    // 比較老的第一個和新的第一個
    oldChilds.forEach((old, idx) = >{
        walk(old, newChilds[idx], ++num, patches)
    });
}
function walk(oldNode, newNode, index, patches) {
    //每個元素都有一個補丁
    let currentPatch = []
    //是否是文本節點
    if (isString(oldNode) && isString(newNode)) {
        if (oldNode !== newNode) {
            currentPatch.push({
                type: TEXT,
                text: newNode
            })
        }
    }
    /**是否新節點爲空:被刪除 */
    else if (!newNode) {
        currentPatch.push({
            type: REMOVE
        })
    } //類型是否一致
    else if (oldNode.type === newNode.type) {
        //比較屬性
        let attrPatch = diffAttrs(oldNode.props, newNode.props)
        //存在屬性差異
        if (Object.keys(attrPatch).length > 0) {
            currentPatch.push({
                type: ATTRS,
                attr: attrPatch
            })
        }
        //比較子節點
        diffChildren(oldNode.childrens, newNode.childrens, patches)
    } else {
        //節點被替換
        currentPatch.push({
            type: REPLACE,
            node: newNode
        })
    }

    if (currentPatch.length > 0) {
        patches[index] = currentPatch
    }
}

/**
 * 比較屬性
 */
function diffAttrs(oldProps, newProps) {
    const patch = {}
    //先遍歷老屬性,比較與新屬性的差異
    for (let key in oldProps) {
        if (oldProps[key] !== newProps[key]) {
            patch[key] = newProps[key]
        }
    }
    //再遍歷新屬性,看是否有新增加的屬性
    for (let key in newProps) {
        //如果舊屬性裏沒有該值
        if (!oldProps.hasOwnProperty(key)) {
            patch[key] = newProps[key]
        }
    }
    return patch
}

export

3.2 根據補丁對象,更新dom

  import {
     render
 } from './element'
 let allPatches;
 let index = 0

 function patch(node, patches) {
     allPatches = patches
     index = 0
     walk(node)
 }

 const ATTRS = 'ATTRS'
 const REPLACE = 'REPLACE'
 const REMOVE = 'REMOVE'
 const TEXT = 'TEXT'
 /**
  * 開始循環打補丁
  */
 function walk(node) {
     let current = allPatches[index++]
     let childNodes = node.childNodes;
     // 先序深度,繼續遍歷遞歸子節點
     childNodes.forEach(child => walk(child));
     if (current) {
         doPatch(node, current);
     }
 }

 function doPatch(node, patches) {
     //同一個元素上可能打了多個補丁
     patches.forEach(current => {
         switch (current.type) {
             case ATTRS:
                 const attrs = current.attr
                 for (let key in attrs) {
                     node.setAttribute(key, attrs[key])
                 }
                 break
             case TEXT:
                 node.textContent = current.text
                 break;
             case REMOVE:
                 node.parentNode.removeChild(node)
                 break;
             case REPLACE:
                 let newNode = current.node
                 if (newNode instanceof Element) {
                     node.parentNode.replaceChild(render(newNode), node)
                 } else {
                     node.parentNode.replaceChild(document.createTextNode(newNode), node)
                 }
                 break;
             default:
                 break;
         }
     })

 }
 export default patch

3.3. 模擬數據變化,觸發重新渲染

import {
    createElement,
    render,
    renderElement
} from './element'
import './index.css'
import diff from './diff'
import patch from './patch'
let virtualDom1 = createElement('ul', {
    style: 'background:red;color:#fff;'
}, [
    createElement('li', {}, ["2"]),
    createElement('li', {}, ["2"]),
    createElement('li', {}, ["3"])
])


//將虛擬dom轉成真實dom
let actualDom = render(virtualDom1)


//將真實dom渲染到頁面上
renderElement(document.getElementById('root'), actualDom)

//模擬數據變化
let virtualDom2 = createElement('ul', {
    style: 'color:red;'
}, [
    createElement('li', {}, ["2"]),
    createElement('li', {}, ["2"]),
    createElement('li', {}, ["4"]),
])

const patchs = diff(virtualDom1, virtualDom2)
patch(actualDom, patchs)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章