IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> vue2.x虚拟DOM与DIFF算法 -> 正文阅读

[JavaScript知识库]vue2.x虚拟DOM与DIFF算法

snabbdom

简介

  1. snabbdom:是瑞典语单词,单词原意为“速度”。是著名的虚拟 DOM 库,是 diff 算法的鼻祖,Vue 源码就是借鉴了 snabbdom
  2. 官方 Git:https://github.com/snabbdom/snabbdom

安装

  1. 在 git 上的 snabbdom 源码是用 TypeScript 写的,git 上并不提供编译好的 JavaScript 版本
  2. 如果要直接使用 build 出来的 JavaScript 版的 snabbdom 库,可以从 npm 上下载:npm i -S snabbdom
  3. 安装步骤:新建文件夹study-snabbdom =》在终端打开study-snabbdom文件夹 =》npm初始化:npm init =》一路回车 =》安装snabbdom:npm i -S snabbdom【终端不要关】
  4. 安装好了之后 打开node_modules\snabbdom文件夹 里面的src中放着TS代码 build文件夹中放着JS代码

测试环境搭建

  1. snabbdom 库是 DOM 库,当然不能在 nodejs 环境运行,所以我们需要搭建 webpack 和 webpack-dev-server 开发环境,好消息是不需要安装任何loader
  2. 这里需要注意,必须安装最新版 webpack@5,不能安装 webpack@4,这是因为 webpack@4 没有读取身份证(package.json)中 exports 的能力,建议大家使用这样的版本:
npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3

(在终端继续输入以上代码安装测试环境)

  1. 在study-snabbdom文件夹中新建webpack.config.js文件 参考webpack官网 将以下代码书写到webpack.config.js文件中
// https://webpack.docschina.org/
const path = require('path')

module.exports = {
  // 入口
  entry: './src/index.js',
  // 出口
  output: {
    // 虚拟打包路径,就是说文件夹不会真正生成,而是在 8080 端口虚拟生成,不会真正的物理生成
    publicPath: 'xuni',
    // 打包出来的文件名 不会真正的物理生成 而是虚拟生成在端口里
    filename: 'bundle.js'
  },
  devServer: {
    // 端口号
    port: 8080,
    // 静态资源文件夹
    contentBase: 'www'
  }
}
  1. study-snabbdom文件夹中新建src文件夹 在该文件夹下新建index.js文件
    study-snabbdom文件夹中新建www文件夹 在该文件夹下新建index.html文件
    将package.json文件夹中的"scripts"的内容改为:
scripts": {
	"dev": "webpack-dev-server"
},
  1. 跑通snabbdom官方git首页的demo程序,即证明调试环境已经搭建成功 src/index.js 中的内容改为snabbdom官方git首页的Example中的内容:
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

var container = document.getElementById('container')这行代码表明页面也定要有一个id为container的div 将www/index.html文件内容:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>静态资源文件夹</title>
</head>
<body>
    <div id="container"></div>
<!--    引入打包文件-->
    <script src="xuni/bundle.js"></script>
</body>
</html>
  • 在终端输入:npm run dev进行打包
  • 访问:http://localhost:8080/ 和 http://127.0.0.1:8080/xuni/bundle.js, 可以看到 www/index.html 和 xuni/bundle.js 文件的内容

虚拟 DOM 和 h 函数

虚拟 DOM

  1. 虚拟DOM:用 JavaScript 对象描述 DOM 的层次结构。DOM 中的一切属性都在虚拟 DOM 中有对应的属性。
  • 真实DOM
<div class="box">
  <h3>我是一个标题</h3>
  <ul>
    <li>牛奶</li>
    <li>咖啡</li>
    <li>可乐</li>
  </ul>
</div>

  • 虚拟DOM
{
  "sel": "div",// 标签名放在sel中
  "data": {// 标签的属性放在这里面
    "class": { "box": true }// 标签的class
  },
  "children": [// 该标签下的子元素放在children中
    {
      "sel": "h3",
      "data": {},
      "text": "我是一个标题"// 标签的内容
    },
    {
      "sel": "ul",
      "data": {},
      "children": [
        { "sel": "li", "data": {}, "text": "牛奶" },
        { "sel": "li", "data": {}, "text": "咖啡" },
        { "sel": "li", "data": {}, "text": "可乐" }
      ]
    }
  ]
}
  1. diff 是发生在虚拟 DOM 上的:新虚拟 DOM 和老虚拟 DOM 进行 diff (精细化比较),算出应该如何最小量更新,最后反映到真实的 DOM 上

