從Preact瞭解一個類React的框架是怎麼實現的(一): 元素建立

  首先歡迎你們關注個人掘金帳號和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來引用propsstatecontext。語法方面咱們再也不多作贅述,如今正式開始咱們的內容。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對外提供兩個接口: hcreateElement,都是指向函數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,並將nodeNamechildrenattributeskey都賦值到節點中。須要注意的是,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、組件設計等概念展開,歡迎你們關注個人帳號得到最新的文章動態。

相關文章
相關標籤/搜索