React 概念模型——脫離React談談它的設計思想

王下邀月熊_Chevalier發表於2016-05-19

本文翻譯自react-basic,本文從屬於筆者的Web 前端入門與最佳實踐

在正式學習React之前,我們希望能脫離React本身來了解下React的設計思想,這有助於我們更好地運用React與進行更好地架構設計。當然,這裡討論的一些設計理念肯定還是有爭論的,見仁見智,各有所感。React.js本身的學習與實現是偏重於工程解決方案、演算法優化、程式碼相容以及除錯工具這些方法論,不過,這些都是會隨著時間以及應用長久的變遷發生改變,唯有設計思想能夠綿延流長。術道相濟,方能長久。

Transformation(轉換)

React的核心前提即是改變了jQuery這種以DOM操作為核心到以資料流驅動為核心,View是不同的資料的投射。並且對於資料的處理函式應該是純函式,即相同的輸入有相同的輸出而不會產生其他副作用:


function NameBox(name) {
  return { fontWeight: `bold`, labelContent: name };
}

`Sebastian Markbåge` ->
{ fontWeight: `bold`, labelContent: `Sebastian Markbåge` };

這樣可以極大地方便對於View建構函式的重用與單元測試等。

Abstraction(抽象)

對於一個複雜的UI,肯定不能全都塞到一個函式裡處理,這就是React另一個重要的思想,將UI抽象拆分為多個可重用的部分,並且各個部分要對上層隱藏實現細節,便如下面這樣的進行函式呼叫:


function FancyUserBox(user) {
  return {
    borderStyle: `1px solid blue`,
    childContent: [
      `Name: `,
      NameBox(user.firstName + ` ` + user.lastName)
    ]
  };
}
{ firstName: `Sebastian`, lastName: `Markbåge` } ->
{
  borderStyle: `1px solid blue`,
  childContent: [
    `Name: `,
    { fontWeight: `bold`, labelContent: `Sebastian Markbåge` }
  ]
};

Composition(組合)

為了達到真正意義上的重用目標,並不僅僅就是把那個葉子元件組合進一個新的容器,我們也需要在容器中構建出能夠組合其他抽象元件的抽象元件。這裡我認為的組合要點在於如何把兩個或者更多的抽象元件合併成一個新的:




function FancyBox(children) {
  return {
    borderStyle: `1px solid blue`,
    children: children
  };
}

function UserBox(user) {
  return FancyBox([
    `Name: `,
    NameBox(user.firstName + ` ` + user.lastName)
  ]);
}

State

一個UI並不僅僅是服務端或者業務邏輯的重現,實際上有很多特定的狀態會被投射到UI上。譬如,如果你正在輸入一個文字框,這個並不會複製到其他的Tab或者你的手機瀏覽器中。另外,滾動位置也是一個典型的你並不想投射到其他地方的狀態。我們希望我們的資料模型會更加地固定,因此,我們從頂部元件開始將更新函式一級一級地注入到實際的顯示的那個模組上。




function FancyNameBox(user, likes, onClick) {
  return FancyBox([
    `Name: `, NameBox(user.firstName + ` ` + user.lastName),
    `Likes: `, LikeBox(likes),
    LikeButton(onClick)
  ]);
}

// Implementation Details

var likes = 0;
function addOneMoreLike() {
  likes++;
  rerender();
}

// Init

FancyNameBox(
  { firstName: `Sebastian`, lastName: `Markbåge` },
  likes,
  addOneMoreLike
);

注意,這裡的例子還是用了帶副作用的函式來更新狀態,不過我本意是想採用純函式,即每次返回最新的狀態來完成這個工作。我會在下面的例子裡闡述這個觀點。

Memoization

純函式的一個好處就是其結果是可以快取的,這就避免了重複呼叫帶來的效能浪費。我們可以建立一個自帶快取的函式來記錄最後呼叫的引數與返回值,這樣我們可以自動地在相同引數的情況下直接返回:




function memoize(fn) {
  var cachedArg;
  var cachedResult;
  return function(arg) {
    if (cachedArg === arg) {
      return cachedResult;
    }
    cachedArg = arg;
    cachedResult = fn(arg);
    return cachedResult;
  };
}

var MemoizedNameBox = memoize(NameBox);

function NameAndAgeBox(user, currentTime) {
  return FancyBox([
    `Name: `,
    MemoizedNameBox(user.firstName + ` ` + user.lastName),
    `Age in milliseconds: `,
    currentTime - user.dateOfBirth
  ]);
}