也就是说 新虚拟 DOM 和老虚拟 DOM 进行 diff算法比较 找出不同的地方 然后对真实的DOM进行局部操作

  • 老虚拟DOM
{
  "sel": "div",
  "data": {
    "class": { "box": true }
  },
  "children": [
    {
      "sel": "h3",
      "data": {},
      "text": "我是一个标题"
    },
    {
      "sel": "ul",
      "data": {},
      "children": [
        { "sel": "li", "data": {}, "text": "牛奶" },
        { "sel": "li", "data": {}, "text": "咖啡" },
        { "sel": "li", "data": {}, "text": "可乐" }
      ]
    }
  ]
}
  • diff 之后的新虚拟DOM
{
  "sel": "div",
  "data": {
    "class": { "box": true }
  },
  "children": [
    {
      "sel": "h3",
      "data": {},
      "text": "我是一个标题"
    },
    {
      "sel": "span",
      "data": {},
      "text": "我是一个新的span"// 新增
    },
    {
      "sel": "ul",
      "data": {},
      "children": [
        { "sel": "li", "data": {}, "text": "牛奶" },
        { "sel": "li", "data": {}, "text": "咖啡" },
        { "sel": "li", "data": {}, "text": "可乐" }{ "sel": "li", "data": {}, "text": "雪碧" }// 新增
      ]
    }
  ]
}
  1. DOM 如何变为虚拟 DOM,属于模板编译原理范畴,本次课不研究

h函数

  1. h 函数用来产生虚拟节点(vnode)
    比如这样调用 h 函数:h('a', { props: { href: 'http://www.atguigu.com' } }, '尚硅谷')
    将得到这样的虚拟节点:{ "sel": "a", "data": { "props": { "href": "http://www.atguigu.com" } }, "text": "尚硅谷" }
    它表示的真正的 DOM 节点:<a href="http://www.atguigu.com">尚硅谷</a>

h函数的第一个参数是字符串类型的 传入标签名字 会被添加到虚拟DOM的sel属性中
第二个参数是个对象 传入标签的属性 属性也是一个对象 第二个参数会被添加到虚拟DOM的data属性中 可省略
第三个参数是字符串 传入标签的内容 会被添加到虚拟DOM的text属性中 也可以是h函数或者h函数构成的数组 传递子元素
在这里插入图片描述

  1. 虚拟节点有哪些属性
{
  children: undefined, // 子元素,undefined表示没有子节点
  data: {}, // 属性样式等
  elm: undefined, // 该元素对应的真正的DOM节点,undefined表示它还没有上树
  key: undefined, // 节点唯一标识
  sel: 'div', // selector选择器 节点类型(现在它是一个div)
  text: '我是一个盒子' // 文字
}

基本使用

将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 myVNode1 = h(
    'a',
    { props: { href: 'http://www.atguigu.com', target: '_blank' } },
    '尚硅谷'
)
const myVNode2 = h('div', { class: { box: true } }, '我是一个盒子')

// 让虚拟节点上树 让虚拟DOM渲染到真实DOM上
// 1.获取容器
const container = document.getElementById('container')
// 让虚拟DOM渲染到真实DOM上:patch(container, myVNode1) 一个容器只能让一个节点上树 下面两行代码只能存在一行
// patch(container, myVNode1)
patch(container, myVNode2)

嵌套使用

  1. h函数用来产生虚拟节点 所以可以嵌套使用 h 函数:
h('ul', {}, [
  h('li', {}, '牛奶'),
  h('li', {}, '咖啡'),
  h('li', {}, '可乐')
])

将得到这样的虚拟 DOM 树:

{
  "sel": "ul",
  "data": {},
  "children": [
    { "sel": "li", "data": {}, "text": "牛奶" },
    { "sel": "li", "data": {}, "text": "咖啡" },
    { "sel": "li", "data": {}, "text": "可乐" }
  ]
}
  1. 第二个参数是标签属性 没有可以省略 第三个参数是标签内容(h函数开头已经展示)或标签子元素 如果元素只有一个子元素 那么不用写数组[] 如果子元素大于1个 需要用数组[]包起来
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 myVNode3 = h('ul', [
  h('li', {}, '苹果'),
  h('li', '西瓜'),
  h('li', [h('div', [h('p', '嘻嘻'), h('p', '哈哈')])]),// 如果子元素大于1个 需要用数组[]包起来
  h('li', h('p', '火龙果'))// 如果只有一个子元素 那么不用写数组[]
])
// 让虚拟节点上树
const container = document.getElementById('container')
patch(container, myVNode3)

h函数源码

