博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Vue源码分析系列四:Virtual DOM
阅读量:6451 次
发布时间:2019-06-23

本文共 6105 字,大约阅读时间需要 20 分钟。

前言

当我们操作Dom其实是一件非常耗性能的事,每个元素都涵盖了许多的属性,因为浏览器的标准就把 DOM 设计的非常复杂。而Virtual Dom就是用一个原生的JS对象去描述一个DOM节点,即VNode,所以它比创建一个真实的Dom元素所产生代价要小得多。而我们主流的框架React和Vue正是采用了这种做法,那我们来看下如何实现一个简单的Virtual Dom。完整代码。喜欢的话希望点个小星星哦 ^_^~~~

核心

  1. 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树
  2. 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
  3. 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了

构建vDOM

首先我们需要构建vDom, 用js对象来描述真正的dom tree,构建好了vDom之后就需要将其render到我们的页面上了

// createElement.js// give some default value.export default (tagName, {attrs = {}, children = []} = {}) => {	return {		tagName,		attrs,		children	}}// main.jsimport createElement from './vdom/createElement'const createVApp = (count) => createElement('div', {	attrs: {		id: 'app',		dataCount: count	},	children: [		createElement('input'), // dom重绘使得Input失焦		String(count), // 文本节点		createElement('img', {			attrs: {				src: 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1555610261877&di=6619e67b4f45768a359a296c55ec1cc3&imgtype=0&src=http%3A%2F%2Fimg.bimg.126.net%2Fphoto%2Fmr7DezX-Q4GLNBM_VPVaWA%3D%3D%2F333829322379622331.jpg'			}		})	]})let count = 0;let vApp = createVApp(count);复制代码

下面这个就是构建的 vDom 啦!

然后我我们看看render 方法,这个方法就是将我们的 vDom 转化成真是的 element.

// render.jsconst renderElem = ({ tagName, attrs, children}) => {	// create root element	let $el = document.createElement(tagName);		// set attributeds	for (const [k, v] of Object.entries(attrs)) {		$el.setAttribute(k, v);	}		// set children (Array)	for (const child of children) {		const $child = render(child);		$el.appendChild($child);	}	return $el;}const render = (vNode) => {	// if element node is text, and createTextNode	if (typeof vNode === 'string') {		return document.createTextNode(vNode);	}	// otherwise return renderElem	return renderElem(vNode);}export default render复制代码

然后我们回到main.js中

// 引入 render.js 模块const $app  = render(vApp); // 开始构建真实的domlet $rootEl = mount($app, document.getElementById('app'));// 创建 mount.jsexport default ($node, $target) => {	// use $node element replace $target element!	$target.replaceWith($node);	return $node;}复制代码

最后你就可以看到效果了. 是不是很帅 ? O(∩_∩)O哈哈 ~~~~

现在我们来做一些好玩的事儿。回到 main.js 中,我们加入如下这段代码:

