一步一步帶你實現virtual dom(二) -- Props和事件


一步一步帶你實現virtual dom(一)
一步一步帶你實現virtual dom(二)--Props和事件

很高興我們可以繼續分享編寫虛擬DOM的知識。這次我們要講解的是產品級的內容,其中包括:設置和DOM一致性、以及事件的處理。

使用Babel

在繼續之前,我們需要彌補前一篇文章中沒有詳細講解的內容。假設有一個沒有任何屬性(props)的節點:

<div></div>

Babel,在處理這個節點的時候會把節點的props屬性設置為“null”,因為它沒有任何的屬性。因此我們會得到這樣的結果:

function h(type, props, ...children) {
	return {type, props: props || {}, children};
}

設置props

設置props非常簡單,記得DOM顯示嗎?我們把props作為簡單的js對象來存儲,所以這樣的標簽:

<ul className="list", style="list-style: none;"></ul>

內存里就會有這樣的對象:

{
	type: 'ul',
	props: {className: 'list', style: 'list-style:none;'}
}

因此每一個props的字段就是一個屬性名,這個字段的值就是屬性值。所以,我們只要把這些值給真正的DOM節點設置了就可以了。我們寫一個方法包裝一個setAttribute()方法:

function setProp($target, name, value) {
	$target.setAttribute(name, value);
}

那么現在我們知道如何設置屬性了(prop)--我們之后可以全部都設置上,只要遍歷prop對象的屬性就可以:

function setProps($target, props) {
	Object.keys(props).forEach(name => {
		setProp($target, name, props[name]);
	})
}

還記得createElement()方法么?我們只需要在真正的DOM節點創建之后調用setProp方法給它設置即可:

function createElement(node) {
	if(typeof node === 'string) {
		return document.createTextNode(node);
	}
	const $el = document.createElement(node.type);
	setProps($el, node.props);
	node.children
		.map(createElement)
		.forEach($el.appendChild.bind($el));
	return $el;
}

但是,這還沒有完。我們忘記了一些小細節。首先,‘class’是js的保留字。所以不能把它用作屬性名稱。我們會使用‘className’:

<nav className="navbar light">
	<ul></ul>
</nav>

但是在真正的DOM里並沒有‘className’,所以我們應該在setProp方法里處理這個問題。

另外一個事情是,設置布爾型的屬性的時候最好使用布爾值:

<input type="checkbox" checked={false} />

在這個例子里,我並不希望這個'checked'屬性值設置在真正的DOM節點上。但是事實上這個值足夠設置DOM節點了,當然這同時還需要給對應的虛擬DOM節點也設置這個值:

function setBooleanProp($target, name, value) {
	if(value) {
		$target.setAttribute(name, value);
		$target.[name] = true;
	} else {
		$target[name] = false;
	}
}

現在我們就來看看如何自定義屬性。這次完全是我們自己的實現,因此后面我們會有不同作用的屬性,並且不是全都要在DOM節點上顯示的。所以要寫一個方法來檢查這個屬性是不是自定義的。現在它是空的,所以我們還沒有任何的自定義屬性:

function isCustomProp(name) {
	return false;
}

下面就是我們完整的setProp()方法,把所有的問題都處理了:

function setProp($target, name, value) {
	if(isCustomProp(name)) {
		return;
	} else if(name === 'className') {
		$target.setAttribute('class', value);
	} else if(typeof value === 'boolean') {
		setBooleanProp($target, name, value);
	} else {
		$target.setAttribute(name, value);
	}
}

現在在JSFiddle里面試試吧.

屬性區分(Diff Props)

現在我們已經可以使用prop來創建元素了,現在要處理的就是如何區分元素的props了。最終要么是設置屬性,要么是刪除它。我們已經有方法可以設置屬性了,現在來寫一個方法來刪除它們吧。事實上這非常簡單:

function removeBooleanProp($target, name) {
	$target.removeAttribute(name);
	$target[name] = false;
}

function removeProp($target, name, value) {
	if(isCustomProp(name)) {
		return;
	} else if(name === 'className') {
		$target.removeAttribute('class');
	} else if(typeof === 'boolean') {
		removeBooleanProp($target, name);
	} else {
		$target.removeAttribute(name);
	}
}

我們再來寫一個updateProp()方法來比較兩個屬性--就的和新的,並根據比較的結果來更新DOM元素的屬性:

  • 在DOM里沒有這個屬性的話,就刪除掉
	new									    old 
<nav></nav>		<nav className='navbar'></nav>
  • 在新的節點里包含了某個屬性,那么就需要在DOM上設置這個屬性
	new																			old
<nav style='background: blue'></nav>   <nav></nav>
  • 某個屬性在新的和舊的節點里都存在,那么我們就需要比較他們的值。如果他們不相等我們就需要根據結果給新的節點設置屬性值了。
	new																					old 
<nav className='navbar default'></nav>   <nav className='navbar'></nav>
  • 在其他情況下,屬性並沒有改變我們什么都不需要做。

