深入理解React Diff算法

2021-02-14 web前端開發

來源 | https://segmentfault.com/a/1190000039021724

fiber上的updateQueue經過React的一番計算之後,這個fiber已經有了新的狀態,也就是state,對於類組件來說,state是在render函數裡被使用的,既然已經得到了新的state,那麼當務之急是執行一次render,得到持有新state的ReactElement。假設render一次之後得到了大量的ReactElement,而這些ReactElement之中若只有少量需要更新的節點,那麼顯然不能全部去更新它們,此時就需要有一個diff過程來決定哪些節點是真正需要更新的。源碼結構我們以類組件為例,state的計算發生在類組件對應的fiber節點beginWork中的updateClassInstance函數中,在狀態計算完畢之後,緊跟著就是去調finishClassComponent執行diff、打上effectTag(即新版本的flag)。打上effectTag可以標識這個fiber發生了怎樣的變化,例如:新增(Placement)、更新(Update)、刪除(Deletion),這些被打上flag的fiber會在complete階段被收集起來,形成一個effectList鍊表,只包含這些需要操作的fiber,最後在commit階段被更新掉。

function updateClassComponent(   current: Fiber | null, workInProgress: Fiber, Component: any, nextProps: any, renderLanes: Lanes,) {   ...      shouldUpdate = updateClassInstance(     current,     workInProgress,     Component,     nextProps,     renderLanes,   );      ...         const nextUnitOfWork = finishClassComponent(     current,      workInProgress,     Component,     shouldUpdate,     hasContext,     renderLanes,     );     return nextUnitOfWork; }

在finishClassComponent函數中,調用reconcileChildFibers去做diff,而reconcileChildFibers實際上就是ChildReconciler,這是diff的核心函數,
該函數針對組件render生成的新節點的類型,調用不同的函數進行處理。

function ChildReconciler(shouldTrackSideEffects) {    ...   function reconcileSingleElement(      returnFiber: Fiber,      currentFirstChild: Fiber | null,      element: ReactElement,      lanes: Lanes,): Fiber {        }     function reconcileChildrenArray(     returnFiber: Fiber,     currentFirstChild: Fiber | null,     newChildren: Array<*>,     lanes: Lanes,): Fiber | null {      }      ...    function reconcileChildFibers(     returnFiber: Fiber,     currentFirstChild: Fiber | null,     newChild: any, lanes: Lanes,): Fiber | null {     const isObject = typeof newChild === 'object' && newChild !== null;     if (isObject) {              switch (newChild.$$typeof) {         case REACT_ELEMENT_TYPE:           return placeSingleChild(             reconcileSingleElement(             returnFiber,             currentFirstChild,             newChild,             lanes,           ),        );                case REACT_PORTAL_TYPE:        ...                case REACT_LAZY_TYPE:        ...            }    }    if (typeof newChild === 'string' || typeof newChild === 'number') {          }    if (isArray(newChild)) {            return reconcileChildrenArray(        returnFiber,        currentFirstChild,        newChild,        lanes,     );   }      ...    }  return reconcileChildFibers; }

Diff的主體

關於Diff的參與者,在reconcileChildren函數的入參中可以看出

workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderLanes, );

workInProgress:作為父節點傳入,新生成的第一個fiber的return會被指向它。

current.child:舊fiber節點,diff生成新fiber節點時會用新生成的ReactElement和它作比較。

nextChildren:新生成的ReactElement,會以它為標準生成新的fiber節點。

renderLanes:本次的渲染優先級,最終會被掛載到新fiber的lanes屬性上。

可以看出,diff的兩個主體是:oldFiber(current.child)和newChildren(nextChildren,新的ReactElement),它們是兩個不一樣的數據結構。

比如現在有組件<Example/>,它計算完新的狀態之後,要基於這兩個東西去做diff,分別是現有fiber樹中(current樹)<Example/>對應fiber的所有子fiber節點和<Example/>的render函數的執行結果,即那些ReactElements。