setInterval(() => {	count++;	$rootEl = mount(render(createVApp(count)), $rootEl); // $rootEl 就是整颗real dom}, 1000)复制代码

然后回到我们的页面,发现什么了吗? 你可以尝试在 input 里面输入一些东西,然后发现了什么异常了吗 ?

查看源代码,原来,每隔一秒我们就刷新了一次页面。可是我们只改变了 count ,就重绘一次页面,未免也夸张了吧,假如我们填写一个表单,填的手都要断了,结果刷新了页面,你猜会怎么着? 会不会想砸电脑呢 ? 别急,diff 算法能帮我们解决这给令人头疼的问题 !

diff

diff 算法的概念我就在这儿就不介绍了,大家可以在网上搜到很多答案。直接上代码 !

// diff.jsimport render from './render'const zip = (xs, ys) => {	const zipped = [];	for (let i = 0; i < Math.min(xs.length, ys.length); i++) {		zipped.push([xs[i], ys[i]]);	}	return zipped;};const diffAttributes = (oldAttrs, newAttrs) => {	const patches = [];	// set new attributes	// oldAttrs = {dataCount: 0, id: 'app'}	// newAttrs = {dataCount: 1, id: 'app'}	// Object.entries(newAttrs) => [['dataCount', 1], ['id', 'app']]	for(const [k, v] of Object.entries(newAttrs)) {		patches.push($node => {			$node.setAttribute(k, v);			return $node;		})	}	// remove old attribute	for(const k in oldAttrs) {		if (!(k in newAttrs)) {			// $node 是整颗真实的 dom tree			patches.push($node => {				$node.removeAttribute(k);				return $node;			})			}	}	return $node => {		for (const patch of patches) {			patch($node);		}	}}const diffChildren = (oldVChildren, newVChildren) => {	const childPatches = [];	for (const [oldVChild, newVChild] of zip(oldVChildren, newVChildren)) {		childPatches.push(diff(oldVChild, newVChild));	}	const additionalPatches = [];	for (const additionalVChild of additionalPatches.slice(oldVChildren.length)) {		additionalPatches.push($node => {			$node.appendChild(render(additionalVChild));			return $node;		})	}	return $parent => {		for (const [patch, child] of zip(childPatches, $parent.childNodes)) {			patch(child);		}		for (const patch of additionalPatches) {			patch($parent);		}		return $parent;	}}const diff = (vOldNode, vNewNode) => {	// remove all	if (vNewNode === 'undefined') {		return $node => {			// Node.remove() 方法,把对象从它所属的DOM树中删除。			$node.remove();			return undefined;		};	}	// when element is textnode (like count)	if (typeof vOldNode === 'string' || typeof vNewNode === 'string') {		if (vOldNode !== vNewNode) {			return $node => {				const $newNode = render(vNewNode);				$node.replaceWith($newNode);				return $newNode;			};		} else {			return $node => undefined;		}	}	if (vOldNode.tagName !== vNewNode.tagName) {		return $node => {			const $newNode = render(vNewNode);			$node.replaceWith($newNode);			return $newNode;		};	}	const patchAttrs = diffAttributes(vOldNode.attrs, vNewNode.attrs);	const patchChildren = diffChildren(vOldNode.children, vNewNode.children);	return $node => {		patchAttrs($node);		patchChildren($node);		return $node;	};};export default diff;// main.jssetInterval(() => {	count++;	// 每隔一秒,重绘一次页面,input失焦(缺点)	// $rootEl = mount(render(createVApp(count)), $rootEl)	// 衍生出 diff 算法	const vNewApp = createVApp(count); // 新的 vDom	const patch = diff(vApp, vNewApp); // 对比差异	$rootEl = patch($rootEl);	vApp = vNewApp; // 每一秒之后都有更新,保存起来以供下次比对。}, 1000)复制代码

废话少说,先看效果 (: ~~

可以发现,input 没有情况,也就是说页面没有刷新,setInterval每次将count++, 页面上也只更新了变化了的属性以及文本,这就是diff算法的威力。

分析一波

  • diff

diff 函数接收两个参数,vOldNode 和 vNewNode.

  1. 判断 vNewNode 是不是 undefined,假如整颗树都给删了呢 ? 那就 $node.remove() 移出就好了
  2. 如果只是改了标签名,那好办,直接 render ,然后 replaceWith 就好了。
  3. 如果新老节点是 'string' 类型,那还得判断新老节点是否相等 !
  4. 所有得到的差异结果都扔进 patches 中, 注意,是个函数哦 , 接收的参数就是 $rootEl
  • diffAttributes

比对属性好办,就是拿到新的 vDom 的属性,然后遍历老的 vDom 的属性,判断老的 vDom 的属性是否存在于新的 vDom 中。关键点我将它描述出来

  1. Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组,其排列与使用 for...in 循环遍历该对象时返回的顺序一致(区别在于 for-in 循环也枚举原型链中的属性)
  2. 通过for of 遍历oldAttrs,拿到所有老的 vDom 中的key
  3. 通过 in 操作符 来判断 2 中的 key 是否存在于 newAttrs 中.
  4. 最后返回一个函数,接收 $rootEl,遍历属性对比出来的 patches.每一项是一个函数.
  • diffChildren

最后就是要对比 children 了。

  1. 接收俩参数,oldVChildren 和 newVChildren
  2. 这里最主要的还是 zip 函数了。得到新老节点的 child, 将每个节点的老的节点和新的节点存放到一个数组中,如图:

  1. 然后遍历这个 zipped 数组.继续diff, 并且保存 diff 后的结果
for (const [oldVChild, newVChild] of zip(oldVChildren, newVChildren)) {    childPatches.push(diff(oldVChild, newVChild));}复制代码

结语

Virtual DOM 最核心的部分就是 diff 算法了,这里还是比较复杂的,需要多加练习反复琢磨,好了,今天的介绍就到这了,如果喜欢你就点点赞哦 !

转载地址:http://zmwzo.baihongyu.com/

你可能感兴趣的文章
盘点国内外那些有野心的BI公司
查看>>
JMeter—断言
查看>>
C++的新类创建:继承与组合
查看>>
m5-第9周作业
查看>>
odoo 权限设置
查看>>
asp操作access提示“无法从指定的数据表中删除”
查看>>
git bash 风格调整
查看>>
997D Cycles in product
查看>>
bzoj4589 Hard Nim
查看>>
java实现pdf旋转_基于Java实现PDF文本旋转倾斜
查看>>
java二维数组内存模型_C++二级指针第二种内存模型(二维数组)
查看>>
java static import 与 import_Java中的import和static import语句之间有什么区别?
查看>>
python time库3.8_python3中datetime库,time库以及pandas中的时间函数区别与详解
查看>>
java 代替Python_Java总是“沉沉浮浮”,替代者会是Python?
查看>>
贪吃蛇java程序简化版_JAVA简版贪吃蛇
查看>>
poi java web_WebPOI JavaWeb 项目 导出excel表格(.xls) Develop 238万源代码下载- www.pudn.com...
查看>>
java 顶点着色_金属顶点着色器绘制纹理点
查看>>
php扩展有哪些G11,php 几个扩展(extension)的安装笔记
查看>>
ajax长连接 php,ajax怎么实现服务器与浏览器长连接
查看>>
oracle报1405,【案例】Oracle报错ORA-15054 asm diskgroup无法mount的解决办法
查看>>