【Vue源码】Vue源码解析之DOM和diff算法(一)

本文是根据尚硅谷前端系列对DOM以及diff算法的讲解而做的笔记,中间也参考了其他的博客

文章目录

  • 一、简介
  • 二、安装snabbdom
  • 三、手写h函数
    • 1、概念
    • 2、真实的h函数在流程中的所处位置
    • 3、手写一个简单的h函数
  • 四、手写一个简单的patch函数,这里便用到了diff算法
    • 1、diff处理新旧节点不是同一个节点的时候
    • 2、创建patch.js文件

一、简介

1、什么是虚拟Dom
就是用javaScript对象描述DOM的层次结构,DOM中的一切属性都在虚拟DOM中有对应的属性。

{ "sel": "a", "data": { props: { href: 'http://www.baidu.com' } }, "text": "百度" }

2、什么是真实Dom

<a href="http://www.baidu.com">百度a>

3、什么是diff
diff主要作用域虚拟DOM,当要进行更新的时候,新旧DOM会进行比较,这个时候就用到了diff算法,当两个DOM的标签名不一样的时候,diff会暴力替换。但是当DOM标签名相同的时候,这个时候就需要精细化比较,算出如何最小量更新,最后反应到真正的DOM上去。

二、安装snabbdom

snabbdom 是著名的虚拟 DOM 库,是 diff 算法的鼻祖,Vue 源码就是借鉴了 snabbdom

安装步骤:

  • 新建文件夹
  • npm init(创建用户标签)
  • npm i -S snabbdom
  • npm i -D webpack@5 webpack-cli@3 webpack-dev-server@3。snabbdom库是DOM库,当然不能在node.js环境运行,所以需要搭建webpack和webpack-dev-server开发环境。还有必须安装webpack@5(这点很重要,不然代码跑不起来。)
  • 创建webpack.config.js文件,里面的具体代码,可以到webpack首页进行复制粘贴。webpack官网
    const path = require('path');
    module.exports = {
        // 入口文件
        entry: './src/index.js',
        // 出口文件
        output: {
            // 虚拟打包路径,就是说文件夹不会真正生成,而是会在8080端口虚拟生成
            publicPath: "xixi",
            // 打包出来的文件名
            filename: 'bundle.js'
        },
        devServer: {
            // 端口号
            port: 8080,
            // 静态资源文件夹
            contentBase: 'www'
        }
    };
    
  • 创建文件夹www,并在其中创建index.html文件,这里要创建一个容器,以便后面可以借助这个容器渲染节点。
  • 将github上的snabbdom官方index.js例子复制下来,放到src/index.js中。这里要把代码里面用到click函数的地方,用空函数替换一下。因为此时www/index.html页面并没有定义这些方法。
  • package.json 文件中新增命令:
    {
      "scripts": {
        "dev": "webpack-dev-server",
      }
    }
    
  • 终端运行npm run dev
  • 此时页面会显示出来一串英文。表示运行成功。

三、手写h函数

1、概念

h函数用来产生虚拟节点(vnode),h函数接收的第一个参数是标签名。
比如说这样调用h函数:

h('a', { props: { href: 'https://www.baidu.com' } }, '百度');

得到的是这样的虚拟节点:

{sel: 'a', data: {props:{href:'https://baidu.com'}}, children: undefined, text: '百度', elm: undefined}

【Vue源码】Vue源码解析之DOM和diff算法(一)_第1张图片
虚拟节点的属性有:

{
	children:undefined//孩子节点
	data:{}//节点上带的属性
	elm:undefined// 表示这个虚拟结点还没有上树,这个一般会放DOM元素
	key:undefined //唯一标识符
	sel:"div"//标签属性
	text:"我是一个盒子"//文本信息
}

2、真实的h函数在流程中的所处位置

在手写h函数之前呢,先看一下它在整体的流程中所处的位置。下面是一个小的Demo,写在了src/index.js里面。

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";
// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
// 创建虚拟节点
var myVnode1 = h('a', { props: { href: 'https://www.baidu.com' } }, '百度');
// const myVnode2 = h('div', {}, '我是一个盒子');
// const myVnode3 = h('ul', [
//     h('li', "西瓜"),
//     h('li', "香蕉"),
//     h('li', "火龙果"),
//     h('li', "桃子")
// ])
//让虚拟节点上树
console.log(myVnode1)
const container = document.getElementById('container');
patch(container, myVnode1);

上树经历了一下几步:

  • 创建patch函数
  • 可以从代码里面看出,第二步用到了h函数创建虚拟节点。
  • 获得页面上的容器,这里用到的是getElementById方法。
  • 然后就是用patch将新建的节点加入进去。

3、手写一个简单的h函数

在src中创建一个文件夹,在此文件夹内创建h.js文件。这里编写的是一个低配版的h函数。这个函数必须要接收3个参数。与真正的h函数相比,只是重载能力弱而已,真正的h函数会考虑多种情况。

  • 形态1:h(‘div’,{},‘文字’)
  • 形态2:h(‘div’,{},[])
  • 形态3:h(‘div’,{},h())

代码如下:

import vnode from './vnode'
export default function (sel, data, c) {
    // vnode(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 = [];
        // for循环值是收集一下获得c的子类就可以了
        for (let i = 0; i < c.length; i++) {
            // 检查c[i]必须是一个对象,如果不满足
            if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel')))
                throw new Error('传入的数组参数中有项不是h函数');
            // 此时只需要收集好就可以了
            children.push(c[i])
        }
        return vnode(sel, data, children, undefined, undefined);
    } else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
        // 说明现在调用h函数的是形态3
        // 此时说明就包含了一个子节点,因为不是数组形式
        let children = [];
        children.push(c)
        return vnode(sel, data, children, undefined, undefined);
    } else {
        throw new Error("传入错误")
    }
}
聪明你的你一定能看懂这个代码的!

