[Analyse du code source frontal] Le principe de base du DOM virtuel

Référence : Cours de la série d'analyse de code source Vue

Remarques sur la série :

Contour:

  • Introduction à snabbdom
  • Comment fonctionne la fonction h de snabbdom
  • principe de l'algorithme diff
  • algorithme diff manuscrit

Code source de ce chapitre : https://gitee.com/szluyu99/vue-source-learn/tree/master/Snabbdom_Study

Une brève introduction à Dom et diff

Présentez brièvement le DOM virtuel et l'algorithme diff :

diff se produit sur le DOM virtuel :

Introduction au snabbdom et à la construction d'environnement

snabbdom est une célèbre bibliothèque virtuelle DOM et à l'origine de l'algorithme diff. Le code source de Vue emprunte à snabbdom

adresse git : https://github.com/snabbdom/snabbdom

notes d'installation de snabbdom :

  • Le code source de snabbdom sur git est écrit en TypeScript, et la version JavaScript compilée n'est pas disponible sur git
  • Si vous souhaitez utiliser directement la version JavaScript intégrée de la bibliothèque snabbdom, vous pouvez la télécharger à partir de npm
npm i -D snabbdom

L'environnement de test de snabbdom est configuré :

  • La bibliothèque snabbdom est une bibliothèque DOM. Bien sûr, elle ne peut pas s'exécuter dans l'environnement nodejs. Elle doit créer un environnement de développement webpack et webpack-dev-server
  • Remarque : La version webpack@5ci-dessus , car l'ancienne version de Webpack n'a pas la capacité de lire les exportations dans la carte d'identité.
npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3
npm init

npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3

npm i -D snabbdom

webpack.config.js:

const path = require('path');

module.exports = {
    
    
  // 入口
  entry: './src/index.js',
  // 出口
  output: {
    
    
    // 虚拟打包路径,文件夹不会真正生成,而是在 8080 端口虚拟生成
    publicPath: 'xuni',
    // 打包出来的文件名
    filename: 'bundle.js',
  },
  devServer: {
    
    
    // 端口号
    port: 8080,
    // 静态资源文件夹
    contentBase: 'www'
  }
};

Placez le code de test sur la page d'accueil de l'entrepôt snabbdom dans index.js, et l'environnement est construit avec succès lorsque vous voyez l'interface suivante :

N'oubliez pas de mettre undiv#container

Apprentissage du code source

Code source de ce chapitre : https://gitee.com/szluyu99/vue-source-learn/tree/master/Snabbdom_Study

DOM virtuel et fonctions h

diff se produit sur le DOM virtuel :

Ce cours n'examine pas comment le DOM réel devient le DOM virtuel .

Thèmes abordés dans ce cours :

  1. Comment le DOM virtuel est-il généré par la fonction de rendu (fonction h) ?
  2. principe de l'algorithme diff
  3. Comment le DOM virtuel devient le vrai DOM via diff

Ce cours n'étudie pas DOM réel => DOM virtuel , mais DOM virtuel => DOM réel est inclus dans l'algorithme diff

La fonction h permet de générer un nœud virtuel (vnode) :

1. Appelez la fonction h

var vnode = h('a', {
    
     props: {
    
     href: 'http://www.atguigu.com' }}, '尚硅谷');

2. Les nœuds virtuels obtenus sont les suivants :

{
    
     "sel": "a", "data": {
    
     props: {
    
     href: 'http://www.atguigu.com' } }, "text": "尚硅谷" }

3. Le vrai nœud DOM qu'il représente :

<a href="http://www.atguigu.com">尚硅谷</a>

Un nœud virtuel possède les propriétés suivantes :

  • enfants nœuds virtuels enfants
  • Certaines données attachées au nœud de donnéesprops , telles que ,class
  • Le nœud DOM réel correspondant à elm
  • L'identifiant unique du nœud clé
  • L'étiquette correspondant à sel
  • contenu de la balise de texte

Exemples d'utilisation de nœuds virtuels :
1. Créer une fonction de correctif
2. Créer un nœud virtuel
3. Créer une arborescence sur un nœud virtuel

// 创建出 patch 函数
var patch = init([classModule, propsModule, styleModule, eventListenersModule]);

// 创建虚拟节点
var myVnode1 = h(
  "a",
  {
    
    
    props: {
    
    
      href: "https://www.baidu.com ",
      target: "_blank",
    },
  },
  "百度"
);