<Example/>對應fiber的所有子fiber節點:oldFiber

 current樹中 <Example/> fiber | | A --sibling---> B --sibling---> C

<Example/>的render函數的執行結果,newChildren

 current fiber 對應的組件render的結果 [    {$$typeof: Symbol(react.element), type: "div", key: "A" },    {$$typeof: Symbol(react.element), type: "div", key: "B" },     {$$typeof: Symbol(react.element), type: "div", key: "B" }, ]

Diff的基本原則

對於新舊兩種結構來說,場景有節點自身更新、節點增刪、節點移動三種情況。面對複雜的情況,即使最前沿的算法,複雜度也極高。面對這種情況,React以如下策略應對:

舊<div> <span>a</span> <span>b</span></div>
新<p> <span>a</span> <span>b</span></p>

舊<p key="a">aa</p><h1 key="b">bb</h1>
新<h1 key="b">bb</h1><p key="a">aa</p>

因為tag 和 key的存在,所以React可以知道這兩個節點只是位置發生了變化。

場景

上面說到diff算法應對三種場景:節點更新、節點增刪、節點移動,但一個fiber的子元素有可能是單節點,也有可能是多節點。所以依據這兩類節點可以再細分為:

單節點更新、單節點增刪。

多節點更新、多節點增刪、多節點移動。

什麼是節點的更新呢?對於DOM節點來說,在前後的節點類型(tag)和key都相同的情況下,節點的屬性發生了變化,是節點更新。若前後的節點tag或者key不相同,Diff算法會認為新節點和舊節點毫無關係。

以下例子中,key為b的新節點的className發生了變化,是節點更新。

舊<div className={'a'} key={'a'}>aa</div><div className={'b'} key={'b'}>bb</div>
新<div className={'a'} key={'a'}>aa</div><div className={'bcd'} key={'b'}>bb</div>

以下例子中,新節點的className雖然有變化,但key也變化了,不屬於節點更新

舊<div className={'a'} key={'a'}>aa</div><div className={'b'} key={'b'}>bb</div>
新<div className={'a'} key={'a'}>aa</div><div className={'bcd'} key={'bbb'}>bb</div>

以下例子中,新節點的className雖然有變化,但tag也變化了,不屬於節點更新

舊<div className={'a'} key={'a'}>aa</div><div className={'b'} key={'b'}>bb</div>
新<div className={'a'} key={'a'}>aa</div><p className={'bcd'} key={'b'}>bb</p>

下面來分開敘述一下單節點和多節點它們各自的更新策略。

單節點

若組件產出的元素是如下的類型:

那麼它最終產出的ReactElement為下面這樣(省略了一些與diff相關度不大的屬性)

{   $$typeof: Symbol(react.element), type: "div", key: "a"    ...}

單節點指newChildren為單一節點,但是oldFiber的數量不一定,所以實際有如下三種場景:

為了降低理解成本,我們用簡化的節點模型來說明問題,字母代表key。

對於單節點的diff,其實就只有更新操作,不會涉及位移和位置的變化,單節點的更新會調用reconcileSingleElement函數處理。該函數中對以上三種場景都做了覆蓋。但實際上面的情況對於React來說只是兩種,oldFiber鏈是否為空。因此,在實現上也只處理了這兩種情況。

oldFiber鏈不為空

遍歷它們,找到key相同的節點,然後刪除剩下的oldFiber節點,再用匹配的oldFiber,newChildren中新節點的props來生成新的fiber節點。

 function reconcileSingleElement(     returnFiber: Fiber,     currentFirstChild: Fiber | null,     element: ReactElement,     lanes: Lanes): Fiber {     const key = element.key;     let child = currentFirstChild;     while (child !== null) {        if (child.key === key) {          switch (child.tag) {            case Fragment:            ...                        case Block:            ...                        default: {              if (child.elementType === element.type) {                                 deleteRemainingChildren(returnFiber, child.sibling);                                const existing = useFiber(child, element.props);                existing.ref = coerceRef(returnFiber, child, element);                existing.return = returnFiber; return existing;              }              break;            }         }                 deleteRemainingChildren(returnFiber, child);        break;      } else {                        deleteChild(returnFiber, child);     }     child = child.sibling;   }    ...  }

