Vue源码解析

编程入门 行业动态 更新时间:2024-10-11 21:22:45

Vue<a href=https://www.elefans.com/category/jswz/34/1770099.html style=源码解析"/>

Vue源码解析

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

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

文章目录

    • 2. snabbdom 简介 及 准备工作
      • 2.1 简介
      • 2.2 搭建初始环境
        • 1. 安装snabbdom
        • 2. 安装webpack5并配置
        • 3. 复制官方demo Example
    • 3. h函数的介绍与使用
      • 3.1 介绍
      • 3.2 使用h函数 创建虚拟节点
      • 3.3 使用patch函数 将虚拟节点上DOM树
      • 3.4 h函数嵌套使用,得到虚拟DOM树(重要)
    • 4. 手写h函数
    • 5. 手写diff算法准备
      • 5.1 diff算法原理
      • 5.2 手写diff预备
        • 5.2.1 源码中如何定义“同一个节点”
        • 5.2.2 源码中创建子节点,需要递归
    • 6. 手写diff——首次上DOM树patch(container, myVnode1)
      • 6.0 DOM 预备知识
        • 6.0.1 Node.insertBefore()
        • 6.0.2 Node.appendChild()
        • 6.0.3 Element.tagName
        • 6.0.4 Node.removeChild
        • 6.0.5 document.createElement
      • 6.1 patch.js

2. snabbdom 简介 及 准备工作

2.1 简介

snabbdom(瑞典语,“速度”)是著名的虚拟DOM库,是diff算法的鼻祖
Vue源码借鉴了snabbdom
源码使用TypeScript写的
从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-serverdevServer: {// 静态根目录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 modulesclassModule, // makes it easy to toggle classespropsModule, // for setting properties on DOM elementsstyleModule, // handles styling on elements with support for animationseventListenersModule, // 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: {} // 属性、样式、keyelm: 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: "",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 modulesclassModule, // makes it easy to toggle classespropsModule, // for setting properties on DOM elementsstyleModule, // handles styling on elements with support for animationseventListenersModule, // 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 modulesclassModule, // makes it easy to toggle classespropsModule, // for setting properties on DOM elementsstyleModule, // handles styling on elements with support for animationseventListenersModule, // 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 vnodereturn {sel, data, children, text, elm}
}

h函数源码中,对传入的参数进行判断,最后调用vnode函数

src\mysnabbdom\h.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]是执行后的结果// 收集childrenchildren.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("传入的第三个参数不正确")}
}


src\index.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 modulesclassModule, // makes it easy to toggle classespropsModule, // for setting properties on DOM elementsstyleModule, // handles styling on elements with support for animationseventListenersModule, // 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 modulesclassModule, // makes it easy to toggle classespropsModule, // for setting properties on DOM elementsstyleModule, // handles styling on elements with support for animationseventListenersModule, // 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


更多推荐

Vue源码解析

本文发布于:2024-03-08 13:37:39,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1721031.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:源码   Vue

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!