Анализ алгоритмов сравнения и виртуального дома (понимание современных интерфейсных фреймворков)

JavaScript
Анализ алгоритмов сравнения и виртуального дома (понимание современных интерфейсных фреймворков)

React и Vue, как основные фреймворки для фронтенд-разработки в Китае, должны быть знакомы каждому в повседневной разработке. Нельзя отрицать, что их существование значительно повысило эффективность нашей разработки и удобство сопровождения нашего кода, но после «умного» их использования вы, увлекающиеся технологиями, должны ли вы понять, что стоит за этими фреймворками, какие идеи? Если нет, ничего страшного, давайте сделаем это вместе!

Весь код в этой статье был загруженgithub🐶

виртуальный DOM

Интуитивно виртуальный DOM на самом деле является структурой данных, представляющей реальную структуру DOM. Причина его использования в том, что частые манипуляции с DOM ухудшат производительность сайта.Чтобы обеспечить производительность, нам нужно максимально упростить работу с DOM.Мы можем сравнить разницу между старым и новые узлы, работая с виртуальным DOM, а затем точно получить минимальное значение, наиболее необходимую коллекцию DOM и, наконец, смонтировать на реальный DOM. Поскольку манипулирование структурой данных происходит намного быстрее, чем прямое изменение узла DOM, наша реальная манипуляция с DOM в лучшем случае, нам нужно сделать это только в конце, верно?

Как представить структуру DOM

Это DOM-структура списка. Давайте проанализируем ее. Информация, которая должна быть включена:

1. Тип тега ul, li...

2. Класс атрибутов тега, стиль...

3. Дочерний узел ul->li li->text ...

Какой бы сложной ни была структура, она похожа, поэтому после того, как мы найдем общность структуры DOM, как мы должны ее выразить?

Из этого изображения мы можем обнаружить, что мы можем легко представить его с помощью объекта JS-объекта, и некоторые свойства также очень легко понять.

  • tagName соответствует реальному типу тега
  • attrs представляют все атрибуты на узле
  • child представляет дочерний узел этого узла

Итак, можем ли мы установить такой класс для этого виртуального DOM?

function newElement(tag,attr,child){ //创建对象函数
    return new Element(tag,attr,child)
}

пройти тест

Хорошо, нет проблем, верно? Теперь, когда виртуальный DOM фактически создан, как я могу подключить его к реальному DOM после того, как у нас есть виртуальный DOM?

Создание реальных узлов DOM

Сначала нам понадобится метод для установки свойств метки в соответствии со свойствами объекта.

Затем мы добавляем метод рендеринга, который создает узел внутри класса.

На этом этапе мы можем создать настоящие узлы DOM с помощью метода рендеринга.Внутри метода мы устанавливаем свойства, вызывая метод SetVdToDom, а затем выполняем суждение о типах дочерних узлов, возвращаясь к последним оставшимся текстовым узлам.

Наконец, мы визуализируем дом в браузере с помощью метода renderDom, чтобы увидеть

//vdmock.js 部分
const VdObj1 = newElement('ul',{id: 'list'},[
    newElement('li',{class: 'list-1',style:'color:red' }, ['lavie']),
    newElement('li',{class: 'list-2' }, ['virtual dom']),
    newElement('li',{class: 'list-3' }, ['React']),  
    newElement('li',{class: 'list-4' }, ['Vue']) 
])
const RealDom = VdObj1.render()
const renderDom = function(element,target){
    target.appendChild(element)
}
export default function start(){
   renderDom(RealDom,document.body)
}

// index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script type="module" src="./vdmock.js"  ></script>
    
    <title>Document</title>
</head>
<body >
    <script type="module" >
        import start from './vdmock.js'
        start()
    </script>
</body>
</html>

Результат выглядит следующим образом:

разница виртуальных DOM

С помощью описанного выше метода мы можем легко сгенерировать виртуальный DOM и отобразить его в браузере.Так как же рассчитать разницу между виртуальным DOM до и после того, как пользователь выполнит операцию? Далее, давайте представим алгоритм diff.

Мы рекурсивно сравниваем узлы через внутренний getDiff, передавая старые и новые узлы в diff, сохраняем изменения и затем возвращаемся.

получить массив минимальной разницы