oldFiber鏈為空

對於沒有oldFiber節點的情況,只能新建newFiber節點。邏輯不複雜。

   function reconcileSingleElement(     returnFiber: Fiber,     currentFirstChild: Fiber | null,     element: ReactElement,     lanes: Lanes): Fiber {     const key = element.key;     let child = currentFirstChild;     while (child !== null) {                     ...     } if (element.type === REACT_FRAGMENT_TYPE) {                ...      } else {                const created = createFiberFromElement(element, returnFiber.mode, lanes);        created.ref = coerceRef(returnFiber, currentFirstChild, element);        created.return = returnFiber;        return created;     }   }

單節點的更新就是這樣的處理,真正比較複雜的情況是多節點的diff。因為它涉及到節點的增刪和位移。

多節點

若組件最終產出的DOM元素是如下這樣:

<div key="a">aa</div><div key="b">bb</div><div key="c">cc</div><div key="d">dd</div>

那麼最終的newChildren為下面這樣(省略了一些與diff相關度不大的屬性)

[ {$$typeof: Symbol(react.element), type: "div", key: "a" }, {$$typeof: Symbol(react.element), type: "div", key: "b" }, {$$typeof: Symbol(react.element), type: "div", key: "c" }, {$$typeof: Symbol(react.element), type: "div", key: "d" }]

多節點的變化有以下四種可能性。

舊:A - B - C新:A - B - C - `D - E`

舊:A - B - C - `D - E`新:A - B - C

舊:A - B - C - D - E新:A - B - `D - C - E`

多節點的情況一定是屬於這四種情況的任意組合,這種情況會調用reconcileChildrenArray進行diff。按照以上四種情況,它會以newChildren為主體進行最多三輪遍歷,但這三輪遍歷並不是相互獨立的,事實上只有第一輪是從頭開始的,之後的每一輪都是上輪結束的斷點繼續。

實際上在平時的實踐中,節點自身的更新是最多的,所以Diff算法會優先處理更新的節點。因此四輪遍歷又可以按照場景分為兩部分:

第一輪是針對節點自身屬性更新,剩下的兩輪依次處理節點的新增、移動,而重點又在移動節點的處理上,所以本文會著重講解節點更新和節點移動的處理,對刪除和新增簡單帶過。

節點更新

第一輪從頭開始遍歷newChildren,會逐個與oldFiber鏈中的節點進行比較,判斷節點的key或者tag是否有變化。

let newIdx = 0;for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {   ...         const newFiber = updateSlot( returnFiber,     oldFiber,     newChildren[newIdx],     lanes,   );      if (newFiber === null) {     if (oldFiber === null) {        oldFiber = nextOldFiber;     }     break;   }    ... }

我們來看一個例子,假設新舊的節點如下:

舊:A - B - C - D - E
新:A - B - D - C

在本輪遍歷中,會遍歷A - B - D - C。A和B都是key沒變的節點,可以直接復用,但當遍歷到D時,發現key變化了,跳出當前遍歷。例子中A 和 B是自身發生更新的節點,後面的D 和 C我們看到它的位置相對於oldFiber鏈發生了變化,會往下走到處理移動節點的循環中。

關於移動節點的參照物

為了方便說明,把保留在原位的節點稱為固定節點。經過這次循環的處理,可以看出固定節點是A 和 B。在newChildren中,最靠右的固定節點的位置至關重要,對於後續的移動節點的處理來說,它的意義是提供參考位置。所以,每當處理到最後一個固定節點時,要記住此時它的位置,這個位置就是lastPlacedIndex。關鍵代碼如下:

