首先歡迎你們關注個人掘金帳號和Github博客,也算是對個人一點鼓勵,畢竟寫東西無法得到變現,能堅持下去也是靠的是本身的熱情和你們的鼓勵。
以前分享過幾篇關於React的文章:javascript
其實我在閱讀React源碼的時候,真的很是痛苦。React的代碼及其複雜、龐大,閱讀起來挑戰很是大,可是這卻又擋不住咱們的React的原理的好奇。前段時間有人就安利過Preact,千行代碼就基本實現了React的絕大部分功能,相比於React動輒幾萬行的代碼,Preact顯得別樣的簡潔,這也就爲了咱們學習React開闢了另外一條路。本系列文章將重點分析相似於React的這類框架是如何實現的,歡迎你們關注和討論。若有不許確的地方,歡迎你們指正。
關於Preact,官網是這麼介紹的: java
Fast 3kb React alternative with the same ES6 API. Components & Virtual DOM.node
咱們用Preact編寫代碼就雷同於React,好比舉個例子: 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
。語法方面咱們再也不多作贅述,如今正式開始咱們的內容。git
本人仍是很是推崇React這一套機制的,React這套機制提咱們完成了數據和視圖的綁定,使得開發人員只須要關注數據和數據流的改變,從而極大的下降的開發的關注度,使得咱們可以集中精力於數據自己。並且React引入了虛擬DOM(virtual-dom)的機制,從而提高渲染性能。在開始接觸React時,以爲虛擬DOM機制十分的高大上,但通過一段時間的學習,開始對虛擬DOM有了進一步的認識。虛擬DOM從本質上將就是將複雜的DOM轉化成輕量級的JavaScript對象,不一樣的渲染中會生成同的虛擬DOM對象,而後經過高效優化過的Diff算法,比較先後的虛擬DOM對象,以最小的變化去更新真實DOM。github
正如上面的圖,其實類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轉化其實會獲得下面結果: 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
中刪除。而後循環遍歷棧中的每個子元素:
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、組件設計等概念展開,歡迎你們關注個人帳號得到最新的文章動態。