Vue源碼分析系列四:Virtual DOM

前言

當咱們操做Dom實際上是一件很是耗性能的事,每一個元素都涵蓋了許多的屬性,由於瀏覽器的標準就把 DOM 設計的很是複雜。而Virtual Dom就是用一個原生的JS對象去描述一個DOM節點,即VNode,因此它比建立一個真實的Dom元素所產生代價要小得多。而咱們主流的框架React和Vue正是採用了這種作法,那咱們來看下如何實現一個簡單的Virtual Dom。完整代碼GitHub。喜歡的話但願點個小星星哦 ^_^~~~node

核心

  1. 用 JavaScript 對象結構表示 DOM 樹的結構;而後用這個樹構建一個真正的 DOM 樹
  2. 當狀態變動的時候,從新構造一棵新的對象樹。而後用新的樹和舊的樹進行比較,記錄兩棵樹差別
  3. 把2所記錄的差別應用到步驟1所構建的真正的DOM樹上,視圖就更新了

構建vDOM

首先咱們須要構建vDom, 用js對象來描述真正的dom tree,構建好了vDom以後就須要將其render到咱們的頁面上了git

// createElement.js

// give some default value.
export default (tagName, {attrs = {}, children = []} = {}) => {
	return {
		tagName,
		attrs,
		children
	}
}



// main.js

import 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 啦!github

而後我咱們看看render 方法,這個方法就是將咱們的 vDom 轉化成真是的 element.算法

// render.js

const 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); // 開始構建真實的dom

let $rootEl = mount($app, document.getElementById('app'));


// 建立 mount.js

export default ($node, $target) => {
	// use $node element replace $target element!
	$target.replaceWith($node);
	return $node;
}
複製代碼

最後你就能夠看到效果了. 是否是很帥 ? O(∩_∩)O哈哈 ~~~~瀏覽器

如今咱們來作一些好玩的事兒。回到 main.js 中,咱們加入以下這段代碼:bash

setInterval(() => {
	count++;
	$rootEl = mount(render(createVApp(count)), $rootEl); // $rootEl 就是整顆real dom
}, 1000)
複製代碼

而後回到咱們的頁面,發現什麼了嗎? 你能夠嘗試在 input 裏面輸入一些東西,而後發現了什麼異常了嗎 ?app

查看源代碼,原來,每隔一秒咱們就刷新了一次頁面。但是咱們只改變了 count ,就重繪一次頁面,未免也誇張了吧,假如咱們填寫一個表單,填的手都要斷了,結果刷新了頁面,你猜會怎麼着? 會不會想砸電腦呢 ? 別急,diff 算法能幫咱們解決這給使人頭疼的問題 !框架

diff

diff 算法的概念我就在這兒就不介紹了,你們能夠在網上搜到不少答案。直接上代碼 !dom

// diff.js

import 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.js
setInterval(() => {
	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 算法了,這裏仍是比較複雜的,須要多加練習反覆琢磨,好了,今天的介紹就到這了,若是喜歡你就點點贊哦 !

相關文章
相關標籤/搜索