下面這個方法就是專門處理prop的:

function updateProp($target, naem, newVal, oldVal) {
	if(!newVal) {
		removeProp($target, name, oldVal);
	} else if(!oldVal || newVal != oldVal) {
		setProp($target, name, newVal);
	}
}

是不是很簡單?但是一個節點會有不止一個屬性--所以我們要寫一個方法可以遍歷全部的屬性,然后調用updateProp()方法來一對一對的處理:

function updateProps($target, newProps, oldProps = {}) {
	const props = Object.assign({}, newProps, oldProps);
	Object.leys(props).forEach(name => {
		updateProp($target, name, newProps[name], oldProps[name]);
	});
}

這里需要注意我們創建的組合對象。它包含了新、舊節點的屬性。因此,在遍歷的時候我們會遇到undefined,不過這沒有關系,我們的方法可以處理這個問題。

最后一件事就是把這個方法放到我們的updateElement()方法里。我們應該放在哪里呢?如果節點本身沒有改變,那么它的子節點呢?這個問題我們也需要處理。所以我們把那個方法放在最后一個if語句塊里。

function updateElement($parent, newNode, oldNode, index=0) {
	if() {
		...
	}	else if(newNode.type) {
		updateProps(
			$parent.childNodes[index],
			newNode.props,
			oldNode.props,
		);

		...
	}
}

接着在這里測試一下吧。

事件

當然一個動態的應用是免不了會有事件的。我們可以使用querySelector()來處理節點,然后用addEventListener()來給節點添加事件的listener。但是,這樣沒啥意思。我們要像React一樣來處理事件。

<button onClick={() => alert('hi')}></button>

這樣看起來就像那么回事兒了。你看到了,我們是用了props來聲明一個事件監聽器的。我們的屬性名都是on開頭的。

function isEventProp(name) {
	return /^on/.test(name);
}

我們來寫一個方法,從屬性里獲取事件名稱。記住事件的名稱都是以on為前綴的。

function extractEventName(name) {
	return name.slice(2).toLowerCase();
}

看起來,如果我們在屬性里聲明了事件,那么我們就需要在setProps()或者updateProps()方法里處理。但是如何處理方法的不同呢?

你不能用相等操作符來比較兩個方法。當然你可以用toString()方法,然后比較兩個方法。但是有個問題,方法里可能會包含native code,這就給比較帶來了問題。

"function () { [native code] }"

當然我們可以使用時間冒泡的方式來處理。我們可以寫我們自己的事件處理管理器,這個管理器會附加到body或者繪制我們節點的容器節點上。因此,我們可以在每次更新的時候添加一次事件處理器,這樣也不會造成多大的資源浪費。

但是,我們不會這么做。因為這樣會增加很多的問題,而且事實上我們的時間處理器不會頻繁的改變。所以,我們只要在創建我們的節點的時候添加一次事件監聽器就可以。那么不會在setProps方法里設置事件屬性。我們自己處理添加事件的問題。怎么實現呢?記得我們的方法可以檢測自定義的屬性嗎?現在它不會是空的了:

function isCustomProp(name) {
	return isEventProp(name);
}

當我們知道了一個真的DOM節點的時候添加事件監聽器,這時屬性對象也非常清晰的。

function addEventListeners($target, props) {
	Object.keys(props).forEach(name => {
		if(isEventProp(name)){
			$target.addEventListener(
				exteactEventName(name),
				props[name]
			);
		}
	}); 
}

把上面的代碼加入到createElement方法里:

function createElement(node) {
	if(typeof node === 'string') {
		return document.createTextNode('node');
	}
	const $el = document.createElement(node.type);
	setProps($el, node.props);
	addEventListeners($el, node.props);
	node.children
		.map(createElement)
		.forEach($el.appendChild.bind($el));
	return $el;
}

再次添加事件

如果你必須要再次添加事件監聽器呢?我們來簡單理解處理一下這個問題。只是這樣的話性能會受到印象。我們會引入一個自定義屬性:forceUpdate。記住,我們怎么檢查節點的更改的:

function changed(node1, node2) {
	return typeof node1 ~== typeof node2 ||
				 typeof node1 === 'string' && node1 !== node2 ||
				 node1.type !== node2.type ||
				 node.props.forceUpdate;
}

如果forceUpdate為true的話,節點就會整個的重新創建並且新的事件監聽器也會被添加進去。整個屬性也不是不應該加到實際的DOM節點的,所以需要處理一下:

function isCustomProp(name) {
	return isEventProp(name) || name === 'forceUpdate';
}

這基本就是全部了。是的,整個解決的方法會影響性能,但是很簡單。

結語

這就基本是全部了。希望你覺得有趣。如果你知道更簡單的解決方法處理事件處理器的不同的方法的話,能分享到評論里就太感謝了。

原文地址:https://medium.com/@deathmood/write-your-virtual-dom-2-props-events-a957608f5c76


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM