在 Javascript ES6 的新功能中,有一個新品種的 function 稱為 generator 這個名字有點奇怪,不過它的行為在第一次看到的時候似乎更加奇怪。這篇翻譯文章是在學習 generator 基本的運作原理過程中的紀錄。

執行到完成

在我們談論關於 generator 時,第一件事情是對於標題下的 執行到完成, generator 是如何不同於普通函式。

不過您是否看懂上面這一小段,您一直都對於 function 有一個相當基本的認知那就是一旦函式開始執行,它就會一直執行到完成為止。

setTimeout(function() {
  console.log("Settimeout excuted");
}, 1); 

function foo() {
  for(var i=0; i<1000; i++) {
    console.log(i);
  }
}

foo();
// 1 - 1000

// Settimeout excuted

// 即使只差 1ms 還是要等 foo 跑完。

這邊 for 迴圈如果數字再大一點那就需要點時間,至少大於 1ms,然後您就發現 setTimeout 明明說好是 1ms 後發動卻無法中斷 foo 所以 setTimeout 就被卡在 event-loop 等待直到輪到它為止。

那如果 foo() 可以被中斷呢? 不會破壞我們的程式嗎?
那的確是個挑戰,當我們採取多執行緒來寫程式的時候,不過好佳在我們用 Javascript 所以我們不需要擔心那些,因為 Javascript 大部份的時候都是單執行緒,意思是在一個時間點永遠只有一個 function 或指令在執行。

注意: Web Worker 是讓你可以啟動另一個完全分離的執行緒,給一部分的 JS 在其中執行的機制,跟您主要的執行緒是平行的。
不在我們的程式使用多執行並發是因為兩個執行緒只能透過非同步事件來互相溝通。雖然是兩個平行的執行緒一旦要溝通還是要遵守 event-loop 的規則,一次只有一個動作,且還是一旦執行就要執行到完成。

執行 - 暫停 - 執行

透過 ES6 generator 我們可以有不同的函式,它可以在執行到一半暫停,然後再回復執行。讓其他程式可以在暫停這段期間先跑。

如果您曾經得讀過關於並發或者執行緒程式設計的文章,您也許看過 cooperative 這個術語,其基本的意思是一個進程(process),在我們 JS 的範例是 function 會自己決定何時應該允許中斷暫停。因此可以和其他的程式碼協同合作。這個觀念的對比是 preemptive 指出一個進程可能會違反原本的設計而中斷。

ES6 generator function 在其並發行為裡是可以協同合作的。在 generator function 裡面您可以使用新的關鍵字 yield 來從內部暫停。沒有東西可以從外部暫停一個 generator ,必須要透過 yield 從內部暫停。

然而一旦 generator 用 yield 暫停了自己,它就不能靠自己回復。必須要有個外部的控制行為來使其回復執行。稍後會解釋該如何做。

所以基本上,一個 generator 函式可以被暫停,重啟,隨您高興開開關關幾次。
事實上您可以用一個無限迴圈來搭配 generator ,在一般 JS 程式中出現無限迴圈通常是寫錯了,不過搭配 generator 卻是合理的而且有時候您的確就是想要這麼做。

更重要的是,這個暫停重啟不只單單是控制 generator 的執行流程,而且還提供了兩種資料溝通的方式,在執行過程中傳遞輸入和輸出的訊息。

在一般函式中您可以傳入參數(Parameters)然後 return 一個結果。在 generator 您可以透過 yield 把資料丟出來,然後傳回其他資料再回復執行。

怎麼寫?

這一小段讓我們來開始介紹關於這些新功能的語法(syntax)

首先是這個新的 generator function 的宣告

function *foo() {
  // ...

}

注意到 * 了嗎? 這個新語法看起來有點奇怪,在其他語言中看起來像是函式指標。不過不要搞混,這只是一個符號用來判斷這是一個特殊的 generator 函式。

您可能看過其他文章使用 function* foo(){} 而不是 function *foo(){},兩種宣告都正確。

generator function 大致上就是一個普通的 function ,只是在內部多了一些新的語法可以使用。

而最主要的新玩具就是我們上面提到的 yield,直接來看點範例

function *foo() {
  var a = 1 + (yield "fooo");
  console.log(x);
}

當 generator 執行到 yield 時會暫停,這個時候會把右邊的 expression 把就是 fooo 字串送出來,當 generator 再次啟動的時候無論資料有沒有送進去 generator 就會取得另外一個 yield expression,把 1 + yield expression 計算的結果。

yield 的意思我喜歡用佔位的概念來形容,有點像 hook 的觀念。

剛剛我說的有點讓你混淆,讓我們再來釐清一次 yield 第一個功能是暫停,當函式走到 yield 的時候會先停止,然後把右邊的 expression 丟到外面。
停一下!這個 expression 跟待會要接回來的資料沒有關係。把 yield 想成佔位符,意思是停在這邊等別人把值丟進來,同時在我停下來的時候也可以丟個東西出去。
有點類似 HTTP 的運作概念,執行到 yield 的時候對外部發送個 request 然後等待外部把資料送回來。再停一下!什麼外部?就是 generator 的實體物件。
他會負責把資料再丟回來。丟回來的時候記住就不會再被那個 "fooo" 混淆了, "fooo" 丟出去後就沒有他的事了。

這個例子太難懂? 讓我們看點更完整的基本用法

function *gen() {
  console.log('start');
  var o = yield "foobar";
  console.log("I am back and bring " + o);
}
var a = gen();          // 第一次呼叫時是返回一個 generator 物件

var b = a.next();       // 開始執行,到 yield 時會暫停執行並返回,返回值是一個物件

console.log(b.value);   // 他的 value 屬性是 yield 右側的 expression 的執行結果

console.log(b.done);    // 是否完成

var c = a.next("something from outside"); // 傳個值回去

console.log(c.done);    // 完成

a.next();                             // 如果再呼叫 next(),就會拋出例外

現在您應該看懂了兩種溝通方式了吧

您可以在任何 expression 的位置單純使用 yield,將其置放在 expression/statement 之中,然後輸出的部分就會是 undefined。

一個片段程式碼產生一個值稱之為 expression,expression 類似語言中的片語,一個短句。
statement 則是一句完整的句子,在 JS 中用 ; 結束當作一個句子。
通常一個 statement 是獨立的,只會完成某項任務,不過如果它影響了整個程式例如: 異動了機器內部的狀態,或者影響後面的 statement,這些造成的改變我們就稱為 side effect (副作用)

function foo(x) {
  console.log("x: " + x);
}

function *bar() {
  yield; // 只會暫停

  foo(yield); // 暫停並等待傳入參數到 foo()

}

Generator Iterator

Iterator 迭代器實際上是一種特殊的行為,也可以表示一個設計模式。這個行為指的是讓我們可以透過呼叫 next()
在一個排序的集合中,特定時間點下一次只取得一個值。舉例來說我們在[1, 2, 3, 4, 5]這個陣列上使用 iterator。
第一次呼叫 next() 時我們會取得 1,第二次 2 以此類推
當所有元素值都被回傳過後,next() 將會回傳 null, false 或者其他通知我們已經跑完所有元素的訊號。

剛剛提到我們在外部用來控制 generator function 的那個實體物件就是 generator iterator ,聽起來好像挺複雜的不過讓我們來看看實際上的例子

// 假設我們有一個 generator function

function *foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
}

為了逐步從 *foo 這個 generator function 中取得 yield 傳出來的資料我們需要一個迭代器

var it = foo(); // 再次強調,第一次呼叫 function 傳回一個迭代器

所以!!第一次像我們平常一樣呼叫 function 的時候並不會真的執行。

在我們的觀念裡這的確有點陌生。您可能也會好奇想知道為什麼不是用 var it = new foo() 因為剛剛不是說回傳一個 iterator 實體物件嗎? 好吧我真的不知道,等我知道了在告訴你。這邊暫時先不討論這個問題 XD

接著讓我們開始來使用 iterator

var message = it.next();
console.log(message); // #=> {value: 1, done: false}

第一次迭代之後我們會拿到 yield 傳出來的資料。再次強調一遍不要把 yield 的觀念當作是 function ,把它分成兩次一次負責輸出,取得資料之後您可以修改操作然後再把您的值丟回去。留在 function yield 右邊的那個 expression 丟出來後就沒用了。不要被它干擾。

每一次我們呼叫 next() 都會取得一個物件這個物件有 valuedone 兩個屬性。done 用來判斷迭代器是否執行完畢。

console.log( it.next() ); // { value:2, done:false }

console.log( it.next() ); // { value:3, done:false }

console.log( it.next() ); // { value:4, done:false }

console.log( it.next() ); // { value:5, done:false }

執行到第五次我們發現 done 還是 false 那是因為技術上來說 generator 還沒有執行完成。yield 傳出資料了還在等待你傳回去繼續執行。所以我們仍然要呼叫最後一次。
所以最後一次如下:

console.log( it.next() ); // { value:undefined, done:true }

現在我們執行完全部的流程了但是我們最後一次並沒有拿到任何資料
因為我們已經用盡了 yield ____

在這個關鍵點,您也許想知道我可以從 generator 回傳值嗎?並且如果我這麼做那這個值會在 {value: , done: true} 這個物件的 value 嗎?

答案是 Yes 可以

function *foo() {
    yield 1;
    return 2;
}

var it = foo();
console.log( it.next() ); // { value:1, done:false }

console.log( it.next() ); // { value:2, done:true }

等等...但也不可以

依賴 return 恐怕不是個好主意,因為當我們使用 for..of 的時候最後一個回傳的值會被捨棄

function *foo() {
    yield 1;
    yield 2;
    return 3;
}

var it = foo();
for(var i of it) {
  console.log("使用 for of " + i); 
}
// 使用 for of 1

// 使用 for of 2

為了完整起見讓我們來看看完整的輸入和輸出是如何操作的

現在我要來回答您怎麼丟資料回去呢? 就是每個 next() 帶入的參數

function *foo(x) {
  // you can use this to inspect

  // console.log(`x: ${x}, y: ${y}, z: ${z}`);


  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);;
  return (x + y + z);
}

var it = foo(5); // 取得 iterator 物件,並不執行


console.log( it.next() ); // { value:6, done:false }

// 第一次呼叫 x: 5, y: undefined, z: undefined

// 執行到 var y 那邊停住,傳出 yield(x+1) = 6


console.log( it.next( 12 ) );   // { value:8, done:false }

// 送 12 進去所以 y = 2 * 12 = 24,第二次呼叫 x:5, y: 24, z: undefined

// 到 var z 那邊停住,輸出 8 等待輸入...


console.log( it.next( 13 ) );   // { value:42, done:true }

// 送 13 進去所以 z = 13 所以第三次呼叫 x: 5, y: 24, z: 13

// 第三次完成並取得 return value 42

你可以看到我們仍然可以透過參數來初始化 x,第一次初始化並建立 iterator 順便讓 x 等於 5。

第一次 next() 我們沒有傳入任何值因為第一次還沒有任何 yield 在等你傳值進去。那如果我們傳值了呢? 沒什麼不行,因為這個值會被丟掉。ES6 表示 generator function 會忽略用不到的值。不過有些還沒完全實作 ES6 的瀏覽器可能會出錯。

yield (x + 1) 先往外丟出 6 ,然後你第二次呼叫 next(12) 所以 y 會是 2 * 12 = 24 接著 yield (y / 3) 就是 yield (24 / 3) 丟出 8 一樣等你把 13 丟進去所以 z = 13

最後 return (x + y + z) 等於 42,看這邊可能會頭暈。多看幾次。

for..of

ES6 也提供一種方便的迭代語法,for...of

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
}

for (var v of foo()) {
    console.log( v );
}
// 1 2 3 4 5


console.log( v ); // still `5`, not `6` :(

如您所見,foo() 會先建立迭代器且 for..of 會自動去擷取它然後為您自動迭代取出每一個 yield express 吐回來的值。直到 done:true 出現。當 donefalse 的時候他會自動擷取 value 屬性,注意不是物件。一旦 done: true 迴圈就停止,而且不會包含最後的 return 的值。

注意上面您可以看到 for..of 迴圈會忽略丟掉 return 6,而且因為沒有 next() 可以使用,所以在這種情況下你就不能用 for..of 必須要自己操作。

結論

OK! 現在您已經懂了 generator 的基本用法了。別擔心如果你現在有點混亂是正常的,我第一次看也是。
很自然的您會想知道,這個新玩具可以在實際專案中做些什麼?

在您熟悉玩過上面這些範例程式碼之後您可能會問

  1. 如何把它用在錯誤處理方面?
  2. Generator 可以呼叫其他 Generator 嗎?
  3. 如何使用非同步的方式操作 Generator?

如果我有時間我會繼續翻譯系列文章

資源

參考翻譯自The Basics Of ES6 Generators

這是一篇記流水帳的操作步驟,文章翻譯自微軟 Developer Tools Blogs
文章加上小弟遇到的特殊情形經官方人員協助處理完成的紀錄。

本文主要是介紹如何使用 Selenium 搭配微軟的雲端服務 Visual Studio Online 執行壓力測試。
透過 Selenium 我們可以達到最貼近使用者操作的情形模擬。
首先讓我們需要概略的認識 Visual Studio Online(VSO) 的 Cloud-based Load Testing(CLT) 和 Selenium。

  • Cloud-based Load Testing:

    • 這是 Visual Studio 2013 後引入的新功能,這篇記錄就不詳細介紹關於 CLT 的部分您可以參考官方的介紹
    • 快速入門的話可參考這個連結
  • Selenium

    • Selenium 是一套模擬瀏覽器操作,自動執行的測試軟體,簡單說就是可以側錄或撰寫 script 讓它自動幫你執行,詳細介紹直接參考官方
    • 請參考官方網站對 Selenium 的基礎操作有些瞭解,這篇文章主題為 Selenium 搭配 CLT 故如何撰寫 script 不再這篇重點
    • 原文教學針對使用 Selenium Nuget 搭配 Phantomjs Nuget ,不過因為小弟的站大量使用 socket.io 在這邊 phantomjs 1.x 會有一些問題,所以我們補上如何使用 Firefox 當作模擬的瀏覽器

正文開始

在 VS 撰寫 Selenium 單元測試

【一】 首先在 VS 中建立一個單元測試專案(Unit Test Project)。 檔案 -> 新增專案 -> 範本 -> Visual C# -> 測試 -> 單元測試專案

【二】 專案建立完成後,安裝 Selenium Nuget 。對專案按右鍵 -> 管理 Nuget 套件 -> 搜尋 Selenium -> 安裝

【三】 安裝完成之後,參考目錄下應該會看到 WebDriver

【四】 下載實際使用的瀏覽器模擬驅動,原文為了單純起見使用了 PhantomJs,接著您可以在 Nuget 搜尋 PhantomJS 並且安裝,安裝後記得修改 phantomjs.exe 的屬性為有更新時才複製

【五】 建立 C# 檔案開始撰寫單元測試,您也可以先透過 Selenium IDE 側錄大部份的行為為程式碼再來修改。

using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.PhantomJS;
using System;
using System.Text;
 
namespace SeleniumSample
{
  [TestClass]
  public class SeleniumTests
  {
    [TestMethod]
    public void TheBingSearchTest()
    {
      TestContext.BeginTimer("BingSearchTest_Navigate");
      _driver.Navigate().GoToUrl("http://www.bing.com/");
      TestContext.EndTimer("BingSearchTest_Navigate");
 
      TestContext.BeginTimer("BingSearchTest_SearchBHarry");
      _driver.FindElement(By.Id("sb_form_q")).SendKeys("Brian harry blog");
      _driver.FindElement(By.Id("sb_form_go")).Click();
      TestContext.EndTimer("BingSearchTest_SearchBHarry");
 
      var elementText = _driver.FindElement(By.XPath("//ol[@id='b_results']/li/h2/a"));
      Assert.IsTrue(elementText.Text.Equals("Brian Harry's blog - Site Home - MSDN Blogs"), "Verified title of the blog page");
    }
 
    public TestContext TestContext { get; set; }
 
    #region Additional test attributes
 
    [TestInitialize]
    public void SetupTestSuite()
    {
      Console.WriteLine("Test init called: {0}");
      _driver = new PhantomJSDriver();
    }
 
    [TestCleanup]
    public void CleanupTestSuite()
    {
      _driver.Quit();
    }
    #endregion
    private IWebDriver _driver;
  }
}

其他範例使用 Firefox

using System;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Text.RegularExpressions;
using OpenQA.Selenium;
using OpenQA.Selenium.Firefox;
using System.Threading;
using OpenQA.Selenium.Interactions;
using System.Collections.Generic;
using OpenQA.Selenium.Support.UI;
using System.Linq;


namespace PasserTesting
{
    [TestClass]
    public class SeleniumCase
    {
        private IWebDriver _driver;
        private const String WEB_SITE = "http://www.google.com";
  
        public TestContext TestContext
        {
            get;
            set;
        }

        [TestMethod]
        public void VisitGoogle()
        {
            TestContext.BeginTimer("Begin View Index");
            _driver.Navigate().GoToUrl(WEB_SITE);
            TestContext.EndTimer("Begin View Index");
            var title = _driver.Title;
            Assert.IsTrue(title.Equals("Google"));
        }


        private bool IsElementPresent(By by)
        {
            try
            {
                _driver.FindElement(by);
                return true;
            }
            catch (NoSuchElementException)
            {
                return false;
            }
        }


        #region Additional test attributes
        [TestInitialize]
        public void SetupTestSuite()
        {
            Console.WriteLine("Test init called: {0}");
            _driver = new FirefoxDriver();
           
        }

        public void CleanupTestSuite()
        {
            _driver.Quit();
        }
        #endregion
    }
}

【六】 對方案右鍵加入 -> 新增項目 -> 測試設定 -> 加入測試設定檔案

【七】 設定測試設定的部署加入 phantomjs.exe

【八】 建置方案

【九】 從 測試 -> 測試設定 -> 選取測試設定擋

【十】 先透過測試總管(Test Explorer)在本機驗證測試 (測試 -> 視窗 -> 測試總管)

到此我們已經完成測試的流程,接著要設定上 CLT 執行

為單元測試加上負載測試

【一】 替專案加上負載測試(右鍵 -> 加入負載測試)

根據精靈提示設定您需要的情節

【二】 在本地端驗證設定執行的狀況

透過 Visual Studio Online 執行 Selenium 測試搭配 CLT

【一】 恭喜!接下來您只要一個鍵就能夠在 Microsoft Azure Cloud 執行您的測試
【二】 當負載測試加完之後,在 測試 -> 測試設定 -> 選取測試檔案 選擇之前建立的 .testsettings
【三】 打開這個測試檔案把設定改成使用 VSO

想瞭解更多關於壓力測試請參考

【四】 當執行全部完成之後你就可以下載報告。就可以點擊檢視報告上方的連結

【五】 最後您可以得到關於你這次的測試資料大略如下圖

使用 Firefox(暫時的解決方案,官方表示正在處理更完整的方式)

當您因為一些其他原因無法使用 PhantomJS 當作 WebDriver 時這裏提供另外一個方案

【一】 首先我們必須要雲上裝上 Firefox 所以我們必須要提供一段 script 讓雲上的機器自己安裝 Firefox

:::::::::::::::::::::::::::::::::::::::::
:: Automatically check & get admin rights
:::::::::::::::::::::::::::::::::::::::::
@echo off
CLS
ECHO.
ECHO =============================
ECHO Running Admin shell
ECHO =============================
 
:checkPrivileges
NET FILE 1>NUL 2>NUL
if '%errorlevel%' == '0' ( goto gotPrivileges ) else ( goto getPrivileges )
 
:getPrivileges
if '%1'=='ELEV' (shift & goto gotPrivileges) 
ECHO.
ECHO **************************************
ECHO Invoking UAC for Privilege Escalation
ECHO **************************************
 
setlocal DisableDelayedExpansion
set "batchPath=%~0"
setlocal EnableDelayedExpansion
ECHO Set UAC = CreateObject^("Shell.Application"^) > "%temp%\OEgetPrivileges.vbs"
ECHO UAC.ShellExecute "!batchPath!", "ELEV", "", "runas", 1 >> "%temp%\OEgetPrivileges.vbs"
"%temp%\OEgetPrivileges.vbs"
exit /B
 
:gotPrivileges
::::::::::::::::::::::::::::
::START
::::::::::::::::::::::::::::
setlocal & pushd .
 
 
"%~dp0\Firefox.exe" /S

【二】 要執行這段 setup script 我們需要先在方案的資料夾下建立一個 Deployment 資料夾把上面那段 script 放入並且命名為 setup.cmd
【三】 下載要安裝的 Firefox.exe 放置到一樣的 Deployment 目錄
【四】 開啟 Visual Studio 對 .testsettings 點擊兩下開啟如下設定

【五】 加入 Deployment 目錄後下一步設定指令碼,選擇剛剛在同個目錄下的 setup.cmd

前言

小弟身為一個資質駑鈍的人,這正是我在學習 Flux 初期最希望有人可以幫我總結的事。服用本篇前須對 React 有基本的認識。
因為底子不好在參透官方範例時一直東奔西跑的查資料一下這個 merge 是什麼意思,一下又怎麼這邊一個 Dispatcher, AppDispatcher 然後又 ActionCreator
總之是你搞得我好亂啊。不過因為最近 React 的盛行讓我得以閱讀許多大大的分享因而有這一篇

我應該使用 Flux 嗎?

如果您的應用程式需要處理很多動態的資料那麼答案是 YES! 您可能應該使用 Flux
但如果您的應用程式只是靜態頁面,且不需要去共用一些應用程式的狀態,也從來不需要更新資料那麼這個答案就是 NO
Flux 不能帶給你任何好處

為什麼要用 Flux?

Flux 是一個相當複雜的概念,為什麼要增加程式的複雜度呢?哈!開玩笑的!!
百分之九十的 iOS 應用程式透過 table view 來呈現資料。iOS toolkit 擁有非常好的架構來處理關於資料模型的問題,這使得開發起來非常容易。

不過在前端的世界(HTML, Javascript, CSS) 我們沒有那些東西也沒人強迫我們一定得用這些,取而代之的是我們有一個大問題。沒人知道該怎麼完美的處理前端架構這個問題。
處理前端的工作已經好一陣子了,所謂的最佳實踐從來沒有完美的解決所有問題,現實反而是針對個別小問題的函式庫解決了他們
jQuery? Backbone? Handlebars? 其實我們也都知道真正的問題是關於資料,一但它邏輯和 UX 變得越來越複雜就很少人可以精準的控制他。

什麼是 Flux?

Flux 是一個 Facebook 創造的術語: 用來描述單一方向的資料流搭配特定的事件和註冊監聽的設計模型。並沒有一定是指 Flux 的函式庫,不過您的確需要 Flux Dispatcher 以及事件函式庫。
官方文件是用一種概念的方式在介紹因此對於像我這種資質比較差的人的確不是個很好的起點。沒幫我分解片段片段程式碼就吸收得很慢。
不過一旦你了解了關於 Flux 的想法您應該就能夠讀懂那些東西。

先不要試圖去比較 Flux 和 MVC 結構,把它們兩者搞在一起的話只會得到混亂。

OK! 談論夠多了,讓我們慢慢的探討,這篇文章將會慢慢解釋所有概念佐以程式碼。

1. 你的 Views (React Component) 分派了動作

一個 dispatcher 本質上是一套事件機制。它負責廣播事件和註冊回呼函式(callback)。而且全部就只有一個,一個全域的 dispatcher 物件。
為了讓事情單純你應該就直接使用 Facebook 的 Dispatcher Library 官方在解釋 Dispatcher 那段一開始的確讓我慌了。

var Dispatcher = require('flux').Dispatcher;
var AppDispatcher = new Dispatcher();  

接著我們假設您的應用程式中有一個新增的按鈕功能是加入一個項目到清單中:

<button onClick={ this.createNewItem }>New Item</button>  

當按鈕點擊的時候會發生什麼事情呢?您的 View 派送了一個非常特別的事件,這個事件包含著兩件事事件名稱該項目的資料

createNewItem: function( evt ) {

    AppDispatcher.dispatch({
        eventName: 'new-item',
        newItem: { name: 'Andy' } // example data
    });

}

2. 您的 Store 需要回應被派送來的事件

就像 Flux 一樣 "store" 也是 Facebook 創造的術語。對於我們的應用程式來說,我們需要一個邏輯集合(就是處理邏輯的物件)與資料來處理這份清單。
這指的就是 Store ,它不只要管理資料模型也需要回應上面提到的特殊的事件。在這邊我們就稱它為 ListStore
一個 store 本身是單獨的物件,應該就只有一個,意思是你不應該 new 出另外一個物件。換言之我們的 ListStore 是獨一無二全域的物件。

// 一個全域物件用來處理清單資料和邏輯
var ListStore = {

    // 實際資料模型的集合
    items: [],

    // 存取方法,後續我們將使用它來取得資料
    getAll: function() {
        return this.items;
    }

};

這個 store 接著要回應 dispatcher 發過來的特殊事件

var ListStore = …

AppDispatcher.register( function( payload ) {

    switch( payload.eventName ) {

        case 'new-item':

            ListStore.items.push( payload.newItem );
            break;

    }

    return true;

}); 

這是典型的 Flux 處理回應 dispatcher 派送過來的 action 的機制。每一個 payload 包含著事件名稱和資料,透過 dispatcher 分派過來。好了我們同時解釋了在官網那張圖上的 action 與 payload 。

dispatcher.dipatch({}) 發動這個 method => 派送一個 action
{} 裡面的物件我們稱為 payload

接著用一個 switch 程式片段用來決定該執行什麼動作。

  • 核心概念 1: 一個 store 不只是一個資料模型,但其包含著資料模型。
  • 核心概念 2: store 在程式中是唯一知道該如何更新資料的角色。這也是整個 Flux 最重要的部分。dispatcher 觸發的事件並不知道如何處理資料。對應回官方的說明這叫做一個 action。
  • 對應的行為是寫在 store 然後透過 AppDispatcher.register 註冊。

舉例來說如果有其他部分需要追蹤關於圖片的資料您就應該再開一個 store 並叫做 ImageStore。一個 store 只負責處理單一需求。
當您的程式變大的時候可以很輕易地根據需求找到對應的部分。如果程式不複雜可能您只需要一個 store。

記住!只有 store 是被允許註冊 dispatcher 的 callback。View 永遠不會呼叫 AppDispatcher.register 。而 dispatcher 也只能夠從 View 送訊息到 store。

3. Store 觸發了一個 "Change" 事件

我們幾乎快學完了!現在您的資料確實已經改變了,但是我們需要通知程式中其他角色。
store 接著會觸發一個事件,但不是靠 dispatcher 。這邊通常是初學者容易搞混的地方,但這就是 Flux 採用的方式。這邊我們讓 store 具備有觸發事件的能力
方法有很東種例如使用 MicroEvent 或者採用 EventEmitter。這邊為了讓你釐清觀念也不要加入太多東西
所以我們先用 MicroEvent 其觀念就是讓 Store 具備廣播事件的能力,你可能就會問廣播什麼事件?大略你可以先理解成這是一個發佈/訂閱的事件機制。
當 store 告訴全世界: 嘿!我飯煮好了!該吃飯的人就自己自動過來吃 XD。

註: 官方採用的方式可能一直在調整,從 merge 到 object-assign 其實觀念都是一樣的就是讓 store 具備廣播事件的能力且官方使用 EventEmitter。

這邊我們就透過 MicroEvent 讓 store 可以通知全世界:

MicroEvent.mixin( ListStore );  

好了!store 已經具備該能力了那就直接在下面呼叫

AppDispatcher.register( function( payload ) {

    switch( payload.eventName ) {

        case 'new-item':

            ListStore.items.push( payload.newItem );
            
            // 告訴其他人我已經改變好了
            ListStore.trigger( 'change' );
            break;

    }

    return true; 

}); 

核心觀念: 當我們觸發事件時我們不需要再把資料帶出去。view 只需要知道資料已經有更新了。讓我們繼續看下去來理解原理

4. View 回應 Change 事件

現在我們需要顯示清單。當清單發生改變,我們的 view 將會全部重新渲染輸出,沒有錯是全部!
為了讓 view 知道何時該更新,從 view 被掛載後它就必須監聽從 store 發出的 change 事件。

componentDidMount: function() {  
    ListStore.bind( 'change', this.listChanged );
},

為了簡單起見,我們會使用 forceUpdate 強制重新渲染。另一個方法是將整個清單存到 state
在 React 元件內的方法就會如下:

listChanged: function() {  
    // Since the list changed, trigger a new render.
    this.forceUpdate();
},

別忘記當卸載時把監聽清除

componentWillUnmount: function() {  
    ListStore.unbind( 'change', this.listChanged );
},

然後呢?讓我們來看看 render 函式,我們特意保留到最後再看

render: function() {

    // 記住, ListStore 是全域物件!
    // 透過它取得資料
    var items = ListStore.getAll();


    var itemHtml = items.map( function( item ) {

        return <li key={ listItem.id }>
            { listItem.name }
          </li>;

    });

    return <div>
        <ul>
            { itemHtml }
        </ul>

        <button onClick={ this.createNewItem }>New Item</button>

    </div>;
}

好了!我們已經完成整個循環。當你加入新的項目 -> view 透過 dispatcher 派送一個 action -> store 回應這個 action 處理資料(處理的 callback 已經被註冊到 dispatcher) -> store 處理完畢觸發 change 事件
-> view 因為有監聽這個事件所以做出對應的處理更新。

不過這邊還有一個問題,每一次我們都重新渲染了整個 view ,這難道不會造成什麼效能異常糟糕嗎?

不會!

沒錯我們的確是讓 render 方法重新在渲染一次,所有在 render 內部的程式碼會重跑一次,不過 React 只會在當資料有所改變的時候才會更新實際的 DOM,關於 render 事實上他只是產生一個虛擬的 DOM。
然後 React 會自動去和上一次的比較,如果兩個虛擬的 DOM 不同的話 React 才會更新實際的 DOM 而且是只有實際 DOM 不同的地方而已。

核心觀念: 當 store 的資料改變 view 不需要知道資料到底是增加還是減少或者修改,view 只要負責重新輸出整個元件,接著 React 的虛擬 DOM 機制會幫你處理如何有效率的更新 DOM。
是不是整個變得很單純。

還有一個東西: Action Creator 是什麼鬼?

記得,當我們點擊我們的按鈕時我們派送了一個特殊的事件:

AppDispatcher.dispatch({  
    eventName: 'new-item',
    newItem: { name: 'Andy' }
});

如果很多 view 需要這一個事件,那麼很快這一小段程式碼將到處重複,很快當你需要修改的時候又會搞不清楚。Flux 建議我們將這些派送的事件抽象化,叫做 action creator。
就只是把這些 AppDispatcher.dispatch 根據其功能分門別類,這樣其他 view 要用就只要引用就好

ListActions = {

    add: function( item ) {
        AppDispatcher.dispatch({
            eventName: 'new-item',
            newItem: item
        });
    }

};

現在您的 view 就可以單純呼叫 ListActions.add

希望到這邊為止可以建立起 Flux 的概念,剩下的就在看看官方的範例應該就比較看得懂了。

介紹

還記得之前小弟很認真的想跟大家分享 Flux 不過老實說在當時自己只能夠"模仿",Dispatcher 和 Store 的觀念也有點模糊。
由於今天看了這篇文章之後,覺得很不錯所以來補貼一下。
不過在這之前強烈推薦您還是先閱讀關於 Reactjs 的部分。

什麼是 Flux

再一次我們說 Flux 是 Facebook 內部搭配 React 使用的一種架構,一種設計模式。它不是一個 Framework 或 Library 。
它單純是一種新的架構用來搭配補充 React 以及其單向資料流的概念。
然而我們也知道 Facebook 有提供一個 Dispatcher 的函式庫。這個 Dispatcher 是一個全域的發佈/訂閱處理函式庫,他可以廣播 payload (實際資料) 到被註冊的 callback 回呼函式。
一個典型的 Flux 架構將會使用這個 Dispatcher 函式庫配合 Nodejs 的 EventEmitter 模組來達成設置一個事件系統以協助管理應用程式的狀態。
如果用另一個角度來說那就是 Dispatcher 其實就只是處理事件的註冊與廣播,概念上我們可以先理解為一個 Pub/Sub 機制,各自把自己需求的事件註冊到這個管理中心,接著如果廣播觸發某一事件的時候
所有對應的事件都要被執行,而實際實作面就是透過 EventEmitter 來完成,想看實際的程式碼大概就是如下:

var EventEmitter = require('events').EventEmitter,
    person = new EventEmitter();

person.on('speak', function() {
  console.log('I am here');
});

person.emit('speak');

要解釋 Flux 比較好的方式可能是透過說明每一個組成的局部:

  • Actions - 輔助函式 Helper methods, 單純只是便利我們將資料傳給 Dispatcher
  • Dispatcher - 接收 Actions 以及廣播 payloads 到被註冊的回呼函式(callback)
  • Stores - 應用程式狀態和處理邏輯(即那些被註冊到 Dispatcher 的回呼函式)的容器,
  • Controller Views - 一般來說就是那些負責管理 State ,把狀態透過 props 往下傳遞到子元件的 React 元件

