【笔记】Vue源码解析之虚拟DOM和diff算法

news/2024/7/10 1:56:23 标签: javascript, vue

diff算法和虚拟DOM

  • 笔记简介
  • diff算法和虚拟DOM简介
  • snabbdom简介和测试环境搭建
  • 虚拟 DOM 和 h 函数
  • 手写h函数
  • 感受diff算法
  • 手写上树
  • 尝试书写diff更新子节点

笔记简介

本文为尚硅谷视频学习笔记,参考博客学习速度更快,跟着视频记录笔记加深印象及补充视频中讲师所讲到的一些知识点,扩充了部分,加上遇到的问题及解决方案。

视频链接
https://www.bilibili.com/video/BV1v5411H7gZ?p=3
博客参考链接
https://blog.csdn.net/weixin_44972008/article/details/115620198
snabbdom库
https://github.com/snabbdom/snabbdom/tree/master/src
git笔记https://gitee.com/thinkerwing/study/tree/master/vue2/%E8%99%9A%E6%8B%9FDOM%E5%92%8Cdiff%E7%AE%97%E6%B3%95/study-snabbdom

注意可能会遇到的问题
1.可能是端口占用的情况导致的404
2.可能是缓存的情况导致新写入的逻辑无法执行,sources中查看
3.如果没有key的话,容易被判断成同一节点,key的作用主要是为了高效的更新虚拟DOM,通过源码学习能更深的感受到这一点的重要性

diff算法和虚拟DOM简介

diff算法
在这里插入图片描述
diff算法可以进行精细化比对,实现最小量更新。h3 和 ul 没有变化就可以不用动,只插入span和雪碧。

虚拟DOM
diff算法是发生在虚拟 DOM 上的
DOM如何变为虚拟DOM属于模板编译原理范畴,mustache
在这里插入图片描述

snabbdom简介和测试环境搭建

npm install -S snabbdom

在这里插入图片描述

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

配置webpack.config.js,参考官网配置
https://www.webpackjs.com/

下面附上几个配置

  • src/index.js
  • www/index.html
  • webpack.config.js

src/index.js

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
  } from "snabbdom";
  
  const patch = init([
    // Init patch function with chosen modules
    classModule, // makes it easy to toggle classes
    propsModule, // for setting properties on DOM elements
    styleModule, // handles styling on elements with support for animations
    eventListenersModule, // attaches event listeners
  ]);
  
  const container = document.getElementById("container");
  
  const vnode = h("div#container.two.classes", { on: { click: function () { } } }, [
    h("span", { style: { fontWeight: "bold" } }, "This is bold"),
    " and this is just normal text",
    h("a", { props: { href: "/foo" } }, "I'll take you places!"),
  ]);
  // Patch into empty DOM element – this modifies the DOM as a side effect
  patch(container, vnode);
  
  const newVnode = h(
    "div#container.two.classes",
    { on: { click: function () { } } },
    [
      h(
        "span",
        { style: { fontWeight: "normal", fontStyle: "italic" } },
        "This is now italic type"
      ),
      " and this is still just normal text",
      h("a", { props: { href: "/bar" } }, "I'll take you places!"),
    ]
  );
  // Second `patch` invocation
  patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
  

www/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div id="container"></div>
    <script src="/xuni/bundle.js"></script>
</body>
</html>

webpack.config.js

const path = require('path')
module.exports = {
    // webpack5 不用配置mode
    // 入口
    entry: "./src/index.js",
    // 出口
    output: {
      // 虚拟打包路径,文件夹不会真正生成,而是在8080端口虚拟生成
      publicPath: "xuni",
      // 打包出来的文件名
      filename: "bundle.js",
    },
    // 配置webpack-dev-server
    devServer: {
      // 静态根目录
      contentBase: 'www',
      // 端口号
      port: 8081,
    },
  };
  

更改package.json中script配置,通过npm run test 启动

 "scripts": {
    "test": "webpack-dev-server"
  },

启动如果遇到404要注意是否端口占用
cmd 查找80端口占用情况

netstat -ano|findstr "80"

在这里插入图片描述

taskkill/pid 117884 -t -f  // PID为117884的进程被杀掉

如果删不掉,可以考虑更换一下端口

虚拟 DOM 和 h 函数

  • 研究1:虚拟 DOM 如何被渲染函数(h函数)产生?
    我们要手写h函数
  • 研究2:diff算法原理?
    我们要手写diff算法
  • 研究3:虚拟 DOM 如何通过diff变为真正的 DOM 的
    事实上,虚拟 DOM 变回真正的 DOM,是涵盖在diff算法里面的