vnode.js代码如下:

export default function (sel, data, children, text, elm) {
    return {
        sel, data, children, text, elm
    };
}

vnode返回的的样子是:

{sel: 'a', data: {props:{href:'https://baidu.com'}}, children: undefined, text: '百度', elm: undefined}
这里h.js函数则是考虑到了更多的情况发生,但是返回的数据仍然是这个样子的。

在src/index.js中进行调用:

import h from './mysnabbdom/h.js'
var myVode1 = h('div', {}, [
    h('p', {}, '哈哈'),
    h('p', {}, '嘿嘿'),
    h('p', {}, '嘻嘻'),
    h('p', {}, '吼吼'),
]);
console.log(myVode1);

前台页面输出结果:
【Vue源码】Vue源码解析之DOM和diff算法(一)_第2张图片

四、手写一个简单的patch函数,这里便用到了diff算法

这里要补充
虚拟节点的属性里面有key这个值,这个值太重要了。key是节点的唯一标识符,告诉diff算法,在更改前后他们是否是同一个DOM节点。

举例说明:在src/index.js中写如下代码

import {
    init,
    classModule,
    propsModule,
    styleModule,
    eventListenersModule,
    h,
} from "snabbdom";
// 得到盒子和按钮
const container = document.getElementById('container');
const btn = document.getElementById('btn');
// 创建出patch函数
const patch = init([classModule, propsModule, styleModule, eventListenersModule]);
const vnode1 = h('ul', {}, [
    h('li', {  }, 'A'),
    h('li', {  }, 'B'),
    h('li', {  }, 'C'),
    h('li', {  }, 'D'),
])
patch(container, vnode1)
const 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);
}

【Vue源码】Vue源码解析之DOM和diff算法(一)_第3张图片

最后一个例子的代码进行了一个小改动,就是吧vnode2中的E提到了A的前面。

当给每个li都添加key属性会怎么样呢?

const vnode1 = h('ul', {}, [
    h('li', { key: 'A' }, 'A'),
    h('li', { key: 'B' }, 'B'),
    h('li', { key: 'C' }, 'C'),
    h('li', { key: 'D' }, 'D'),
])
const 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'),

])

【Vue源码】Vue源码解析之DOM和diff算法(一)_第4张图片

  • 只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。延伸问题:如何定义是同一个虚拟节点?答:选择器相同且Key相同。
  • 只进行同层比较,不会进行跨层比较。即使是同一片虚拟节点,但是跨层了,精细化比较不diff。而是暴力删除旧的,插入新的。(跨层是个咋跨层?我尝试在children中添加子节点,但是之前的子节点还是会保存下来,不会重复更新。)

1、diff处理新旧节点不是同一个节点的时候

【Vue源码】Vue源码解析之DOM和diff算法(一)_第5张图片

2、创建patch.js文件

首先要现在src/index.js中调用patch.js

import h from './mysnabbdom/h.js';
import patch from './mysnabbdom/patch'
// const myVnode1 = h('h1', {}, '你好');
const myVnode1 = h('ul', {}, [
    h('li', {}, 'A'),
    h('li', {}, 'B'),
    h('li', {}, 'C'),
    h('li', {}, 'D'),
]);
const container = document.getElementById('container');
// 上树,如何上树呢,将在patch.js中进行编写
patch(container, myVnode1);

在src下创建文件夹,在文件夹下创建patch.js。下面是他的代码:

import vnode from './vnode.js'
import createElement from './createElement'
export default function (oldVnode, newVnode) {
    // 判断传入的第一个参数,是DOM节点还是虚拟节点?
    if (oldVnode.sel == '' || oldVnode.sel == undefined) {
        // 传入的第一个参数是DOM节点,此时要包装为虚拟节点
        oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode);
    }
    // 判断oldVnode和newVnode是不是同一个节点,是同一个节点
    if (oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
        // 那么就是精细比较
    } else {
        // 如果说oldVnode和newVnode不是同一个节点
        let newVnodeDOM = createElement(newVnode)
        // 在老节点的父节点的子节点里面插入新节点
        if (oldVnode.elm.parentNode != undefined && newVnodeDOM != undefined) {
            oldVnode.elm.parentNode.insertBefore(newVnodeDOM, oldVnode.elm)
        }
        console.log(oldVnode);
        // 在老节点的父节点的子节点里面删除老节点
        oldVnode.elm.parentNode.removeChild(oldVnode.elm);
    }
}
  • 判断是DOM节点还是虚拟节点,要是DOM就要用到vnode了,包装它

  • 判断新旧节点是不是同一个节点,刚才说到的判断是否是同一个节点,要比较俩值。

  • 是就进行精细比较

  • 不是就是粗暴的删除,添加

  • 对新节点创建真实DOM节点,放到elm属性里面去。这一步在createElement.js中完成。

    // 真正创建节点,将vnode创建为DOM,插入到pivot这个元素之前
    export default function createElement(vnode) {
        console.log('目的是把虚拟节点', vnode, '真正变为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++) {
                let ch = vnode.children[i];
                let chDOM = createElement(ch);
                domNode.appendChild(chDOM);
            }
        }
        // 补充elm属性
        vnode.elm = domNode;
        // 返回elm,elm属性时一个纯DOM对象
        return vnode.elm;
    }
    
    这一步用到了递归,但是也是for循环遍历递归,就递归了一层,不算难理解。
    
  • 返回创建好的vnode虚拟节点,然后再老节点的父元素的中插入新的节点。

  • 删除旧的节点。

  • 页面就可以展示出来啦。

你可能感兴趣的:(vue系列知识点,vue.js,javascript,webpack)