Skip to content

docs: Reconciliation #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Oct 7, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 45 additions & 45 deletions content/docs/reconciliation.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,32 @@ title: Reconciliation
permalink: docs/reconciliation.html
---

React provides a declarative API so that you don't have to worry about exactly what changes on every update. This makes writing applications a lot easier, but it might not be obvious how this is implemented within React. This article explains the choices we made in React's "diffing" algorithm so that component updates are predictable while being fast enough for high-performance apps.
React 提供聲明式 (declarative) 的 API,讓開發者在使用 React 時,可以不用關注每次 Component 更新時底層有什麼改變。這讓開發應用程式簡單許多,但也可能讓開發者對 React 底層的相關實作不夠瞭解。因此在這篇文章中描述了實作 React 底層中「diffing」演算法時,我們採取什麼策略讓 component 的更新是可預測的,同時可以滿足要求高效能的應用程式。

## Motivation {#motivation}
## 動機 {#motivation}

When you use React, at a single point in time you can think of the `render()` function as creating a tree of React elements. On the next state or props update, that `render()` function will return a different tree of React elements. React then needs to figure out how to efficiently update the UI to match the most recent tree.
在使用 React 時,每次呼叫 `render()` 函式,我們都可以當成是建立了一顆由 React element 構成的樹狀結構。而在每一次有 state props 更新時,`render()` 函式就會回傳一顆不同的 tree。因此,React 需要判斷如何有效率的把 UI 從舊的 tree 更新成新的 tree

There are some generic solutions to this algorithmic problem of generating the minimum number of operations to transform one tree into another. However, the [state of the art algorithms](https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf) have a complexity in the order of O(n<sup>3</sup>) where n is the number of elements in the tree.
對於這個「如何用最少操作去將舊的 tree 轉換成新的 tree」的演算法問題有一些通用的解法,但即使是目前[最先進的演算法](<(https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf)>)都還需要 O(n<sup>3</sup>) 的時間複雜度(n 為 tree 中 element 的數量)。

If we used this in React, displaying 1000 elements would require in the order of one billion comparisons. This is far too expensive. Instead, React implements a heuristic O(n) algorithm based on two assumptions:
假設 React 使用這種演算法,則呈現 1000 個 element 需要 10 億次的比較。因為這個比較成本實在太高,所以 React 在以下兩個假設下採用了一個 O(n) 的啓發式 (heuristic) 演算法:

1. Two elements of different types will produce different trees.
2. The developer can hint at which child elements may be stable across different renders with a `key` prop.
1. 兩個不同類型的 element 會產生出不同的 tree
2. 開發者可以通過 `key` prop 來指出哪些子 element 在不同的 render 下能保持不變;

In practice, these assumptions are valid for almost all practical use cases.
而事實上,以上假設在幾乎所有實務上會出現的場景都成立。

## The Diffing Algorithm {#the-diffing-algorithm}
## Diffing 演算法 {#the-diffing-algorithm}

When diffing two trees, React first compares the two root elements. The behavior is different depending on the types of the root elements.
當比對兩顆 tree 時,React 首先比較兩棵 tree 的 root element。不同類型的 root element 會有不同的處理方式。

### Elements Of Different Types {#elements-of-different-types}
### 比對不同類型的 Element {#elements-of-different-types}

Whenever the root elements have different types, React will tear down the old tree and build the new tree from scratch. Going from `<a>` to `<img>`, or from `<Article>` to `<Comment>`, or from `<Button>` to `<div>` - any of those will lead to a full rebuild.
當比對的兩個 root element 為不同類型的元素時,React 會將原有的 tree 整顆拆掉並且重新建立起新的 tree。例如,當一個元素從 `<a>` 變成 `<img>`、從 `<Article>` 變成 `<Comment>`、或從 `<Button>` 變成 `<div>` 時,都會觸發一個完整的重建流程。

When tearing down a tree, old DOM nodes are destroyed. Component instances receive `componentWillUnmount()`. When building up a new tree, new DOM nodes are inserted into the DOM. Component instances receive `componentWillMount()` and then `componentDidMount()`. Any state associated with the old tree is lost.
當拆掉一顆 tree 時,舊的 DOM 節點會被銷毀,且該 component instance 會執行 `componentWillUnmount()` 函式。當建立一顆新的 tree 時,新建立的 DOM 節點會被插入到 DOM 中,且該 component instance 會依次執行 `componentWillMount()` `componentDidMount()` 方法。而所有跟之前舊的 tree 所關聯的 state 也會被銷毀。

Any components below the root will also get unmounted and have their state destroyed. For example, when diffing:
任何在 root 以下的 component 也會被 unmount,它們的狀態會被銷毀。例如,當比對以下變更時:

```xml
<div>
Expand All @@ -41,43 +41,43 @@ Any components below the root will also get unmounted and have their state destr
</span>
```

This will destroy the old `Counter` and remount a new one.
React 會 destroy 舊的 `Counter` 並且重新建立一個新的。

### DOM Elements Of The Same Type {#dom-elements-of-the-same-type}
### 比對同一類型的 DOM Element {#dom-elements-of-the-same-type}

When comparing two React DOM elements of the same type, React looks at the attributes of both, keeps the same underlying DOM node, and only updates the changed attributes. For example:
當比對兩個相同類型的 React element 時,React 會保留 DOM 節點,只比對及更新有改變的 attribute。例如:

```xml
<div className="before" title="stuff" />

<div className="after" title="stuff" />
```

By comparing these two elements, React knows to only modify the `className` on the underlying DOM node.
透過比對這兩個 element,React 知道只需要修改 DOM 節點上的 className

When updating `style`, React also knows to update only the properties that changed. For example:
當更新 `style` 時,React 僅更新有所更變的屬性。例如:

```xml
<div style={{color: 'red', fontWeight: 'bold'}} />

<div style={{color: 'green', fontWeight: 'bold'}} />
```

When converting between these two elements, React knows to only modify the `color` style, not the `fontWeight`.
透過比對這兩個 element,React 知道只需要修改 `color` 的樣式,而不需修改 `fontWeight。`

After handling the DOM node, React then recurses on the children.
在處理完目前節點之後,React 會繼續對 children 進行遞迴處理。

### Component Elements Of The Same Type {#component-elements-of-the-same-type}
### 比對同類型的 Component Element {#component-elements-of-the-same-type}

When a component updates, the instance stays the same, so that state is maintained across renders. React updates the props of the underlying component instance to match the new element, and calls `componentWillReceiveProps()` and `componentWillUpdate()` on the underlying instance.
當一個 component 更新時,該 component 的 instance 保持不變,這樣 state 能夠被保留在不同次的 render 中。React 會更新該 component instance 的 props 以跟最新的 element 保持一致,並且呼叫該 instance 的 `componentWillReceiveProps()` `componentWillUpdate()` 方法。

Next, the `render()` method is called and the diff algorithm recurses on the previous result and the new result.
接下來,該 instance 會再呼叫 `render()` 方法,而 diff 算法將會遞迴處理舊的結果以及新的結果。

### Recursing On Children {#recursing-on-children}
### Children 進行遞迴處理 {#recursing-on-children}

By default, when recursing on the children of a DOM node, React just iterates over both lists of children at the same time and generates a mutation whenever there's a difference.
在預設條件下,當遞迴處理 DOM 節點的 children 時,React 只會同時遍歷兩個 children 的 array,並在發現差異時,產生一個 mutation

For example, when adding an element at the end of the children, converting between these two trees works well:
例如,當在 children 的 array 尾端新增一個 element 時,在這兩個 tree 之間的轉換效果很好:

```xml
<ul>
Expand All @@ -92,9 +92,9 @@ For example, when adding an element at the end of the children, converting betwe
</ul>
```

React will match the two `<li>first</li>` trees, match the two `<li>second</li>` trees, and then insert the `<li>third</li>` tree.
React 會先匹配兩個 `<li>first</li>` 對應的 tree,然後匹配第二個元素 `<li>second</li>` 對應的 tree,最後插入第三個元素 `<li>third</li>` tree

If you implement it naively, inserting an element at the beginning has worse performance. For example, converting between these two trees works poorly:
如果只是單純的實作,則在 array 開頭插入新元素會讓效能變差。例如,在兩個 tree 之間的轉換效果很差:

```xml
<ul>
Expand All @@ -109,11 +109,11 @@ If you implement it naively, inserting an element at the beginning has worse per
</ul>
```

React will mutate every child instead of realizing it can keep the `<li>Duke</li>` and `<li>Villanova</li>` subtrees intact. This inefficiency can be a problem.
這個情況下,React 會針對每個 child 都進行 mutate,而不是讓兩個相同的 `<li>Duke</li>` `<li>Villanova</li>` subtree 不參與 mutate。這種低效率的情況下可能會帶來效能問題。

### Keys {#keys}
**Keys**

In order to solve this issue, React supports a `key` attribute. When children have keys, React uses the key to match children in the original tree with children in the subsequent tree. For example, adding a `key` to our inefficient example above can make the tree conversion efficient:
為了解決以上問題,React 提供了 key 屬性。當 children 擁有 `key` 屬性時,React 使用 key 來匹配原有 tree 上的 children 以及後續 tree 的 children。例如,以下範例在新增 `key` 屬性之後,可以改善在上個例子中發生的效能問題:

```xml
<ul>
Expand All @@ -128,30 +128,30 @@ In order to solve this issue, React supports a `key` attribute. When children ha
</ul>
```