h函数用来产生虚拟节点(vnode)
在这里插入图片描述

虚拟节点vnode的属性

{
	children: undefined// 子元素 数组
	data: {} // 属性、样式、key
	elm: undefined // 对应的真正的dom节点(对象),undefined表示节点还没有上dom树
	key: // 唯一标识
	sel: "div" // 选择器
	text: "我是一个盒子" // 文本内容
}

使用h函数 创建虚拟节点

// 创建虚拟节点
var myVnode1 = h('a', { props: { href: 'https://www.baidu.com' } }, 'baidu')
console.log(myVnode1)

在这里插入图片描述

使用patch函数 将虚拟节点上DOM树
一个容器只能让一个虚拟节点上树,除非有内嵌的

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

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

// 一个容器只能让一个虚拟节点上树
const myVnode2 = h('div', {class: {'box': true}},'我是一个盒子')

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

h函数嵌套使用,得到虚拟DOM树(重要)
在这里插入图片描述
一定要套h函数,并且里面可以继续嵌套

const myVnode3 = h('ul', [
    h('li', '苹果'),
    h('li', '香蕉'),
    h('li',  [
        h('div', '西瓜籽'),
    ]),
    h('li', '番茄'),
  ])

手写h函数

  • 看源码的TS版代码,然后仿写JS代码

在这里插入图片描述

h.js

import vnode from "./vnode";
/**
 * 产生虚拟DOM树,返回的一个对象
 * 低配版本的h函数,这个函数必须接受三个参数,缺一不可
 * 调用只有三种形态 文字、数组、h函数
 * ① h('div', {}, '文字')
 * ② h('div', {}, [])
 * ③ h('div', {}, h())
 */
export default function (sel, data, c) {
  // 检查参数个数
  if (arguments.length !== 3) {
    throw new Error("h函数必须传入3个参数");
  }
  // 检查第参数c的类型
  if (typeof c === "string" || typeof c === "number") {
    // 说明现在调用h函数是形态1
    return vnode(sel, data, undefined, c, undefined);
  } else if (Array.isArray(c)) {
    // 说明现在调用h函数是形态2 数组
    let children = [];
    // 遍历 c 数组,收集children
    for (let item of c) {
      // 检查c[i]必须是一个对象,如果不满足 
      if (!(typeof item === "object" && item.hasOwnProperty("sel"))) {
        throw new Error("传入的数组参数中有项不是h函数");
      }
      // 不用执行item, 因为测试语句中已经有了执行,此时只要收集数组中的每一个对象
      children.push(item);
    }
    //循环结束了,就说明children收集完毕了,此时可以返回虚拟节点了,它有children属性
    return vnode(sel, data, children, undefined, undefined);
  } else if (typeof c === "object" && c.hasOwnProperty("sel")) {
    // 说明现在调用h函数是形态3 即,传入的c是唯一的children,不用执行c
    let children = [c];
    return vnode(sel, data, children, undefined, undefined);
  } else {
    throw new Error("传入的第三个参数类型不对");
  }
}

vnode.js

/**
 * 产生虚拟节点
 * 将传入的参数组合成对象返回
 * @param {string} sel 选择器
 * @param {object} data 属性、样式
 * @param {Array} children 子元素
 * @param {string|number} text 文本内容
 * @param {object} elm 对应的真正的dom节点(对象),undefined表示节点还没有上dom树
 * @returns 
 */
 // 函数的功能非常简单,就是把传入的5个参数组合成对象返回
 export default function(sel, data, children, text, elm) {
    const key = data.key;
    return { sel, data, children, text, elm, key };
  }

index.js

import h from "./mysnabbdom/h";

const myVnode1 = h("div", {}, [
  h("p", {}, "文字"),
  h("p", {}, []),
  h("p", {}, h('span', {}, '呵呵')),
]);
console.log(myVnode1);

在这里插入图片描述

感受diff算法

1. 最小量更新,key很关键。key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。
2.只有是同一个虚拟节点才进行精细化比较,否则就是暴力删除旧的、插入新的。
延伸问题:如何定义是同一个虚拟节点?答:选择器相同且key相同
3.只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,diff就是暴力删除旧的,然后插入新的

下面通过三个案例来对比

// 创建虚拟节点
var vnode1 = h(
    "ul", {}, [
        h('li', {}, 'A'),
        h('li', {}, 'B'),
        h('li', {}, 'C'),
        h('li', {}, 'D')
    ]
);

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