import { vnode, VNode, VNodeData } from "./vnode";
import * as is from "./is";

export type VNodes = VNode[];
export type VNodeChildElement = VNode | string | number | undefined | null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>;

function addNS(
  data: any,
  children: VNodes | undefined,
  sel: string | undefined
): void {
  data.ns = "http://www.w3.org/2000/svg";
  if (sel !== "foreignObject" && children !== undefined) {
    for (let i = 0; i < children.length; ++i) {
      const childData = children[i].data;
      if (childData !== undefined) {
        addNS(childData, children[i].children as VNodes, children[i].sel);
      }
    }
  }
}

export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(
  sel: string,
  data: VNodeData | null,
  children: VNodeChildren
): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
  let data: VNodeData = {};
  let children: any;
  let text: any;
  let i: number;
  if (c !== undefined) {
    if (b !== null) {
      data = b;
    }
    if (is.array(c)) {
      children = c;
    } else if (is.primitive(c)) {
      text = c;
    } else if (c && c.sel) {
      children = [c];
    }
  } else if (b !== undefined && b !== null) {
    if (is.array(b)) {
      children = b;
    } else if (is.primitive(b)) {
      text = b;
    } else if (b && b.sel) {
      children = [b];
    } else {
      data = b;
    }
  }
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      if (is.primitive(children[i]))
        children[i] = vnode(
          undefined,
          undefined,
          undefined,
          children[i],
          undefined
        );
    }
  }
  if (
    sel[0] === "s" &&
    sel[1] === "v" &&
    sel[2] === "g" &&
    (sel.length === 3 || sel[3] === "." || sel[3] === "#")
  ) {
    addNS(data, children, sel);
  }
  // 返回的是一个vnode函数
  return vnode(sel, data, children, text, undefined);
}
  1. 返回一个vnode函数创造出一个虚拟节点 vnode函数源码:
export function vnode(
  sel: string | undefined,// 冒号后面是该参数类型
  data: any | undefined,
  children: Array<VNode | string> | undefined,
  text: string | undefined,
  elm: Element | Text | undefined
): VNode {
  const key = data === undefined ? undefined : data.key;
  return { sel, data, children, text, elm, key };
}

vnode函数就是把传入的参数返回成一个对象

手写h函数

  1. 假设h函数必须传入3个参数 本文不考虑h函数参数省略情况
  2. 将原来的src/index.js改名为index-demo.js 新建index.js文件、mysnabbdom文件夹 在mysnabbdom文件夹下新建h.js和vnode.js文件
  3. vnode.js中的代码:
// 函数的功能非常简单,就是把传入的5个参数组合成对象返回
export default function (sel, data, children, text, elm) {
  return {
    sel,
    data,
    children,
    text,
    elm
  }
}
  1. mysnabbdom/h.js中我们自己写的源码:
import vnode from './vnode'
// 编写一个低配版的h函数,这个函数必须接受3个参数,缺一不可
// 相当于它的重置功能较弱
// 也就是说,调用的时候形态必须是下面的三种之一
/*
  形态1:h('div', {}, '文字')
  形态2:h('div', {}, [])
  形态3:h('div', {}, h())
*/
export default function (sel, data, c) {
  // 检查参数的个数
  if (arguments.length !== 3)
    throw new Error('对不起,h函数必须传入3个参数,我们是低配版h函数')
  // 检查参数 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
    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')) {
  	// 虚拟节点一定有sel属性
    // 说明现在调用h函数的是形态3
    // 即,传入的c是唯一的children,不用执行c,因为调用的时候已经执行过了
    let children = [c]
    return vnode(sel, data, children, undefined, undefined)
  } else {
    throw new Error('传入的第三个参数类型不对')
  }
}

5.在src/index.js中:

import h from './mysnabbdom/h'
const myVNode1 = h('div', {}, [
  h('p', {}, '哈哈'),
  h('p', {}, '嘻嘻'),
  h('p', {}, '呵呵'),
  h('p', {}, [h('span', {}, 'aa'), h('span', {}, 'bb')]),
  h('p', {}, h('span', {}, 'A'))
])

const myVNode2 = h('ul', {}, [
  h('li', {}, '牛奶'),
  h('li', {}, '咖啡'),
  h('li', {}, [h('div', {}, [h('p', {}, '可口可乐'), h('p', {}, '百事可乐')])]),
  h('li', {}, h('p', {}, '雪碧'))
])
console.log(myVNode1, myVNode2)

diff 算法

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-08-19 11:57:40  更:2021-08-19 12:00:01 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 9:44:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码