您现在的位置是:首页 >学无止境 >Vue源码解析--更新中网站首页学无止境

Vue源码解析--更新中

宁然也 2024-10-04 00:01:04
简介Vue源码解析--更新中

【尚硅谷】Vue源码解析之虚拟DOM和diff算法

【Vue源码】图解 diff算法 与 虚拟DOM-snabbdom-最小量更新原理解析-手写源码-updateChildren]

2. snabbdom 简介 及 准备工作

2.1 简介

snabbdom(瑞典语,“速度”)是著名的虚拟DOM库,是diff算法的鼻祖
Vue源码借鉴了snabbdom
源码使用TypeScript写的https://github.com/snabbdom/snabbdom
从npm下载的是build出来的JavaScript版本
-D 是开发dev版本的依赖 -S是项目真正依赖

2.2 搭建初始环境

1. 安装snabbdom

npm init
npm install -D snabbdom

下载好的源码在node_modules
在这里插入图片描述
需要读取一些init等文件,使用webpack5,不能是webpack4(读取的地址不对)。接下来安装webpack5

2. 安装webpack5并配置

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

配置webpack5
根目录下创建webpack.config.js

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

“dev”: “webpack-dev-server” npm run dev实际跑的是npm run webpack-dev-server会读取webpack.config.js文件
在这里插入图片描述
webpack.config.js配置文件


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

3. 复制官方demo Example

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>

npm run dev跑起来
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

3. h函数的介绍与使用

3.1 介绍

diff算法发生在 虚拟DOM上,新旧虚拟DOM的比较
h 函数产生虚拟节点
在这里插入图片描述
在这里插入图片描述
虚拟节点vnode的属性

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

3.2 使用h函数 创建虚拟节点

3.3 使用patch函数 将虚拟节点上DOM树

src/index.js

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
  } from "snabbdom";
  

  
var myVnode1 = h('a', {
    props: {
        href: "http://www.baidu.com",
        target: '_blank',
      }
}, '百度')
// const myVnode2 = h('div', '我是盒子' )
const myVnode3 = h('div', {class:{'box': true}},  '我是盒子')

console.log(myVnode1)
//   将虚拟节点渲染成真实节点 需要使用patch函数
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");
patch(container, myVnode1)

在这里插入图片描述
console.log(myVnode1)的输出
在这里插入图片描述

3.4 h函数嵌套使用,得到虚拟DOM树(重要)

在这里插入图片描述

const myVnode4 = h('ul', [
    h('li', '苹果1'),
    h('li', '苹果2'),
    h('li', '苹果3'),
    h('li', '苹果4'),
])
console.log(myVnode1)
//   将虚拟节点渲染成真实节点 需要使用patch函数
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");
patch(container, myVnode4)

在这里插入图片描述

const myVnode4 = h('ul', [
    h('li', '苹果1'),
    h('li', [
        h('div', [
            h('p', "香蕉皮"),
            h('p', "苹果皮")
        ])
    ]),
    h('li', h('span', '西瓜')),
    h('li', '番茄'),
])

在这里插入图片描述

4. 手写h函数

参考ts版本的h函数,手写Js版本
在这里插入图片描述
看h函数源码,h最后调用vnode函数
在这里插入图片描述
h函数有很多形式,下面只是部分的形式。我们将写三个参数的h函数,进行基本的学习,参数1:标签 参数2 {} 参数三[]或者文字
在这里插入图片描述
在自己的src下创建如下文件,参考源码写h函数
在这里插入图片描述

源码中vnode函数是将接收的参数整合成一个对象,返回。我们自己手写vnode也是如此
在这里插入图片描述
src/mysnabbdom/vnode.js


export default function (sel, data, children, text, elm) {
    // sel, data, children, text, elm 参考function vnode
    return {sel, data, children, text, elm}
}

h函数源码中,对传入的参数进行判断,最后调用vnode函数
在这里插入图片描述
srcmysnabbdomh.js

import vnode from "./vnode";