var vnode2 = h(
    "ul", {}, [
        h('li', {}, 'A'),
        h('li', {}, 'B'),
        h('li', {}, 'C'),
        h('li', {}, 'D'),
        h('li', {}, 'E')
    ]
);

//  点击按钮时, 将vnode1变为vnode2
btn.onclick = function() {
    patch(vnode1, vnode2)
}

在这里插入图片描述
用案例测试最小量更新,点击按钮,并没有刷新变回B。

再用一个新的案例测试前面插入E,全部都会更新,因为原来的A变成了E,B变成了A

var vnode2 = h(
    "ul", {}, [
        h('li', {}, 'E'),
        h('li', {}, 'A'),
        h('li', {}, 'B'),
        h('li', {}, 'C'),
        h('li', {}, 'D')
    ]
);

在这里插入图片描述
再来第三个测试

// 创建虚拟节点
var vnode1 = h(
    "ul", {}, [
        h('li', { key: 'A' }, 'A'),
        h('li', { key: 'B' }, 'B'),
        h('li', { key: 'C' }, 'C'),
        h('li', { key: 'D' }, 'D')
    ]
);

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

var vnode2 = h(
    "ul", {}, [
        h('li', { key: 'E' }, 'E'),
        h('li', { key: 'A' }, 'A'),
        h('li', { key: 'B' }, 'B'),
        h('li', { key: 'C' }, 'C'),
        h('li', { key: 'D' }, 'D')
    ]
);

在这里插入图片描述

只进行同层比较,不会跨层比较
在这里插入图片描述
diff处理新旧节点不是同一节点时

在这里插入图片描述

旧节点的key要和新节点的key相同且旧节点的选择器要和新节点的选择器相同

function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  const isSameKey = vnode1.key === vnode2.key;
  const isSameIs = vnode1.data?.is === vnode2.data?.is;
  const isSameSel = vnode1.sel === vnode2.sel;

  return isSameSel && isSameKey && isSameIs;
}

源码中创建子节点,需要递归

if (is.array(children)) {
        for (i = 0; i < children.length; ++i) {
          const ch = children[i];
          if (ch != null) {
            api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
          }
        }
      } else if (is.primitive(vnode.text)) {
        api.appendChild(elm, api.createTextNode(vnode.text));
      }

手写上树

  • patch方法是虚拟dom核心中的核心。在VNode(虚拟节点)改变后和初始化前都会调用。
  • patch的本质是将新旧vnode进行比较,创建、删除或者更新DOM节点/组件实例。
    了解一下递归的过程
    这一层调用下一层一直往下调,遇见文本节点,一层一层往回调。
    在这里插入图片描述
    index.js
import h from './mysnabbdom/h.js'
import patch from './mysnabbdom/patch.js'

const myVnode1 = h(
    "ul", {}, [
        h('li', {}, 'A'),
        h('li', {}, 'B'),
        h('li', {}, [
            h('div', {}, [
                h('ol', {}, [
                    h('li', {}, 'C1'),
                    h('li', {}, 'C2'),
                    h('li', {}, 'C3'),
                ])
            ])
        ]),
        h('li', {}, 'D')
    ]
);

const container = document.getElementById('container')
patch(container, myVnode1)

patch.js

import vnode from './vnode';
import createElement from './createElement';

export default function (oldVnode, newVnode) {
  // 判断传入的第一个参数是 DOM节点 还是 虚拟节点
  if (oldVnode.sel == '' || oldVnode.sel === undefined) {
    // 传入的第一个参数是DOM节点,此时要包装成虚拟节点
    oldVnode = vnode(
      oldVnode.tagName.toLowerCase(), // sel
      {}, // data
      [], // children
      undefined, // text
      oldVnode // elm
    );
  }
  // 判断 oldVnode 和 newVnode 是不是同一个节点
  if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
    console.log("是同一个节点,需要精细化比较");
  } else {
    console.log("不是同一个节点,暴力插入新节点,删除旧节点");
    let newVnodeElm = createElement(newVnode);
    let oldVnodeElm = oldVnode.elm;
    // 以oldVnodeElm为标杆
    if (oldVnodeElm.parentNode && newVnodeElm) {
      // 判断newVnodeElm是存在的,插入到老节点之前
      oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm);
    }
    // 删除老节点
    oldVnodeElm.parentNode.removeChild(oldVnodeElm);
  }
}

