摘要:如果我們不使用提供的事件系統(tǒng)定時器和,如在事件中進行數(shù)據(jù)更新時,我們需要手動調(diào)用。
前言
在傳統(tǒng)的WEB開發(fā)中,當與用戶或服務器發(fā)生交互時,需要我們手動獲取數(shù)據(jù)并更新DOM,這個過程是繁瑣的、易錯的。
特別是當頁面功能過于復雜時,我們既要關(guān)注數(shù)據(jù)的變化,又要維護DOM的更新,這樣寫出來的代碼是很難維護的。
新一代的框架或庫,例如Angular、React、Vue等等讓我們的關(guān)注點只在數(shù)據(jù)上,當數(shù)據(jù)更新時,這些框架/庫會幫我們更新DOM。
那么這里就有兩個很重要的問題了:當數(shù)據(jù)變化時,這些框架/庫是如何感知到的?當我們連續(xù)更新數(shù)據(jù)時,這些框架/庫如何避免連續(xù)更新DOM,而是進行批量更新?
帶著這兩個問題,我將簡要分析一下React、Angular1、Angular2及Vue的實現(xiàn)機制。
React在更新UI的時候會根據(jù)新老state生成兩份虛擬DOM,所謂的虛擬DOM其實就是JavaScript對象,然后在根據(jù)特定的diff算法比較這兩個對象,找出不同的部分,最后根據(jù)改變的那部分進行對應DOM的更新。
那么React是如何知道數(shù)據(jù)變化了呢?我們通過手動調(diào)用setState告知React我們需要更新的數(shù)據(jù)。
例如我們這里有一個很簡單的組件:
class App extends React.Component { constructor() { super(); this.handleClick = this.handleClick.bind(this); this.state = { val: 0, }; } handleClick() { this.setState({val: 1}); } render() { return ({this.state.val}) } }
當我點擊按鈕的時候調(diào)用this.setState({val: 1});,React就會將this.state.val更新成1,并且自動幫我們更新UI。
如果點擊按鈕的時候我們連續(xù)調(diào)用setState會怎么樣?React是連續(xù)更新兩次,還是只更新一次呢?為了更好的觀察出React的更新機制,我們將點擊按鈕的邏輯換成下面的代碼
this.setState({val: 1}); console.log(this.state.val); this.setState({val: 2}); console.log(this.state.val);
打開控制臺,點擊按鈕你會發(fā)現(xiàn)打印了0 0,同時頁面數(shù)據(jù)也更新成了2。所以我們就得出結(jié)論:React的更新并不是同步的,而是批量更新的。
我們別急著下結(jié)論,我們知道應用程序狀態(tài)的改變主要是下面三種情況引起的:
Events - 如點擊按鈕
Timers - 如setTimeout
XHR - 從服務器獲取數(shù)據(jù)
我們才測試了事件這一種情景,我們試著看看其余兩種情景下state的變化,將點擊按鈕的邏輯換成如下代碼
setTimeout(() => { this.setState({val: 1}); console.log(this.state.val); this.setState({val: 2}); console.log(this.state.val); });
打開控制臺,點擊按鈕你會發(fā)現(xiàn)打印了1 2,相信這個時候很多人就懵了,為啥和第一種情況的輸出不一致,不是說好的批量更新的么,怎么變成連續(xù)更新了。
我們再試試第三種情景XHR,將點擊按鈕的邏輯換成下面的代碼
fetch("/") .then(() => { this.setState({val: 1}); console.log(this.state.val); this.setState({val: 2}); console.log(this.state.val); });
打開控制臺,點擊按鈕你會發(fā)現(xiàn)打印的還是1 2,這究竟是什么情況?如果仔細觀察的話,你會發(fā)現(xiàn)上面的輸出符合一個規(guī)律:在React調(diào)用的方法中連續(xù)setState走的是批量更新,此外走的是連續(xù)更新。
為了驗證這個的猜想,我們試著在React的生命周期方法中連續(xù)調(diào)用setState
componentDidMount() { this.setState({val: 1}); console.log(this.state.val); this.setState({val: 2}); console.log(this.state.val); }
打開控制臺你會發(fā)現(xiàn)打印了0 0 ,更加驗證了我們的猜想,因為生命周期方法也是React調(diào)用的。到此我們可以得出這樣一個結(jié)論:
在React調(diào)用的方法中連續(xù)setState走的是批量更新,此外走的是連續(xù)更新
說到這里,有些人可能會有這樣一個疑惑
handleClick() { setTimeout(() => { this.setState({val: 1}); console.log(this.state.val); this.setState({val: 2}); console.log(this.state.val); }); }
setTimeout也是在handleClick當中調(diào)用的,為啥不是批量更新呢?
setTimeout確實是在handleClick當中調(diào)用的,但是兩個setState可不是在handleClick當中調(diào)用的,它們是在傳遞給setTimeout的參數(shù)——匿名函數(shù)中執(zhí)行的,走的是事件輪詢,不要弄混了。
綜上,說setState是異步的需要加一個前提條件,在React調(diào)用的方法中執(zhí)行,這時我們需要通過回調(diào)獲取到最新的state
this.setState({val: 1}, () => { console.log(this.state.val); });
相信這個道理大家不難理解,因為事件和生命周期方法都是React調(diào)用的,它想怎么玩就怎么玩。那么React內(nèi)部是如何實現(xiàn)批量更新的呢?
事務React當中事務最主要的功能就是拿到一個函數(shù)的執(zhí)行上下文,提供鉤子函數(shù)。啥意思?看個例子
import Transaction from "react/lib/Transaction"; const transaction = Object.assign({}, Transaction.Mixin, { getTransactionWrappers() { return [{ initialize() { console.log("initialize"); }, close() { console.log("close"); } }]; } }); transaction.reinitializeTransaction(); const fn = () => { console.log("fn"); }; transaction.perform(fn);
執(zhí)行這段代碼,打開控制臺會發(fā)現(xiàn)打印如下
initialize fn close
事務最主要的功能就是可以Wrapper一個函數(shù),通過perform調(diào)用,在執(zhí)行這個函數(shù)之前會先調(diào)用initialize方法,等這個函數(shù)執(zhí)行結(jié)束了在調(diào)用close方法。事務的核心代碼很短,只有五個方法,有興趣的可以去看下。
結(jié)合上面setState連續(xù)調(diào)用的情況,我們可以大致猜出React的更新機制,例如執(zhí)行handleClick的時候
let updating = false; setState = function() { if(updating){ // 緩存數(shù)據(jù) }else { // 更新 } } const transaction = Object.assign({}, Transaction.Mixin, { getTransactionWrappers() { return [{ initialize() { updating = true; }, close() { updating = false; // 更新 } }]; } }); transaction.reinitializeTransaction(); transaction.perform(instance.handleClick);
我們再來深入一下setState的實現(xiàn),看看是不是這么回事,下面是setState會調(diào)用到的方法
function enqueueUpdate(component) { ensureInjected(); if (!batchingStrategy.isBatchingUpdates) { batchingStrategy.batchedUpdates(enqueueUpdate, component); return; } dirtyComponents.push(component); if (component._updateBatchNumber == null) { component._updateBatchNumber = updateBatchNumber + 1; } }
看變量名稱我們也都能猜到大致功能,通過batchingStrategy.isBatchingUpdates來決定是否進行batchedUpdates(批量更新),還是dirtyComponents.push(緩存數(shù)據(jù)),結(jié)合事務,React的批量更新策略應該是這樣的
const transaction = Object.assign({}, Transaction.Mixin, { getTransactionWrappers() { return [{ initialize() { batchingStrategy.isBatchingUpdates = true; }, close() { batchingStrategy.isBatchingUpdates = false; } }]; } }); transaction.reinitializeTransaction(); transaction.perform(instance.handleClick); transaction.perform(instance.componentDidMount);小結(jié)
React通過setState感知到數(shù)據(jù)的變化,通過事務進行批量更新,通過Virtual DOM比較進行高效的DOM更新。
Angular1 Dirty CheckingAngular1通過臟值檢測去更新UI,所謂的臟值檢測其實指Angular1從$rootScope開始遍歷所有scope的$$watchers數(shù)組,通過比較新老值來決定是否更新DOM。看個例子
{{val}}
angular.module("myApp", []) .controller("MyCtrl", function($scope) { $scope.val = 0; });
這個是一個很簡單的數(shù)據(jù)渲染的例子,我們在控制臺打印下scope,看下$$watchers的內(nèi)容
因為只有val一個表達式所以$$watchers長度只有1
eq 是否進行數(shù)據(jù)的深度比較
exp 檢測出錯時log所用
fn 更新DOM
get 獲取當前數(shù)據(jù)
last 老的數(shù)據(jù)
那么Angular1是如何感知到數(shù)據(jù)變化的呢?
$applyAngular1通過調(diào)用$scope.$apply()進行臟值檢測的,核心代碼如下
遍歷所有scope的$$watchers,通過get獲取到最新值同last比較,值變化了則通過調(diào)用fn更新DOM。有人可能會疑惑了,我們在編碼的時候并沒有調(diào)用$apply,那么UI是怎么更新的呢?
實際上是Angular1幫我們調(diào)用了,我們看下ng事件的源碼實現(xiàn)
forEach( "click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste".split(" "), function(eventName) { var directiveName = directiveNormalize("ng-" + eventName); ngEventDirectives[directiveName] = ["$parse", "$rootScope", function($parse, $rootScope) { return { restrict: "A", compile: function($element, attr) { var fn = $parse(attr[directiveName], /* interceptorFn */ null, /* expensiveChecks */ true); return function ngEventHandler(scope, element) { element.on(eventName, function(event) { var callback = function() { fn(scope, {$event:event}); }; if (forceAsyncEvents[eventName] && $rootScope.$$phase) { scope.$evalAsync(callback); } else { scope.$apply(callback); } }); }; } }; }]; } );
很明顯調(diào)用了$scope.$apply,我們再看下$timeout的源碼
function timeout(fn, delay, invokeApply) { // ... timeoutId = $browser.defer(function() { try { deferred.resolve(fn.apply(null, args)); } catch (e) { deferred.reject(e); $exceptionHandler(e); } finally { delete deferreds[promise.$$timeoutId]; } if (!skipApply) $rootScope.$apply(); }, delay); // ... }
最后也調(diào)用了$rootScope.$apply,$http服務實際上也做了同樣的處理,說到這,三種引起應用程序狀態(tài)變化的情景,Angular1都做了封裝,所以我們寫代碼的時候不需要手動去調(diào)用$apply了。
新手常碰到的一個問題就是為啥下面的代碼不起作用
$("#btn").on("click", function() { $scope.val = 1; });
因為我們沒有用Angular1提供的事件系統(tǒng),所以Angular1沒法自動幫我們調(diào)用$apply,這里我們只能手動調(diào)用$apply進行臟值檢測了
$("#btn").on("click", function() { $scope.val = 1; $scope.$apply(); });小結(jié)
在Angular1中我們是直接操作數(shù)據(jù)的,這個過程Angular1是感知不到的,只能在某個點調(diào)用$apply進行臟值檢測,所以默認就是批量更新。如果我們不使用Angular1提供的事件系統(tǒng)、定時器和$http,如在jQuery事件中進行數(shù)據(jù)更新時,我們需要手動調(diào)用$apply。
Angular2當數(shù)據(jù)變化時,Angular2從根節(jié)點往下遍歷進行更新,默認Angular2深度遍歷數(shù)據(jù),進行新老數(shù)據(jù)的比較來決定是否更新UI,這點和Angular1的臟值檢測有點像,但是Angular2的更新沒有副作用,是單向數(shù)據(jù)流。
同時大家也不用擔心性能問題
It can perform hundreds of thousands of checks within a couple of milliseconds. This is mainly due to the fact that Angular generates VM friendly code — by Pascal Precht
Angular2也提供了不同的檢測策略,例如
@Component({ selector: "child", template: `{{data.name}}`, changeDetection: ChangeDetectionStrategy.OnPush })
設置了變化檢測策略為OnPush的組件不走深度遍歷,而是直接比較對象的引用來決定是否更新UI。
Zone.jsAngular2同Angular1一樣都是直接操作數(shù)據(jù)的,框架都無法直接感知數(shù)據(jù)的變化,只能在特定的時機去做批量更新。
Angular1是通過封裝自動調(diào)用$apply,但是存在手動調(diào)用的場景,為了解決這個問題,Angular2沒有采用1的實現(xiàn)機制,轉(zhuǎn)而使用了Zone.js。
Zone.js最主要的功能就是可以獲取到異步方法執(zhí)行的上下文。什么是執(zhí)行上下文?例如
function foo() { bar(); } foo(); baz();
同步的方法我們可以明確的知道bar什么時候執(zhí)行和結(jié)束,可以在bar結(jié)束的時候調(diào)用baz。但是對于異步方法,例如
function foo() { bar(); } setTimeout(foo); baz();
我們無法知道foo是什么時候開始執(zhí)行和結(jié)束,因為它是異步的。如果調(diào)用改成這樣
function foo() { bar(); } setTimeout(function() { foo(); baz(); });
通過添加一層wrapper函數(shù),不就可以保證在foo執(zhí)行完調(diào)用baz了么。Zone.js主要重寫了瀏覽器所有的異步實現(xiàn),如setTimeout、XMLHttpRequest、addEventListener等等,然后提供鉤子函數(shù),
new Zone().fork({ beforeTask: function() { console.log("beforeTask"); }, afterTask: function() { console.log("afterTask"); } }).run(function mainFn() { console.log("main exec"); setTimeout(function timeoutFn() { console.log("timeout exec"); }, 2000); });
打開控制臺,你會發(fā)現(xiàn)打印如下
beforeTask main exec afterTask beforeTask timeout exec afterTask
Zone.js捕獲到了mainFn和timeoutFn執(zhí)行的上下文,這樣我們就可以在每個task執(zhí)行結(jié)束后執(zhí)行更新UI的操作了。Angular2更新機制大體如下
class ApplicationRef { changeDetectorRefs:ChangeDetectorRef[] = []; constructor(private zone: NgZone) { this.zone.onTurnDone .subscribe(() => this.zone.run(() => this.tick()); } tick() { this.changeDetectorRefs .forEach((ref) => ref.detectChanges()); } }
ngZone是對Zone.js的服務封裝,Angular2會在每個task執(zhí)行結(jié)束后觸發(fā)更新。
小結(jié)由于Zone.js的存在,我們可以在任何場景下更新數(shù)據(jù)而無需手動調(diào)用檢測,Angular2也是批量更新。
VueVue模板中每個指令/數(shù)據(jù)綁定都有一個對應的watcher對象,當數(shù)據(jù)變化時,會觸發(fā)watcher重新計算并更新相應的DOM。
setterVue通過Object.defineProperty將data轉(zhuǎn)化為getter/setter,這樣我們直接修改數(shù)據(jù)時,Vue就能夠感知到數(shù)據(jù)的變化了,這個時候就可以進行UI更新了。
如果我們連續(xù)更新數(shù)據(jù),Vue會立馬更新DOM還是和React一樣先緩存下來等待狀態(tài)穩(wěn)定進行批量更新呢?我們還是從應用程序狀態(tài)改變的三種情景來看
var vm = new Vue({ el: "#app", data: { val: 0 }, methods: { onClick: function() { vm.val = 1; console.log(vm.$el.textContent); vm.val = 2; console.log(vm.$el.textContent); } } });
打開控制臺,點擊按鈕會發(fā)現(xiàn)打印0 0,說明Vue并不是立馬更新的,走的是批量更新。由于事件系統(tǒng)用的Vue提供的,是可控的,我們再看下定時器下執(zhí)行的情況
var vm = new Vue({ el: "#app", data: { val: 0 } }); setTimeout(function() { vm.val = 1; console.log(vm.$el.textContent); vm.val = 2; console.log(vm.$el.textContent); });
打開控制臺,點擊按鈕會發(fā)現(xiàn)依舊打印了0 0,有人可能就疑惑了Vue是不是跟Angular2一樣也修改了異步方法的原生實現(xiàn)呢?
Vue并沒有這么干,不用于React、Angular1/2捕獲異步方法上下文去更新,Vue采用了不同的更新策略。
每當觀察到數(shù)據(jù)變化時,Vue就開始一個隊列,將同一事件循環(huán)內(nèi)所有的數(shù)據(jù)變化緩存起來。如果一個watcher被多次觸發(fā),只會推入一次到隊列中。
等到下一次事件循環(huán),Vue將清空隊列,只進行必要的DOM更新。在內(nèi)部異步隊列優(yōu)先使用MutationObserver,如果不支持則使用setTimeout(fn, 0) — vuejs.org
這是官方文檔上的說明,抽象成代碼就是這樣的
var waiting = false; var queue = []; function setter(val) { if(!waiting) { waiting = true; setTimeout(function() { queue.forEach(function(item) { // 更新DOM }); waiting = false; queue = []; }, 0); } else { queue.push(val); } } setter(1); setter(2);
Vue是通過JavaScript單線程的特性,利用事件隊列進行批量更新的。
config.async我們可以通過將Vue.config.async設置為false,關(guān)閉異步更新機制,讓它變成同步更新,看下面的例子
Vue.config.async = false; var vm = new Vue({ el: "#app", data: { val: 0 } }); setTimeout(function() { vm.val = 1; console.log(vm.$el.textContent); vm.val = 2; console.log(vm.$el.textContent); });
打開控制臺你會發(fā)現(xiàn)打印了1 2,但是最好別這么干
總結(jié)如果關(guān)閉了異步模式,Vue 在檢測到數(shù)據(jù)變化時同步更新 DOM。在有些情況下這有助于調(diào)試,但是也可能導致性能下降,并且影響 watcher 回調(diào)的調(diào)用順序。async: false不推薦用在生產(chǎn)環(huán)境中 — vuejs.org
自此我們分析了React、Angular1/2和Vue的變化檢測以及批量更新的策略。
React和Angular1/2都是通過獲取執(zhí)行上下文來進行批量更新,但是React和Angular1支持的并不徹底,都有各自的問題。
Angular2可以適配任意情況,但是是通過篡改了原生方法實現(xiàn)的。Vue則通過ES5特性和JavaScript單線程的特性進行批量更新,無需特殊處理,可以滿足任何情況。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/80450.html
摘要: TensorFlow Object Detection API Hangs On?—?Training and Evaluating using Custom Object Detector *The links to all files updated and the GitHub repo address added. First of All Install TensorF...
摘要:感謝您的閱讀如果喜歡這篇文章請點贊。它對我意義重大,它能幫助其他人看到這篇文章。對于更高級的文章,你可以在或上跟隨我。 I’ve worked with Angular.js for a few years and despite the widespread criticism I think this is a fantastic framework. I’ve started w...
摘要:在這堂課中,學生將可以學習到深度學習的基礎,學會構(gòu)建神經(jīng)網(wǎng)絡,包括和等。課程中也會有很多實操項目,幫助學生更好地應用自己學到的深度學習技術(shù),解決真實世界問題。 深度學習入門首推課程就是吳恩達的深度學習專項課程系列的 5 門課。該專項課程最大的特色就是內(nèi)容全面、通俗易懂并配備了豐富的實戰(zhàn)項目。今天,給大家推薦一份關(guān)于該專項課程的核心筆記!這份筆記只能用兩個字形容:全面! showImg(...
摘要:編寫工作首先介紹了一個稱為的內(nèi)部組件表示,并解釋了變更檢測過程在視圖上運行。本文主要由兩部分組成第一部分探討錯誤產(chǎn)生的原因,第二部分提出可能的修正。它對我意義重大,它能幫助其他人看到這篇文章。 在過去的8個月里,我大部分空閑時間都是reverse-engineering Angular。我最感興趣的話題是變化檢測。我認為它是框架中最重要的部分,因為它負責像DOM更新、輸入綁定和查詢列表...
摘要:但如果一個組件在生命周期鉤子里改變父組件屬性,卻是可以的,因為這個鉤子函數(shù)是在更新父組件屬性變化之前調(diào)用的注即第步,在第步之前調(diào)用。 原文鏈接:Angular.js’ $digest is reborn in the newer version of Angular showImg(https://segmentfault.com/img/remote/146000001468785...
閱讀 718·2021-09-24 09:48
閱讀 2516·2021-08-26 14:14
閱讀 542·2019-08-30 13:08
閱讀 1475·2019-08-29 15:22
閱讀 3112·2019-08-29 11:06
閱讀 1029·2019-08-26 18:26
閱讀 1131·2019-08-26 13:53
閱讀 2605·2019-08-26 12:21