React設(shè)計(jì)思想

原文

譯者序:本文是 React 核心開(kāi)發(fā)者、有 React API 終結(jié)者之稱(chēng)的 Sebastian Markb?ge 撰寫(xiě),闡述了他設(shè)計(jì) React 的初衷。閱讀此文,你能站在更高的高度思考 React 的過(guò)去、現(xiàn)在和未來(lái)。原文地址:https://github.com/reactjs/react-basic

我寫(xiě)此文是想正式地闡述我心中 React 的心智模型。目的是解釋為什么我們會(huì)這樣設(shè)計(jì) React,同時(shí)你也可以根據(jù)這些論點(diǎn)反推出 React。

不可否認(rèn),此文中的部分論據(jù)或前提尚存爭(zhēng)議,而且部分示例的設(shè)計(jì)可能存在 bug 或疏忽。這只是正式確定它的最初階段。如果你有更好的完善它的想法可以隨時(shí)提交 pull request。本文不會(huì)介紹框架細(xì)節(jié)中的奇技淫巧,相信這樣能提綱挈領(lǐng),讓你看清 React 由簡(jiǎn)單到復(fù)雜的設(shè)計(jì)過(guò)程。
React.js 的真實(shí)實(shí)現(xiàn)中充滿(mǎn)了具體問(wèn)題的解決方案,漸進(jìn)式的解法,算法優(yōu)化,歷史遺留代碼,debug 工具以及其他一些可以讓它真的具有高可用性的內(nèi)容。這些代碼可能并不穩(wěn)定,因?yàn)槲磥?lái)瀏覽器的變化和功能權(quán)重的變化隨時(shí)面臨改變。所以具體的代碼很難徹底解釋清楚。

我偏向于選擇一種我能完全 hold 住的簡(jiǎn)潔的心智模型來(lái)作介紹。

變換(Transformation)

設(shè)計(jì) React 的核心前提是認(rèn)為 UI 只是把數(shù)據(jù)通過(guò)映射關(guān)系變換成另一種形式的數(shù)據(jù)。同樣的輸入必會(huì)有同樣的輸出。這恰好就是純函數(shù)。

function NameBox(name) {
  return { fontWeight: 'bold', labelContent: name };
}
'Sebastian Markb?ge' ->
{ fontWeight: 'bold', labelContent: 'Sebastian Markb?ge' };

抽象(Abstraction)

你不可能僅用一個(gè)函數(shù)就能實(shí)現(xiàn)復(fù)雜的 UI。重要的是,你需要把 UI 抽象成多個(gè)隱藏內(nèi)部細(xì)節(jié),又可復(fù)用的函數(shù)。通過(guò)在一個(gè)函數(shù)中調(diào)用另一個(gè)函數(shù)來(lái)實(shí)現(xiàn)復(fù)雜的 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)

為了真正達(dá)到重用的特性,只重用葉子然后每次都為他們創(chuàng)建一個(gè)新的容器是不夠的。你還需要可以包含其他抽象的容器再次進(jìn)行組合。我理解的“組合”就是將兩個(gè)或者多個(gè)不同的抽象合并為一個(gè)。

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

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

狀態(tài)(State)

UI 不單單是對(duì)服務(wù)器端或業(yè)務(wù)邏輯狀態(tài)的復(fù)制。實(shí)際上還有很多狀態(tài)是針對(duì)具體的渲染目標(biāo)。舉個(gè)例子,舉個(gè)例子,在一個(gè) text field 中打字。它不一定要復(fù)制到其他頁(yè)面或者你的手機(jī)設(shè)備。滾動(dòng)位置這個(gè)狀態(tài)是一個(gè)典型的你幾乎不會(huì)復(fù)制到多個(gè)渲染目標(biāo)的。

我們傾向于使用不可變的數(shù)據(jù)模型。我們把可以改變 state 的函數(shù)串聯(lián)起來(lái)作為原點(diǎn)放置在頂層。

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

// 實(shí)現(xiàn)細(xì)節(jié)

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

// 初始化

