一晃就到2020年了,時間過得真的是飛快,伴隨着q羣一些熱心小夥伴的反饋和我我的實際的業務落地場景,Concent
已進入一個很是穩定的運行階段了,在此開年之際,新開一個雜談系列,會不按期更新,用於作一些總結或者回顧,內容比較隨心,想到哪裏寫到哪裏,不會擡拘於風格和形式,重在探討和溫故知新,並激發靈感,本期雜談的主題是精確更新,文章將綜合對比現有業界的各類方案,來看看Concent
如何另闢蹊徑,給React
加上精確更新這門不可或缺的重型武器吧。javascript
本文主題是精確更新,爲什麼這裏要提變化檢測呢,由於歸根到底,3個框架Angular
、Vue
和React
可以實現數據驅動視圖,本質就是須要首先創建起一套完善的機制來感知到數據發生變化且是哪些數據發生變化了,從而進一步去作數據對應的視圖更新工做。html
那麼差別化的部分就是各家對如何感知到數據發生變化了這個細節的具體實現了,下面咱們淺顯的總結一下它們的變化檢測套路。vue
這裏主要說的是ng2以後改進髒檢查機制,在咱們寫下下面一段代碼聲明瞭這樣一個組件後,在每個組件實例化的過程當中ng都會配套維護着一個變化檢測器,因此視圖渲染完畢生成dom樹後,其實ng也同時擁有了一個變化檢測樹,angular利用zone
優化了整個變化檢測週期的觸發時機,每一輪變化檢測週期內經過淺比較收集到發生改變的屬性來進一步以爲該更新哪些dom片斷了,同時也配套提供ChangeDetectorRef
來讓用戶重寫變化檢測規則,人工干預某個組件的變化檢測關閉和激活時機,來進一步提高性能。java
一個簡單的angular組件以下react
@Component({
template: ` <h1>{{firstName}} {{lastName}}</h1> <button (click)="changeName()">change name</button> `
})
class MyApp {
firstName:string = 'Jim';
lastName:string = 'Green';
changeName() {
this.firstName = 'JimNew';
this.lastName = 'GreenNew';
}
}
複製代碼
注意上文裏提到了在變化檢測週期內經過淺比較收集變化屬性,這也是爲何當成員變量是對象時,咱們須要重賦值對象引用,而不是改原有引用的值,以免檢測失效。git
@Component(/**略*/)
class MyApp {
list: string[] = [];
changeList() {
const list = this.list;
list.push('new item');
this.list = list;// bad
this.list = list.slice();// good
}
}
複製代碼
Vue
號稱響應式的mvvm,核心原理就是在你實例化你的vue組件時,框架劫持了你的組件數據源,轉變爲一個個Observable
可觀察對象,因此模板裏的各類取值表達式在模板編譯爲函數期間或者再次渲染期間都隱式的觸發了可觀察對象的getter
,這樣vue就順利的收集到了不一樣視圖對不一樣數據的依賴,這些依賴Dep
則添加相關的訂閱者Watcher
實例(即組件實例,每一個組件實例都對應一個 watcher 實例),當若是用戶修改了數據則隱式的觸發了setter
,框架感知到了數據變動就會發布通知,讓全部訂閱者更新內容,改變視圖(即調用了相關組件實例的update方法)github
一個簡單的vue組件以下(採用單文件寫法):編程
<template>
<h1>{{firstName}} {{lastName}}</h1>
<button @click="changeName">change name</button>
</template>
<script> export default { data() { return { firstName: "Jim", lastName: "Green", } }, methods: { changeName: function () { this.firstName = 'JimNew'; this.lastname = 'GreenNew'; } } } </script>
複製代碼
固然了,可觀察對象的轉換也並非如咱們想象的那樣所有轉換掉,vue爲了性能考慮會折中考慮只監聽一層,若是對象層級過深時,watch
表達式裏須要用戶手寫深度監聽函數,對象賦值處須要調用工具函數來處理redux
methods: {
changeName: function () {
this.somObj.name = 'newName';// bad
Vue.set(this.somObj, 'name', 'newName');// good
this.somObj = Object.assign({}, this.somObj, {name: 'newName'});// good
}
}
複製代碼
methods: {
replaceListItem: function () {
this.somList[2] = 'newName';// bad
Vue.set(this.somList, 2, 'newName');// good
}
}
複製代碼
固然若是你不想使用工具函數的話,使用$forUpdate
也能達到刷新視圖的目的api
methods: {
replaceListItem: function () {
// not good, but it works
this.somList[2] = 'newName';
this.$forceUpdate();
}
}
複製代碼
注,vue2 與 vue3轉變可觀察對象的方式已經不同了,2採用
defineProperty
,3採用proxy
,因此在vue3在對象動態添加屬性這種場景下也能主動感知到數據變化了。
記得很早以前,尤雨溪的一篇訪談裏談論react
和vue
的異同時,提到了react
是一個pull based
的框架而vue
是一個push based
的框架,兩種設計理念沒有孰好孰壞之分,只有不一樣場景下看誰更適合而已,push based
可讓框架主動分析出數據的更新粒度和拆分出渲染區域不一樣依賴,因此對於初學者來講不用關注細節就能更容易寫出一些性能較好的代碼。
react
感知到數據變化的入口是setState
,用戶主動觸發這個接口,框架拉取到最新的數據從而進行視圖更新,可是其實從react
角度來看沒有感知到數據變化一說,由於你只要顯示的調用了setState
就表示要驅動進行新一輪的渲染了。
以下面例子所示,上一刻的obj和新的obj是同一個引用,點擊了按鈕照樣會觸發視圖渲染。
class Foo extends React.Component{
state = { obj:{} };
handleClick = ()=> this.setState({obj:this.state.obj});
render(){
return <button onCLick={this.handleClick}>click me</button>
}
}
複製代碼
因此很顯然react
把變化檢測這個這一步交給了用戶,若是obj沒有變化,你爲何要調用setState
呢,若是你調用了就是告訴react
須要更新視圖了,哪怕上一刻和下一刻數據源如出一轍也同樣會更新視圖。
更重要的是,默認狀況下react
組件是至上而下所有渲染的,因此react
配套出了shouldComponentUpdate
接口,React.memo
接口和PureComponent
組件等來幫助react
識別出不須要更新的視圖區域,來阻礙這種株連式的更新策略,從而致使了有些人議論react
學習曲線較大,給人更多的心智負擔。
固然了,react16以後穩定了的Context api
也算是變化檢測的手段之一,經過Context.Provider
來從某個組件根節點注入關心變化的對象,在根節點裏各個子孫節點須要消費的具體數據處包裹Context.Comsumer
來達到目的。
上面咱們提到裸寫的react
是沒有變化檢測的,可是提供了配套的函數來輔助其完成檢測,社區裏固然也有很多優秀的方案,如redux
,提供一個全局的單一數據源,讓不一樣的視圖監聽數據源裏不一樣的數據,從而當用戶修改數據時,遍歷全部監聽去執行對應回調。
固然redux
自己與框架無關只是一個庫,具體的變化檢測須要框架相關的對應的去實現,這裏咱們要提到的實現就是react-redux
了,提供了connect
裝飾器來幫助組件完成檢測過程,以便決定組件是否須要被更新。
咱們來看一個典型的使用了redux的組件
const mapStateToProps = state => {
return { loginName: state.login.name, product: state.product };
}
@connect(mapStateToProps)
class Foo extends React.Component {
render() {
const { loginName, product } = this.props;
// 渲染邏輯略
}
}
複製代碼
mapStateToProps
實際上是一個狀態選擇操做,挑出想要的狀態映射到實例的props
上,變化檢測發生哪一步呢?經過源碼咱們會知道connect
經過高階組件,在包裹層完成了訂閱操做以便監聽store
數據變化,訂閱的回調函數計算出當前組件該不應渲染,咱們實例化的組件時實際上是包裹後的組件,該組件實現了shouldComponentUpdate
行爲,在它重渲染期間會按照react
的生命週期流程調用到shouldComponentUpdate
以決定當前組件實例是否須要更新。
注意咱們提到了一個訂閱機制,由於redux
自身的實現原理,當單一狀態樹上任何一個數據節點發生改變時,其實因此高階組件的訂閱回調都會被執行,具體組件該不應更新,回調函數裏會淺比較前一刻的狀態和後一刻狀態來決定當前實例需不要更新,因此這也是爲何redux
強調若是狀態改變了,必定老是要返回新的狀態,以便輔助淺比較可以正常工做,固然順帶實現了時間回溯功能,可是大多數時候咱們的應用自己是不須要此功能的,而redux-dev-tool
卻是很是依賴單一狀態在不一樣時間的快照來實現重放功能。
因此從使用者角度來講,不須要顯示去關心shouldComponentUpdate
也可以寫出性能更好的應用了。
下面示例演示了state發生了改變,必需老是返回最新的
const initState = { list: [] };
export const oneReudcer = (state = initState, action) => {
const { type, payload } = action;
switch (type) {
case 'ADD':
const list = state.list;
list.push(payload);
return { list: [...list] };// right
return { list] };// wrong !!!
default:
return state;
}
}
複製代碼
由於list提高到了store,因此咱們在react組件某個方法裏若是寫爲下面格式是起效的,可是放redux
裏,就必須嚴格按照它的運行規則來。
const list = this.state.list;
list.push(payload);
this.setState({list})
複製代碼
某種程度來講,mobx
結合了react
後有種vue
的味道了,mobx
也有一個本身的store
,可是數據都是observalbe
的,因此同樣的主動檢測到數據變化。
當時代碼組織方式更oop
而非函數式。
Concent
本質上也沒有擴展額外的檢測策略,和react
保持100%一致,setState
就是更新入口,react
的setState
行爲和Concent
的setState
行爲徹底同樣,惟一的區別就是Concent
爲了用戶的書寫體驗新增了其餘更新入口函數,以及擴展了函數的參數(非必需填入)。
咱們先建立store的一個子模塊foo
來演示下3種主要入口
import { run } from 'concent';
run({
foo: {//聲明一個模塊foo
state: { list: [], name:'' }
}
});
複製代碼
setState
import { register, useConcent } from 'concent';
//類寫法
@register('foo')
class CompClazz extends React.Component {
addItem = () => {
const list = this.state.list;
list.push(Math.random());
this.setState({ list });// trigger render
}
render() {
return (
<div> {this.state.list.length} <button onCLick={this.addItem}>add item</button> </div>
)
}
}
//函數寫法
function CompFn() {
const ctx = useConcent('foo');
addItem = () => {
const list = ctx.state.list;
list.push(Math.random());
ctx.setState({ list });// trigger render
};
return (
<div> {ctx.state.list.length} <button onCLick={ctx.addItem}>add item</button> </div>
)
}
複製代碼
固然了上面寫法裏咱們能夠進一步優化下,抽出setup
,避免了函數組件裏重複建立新的函數,同時能夠和類一塊兒複用
const setup = (ctx) => {
return {
addItem = () => {
const list = ctx.state.list;
list.push(Math.random());
ctx.setState({ list });// trigger render
}
}
}
@register({ module: 'foo', setup })
class CompClazz extends React.Component {
render() {
return (
<div> {this.state.list.length} <button onCLick={this.ctx.settings.addItem}>add item</button> </div>
)
}
}
//函數寫法
function CompFn() {
const ctx = useConcent({ module: 'foo', setup });
return (
<div> {ctx.state.list.length} <button onCLick={ctx.settings.addItem}>add item</button> </div>
)
}
複製代碼
dispatch
先在模塊定義裏添加reducer函數
run({
foo: {//聲明一個模塊foo
state: { list: [], name: '' },
reducer: {
addItem(payload, moduleState) {// 定義reducer函數
const list = moduleState.list;
list.push(Math.random());
return { list };// trigger render
},
async addItemAsync(){/** 一樣也支持async函數 */}
}
}
});
複製代碼
改寫下setup
const setup = (ctx) => {
return {
addItem = () => ctx.dispatch('addItem'),
// 固然了這裏這直接支持調用reducer函數
addItem = () => ctx.moduleReducer.addItem(),
}
}
@register({ module: 'foo', setup })
class CompClazz extends React.Component {/**略*/}
function CompFn() {
const ctx = useConcent({ module: 'foo', setup });
/**略*/
}
複製代碼
invoke
invoke
直接繞過reducer
函數定義,調用用戶的自定義函數改寫狀態,咱們先定義一個addItem
函數,它和reducer裏的函數並沒有寫法區別,只是放置的位置不一樣而已,逃離了reducer
這個區域,直接和setup
放在一塊兒。
function addItem(payload, moduleState) {
const list = moduleState.list;
list.push(Math.random());
return { list };// trigger render
}
const setup = (ctx) => {
return {
addItem = () => ctx.invoke(addItem)
}
}
@register({ module: 'foo', setup })
class CompClazz extends React.Component {/**略*/}
function CompFn() {
const ctx = useConcent({ module: 'foo', setup });
/**略*/
}
複製代碼
總之無論形式怎麼變,本質仍是和react
數據驅動的核心保持一致,即經過入口輸入一個新的片斷狀態,觸發視圖渲染。
這裏談到了精確更新,咱們先明確爲什麼須要精確更新,當咱們的數據提高到store
後,有多個組件消費着store
不一樣模塊的不一樣部分數據,注意這裏提到的模塊,redux
裏自己是沒有模塊的概念的,儘管子reducer
塊看起來有點雛形了,可是dva
、rematch
等基於redux
底層封裝出模塊概念更切合咱們的編程思路,將模塊的狀態和修改方法都內聚到一個model
下,而不是分散的寫在各個文件裏,讓咱們更友好的按功能來切分各個模塊和組織代碼。
在模塊多且組件多以後,可能會產生了一些錯綜複雜的關係,不一樣組件會鏈接不一樣的多個模塊,消費着模塊裏的不一樣部分數據,當這些模塊裏的數據發生變動時,只應該通知對應的關心者觸發渲染,而不是暴力的所有都渲染,因此咱們須要一些額外的機制來保證渲染區域的精確度,即最大限度的縮小渲染範圍,已得到更高的運行時性能。
如下咱們提出的案例場景,以及精確更新比較,主要是針對react內部的3個框架
react-redux
、react-mobx
、concent
三個庫作比較,再也不說起vue
和angular
這種場景很是常見,多個組件消費同一個模塊的數據,可是消費的粒度不同,假設咱們有以下一個模塊的狀態
bookState = {
name:'',
age:'',
list: [],
}
複製代碼
組件A鏈接book模塊,消費name
與age
,組件B鏈接book模塊消費list
,組件C鏈接book模塊全部數據
@connect(state=> ({name: state.book.name, age: state.book.age }))
class A extends React.Component{}
@connect(state=> ({list: state.book.list }))
class B extends React.Component{}
@connect(state=> state.book)
class C extends React.Component{}
複製代碼
@inject('book')
@observer
class A extends React.Component{
render(){
const { name, age } = this.props.book;
//使用name,age
}
}
@inject('book')
@observer
class B extends React.Component{
render(){
const { list } = this.props.book;
//使用list
}
}
@inject('book')
@observer
class C extends React.Component{
render(){
const { name, age, list } = this.props.book;
//使用name age list
}
}
複製代碼
@register({ module:'book', watchedKeys:['name', 'age']})
class A extends React.Component{
render(){
const { name, age } = this.state;
//使用name,age
}
}
@register({ module:'book', watchedKeys:['list']})
class B extends React.Component{
render(){
const { list } = this.state;
//使用list
}
}
@register('book')// 感知book模塊的所有key變化,就不須要在顯式的指定watchedKeys範圍了
class C extends React.Component{
render(){
const { name, age, list } = this.state;
//使用name age list
}
}
複製代碼
以上代碼都在約束react的渲染範圍,從寫法來看,mbox
自動的完成了依賴收集,concent
因其依賴標記原理須要顯示的讓用戶標定須要感知變化的key,因此會多一些筆墨,redux
這須要connnect
經過函數完成狀態的挑選,會有更多的代碼產生,因此代碼輕量程度來講結果是
mobx
>concent
>redux
效率來講,mbox
和concent
都是在作精準通知,由於mbox
經過getter
收集到數據變動關聯的視圖依賴,而concent
經過依賴標記和引用收集完成了數據變動關聯的視圖依賴,當數據變動時都是直接通知相對應的視圖直接更新,而redux
須要遍歷全部的listeners,觸發全部實例的訂閱回調函數,又回調函數計算出當前訂閱組件實例需不須要更新。
Concent本身維護着一個全局上下文,用於分類和索引全部的組件實例,任何一個Concent組件實例修改狀態的行爲都會攜帶有模塊信息,當狀態改變的那一刻,Concent已知道該怎麼分發狀態到其餘實例!
索引模塊與類的關係
索引類和類實例的關係
鎖定相關實例觸發更新
因此效率上來講結果是
(mobx
,concent
)>redux
由於其不一樣的場景有不一樣的測試準則mobx
和concent
還暫時作不出比較。
這個場景很常見,例如遍歷某個list下的全部元素,爲每個元素渲染一個組件,這個組件可以走統一的方法修改本身在store裏的數據,可是由於修改的本身的數據,理論上來講只應該觸發本身渲染,而不是觸發整個list渲染.
如下代碼暫時沒法實現此場景,由於基於redux的設計目前還辦不到這一點,對於經過store的list遍歷出來的視圖,沒法經過參數來標記當前組件消費消費的是某一個下標的元素,同時又修改了它處於list裏某個位置的元素數據後,只渲染這個元素對應的視圖。
// BookItem聲明
@conect(state => {
return { list: state.book.list },
}, dispatch=>{
return {modBookName: (idx)=> dispatch('modBookName', idx)}
})
class BookItem extends React.Component(){
render(){
const { idx, list } = this.props;
const bookData = list[idx];
const modBookName = ()=> this.props.modBookName(idx);
// ui 略
}
}
// BookItemContainer聲明
@conect(state => {
return { list: state.book.list }
})
class BookItemContainer extends React.Component(){
render(){
const { list } = this.props;
return (
<div> {list.map((v, idx) => <BookItem key={idx} idx={idx} />)} </div> ) } } 複製代碼
reducer裏
export const book = (state, action)=>{
switch(action.type){
case 'modBookName':
const list = state.list;
const idx = action.payload;
const bookItem = list[idx];
bookItem.name = Math.random();
// 此處一定會引發整個BookItemContainer以及包含的全部BookItem重渲染
return {list:[...list]};
}
}
複製代碼
@register({module:'book', watchedKeys:['list']})
class BookItem extends React.Component(){
render(){
const { list } = this.state;
const bookData = list[this.props.idx];
const renderKey = this.ctx.ccUniqueKey;
//dispatch(type:string, payload?:any, renderKey?:string)
const modBookName = ()=> this.ctx.dispatch('modBookName', idx, renderKey;
//也能夠寫爲
const modBookName = ()=> this.ctx.moduleReducer.modBookName(idx, renderKey);
}
}
// BookItemContainer聲明
@register({module:'book', watchedKeys:['list']})
class BookItemContainer extends React.Component(){
render(){
const { list } = this.state;
return (
<div> {list.map((v, idx) => <BookItem key={idx} idx={idx} />)} </div> ) } } 複製代碼
當實例攜帶renderKey調用時,concent會去尋找和傳遞的renderKey值同樣的實例觸發渲染,而每個cc實例,若是沒有人工設置renderKey的話,默認的renderKey值就是ccUniqueKey(即每個cc實例的惟一索引),因此當咱們擁有大量的消費了store某個模塊下同一個key如sourceList(一般是map和list)下的不一樣數據的組件時,若是調用方傳遞的renderKey就是本身的ccUniqueKey, 那麼renderKey機制將容許組件修改了sourceList下本身的數據同時也只觸發本身渲染,而不觸發其餘實例的渲染,這樣大大提升這種list場景的渲染性能。
此示例完整代碼在線示例見此處 stackblitz.com/edit/concen…
redux
的更新機制在典型的list或者map場合已不能知足需求,mobx
和concent
都能知足,mobx
偏向於oop
的方式組織代碼,concent
完全的面向函數式且因其setState
就與store
打通了的能力,能與react
天生無縫結合,能夠若入侵的直接接入,且其精確更新能力依然保持非凡實力。
另外concent
獨立代碼組織方式,讓你少寫大量中間代碼且架構更爲優雅,以下兩個計算器示例。
實例1基於hook,來自於一個印度同志。 點我查看實例1
實例2基於concent,上圖中箭頭處都將抽象爲model的不一樣部分。點我查看實例1
最後的視圖渲染則經過useConcent
輕鬆和模塊打通。
❤ star me if you like concent ^_^,Concent的發展離不開你們的精神鼓勵與支持,也期待你們瞭解更多和提供相關反饋,讓咱們一塊兒構建更有樂趣,更加健壯和更高性能的react應用吧。
強烈建議有興趣的你進入在線IDE fork代碼修改哦(如點擊圖片無效可點擊文字連接)
若是有關於concent的疑問,能夠掃碼加羣諮詢,我會盡力答疑解惑,幫助你瞭解更多。