首頁>技術>

當我發現 React 所做的一切非常簡單,甚至如果我們不是下一家大型初創公司增加籌碼,僅需要很少的JS程式碼就可以構建它。這也是促使我寫這篇文章的動力,希望你讀完這篇文章也有相同的感覺。

當我學習 React 的時候,我覺得它所做的一切都是魔術,然後我就開始思考這種魔術究竟是什麼。我感到非常驚訝,當我發現 React 所做的一切非常簡單,甚至如果我們不是下一家大型初創公司增加籌碼,僅需要很少的JS程式碼就可以構建它。這也是促使我寫這篇文章的動力,希望你讀完這篇文章也有相同的感覺。

我們將構建什麼功能?

JSX 函式元件 類元件 生命週期鉤子函式

我們不會構建什麼?

虛擬DOM

再次為了簡單起見,我們不會在本文中實現我們自己的虛擬DOM,我們將使用 snabbdom ,有趣的是,Vue.js 虛擬DOM借鑑了它,你可以在這裡讀更多關於 snabbdom 的內容: https://github.com/snabbdom/snabbdom

React Hooks

有些人可能對此感動失望,但是,一口吃不成一個胖子,我們需要一步一步來,因此讓我們首先構建基本的東西,然後再在此基礎上加以補充。我計劃後續文章中在我們此次構建的內容之上,編寫我們自己的 React Hooks 以及虛擬DOM,

可除錯性

這是增加任何庫或框架的複雜度的關鍵部分之一,由於我們只是出於娛樂目的而做,因此我們可以放心地忽略 React 提供的可除錯性功能,例如 dev tools 和分析器。

效能和相容性

我們不會過於關注我們的庫的效能,我們只想構建能正常執行的庫。讓我們也不要費力地確保它可以在市場上的所有瀏覽器上使用,只有能夠在某些現代瀏覽器上可以使用,那就已經很好了。

讓我們開始動手

在開始之前,我們需要一個支援ES6,自動熱更新的腳手架。我已經建立了一個非常基礎的 webpack 腳手架,你可以進行克隆和設定: https://github.com/ameerthehacker/webpack-starter-pack

JSX

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(...) 方法中打印出資料看一下原因。

如果可以看到,函式元件傳遞過來的 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);  componentInstancecomponentInstance.__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);  componentInstancecomponentInstance.__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(...) 方法找出問題所在。

我自己是一名從事了多年的前端老程式設計師,小編為大家準備了新出的前端程式設計學習資料,免費分享給大家!

如果你也想學習前端,那麼幫忙轉發一下然後再關注小編後私信【1】可以得到我整理的這些前端資料了(私信方法:點選我頭像進我主頁有個上面有個私信按鈕)

最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 使用開源軟體快速搭建資料分析平臺