[TOC]
# 一、監聽數據變化的實現原理不同
*****
- Vue 通過 getter/setter 以及一些函數的劫持,能精確知道數據變化,不需要特別的優化就能達到很好的性能
- React 默認是通過比較引用的方式進行的,如果不優化(PureComponent/shouldComponentUpdate)可能導致大量不必要的 VDOM 的重新渲染
# 二、數據流的不同
*****

在 Vue1.0 中我們可以實現兩種雙向綁定(這里的雙向綁定指的是數據流而不是 View 與 Model):
1. 父子組件之間,props 可以雙向綁定
2. 組件與 DOM 之間可以通過 v-model 雙向綁定
在 Vue2.x 中去掉了第一種,也就是父子組件之間不能雙向綁定了(但是提供了一個語法糖自動幫你通過事件的方式修改),并且 Vue2.x 已經不鼓勵組件對自己的 props 進行任何修改了。
所以現在我們只有組件 DOM 之間的雙向綁定這一種。
然而 React 從誕生之初就不支持雙向綁定,React 一直提倡的是單向數據流,他稱之為 onChange/setState() 模式。
不過由于我們一般都會用 Vuex 以及 Redux 等單向數據流的狀態管理框架,因此很多時候我們感受不到這一點的區別了。
# 三、組件通信的區別
*****
單純地看父子組件通信:
- Vue 中父組件可以通過 props 向子組件傳遞數據和回調,但是我們一般都只傳數據,通過事件 `$emit` 的機制來處理子組件向父組件的通信。
- React 也可以向子組件傳遞數據和回調,在 React 中子組件向父組件通信都是采用回調的方式進行的。
# 四、Vuex 和 Redux 的區別
*****
在 Vuex 中,store 被直接注入到了所有的組件實例中,因此可以比較靈活的使用:
- 使用 dispatch 和 commit 提交更新
- 通過 mapState 或者直接通過 this.$store 來讀取數據
在 Redux 中,我們每一個組件都需要顯式地用 connect 把需要的 props 和 dispatch 連接起來。
另外 Vuex 更加靈活一些,組件中既可以 dispatch action 也可以 commit updates,而 Redux 中只能進行 dispatch,并不能直接調用 reducer 進行修改。
從實現原理上來說,最大的區別是兩點:
* Redux 使用的是不可變數據,而 Vuex 的數據是可變的。Redux 每次都是用新的 state 替換舊的 state,而 Vuex 是直接修改
* Redux 在檢測數據變化的時候,是通過 diff 的方式比較差異的,而 Vuex 其實和 Vue 的原理一樣,是通過 getter/setter 來比較的(如果看 Vuex 源碼會知道,其實他內部直接創建一個 Vue 實例用來跟蹤數據變化)
而這兩點的區別,其實也是因為 React 和 Vue 的設計理念上的區別。React 更偏向于構建穩定大型的應用,非常的科班化。相比之下,Vue 更偏向于簡單迅速的解決問題,更靈活,不那么嚴格遵循條條框框。因此也會給人一種大型項目用 React,小型項目用 Vue 的感覺。
# 五、css 管理的區別
*****
Vue 中我們直接給 \<style> 加上 scoped 屬性就能保證它的樣式只作用于當前組件:
```html
<style scoped>
.example {
color: red;
}
</style>
<template>
<div class="example">hi</div>
</template>
```
```html
<style>
.example[data-v-5558831a] {
color: red;
}
</style>
<template>
<div class="example" data-v-5558831a>hi</div>
</template>
```
PostCSS 給一個組件中的所有 dom 添加了<span style="color: red">一個獨一無二的動態屬性</span>,然后,給 CSS 選擇器額外添加一個對應的屬性選擇器來選擇該組件中 dom,這種做法使得樣式只作用于含有該屬性的 dom——組件內部 dom
>為什么 Vue 在開發環境下我們能看到原本的樣式?而 React 使用 Styled-Component 看到的就直接是哈希后的樣式?能做轉換嗎?
這使得我們寫一個 Vue 組件只需要一個 .vue 文件即可,而 React 中有多種樣式管理方案,比如直接寫 css 文件 import 導入,使用 styled-component 等,文件結構看起來就不那么整潔了。
# 六、diff 算法的差異
## 傳統 diff 算法

計算兩顆樹形結構差異并進行轉換,傳統 diff 算法是這樣做的:循環遞歸每一個節點
比如左側樹 a 節點依次進行如下對比,左側樹節點 b、c、d、e 亦是與右側樹每個節點對比
算法復雜度能達到 O(n^2),n 代表節點的個數
```js
a->e、a->d、a->b、a->c、a->a
```
查找完差異后還需計算最小轉換方式,最終達到的算法復雜度是 O(n^3)
## React 的 diff 策略
React 的 diff 策略將時間復雜度優化到了 O(n),這一壯舉是通過以下三條策略來實現的:
* Web UI 中 DOM 節點跨層級的移動操作特別少,可以忽略不計,所以 React 的 diff 是同層級比較
* 擁有相同類型的兩個組件將會生成相似的樹形結構,不同類型的兩個組件將會生成不同的樹形結構
* 對于同一層級的一組子節點,它們可以通過唯一 id 進行區分
這三個策略分別對于 tree diff、component diff 和 element diff
### tree diff
既然 DOM 節點跨層級的移動操作少到可以忽略不計,針對這一現象,React 只會對相同層級的 DOM 節點進行比較,即同一個父節點下的所有子節點。當發現節點已經不存在時,則該節點及其子節點會被完全刪除掉,不會用于進一步的比較。這樣只需要對樹進行一次遍歷,便能完成整個 DOM 樹的比較。

這個策略的前提是操作 DOM 時跨層級操作較少,那么如果發生了跨層級操作應該如何處理呢?

這一過程可以用下圖來描述:

A 節點(包括其子節點)整個被移動到 D 節點下,由于 React 只會簡單地考慮同層級節點的位置變換,而對于不同層級的節點,只有創建和刪除操作。當根節點發現子節點中 A 消失了,就會直接銷毀 A;當 D 發現多了一個子節點 A,則會創建新的 A(包括子節點)作為其子節點。此時,diff 的執行情況:create A → create B → create C → delete A。
由此可以發現,當出現節點跨層級移動時,并不會出現想象中的移動操作,而是以 A 為根節點的整個樹被重新創建。這是一種影響 React 性能的操作,因此官方建議不要進行 DOM 節點跨層級的操作。
### component diff
React 是基于組件構建應用的,對于組件間的比較所采取的策略也是非常簡潔、高效的:
* 如果是同一類型的組件,按照原策略繼續比較 Virtual DOM 樹即可
* 如果不是,則將該組件判斷為 dirty component,從而替換整個組件下的所有子節點
* 對于同一類型的組件,有可能其 Virtual DOM 沒有任何變化,如果能夠確切知道這點,那么就可以節省大量的 diff 運算時間。因此,React 允許用戶通過 shouldComponentUpdate() 來判斷該組件是否需要進行 diff 算法分析,但是如果調用了 forceUpdate 方法,shouldComponentUpdate 則失效
接下來我們看下面這個例子是如何實現轉換的:

轉換流程如下:

當組件 D 變為組件 G 時,即使這兩個組件結構相似,一旦 React 判斷 D 和 G 是不同類型的組件,就不會比較二者的結構,而是直接刪除組件 D,重新創建組件 G 及其子節點。雖然當兩個組件是不同類型但結構相似時,diff 會影響性能,但正如 React 官方博客所言:不同類型的組件很少存在相似 DOM 樹的情況,因此這種極端因素很難在實際開發過程中造成重大的影響。
### element diff
當節點處于同一層級時,diff 提供了 3 種節點操作,分別為 INSERT\_MARKUP (插入)、MOVE\_EXISTING (移動)和 REMOVE\_NODE (刪除)。
* INSERT\_MARKUP :新的組件類型不在舊集合里,即全新的節點,需要對新節點執行插入操作。
* MOVE\_EXISTING :舊集合中有新組件類型,且 element 是可更新的類型,generateComponentChildren 已調用 receiveComponent ,這種情況下 prevChild = nextChild ,就需要做移動操作,可以復用以前的 DOM 節點。
* REMOVE\_NODE :舊組件類型,在新集合里也有,但對應的 element 不同則不能直接復用和更新,需要執行刪除操作,或者舊組件不在新集合里的,也需要執行刪除操作。
我們可以忽略上面的說明直接來看例子 (-.-)
當一個組件包含多個子組件的情況:
```html
<ul>
<TodoItem text="First" completed={false} />
<TodoItem text="Second" completed={false} />
</ul>
// 更新為
<ul>
<TodoItem text="Zero" completed={false} />
<TodoItem text="First" completed={false} />
<TodoItem text="Second" completed={false} />
</ul>
```
直觀上看,只需要創建一個新組件,更新之前的兩個組件;但是實際情況并不是這樣的,React 并沒有找出兩個序列的精確差別,而是直接挨個比較每個子組件。
在上面的新的 TodoItem 實例插入在第一位的例子中,React 會首先認為把 text 為 First 的 TodoItem 組件實例的 text 改成了 Zero,text 為 Second 的 TodoItem 組件實例的 text 改成了 First,在最后面多出了一個 TodoItem 組件實例。這樣的操作的后果就是,現存的兩個實例的 text 屬性被改變了,強迫它們完成了一個更新過程,創造出來的新的 TodoItem 實例用來顯示 Second。
我們可以看到,理想情況下只需要增加一個 TodoItem 組件,但實際上其還強制引發了其他組件實例的更新。
假設有 100 個組件實例,那么就會引發 100 次更新,這明顯是一個浪費;所以就需要開發人員在寫代碼的時候提供一點小小的幫助,這就是接下來要講的 key 的作用
```html
<ul>
<TodoItem key={1} text="First" completed={false} />
<TodoItem key={2} text="Second" completed={false} />
</ul>
// 新增一個 TodoItem 實例
<ul>
<TodoItem key={0} text="Zero" completed={false} />
<TodoItem key={1} text="First" completed={false} />
<TodoItem key={2} text="Second" completed={false} />
</ul>
```
React 根據 key 值,就可以知道現在的第二個和第三個 TodoItem 實例其實就是之前的第一個和第二個實例,所以 React 就會把新創建的 TodoItem 實例插在第一位,對于原有的兩個 TodoItem 實例只用原有的 props 來啟動更新過程,這樣 shouldComponentUpdate 就會發生作用,避免無謂的更新操作;
了解了這些之后,我們就知道 key 值應該是**唯一**且**穩定不變的**
比如用數組下標值作為 key 就是一個典型的 “錯誤”,看起來 key 值是唯一的,但是卻不是穩定不變的(但是一般用數組下標就行了)
比如:\[a, b, c\] 值與下標的對應關系:a: 0 b:1 c:2
刪除a -> \[b, c\] 值與下標的對應關系 b:0 c:1
無法用 key 值來確定比對關系(新的 b 應該與舊的 b 比,如果按 key 值則是與 a 比)

