沒有用到React,為什麼我需要import引入React?
本質上來說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')
經過轉換,這段代碼變成了這樣
React.createElement( 'h1', null, 'Hello, world!' ),
是以
render
的第一個參數實際上接受的是createElement傳回的對象,也就是虛拟DOM
而第二個參數則是挂載的目标DOM
總而言之,render方法的作用就是将虛拟DOM渲染成真實的DOM,下面是它的實作:
function render( vnode, container ) {
// 當vnode為字元串時,渲染結果是一段文本
if ( typeof vnode === 'string' ) {
const textNode = document.createTextNode( vnode );
return container.appendChild( textNode );
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 container.appendChild( dom ); // 将渲染結果挂載到真正的DOM上
設定屬性需要考慮一些特殊情況,我們單獨将其拿出來作為一個方法setAttribute
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中添加一個根節點
<div id="root"></div>
我們先來試試官方文檔中的
Hello,World