// const myVnode2 = h('div', {}, '我是一个盒子')
const myVnode2 = h('div', '我是一个盒子') // 没有属性可以简写

const myVnode3 = h('ul', [
  h('li', '苹果'),
  h('li', '香蕉'),
  h('li', '西瓜'),
  h('li', [h('span', '火龙果'), h('span', '榴莲')])
])


// 让虚拟节点上树
const container = document.getElementById("container");
patch(container, myVnode3);

Utilisation imbriquée de la fonction h :

L'utilisation de la fonction h est très flexible :

Fonction h manuscrite

Ce que nous implémentons est une version à profil bas de la fonction h, qui ne considère pas trop de fonctions de surcharge de fonctions, et satisfait les trois utilisations suivantes :

  • h('div', {}, 'hello'), le troisième paramètre est une chaîne ou un nombre
  • h('div', {}, [], le troisième paramètre est un tableau (et les éléments du tableau sont tous des fonctions h)
  • h('div', {}, h()), le troisième paramètre est une fonction h

L'utilisation de la fonction h montre :

var myVnode1 = h('div', {
    
    }, 'hello')

var myVnode2 = h('div', {
    
    }, [
    h('p', {
    
    }, 'A'),
    h('p', {
    
    }, 'B'),
    h('p', {
    
    }, h('span', {
    
    }, 'C')),
])

vnode.js

/**
 * 该函数的功能非常简单,就是把传入的 5 个参数组合成对象返回
 */
export default function(sel, data, children, text, elm) {
    
    
    const key = data.key
    return {
    
    
        sel, data, children, text, elm, key
    }
}

h.js

// 低配版本的 h 函数,必须接受 3 个参数,弱化它的重载功能
// 对于函数 h(sel, data, c) 有三种形态:
// 形态1:h('div', {}, 'hello')
// 形态2:h('div', {}, [])
// 形态3:h('div', {}, h())
export default function h(sel, data, c) {
    
    
  // 检查参数的个数
  if (arguments.length != 3) {
    
    
    throw new Error("对不起,h 函数必须传入 3 个参数,这是低配版 h 函数");
  }
  // 检查参数 c 的类型
  if (typeof c == "string" || typeof c == "number") {
    
    
    // 调用 h 函数是形态1 h('div', {}, 'hello')
    return vnode(sel, data, undefined, c, undefined);
  } else if (Array.isArray(c)) {
    
    
    // 调用 h 函数是形态2 h('div', {}, [])
    let children = []
    // 遍历 c,收集 children
    for (let i = 0; i < c.length; i++) {
    
    
      // 检查 c[i] 必须是一个对象,且满足以下条件
      if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel')))
        throw new Error('传入的数组参数中有项不是 h 函数')
      // 这里不用执行 c[i],因为测试语句中已经有了执行
      // 此时只需要收集好就可以
      children.push(c[i])
    }
    // 循环结束了,说明 children 收集完毕,返回虚拟节点,具有 children 属性
    return vnode(sel, data, children, undefined, undefined)
  } else if (typeof c == "object" && c.hasOwnProperty("sel")) {
    
    
    // 调用 h 函数是形态3 h('div', {}, h())
    // 即,传入的 c 是唯一的 children
    let children = [c]
    return vnode(sel, data, children, undefined, undefined)
  } else {
    
    
    throw new Error("传入的第三个参数类型不对");
  }
}

algorithme de différence

Expérience avec l'algorithme diff :

  • La clé est très importante , la clé est l'identifiant unique du nœud, elle indiquera à l'algorithme diff qu'il s'agit du même nœud DOM avant et après le changement
  • Ce n'est que lorsqu'il s'agit du même nœud virtuel que la comparaison raffinée sera effectuée , sinon, l'ancien sera violemment supprimé et le nouveau sera inséré
  • Seule la même comparaison de couches est effectuée, et aucune comparaison entre couches n'est effectuée , sinon, l'ancienne est toujours violemment supprimée et la nouvelle est insérée

Les deux dernières opérations ne sont pratiquement pas rencontrées dans le développement réel de Vue, ce qui est un mécanisme d'optimisation raisonnable

La logique globale de la fonction patch :

Comment définir la notion de « même nœud » ?

/**
 * 判断 vnode1 和 vnode2 是不是同一个节点
 */
function sameVnode(vnode1, vnode2) {
    
    
	// 旧节点的 key === 新节点的 key 且 旧节点的选择器 === 新节点的选择器
    return vnode1.key == vnode2.key && vnode1.sel == vnode2.sel
}

patch.js

export default function patch(oldVnode, newVnode) {
    
    
    // 判断传入的第一个参数,是 DOM 节点还是虚拟节点
    if (!oldVnode.sel) {
    
    
        // 如果是 DOM 节点,则包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {
    
    }, [], undefined, oldVnode)
    }
    // 判断 oldVnode 和 newVnode 是不是同一个节点
    if (sameVnode(oldVnode, newVnode)) {
    
    
        // 是同一个节点,精细化比较
        patchVnode(oldVnode, newVnode)
    } else {
    
    
        // 不是同一个节点,暴力删除旧的,插入新的
        let newVnodeElm = createElement(newVnode)
        // 插入到老节点之前
        if (oldVnode.elm.parentNode && newVnodeElm) {
    
    
            oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
        }
        // 删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm)
    }
}