看起來就會像下圖:

怎麼 API 也有關聯?

當你需要處理來自外部的資料,我們發現透過 Actions 來讓資料進入 Flux 流程接著進入 Stores 後續各個流程都將比較方便,是最無痛的方式。

Dispatcher

所以到底什麼是 Dispatcher ?
基本上 Dispatcher 管理著整個流程。他就像是您應用程式的中央集線器,或者要把它比喻成電話總機。Dispatcher 收到 Actions 來的執行動作,接著分派這個動作和資料去給那些註冊的回呼函式。

所以本質上這就只是一個 發佈/訂閱 系統?
不全然是, Dispatcher 會廣播 payload 就是實際要傳遞的資料到所有被註冊的回呼函式包含讓你以特定順序執行回呼的功能,甚至是在執行之前先等更新完成。
記住在您的程式中永遠只有一個 Dispatcher ,只有一個總機小姐XD

接著讓我們來看看程式碼的部分:

var Dispatcher = require('flux').Dispatcher;
var AppDispatcher = new Dispatcher();

AppDispatcher.handleViewAction = function(action) {
  this.dispatch({
    source: 'VIEW_ACTION',
    action: action
  });
}

module.exports = AppDispatcher;

在上面的範例,我們建立了一個 Dispatcher 的實例物件,建立了一個 handleViewAction 的方法。這樣的抽象化有助於區分是 View 觸發 Action 或者 Server/API 觸發 Action。
dispatch method 就是用來廣播 action 和 payload 到回呼函式。這個 action 接著就會觸發 Stores 內實際的行為函式然後更新狀態。
圖解如下

相依性

Dispatcher 模組提供的其中一個最酷的部分就是可以替回呼函式定義相依性和序列化。所以當其中一個函式需要相依於另外一個時,為了適當的輸出,您可以使用 Dispatcher 的 waitFor 方法。
為了利用這個特性,我們需要在 Store 回傳並儲存 Dispatcher 的識別索引,而方式就是透過儲存 register 方法回傳的值。
程式碼範例如下:

ShoeStore.dispatcherIndex = AppDispatcher.register(function(payload) {

});

然後在我們的 Store 裡,當處理一個被分派的 action 時我們就可以使用 waitFor 來確保上面那個回呼函式已經被執行

case 'BUY_SHOES':
  AppDispatcher.waitFor([
    ShoeStore.dispatcherIndex
  ], function() {
    CheckoutStore.purchaseShoes(ShoeStore.getSelectedShoes());
  });
  break;

Stores

在 Flux 中,Stores 針對特定需求管理應用程式的狀態。這基本上是指針對每一個應用程式,stores 管理著資料,接收資料的方法和 dispatcher 的回呼函式。
讓我們來看看 Store 的程式碼:

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ShoeConstants = require('../constants/ShoeConstants');
var EventEmitter = require('events').EventEmitter;
var merge = require('react/lib/merge');

// shoes 物件,用在內部處理資料
var _shoes = {};

// 從 action 載入 shoes 資料
function loadShoes(shoes) {
  _shoes = data.shoes;
}

// 將我們的 store 物件與 Node 的 Event Emitter 合體
var ShoeStore = merge(EventEmitter.prototype, {

  // 取得所有鞋子資料
  getShoes: function() {
    return _shoes;
  },

  emitChange: function() {
    this.emit('change');
  },

  addChangeListener: function(callback) {
    this.on('change', callback);
  },

  removeChangeListener: function(callback) {
    this.removeListener('change', callback);
  }

});

// 註冊 dispatcher callback
AppDispatcher.register(function(payload) {
  var action = payload.action;
  var text;
  // 定義如何處理特定動作 action 
  switch(action.actionType) {
    case ShoeConstants.LOAD_SHOES:
      loadShoes(action.data);
      break;

    default:
      return true;
  }
  
  // 當 action 執行完畢則觸發 change event
  ShoeStore.emitChange();

  return true;

});

module.exports = ShoeStore;

上面程式碼中最重要的一件事就是我們擴充了 store 的功能加入了 EventEmitter。之後我們的 Stores 就可以監聽和廣播事件。
而我們的 View 和元件就可以根據事件來更新,因為 Contriller View 監聽 Stores,利用觸發 change event 就可以讓 Controller View 知道程式的狀態已經變更了該更新狀態。
我們同時也透過 register 方法註冊了一個 Callback 到 AppDispatcher ,這麼一來 Store 就開始監聽 AppDispatcher 的廣播。
上面的 switch 敘述式會決定處理的方式針對對應的 action 然後發出一個廣播,接著觸發 change event 因為 view 監聽著 change event 所以就可以根據事件來更新狀態。

Controller View 就可以透過 getShoes 來檢索 _shoes 裡面的資料,當然這只是一個單純的範例,複雜的邏輯都可以放在這邊。

Action Creators 和 Actions

Action Creators 是一系列方法的集合,我們可以在 View 呼叫他們,透過他們把 action 發送給 Dispatcher. Actions 實際上只是透過 dispatcher 來分派實際資料。
關於 Facebook 如何使用它們 - action 類型常數被用來定義該執行什麼樣的行為,並且包含著資料一起被送出。在註冊的回呼函式裡這些 action 可以根據"類型"被處理,且方法可把 action 中的 data 當作參數。

讓我們來看看關於常數是如何定義的:

var keyMirror = require('react/lib/keyMirror');

module.exports = keyMirror({
  LOAD_SHOES: null
});

上面程式碼,我們使用了 React 的 keyMirror 函式庫,沒錯!您已經猜到了。鏡射我們的 keys 所以我們的值就會直接等於我們的 key.

{ LOAD_SHOES: 'LOAD_SHOES' }

之後我們只要透過觀察這支檔案,我們就能得知關於 ShoeStore 的行為,透過常數幫助我們讓事情更具有組織性,也能讓我們得知關於這個程式實際上在做什麼。

現在我們可以來看看對應的 Action Creator

var AppDispatcher = require('../dispatcher/AppDispatcher');
var ShoeStoreConstants = require('../constants/ShoeStoreConstants');

var ShoeStoreActions = {

  loadShoes: function(data) {
    AppDispatcher.handleAction({
      actionType: ShoeStoreConstants.LOAD_SHOES,
      data: data
    })
  }

};

module.exports = ShoeStoreActions;

在上面的範例中,我們在 ShoeStoreActions 建立了一個方法,它可以呼叫 dispatcher 並傳入我們提供的資料。現在我們就能夠在我們的 view 或者 API 匯入這隻 actions 檔案
透過 ShoeStoreActions.loadShoes(ourData) 把資料和 actionType 傳給 Dispatcher 並廣播。接著 ShoeStore 將會聽到該事件被呼叫而去執行對應的方法來載入鞋子的資料。

Controller Views

Controller Views 就只是 React 元件且該元件正監聽著 change event ,從 Stores 接受應用程式的狀態和資料。當然它可以把資料透過 props 傳遞給子元件。

程式碼看起來便會如下

/** @jsx React.DOM */

var React = require('react');
var ShoesStore = require('../stores/ShoeStore');

// Method to retrieve application state from store
function getAppState() {
  return {
    shoes: ShoeStore.getShoes()
  };
}

// Create our component class
var ShoeStoreApp = React.createClass({

  // Use getAppState method to set initial state
  getInitialState: function() {
    return getAppState();
  },
  
  // Listen for changes
  componentDidMount: function() {
    ShoeStore.addChangeListener(this._onChange);
  },

  // Unbind change listener
  componentWillUnmount: function() {
    ShoesStore.removeChangeListener(this._onChange);
  },

  render: function() {
    return (
      <ShoeStore shoes={this.state.shoes} />
    );
  },
  
  // Update view state when change event is received
  _onChange: function() {
    this.setState(getAppState());
  }

});

module.exports = ShoeStoreApp;

上面的範例中,我們透過 addChangeListener 把對應更新的事件註冊進去,當事件被廣播時元件就可以執行 this._onChange 函式來更新元件的狀態。
應用程式的狀態和資料都被保存在 Stores,所以我們可以使用 Stores 中任何 public 的方法來取得資料或狀態。

結合所有的東西

現在我們已經各別解釋關於 Flux 結構的每個部分,我們應該具備關於這個架構實際上是如何運作的觀念,讓我們更仔細地來看看下面運作的圖示

實作一個 Tabs 元件

複合式(組合)元件

在 React 中任何東西都是元件,就像樂高一樣,你可以用小片的積木組成大塊的,再組合出您想到的東西。
同樣的道理您也可以用許多的小元件(小功能模組)來組合出您的應用程式。所謂的複合式元件或稱作組合元件,
他其實就是由多個元件去組成一個多功能的大元件。

在這篇文章我們要來建立一個 tabs 標簽切換功能的元件,為了達成這個功能我們需要 4 個不同的元件:
<Tabs />, <TabList />, <Tab /><TabPanel /> 分別用來呈現整個 tabs , 列出
標簽列,標簽列的按鈕,以及顯示的內容。結構如下:

 <Tabs>
  <TabList>
    <Tab>Iron man</Tab>
    <Tab>Superman</Tab>
    <Tab>Lucy</Tab>
  </TabList>
  <TabPanel>
    鋼鐵人介紹
  </TabPanel>

  <TabPanel>
    超人介紹
  </TabPanel>

  <TabPanel>
    鹿茸介紹
  </TabPanel>
 </Tabs>

首先是 <Tabs/> 的行為,它被用來當做一個容器,其角色有點像是一個 controller ,因為它必須要掌管所有 DOM 的事件(點擊 Tab 切換至該內容,被選取到的 index)
同時也需要管理 state 看看哪個 <Tab/> 目前正被選取到,所以我們會稱 <Tabs /> 為擁有者元件(owner component)。

我們遭遇到的第一個挑戰是: 元件之間該如何溝通。每一個元件都有一個 state 。每當 state 發生變動,React 就會更新並重新渲染元件以使其跟 state 一致。
當我們選了某個索引後,<Tabs/> 元件就要去更新 <Tab/><TabPanel/>state

典範轉移

首先我們為每一個元件建立一個 API,透過建立一個方法來變更 state。在 React 中一個元件可以透過 this.props.children 去存取子元件。
所以一開始我們理論上只要使用 handleSelected 去設定適當該顯示的子元件如下:

var tabs = this.props.children[0].props.children,
    panels = this.props.children.slice(1),
    index = this.state.selectedIndex;
tabs[index].handleSelected(true);
panels[index].handleSelected(true);

這樣的做法在 v0.10.0 以前的版本是可以運作的,概略的實作如下:

/**
 * @jsx React.DOM
 */


var Tab = React.createClass({
  getInitialState: function () {
    return {selected: false}
  },
  handleSelected: function (status) {
    this.setState({selected: status});
  },
  render: function () {
    var cx = React.addons.classSet;
    var classes = cx({
      'react-tab': true,
      'active': this.state.selected
    });
    return (
      <li className={classes}>
        <a href='#' data-index={this.props.index}>{this.props.children}</a>
      </li>
    );
  }
});