Now React knows that the element with key `'2014'` is the new one, and the elements with the keys `'2015'` and `'2016'` have just moved.
現在 React 知道只有帶著 `'2014'` key 的 element 是新的,而帶著 `'2015'` 以及 `'2016'` key 的 element 只是相對位置移動了。

In practice, finding a key is usually not hard. The element you are going to display may already have a unique ID, so the key can just come from your data:
實際上,找出一個 key 通常並不困難。你要顯示的 element 可能已經具有唯一的 ID 了,這個 key 可能是來自於你的資料:

```js
<li key={item.id}>{item.name}</li>
```

When that's not the case, you can add a new ID property to your model or hash some parts of the content to generate a key. The key only has to be unique among its siblings, not globally unique.
當以上情況不成立時,你可以新增一個 ID 字串到你的 Model 中,或者利用一部分內容作為 hash 來產生一個 key。這個 key 不需要是全域唯一,但在 array 中需要保持唯一。

As a last resort, you can pass an item's index in the array as a key. This can work well if the items are never reordered, but reorders will be slow.
最後,你也可以使用元素在 array 中的索引值作為 key。這個方法在元素不進行重新排序時比較合適,但如果有順序修改,diff 就會變慢。

Reorders can also cause issues with component state when indexes are used as keys. Component instances are updated and reused based on their key. If the key is an index, moving an item changes it. As a result, component state for things like uncontrolled inputs can get mixed up and updated in unexpected ways.
當使用 array 索引值作為 key 的 component 進行重新排序時,component state 可能會遇到一些問題。由於 component instance 是基於它們的 key 來決定是否更新以及重複使用,如果 key 是一個索引值,那麼修改順序時會修改目前的 key,導致 component 的 state(例如不受控制輸入框)可能相互篡改導致無法預期的變動。

[Here](codepen://reconciliation/index-used-as-key) is an example of the issues that can be caused by using indexes as keys on CodePen, and [here](codepen://reconciliation/no-index-used-as-key) is an updated version of the same example showing how not using indexes as keys will fix these reordering, sorting, and prepending issues.
[這是](codepen://reconciliation/index-used-as-key) 在 Codepen 上的範例,示範使用索引值作為 key 時導致的問題,以及[這裡](codepen://reconciliation/no-index-used-as-key)是一個不使用索引值作為 key 的版本,修復了重新排列、排序、以及在array 開頭插入的問題。

## Tradeoffs {#tradeoffs}
## 權衡 {#tradeoffs}

It is important to remember that the reconciliation algorithm is an implementation detail. React could rerender the whole app on every action; the end result would be the same. Just to be clear, rerender in this context means calling `render` for all components, it doesn't mean React will unmount and remount them. It will only apply the differences following the rules stated in the previous sections.
請記得 Reconciliation 只是一個實作細節,即使 React 在每個動作後之後對整個應用進行重新 render,得到的最終結果也會和進行 Reconciliation 後是一樣的。在這個情形下,重新 render 表示在所有 component 內呼叫 `render`,這不代表 React unmount 或重新 mount 它們。React 只會基於以上提到的規則來決定如何進行差異的合併。

We are regularly refining the heuristics in order to make common use cases faster. In the current implementation, you can express the fact that a subtree has been moved amongst its siblings, but you cannot tell that it has moved somewhere else. The algorithm will rerender that full subtree.
我們會定期改善啟發式演算法,讓常見的使用場景能夠更有效率地執行。在目前的實作中,可以理解為一棵 subtree 能在其相同階層的兄弟之間移動,但不能移動到其他位置。如果移動到其他位置,則演算法會重新 render 整棵 subtree

Because React relies on heuristics, if the assumptions behind them are not met, performance will suffer.
由於 React 依賴啓發式的演算法,因此當以下假設沒有得到滿足,效能將會有所影響。

1. The algorithm will not try to match subtrees of different component types. If you see yourself alternating between two component types with very similar output, you may want to make it the same type. In practice, we haven't found this to be an issue.

2. Keys should be stable, predictable, and unique. Unstable keys (like those produced by `Math.random()`) will cause many component instances and DOM nodes to be unnecessarily recreated, which can cause performance degradation and lost state in child components.
1. 該演算法不會嘗試匹配不同 component 類型的 subtree。如果你發現你在兩種不同類型的 component 中切換,但輸出非常相似的內容,建議把它們改成同一類型。實際上,我們沒有發現在改成同一種類型後會發生問題。
2. Key 應該具有穩定、可預測、以及 array 內唯一的特質。不穩定的 key(例如透過 `Math.random()` 隨機生成的)會導致許多 component instance 和 DOM 節點被不必要地重新建立,這可能導致效能低下和 child component 中的 state 丟失。