let newIdx = 0;for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { ...   ...  lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);  ...}

placeChild方法實際上是移動節點的方法,但當節點無需移動的時候,會返回當前節點的位置,對於固定節點來說,因為無需移動,所以返回的就是固定節點的index。

節點刪除

我們沒有提到對刪除節點的處理,實際上刪除節點比較簡單。

舊:A - B - C - D - E
新:A - B - C

因為遍歷的是newChildren,當它遍歷結束,但oldFiber鏈還沒有遍歷完,那麼說明剩下的節點都要被刪除。直接在oldFiber節點上標記Deletion的effectTag來實現刪除。

if (newIdx === newChildren.length) {      deleteRemainingChildren(returnFiber, oldFiber);   return resultingFirstChild;}

deleteRemainingChildren調用了deleteChild,值得注意的是,刪除不僅僅是標記了effectTag為Deletion,還會將這個被刪除的fiber節點添加到父級的effectList中。

function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {   ...   const last = returnFiber.lastEffect;      if (last !== null) {     last.nextEffect = childToDelete;     returnFiber.lastEffect = childToDelete;   } else {     returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;   }   childToDelete.nextEffect = null;   childToDelete.effectTag = Deletion;}

節點新增

新增節點的場景也很好理解,當oldFiber鏈遍歷完,但newChildren還沒遍歷完,那麼餘下的節點都屬於新插入的節點,會新建fiber節點並以sibling為指針連成fiber鏈。

舊:A - B - C
新:A - B - C - D - E

插入的邏輯(省略了相關度不高的代碼)

if (oldFiber === null) {  for (; newIdx < newChildren.length; newIdx++) {     const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);    ...        if (previousNewFiber === null) {        resultingFirstChild = newFiber;    } else {        previousNewFiber.sibling = newFiber;    }    previousNewFiber = newFiber;  }  return resultingFirstChild;}

節點移動

節點的移動是如下場景:

舊 A - B - C - D - E - F
新 A - B - D - C - E

經過第一輪遍歷的處理,固定節點為A B,最新的固定節點的位置(lastPlacedIndex)為1(B的位置)。此時oldFiber鏈中還剩C - D - E - F,newChildren中還剩D - C - E。

接下來的邏輯對於位置不一樣的節點,它自己會先更新再移動。因為此時剩餘的節點位置變了,更新又要復用oldFiber節點,所以為了在更新時方便查找,會將剩餘的oldFiber節點放入一個以key為鍵,值為oldFiber節點的map中。稱為existingChildren。

由於newChildren 和 oldFiber節點都沒遍歷完,說明需要移動位置。此刻需要明確一點,就是這些節點都在最新的固定節點的右邊。

移動的邏輯是:newChildren中剩餘的節點,都是不確定要不要移動的,遍歷它們,每一個都去看看這個節點在oldFiber鏈中的位置(舊位置),遍歷到的節點有它在newChildren中的位置(新位置):

如果舊位置在lastPlacedIndex的右邊,說明這個節點位置不變。

原因是舊位置在lastPlacedIndex的右邊,而新節點的位置也在它的右邊,所以它的位置沒變化。因為位置不變,所以它成了固定節點,把lastPlacedIndex更新成新位置。

如果舊位置在lastPlacedIndex的左邊,當前這個節點的位置要往右挪。

原因是舊位置在lastPlacedIndex的左邊,新位置卻在lastPlacedIndex的右邊,所以它要往右挪,但它不是固定節點。此時無需更新lastPlacedIndex。

我們來用上邊的例子過一下這部分邏輯。

舊 A - B - C - D - E - F
新 A - B - D - C - E

位置固定部分 A - B,最右側的固定節點為B,lastPlacedIndex為1。這時剩餘oldFiber鏈為C - D - E - F,existingChildren為

