Vue源码:虚拟DOM和diff算法,10天拿到字节跳动前端岗位offer

  • 跑通snabbdom官方git首页的demo程序,即证明调试环境已经搭建成功

  • 不要忘记在index.html中放置一个div#container

虚拟DOM和h函数

====================================================================

在这里插入图片描述

虚拟DOM类似于mustache中的token

diff是发生在虚拟DOM上的


在这里插入图片描述

课程不研究DOM如何变成虚拟DOM

在这里插入图片描述

研究内容

===============================================================

  • 虚拟函数如何被渲染函数(h函数)产生?

手写h函数

  • diff算法原理?

手写diff算法

  • 虚拟DOM如何通过diff变为真正的DOM的

事实上,虚拟DOM变回真正的DOM是涵盖在diff算法里面的

虚拟函数如何被渲染函数(h函数)产生?

==============================================================================

h函数用来产生虚拟节点


h函数用来产生虚拟节点(vnode)

比如这样会调用h函数

在这里插入图片描述

将得到这样的虚拟节点

在这里插入图片描述

{

“sel”: “a”,

“data”: {

props: {

href: “http://www.atguigu.com”

}

},

“text”: “尚硅谷”

}

它表示的真正DOM节点

在这里插入图片描述

一个虚拟节点都有哪些属性


{

children: undefined, // 子元素

data: {}, // 属性、样式

elm: undefined, // 对应真正DOM节点,如果为undefined,表示该节点还未上树

key: undefined, // 节点唯一标识

sel: “div”, // 选择器

text: “我是一个盒子” // 文字

}

h函数可以嵌套使用,从而得到虚拟DOM树(🧨)


比如这样嵌套使用h函数

在这里插入图片描述

将得到这样的虚拟DOM树

在这里插入图片描述

h函数用法很活


例如

在这里插入图片描述

手写h函数


vnode

vnode.js

/**

  • vnode函数的功能非常简单,就是把传入的5个参数组合对象返回

*/

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

return {

sel, data, children, text, elm

};

}

h函数

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函数必须传入3个参数,我们是低配版h函数’);

// 检查参数c的类型

if (typeof c === ‘string’ || typeof c === ‘number’) {

return vnode(sel, data, undefined, c, undefined);

} else if (Array.isArray©) {

// 说明现在调用h函数是形态②

let children = [];

// 遍历c,手机children

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

children.push(c[i]);

}

// 循环结束了,说明children收集完毕了,此时可以返回虚拟节点,有children节点

return vnode(sel, data, children, undefined, undefined);

} else if (typeof c === ‘object’ && c.hasOwnProperty(‘sel’)) {

// 说明现在调用h函数是形态③

// 即传入的c是唯一的children. 不用执行c,因为测试语句中已经执行了c

return vnode(sel, data, [c], undefined, undefined);

} else {

throw new Error(‘传入的参数类型有误’);

}

};

看TS代码,写JS代码


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

  • 只要主干功能,放弃实现一些细节

感受diff算法

===================================================================

通过更改li标签内容得知,diff算法为最小量更新

通过key可以唯一标识节点,服务于最小量更新

import {init} from ‘snabbdom/init’;

import {classModule} from “snabbdom/modules/class”;

import {propsModule} from “snabbdom/modules/props”;

import {styleModule} from “snabbdom/modules/style”;

import {eventListenersModule} from “snabbdom/modules/eventlisteners”;

import {h} from ‘snabbdom/h’;

// 创建出patch函数

const patch = init([classModule, propsModule, styleModule, eventListenersModule]);

// 得到盒子和按钮

const container = document.getElementById(‘container’);

const btn = document.getElementById(‘btn’);

// 创建虚拟节点

const vnode1 = h(‘ul’, {}, [

h(‘li’, {key: ‘A’}, ‘A’),

h(‘li’, {key: ‘B’}, ‘B’),

h(‘li’, {key: ‘C’}, ‘C’),

h(‘li’, {key: ‘D’}, ‘D’)

])

patch(container, vnode1)

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’),

])