FancyNameBox(
  { firstName: 'Sebastian', lastName: 'Markb?ge' },
  likes,
  addOneMoreLike
);

注意:本例更新?tīng)顟B(tài)時(shí)會(huì)帶來(lái)副作用(addOneMoreLike 函數(shù)中)。我實(shí)際的想法是當(dāng)一個(gè)“update”傳入時(shí)我們返回下一個(gè)版本的狀態(tài),但那樣會(huì)比較復(fù)雜。此示例待更新

Memoization

對(duì)于純函數(shù),使用相同的參數(shù)一次次調(diào)用未免太浪費(fèi)資源。我們可以創(chuàng)建一個(gè)函數(shù)的 memorized 版本,用來(lái)追蹤最后一個(gè)參數(shù)和結(jié)果。這樣如果我們繼續(xù)使用同樣的值,就不需要反復(fù)執(zhí)行它了。

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 都是展示列表數(shù)據(jù)中不同 item 的列表結(jié)構(gòu)。這是一個(gè)天然的層級(jí)。

為了管理列表中的每一個(gè) item 的 state ,我們可以創(chuàng)造一個(gè) Map 容納具體 item 的 state。

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);

注意:現(xiàn)在我們向 FancyNameBox 傳了多個(gè)不同的參數(shù)。這打破了我們的 memoization 因?yàn)槲覀兠看沃荒艽鎯?chǔ)一個(gè)值。更多相關(guān)內(nèi)容在下面。

連續(xù)性(Continuations)

不幸的是,自從 UI 中有太多的列表,明確的管理就需要大量的重復(fù)性樣板代碼。

我們可以通過(guò)推遲一些函數(shù)的執(zhí)行,進(jìn)而把一些模板移出業(yè)務(wù)邏輯。比如,使用“柯里化”(JavaScript 中的 bind)。然后我們可以從核心的函數(shù)外面?zhèn)鬟f state,這樣就沒(méi)有樣板代碼了。

下面這樣并沒(méi)有減少樣板代碼,但至少把它從關(guān)鍵業(yè)務(wù)邏輯中剝離。

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

之前我們知道可以使用組合避免重復(fù)執(zhí)行相同的東西這樣一種重復(fù)模式。我們可以把執(zhí)行和傳遞 state 邏輯挪動(dòng)到被復(fù)用很多的低層級(jí)的函數(shù)中去。

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

一旦我們想在一個(gè) memoization 列表中 memoize 多個(gè) item 就會(huì)變得很困難。因?yàn)槟阈枰贫◤?fù)雜的緩存算法來(lái)平衡調(diào)用頻率和內(nèi)存占有率。

還好 UI 在同一個(gè)位置會(huì)相對(duì)的穩(wěn)定。相同的位置一般每次都會(huì)接受相同的參數(shù)。這樣以來(lái),使用一個(gè)集合來(lái)做 memoization 是一個(gè)非常好用的策略。

我們可以用對(duì)待 state 同樣的方式,在組合的函數(shù)中傳遞一個(gè) memoization 緩存。

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);

代數(shù)效應(yīng)(Algebraic Effects)

多層抽象需要共享瑣碎數(shù)據(jù)時(shí),一層層傳遞數(shù)據(jù)非常麻煩。如果能有一種方式可以在多層抽象中快捷地傳遞數(shù)據(jù),同時(shí)又不需要牽涉到中間層級(jí),那該有多好。React 中我們把它叫做“context”。

有時(shí)候數(shù)據(jù)依賴(lài)并不是嚴(yán)格按照抽象樹(shù)自上而下進(jìn)行。舉個(gè)例子,在布局算法中,你需要在實(shí)現(xiàn)他們的位置之前了解子節(jié)點(diǎn)的大小。

現(xiàn)在,這個(gè)例子有一點(diǎn)超綱。我會(huì)使用 代數(shù)效應(yīng) 這個(gè)由我發(fā)起的 ECMAScript 新特性提議。如果你對(duì)函數(shù)式編程很熟悉,它們 在避免由 monad 強(qiáng)制引入的儀式一樣的編碼。

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)
  );
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容