{   C: '節點C',   D: '節點D',   E: '節點E',   F: '節點F'}

newChildren的剩餘部分D - C - E繼續遍歷。

首先遍歷到D,D在oldFiber鏈中(A - B - C - D - E)的位置為3

3 > 1,oldFiber中D的位置在B的右邊,newChildren中也是如此,所以D的位置不動,此時最新的固定節點變成了D,更新lastPlacedIndex為3。並從existingChildren中刪除D,

{   C: '節點C',   E: '節點E',   F: '節點F'}

再遍歷到C,C在oldFiber鏈中(A - B - C - D - E)的索引為2

2 < 3,C原來在最新固定節點(D)的左邊,newChildren中C在D的右邊,所以要給它移動到右邊。並從existingChildren中刪除C。

再遍歷到E,E在oldFiber鏈中(A - B - C - D - E)的位置為4

4 > 3,oldFiber鏈中E位置在D的位置的右邊,新位置中也是如此,所以E的位置不動,此時最新的固定節點變成了E,更新lastPlacedIndex為4。並從existingChildren中刪除E,

這個時候newChildren都處理完了,針對移動節點的遍歷結束。此時還剩一個F節點,是在oldFiber鏈中的,因為newChildren都處理完了,所以將它刪除即可。

existingChildren.forEach(child => deleteChild(returnFiber, child));

可以看到,節點的移動是以最右側的固定節點位置作為參照的。這些固定節點是指位置未發生變化的節點。每次對比節點是否需要移動之後,及時更新固定節點非常重要。

源碼

了解了上邊的多節點diff原理後,將上邊的關鍵點匹配到源碼上更方便能進一步理解。下面放出帶有詳細注釋的源碼。

 function reconcileChildrenArray(    returnFiber: Fiber,    currentFirstChild: Fiber | null,    newChildren: Array<*>,    lanes: Lanes,): Fiber | null {                let resultingFirstChild: Fiber | null = null;            let previousNewFiber: Fiber | null = null;            let oldFiber = currentFirstChild;        let lastPlacedIndex = 0;        let newIdx = 0;        let nextOldFiber = null;            for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {                if (oldFiber.index > newIdx) {            nextOldFiber = oldFiber; oldFiber = null;        } else {                        nextOldFiber = oldFiber.sibling;        }                                const newFiber = updateSlot(            returnFiber,            oldFiber,            newChildren[newIdx],            lanes,        );                        if (newFiber === null) {            if (oldFiber === null) {                                                            }            break;        }        if (shouldTrackSideEffects) {                        if (oldFiber && newFiber.alternate === null) {                                                                                                deleteChild(returnFiber, oldFiber);                }            }                        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);                        if (previousNewFiber === null) {                resultingFirstChild = newFiber;            } else {                previousNewFiber.sibling = newFiber;            }            previousNewFiber = newFiber;                        oldFiber = nextOldFiber;         }                         if (newIdx === newChildren.length) {                        deleteRemainingChildren(returnFiber, oldFiber);            return resultingFirstChild;        }                        if (oldFiber === null) {            for (; newIdx < newChildren.length; newIdx++) {                                            const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);                if (newFiber === null) {                    continue;                }                                lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);                                 if (previousNewFiber === null) {                    resultingFirstChild = newFiber;                } else {                    previousNewFiber.sibling = newFiber;                 }                previousNewFiber = newFiber;            }            return resultingFirstChild;        }                        const existingChildren = mapRemainingChildren(returnFiber, oldFiber);                        for (; newIdx < newChildren.length; newIdx++) {                        const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], lanes, );             if (newFiber !== null) {                if (shouldTrackSideEffects) {                    if (newFiber.alternate !== null) {                                                                                                                                                                        existingChildren.delete(                            newFiber.key === null ? newIdx : newFiber.key,                        );                     }                  }                                                    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);                                 if (previousNewFiber === null) {                    resultingFirstChild = newFiber;                } else {                    previousNewFiber.sibling = newFiber;                 }                previousNewFiber = newFiber;            }         }        if (shouldTrackSideEffects) {                      existingChildren.forEach(child => deleteChild(returnFiber, child));        }        return resultingFirstChild; }