var TabList = React.createClass({
  render: function () {
    return (
      <ul className='react-tab-list'>
        {this.props.children}
      </ul>
    );
  }
});

var Tabs = React.createClass({
  getInitialState: function () {
    return {selectedIndex: 0}
  },
  componentDidMount: function () {
    var tabs = this.props.children[0].props.children,
        panels = this.props.children.slice(1),
        index = this.state.selectedIndex;
    for (i in tabs) {
      if (i == index) {
        tabs[i].handleSelected(true);
        panels[i].handleSelected(true);
      } else {
        tabs[i].handleSelected(false);
        panels[i].handleSelected(false);
      }
    }
  },
  handleClick: function (e) {
    var index = parseInt(e.target.getAttribute('data-index'));
    var tabs = this.props.children[0].props.children,
        panels = this.props.children.slice(1);
    for (i in tabs) {
      if (i == index) {
        tabs[i].handleSelected(true);
        panels[i].handleSelected(true);
      } else {
        tabs[i].handleSelected(false);
        panels[i].handleSelected(false);
      }
    }

  },
  render: function () {
    return (
      <div className='react-tabs' onClick={this.handleClick}>
        {this.props.children}
      </div>
    );
  }
});

var TabPanel = React.createClass({
  getInitialState: function () {
    return {selected: false}
  },
  handleSelected: function (status) {
    console.log(this.props.name + ' selected: ' + status);
    this.setState({selected: status});
  },
  render: function () {
    var cx = React.addons.classSet;
    var classes = cx({
      'react-tab-panel': true,
      'active': this.state.selected
    });
    return (
      <div className={classes}>
        {this.props.children}
      </div>
    )
  }
});

var App = React.createClass({
  render: function () {
    return (
      <div className='container'>
        <Tabs>
          <TabList>
            <Tab name='ironman' index={0}>Iron man</Tab>
            <Tab name='superman' index={1}>Superman</Tab>
            <Tab name='lucy' index={2}>Lucy</Tab>
          </TabList>

          <TabPanel name='panel-ironman'>
          鋼鐵人
          </TabPanel>

          <TabPanel name='panel-superman'>
          超人再起
          </TabPanel>

          <TabPanel name='panel-lucy'>
          露西
          </TabPanel>
        </Tabs>
      </div>
    );
  }
});

React.renderComponent(
  <App />,
  document.getElementById('example')
);

See the Pen oitzv by AndyYou (@AndyYou) on CodePen.

不過到了 React v0.10.0 版本的時候這樣做會出現警告:
Invalid access to component property "setSelected"


到了 v0.11.0 的時候更慘您已經無法直接存取子元件的方法,因為是新版的 React this.props.children 回傳的物件只是描述物件(discriptors)。不再是對應元件的參考物件,且官方建議您不應該直接存取子元件的實際物件。

Component Refs

React 提供一種機制給你取得實際元件的物件,就是使用 refs

var App = React.createClass({
  handleClick: function () {
    alert(this.refs.myInput.getDOMNode().value);
  },

  render: function () {
    return (
      <div>
          <input ref="myInput"/>
          <button
              onClick={this.handleClick}>
              Submit
          </button>
      </div>
    );
  }
});

透過 refs 屬性您就可以取得該子元件的參考

動態的子元件

典型的 refs 使用方式是父元件已經知道子元件的情況,所以可以直接在 tag 中指定 ref 如上面的範例。
不過這次我們希望我們的 Tabs 元件可以動態的放入 <Tab/><TabPanel/>
例如:

App.js
var App = React.createClass({
  render: function () {
    return (
      <div className='container'>
        <Tabs>
          <TabList>
            <Tab name='ironman' >Iron man</Tab>
            <Tab name='superman' >Superman</Tab>
            <Tab name='lucy' >Lucy</Tab>
          </TabList>

          <TabPanel name='panel-ironman'>
          鋼鐵人
          </TabPanel>

          <TabPanel name='panel-superman'>
          超人再起
          </TabPanel>

          <TabPanel name='panel-lucy'>
          露西
          </TabPanel>
        </Tabs>
      </div>
    );
  }
});

而 Tabs 只是動態地把開發者加入的任意結構輸出

Tabs.js
var Tabs = React.createClass({
  render: function () {
    return (
      <div className='react-tabs'>
        {this.props.children}
      </div>
    );
  }
});

因此我們需要一些動態指定 refs 的方法,而這個方法就是透過 cloneWithProps

var App = React.createClass({
  render: function () {
    var index = 0,
        children = React.Children.map(this.props.children, function (child) {
        return React.addons.cloneWithProps(child, {
            ref: 'child-' + (index++)
        });
    });

    return (
        <div>
            {children}
        </div>
    );
  }
});

React 揭秘

關於這篇文章將會試著解釋關於 React 核心的概念。

鳥瞰架構

在傳統的網頁應用程式中,我們如果要增加互動性時勢必廣泛的操作 DOM 元素,一般來說現在最普遍的技術是使用 jQuery:


上圖我們故意讓 DOM 示意為紅色這是因為操作更新 DOM 是需要付出昂貴的代價,也意味著這很吃效能。
很多時候我們會使用 Model 來記錄關於 APP 狀態,不過通常我們最後目標是必須要將狀態呈現給使用者,所以我們必須自己實作這些細節。
這已經是我們很稀鬆平常的開發模式。

而 React 的主要目標就是提供一種不同且更有效率的方式去執行關於操作更新 DOM 這個部分,最終這個方式會取代我們直接操作 DOM 的方法。
React 使用的方式是透過建立一套虛擬 DOM 的機制,React 幫你處理關於操作 DOM 方面的事情。

為什麼多引進一層架構會讓效能增加? 如果在其架構之上多引入一層可以提升速度,這不是暗示瀏覽器並沒有實作最佳的 DOM 操作方式。
這也意味著虛擬 DOM 有著跟實際 DOM 不同的語義和行為。值得關注的是當我們改變虛擬 DOM 時並不能保證立即得到效果。
也因為這個機制導致 React 在實際接觸 DOM 之前必須要等待事件回圈結束。在同一時間它會去計算最小差異並盡可能的用最少的步驟去更新 DOM。

如此一來應用程式便能獨立執行批次更新,套用計算後的差異到實際 DOM 上,任何應用程式如果這麼做那麼都能夠像 React 一樣有效率。
但實際上自己編寫程式碼去做這些任務是很繁瑣且容易出錯,React 的精華之處就是幫你處理掉這些問題。

元件

就上面所提到的虛擬 DOM 的機制有著跟直接操作實際 DOM 不一樣的語義和行為,所以也會有明顯不同的 API。
所謂的元素即在 DOM 結構中的一個節點(node),不過在虛擬 DOM 機制底下一個節點完全是不一樣的東西,我們稱這個節點為元件

使用元件對 React 來說是一件非常重要的事情,因為元件的設計概念是要拿來做計算的,就是計算和實際 DOM 的差異。
比起計算整個結構的差異,React 透過虛擬 DOM 將使得實際執行的時間複雜度大幅下降。

為了理解為什麼? 我們必須深入探討元件的設計,就從 Hello World 範例:

/** @jsx React.DOM */
var HelloMessage = React.createClass({
  render: function() {
    return <div>Hello {this.props.name}</div>;
  }
});

React.renderComponent(<HelloMessage name="Andy" />, mountNode);

上面這段程式碼出現了一些可怕的東西,且在這個階段無法完全說明清楚。即使是這麼小的一段範例都包含著一個很強大的概念,所以在這邊我們將會花些時間慢慢一點一點說明。

這個範例建立了一個 React 元件的類別(class): HelloMessage,然後透過 renderComponent() 在虛擬的 DOM 的機制中建立一個元件(<HelloMessage />, 本質上它就是 HelloMessage 類別實例化的物件,同時也是一個虛擬的 DOM)
最後把這個物件裝到真實的 DOM 元素(mountNode)。

首先是需要注意的事情是 React 的虛擬 DOM 通常來自您在應用程式中客制的元件(在這個例子是 <HelloMessage>)。這是一個意義重大的新嘗試,從內建的 DOM 分離出來。
DOM 通常不帶有任何程式邏輯,就只是一個被動的資料結構,且讓我們能夠附加處理事件。換句話說 React 的虛擬 DOM 是透過特定程式中的元件所創造的,且能夠加入程式中的特定 API 及內部邏輯。
這樣的方式比起直接修改操作 DOM ,例如: 使用 jQuery 的方式,這種建置 View 的方法是一種全新的抽象化方式與框架。

值得一提的是: 如果您一直持續關注 HTML 你也許知道關於 HTML 也許很快的也能自訂 DOM
這將會帶給 DOM 類似的功能: 定義特定程式使用的 DOM 元素,不過 React 並不需要等到官方和瀏覽器完全實作這件事,因為虛擬 DOM 並不是真的 DOM。這讓 React 搶先在自訂元素與 Shadow DOM
這些功能實作普及之前您就能先用了。

回到我們的範例,我們已經建立了一個叫做 <HelloMessage> 的元件並且掛載 mountNode 裡面。
讓我們用圖片來說明初始化幾個部分的情形,首先,我們將虛擬 DOM 與實際 DOM 的關係視覺化,假設 mountNode 是網頁中的 <body> 標簽:

關於掛載(mount)一詞就理解為『對應』,把 a 掛載到 b 上 = 可以把 b 視為 a 。

箭頭表示虛擬元件已經被掛載到原生 DOM 元素中。這段過程非常短,不過也讓我們來看看關於應用程式的視圖部分的邏輯:

這張圖片指的是整個網頁的內容是由我們客制的 <HelloMessage/> 來呈現,那麼關於 <HelloMessage/> 看起來到底長怎樣?

關於元件輸出渲染的部分是透過 render() 去定義欲呈現的元素。React 並沒有確切的說明關於何時或多頻繁的會去執行 render()
只有告訴我們當它注意到有效合法的改變時本身會去執行足夠次數的 render(),無論你回傳什麼樣的 DOM 結構。

在我們這個案例,render() 回傳了一個 <div> 裡面包含了一些內容。React 會執行這個 render() 取得 <div> 然後更新實際的 DOM ,使兩者一致。

不僅僅是更新 DOM,還會幫你記住已經更新的東西。這也是我們待會會提到的關於 React 如何快速的判斷其中差異的方法。
關於 render() 如何回傳 DOM 節點,這邊我先簡略帶過。它是透過 JSX 去定義結構,這是一個非原生的 Javascript,注意雖然它看起來像是 XML。
最後 JSX 會被編譯回 Javascript,看看 JSX 的編譯結果有助我們理解這個架構:

/** @jsx React.DOM */
var HelloMessage = React.createClass({displayName: 'HelloMessage',
  render: function() {
    return React.DOM.div(null, "Hello ", this.props.name);
  }
});

React.renderComponent(HelloMessage( {name:"John"} ), mountNode);

看到了吧!我們真的不是回傳一個 DOM 元素,而是一個等價于 DOM 元素的 React Shadow DOM。所以我們得知 React 回傳的並不是真的 DOM。
您可以理解為標記物件。

