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/...
注意一下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上回復我~
博客地址: https://github.com/hujiulong/blog
關注點star,訂閱點watch