Android真響應式架構——MvRx

前言

Android真響應式架構系列文章:

Android真響應式開發——MvRx
Epoxy——RecyclerView的絕佳助手
Android真響應式架構——Model層設計
Android真響應式架構——數據流動性
Android真響應式架構——Epoxy的使用
Android真響應式架構——MvRx和Epoxy的結合
Android單向數據流——MvRx核心源碼解析

Airbnb 最近開源了一個庫,他們稱之為Android界的Autopilot——MvRx(ModelView ReactiveX的縮寫,讀作mavericks)。這個庫并不“單純”,它其實是一個架構,已經被應用在了Airbnb幾乎所有的產品上。
這個庫綜合運用了以下幾種技術

  • Kotlin (MvRx is Kotlin first and Kotlin only)
  • Android Architecture Components
  • RxJava
  • React (概念上的)
  • Epoxy (可選但推薦)

光看這個清單,也知道事情并不簡單。利用這個庫我們可以方便地構建出MVVM架構的APP,讓開發更加的簡單、高效。

1. 真響應式架構

響應式(React)架構并沒有什么定義,只是我覺得這么描述MvRx比較準確。這里所說的響應式架構是指,數據響應式以及界面響應式。數據響應式大體指數據以流的形式呈現(RxJava那套東西),界面響應式大體指數據驅動界面更新,界面顯示與數據狀態保持一致。
以如上的定義來看,在RxJava的幫助下,幾乎所有架構都可以實現數據響應式,因為數據響應式實際上是Model層的設計。但是界面響應式則基本上沒有哪個框架實現了,最接近的應該是Android Architecture Components,但是Android Architecture Components并沒有保證界面與數據狀態的一致,我們通過LiveData通知界面更新,只是把數據帶給了界面,界面顯示與數據狀態并不一定是一致的(例如,LiveData攜帶了下一頁的數據,界面只是把該數據加到了RecyclerView的后面,數據并沒有完全代表了當前界面的狀態)。而MvRx真正實現了界面的響應式,所以我稱之為真響應式架構。
如果你了解過Flutter,那么MvRx很容易理解,因為兩者都采用了響應式構建的思想,以下是關于Flutter的描述,把它替換為MvRx也基本上適用。

Flutter 組件采用現代響應式框架構建,這是從 React 中獲得的靈感,中心思想是用組件 (widget) 構建你的 UI。 組件描述了在給定其當前配置和狀態時他們顯示的樣子。當組件狀態改變,組件會重構它的描述 (description),Flutter 會對比之前的描述,以確定底層渲染樹從當前狀態轉換到下一個狀態所需要的最小更改。

由于Flutter的實現不受原生的限制,它完全用另外一套方式實現了界面的渲染,并且響應式在設計之初就是Flutter的核心,所以在Flutter中任何組件(可以理解為Android中的View)都是響應式的,都可以確定它從當前狀態轉換到下一個狀態所需要的最小更改,顯然這一點在原生Android上是實現不了的。而MvRx在原生Android的基礎上幾乎實現了所有界面的響應式,這一點還是非常厲害的。

1.1 命令式MVP與響應式MVVM

MVP模式在Android界一直很流行,因為它比較好理解。其核心思想是,通過接口隔離數據與顯示,數據的變動通過接口回調的方式去通知界面更新。這正是典型的命令式M-V(數據-顯示)鏈接。在這種模式下View層是完全被動的,完全受控于Presenter層的命令。這種模式并沒有什么大問題,只是有一些不太方便之處,主要體現在M-V的緊密鏈接,導致復用比較困難,要么View層需要定義不必要的接口(這樣Presenter可以復用),要么就需要為幾乎每個View都定義一個對應的Presenter,想想都心累。
不同于MVP通過接口的方式來隔離數據與顯示,MVVM是使用觀察者的方式來隔離數據與顯示。以Android Architecture Components構建的MVVM模式為例,View通過觀察LiveData來驅動界面更新。MVVM帶來的主要好處是打破了M-V的緊密鏈接,ViewModel復用變得很簡單,View層需要什么數據觀察什么數據即可。將View抽離為觀察者,可以實現響應式MVVM架構,只是View本身不是響應式的。
以我的實踐來看Android Architecture Components構建的MVVM的主要問題是,RxJava與LiveData的銜接并不方便,還有就是按照Google給出的sample,數據加載的狀態需要和數據本身打包在一起,然后通過LiveData傳遞出去,我覺得這不是一個好的做法。我在實踐中是在Observer的onSubscribe,onNext,onError方法中分別對不同的MutableLiveData賦值,然后在View中去觀察這些LiveData來更新界面的。說實話,這很丑陋,但是比Google給出的sample要方便許多。