const REMOVE = 'remove'
const MODIFY_TEXT =  'modify_text'
const CHANGE_ATTRS = 'change_attrs'
const TAKEPLACE = 'replace'
let initIndex = 0
const getDiff = (oldNode,newNode,index,difference)=>{
    let diffResult = []
    //新节点不存在的话说明节点已经被删除
    if(!newNode){
        diffResult.push({
            index,
            type: REMOVE
        }) //如果是文本节点直接替换就行
    }else if(typeof newNode === 'string' && typeof oldNode === 'string'){
        if(oldNode !== newNode){
            diffResult.push({
                index,
                value: newNode,
                type: MODIFY_TEXT
            })
        } //如果节点类型相同则则继续比较属性是否相同
    }else if(oldNode.tagName === newNode.tagName){
        let storeAttrs = {}
        for(let  key in oldNode.attrs){ 
            if(oldNode.attrs[key] !== newNode.attrs[key]){
               
                storeAttrs[key] = newNode.attrs[key]
            }
        }
        for (let key in newNode.attrs){
            if(!oldNode.attrs.hasOwnProperty(key)){
                storeAttrs[key] = newNode[key]
            }
        }   
        
        //判断是否有不同
        if(Object.keys(storeAttrs).length>0){
            diffResult.push({
                index,
                value: storeAttrs,
                type: CHANGE_ATTRS
            })
        } //遍历子节点
        oldNode.child.forEach((child,index)=>{
            //深度遍历所以要保留index
             getDiff(child,newNode.child[index],++initIndex,difference)
        }) 
        //如果类型不相同,那么无需对比直接替换掉就行
    }else if(oldNode.tagName !== newNode.tagName){
        diffResult.push({
            type: TAKEPLACE,
            index,
            newNode
        })
    } //最后将结果返回
    if(!oldNode){
        diffResult.push({
            type: TAKEPLACE,
            newNode
        })
    }
    if(diffResult.length){
        difference[index] = diffResult
    }
}


Результаты теста следующие:

обновить дом

Теперь, когда мы сгенерировали два виртуальных DOM и сохранили различия между двумя DOM в виде объектов, мы будем использовать их для обновления различий до реального DOM! ! !

paceФункция рекурсивно обновит разницу текущего узла с помощью dofix.

const doFix = (node,difference) =>{
     difference.forEach(item=>{
         switch (item.type){
             case 'change_attrs':
                 const attrs = item.value
                 for( let key in attrs ){
                     if(node.nodeType !== 1) 
                     return 
                     const value = attrs[key]
                     if(value){
                         SetVdToDom(node,key,value)
                         
                     }else{
                         node.removeAttribute(key)
                     }
                 }
                 break
                 case 'modify_text':
                     node.textContent = item.value
                     break
                case 'replace': 
                   let newNode = (item.newNode instanceof Element) ? item.newNode.render(item.newNode) : 
                   document.createTextNode(item.newNode)
                    node.parentNode.replaceChild(newNode,node)
                    break
                case 'remove' :
                    node.parentNode.removeChild(node)
                    break
                default: 
                    break
         }
     })
}

Все готово, давайте тестировать!

const VdObj1 = newElement('ul',{id: 'list'},[
    newElement('li',{class: 'list-1',style:'color:red' }, ['lavie']),
    newElement('li',{class: 'list-2' }, ['virtual dom']),
    newElement('li',{class: 'list-3' }, ['React']),  
    newElement('li',{class: 'list-4' }, ['Vue']) ,
])
const VdObj = newElement('ol',{id: 'list'},[
    newElement('h2',{class: 'list-1',style:'color:green' }, ['lavieee']),
    newElement('li',{class: 'list-2' }, ['virtual dom']),
    newElement('li',{class: 'list-3' }, ['React']), 
    newElement('li',{class: 'list-4' }, ['Vue']) ,
    newElement('li',{class: 'list-5' }, ['Dva']) ,
    newElement('li',{class: 'list-5' }, ['Dva']) 
 
])
const RealDom = VdObj1.render()
const renderDom = function(element,target){
    target.appendChild(element)
}
export default function start(){
   renderDom(RealDom,document.body)
   const diffs = diff(VdObj1,VdObj)
   fixPlace(RealDom,diffs)
}

before

diff after

хи хи идеально

На этих примерах, собственно, и может быть реализована идея виртуального дома.Если мы сможем разобраться в основных понятиях в процессе использования фреймворка, мы точно пойдем стабильнее.

2020.2.12 Обновление

Изменен код функции dofix и функции getdiff, есть некоторые проблемы до~~

Напоследок поздравляю всех с наступающим Новым годом, предложение Года Быка мягкое, прибавки к зарплате и никаких сверхурочных работ🐶! ! ! !