了解dom-diff算法

4 篇文章 0 订阅

虚拟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)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值