1.2 MvRx的真響應式MVVM

MvRx構建的MVVM模式,完美地解決了上述的問題。MvRx放棄了LiveData,使用State來通知View層數據的改變(當然仍然是可感知生命周期的)。MvRx可以方便地把RxJava Observable的請求過程包裝成Ansyc類,不僅可以改變State來通知View層,而且也包含了數據加載的狀態(成功、失敗、加載中等)。如果結合Airbnb的另一個開源庫Epoxy,那么幾乎可以做到真正的響應式,即View層在數據改變時僅僅描述當前數據狀態下界面的樣子,Epoxy可以幫我們實現與之前數據狀態的比較,然后找出差別,僅更新那些有差別的View部分。這是對MvRx的大致描述。下面來看看MvRx是如果使用的。

2. MvRx的使用

2.1 MvRx的重要概念

MvRx有四個重要的概念,分別是State、ViewModel、View和Async。

State

包含界面顯示的所有數據,實現類需是繼承自MvRxState的immutable Kotlin data class。像是這樣

data class TasksState(
    val tasks: List<Task> = emptyList(),
    val taskRequest: Async<List<Task>> = Uninitialized,
    val isLoading: Boolean = false,
    val lastEditedTask: String? = null
) : MvRxState //MvRxState 僅是一個標記接口

State的作用是承載數據,并且應該包含有界面顯示的所有數據。當然可以對界面進行拆分,使用多個State共同決定界面的顯示。
State必須是不可變的(immutable),即State的所有屬性必須是val的。只有ViewModel可以改變State,改變State時一般使用其copy方法,創建一個新的State對象。
可以把MvRx的State類比成Architecture Components中的LiveData,它們的相同點是都可以被View觀察,不同點是,State的改變會觸發View的invalidate()方法,從而通知界面重繪。

ViewModel

完全繼承自Architecture Components中的ViewModel,ViewModel包含有除了界面顯示之外的業務邏輯。此外,最關鍵的一點是,ViewModel還包含有一個State,ViewModel可以改變State的狀態,然后View可以觀察State的狀態。實現類需繼承BaseMvRxViewModel,并且必須向BaseMvRxViewModel傳遞initialState(代表了View的初始狀態)。像是這樣

class TasksViewModel(initialState: TasksState) : BaseMvRxViewModel<TasksState>(initialState)

View

一般而言是一個繼承自BaseMvRxFragment的Fragment。BaseMvRxFragment實現了接口MvRxView,這個接口有一個invalidate()方法,每當ViewModel的state發生改變時invalidate()方法都會被調用。View也可以觀察State中的某個或某幾個屬性的變化,View是沒辦法改變State狀態的,只有ViewModel可以改變State的狀態。

Async

代表了數據加載的狀態。Async是一個Kotlin sealed class,它有四種類型:Uninitialized, Loading, Success, Fail(包含了一個名為error的屬性,可以獲取錯誤類型)。Async重載了操作符invoke,除了在Success返回數據外,其它情況下都返回null:

var foo = Loading()
println(foo()) // null
foo = Success<Int>(5)
println(foo()) // 5
foo = Fail(IllegalStateException("bar"))
println(foo()) // null

在ViewModel中可以通過擴展函數executeObservable<T>的請求過程包裝成Asnyc<T>,這可以方便地表示數據獲取的狀態(下面會有介紹)。

以上四個核心概念是怎么聯系到一起的呢?請看下圖:

MvRx

圖中沒有包含AsnycState可包含若干個Asnyc,用來表示數據加載的狀態,便于顯示Loading或者加載錯誤信息等。
按照理想情形,View不需要主動觀察State,State的任意改變都會調用View的invalidate方法,在invalidate方法中根據當前的State(在View中通過ViewModel的withState方法獲取State)直接重繪一下View即可。然而這太過于理想,實際上可以通過selectSubscribe,asyncSubscribe等方法觀察State中某個屬性的改變,根據特定的屬性更新View的特定部分。