createElement.js

  // 真正创建节点。将vnode虚拟节点创建为DOM,是孤儿节点,不进行插入
 export default function createElement(vnode) {
    console.log('目的是把虚拟节点', vnode, '真正变为DOM');
    // 根据虚拟节点sel选择器属性 创建一个DOM节点,这个节点现在是孤儿节点
    let domNode = document.createElement(vnode.sel);
    // 判断是有子节点还是有文本
    if ( vnode.text !== "" && (vnode.children === undefined || vnode.children.length === 0)
    ) {
      // 说明内部是文本
      domNode.innerText = vnode.text;
      // 这里不上树,因为设置内部文字就相当于上树
    } else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
      // 说明内部是子节点,需要递归创建节点 
      for (let i = 0; i < vnode.children.length; i++) {
        // 得到当前这个children
        let ch = vnode.children[i]
        // 创建出它的DOM,一旦调用createElement意味着:创建出DOM了,并且它的elm属性指向了创建出的DOM,
        // 但是还没上树,是一个孤儿节点
        console.log(ch);
        let chDOM = createElement(ch)
        // n+1层就可以被n层创建回来
        // 上树
        domNode.appendChild(chDOM)
      }
    }
    // 补充虚拟节点的elm属性
    vnode.elm = domNode;
    // 返回domNode 和 vnode.elm 是一样的 引用类型值内存中同一个对象,elm的属性是一个纯DOM对象
    return domNode;
  }

测试结果:
在这里插入图片描述

尝试书写diff更新子节点

在这里插入图片描述
在这里插入图片描述
代码
index.js

import h from './mysnabbdom/h.js'
import patch from './mysnabbdom/patch.js'

const myVnode1 = h(
    "ul", {}, [
        h('li', { key: 'A' }, 'A'),
        h('li', { key: 'B' }, 'B'),
        h('li', { key: 'C' }, 'C')
    ]
);

// 得到盒子和按钮
const container = document.getElementById('container')
const btn = document.getElementById('btn')

// 第一次上树
patch(container, myVnode1)

// 新节点
const myVnode2 = h(
    "ul", {}, [
        h('li', { key: 'A' }, 'A'),
        h('li', { key: 'B' }, 'B'),
        h('li', { key: 'D' }, 'D'),
        h('li', { key: 'D' }, 'E'),
        h('li', { key: 'C' }, 'C'),
        h('li', { key: 'C' }, 'F'),
        h('li', { key: 'G' }, 'G'),
    ] 
)

btn.onclick = function() {
    patch(myVnode1, myVnode2)
}

patch.js

import vnode from './vnode';
import createElement from './createElement';
import patchVnode from './patchVnode.js';

export default function (oldVnode, newVnode) {
  // 判断传入的第一个参数是 DOM节点 还是 虚拟节点
  if (oldVnode.sel == '' || oldVnode.sel === undefined) {
    // 传入的第一个参数是DOM节点,此时要包装成虚拟节点
    oldVnode = vnode(
      oldVnode.tagName.toLowerCase(), // sel
      {}, // data
      [], // children
      undefined, // text
      oldVnode // elm
    );
  }
  // 判断 oldVnode 和 newVnode 是不是同一个节点
  if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
    console.log('是同一个节点');
    patchVnode(oldVnode, newVnode)
  } else {
    console.log("不是同一个节点,暴力插入新节点,删除旧节点");
    let newVnodeElm = createElement(newVnode);
    let oldVnodeElm = oldVnode.elm;
    // 以oldVnodeElm为标杆
    if (oldVnodeElm.parentNode && newVnodeElm) {
      // 判断newVnodeElm是存在的,插入到老节点之前
      oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm);
    }
    // 删除老节点
    oldVnodeElm.parentNode.removeChild(oldVnodeElm);
  }
}

patchVNode.js

import createElement from "./createElement";

export default function patchVnode(oldVnode, newVnode) {
    console.log("是同一个节点,需要精细化比较");
    // 判断新旧vnode是否是同一个对象
    if (oldVnode === newVnode) return;
    // 判断新的vnode有没有text属性
    if (newVnode.text !== undefined && newVnode.children == undefined || newVnode.
      children.length == 0) {
        console.log('新vnode有text属性');
        if (newVnode.text !== oldVnode.text) {
          // 如果新虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可。
          // 如果老的elm中是children,那么也会立即消失。
          oldVnode.elm.innerText = newVnode.text
        }
      } else {
        // 新vnode没有text属性
        console.log('新vnode没有text属性');
        // 判断老的有没有children
        if (oldVnode.children != undefined && oldVnode.children.length > 0) {
          // 老的有children,此时就是最复杂的情况。就是新老都有children
          // 所有未处理的节点的开头
          let un = 0
          for (let i = 0; i < newVnode.children.length; i++) {
              let ch = newVnode.children[i]
              // 再次遍历,看看oldVnode中有没有节点和它是same的
              let isExist = false
              for (let j = 0; j < oldVnode.children.length; j++) {
                  if (oldVnode.children[j].sel == ch.sel && oldVnode.children[j].key == ch.key) {
                    isExist = true
                  }
              }
              if (!isExist) {
                  console.log('ch', ch, i);
                  let dom = createElement(ch)
                  ch.elm = dom
                  if (un < oldVnode.children.length) {
                     oldVnode.elm.insertBefore(dom, oldVnode.children[un].elm)
                  } else {
                     oldVnode.elm.appendChild(dom)
                  }
              } else {
                  // 让处理的节点指针下移
                  un ++
              }
          }
        } else {
          // 老的没有children,新的有children
          // 清空老的节点的内容
          oldVnode.elm.innerHTML = ''
          // 遍历新的vnode的子节点,创建DOM,上树
          for (let i = 0; i < newVnode.children.length; i++) {
            let dom = createElement(newVnode.children[i])
            oldVnode.elm.appendChild(dom)
          }
        }
      }
}

测试
在这里插入图片描述


http://www.niftyadmin.cn/n/883287.html

相关文章

Study-VTK

Study-VTK 工作中需要用到vtk&#xff0c;他效率虽然比不上opengl&#xff0c;但是其比较注重代码结构严谨&#xff0c;功能完善而接口清晰&#xff0c;易于使用。在这里记录自己学习vtk和itk的过程。 以下内容/链接中自己写的博客主要根据 【VTK图形图像开发进阶&#xff08;张…

vue中使用element进行表单验证

因项目需求用到表单验证&#xff0c;但是网上缺少直接能用的&#xff0c;通过借鉴加以改造&#xff0c;用引用cdn的形式&#xff0c;可以直接打开html文件&#xff0c;不用搭建vue项目&#xff0c;便于参考学习&#xff0c;后期慢慢增补用到的表单验证。 参考链接 https://blog…

Study-VTK:求两个模型距离

计算距离使用的vtkDistancePolyDataFilter这个类。 DistancePolyData类输入两个模型、压缩两个模型、比较两个模型体积、计算两个模型距离、渲染在小模型上显示。double target_reduction_;// 输入模型压缩系数&#xff08;模型比较大时候压缩下&#xff0c;减少计算时间&…

qt 屏蔽QScrollArea滚动条响应菜单事件

ui->scroll_area->verticalScrollBar()->setContextMenuPolicy(Qt::NoContextMenu); ui->scroll_area->horizontalScrollBar()->setContextMenuPolicy(Qt::NoContextMenu);

【笔记】react 学习与记录

react 学习与记录基础精讲起步src-> App.js、index.js响应式设计思想和事件绑定计数器增减和循环案例写法注意面向对象拆分组件与组件间传值父组件向子组件传递数据子组件向父组件传递数据代码代码优化围绕react衍生出的思考react高级内容安装react开发调试工具PropTypes 与…

Study-VTK:vtkWidget 分割/配准类之 放置种子点(Qt + vtkSeedWidget)

vtkWidget 分割/配准类之 放置种子点&#xff08;Qt vtkSeedWidget&#xff09;1 vtkSeedWidget介绍2 vtkSeedWidget 官方案例3 vtkSeedWidget常用函数4 vtkSeedWidget使用技巧4.1 开启/关闭 放置种子点交互4.2 绑定种子点增加/删除信号到Qt4.3 创建种子点增加/删除/移动 回调…

redux-devtools安装 以及redux No store found. Make sure to follow the instructions.解决

插件链接 链接&#xff1a;https://pan.baidu.com/s/17ggewOjfpR_fRWhvHHUxug 提取码&#xff1a;zrch No store found. Make sure to follow the instructions 其实是缺少了 const composeEnhancers window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;原先代码 imp…

Study-VTK:安装vtk(python)

先看下deepin有没有安装python环境、以及版本。 ls /usr/bin/python* -l python2 --version python3 --versiondeepin默认装机是安装python2.7和python3.5的&#xff0c;这两个一般不要动。我之前把python3.5卸载了&#xff0c;deepin商店所有软件无法安装&#xff0c;浏览器输…