React是前端最受歡迎的框架之一,解讀其源碼的文章很是多,可是我想從另外一個角度去解讀React:從零開始實現一個React,從API層面實現React的大部分功能,在這個過程當中去探索爲何有虛擬DOM、diff、爲何setState這樣設計等問題。html
提起React,老是免不了和Vue作一番對比前端
Vue的API設計很是簡潔,可是其實現方式卻讓人感受是「魔法」,開發者雖然能立刻上手,可是爲何能實現功能卻很難說清楚。node
相比之下React的設計哲學很是簡單,雖然常常有須要本身處理各類細節問題,可是卻讓人感受它很是「真實」,能清楚地感受到本身仍然是在寫js。react
在開始以前,咱們有必要搞清楚一些概念。git
咱們來看一下這樣一段代碼:es6
const title = <h1 className="title">Hello, world!</h1>;
複製代碼
這段代碼並非合法的js代碼,它是一種被稱爲jsx的語法擴展,經過它咱們就能夠很方便的在js代碼中書寫html片斷。github
本質上,jsx是語法糖,上面這段代碼會被babel轉換成以下代碼npm
const title = React.createElement(
'h1',
{ className: 'title' },
'Hello, world!'
);
複製代碼
你能夠在babel官網提供的在線轉譯測試jsx轉換後的代碼,這裏有一個稍微複雜一點的例子json
爲了集中精力編寫邏輯,在代碼打包工具上選擇了最近火熱的零配置打包工具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
將它跑起來了,固然,如今它還什麼都沒有。
前文提到,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(
<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,從API層面實現React的大部分功能,在這個過程當中去探索爲何有虛擬DOM、diff、爲何setState這樣設計等問題。
整個系列大概會有六篇左右,我每週會更新一到兩篇,我會第一時間在github上更新,有問題須要探討也請在github上回復我~
博客地址: github.com/hujiulong/b… 關注點star,訂閱點watch