首先歡迎大家關注我的掘金賬號和Github部落格,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵。
之前分享過幾篇關於React的文章:
其實我在閱讀React原始碼的時候,真的非常痛苦。React的程式碼及其複雜、龐大,閱讀起來挑戰非常大,但是這卻又擋不住我們的React的原理的好奇。前段時間有人就安利過Preact,千行程式碼就基本實現了React的絕大部分功能,相比於React動輒幾萬行的程式碼,Preact顯得別樣的簡潔,這也就為了我們學習React開闢了另一條路。本系列文章將重點分析類似於React的這類框架是如何實現的,歡迎大家關注和討論。如有不準確的地方,歡迎大家指正。
關於Preact,官網是這麼介紹的:
Fast 3kb React alternative with the same ES6 API. Components & Virtual DOM.
我們用Preact編寫程式碼就雷同於React,比如舉個例子:
import { Component , h } from 'preact'
export default class TodoList extends Component {
state = { todos: [], text: '' };
setText = e => {
this.setState({ text: e.target.value });
};
addTodo = () => {
let { todos, text } = this.state;
todos = todos.concat({ text });
this.setState({ todos, text: '' });
};
render({ }, { todos, text }) {
return (
<form onSubmit={this.addTodo} action="javascript:">
<input value={text} onInput={this.setText} />
<button type="submit">Add</button>
<ul>
{ todos.map( todo => (
<li>{todo.text}</li>
)) }
</ul>
</form>
);
}
}複製程式碼
上面就是用Preact編寫TodoList的例子,掌握React的你是不是感覺再熟悉不過了,上面的例子和React不太相同的地方是render
函式有引數傳入,分別是render(props,state,context)
,其目的是為了你解構賦值方便,當然你仍然可以render
函式中通過this
來引用props
、state
和context
。語法方面我們不再多做贅述,現在正式開始我們的內容。
本人還是非常推崇React這一套機制的,React這套機制提我們完成了資料和檢視的繫結,使得開發人員只需要關注資料和資料流的改變,從而極大的降低的開發的關注度,使得我們能夠集中精力於資料本身。而且React引入了虛擬DOM(virtual-dom)的機制,從而提升渲染效能。在開始接觸React時,覺得虛擬DOM機制十分的高大上,但經過一段時間的學習,開始對虛擬DOM有了進一步的認識。虛擬DOM從本質上將就是將複雜的DOM轉化成輕量級的JavaScript物件,不同的渲染中會生成同的虛擬DOM物件,然後通過高效優化過的Diff演算法,比較前後的虛擬DOM物件,以最小的變化去更新真實DOM。
正如上面的圖,其實類React的框架的程式碼都基本可以分為兩部分,元件到虛擬DOM的轉化、以及虛擬DOM到真實DOM的對映。當然細節性的東西還有非常多,比如生命週期、事件機制(代理)、批量重新整理等等。其實Preact精簡了React中的很多部分,比如React中採用的是事件代理機制,Preact就沒這麼做。這篇文章將著重於敘述Preact的JSX與元件相關的部分程式碼。
最開始學習React的時候,以為JSX是React的所獨有的,現在其實明白了JSX語法並不是某個庫所獨有的,而是一種JavaScript函式呼叫的語法糖。我們舉個例子,假如有下面的程式碼:
import ReactDOM from 'react-dom'
const App = (props)=>(<div>Hello World</div>)
ReactDOM.render(<APP />, document.body);複製程式碼
請問可以執行嗎?事實上是不能只能的,瀏覽器會告訴你:
Uncaught ReferenceError: React is not defined
如果你不瞭解JSX你就會感覺奇怪,因為沒有地方顯式地呼叫React,但是事實上上面的程式碼確實用到了React模組,奧祕就在於JSX。JSX其實相當於JavaScript + HTML(也被稱為hyperscript,即hyper + script,hyper是HyperText超文字的簡寫,而script是JavaScript的簡寫)。JSX並不屬於新的語法,其目的也只是為了在JavaScript指令碼中更方便的構建UI檢視,相比於其他的模板語言更加的易於上手,提升開發效率。上面的例項如果經過Babel轉化其實會得到下面結果:
var App = function App(props) {
return React.createElement(
'div',
null,
'Hello World'
);
};複製程式碼
我們可以看到,之前的JSX語法都被轉換成函式React.createElement
的呼叫方式。這就是為什麼在React中有JSX的地方都需要顯式地引入React的原因,也是為什麼說JSX只是JavaScript的語法糖。但是按照上面的說法,所有的JSX語法都會被轉化成React.createElement
,那豈不是JSX只是React所獨有的?當然不是,比如下面程式碼:
/** @jsx h */
let foo = <div id="foo">Hello!</div>;複製程式碼
我們通過為JSX新增註釋@jsx
(這也被成為Pragma,即編譯註釋),可以使得Babel在轉化JSX程式碼時,將其裝換成函式h
的呼叫,轉化結果成為:
/** @jsx h */
var foo = h(
"div",
{ id: "foo" },
"Hello!"
);複製程式碼
當然在每個JSX上都設定Pragma是沒有必要的,我們可以在工程全域性進行配置,比如我們可以在Babel6中的.babelrc
檔案中設定:
{
"plugins": [
["transform-react-jsx", { "pragma":"h" }]
]
}複製程式碼
這樣工程中所有用到JSX的地方都是被Babel轉化成使用h
函式的呼叫。
說了這麼多,我們開始瞭解一下Preact是怎麼構造h
函式的(關於為什麼Preact將其稱為h
函式,是因為作為hyperscript
的縮寫去命名的),Preact對外提供兩個介面: h
與createElement
,都是指向函式h
:
import {VNode} from './vnode';
const stack = [];
const EMPTY_CHILDREN = [];
export function h(nodeName, attributes) {
let children = EMPTY_CHILDREN, lastSimple, child, simple, i;
for (i = arguments.length; i-- > 2;) {
stack.push(arguments[i]);
}
if (attributes && attributes.children != null) {
if (!stack.length) stack.push(attributes.children);
delete attributes.children;
}
while (stack.length) {
if ((child = stack.pop()) && child.pop !== undefined) {
for (i = child.length; i--;) stack.push(child[i]);
}
else {
if (typeof child === 'boolean') child = null;
if ((simple = typeof nodeName !== 'function')) {
if (child == null) child = '';
else if (typeof child === 'number') child = String(child);
else if (typeof child !== 'string') simple = false;
}
if (simple && lastSimple) {
children[children.length - 1] += child;
}
else if (children === EMPTY_CHILDREN) {
children = [child];
}
else {
children.push(child);
}
lastSimple = simple;
}
}
let p = new VNode();
p.nodeName = nodeName;
p.children = children;
p.attributes = attributes == null ? undefined : attributes;
p.key = attributes == null ? undefined : attributes.key;
return p;
}複製程式碼
函式h
接受兩個引數節點名nodeName
,與屬性attributes
。然後將除了前兩個之外的引數都壓如棧stack。這種寫法挺令人吐槽的,寫成h(nodeName, attributes, ...children)
不是一目瞭然嗎?因為h
的引數是不限的,從第三個引數起的所有引數都是節點的子元素,所以棧儲存的是當前元素的子元素。然後會再排除一下第二個引數(其實就是props
)中是否含有children
屬性,有的話也將其壓如棧中,並且從attributes
中刪除。然後迴圈遍歷棧中的每一個子元素:
- 首先判別該元素是不是陣列型別,這裡採用的就是鴨子型別(duck type),即看起來來一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子,我們在這裡通過是否含有函式
pop
去判別是否是一個陣列,如果子元素是一個陣列,就將其全部壓入棧中。為什麼這麼做呢?因為子元素有可能是陣列,比如:render(){ return( <ul> { [1,2,3].map((val)=><li>{val}</li>) } </ul> ) }複製程式碼
因為子元素是不支援布林型別的,因此將其置為:
null
。 如果傳入的節點不是函式的話,分別判斷如果是null
,則置為空字元,如果是數字的話,將其轉化成字串型別。變數simple
用來記錄節點是否是簡單型別,比如dom
名稱或者函式就不屬於,如果是字串或者是數字,就會被認為是簡單型別然後程式碼
if (simple && lastSimple) { children[children.length - 1] += child; }複製程式碼
其實做的就是一個字串拼接,lastSimple是用來記錄上次的節點是否是簡單型別。之所以這麼做,是因為某些編譯器會將下面程式碼
let foo = <div id="foo">Hello World!</div>;複製程式碼
轉化為:
var foo = h( "div", { id: "foo" }, "Hello", "World!" );複製程式碼
這是時候
h
函式就會將後兩個引數拼接成一個字串。最後將處理子節點的傳入陣列
children
中,現在傳入children
中的節點有三種型別: 純字串、代表dom
節點的字串以及代表元件的函式(或者是類)
函式結束迴圈遍歷之後,建立了一個VNODE
,並將nodeName
、children
、attributes
、key
都賦值到節點中。需要注意的是,VNODE
只是一個普通的建構函式:
function VNode() {}複製程式碼
說了這麼多,我們看幾個轉化之後的例子:
//jsx
let foo = <div id="foo">Hello World!</div>;
//js
var Element = h(
"div",
{ id: "foo" },
"Hello World!"
);
//轉化為的元素節點
{
nodeName: "div",
children: [
"Hello World!"
],
attributes: {
id: "foo"
},
key: undefined
}複製程式碼
/* jsx
class App extends Component{
//....
}
class Child extends Component{
//....
}
*/
let Element = <App><Child>Hello World!</Child></App>
//js
var Element = h(
App,
null,
h(
Child,
null,
"Hello World!"
)
);
//轉化為的元素節點
{
nodeName: ƒ App(argument),
children: [
{
nodeName: ƒ Child(argument),
children: ["Hello World!"],
attributes: undefined,
key: undefined
}
],
attributes: undefined,
key: undefined
}複製程式碼
上面JSX元素轉化成的JavaScript物件就是DOM在記憶體中的表現。在Preact中不同的資料會生成不同的虛擬DOM節點,通過比較前後的虛擬DOM節點,Preact會找出一種最簡單的方式去更新真實DOM,以使其匹配當前的虛擬DOM節點,當然這會在後面的系列文章講到,我們會將原始碼和概念分割成一塊塊內容,方便大家理解,這篇文章著重講述了Preact的元素建立與JSX,之後的文章會繼續圍繞Preact類似於diff、元件設計等概念展開,歡迎大家關注我的賬號獲得最新的文章動態。