createElement.js: Créer un vrai DOM basé sur vnode

/**
 * 根据 vnode 创建 DOM 节点
 */
export default function createElement(vnode){
    
    
    // 创建一个 DOM 节点,这个节点现在还是孤儿节点
    let domNode = document.createElement(vnode.sel);
    // 判断内部是文本还是有子节点
    if (vnode.text && (!vnode.children|| !vnode.children.length )){
    
    
        // 内部是文本(让文字上树)
        domNode.innerText = vnode.text
    } else if (Array.isArray(vnode.children) && vnode.children.length) {
    
    
        // 内部是子节点,递归创建节点
        for (let i = 0; i < vnode.children.length; i++) {
    
    
            // 得到当前子节点
            let ch = vnode.children[i]
            // 创建出它的 DOM,一旦调用 createElement 函数,就会创建出一个 DOM 节点,但是还没有上树
            let chDOM = createElement(ch)
            // 上树
            domNode.appendChild(chDOM)
        }
    }
    // 补充 elm 属性,elm 是一个纯 DOM 对象
    vnode.elm = domNode
    return domNode
}

La logique de la comparaison raffinée :

patchVnode.js

La difficulté et le cœur de diff sont extraits dans updateChildrenla fonction , qui sera introduite plus tard

/**
 * 对比同一个虚拟节点,精细化比较
 */
export default function patchVnode(oldvnode, newvnode) {
    
    
    // 判断新旧 vnode 是否是同一个节点
    if (oldvnode == newvnode) return
    // 判断新 vnode 是否有 text 属性
    if (newvnode.text && (!newvnode.children || !newvnode.children.length)) {
    
    
        // 新 vnode 有 text 属性
        console.log('新 vnode 有 text 属性');
        if (newvnode.text != oldvnode.text) {
    
    
            // 新vnode 和 旧vnode 的 text 属性不一样,则更新 text 
            // 如果 旧node 是 children 属性,会消失掉
            oldvnode.elm.innertext = newvnode.text
	        newVnode.elm = oldVnode.elm // 补充
        }
    } else {
    
    
        // 新 vnode 没有 text 属性,有 children
        console.log('新 vnode 没有 text 属性');
        if (oldvnode.children && oldvnode.children.length) {
    
    
            // 新老节点都有 children,最复杂的情况
            // 这里是 diff 的难点与核心!!!!!!
            updateChildren(oldvnode.elm, oldvnode.children, newvnode.children)
        } else {
    
    
            // 老的没有 children,新的有 children
            // 清空老的节点的内容
            oldvnode.elm.innertext = ''
            // 遍历新 vnode 的子节点,创建 DOM,上树
            for (let i = 0; i < newvnode.children.length; i++) {
    
    
                let dom = createElement(newvnode.children[i])
                oldvnode.elm.appendChild(dom)
            }
        }
    }
}

Quatre types de recherches de résultats dans diff :

  1. Le nouveau front et l'ancien front , frappez ensuite, descendez en même temps
  2. La nouvelle reine et l'ancienne reine , lorsqu'elles sont touchées, montent en même temps
  3. Après le nouveau poste et l'ancien front , s'il est touché, le nœud pointé par le nouveau front se déplace vers le poste après l'ancien
  4. Le nouveau front et l'ancien poste , s'il est touché, le nœud pointé par le nouveau front se déplace vers le front de l'ancien front

Si l'on touche, aucun jugement ne sera porté, et s'il rate, il sera jugé de haut en bas

S'il n'y a pas de résultat, vous devez utiliser une boucle pour trouver

Vous avez encore beaucoup de questions sur cet endroit ? ? ?