>需要注意,雖然 key 是一個 prop,但是接受 key 的組件并不能讀取到 key 的值,因為 key 和 ref 是 React 保留的兩個特殊 prop,并沒有預期讓組件直接訪問
## Vue 的 diff 策略
首先與傳統 diff 策略相比,Vue 也采用了同層節點對比的方式。
接下來就是與 React 的策略的第一個區別了:通知更新的方式
Vue 通過 Watcher 來監聽數據變化實現視圖更新,而 React 顯然沒有這些監聽器,至于 React 是怎么通知組件更新的我沒看過源碼,我猜是 setState 或其他操作造成 state 或 prop 改變時觸發某些函數吧。
下面看下 Vue 的 dff 流程圖:

可以大致地梳理下其流程:
首先要知道 VNode 大致是怎樣的:
```js
// body下的 <div id="v" class="classA"><div> 對應的 oldVnode 就是
{
el: div // 對真實的節點的引用,本例中就是document.querySelector('#id.classA')
tagName: 'DIV', // 節點的標簽
sel: 'div#v.classA' // 節點的選擇器
data: null, // 一個存儲節點屬性的對象,對應節點的 el[prop] 屬性,例如 onclick , style
children: [], // 存儲子節點的數組,每個子節點也是 vnode 結構
text: null, // 如果是文本節點,對應文本節點的 textContent,否則為 null
}
```
### patch
然后看下`patch`是如何判斷新舊 Vnode 是否相同的:
```js
function patch (oldVnode, vnode) {
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
const oEl = oldVnode.el
let parentEle = api.parentNode(oEl)
createEle(vnode)
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
api.removeChild(parentEle, oldVnode.el)
oldVnode = null
}
}
return vnode
}
```
patch 函數內第一個`if`判斷`sameVnode(oldVnode, vnode)`就是判斷這兩個節點是否為同一類型節點,以下是它的實現:
```js
function sameVnode(oldVnode, vnode){
// 兩節點 key 值相同,并且 sel 屬性值相同,即認為兩節點屬同一類型,可進行下一步比較
return vnode.key === oldVnode.key && vnode.sel === oldVnode.sel
}
```
也就是說,即便同一個節點元素比如 div,他的`className`不同,Vue 就認為是兩個不同類型的節點,執行刪除舊節點、插入新節點操作。這與 React diff 的實現是不同的,React 對于同一個元素節點認為是同一類型節點,只更新其節點上的屬性。
### patchVnode
之后就看`patchVnode`,對于同類型節點調用`patchVnode(oldVnode, vnode)`進一步比較,偽代碼如下:
```js
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el // 讓 vnode.el 引用到現在的真實 dom,當 el 修改時,vnode.el 會同步變化
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return // 新舊節點引用一致,認為沒有變化
// 1.文本節點的比較
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
// 2.對于擁有子節點(兩者的子節點不同)的兩個節點,調用 updateChildren
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){ // 3.只有新節點有子節點,添加新的子節點
createEle(vnode) //create el's children dom
}else if (oldCh){ // 4.只有舊節點內存在子節點,執行刪除子節點操作
api.removeChildren(el)
}
}
}
```
其中第二種情況是一種比較常見的情況,執行`updateChildren`函數
### updateChildren
源碼就不貼出來了,其思路大致如下:

