本質上來說JSX是React.createElement(component, props, ...children)方法的語法糖。
所以我們如果使用了JSX,我們其實就是在使用React,所以我們就需要引入React
前言
React是前端最受歡迎的框架之一,解讀其源碼的文章非常多,但是我想從另一個角度去解讀React:從零開始實現一個React,從API層面實現React的大部分功能,在這個過程中去探索為什么有虛擬DOM、diff、為什么setState這樣設計等問題。
提起React,總是免不了和Vue做一番對比
Vue的API設計非常簡潔,但是其實現方式卻讓人感覺是“魔法”,開發者雖然能馬上上手,但其原理卻很難說清楚。
相比之下React的設計哲學非常簡單,雖然有很多需要自己處理的細節問題,但它沒有引入任何新的概念,相對更加的干凈和簡單。
關於jsx
在開始之前,我們有必要搞清楚一些概念。
我們來看一下這樣一段代碼:
const title = <h1 className="title">Hello, world!</h1>;
這段代碼並不是合法的js代碼,它是一種被稱為jsx的語法擴展,通過它我們就可以很方便的在js代碼中書寫html片段。
本質上,jsx是語法糖,上面這段代碼會被babel轉換成如下代碼:
const title = React.createElement(
'h1',
{ className: 'title' },
'Hello, world!'
);
React.createElement和虛擬DOM
前文提到,jsx片段會被轉譯成用React.createElement方法包裹的代碼。所以第一步,我們來實現這個React.createElement方法
從jsx轉譯結果來看,createElement方法的參數是這樣:
createElement( tag, attrs, child1, child2, child3 );
第一個參數是DOM節點的標簽名,它的值可能是div,h1,span等等
第二個參數是一個對象,里面包含了所有的屬性,可能包含了className,id等等
從第三個參數開始,就是它的子節點
我們對createElement的實現非常簡單,只需要返回一個對象來保存它的信息就行了。
function createElement( tag, attrs, ...children ) {
return {
tag,
attrs,
children
}
}
函數的參數 ...children使用了ES6的rest參數,它的作用是將后面child1,child2等參數合並成一個數組children。
現在我們來試試調用它
// 將上文定義的createElement方法放到對象React中
const React = {
createElement
}
const element = (
<div>
hello<span>world!</span>
</div>
);
console.log( element );
打開調試工具,我們可以看到輸出的對象和我們預想的一致
我們的createElement方法返回的對象記錄了這個DOM節點所有的信息,換言之,通過它我們就可以生成真正的DOM,這個記錄信息的對象我們稱之為虛擬DOM。
ReactDOM.render
接下來是ReactDOM.render方法,我們再來看這段代碼
ReactDOM.render(
<h1>Hello, world!</h1>
document.getElementById('root')
);
以上經過轉換,這段代碼變成了這樣
ReactDOM.render(
React.createElement( 'h1', null, 'Hello, world!' ),
document.getElementById('root')
)
```
所以render的第一個參數實際上接受的是createElement返回的對象,也就是虛擬DOM
而第二個參數則是掛載的目標DOM
總而言之,render方法的作用就是將虛擬DOM渲染成真實的DOM,下面是它的實現:
```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 );
}
}
}
這里其實還有個小問題:當多次調用render函數時,不會清除原來的內容。所以我們將其附加到ReactDOM對象上時,先清除一下掛載目標DOM的內容:
const ReactDOM = {
render: ( vnode, container ) => {
container.innerHTML = '';
return render( vnode, container );
}
}
渲染和更新
到這里我們已經實現了React最為基礎的功能,可以用它來做一些事了。
我們先在index.html中添加一個根節點
我們先來試試官方文檔中的Hello,World
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);```
運行以后 效果基本就出來了