export default function updateChildren(parentElm, oldCh, newCh) {
    
    
    console.log('updateChildren');
    console.log(oldCh, newCh);

    // 旧前
    let oldStartIdx = 0
    // 新前
    let newStartIdx = 0
    // 旧后
    let oldEndIdx = oldCh.length - 1
    // 新后
    let newEndIdx = newCh.length - 1
    // 旧前节点
    let oldStartVnode = oldCh[oldStartIdx]
    // 旧后节点
    let oldEndVnode = oldCh[oldEndIdx]
    // 新前节点
    let newStartVnode = newCh[newStartIdx]
    // 新后节点
    let newEndVnode = newCh[newEndIdx]

    let keyMap = {
    
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    
    

        // 首先不是判断 1234 命中,而是要略过已经加undefined标记的东西
        if (!oldStartVnode || !oldCh[oldStartIdx]) {
    
    
            oldStartVnode = oldCh[++oldStartIdx]
        } else if (!oldEndVnode || !oldCh[oldEndIdx]) {
    
    
            oldEndVnode = oldCh[--oldEndIdx]
        } else if (!newStartVnode || !newCh[newStartIdx]) {
    
    
            newStartVnode = newCh[++newStartIdx]
        } else if (!newEndVnode || !newCh[newEndIdx]) {
    
    
            newEndVnode = newCh[--newEndIdx]
        }
        // 开始依次判断 1234 命中
        else if (sameVnode(oldStartVnode, newStartVnode)) {
    
    
            // 命中1 新前 和 旧前
            console.log('1 新前 和 旧前');
            patchVnode(oldStartVnode, newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        } else if (sameVnode(oldEndVnode, newEndVnode)) {
    
    
            // 命中2 新后 和 旧后
            console.log('2 新后 和 旧后');
            patchVnode(oldEndVnode, newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(newEndVnode, oldStartVnode)) {
    
    
            // 命中3 新后 和 旧前
            console.log('3 新后 和 旧前');
            patchVnode(oldStartVnode, newEndVnode)
            // 移动节点,移动新前指向的节点到旧后的后面
            parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling)
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        } else if (sameVnode(newStartVnode, oldEndVnode)) {
    
    
            // 命中4 新前 和 旧后
            console.log('4 新前 和 旧后');
            patchVnode(oldEndVnode, newStartVnode)
            // 移动节点,移动新前指向的节点到旧前的前面
            parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        } else {
    
    
            // 四种命中都没有命中
            console.log('5 都没有匹配');
            // 寻找 key 的 map
            if (!keyMap) {
    
    
                for (let i = oldStartIdx; i <= oldEndIdx; i++) {
    
    
                    const key = oldCh[i].key
                    if (key) keyMap[key] = i
                }
            }
            console.log(keyMap);
            const idxInOld = keyMap[newStartVnode.key];
            console.log(idxInOld);
            if (idxInOld == undefined) {
    
    
                // 判断,如果idxInOld是undefined表示它是全新的项
                // 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
                parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
            } else {
    
    
                // 如果不是undefined,不是全新的项,而是要移动
                const elmToMove = oldCh[idxInOld];
                patchVnode(elmToMove, newStartVnode);
                // 把这项设置为undefined,表示我已经处理完这项了
                oldCh[idxInOld] = undefined;
                // 移动,调用insertBefore也可以实现移动。
                parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
            }
            // 指针下移,只移动新的头
            newStartVnode = newCh[++newStartIdx];
        }
    }

    // 继续看看有没有剩余的节点
    if (newStartIdx <= newEndIdx) {
    
    
        console.log('还有剩余节点没有处理');
        // 遍历新的newCh,添加到老的没有处理的之前
        for (let i = newStartIdx; i <= newEndIdx; i++) {
    
    
            // insertBefore方法可以自动识别null,如果是null就会自动排到队尾去。和appendChild是一致了。
            // newCh[i]现在还没有真正的DOM,所以要调用createElement()函数变为DOM
            parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
        }
    } else if (oldStartIdx <= oldEndIdx) {
    
    
        console.log('old还有剩余节点没有处理,要删除项');
        // 批量删除oldStart和oldEnd指针之间的项
        for (let i = oldStartIdx; i <= oldEndIdx; i++) {
    
    
            if (oldCh[i]) {
    
    
                parentElm.removeChild(oldCh[i].elm);
            }
        }
    }
}

Je suppose que tu aimes

Origine blog.csdn.net/weixin_43734095/article/details/125469803
conseillé
Classement