原文連接: dev.to/ameerthehac…javascript
本篇翻譯已徵得原做者贊成:java
更多文章可戳: github.com/YvetteLau/B…react
譯者注:webpack
本文中的實現藉助了 snabbdom
,所以若是你的關注點是虛擬DOM的實現或是將虛擬DOM渲染到瀏覽器的底層實現,本篇文章中並不會涉及到。有些人可能對此感動失望,可是,一口吃不成一個胖子,咱們須要一步一步來。git
正文:github
我沒法理解我不能創造的東西 —— 費曼web
當我學習 React 的時候,我以爲它所作的一切都是魔術,而後我就開始思考這種魔術到底是什麼。我感到很是驚訝,當我發現 React 所作的一切很是簡單,甚至若是咱們不是下一家大型初創公司增長籌碼,僅須要不多的JS代碼就能夠構建它。這也是促使我寫這篇文章的動力,但願你讀完這篇文章也有相同的感受。shell
再次爲了簡單起見,咱們不會在本文中實現咱們本身的虛擬DOM,咱們將使用 snabbdom
,有趣的是,Vue.js
虛擬DOM借鑑了它,你能夠在這裏讀更多關於 snabbdom
的內容: github.com/snabbdom/sn…npm
有些人可能對此感動失望,可是,一口吃不成一個胖子,咱們須要一步一步來,所以讓咱們首先構建基本的東西,而後再在此基礎上加以補充。我計劃後續文章中在咱們這次構建的內容之上,編寫咱們本身的 React Hooks 以及虛擬DOM,react-native
這是增長任何庫或框架的複雜度的關鍵部分之一,因爲咱們只是出於娛樂目的而作,所以咱們能夠放心地忽略 React
提供的可調試性功能,例如 dev tools
和分析器。
咱們不會過於關注咱們的庫的性能,咱們只想構建能正常運行的庫。讓咱們也不要費力地確保它能夠在市場上的全部瀏覽器上使用,只有可以在某些現代瀏覽器上可使用,那就已經很好了。
在開始以前,咱們須要一個支持ES6,自動熱更新的腳手架。我已經建立了一個很是基礎的 webpack
腳手架,你能夠進行克隆和設置: github.com/ameerthehac…
JSX
是一個開放標準,不只限於 React
,咱們能夠在沒有 React
的狀況下使用它,它比你想象得還有容易。想要了解如何讓咱們的庫支持 JSX
,咱們首先須要看看在咱們使用 JSX
時背後究竟發生了什麼。
const App = (
<div> <h1 className="primary">QndReact is Quick and dirty react</h1> <p>It is about building your own React in 90 lines of JavsScript</p> </div>
);
// 上面的 jsx 被轉換成下面這樣:
/** * React.createElement(type, attributes, children) */
var App = React.createElement(
"div",
null,
React.createElement(
"h1",
{
className: "primary"
},
"QndReact is Quick and dirty react"
),
React.createElement(
"p",
null,
"It is about building your own React in 90 lines of JavsScript"
)
);
複製代碼
正如你看到的,每一個 JSX
元素都經過 @babel/plugin-transform-react-jsx
插件轉換爲了 React.createElement(...)
函數調用的形式,你能夠在這裏使用 JSX
進行更多的轉換
爲了使上述轉換運行正常,在編寫 JSX
時,你須要引入 React
,你就是爲何當你不引入 React
時,編寫 JSX
會出現錯誤的緣由。 @babel/plugin-transform-react-jsx
插件已經添加在了咱們的項目依賴中,下面咱們先安裝一下依賴
npm install
複製代碼
把項目的配置增長到 .babelrc
文件中:
{
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "QndReact.createElement", // default pragma is React.createElement
"throwIfNamespace": false // defaults to true
}
]
]
}
複製代碼
此後,只要 Babel
看到 JSX
,它就會調用 QntReact.createElement(...)
,可是咱們還未定義此函數,如今咱們將其寫到 src/qnd-react.js
中。
const createElement = (type, props = {}, ...children) => {
console.log(type, props, children);
};
// 像 React.createElement 同樣導出
const QndReact = {
createElement
};
export default QndReact;
複製代碼
咱們在控制檯打印出了傳遞給咱們的 type
、 props
、 children
。爲了測試咱們的轉換是否正常,咱們能夠在 src/index.js
中編寫一些 JSX
。
// QndReact 須要被引入
import QndReact from "./qnd-react";
const App = (
<div> <h1 className="primary"> QndReact is Quick and dirty react </h1> <p>It is about building your own React in 90 lines of JavsScript</p> </div>
);
複製代碼
啓動項目: npm start
,在瀏覽器輸入localhost:3000
,如今你的控制檯看起來應該與下圖相似:
根據以上信息,咱們可使用 snabbdom
建立咱們內部的 虛擬DOM節點 ,而後咱們才能將其用於咱們的協調(reconciliation
) 過程,可使用以下的命令安裝 snabbdom:
npm install snabbdom
複製代碼
當 QndReact.createElement(...) 被調用時嗎,建立和返回 虛擬DOM節點。
//src/qnd-react.js
import { h } from 'snabbdom';
const createElement = (type, props = {}, ...children) => {
return h(type, { props }, children);
};
const QndReact = {
createElement
};
export default QndReact;
複製代碼
很好,如今咱們能夠解析 JSX
並建立本身的虛擬DOM節點,可是仍然沒法在瀏覽器中呈現出來。爲此,咱們在 src/qnd-react-dom.js
添加一個 render
方法。
//src/qnd-react-dom.js
//React.render(<App />, document.getElementById('root'));
const render = (el, rootElement) => {
//將el渲染到rootElement的邏輯
}
const QndReactDom = {
render
}
複製代碼
與其咱們本身去處理將元素放到 DOM
上的繁重工做,不如讓 snabbdom
去處理。爲此咱們能夠引入模塊去初始化 snabbdom
。snabbdom
中的模塊能夠看作是插件,能夠支持 snabbdom
作更多的事。
//src/qnd-react-dom.js
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';
const reconcile = snabbdom.init([propsModule]);
const render = (el, rootDomElement) => {
//將el渲染到rootElement
reconcile(rootDomElement, el);
}
const QndReactDom = {
render
}
export default QndReactDom;
複製代碼
咱們使用這個新的 render
函數去 src/index
中去作一些魔法。
//src/index.js
import QndReact from "./qnd-react";
import QndReactDom from './qnd-react-dom';
const App = (
<div> <h1 className="primary"> QndReact is Quick and dirty react </h1> <p>It is about building your own React in 90 lines of JavsScript</p> </div>
);
QndReactDom.render(App, document.getElementById('root'));
複製代碼
瞧,咱們的JSX已經能夠渲染到屏幕上了。
等下,這個有一個小問題,當咱們兩次調用 render
時,咱們會在控制檯看到一些奇怪的錯誤(譯者注: 能夠在 index.js 中屢次調用 render
,查看控制檯錯誤),背後的緣由是咱們只有在第一次渲染時,能夠在真實的DOM節點上調用 reconcile
方法,而後,咱們應該在以前返回的虛擬DOM節點上調用。
//src/qnd-react-dom.js
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';
const reconcile = snabbdom.init([propsModule]);
let rootVNode;
//QndReactDom.render(App, document.getElementById('root'))
const render = (el, rootDomElement) => {
if(rootVNode == null) {
//第一次調用 render 時
rootVNode = rootDomElement;
}
rootVNode = reconcile(rootVNode, el);
}
const QndReactDom = {
render
}
export default QndReactDom;
複製代碼
很開心,咱們的應用程序中有一個能正常工做的 JSX 渲染,如今讓咱們開始渲染一個函數組件,而不只僅是一些普通的 HTML。
讓咱們向 src/index.js
添加一個 Greeting
函數組件,以下所示:
//src/index.js
import QndReact from "./qnd-react";
import QndReactDom from './qnd-react-dom';
const Greeting = ({ name }) => <p>Welcome {name}!</p>;
const App = (
<div> <h1 className="primary"> QndReact is Quick and dirty react </h1> <p>It is about building your own React in 90 lines of JavsScript</p> <Greeting name={"Ameer Jhan"} /> </div> ); QndReactDom.render(App, document.getElementById('root')); 複製代碼
此時,在控制檯會出現如下錯誤:
咱們能夠在 QndReact.createElement(...)
方法中打印出數據看一下緣由。
//src/qnd-react.js
import { h } from 'snabbdom';
const createElement = (type, props = {}, ...children) => {
console.log(type, props, children);
return h(type, { props }, children);
};
...
複製代碼
若是能夠看到,函數組件傳遞過來的 type
是一個JS函數。若是咱們調用這個函數,就能得到組件但願渲染的 HTML
結果。
咱們根據 type
參數的類型,若是是函數類型,咱們就調用這個函數,並將 props
做爲參數傳給它,若是不是函數類型,咱們就看成普通的 HTML
元素處理。
//src/qnd-react.js
import { h } from 'snabbdom';
const createElement = (type, props = {}, ...children) => {
//若是是函數組件,那麼調用它,並返回執行結果
if (typeof (type) == 'function') {
return type(props);
}
return h(type, { props }, children);
};
const QndReact = {
createElement
};
export default QndReact;
複製代碼
歡呼!咱們的函數組件已經能夠正常工做了。
咱們已經完成了不少,讓咱們深吸一口氣,喝杯咖啡,由於咱們已經差很少實現了 React
,不過咱們還須要攻克類組件。
咱們首先在 src/qnd-react.js
中建立 Component
基類:
//src/qnd-react.js
import { h } from 'snabbdom';
const createElement = (type, props = {}, ...children) => {
//若是是函數組件,那麼調用它,並返回執行結果
if (typeof (type) == 'function') {
return type(props);
}
return h(type, { props }, children);
};
class Component {
constructor() { }
componentDidMount() { }
setState(partialState) { }
render() { }
}
const QndReact = {
createElement,
Component
};
export default QndReact;
複製代碼
如今咱們在 src/counter.js
中編寫咱們的第一個 Counter
類組件:
//src/counter.js
import QndReact from './qnd-react';
export default class Counter extends QndReact.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
componentDidMount() {
console.log('Component mounted');
}
render() {
return <p>Count: {this.state.count}</p>
}
}
複製代碼
是的,我知道咱們還沒有在計數器中實現任何邏輯,可是別擔憂,一旦咱們的狀態管理系統運行正常,咱們就會添加這些內容。如今,讓咱們嘗試在 src/index.js
中渲染它。
//src/index.js
import QndReact from "./qnd-react";
import QndReactDom from './qnd-react-dom';
import Counter from "./counter";
const Greeting = ({ name }) => <p>Welcome {name}!</p>;
const App = (
<div> <h1 className="primary"> QndReact is Quick and dirty react </h1> <p>It is about building your own React in 90 lines of JavsScript</p> <Greeting name={"Ameer Jhan"} /> <Counter /> </div> ); QndReactDom.render(App, document.getElementById('root')); 複製代碼
和料想中的同樣,又又又報錯了。
上面的錯誤看起來是否是很熟悉,當你嘗試使用類組件而不集成自 React.Component
時,可能遇到過以上錯誤。要知道爲何會這樣,咱們能夠在 React.createElement(...)
中添加一個 console.log
,以下所示:
//src/qnd-react.js
import { h } from 'snabbdom';
const createElement = (type, props = {}, ...children) => {
console.log(typeof (type), type);
//若是是函數組件,那麼調用它,並返回執行結果
if (typeof (type) == 'function') {
return type(props);
}
return h(type, { props }, children);
};
複製代碼
咱們來看看控制檯打印了什麼內容。
你能夠看出 Counter
的 type
類型也是函數,這是由於 Babel
會將 ES6 類轉換爲普通的 JS 函數,那麼咱們該如何類組件的狀況呢。其實,咱們能夠在咱們的 Component
基類中添加一個靜態屬性,這樣咱們利用該屬性去檢查 type
參數是不是一個類。React
中也是相同的處理邏輯,你能夠閱讀 Dan的博客
//src/qnt-react.js
import { h } from 'snabbdom';
const createElement = (type, props = {}, ...children) => {
console.log(typeof (type), type);
//若是是函數組件,那麼調用它,並返回執行結果
if (typeof (type) == 'function') {
return type(props);
}
return h(type, { props }, children);
};
class Component {
constructor() { }
componentDidMount() { }
setState(partialState) { }
render() { }
}
//給 Component 組件添加靜態屬性來區分是函數仍是類
Component.prototype.isQndReactClassComponent = true;
const QndReact = {
createElement,
Component
};
export default QndReact;
複製代碼
如今,咱們在 QndReact.createElement(...)
中增長一些代碼來處理類組件。
//src/qnd-react.js
import { h } from 'snabbdom';
const createElement = (type, props = {}, ...children) => {
console.log(type.prototype);
/** * 若是是類組件 * 1.建立一個實例 * 2.調用實例的 render 方法 */
if (type.prototype && type.prototype.isQndReactClassComponent) {
const componentInstance = new type(props);
return componentInstance.render();
}
//若是是函數組件,那麼調用它,並返回執行結果
if (typeof (type) == 'function') {
return type(props);
}
return h(type, { props }, children);
};
class Component {
constructor() { }
componentDidMount() { }
setState(partialState) { }
render() { }
}
//給 Component 組件添加靜態屬性來區分是函數仍是類
Component.prototype.isQndReactClassComponent = true;
const QndReact = {
createElement,
Component
};
export default QndReact;
複製代碼
如今,咱們的類組件已經可以渲染到瀏覽器上了:
咱們向類組件中增長 state
,在此以前,咱們須要知道,每次調用 this.setState({})
時,如何更新 DOM 的責任是 react-dom
包,而不是 React
的責任。這是爲了使 React
的核心部分,例如Component
類與平臺分離,從而提高代碼的可重用性。即在 ReactNative
中,你也可使用一樣的 Component
類,react-native
負責如何更新UI。你可能會問本身:當調用 this.setState(...)
時,React
如何知道該怎麼作,答案就是 react-dom
經過在 React
上設置了一個 __updater
屬性與 React
進行通訊。Dan 對此也有出色的文章,你能夠點擊閱讀。如今讓咱們在 QndReactDom
中爲 QndReact
添加 __updater
屬性。
//src/qnd-react-dom.js
import QndReact from './qnd-react';
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';
...
//QndReactDom 告訴 QndReact 如何更新 DOM
QndReact.__updater = () => {
//當調用 this.setState 的時候更新 DOM 邏輯
}
複製代碼
不管什麼時候咱們調用 this.setState({...})
,咱們都須要比較組件的 oldVNode
和在組件上調用了 render
方法以後生成的 newVNode
。爲了進行比較,咱們在類組件上添加 __vNode
屬性,以維護該組件當前的 VNode
實例。
//src/qnd-react.js
...
const createElement = (type, props = {}, ...children) => {
/** * 若是是類組件 * 1.建立一個實例 * 2.調用實例的 render 方法 */
if (type.prototype && type.prototype.isQndReactClassComponent) {
const componentInstance = new type(props);
componentInstance.__vNode = componentInstance.render();
return componentInstance.__vNode;
}
//若是是函數組件,那麼調用它,並返回執行結果
if (typeof (type) == 'function') {
return type(props);
}
return h(type, { props }, children);
};
...
複製代碼
如今咱們來在 Component
的基類中實現 setState
方法。
//src/qnd-react.js
...
class Component {
constructor() { }
componentDidMount() { }
setState(partialState) {
this.state = {
...this.state,
...partialState
}
//調用 QndReactDom 提供的 __updater 方法
QndReact.__updater(this);
}
render() { }
}
...
複製代碼
處理 QndReactDom 中的 __updater
方法。
//src/qnd-react-dom.js
...
QndReact.__updater = (componentInstance) => {
//當調用 this.setState 的時候更新 DOM 邏輯
//獲取在 __vNode 上存儲的 oldVNode
const oldVNode = componentInstance.__vNode;
//獲取 newVNode
const newVNode = componentInstance.render();
//更新 __vNode
componentInstance.__vNode = reconcile(oldVNode, newVNode);
}
...
export default QndReactDom;
複製代碼
OK,咱們在 Counter
組件中增長 state
來檢驗咱們的 setState
實現是否生效。
//src/counter.js
import QndReact from './qnd-react';
export default class Counter extends QndReact.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
// update the count every second
setInterval(() => {
this.setState({
count: this.state.count + 1
})
}, 1000);
}
componentDidMount() {
console.log('Component mounted');
}
render() {
return <p>Count: {this.state.count}</p>
}
}
複製代碼
太棒啦,如今 Counter
組件運行狀況與咱們預期徹底一致。
咱們繼續添加 componentDidMount
的生命週期鉤子函數。 Snabbdom
提供了一些鉤子函數,經過他們,咱們能夠知道真實DOM上面是否有添加,刪除或是更新了虛擬DOM節點,你能夠在此處瞭解更多信息。
//src/qnd-react.js
import { h } from 'snabbdom';
const createElement = (type, props = {}, ...children) => {
/** * 若是是類組件 * 1.建立一個實例 * 2.調用實例的 render 方法 */
if (type.prototype && type.prototype.isQndReactClassComponent) {
const componentInstance = new type(props);
componentInstance.__vNode = componentInstance.render();
return componentInstance.__vNode;
//增長鉤子函數(當虛擬DOM被添加到真實DOM節點上時)
componentInstance.__vNode.data.hook = {
create: () => {
componentInstance.componentDidMount()
}
}
}
//若是是函數組件,那麼調用它,並返回執行結果
if (typeof (type) == 'function') {
return type(props);
}
return h(type, { props }, children);
};
...
export default QndReact;
複製代碼
至此,咱們已經在類組件上支持了 componentDidMount
生命週期鉤子函數。
結束以前,咱們再添加下事件綁定的支持。爲此,咱們能夠在 Counter
組件中增長一個按鈕,點擊的時候,計數器的數字增長。請注意,咱們遵循的是基於常規的JS事件命名約定,而非基於 React
,即雙擊事件使用 onDblClick
,而非 onDoubleClick
。
import QndReact from './qnd-react';
export default class Counter extends QndReact.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
componentDidMount() {
console.log('Component mounted');
}
render() {
return (
<div> <p>Count: {this.state.count}</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}>Increment</button> </div>
)
}
}
複製代碼
上面的組件不會正常工做,由於咱們沒有告訴咱們的 VDom
如何去處理它。首先,咱們給 Snabdom
增長事件監聽模塊。
//src/qnd-react-dom.js
import QndReact from './qnd-react';
import * as snabbdom from 'snabbdom';
import propsModule from 'snabbdom/modules/props';
import eventlistenersModule from 'snabbdom/modules/eventlisteners';
const reconcile = snabbdom.init([propsModule, eventlistenersModule]);
...
複製代碼
Snabdom
但願將文本屬性和事件屬性做爲兩個單獨的對象,咱們咱們須要這樣作:
//src/qnd-react.js
import { h } from 'snabbdom';
const createElement = (type, props = {}, ...children) => {
...
let dataProps = {};
let eventProps = {};
for (let propKey in props) {
// event 屬性老是以 `on` 開頭
if (propKey.startsWith('on')) {
const event = propKey.substring(2).toLowerCase();
eventProps[event] = props[propKey];
} else {
dataProps[propKey] = props[propKey];
}
}
return h(type, { props: dataProps, on: eventProps }, children);
};
...
複製代碼
如今當咱們點擊 Counter
組件的按鈕的時候,計數器加1。
太棒了,咱們終於完成了一個React的簡陋的實現。可是,咱們還不能呈現列表,我想把它做爲有趣的小任務交給您。我建議您嘗試在 src/index.js
中呈現一個列表,而後調試 QndReact.createElement(...)
方法找出問題所在。
感謝您一直陪伴我,但願您喜歡構建本身的 React
,並瞭解了 React
在此過程當中是如何工做的。若是您在任何地方卡住了,請隨時參考我共享的代碼: github.com/ameerthehac…