狀態與變化

到目前為止,我們忽略了故事中很重要的一段,關於元件可以被改變這件事。如果一個元件不允許被調整修改,那 React 跟 static rendering framework(靜態渲染框架) 也沒啥兩樣,功能就類似于 Mustache 或者 HandlebarsJS
這些樣板引擎,不過 React 的重點就是有效率的更新,要能夠更新元件勢必要允許我們修改一些狀態之類的東西。

React 使用元件的 state 屬性來表示其狀態的資料模型。
關於這點在官方文件的第二個範例就有舉例說明:

/** @jsx React.DOM */
var Timer = React.createClass({
  getInitialState: function() {
    return {secondsElapsed: 0};
  },
  tick: function() {
    this.setState({secondsElapsed: this.state.secondsElapsed + 1});
  },
  componentDidMount: function() {
    this.interval = setInterval(this.tick, 1000);
  },
  componentWillUnmount: function() {
    clearInterval(this.interval);
  },
  render: function() {
    return (
      <div>Seconds Elapsed: {this.state.secondsElapsed}</div>
    );
  }
});

React.renderComponent(<Timer />, mountNode);

React 會在適當的時間點執行回呼函式 getinitialState(), componentDidMount() 以及 componentWillUnmount() ,根據到目前為止的解釋您應該可以清楚地理解這些函式名稱的其含義。
所以我們推測元件和狀態背地裡的行為:

  1. render()stateprops 的一個 function,也就是當它們發現異動會執行 render。
  2. state 只能透過 setState() 去改變。
  3. props 不應該持續變動,只有當其父元素用新的屬性重新輸出時才改變。

(在這之前我們沒有明確地提到 props ,不過他們就是屬性 attributes。當元件要 render 時,它們就來自那些 JSX Tag 中的屬性)

稍早,我們曾經提到 React 會自己執行足夠次數的 render ,意思是除非有需要不然 React 不會執行 render。
那需要什麼?當你發動 setState() 或者父元件重新賦予新的 props,React 就會重新輸出。

現在我們將所有的事情放在一起,用一張圖來說明當程式更新了虛擬 DOM 的資料流(例如: 回應 AJAX 呼叫):

  1. 發動了 AJAX
  2. React 內部需要呼叫 setState 用以改變內部狀態
  3. 因內部狀態改變進而觸發 render
  4. render 執行需要依照生命週期呼叫像 componentWillMount 這類的方法
  5. 最後根據計算的最小差異更新 DOM

從 DOM 取得資料

截至目前為止,我們只有討論到關於收到狀態改變,到如何傳遞到實際 DOM,但實務上我們會需要從 DOM 取得資料,例如從 input 取得使用者輸入的資料。
為了觀察如何運作,我們取用了官方第三個範例:

/** @jsx React.DOM */
var TodoList = React.createClass({
  render: function() {
    var createItem = function(itemText) {
      return <li>{itemText}</li>;
    };
    return <ul>{this.props.items.map(createItem)}</ul>;
  }
});
var TodoApp = React.createClass({
  getInitialState: function() {
    return {items: [], text: ''};
  },
  onChange: function(e) {
    this.setState({text: e.target.value});
  },
  handleSubmit: function(e) {
    e.preventDefault();
    var nextItems = this.state.items.concat([this.state.text]);
    var nextText = '';
    this.setState({items: nextItems, text: nextText});
  },
  render: function() {
    return (
      <div>
        >h3<TODO</h3>
        <TodoList items={this.state.items} />
        <form onSubmit={this.handleSubmit}>
          <input onChange={this.onChange} value={this.state.text} />
          <button>{'Add #' + (this.state.items.length + 1)}</button>
        </form>
      </div>
    );
  }
});
React.renderComponent(<TodoApp />, mountNode);

簡單的說,我們把我們要的行為綁定到 DOM 的事件(在這個範例就是 onChange),接著你在這個事件中呼叫 setState 讓 React 幫您更新介面(實際的 DOM)。
如果您的程式有資料模型,您的事件大概就是透過 setState 更新那個資料模型,React 發現狀態異動就會去更新。
另外如果您曾經用過其他提供雙向資料繫結的框架,看起來可能會懷疑 React 本身在技術上退化了?

儘管這個範例看起來 React 並不是真的把事件加到 <input> 的 "onChange" ,取而代之的是加到文檔層級。讓事件透過汽泡傳遞的機制然後分派他們到正確的虛擬 DOM 元素。
這麼做的好處是包含提升速度(在 DOM 綁定太多處理事件會讓網站變慢),跨瀏覽器實現一樣的行為(處理事件的屬性和其派送的行為並沒有統一的標準,意思是在不同瀏覽器可能有些許的差異)。

所以最後我們可以總結一張完整的圖片來說明關於資料流和事件處理機制:

結論

  • React 是一個處理 View 的函式庫: React 並不強迫您要在 Model 做些什麼設定或改變。一個 React 元件只是一個 View-Level 的概念,而元件的狀態就只是 UI 方面的狀態。您可以繫結任何類型的資料模型或者函式庫到 React(雖然某些資料模型的處理方式會更有效率,例如 Om)
  • React 的元件抽象化在更新 DOM 的方面尤其優秀: 元件抽象化是一個原則,使得我們可以編寫組織良好的架構,同時又提供高效率的更新機制。
  • React 元件從 DOM 的角度執行更新不太方便: 比起函式庫自動傳遞同步資料模型,撰寫事件處理給 React 帶來一種很低階的感覺。
  • React 是抽象漏洞: 意味著 React 有本質上的缺陷,但有提供避免問題發生的方向。大部份的時間你的程式只會和虛擬 DOM 打交道,但有時候你需要直接對 DOM 做些操作。此時您可以查閱手冊的這部分

在 codepen.io 上使用 React

為了能夠在 CodePen 上使用 React 和 JSX 您必須要:

  1. 加入這支 script 到 CodePen http://codepen.io/chriscoyier/pen/yIgqi.js
  2. React: http://fb.me/react-0.11.1.js
  3. JSX Transformer: http://fb.me/JSXTransformer-0.11.0.js


緣由

React 是一個由 Facebook 團隊所提供的一組 Javascript 函式庫。
當您開始使用 CodePen 撰寫一些 React 範例時會發現 CodePen 無法正常運作。
這是因為當您在 Javascript 區塊輸入程式碼時,他其實只是在您的文件上加上一個
<script> 標簽且並沒有定義任何 type
所以當您想使用 CodePen 轉寫一些小範例時您有幾種選擇:

  1. 不使用 JSX,使用類似像 React.DOM.div 之類的原生 JS 取代
  2. 將 JS 寫在 html 區塊,並且使用 <script type='text/jsx'>
  3. 使用上述的方式加入一段 script

關於上面這一小段程式碼是由Mark Funk所提出的一個解法。

RunScript.js
(function() {
  function runScripts() {
    var bodyScripts = 'body script:not([src])';
    Array.prototype.forEach.call(document.querySelectorAll(bodyScripts), function setJSXType(element) {
      element.setAttribute('type', 'text/jsx');
    });
  };

  if (window.addEventListener) {
    window.addEventListener('DOMContentLoaded', runScripts, false);
  } else {
    window.attachEvent('onload', runScripts);
  }
})();

簡單來說這段程式碼會尋找 CodePen 放置到預覽中的 script 標簽並且加入 type
附帶一提的是,JSX Transformer 是用來協助您方便開發的並不適用于發佈的產品上。
最後,當您發生錯誤時請檢查您瀏覽器的 console,JSX Transformer 會很貼心的
提示您錯誤訊息。

這篇文章為官方部落格的文章隻翻譯。原文

以觀念來說React 是一種使用 Javascript 來快速建立大型 Web 的方式,它非常容易擴展,且官方已將其使用在 Facebook 與 Instagram 上。

其中最好的部分就是 React 讓您在建立程式時重新思考關於應用程式。在這篇文章,將會引導您使用 React 完成一個可以搜尋過濾產品資料的範例。

從模擬架構開始

想像我們已經有了一個 JSON 的 API 以及一個設計師模擬的草圖。我們的設計師顯然不是很優,因為他的模擬像這樣:

而我們的 JSON API 傳回來的資料長得像這樣:

[
  {category: "Sporting Goods", price: "$49.99", stocked: true, name: "Football"},
  {category: "Sporting Goods", price: "$9.99", stocked: true, name: "Baseball"},
  {category: "Sporting Goods", price: "$29.99", stocked: false, name: "Basketball"},
  {category: "Electronics", price: "$99.99", stocked: true, name: "iPod Touch"},
  {category: "Electronics", price: "$399.99", stocked: false, name: "iPhone 5"},
  {category: "Electronics", price: "$199.99", stocked: true, name: "Nexus 7"}
];

第一步:拆解 UI 為元件階層結構

您即將要做的第一個步驟是根據模擬的 UI 畫出每一個元件(包含子元件)的階層方塊並且給予名稱,如果你正在與設計師一起工作那可能你已經完成這個任務。
看看他們的 Photoshop 中圖層的名稱大略就是你 React 元件最後的名稱。
不過我們怎麼知道哪個部分應該是元件?當您建立一個新的函式或物件,您可以根據單一職責原則,指的是每一個元件理想的情況下
應該只做一件事。如果該元件的功能不斷增加那就應該再把它拆解,並建立更小的子元件。

常見的需求是 - 顯示 JSON 的資料給使用者。根據過去的經驗,您會發現如果您的資料模型(Model)建立的正確,您的 UI (同時表示您的元件結構)就可以輕鬆的將資料呈現給使用者。
其原因是使用者界面和資料模型往往遵循一樣的資料結構,意味著其實將 UI 獨立為元件並非很困難。
就只是根據每一個小區塊需要呈現的資料模型去分解成個別的元件。

看到上圖,這個應用程式將會有 5 個元件,下面的斜體字表示每一個元件對應的模型

  1. FilterableProductTable (橘色) 用來組織包含其他子元件,即這個元件的最上層的容器。
  2. SearchBar (藍色) 取得 使用者輸入的搜尋條件。
  3. ProductTable (綠色) 根據 使用者輸入的搜尋條件 顯示過濾後的資料列表。
  4. ProductCategoryRow (青色) 顯示 分類 標題。
  5. ProductRow (紅色) 顯示每一個 產品

如果您認真觀察 ProductTable 你會看到表格還有標題列(即 NamePrice 欄位名稱那邊)並沒有被規劃為獨立元件。這只是偏好問題。
根據這個範例,我們規劃這個區塊為 ProductTable 的一部份,這是因為輸出產品列表資料是 ProductTable 的責任,當然包含欄位名稱,且目前看來它的工作很單純並不需要再拆出一個元件。
然而如果這個標題列變得越來越複雜(舉例來說:如果我們需要增加排序功能),如此一來增加一個 ProductTableHeader 元件會是比較好的做法。

現在我們已經定義好關於這個模擬的元件架構,讓我們重新組織成一個階層圖,這樣我們就能清楚看出元件的主從關係。

  • FilterableProductTable
    • SearchBar
    • ProductTable
      • ProductCategoryRow
      • ProductRow

第二步:建立一個靜態版本的 React 元件