以上是MvRx的四個核心概念。下面以官方sample為例,展示一下MvRx應該怎樣使用。

2.2 如何使用

ToDo Sample,架構界的Hello World。界面張這個樣子。

https://upload-images.jianshu.io/upload_images/4803763-cd28075c161e7b28.gif?imageMogr2/auto-orient/strip

以下以首界面為例,介紹應該如何使用MvRx。

2.2.1 State的使用

//待辦事的定義,包含有id, title, description以及是否完成標志complete
data class Task(
    var title: String = "",
    var description: String = "",
    var id: String = UUID.randomUUID().toString(),
    var complete: Boolean = false
)

data class TasksState(
    val tasks: List<Task> = emptyList(), //界面上的待辦事
    val taskRequest: Async<List<Task>> = Uninitialized, //代表請求的狀態
    val isLoading: Boolean = false, //是否顯示Loading
    val lastEditedTask: String? = null //上次編輯的待辦事ID
) : MvRxState

State包含了這個界面要顯示的所有數據。

2.2.2 ViewModel的使用

具體的業務邏輯并不重要,主要看ViewModel是如何定義的。

/**
 * 必須有一個initialState
 * source是數據源,可以是數據庫,也可以是網絡請求等(例子中是數據庫)
 **/
class TasksViewModel(initialState: TasksState, private val source: TasksDataSource) : MvRxViewModel<TasksState>(initialState) {
    //工廠方法,必須實現MvRxViewModelFactory接口
    companion object : MvRxViewModelFactory<TasksViewModel, TasksState> {
        /**
         * 主要用途是通過依賴注入傳入一些參數來構造ViewModel
         * TasksState是MvRx幫我們構造的(通過反射)
         **/
        override fun create(viewModelContext: ViewModelContext, state: TasksState): BaseMvRxViewModel<TasksState> {
            //例子中并沒有使用依賴注入,而是直接獲取數據庫
            val database = ToDoDatabase.getInstance(viewModelContext.activity)
            val dataSource = DatabaseDataSource(database.taskDao(), 2000)
            return TasksViewModel(state, dataSource)
        }
    }
    
    init {
        //方便調試,State狀態改變時打印出來
        logStateChanges()
        //初始加載任務
        refreshTasks()
    }

    //獲取待辦事
    fun refreshTasks() {
        source.getTasks()
            .doOnSubscribe { setState { copy(isLoading = true) } }
            .doOnComplete { setState { copy(isLoading = false) } }
            //execute把Observable包裝成Async
            .execute { copy(taskRequest = it, tasks = it() ?: tasks, lastEditedTask = null) }
    }

    //新增或者更新待辦事
    fun upsertTask(task: Task) {
        //通過setState改變 State的狀態
        setState { copy(tasks = tasks.upsert(task) { it.id == task.id }, lastEditedTask =  task.id) }
        //因為是數據庫操作,一般不會失敗,所以沒有理會數據操作的狀態
        source.upsertTask(task)
    }

    //標記任務完成與否
    fun setComplete(id: String, complete: Boolean) {
        setState {
            //沒有這個任務,拉倒;this指之前的 State,直接返回之前的 State意思就是無需更新
            val task = tasks.findTask(id) ?: return@setState this
            //這個任務已經完成了,拉倒
            if (task.complete == complete) return@setState this
            //找到這個任務,并更新
            copy(tasks = tasks.copy(tasks.indexOf(task), task.copy(complete = complete)), lastEditedTask = id)
        }
        //數據庫更新
        source.setComplete(id, complete)
    }

    //清空已完成的待辦事
    fun clearCompletedTasks() = setState {
        source.clearCompletedTasks()
        copy(tasks = tasks.filter { !it.complete }, lastEditedTask = null)
    }

    //刪除待辦事
    fun deleteTask(id: String) {
        setState { copy(tasks = tasks.delete { it.id == id }, lastEditedTask = id) }
        source.deleteTask(id)
    }
}

ViewModel實現了業務邏輯,其核心作用就是與Model層(這里的source)溝通,并更新State。這里有幾點需要說明:

  1. 按照MvRx的要求,ViewModel可以沒有工廠方法,這樣的話MvRx會通過反射構造出ViewModel(當然這一般不可能,畢竟ViewModel一般都包含Model層)。如果ViewModel包含有除initialState之外的其它構造參數,則需要我們實現工廠方法。如上所示,必須通過伴生對象實現MvRxViewModelFactory接口。
  2. 只能在ViewModel中更新State。更新State有兩種方法,setState或者executesetState很好理解,直接更新State即可。其定義如下
