react,想必作為前端開發一定不陌生,組件化以及虛擬dom使得react成為最受歡迎額前端框架之一。我們知道react是基於虛擬dom的,但是什么是虛擬dom呢,其實就是一組js對象,那么我們今天就來認識什么是虛擬dom,以及如何轉成真實的dom結構,完整的 簡易版react 在個人github,實現了diff算法,組件渲染,組件更新,鈎子函數。
一.認識虛擬dom
首先我們看如下代碼
const title = <h1 className="title">Hello, world!</h1>;
這並不是合法的js代碼,它是一種被稱為jsx的語法擴展,通過它我們就可以很方便的在js代碼中書寫html片段。
本質上,jsx是語法糖,上面這段代碼會被babel轉換成如下代碼
我們下載插件 babel-plugin-transform-react-jsx,並且配置.babelrc文件
{
"presets": ["env"],
"plugins": [
["transform-react-jsx", {
"pragma": "React.createElement"//大部分框架喜歡改成h
}]
]
}
於是頁面中的jsx就會被babel轉成如下的結構
const title = React.createElement(
'h1',
{ className: 'title' },
'Hello, world!'
);
可以看出babel已經把一個dom元素分解成標簽名稱h1,屬性集合對象,以及內部子節點(這里是hello world文本節點),我們首先修改這個方法,為了轉成我們需要的結構
function createElement( tag, attrs, ...children ) {
return {
tag,
attrs,
children
}
}
// 將上文定義的createElement方法放到對象React中
const React = {
createElement,
}
函數的參數...children使用了ES6的rest參數,它的作用是將后面child1,child2等參數合並成一個數組children。
現在我們來試試調用它,一下結構都是babel自動調用React.createElement給我們轉成的,當然你也可以自己寫方法將真實的dom轉為js對象
const element = (
<div>
hello<span>world!</span>
</div>
);
console.log( element );

二.將上列的虛擬dom結構轉成真實的dom
1.如果遇到文本節點則直接返回新建的文本節點
//處理文本節點
if( typeof vnode === 'string'){
const textNode = document.createTextNode( vnode )
return textNode;
}
2.處理普通的元素
//普通的dom
const dom = document.createElement( vnode.tag );
if( vnode.attrs ){
Object.keys( vnode.attrs ).forEach( key => {
const value = vnode.attrs[ key ];
setAttribute( dom, key, value ); // 設置屬性
} );
}
vnode.children.forEach( child => render( child, dom ) ); // 遞歸渲染子節點
return dom ; // 返回虛擬dom為真正的DOM
3.遇到普通元素的屬性,需要這是屬性節點,但是分為兩種,一種是普通的屬性,比如className,另一種是方法綁定,比如是onClick
function setAttribute( dom, name, value ) {
// 如果屬性名是className,則改回class
if ( name === 'className' ) name = 'class';
// 如果屬性名是onXXX,則是一個事件監聽方法
if ( /on\w+/.test( name ) ) {
name = name.toLowerCase();
dom[ name ] = value || '';
// 如果屬性名是style,則更新style對象
} else if ( name === 'style' ) {
if ( !value || typeof value === 'string' ) {
dom.style.cssText = value || '';
} else if ( value && typeof value === 'object' ) {
for ( let name in value ) {
// 可以通過style={ width: 20 }這種形式來設置樣式,可以省略掉單位px
dom.style[ name ] = typeof value[ name ] === 'number' ? value[ name ] + 'px' : value[ name ];
}
}
// 普通屬性則直接更新屬性
} else {
if ( name in dom ) {
dom[ name ] = value || '';
}
if ( value ) {
dom.setAttribute( name, value );
} else {
dom.removeAttribute( name, value );
}
}
}
三.查看完整的代碼
function render ( vnode, container ){
return container.appendChild( _render( vnode ) );
}
function _render( vnode ){
if ( typeof vnode === 'number' ) {
vnode = String( vnode );
}
//處理文本節點
if( typeof vnode === 'string'){
const textNode = document.createTextNode( vnode )
return textNode;
}
//處理組件
if ( typeof vnode.tag === 'function' ) {
const component = createComponent( vnode.tag, vnode.attrs );
setComponentProps( component, vnode.attrs );
return component.base;
}
//普通的dom
const dom = document.createElement( vnode.tag );
if( vnode.attrs ){
Object.keys( vnode.attrs ).forEach( key => {
const value = vnode.attrs[ key ];
setAttribute( dom, key, value ); // 設置屬性
} );
}
vnode.children.forEach( child => render( child, dom ) ); // 遞歸渲染子節點
return dom ; // 返回虛擬dom為真正的DOM
}
//實現dom掛載到頁面某個元素
const ReactDOM = {
render: ( vnode, container ) => {
container.innerHTML = '';
return render( vnode, container );
}
}
現在我們已經實現將虛擬dom轉為真實的dom,已經綁定屬性,我們現在來像react一樣調用這個方法
const element = (
<div>
hello<span>world!</span>
</div>
);
ReactDOM.render(
element,
document.getElementById( 'main' )
);
現在就實現往頁面中元素id為main的元素上掛載了該element。