總結

Diff算法通過key和tag來對節點進行取捨,可直接將複雜的比對攔截掉,然後降級成節點的移動和增刪這樣比較簡單的操作。

對oldFiber和新的ReactElement節點的比對,將會生成新的fiber節點,同時標記上effectTag,這些fiber會被連到workInProgress樹中,作為新的WIP節點。

樹的結構因此被一點點地確定,而新的workInProgress節點也基本定型。這意味著,在diff過後,workInProgress節點的beginWork節點就完成了。接下來會進入completeWork階段。

點擊進入(地址:https://github.com/neroneroffy/react-source-code-debug)react源碼調試倉庫

相關焦點

  • 詳解 React 16 的 Diff 策略
    具體可以參考我公眾號以前發的這兩篇文章:別再說虛擬 DOM 快了,要被打臉的深入理解虛擬 DOM,它真的不快如果你對標題不滿意,請把文章看完,至少也得把文章最後的結論好好看下在上一篇將 React Fiber 架構中,已經說到過,React 現在將整體的數據結構從樹改為了鍊表結構。
  • React源碼揭秘(三):Diff算法詳解
    本篇文章主要講解Rect Diff 算法4的內部實現,對 Diff 的簡單講解請參考React 文檔5Diff的瓶頸以及React如何應對由於 Diff 操作本身也會帶來性能損耗,React文檔中提到,即使在最前沿的算法中,將前後兩棵樹完全比對的算法的複雜程度為 O(n 3 ),其中 n 是樹中元素的數量。
  • 深入理解 React 的 Virtual DOM
    React在前端界一直很流行,而且學起來也不是很難,只需要學會JSX、理解State和Props,然後就可以愉快的玩耍了,但想要成為React的專家你還需要對React有一些更深入的理解,希望本文對你有用。
  • React源碼解析,實現一個React
    每當頁面產生了變化,React的diff算法會先在內存模型中進行比對,提取出差異點,在將Virtual DOM轉化為原生DOM輸出時,按照差異點,只patch出有變動的部分。_reactInternalInstance.updateComponent(null, newState) 這個函數。而 this._reactInternalInstance指向CompositeComponent,困此更新邏輯交回CompositeComponent.updateComponent()來完成。
  • React系列八 - 深入理解setState
    在這篇文章中,我對React的setState進行了很多解析,希望可以幫助大家真正理解setState。(其中涉及到一個源碼,我有貼出,但是沒有詳細展開,有機會我們再對源碼進行解析,大家不是很懂也不影響你的學習,只需要知道React內部是這樣做的即可,面試時也可以回答出來)一. setState的使用 1.1.
  • 從 React 歷史的長河裡聊虛擬DOM及其價值
    最近我發現很多面試題裡面都有「如何理解虛擬 DOM」這個題,我覺得這個題應該沒有想像中那麼好答,因為很多人沒有真正理解虛擬 DOM 它的價值所在,我這篇從虛擬 DOM 的誕生過程來引出它的價值以及歷史地位,幫助你深入的理解它。什麼是虛擬 DOM本質上是 JavaScript 對象,這個對象就是更加輕量級的對 DOM 的描述。
  • 深入介紹 React 中的 state 和 props 更新
    在之前我寫了一篇深入介紹 React Fiber的文章,在那篇文章中我介紹了 React 團隊為什麼要重新實現 reconciliation 算法、fiber 節點與 react element 的關係、fiber 節點的欄位以及 fiber 節點是如何被組織在一起的。
  • React-深入理解Props和States
    為了更好地理解,可以將整個UI看作一棵樹。在這裡,起始組件成為根,每個獨立的部分成為分支,這些分支又被進一步劃分為子分支。這使我們的UI具有組織性,並允許數據和狀態更改邏輯地從根流向分支,然後再流向子分支。組件直接從客戶端調用伺服器,這允許DOM在不刷新頁面的情況下動態更新。這是因為react組件是基於AJAX請求的概念構建的。每個組件都有自己的接口,可以調用伺服器並更新它們。
  • 深入 React Fiber 內部
    從 React 16 開始,React 重新實現了 reconciliation 算法,它被稱為 Fiber Reconciler。在 React 16 之前你可能聽說過 virtualDOM,那是老的 reconciliation 算法,老的 reconciler 算法使用了堆棧,所以它也被稱為 Stack Reconciler。
  • 每個前端開發必看的React生命周期函數
    在初始化階段有4個生命周期函數:getDefaultProps()是可以設置組件的默認屬性值;componentWillMount()是組件初始化時調用,在整個生命周期中只調用一次;render()是組件在創建虛擬dom,進行diff
  • 手寫React-Router源碼,深入理解其原理
    本文會繼續深入React-Router講講他的源碼,套路還是一樣的,我們先用官方的API實現一個簡單的例子,然後自己手寫這些API來替換官方的並且保持功能不變。看起來我們要搞懂react-router-dom的源碼還必須得去看react-router和history的源碼,現在我們手上有好幾個需要搞懂的庫了,為了看懂他們的源碼,我們得先理清楚他們的結構關係。
  • 前端技術:React&Vue對比
    React和vue的業務邏輯是差不多,vue在react上封裝了更簡潔的方法,使用起來更加的便捷,如:提供了便捷的指令(v-for,v-if,v-model),還提供了更多的屬性(computed,watch),我還是比較喜歡用react的,更接近js原生,更容易於理解它。
  • 第299天:React中JSX的理解
    React中JSX的理解JSX是快速生成react元素的一種語法,實際是React.createElement
  • 深入理解EM算法
    為了詳細的理解這些原理,曾經看過西瓜書,統計學習方法,機器學習實戰等書,也聽過一些機器學習的課程,但總感覺話語裡比較深奧,讀起來沒有耐心,並且理論到處有,而實戰最重要, 所以在這裡想用最淺顯易懂的語言寫一個白話機器學習算法理論+實戰系列。
  • Git命令解析-patch、apply、diff
    可以使用gitdiff>patchfile將差異輸出到patch文件,保存或者分享給他人。使用gitdiff命令可以查看工作區修改的內容,gitdiff—cached命令查看添加到暫存區但還未提交的內容。這兩種命令會生成兼容unix系統的標準格式patch。
  • ReactNative學習資源大匯集
    react-native 官方api文檔 http://facebook.github.io/react-native/docs/getting-started.htmlreact-native中文文檔(極客學院) http://wiki.jikexueyuan.com/project/react-native/react-native中文文檔(react
  • 第329天:React的refs的理解
    React的refs的理解Refs提供了一種方式,允許我們訪問DOM節點或在render方法中創建的React
  • Vue和React的區別,你在用哪個呢?
    在深層上,模板的原理不同,這才是他們的本質區別:React是在組件JS代碼中,通過 原生JS實現 模板中的常見語法,比如插值,條件,循環等,都是通過JS語法實現的Vue是在和組件JS代碼分離的單獨的模板中, 通過指令來實現的 ,比如 條件語句就需要 v-if 來實現react中 render函數是支持閉包特性的,所以我們import的組件在render
  • Filecoin - 深入理解NSE算法
    PoREP算法,從window SDR改成SDR,時間並不長。新的PoREP算法NSE已經在醞釀中。在rust-fil-proofs的feat/nse分支,可以查看NSE算法的實現。   Merge pull request #1118 from filecoin-project/feat/nse-update-neptune-alt        Feat/nse update neptune alt理解