/** @jsx React.DOM */

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        });
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    render: function() {
        return (
            <form>
                <input type="text" placeholder="Search..." />
                <p>
                    <input type="checkbox" />
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    render: function() {
        return (
            <div>
                <SearchBar />
                <ProductTable products={this.props.products} />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

React.renderComponent(<FilterableProductTable products={PRODUCTS} />, document.body);

現在您已經有了元件的階層結構。該是時候實作程式的功能了。
在學習 React 的過程中,我們推薦最簡單的方式是一開始只要建立一個能取得資料模型和渲染出 UI 畫面但是不能互動的版本。
拆成這些步驟是因為建立靜態版本通常是需要打很多字且不太需要思考,而建立互動機制需要您仔細的構思,如此一來在建立元件時比較不會出錯。

在建立靜態版本的時候通常你會思考關於元件 重複使用 的機會以及該怎麼透過 props 從父元素傳入資料。如果您已經熟悉關於 state 的觀念,就會知道在建立靜態版本時根本不應該使用 state
state 是互動時才會需要用到的功能,所謂的互動指的是當資料變動,而 UI 也需要對應更新。由於這只是靜態版本所以根本不需要用。

您可以由底層往上或者由上而下撰寫您的元件,意思是說你可以選擇從結構中最外層的元件開始建起(即從 FilterableProductTable)開始,或者從最內部的子元件開始(ProductRow)。
在單純的範例中,通常從上至下相對快速,而如果專案較大,通常從下而上會比較推薦,因為也同時方便您撰寫測試,可以逐步測試元件是否正常。

在這一步的最後,您會得到一個可重複使用元件的函式庫,你可以用它來輸出呈現你的資料模型,以確認 UI 的呈現是否有誤。
不過這個元件只有 render() 方法,因為截至目前為止它還只是靜態版本。

元件的最上層(FilterableProductTable)將會取得資料模型,透過屬性傳入資料。如果你修改了 Model 的資料且再次執行 renderComponent() 你應該會看到資料更新了。
這讓你可以清楚地觀察這個元件是怎麼更新資料,這就是 React 透過 one-way data flow (或稱 one-way binding) 單向數據流的方式去保持所有資料一致,同時也方便模組化。

簡易補充: props vs state

在 React 裏有兩種類型的 Model 就是你放資料的地方:propsstate。理解他們的區別非常重要,如果您還不懂他們之間的差別請閱讀官方或者這篇文章

第三步:定義最少但完整的 UI 狀態

為了讓您的 UI 俱有互動性,你可能會需要讓資料模型做些修改,接著 UI 根據 one-way binding 更新資料。
React 透過使用 state 讓這一切變得很簡單。您可以把 state 想是讓您存放動態資料的地方,而當資料有所變動,React 會自動呼叫 render() 執行 UI 的更新。

而要讓建立的程式能夠正確執行,首先需要思考關於這個程式最少需要哪些可變動的狀態,只有變動的資料才需要放到 state
關鍵的原則是 DRY (Don't Repeat Yourself) ,不重複原則。找出程式在特定需求內必須要的最少狀態。例如:如果你要建立一個 TODO List,其實你就只要一個陣列包含待辦清單的項目。
當你需要計算項目總數時,不需要在 state 中的儲存另一個變數,而是單純使用陣列取得數量即可。

思考我們這個範例中各種取得的資料

  • 所有產品的列表
  • Search input 的搜尋條件
  • checkbox 是否有被選取的值
  • 過濾後的清單

讓我們一個一個討論看看誰是屬於 state 。簡單的思考關於這三個問題

  1. 資料是透過 props 從父元素傳進來的嗎?如果是,這可能不屬於 state 。
  2. 這資料會隨著時間推移而改變嗎? 如果不是,那它應該不屬於 state 。
  3. 你能從現有任何 state 或者 props 計算出這個資料嗎?如過是!那這肯定不屬於 staet。

產品列表是透過 props 傳遞進來的,所以這不應該存在 state,當然有經驗的開發者會說這通常從資料庫來,但這個範例不是。
搜尋條件和 checkbox 似乎是狀態,因為他們會改變。而且是不能透過計算得到的。
最後過濾後的清單也不該儲存在 state ,因為他是可以被計算出來的,根據我們拿到的過濾條件去運算。
所以最後我們歸納出應該被放在 state 的有:

  • 搜尋條件
  • checkbox 的值

第四步:應該在何處使用 state

/** @jsx React.DOM */

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
                return;
            }
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        }.bind(this));
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    render: function() {
        return (
            <form>
                <input type="text" placeholder="Search..." value={this.props.filterText} />
                <p>
                    <input type="checkbox" value={this.props.inStockOnly} />
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    getInitialState: function() {
        return {
            filterText: '',
            inStockOnly: false
        };
    },

    render: function() {
        return (
            <div>
                <SearchBar
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
                <ProductTable
                    products={this.props.products}
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

React.renderComponent(<FilterableProductTable products={PRODUCTS} />, document.body);

OK 我們已經決定了這個元件程式最少的 state,下一步我們需要定義哪些元件是要變動的,以及是哪些要使用 state
記住!React 提供的是一種單向資料流的結構,通常是由上而下。剎那間可能不太輕易判斷哪個元件該管理或使用 state,這通常也是初學者最難理解的部分。
請跟著下面這些規則去推敲:

思考程式中需要使用到 state 的部分

  • 找出哪些元件需要根據 state 輸出不同的結果
  • 找出共同的擁有者元件(最上層的元件通常需要管理 state)
  • 如果你不能找出某一個元件該擁有狀態的理由,那就建立一個新的元件用來管理狀態,並且加在共同擁有者元件之上。

讓我們應用這些規則在這個範例上

  • ProductTable 需要根據 state 過濾產品列表以及 SearchBar 需要顯示搜尋條件的值和 checkbox 的狀態。
  • 共同擁有者元件是 FilterableProductTable
  • 把過濾條件和 checkbox 值都放在 FilterableProductTable 在概念上也是合理的。

所以我們決定 state 應該放在 FilterableProductTable ,首先加上 getInitialState() 方法,讓它回傳一個物件 {filterText: '', inStockOnly: false}
這是用來初始化 state 的,接著傳入 filterTextinStockOnlySearchBar 當作屬性,最後在 ProductTable 中使用這些屬性值去過濾,並且設定 form 的值。

現在你可以看到您的應用程式俱有這些行為:在 state 中把 filterText 的值設成 ball 然後資料就會更新。

註:先別急著操作網頁上的 form。

第五步:加入反向數據流

/** @jsx React.DOM */

var ProductCategoryRow = React.createClass({
    render: function() {
        return (<tr><th colSpan="2">{this.props.category}</th></tr>);
    }
});

var ProductRow = React.createClass({
    render: function() {
        var name = this.props.product.stocked ?
            this.props.product.name :
            <span style={{color: 'red'}}>
                {this.props.product.name}
            </span>;
        return (
            <tr>
                <td>{name}</td>
                <td>{this.props.product.price}</td>
            </tr>
        );
    }
});

var ProductTable = React.createClass({
    render: function() {
        console.log(this.props);
        var rows = [];
        var lastCategory = null;
        this.props.products.forEach(function(product) {
            if (product.name.indexOf(this.props.filterText) === -1 || (!product.stocked && this.props.inStockOnly)) {
                return;
            }
            if (product.category !== lastCategory) {
                rows.push(<ProductCategoryRow category={product.category} key={product.category} />);
            }
            rows.push(<ProductRow product={product} key={product.name} />);
            lastCategory = product.category;
        }.bind(this));
        return (
            <table>
                <thead>
                    <tr>
                        <th>Name</th>
                        <th>Price</th>
                    </tr>
                </thead>
                <tbody>{rows}</tbody>
            </table>
        );
    }
});

var SearchBar = React.createClass({
    handleChange: function() {
        this.props.onUserInput(
            this.refs.filterTextInput.getDOMNode().value,
            this.refs.inStockOnlyInput.getDOMNode().checked
        );
    },
    render: function() {
        return (
            <form>
                <input
                    type="text"
                    placeholder="Search..."
                    value={this.props.filterText}
                    ref="filterTextInput"
                    onChange={this.handleChange}
                />
                <p>
                    <input
                        type="checkbox"
                        value={this.props.inStockOnly}
                        ref="inStockOnlyInput"
                        onChange={this.handleChange}
                    />
                    Only show products in stock
                </p>
            </form>
        );
    }
});

var FilterableProductTable = React.createClass({
    getInitialState: function() {
        return {
            filterText: '',
            inStockOnly: false
        };
    },

    handleUserInput: function(filterText, inStockOnly) {
        this.setState({
            filterText: filterText,
            inStockOnly: inStockOnly
        });
    },

    render: function() {
        return (
            <div>
                <SearchBar
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                    onUserInput={this.handleUserInput}
                />
                <ProductTable
                    products={this.props.products}
                    filterText={this.state.filterText}
                    inStockOnly={this.state.inStockOnly}
                />
            </div>
        );
    }
});


var PRODUCTS = [
  {category: 'Sporting Goods', price: '$49.99', stocked: true, name: 'Football'},
  {category: 'Sporting Goods', price: '$9.99', stocked: true, name: 'Baseball'},
  {category: 'Sporting Goods', price: '$29.99', stocked: false, name: 'Basketball'},
  {category: 'Electronics', price: '$99.99', stocked: true, name: 'iPod Touch'},
  {category: 'Electronics', price: '$399.99', stocked: false, name: 'iPhone 5'},
  {category: 'Electronics', price: '$199.99', stocked: true, name: 'Nexus 7'}
];

React.renderComponent(<FilterableProductTable products={PRODUCTS} />, document.body);

到上面為止你會發現除非你手動更改 state 的設定,如果你在網頁的表單上輸入任何東西,input 完全沒反應。這是因為 React 是單向數據流的模式。
現在讓我們補上其他方向來的數據,由於表單元件在這個結構的內部,而我們只能用 FilterableProductTable 去更新 state
React 使得數據流非常明確,清楚易懂,歸納的結論就是更新 state 和資料操作請在 owner 擁有者元件裡作,而當子元件的觸發的行為需要更新數據時還是拿父元件的方法。
不過這個方式的缺點就是你需要多打一些字,相較于 two-way binding。雖然 React 也提供一個擴充套件叫做 ReactLink 它可以協助您快速做到 two-way binding,不過這篇文章是用來說明整個
React 基礎的觀念,所以我們不打算在這篇提太多額外的東西以免造成混淆。

如果您是著輸入一些條件或者勾起 checkbox 在這上一版的程式碼,您會看到 React 忽略您的輸入。這是故意的,因為我們已經設定 input 的 value 是 this.state.filterText ,他就要確保永遠等於這個參考
讓我們來想想我們希望怎樣,我們希望確保使用者輸入的任何改變都是去更新 state ,而 input 則一樣從 state 取得資料。

FilterableProductTable 就要把修改 state 的函式傳給 SearchBar,如此一來當 input 觸發 onChange 時才能變更 state。
雖然這樣聽起來好像會多了不少程式碼,但這能確保資料流向是非常清楚的。

最後,就這樣而已

希望這篇文章能夠使您理解關於 React 如何建立元件和應用程式的觀念。
雖然它比起你現在的框架或程式碼的確讓你多打了一些字,不過記住讀程式碼遠遠比撰寫還要困難,而這麼做會讓你的程式碼模組化且非常容易閱讀。

Function.prototype.bind 函式繫結大概是當您開始學習 Javascript 時最後關注到的議題。
通常是當您遇到一種狀況:需要在其他 Function 保留 this 的執行環境(Context)。
講執行環境可能太抽象,舉例來說就是當您需要在函式的另外一個函式中呼叫 this.action() 的時候。
(這邊如果看不懂請耐著性子看下去)
不過通常這時您可能也不知道您需要的就是 Function.prototype.bind()

第一次您遇到上述的問題,您可能會傾向于把 this 儲存成一個變數,接著即便您切換了 Context 還是可以參考到這個物件。
如果您看不懂上面在說什麼,請先參考這篇文章
許多人會採用 self, _this 或者 context 當作變數名稱,並且把 this 放進去。
這些方法都是可行的且並沒有什麼不妥,但有一個更不錯的方式。

我們實際上要解決的問題是?

下面有一份簡單的範例程式碼,情況是某人忘記把 Context 存成一個變數:

/**
 * 我們舉例一個機器人物件,機器人有一些基本的 function 來執行動作
 * 不過問題是當我們要求機器人執行動作的時候,他需要先到確定還有沒有能量。
 * 
 *
 * Note: 這段程式碼只是希望能夠用具像化一點的比喻來說明。
 */
var Robot = {
  /** private */
  power: 100,

  walk: function () {
    console.log('Robot walked');
    this.power -= 10;
  },

  fly: function () {
    console.log('Robot flied');
    this.power -= 20;
  },

  check: function (excute) {
    if (this.power > 0)
      excute();
  },
  
  /** public */
  showoff: function () {
    this.check(function () {
      this.walk(); /* 實際執行的動作。 */
      this.fly();
    })
  }
}

Robot.showoff();

如果照著上面把實際要執行的動作當作 callback 傳給 check(),當您要再次呼叫 this.walk() 的時候
就會發現出現錯誤訊息

TypeError: Object #<Object> has no method 'walk'

這是因為我們再次傳進去的匿名函式不知道關於 this 的東西,在這裏我們並沒有善用閉包來保存 Context。
而對很多人可能就會把上面的範例修改為如下

var Robot = {
  /** private */
  power: 100,

  walk: function () {
    console.log('Robot walked');
    this.power -= 10;
  },

  fly: function () {
    console.log('Robot flied');
    this.power -= 20;
  },

  check: function (excute) {
    if (this.power > 0)
      excute();
  },
  
  /** public */
  showoff: function () {
    var that = this;
    this.check(function () {
      that.walk(); /* 實際執行的動作。 */
      that.fly();
    })
  }
}

Robot.showoff();

宣告成區域變數之後,閉包就會幫助我們 Keep 這個 Context,這也是相對直覺的方式,同上面說的這沒有任何不妥。
不過我們知道了一件事,就是我們需要保存 Robot 這個物件參考的 Context,給 Callback 即範例中的 excute。
當我們呼叫 that.walk() 的時候其實就是在使用閉包。根據 MDN 說明,其實閉包就是一個特殊的物件,它有兩個含義:

  1. 它是一個 function。
  2. 它產生了一個 Context ,概略的說就是幫你記錄上一層有宣告的變數。

這裏就不詳細說明關於閉包,不理解的推薦這篇文章這篇

閉包的方式已經可以運作了,但是我們覺得他不夠漂亮,因此我們就來使用 Function.prototype.bind()

讓我們來重構上面的程式範例

var Robot = {
  /** private */
  power: 100,

  walk: function () {
    console.log('Robot walked');
    this.power -= 10;
  },

  fly: function () {
    console.log('Robot flied');
    this.power -= 20;
  },

  check: function (excute) {
    if (this.power > 0)
      excute();
  },
  
  /** public */
  showoff: function () {
    // var that = this;

    this.check(function () {
      this.walk();
      this.fly();
    }.bind(this));
  }
}

Robot.showoff();

我們剛剛做了什麼??

當我們呼叫了 .bind() 的時候,其實它非常單純的建立了一個新的 function,只不過這個 function 把 this 的值綁定進去。
所以我們同時把我們想要的 Context(即 Robot 物件) 給保存了下來。接著當我們的回呼函式在執行的時候 this 就是參考到 Robot 這個物件。

如果你有興趣了解 Function.prototype.bind() 內部運行機制,他看起來大概就像下面這樣

Function.prototype.bind = function (scope) {
    var fn = this;
    return function () {
        return fn.apply(scope);
    };
}

接著我們再來看看一個非常簡單的案例:

var foo = {
    x: 3
}

var bar = function () {
  console.log(this.x);
}

bar(); // undefined


var boundFunc = bar.bind(foo);

boundFunc(); // 3

這個範例是說,bind() 幫我們建立了一個新的 function ,並且當我們執行時這個 function 的 this 是指向 foo,而不是全域。
如果您還是不清楚可以大略理解為:把 function 掛到某個物件底下(當然不是真的加進去),只是這樣一來可以透過 this 取得該物件的 Context。

實務應用

當我們學習某些東西時,我不只需要理解觀念,也會試著將其套用在實務上以驗證自己是否明白。來看看一些實務上的應用吧!

Click 事件處理

其中一個用途是拿它來追蹤點擊次數,然後可能是要把它存在某個物件裡類似下面這樣

var logger = {
  x: 0,
  increment: function () {
    this.x++;
    console.log(this.x);
  }
}

然後指派一個按鈕的 Click 處理函式去呼叫 logger 物件

document.querySelector('button').addEventListener('click', function () {
  logger.increment();
});

不過上面這種做法,我們已經建立了一個不必要的匿名函式,並且因為這個匿名函式使用了 logger 呼叫了 increment() 所以產生了一個閉包用以確保了 this 是正確的參考物件。
不明白!?再看看下面這個最根本的寫法吧

document.querySelector('button').addEventListener('click', logger.increment); // NaN

原本是這樣的,當你 Click 的時候執行一個 function ,不過如果你用上面這種寫法的話,意思是你只是把 function 傳進去,根據 this 的定義他其實是指的是誰(哪個物件)呼叫這個函式 this 就是指向它。
而這裡呼叫的人根本不是 logger 這個物件。也因此使用了一個匿名函式,建立了一個閉包,就是為了保留住 logger 物件的狀態。

好了!講完上面這些我們來看看更乾淨的寫法:

document.querySelector('button').addEventListener('click', logger.increment.bind(logger)); 

現在,對於 bind() 應該比較不陌生了吧!本篇是在寫 React 時產生了一點疑問所研究的筆記,因為在 React 中蠻多機會的使用 bind
如官方的範例

componentDidMount: function() {
  $.ajax({
    url: this.props.url,
    dataType: 'json',
    success: function(data) {
      this.setState({data: data});
    }.bind(this),
    error: function(xhr, status, err) {
      console.error(this.props.url, status, err.toString());
    }.bind(this)
  });
}

這篇文章除了記錄一下最後只是堪用的解法,如果有高手知道更好的做法煩請賜教。

話說小弟今天要來寫一些 React 的測試專案,就想說那就搭上 Gulp 吧!原本的目的很單純就只是用 Gulp compile 一些常用的 meta language,像是 Less, Jade 外加上 Jsx。也不是什麼很大的專案所以架構上其實就是在 React 的 StartKit上面再開個自己的測試目錄。

一開始也都用的很開心,然後就發現:在啟動 server 支援 livereload 後,當從 src 目錄刪除檔案,dist 目錄的檔案竟然沒被刪掉!!!(好啦!我知道正確來說應該是要先輸出到 .tmp 在 copy)。
說明一下我想完成的目標就是:

  • 一個簡單的 Server 支援 watch & livereload。
  • 存檔後直接把 src 底下的檔案編譯至對應的 dist 目錄下。
  • 接著我希望 dist 目錄下的檔案要正確的對應,意思是如果我新增/刪除一個檔案,那 dist 也要新增/刪除。

直覺反應這也不是什麼大問題,那就補上 clean 的套件就好,一開始沒注意到 gulp-clean 有非同步的小問題因為我是要刪 dist 而不是 src 所以直覺得拆成兩個 task,想說我任務都有照順序先 clean 在 compile 為什麼一下噴 Error,一下又正常,再加上一開始不想要用全部清掉這種方式。

於是就讓我 Google 到這一篇 Delete feature request ,因為下面有人提到使用 gulp-filter 的方式,接著我就將 watch 的 task 部分換成

gulp.task('default', function () {
    watch('css/**/*.css').pipe(gulp.dest('./dist/'));
});

這種寫法,並補上 filter ,本來以為要打完收工的時候,卻發現我對 Node 很多東西觀念太薄弱,我不會替 vinyl-fs 物件綁上 event,也不知道怎麼根據 pipe() 來的檔案資訊來切換目錄,且有人提到可以用 gaze 的方式我試了半天也宣告失敗。附帶一提當你使用上面這種 watch 的寫法時其實 log 的資訊比較清楚。

最後差強人意的 gulpfile 在下面,最後如果有興趣要測的可以用 Github 測試環境在 playground 目錄下

var gulp = require('gulp'),
    connect = require('gulp-connect'),
    less = require('gulp-less'),
    react = require('gulp-react'),
    watch = require('gulp-watch'),
    jade = require('gulp-jade'),
    clean = require('gulp-clean');


/**
* Compilers
*/
gulp.task('less', ['clean-css'], function () {
  gulp.src('playground/src/styles/less/*.less')
      .pipe(less())
      .pipe(gulp.dest('playground/dist/css'));
});


gulp.task('clean-css', function () {
  gulp.src('playground/dist/css/*', {read: false}).pipe(clean({force: true}));
});


gulp.task('jsx', ['clean-js'], function () {
  gulp.src('playground/src/scripts/jsx/*.jsx')
      .pipe(react())
      .pipe(gulp.dest('playground/dist/js/'));
});


gulp.task('clean-js', function () {
  gulp.src('playground/dist/js/*', {read: false}).pipe(clean({force: true}));
});


gulp.task('jade', ['clean-html'], function () {
  gulp.src('playground/src/templates/**.jade')
      .pipe(jade())
      .pipe(gulp.dest('playground'));
});


gulp.task('clean-html', function () {
  gulp.src('playground/*.html', {read: false}).pipe(clean({force: true}));
});


/*********************************************************/


/**
* Web Server
*/
gulp.task('server', function () {
  connect.server({
    root: ['playground'],
    livereload: true
  });
});


gulp.task('livereload', function () {
  watch(['playground/*.html', 'playground/dist'])
      .pipe(connect.reload());
});


gulp.task('watch', function () {
  gulp.watch('playground/src/styles/less/*.less', ['less']);
  gulp.watch('playground/src/scripts/jsx/*.jsx', ['jsx']);
  gulp.watch('playground/src/templates/**.jade', ['jade']);
});


/*********************************************************/

/**
* Mixin feature of usage
*/
gulp.task('default', ['less', 'jsx', 'jade', 'server', 'livereload', 'watch']);