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

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

在正式學習React以前,咱們但願能脫離React自己來了解下React的設計思想,這有助於咱們更好地運用React與進行更好地架構設計。固然,這裏討論的一些設計理念確定仍是有爭論的,見仁見智,各有所感。React.js自己的學習與實現是偏重於工程解決方案、算法優化、代碼兼容以及調試工具這些方法論,不過,這些都是會隨着時間以及應用長久的變遷發生改變,惟有設計思想可以綿延流長。術道相濟,方能長久。react

Transformation(轉換)

React的核心前提便是改變了jQuery這種以DOM操做爲核心到以數據流驅動爲核心,View是不一樣的數據的投射。而且對於數據的處理函數應該是純函數,即相同的輸入有相同的輸出而不會產生其餘反作用:git

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

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

這樣能夠極大地方便對於View構造函數的重用與單元測試等。github

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(組合)

爲了達到真正意義上的重用目標,並不單單就是把那個葉子組件組合進一個新的容器,咱們也須要在容器中構建出可以組合其餘抽象組件的抽象組件。這裏我認爲的組合要點在於如何把兩個或者更多的抽象組件合併成一個新的:segmentfault

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

相關文章
相關標籤/搜索