// 点击按钮时,将vnode1变为vnode2

btn.onclick = () => {

patch(vnode1, vnode2)

}

心得


  • 最小量更新非常厉害!真的是最小量更新,当然,key很重要

key是这个节点的唯一标识,告诉diff算法,在更改前后它们是同一个DOM节点。

  • 只有是同一个虚拟节点,才能进行精细化比较。,否则就是暴力删除旧的、插入新的。

延伸问题:如何定义是同一个虚拟节点?答:选择器相同且key 相同。

  • 只进行同层比较,不会进行跨层比较。。即使是同一片虚拟节点,但是跨层了,对不起,精细化比较不diff 你,而是暴力删除旧的、然后插入新的。

diff 并不是那么的“无微不至”啊!真的影响效率么??

答:上面2、3操作在实际Vue 开发中,基本不会遇见,所以这是合理的优化机制。

同层比较示意图


在这里插入图片描述

// 创建虚拟节点

const vnode1 = h(‘ul’, {}, [

h(‘li’, {key: ‘A’}, ‘A’),

h(‘li’, {key: ‘B’}, ‘B’),

h(‘li’, {key: ‘C’}, ‘C’),

h(‘li’, {key: ‘D’}, ‘D’)

])

patch(container, vnode1)

const vnode2 = h(‘ul’, {}, h(‘section’, {}, [

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相同

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

创建节点时,所有子节点都需要递归创建


在这里插入图片描述

手写第一次上树时


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(), {}, [], undefined, oldVnode);

}

// 判断oldVnode和newVnode是不是同一个节点

if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {

// 是同一个节点

// TODO:精细化比较

} else {

// 不是同一个节点

createElement(newVnode, oldVnode.elm);

}

};

createElement.js

// 真正创建节点。将vnode创建为DOM,插入到pivot元素之前

export default function (vnode, pivot) {

// 目的是把虚拟节点vnode插入到标杆pivot之前

// 创建一个DOM节点,这个节点目前还是孤儿节点

let domNode = document.createElement(vnode.sel);

// 有子节点还是文本

if (vnode.text !== “” && (vnode.children === undefined || vnode.children.length === 0)) {

// 内部是文字

domNode.innerText = vnode.text;

// 将孤儿节点上树。让标杆节点的父元素调用insertBefore方法,将新的孤儿节点插入到标签节点之前

pivot.parentNode.insertBefore(domNode, pivot);

} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {

}

};

手写递归创建子节点


为了适应递归操作,将插入操作放入到patch.js中,而不是在createElement中进行

这里处理的是diff处理新旧节点不是同一个节点的情况,创建新的插入并暴力删除

index.js

import h from ‘./mySnabbdom/h’;

import patch from ‘./mySnabbdom/patch’

const container = document.getElementById(‘container’);

const btn = document.getElementById(‘btn’);

const myVnode1 = h(‘h1’, {}, ‘你好’);

const myVnode2 = h(‘ul’, {}, [

h(‘li’, {}, ‘A’),

h(‘li’, {}, ‘B’),

h(‘li’, {}, [

h(‘div’, {}, [

h(‘ol’, {}, [

h(‘li’, {}, ‘哈哈哈’),

h(‘li’, {}, ‘嘿嘿嘿’),

h(‘li’, {}, ‘呵呵呵’),

])

])

]),

h(‘li’, {}, ‘D’),

])

const myVnode3 = h(‘section’, {}, [

h(‘h1’, {}, ‘我是新的h1’),

h(‘h2’, {}, ‘我是新的h2’),

h(‘h3’, {}, ‘我是新的h3’),

])

patch(container, myVnode2);

btn.onclick = function () {

patch(myVnode2, myVnode3);

}

createElement.js

// 真正创建节点。将vnode创建为DOM,是孤儿节点,不进行插入

export default function createElement(vnode) {

// console.log(目的是把虚拟节点${vnode}变成真正的DOM)

// 创建一个DOM节点,这个节点目前还是孤儿节点

let domNode = document.createElement(vnode.sel);

// 有子节点还是文本

if (vnode.text !== “” && (vnode.children === undefined || vnode.children.length === 0)) {

// 内部是文字

domNode.innerText = vnode.text;

// 补充elm属性

vnode.elm = domNode;

} 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,但是还没有上树,是一个孤儿节点

let chDom = createElement(ch);

// 上树

domNode.appendChild(chDom);

}

}

// 补充elm属性

vnode.elm = domNode;

// 返回elm,elm是一个纯DOM对象

return vnode.elm;

};

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(), {}, [], undefined, oldVnode);

}

// 判断oldVnode和newVnode是不是同一个节点

if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {

// 是同一个节点

// TODO:精细化比较

} else {

// 不是同一个节点

let newVnodeElm = createElement(newVnode);

// 插入到老节点之前

if (oldVnode.elm.parentNode && newVnodeElm)

oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);

// 删除老节点

oldVnode.elm.parentNode.removeChild(oldVnode.elm);

}

};

diff处理新旧节点是同一个节点时候


在这里插入图片描述

手写新旧节点text的不同情况


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(), {}, [], undefined, oldVnode);

}

// 判断oldVnode和newVnode是不是同一个节点

if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {

console.log(‘是同一个节点’)

// 判断新旧node是否是同一个对象

if (oldVnode === newVnode) return;

// 判断newVnode有没有text属性

if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {

// 新vnode有text属性

// console.log(‘新vnode有text属性’)

if (newVnode.text !== oldVnode.text)

// 如果新的虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可,如果老的elm中是children,那么也会立即消失掉

oldVnode.elm.innerText = newVnode.text;

} else {

// 新vnode没有text属性,有children

// console.log(‘新vnode没有text属性’)

// 判断老的有没有children

if (oldVnode.children !== undefined && oldVnode.children.length > 0) {

// 老的有children,此时就是最复杂的情况。就是新老都有children

} else {

// 老的没有children,新的有children

// 清空老的节点的内容

oldVnode.elm.innerHTML = ‘’;

// 遍历newVnode的子节点,创建dom,循环上树

newVnode.children.forEach(node => {

let dom = createElement(node);

oldVnode.elm.appendChild(dom);

})

}

}

} else {

// 不是同一个节点

let newVnodeElm = createElement(newVnode);

// 插入到老节点之前

if (oldVnode.elm.parentNode && newVnodeElm)

oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm);

// 删除老节点

oldVnode.elm.parentNode.removeChild(oldVnode.elm);

}

};

vnode中添加key


如果data中存在key,将key也绑定在vnode上

vnode.js

/**

  • vnode函数的功能非常简单,就是把传入的5个参数组合对象返回

*/

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

const key = data.key;

return {

sel, data, children, text, elm, key

};

}

尝试书写diff更新子节点


patchVnode.js

import createElement from “./createElement”;

export default function patchVnode(oldVnode, newVnode) {

// 判断新旧node是否是同一个对象

if (oldVnode === newVnode) return;

// 判断newVnode有没有text属性

if (newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)) {

// 新vnode有text属性

// console.log(‘新vnode有text属性’)

if (newVnode.text !== oldVnode.text)

// 如果新的虚拟节点中的text和老的虚拟节点的text不同,那么直接让新的text写入老的elm中即可,如果老的elm中是children,那么也会立即消失掉

oldVnode.elm.innerText = newVnode.text;

} else {

// 新vnode没有text属性,有children

// 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) {

let dom = createElement(ch);

ch.elm = dom;

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img
img
img
img

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注:前端)
img

hildren

// 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) {

let dom = createElement(ch);

ch.elm = dom;

小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》送给大家,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-LIkVeaI3-1710897896673)]
[外链图片转存中…(img-xJU5c1dW-1710897896674)]
[外链图片转存中…(img-LhWISq5I-1710897896674)]
[外链图片转存中…(img-sCtW6gXz-1710897896674)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频

如果你觉得这些内容对你有帮助,可以添加下面V无偿领取!(备注:前端)
[外链图片转存中…(img-SBPq0hyl-1710897896675)]

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值