oldCh 和 newCh 各有兩個頭尾的變量 StartIdx 和 EndIdx,它們的 2 個變量相互比較,一共有 4 種比較方式。如果 4 種比較都沒匹配,則以遍歷的方式作比較;如果設置了 key,就會用 key 進行比較,在比較的過程中,變量會往中間靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一個已經遍歷完了,就會結束比較。
這一具體過程可以參考這篇文章:[https://www.cnblogs.com/wind-lanyan/p/9061684.html](https://www.cnblogs.com/wind-lanyan/p/9061684.html)
有空的話我把他的圖重畫一下......
同樣的條件下,React 采取的比較方式是從左至右一一比較(如果沒有 key),而 Vue 采取的是這種指針匹配的形式,這也是其 diff 策略的一個不同之處。
# 參考鏈接
[https://juejin.im/post/5b8b56e3f265da434c1f5f76#heading-0](https://juejin.im/post/5b8b56e3f265da434c1f5f76#heading-0)
[https://segmentfault.com/a/1190000017508285](https://segmentfault.com/a/1190000017508285)
[https://segmentfault.com/a/1190000018914249?utm\_source=tag-newest](https://segmentfault.com/a/1190000018914249?utm_source=tag-newest)
[https://www.jianshu.com/p/398e63dc1969](https://www.jianshu.com/p/398e63dc1969)
[https://www.cnblogs.com/wind-lanyan/p/9061684.html](https://www.cnblogs.com/wind-lanyan/p/9061684.html)
- 序言 & 更新日志
- H5
- Canvas
- 序言
- Part1-直線、矩形、多邊形
- Part2-曲線圖形
- Part3-線條操作
- Part4-文本操作
- Part5-圖像操作
- Part6-變形操作
- Part7-像素操作
- Part8-漸變與陰影
- Part9-路徑與狀態
- Part10-物理動畫
- Part11-邊界檢測
- Part12-碰撞檢測
- Part13-用戶交互
- Part14-高級動畫
- CSS
- SCSS
- codePen
- 速查表
- 面試題
- 《CSS Secrets》
- SVG
- 移動端適配
- 濾鏡(filter)的使用
- JS
- 基礎概念
- 作用域、作用域鏈、閉包
- this
- 原型與繼承
- 數組、字符串、Map、Set方法整理
- 垃圾回收機制
- DOM
- BOM
- 事件循環
- 嚴格模式
- 正則表達式
- ES6部分
- 設計模式
- AJAX
- 模塊化
- 讀冴羽博客筆記
- 第一部分總結-深入JS系列
- 第二部分總結-專題系列
- 第三部分總結-ES6系列
- 網絡請求中的數據類型
- 事件
- 表單
- 函數式編程
- Tips
- JS-Coding
- Framework
- Vue
- 書寫規范
- 基礎
- vue-router & vuex
- 深入淺出 Vue
- 響應式原理及其他
- new Vue 發生了什么
- 組件化
- 編譯流程
- Vue Router
- Vuex
- 前端路由的簡單實現
- React
- 基礎
- 書寫規范
- Redux & react-router
- immutable.js
- CSS 管理
- React 16新特性-Fiber 與 Hook
- 《深入淺出React和Redux》筆記
- 前半部分
- 后半部分
- react-transition-group
- Vue 與 React 的對比
- 工程化與架構
- Hybird
- React Native
- 新手上路
- 內置組件
- 常用插件
- 問題記錄
- Echarts
- 基礎
- Electron
- 序言
- 配置 Electron 開發環境 & 基礎概念
- React + TypeScript 仿 Antd
- TypeScript 基礎
- React + ts
- 樣式設計
- 組件測試
- 圖標解決方案
- Storybook 的使用
- Input 組件
- 在線 mock server
- 打包與發布
- Algorithm
- 排序算法及常見問題
- 劍指 offer
- 動態規劃
- DataStruct
- 概述
- 樹
- 鏈表
- Network
- Performance
- Webpack
- PWA
- Browser
- Safety
- 微信小程序
- mpvue 課程實戰記錄
- 服務器
- 操作系統基礎知識
- Linux
- Nginx
- redis
- node.js
- 基礎及原生模塊
- express框架
- node.js操作數據庫
- 《深入淺出 node.js》筆記
- 前半部分
- 后半部分
- 數據庫
- SQL
- 面試題收集
- 智力題
- 面試題精選1
- 面試題精選2
- 問答篇
- 2025面試題收集
- Other
- markdown 書寫
- Git
- LaTex 常用命令
- Bugs