abstract class BaseMvRxViewModel<S : MvRxState> {
    //參數是State上的擴展函數,會接收到上次 State的值
    protected fun setState(reducer: S.() -> S) {
        //...
    }
}

因為State是immutable Kotlin data class,所以一般而言都是通過data class的copy方法返回新的State。execute是一個擴展方法,其定義如下

abstract class BaseMvRxViewModel<S : MvRxState> {
    /**
     * Helper to map an observable to an Async property on the state object.
     */
    //參數依然是State上的擴展函數
    fun <T> Observable<T>.execute(
        stateReducer: S.(Async<T>) -> S
    ) = execute({ it }, null, stateReducer)

    /**
     * Execute an observable and wrap its progression with AsyncData reduced to the global state.
     *
     * @param mapper A map converting the observable type to the desired AsyncData type.
     * @param successMetaData A map that provides metadata to set on the Success result.
     *                        It allows data about the original Observable to be kept and accessed later. For example,
     *                        your mapper could map a network request to just the data your UI needs, but your base layers could
     *                        keep metadata about the request, like timing, for logging.
     * @param stateReducer A reducer that is applied to the current state and should return the
     *                     new state. Because the state is the receiver and it likely a data
     *                     class, an implementation may look like: `{ copy(response = it) }`.
     *
     *  @see Success.metadata
     */
    fun <T, V> Observable<T>.execute(
        mapper: (T) -> V,
        successMetaData: ((T) -> Any)? = null,
        stateReducer: S.(Async<V>) -> S
    ): Disposable {
        setState { stateReducer(Loading()) }

        return map {
                val success = Success(mapper(it))
                success.metadata = successMetaData?.invoke(it)
                success as Async<V>
            }
            .onErrorReturn { Fail(it) }
            .subscribe { asyncData -> setState { stateReducer(asyncData) } }
            .disposeOnClear() //ViewModel clear的時候dispose
    }
}

execute方法可以把Observable的請求過程包裝成Async,我們都知道訂閱Observable需要有onNext,onComplete,onError等方法,execute就是把這些個方法包裝成了統一的Async類。前面已經說過,Async是sealed class,只有四個子類:Uninitialized, Loading, Success, Fail。這些子類完美的描述了一次請求的過程,并且它們重載了invoke操作符(Success情況下返回請求的數據,其它情況均為null)。因此經常看到這樣的樣板代碼:

fun <T> Observable<T>.execute(
    stateReducer: S.(Async<T>) -> S
)

/**
 * 根據上面execute的定義,我們傳遞過去的是State上的以Async<T>為參數的擴展函數
 * 因此下面的it參數是指 Async<T>,it()是獲取請求的結果,tasks = it() ?: tasks 表示只在請求 Success時更新State
 **/
fun refreshTasks() {
    source.getTasks()
        //...
        .execute { copy(taskRequest = it, tasks = it() ?: tasks, lastEditedTask = null) }
}

2.2.3 View的使用

abstract class BaseFragment : BaseMvRxFragment() {
    //activityViewModel是MvRx定義的獲取ViewModel的方式
    //按照規范必須使用activityViewModel、fragmentViewModel、existingViewModel(都是Lazy<T>類)獲取ViewModel
    protected val viewModel by activityViewModel(TasksViewModel::class)

    //Epoxy的使用
    protected val epoxyController by lazy { epoxyController() }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        //可以觀察State中某個(某幾個)屬性的變化
        viewModel.selectSubscribe(TasksState::tasks, TasksState::lastEditedTask) { tasks, lastEditedTask ->
            //...
        }

        //觀察Async屬性
        viewModel.asyncSubscribe(TasksState::taskRequest, onFail = {
            coordinatorLayout.showLongSnackbar(R.string.loading_tasks_error)
        })
    }

    //State的改變均會觸發
    override fun invalidate() {
        //Epoxy的用法
        recyclerView.requestModelBuild()
    }

    abstract fun epoxyController(): ToDoEpoxyController
}

class TaskListFragment : BaseFragment() {
    //另一個ViewModel
    private val taskListViewModel: TaskListViewModel by fragmentViewModel()

