前言
React是前端最受歡迎的框架之一,解讀其原始碼的文章非常多,但是我想從另一個角度去解讀React:從零開始實現一個React,從API層面實現React的大部分功能,在這個過程中去探索為什麼有虛擬DOM、diff、為什麼setState這樣設計等問題。
提起React,總是免不了和Vue做一番對比
Vue的API設計非常簡潔,但是其實現方式卻讓人感覺是“魔法”,開發者雖然能馬上上手,但是為什麼能實現功能卻很難說清楚。
相比之下React的設計哲學非常簡單,雖然經常有需要自己處理各種細節問題,但是卻讓人感覺它非常“真實”,能清楚地感覺到自己仍然是在寫js。
關於jsx
在開始之前,我們有必要搞清楚一些概念。
我們來看一下這樣一段程式碼:
const title = <h1 className="title">Hello, world!</h1>;
複製程式碼
這段程式碼並不是合法的js程式碼,它是一種被稱為jsx的語法擴充套件,通過它我們就可以很方便的在js程式碼中書寫html片段。
本質上,jsx是語法糖,上面這段程式碼會被babel轉換成如下程式碼
const title = React.createElement(
'h1',
{ className: 'title' },
'Hello, world!'
);
複製程式碼
你可以在babel官網提供的線上轉譯測試jsx轉換後的程式碼,這裡有一個稍微複雜一點的例子
準備工作
為了集中精力編寫邏輯,在程式碼打包工具上選擇了最近火熱的零配置打包工具parcel,需要先安裝parcel:
npm install -g parcel-bundler
複製程式碼
接下來新建index.js
和index.html
,在index.html
中引入index.js
。
當然,有一個更簡單的方法,你可以直接下載這個倉庫的程式碼:
https://github.com/hujiulong/simple-react/tree/chapter-1
注意一下babel的配置 .babelrc
{
"presets": ["env"],
"plugins": [
["transform-react-jsx", {
"pragma": "React.createElement"
}]
]
}
複製程式碼
這個transform-react-jsx
就是將jsx轉換成js的babel外掛,它有一個pragma
項,可以定義jsx轉換方法的名稱,你也可以將它改成h
(這是很多類React框架使用的名稱)或別的。
準備工作完成後,我們可以用命令parcel index.html
將它跑起來了,當然,現在它還什麼都沒有。
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 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 => {
if ( key === 'className' ) key = 'class'; // 當屬性名為className時,改回class
dom.setAttribute( key, vnode.attrs[ key ] )
} );
}
vnode.children.forEach( child => render( child, dom ) ); // 遞迴渲染子節點
return container.appendChild( dom ); // 將渲染結果掛載到真正的DOM上
}
複製程式碼
這裡注意React為了避免類名class
和js關鍵字class
衝突,將類名改成了className,在渲染成真實DOM時,需要將其改回。
這裡其實還有個小問題:當多次呼叫render
函式時,不會清除原來的內容。所以我們將其附加到ReactDOM物件上時,先清除一下掛載目標DOM的內容:
const ReactDOM = {
render: ( vnode, container ) => {
container.innerHTML = '';
return render( vnode, container );
}
}
複製程式碼
渲染和更新
到這裡我們已經實現了React最為基礎的功能,可以用它來做一些事了。
我們先在index.html中新增一個根節點
<div id="root"></div>
複製程式碼
我們先來試試官方文件中的Hello,World
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
複製程式碼
可以看到結果:
試試渲染一段動態的程式碼,這個例子也來自官方文件
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
ReactDOM.render(
element,
document.getElementById( 'root' )
);
}
setInterval( tick, 1000 );
複製程式碼
可以看到結果:
後話
這篇文章中,我們實現了React非常基礎的功能,也瞭解了jsx和虛擬DOM,下一篇文章我們將實現非常重要的元件功能。
最後留下一個小問題 在定義React元件或者書寫React相關程式碼,不管程式碼中有沒有用到React這個物件,我們都必須將其import進來,這是為什麼?
例如:
import React from 'react'; // 下面的程式碼沒有用到React物件,為什麼也要將其import進來
import ReactDOM from 'react-dom';
ReactDOM.render( <App />, document.getElementById( 'editor' ) );
複製程式碼
不知道答案的同學再仔細看看這篇文章哦
從零開始實現React系列
React是前端最受歡迎的框架之一,解讀其原始碼的文章非常多,但是我想從另一個角度去解讀React:從零開始實現一個React,從API層面實現React的大部分功能,在這個過程中去探索為什麼有虛擬DOM、diff、為什麼setState這樣設計等問題。
整個系列大概會有六篇左右,我每週會更新一到兩篇,我會第一時間在github上更新,有問題需要探討也請在github上回復我~
部落格地址: github.com/hujiulong/b… 關注點star,訂閱點watch