// 编写一个低配版的h函数,这个函数必须接受3个参数,缺一不可
/*

h('div', {}, '文字')
h('div', {}, [])
h('div', {}, h())
*/
export default function (sel, data, c) {
    // 检查参数个数
    if (arguments.length != 3) {
        throw new Error("低配版的h函数必须三个参数")
    }
    // 检查参数c的类型
    if (typeof c == 'string' || typeof c == 'number') {
        //调用这种版本 h('div', {}, '文字')
        return vnode(sel, data,undefined, c, undefined)
    } else if (Array.isArray(c)) {
        let children = []
        // h('div', {}, []) ,[]内部是h函数,h函数返回的是对象,需要对数组每一项判断
        for (let i = 0; i < c.length; ++i) {
            if (!(typeof c[i] == "object" && c[i].hasOwnProperty('sel'))) {
                throw new Error("传入的数组参数中有项不是h函数")
            }
            // 这里需不要执行c[i]因为c[i]已经执行过了,在[h()]数组内已经执行了,此时的c[i]是执行后的结果
            // 收集children
            children.push(c[i])
        }
        // 循环结束,children收集完毕
        return vnode(sel,  data, children, undefined, undefined)


    } else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
        // h('div', {}, h()) 内部的h返回的是对象并且有sel属性
        let children = [c]
        return vnode(sel,data,children, undefined, undefined)

        
    } else {
        throw new Error("传入的第三个参数不正确")
    }
}

在这里插入图片描述
srcindex.js调用自己得h函数,其他函数调用官方的,进行渲染

// 导入自己的h函数
import h from './mysnabbdom/h'
import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
  } from "snabbdom";
  

let b = h('li', { }, [
    h('li', {  }, "xx1"),
    h('div', { }, "xx2")

])
//   将虚拟节点渲染成真实节点 需要使用patch函数
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");

patch(container, b)

在这里插入图片描述

5. 手写diff算法准备

5.1 diff算法原理

最小量更新,key很关键。key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。
问题: 如何定义是同一个虚拟节点
答:选择器相同且key相同

只进行同层比较,不会进行跨层比较。即使是同一片 虚拟节点,但是跨层了,diff就是暴力删除旧的,然后插入新的
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

// 导入自己的h函数
import h from './mysnabbdom/h'
import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
} from "snabbdom";


let vnode1 = h('ul', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D'),

])
let vnode2 = h('li', {}, [
    h('session', { key: 'SS' },
        [
            h('li', { key: 'A' }, 'A'),
            h('li', { key: 'B' }, 'B'),
            h('li', { key: 'C' }, 'C'),
            h('li', { key: 'D' }, 'D'),
            h('li', { key: 'E' }, 'E'),
        ])

])
//   将虚拟节点渲染成真实节点 需要使用patch函数
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");
patch(container, vnode1)
const btn = document.getElementById("btn");
btn.addEventListener("click", function () {
    // 点击按钮,将vnode1变成vnode2
    // patch 意思是修补 ,对虚拟节点vnode1、vnode2对比执行最小量更新
    patch(vnode1, vnode2)
})

更改顺序时,我的是部分更新,老师的是全部都没有更新
在这里插入图片描述

5.2 手写diff预备

在这里插入图片描述

5.2.1 源码中如何定义“同一个节点”

最新的版本是比较:选择器sel相同, key相同, data相同, text相同
老版本的比较: 选择器sel相同, key相同
在这里插入图片描述

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

6. 手写diff——首次上DOM树patch(container, myVnode1)

6.0 DOM 预备知识

6.0.1 Node.insertBefore()

var insertNode = parentNode.insertBefore(newNode, referenceNode);

insertedNode :被插入节点(newNode)
parentNode :新插入节点的父节点
newNode :用于插入的节点
referenceNode :newNode 将要插在这个节点之前
在当前节点下增加一个子节点 Node,并使该子节点位于参考节点的前面。

6.0.2 Node.appendChild()

element.appendChild(aChild)

将一个节点附加到指定父节点的子节点列表的末尾处。
如果将被插入的节点已经存在于当前文档的文档树中,那么 appendChild() 只会将它从原先的位置移动到新的位置(不需要事先移除要移动的节点)。

6.0.3 Element.tagName

返回当前元素的标签名

elementName = element.tagName

elementName 是一个字符串,包含了element元素的标签名.
在HTML文档中, tagName会返回其大写形式

6.0.4 Node.removeChild

从DOM中删除一个子节点。返回删除的节点

let oldChild = node.removeChild(child);
//OR
element.removeChild(child);

child 是要移除的那个子节点.
node 是child的父节点.
oldChild保存对删除的子节点的引用. oldChild === child.

6.0.5 document.createElement

var element = document.createElement(tagName[, options]);

tagName:指定要创建元素类型的字符串, 创建元素时的 nodeName 使用 tagName 的值为初始化,该方法不允许使用限定名称(如:“html:a”),在 HTML 文档上调用 createElement() 方法创建元素之前会将tagName 转化成小写,在 Firefox、Opera 和 Chrome 内核中,createElement(null) 等同于 createElement(“null”)
返回 新建的元素(Element)

6.1 patch.js


风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。