Lists

大部分的UI元件都是會包含著列表,每一行會顯示不同的值。我們需要維護一個Map來記錄列表中每個專案的狀態資訊:




function UserList(users, likesPerUser, updateUserLikes) {
  return users.map(user => FancyNameBox(
    user,
    likesPerUser.get(user.id),
    () => updateUserLikes(user.id, likesPerUser.get(user.id) + 1)
  ));
}

var likesPerUser = new Map();
function updateUserLikes(id, likeCount) {
  likesPerUser.set(id, likeCount);
  rerender();
}

UserList(data.users, likesPerUser, updateUserLikes);

注意,在這個函式裡我們傳入了多個值,這樣就不能快取結果了。

Continuations

B狗的事情發生了,因為存在著很多的列表,我們也需要維護很多的模板,不同的列表顯示的資料有交集有差異,譬如使用者列表和你關注的使用者列表,它們可能就是操作按鈕上的不同。我們可以將部分模板和業務邏輯解耦合以下,譬如使用柯里化這種構造高階函式的手段。這種手段本身並不能減少業務邏輯或者最終模板的複雜度,不過能夠將一部分程式碼移出業務邏輯:


function FancyUserList(users) {

  return FancyBox(

    UserList.bind(null, users)

  );

}



const box = FancyUserList(data.users);

const resolvedChildren = box.children(likesPerUser, updateUserLikes);

const resolvedBox = {

  ...box,

  children: resolvedChildren

};


State Map

早前我們就知道著名的23種設計模式裡會避免重複的實現一些通用模式,我們也可以將一些狀態管理的邏輯函式移到統一的初級函式裡,這樣就方便重複使用了:


function FancyBoxWithState(
  children,
  stateMap,
  updateState
) {
  return FancyBox(
    children.map(child => child.continuation(
      stateMap.get(child.key),
      updateState
    ))
  );
}

function UserList(users) {
  return users.map(user => {
    continuation: FancyNameBox.bind(null, user),
    key: user.id
  });
}

function FancyUserList(users) {
  return FancyBoxWithState.bind(null,
    UserList(users)
  );
}

const continuation = FancyUserList(data.users);
continuation(likesPerUser, updateUserLikes);

Memoization Map

上面提到過,當存在多個輸入引數的情況下要再想進行快取就會麻煩一點,我們要使用一些複雜的快取策略來平衡記憶體使用與頻次。幸運的是很多地方View還是比較固定的,整個樹上的相同位置的值一般都是相同的,因此可以用樹結構來進行快取。


function memoize(fn) {
  return function(arg, memoizationCache) {
    if (memoizationCache.arg === arg) {
      return memoizationCache.result;
    }
    const result = fn(arg);
    memoizationCache.arg = arg;
    memoizationCache.result = result;
    return result;
  };
}

function FancyBoxWithState(
  children,
  stateMap,
  updateState,
  memoizationCache
) {
  return FancyBox(
    children.map(child => child.continuation(
      stateMap.get(child.key),
      updateState,
      memoizationCache.get(child.key)
    ))
  );
}

const MemoizedFancyNameBox = memoize(FancyNameBox);

Algebraic Effects

如果我們在一個巢狀多層的UI體系裡每次都把一些引數一級一級的傳遞下去,那約莫是非常麻煩的。因此我們需要創造一些捷徑來在兩個不直接相連的抽象元件之間傳遞資料,而不需要通過中間層。在React裡面叫他Context。(官方文件裡Context還是屬於測試階段)。有時候這個資料依賴的關係並不嚴格按照抽象樹的邏輯,譬如在一個佈局演算法裡你需要知道你的子元素的大小你才能夠完整地決定他們的位置。我在這裡使用 Algebraic Effects 作為 proposed for ECMAScript。



function ThemeBorderColorRequest() { }

function FancyBox(children) {
  const color = raise new ThemeBorderColorRequest();
  return {
    borderWidth: `1px`,
    borderColor: color,
    children: children
  };
}

function BlueTheme(children) {
  return try {
    children();
  } catch effect ThemeBorderColorRequest -> [, continuation] {
    continuation(`blue`);
  }
}

function App(data) {
  return BlueTheme(
    FancyUserList.bind(null, data.users)
  );
}

Further Reading

相關文章