React設(shè)計思想

原文

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

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

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

我偏向于選擇一種我能完全 hold 住的簡潔的心智模型來作介紹。

變換(Transformation)

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

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

抽象(Abstraction)

你不可能僅用一個函數(shù)就能實現(xiàn)復(fù)雜的 UI。重要的是宜岛,你需要把 UI 抽象成多個隱藏內(nèi)部細(xì)節(jié)长踊,又可復(fù)用的函數(shù)。通過在一個函數(shù)中調(diào)用另一個函數(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)建一個新的容器是不夠的列敲。你還需要可以包含其他抽象的容器再次進(jìn)行組合阱佛。我理解的“組合”就是將兩個或者多個不同的抽象合并為一個。

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 不單單是對服務(wù)器端或業(yè)務(wù)邏輯狀態(tài)的復(fù)制戴而。實際上還有很多狀態(tài)是針對具體的渲染目標(biāo)凑术。舉個例子,舉個例子所意,在一個 text field 中打字淮逊。它不一定要復(fù)制到其他頁面或者你的手機設(shè)備。滾動位置這個狀態(tài)是一個典型的你幾乎不會復(fù)制到多個渲染目標(biāo)的扶踊。

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

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

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

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

// 初始化

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

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

Memoization

對于純函數(shù)绣版,使用相同的參數(shù)一次次調(diào)用未免太浪費資源。我們可以創(chuàng)建一個函數(shù)的 memorized 版本歼疮,用來追蹤最后一個參數(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)缩麸。這是一個天然的層級。

為了管理列表中的每一個 item 的 state 赡矢,我們可以創(chuàng)造一個 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 傳了多個不同的參數(shù)。這打破了我們的 memoization 因為我們每次只能存儲一個值吹散。更多相關(guān)內(nèi)容在下面弧械。

連續(xù)性(Continuations)

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

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

下面這樣并沒有減少樣板代碼热鞍,但至少把它從關(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 邏輯挪動到被復(fù)用很多的低層級的函數(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

一旦我們想在一個 memoization 列表中 memoize 多個 item 就會變得很困難。因為你需要制定復(fù)雜的緩存算法來平衡調(diào)用頻率和內(nèi)存占有率米奸。

還好 UI 在同一個位置會相對的穩(wěn)定昼接。相同的位置一般每次都會接受相同的參數(shù)。這樣以來悴晰,使用一個集合來做 memoization 是一個非常好用的策略慢睡。

我們可以用對待 state 同樣的方式,在組合的函數(shù)中傳遞一個 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ù)據(jù)非常麻煩。如果能有一種方式可以在多層抽象中快捷地傳遞數(shù)據(jù)棕硫,同時又不需要牽涉到中間層級髓涯,那該有多好。React 中我們把它叫做“context”哈扮。

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

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

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)容合作請聯(lián)系作者
  • 序言:七十年代末护姆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子掏击,更是在濱河造成了極大的恐慌签则,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件铐料,死亡現(xiàn)場離奇詭異渐裂,居然都是意外死亡豺旬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進(jìn)店門柒凉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來族阅,“玉大人,你說我怎么就攤上這事膝捞√沟叮” “怎么了?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵蔬咬,是天一觀的道長鲤遥。 經(jīng)常有香客問我,道長林艘,這世上最難降的妖魔是什么盖奈? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮狐援,結(jié)果婚禮上钢坦,老公的妹妹穿的比我還像新娘。我一直安慰自己啥酱,他們只是感情好爹凹,可當(dāng)我...
    茶點故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著镶殷,像睡著了一般禾酱。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绘趋,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天宇植,我揣著相機與錄音,去河邊找鬼埋心。 笑死,一個胖子當(dāng)著我的面吹牛忙上,可吹牛的內(nèi)容都是我干的拷呆。 我是一名探鬼主播,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼疫粥,長吁一口氣:“原來是場噩夢啊……” “哼茬斧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起梗逮,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤项秉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后慷彤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體娄蔼,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨居荒郊野嶺守林人離奇死亡怖喻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了岁诉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锚沸。...
    茶點故事閱讀 40,973評論 1 354
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖涕癣,靈堂內(nèi)的尸體忽然破棺而出哗蜈,到底是詐尸還是另有隱情,我是刑警寧澤坠韩,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布距潘,位于F島的核電站,受9級特大地震影響只搁,放射性物質(zhì)發(fā)生泄漏音比。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一须蜗、第九天 我趴在偏房一處隱蔽的房頂上張望硅确。 院中可真熱鬧,春花似錦明肮、人聲如沸菱农。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽循未。三九已至,卻和暖如春秫舌,著一層夾襖步出監(jiān)牢的瞬間的妖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工足陨, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嫂粟,地道東北人。 一個月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓墨缘,卻偏偏與公主長得像星虹,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子镊讼,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,982評論 2 361

推薦閱讀更多精彩內(nèi)容