    //Epoxy的使用
    override fun epoxyController() = simpleController(viewModel, taskListViewModel) { state, taskListState ->
        // We always want to show this so the content won't snap up when the loader finishes.
        horizontalLoader {
            id("loader")
            loading(state.isLoading)
        }

        //...
    }
}

按照MvRx的規范,View通過activityViewModel(ViewModel被置于Activity中), fragmentViewModel(ViewModel被置于Fragment中), existingViewModel(從Activity中獲取已存在的ViewModel) 來獲取ViewModel,這是因為,以這幾種方式獲取ViewModel,MvRx會幫我們完成如下幾件事:

  1. activityViewModel, fragmentViewModel, existingViewModel其實都是Kotlin的Lazy子類,顯然會是懶加載。但是它不是真正的“懶”,因為在這些子類的構造函數中會添加一個對View生命周期的觀察者,在ON_CREATE事件發生時會構造出ViewModel,也就是說ViewModel最晚到ON_CREATE時即被構造完成(為了及早發出網絡請求等)。
  2. 通過反射構造出State,ViewModel。
  3. 調用ViewModel的subscribe方法,觀察State的改變,如果改變則調用View的invalidate方法。

當State發生改變時,View的invalidate方法會被調用。invalidate被調用僅說明了State發生了改變,究竟是哪個屬性發生的改變并不得而知,按照MvRx的“理想”,哪個屬性發生改變并不重要,只要View根據當前的State“重繪”一下View即可。這里“重繪”顯然指的不是簡單地重繪整個界面,應該是根據當前State“描繪”當前界面,然后與上次界面作比較,只更新差異部分。顯然這種“理想”太過于高級,需要有一個幫手來完成這項任務,于是就有了Epoxy(其實是先有的Epoxy)。
Epoxy簡單來說就是RecyclerView的高級助手,我們只需要定義某個數據在RecyclerView的ItemView上是如何顯示的,然后把一堆數據扔給Epoxy就行了。Epoxy會幫我們分析這次的數據跟上次的數據有什么差別,只更新差別的部分。如此看來Epoxy真的是MvRx的絕佳助手。關于Epoxy有非常多的內容,查看Epoxy——RecyclerView的絕佳助手了解更多。
Epoxy雖然“高級”,但也僅僅適用于RecyclerView。因此可以看到MvRx的例子中把所有界面的主要部分都以RecyclerView承載,例如,Loading出現在RecyclerView的頭部;如果界面是非滾動的,就把界面作為RecyclerView唯一的元素放入其中,等等。這都是為了使用Epoxy,使開發模式更加統一,并且更加接近于完全的響應式。但是總有些情形下界面不適合用RecyclerView展示,沒關系,我們還可以單獨觀察State中的某(幾)個屬性的改變(這幾乎與LiveData沒有差別)。例如:

    //觀察兩個屬性的改變,任意一個屬性方式了改變都會調用
    viewModel.selectSubscribe(TasksState::tasks, TasksState::lastEditedTask) { tasks, lastEditedTask ->
        //根據屬性值做更新
    }

    //觀察Async屬性,可以傳入onSuccess、onFail參數
    //和上面觀察普通屬性沒有區別,只是內部幫我們判斷了Async是否成功
    viewModel.asyncSubscribe(TasksState::taskRequest, onFail = {
        coordinatorLayout.showLongSnackbar(R.string.loading_tasks_error)
    })

3. 問題

使用MvRx有幾個問題需要注意:

  1. State是immutable Kotlin data class,Kotlin幫我們生成了equals方法(即調用每個屬性的equals方法),在ViewModel中通過setState,execute方法更新State時,只有更新后的State確實與上一次的State不相等時,View才會收到通知。經常犯的錯誤是這樣的:
data class CheckedData(
    val id: Int,
    val name: String,
    var checked: Boolean = false
)

//List的equals方法的實現是,項數相同,并且每項都equals
data class SomeState(val data: List<CheckedData> = emptyList()) : MvRxState

class SomeViewModel(initialState: SomeState) : MvRxViewModel<SomeState>(initialState) {
    fun setChecked(id: Int) {
        setState {
            copy(data = data.find { it.id == id }?.checked = true)
        }
    }
}

這樣做是不行的(也是不允許的),SomeState的data雖然改變了,但對比上一次的SomeState,它們是相等的,因為前后兩個SomeState的data指向了同一塊內存,必然是相等的,因此不會觸發View更新。需要這么做:

fun <T> List<T>.update(newValue: (T) -> T, finder: (T) -> Boolean) = indexOfFirst(finder).let { index ->
    if (index >= 0) copy(index, newValue(get(index))) else this
}

fun <T> List<T>.copy(i: Int, value: T): List<T> = toMutableList().apply { set(i, value) }

//最好修改為如下定義,防止直接修改checked屬性
data class CheckedData(
    val id: Int,
    val name: String,
    //只讀的
    val checked: Boolean = false
)

class SomeViewModel(initialState: SomeState) : MvRxViewModel<SomeState>(initialState) {
    fun setChecked(id: Int) {
        setState {
            copy(data = data.update({ it.copy(checked = true) }, { it.id == id }))
        }
    }
}

這樣前后兩個SomeState的data指向不同的內存,并且這兩個data確實不同,會觸發View更新。

  1. 緊接著上一點來說,對于State而言,如果改變的值與上次的值相同是不會引起View更新的,這是很合理的行為。但是,如果確實需要在State不變的情況下更新View(例如State中包含的某個屬性更新頻繁,你不想創造太多新對象;或者某些屬性只能在原來的對象上更新,例如SparseArray,查看源碼后發現,壓根兒就不能在State的屬性中使用SparseArray),那么MvRx的確沒有辦法。別忘了,MvRx與Android Architecture Components是并行不悖的,你總是可以使用LiveData去實現。對于MutableLiveData而言,設置相同的值還是會通知其觀察者,是MvRx很好的補充。(但是,并不推薦這么做,因為使用LiveData會破壞State的不可變性,等于你繞開了MvRx,用另外一種方式去傳遞數據,這不利于數據的統一,也不利于數據界面的一致,不到萬不得已不推薦這么做。)

  2. MvRx構建初始的initialState和ViewModel都使用的是反射,并且MvRx支持通過Fragment的arguments構造initialState,然而,大多數時候,ViewModel的initialState是確定的,完全沒有必要通過反射獲取。如果使用MvRx規范中的fragmentViewModel等方式獲取,反射是不可避免的,如果追求性能的話,可以通過拷貝fragmentViewModel的代碼,去除其中的反射,構建自己的獲取ViewModel的方法。

  3. 雖說MvRx為ViewModel的構建提供了工廠方法,并且這些工廠方法主要目的也是為了依賴注入,但實際上如果真的結合dagger依賴注入的話,你會發現構造ViewModel變得比較麻煩。而且這種做法并沒有利用dagger multiBindings的優勢。實際上dagger可以為ViewModel提供非常友好且便利的ViewModelProvider.Factory類(這在Android Architecture Components的sample中已經有展示),但是MvRx卻沒有提供一種方法來使用自定義的ViewModelProvider.Factory類(見Issues)。

  4. 在我看來,MvRx最大的特點是響應式,最大的問題也是響應式。因為這種開發模式,與我們之前培養的命令式的開發思維是沖突的,開始的時候總會有種不適應感。最重要的是切換我們的思維方式。

總結

總的來說,MvRx提供了一種Android更純粹響應式開發的可能性。并且以Airbnb的實踐來看,這種可能性已經被擴展到相當廣的范圍。MvRx最適合于那些復雜的RecyclerView界面,通過結合Epoxy,不僅可以大大提高開發效率,而且其提供的響應式思想可以大大簡化我們的思維。其實,有了Epoxy的幫助,絕大部分界面都可以放入RecyclerView中。對于不適宜使用RecyclerView的界面,或者RecyclerView之外的一些界面元素,MvRx至少也提供了與Android Architecture Components相似的能力,并且其與RxJava的結合更加的友好。
MvRx的出現非常符合安迪-比爾定律,硬件的升級遲早會被軟件給消耗掉,或者換種更積極的說法啊,正是因為硬件的發展才給了軟件開發更多的創造力。想想MvRx,由于State是Immutable的,每次更新View必然會產生新的State;想實現真正的響應式,也必然需要浪費更多的計算力,去幫我們計算界面真正更新的部分(實際上我們是可以提前知曉的)。但我覺得這一切都是值得的,畢竟這些許的算力對于現在的手機來說不值一提,但是對于“人”的效率的提升卻是巨大的。還是那句話,最關鍵的因素還是人啊!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容