目錄html
wiki上的解釋前端
reactive programming is a declarative programming paradigm concerned with data streams and the propagation of change(響應式開發是一種專一於數據流和變化傳播的聲明式編程範式)vue
所謂響應式編程,是指不直接進行目標操做,而是用另一種更爲簡潔的方式經過代理達到目標操做的目的。react
聯想一下,在各個前端框架中,咱們如今要改變視圖,不是用jquery命令式地去改變dom,而是經過setState(),修改this.data或修改$scope.data...jquery
舉個例子angularjs
let a =3; let b= a*10; console.log(b) //30 a=4 //b = a * 10 console.log(b)//30
這裏b並不會自動根據a的值變化,每次都須要b = a * 10再設置一遍,b纔會變。因此這裏不是響應式的。算法
B和A之間就像excel裏的表格公式同樣。
B1的值要「響應式」地根據A1編輯的值相應地變化編程
A | B | |
---|---|---|
1 | 4 | 40(fx=A1*10) |
onAChanged(() => { b = a * 10 })
假設咱們實現了這個函數:onAChanged。你能夠認爲這是一個觀察者,一個事件回調,或者一個訂閱者。
這無所謂,關鍵在於,只要咱們完美地實現了這個方法,B就能永遠是10倍的a。redux
若是用命令式(命令式和聲明式)的寫法來寫,咱們通常會寫成下面這樣:後端
<span class="cell b1"></span> document .querySelector(‘.cell.b1’) .textContent = state.a * 10
把它改的聲明式一點,咱們給它加個方法:
<span class="cell b1"></span> onStateChanged(() => { document .querySelector(‘.cell.b1’) .textContent = state.a * 10 })
更進一步,咱們的標籤轉成模板,模板會被編譯成render函數,因此咱們能夠把上面的js變簡單點。
模板(或者是jsx渲染函數)設計出來,讓咱們能夠很方便的描述state和view之間的關係,就和前面說的excel公式同樣。
<span class="cell b1"> {{ state.a * 10 }} </span> onStateChanged(() => { view = render(state) })
咱們如今已經獲得了那個漂亮公式,你們對這個公式都很熟悉了:
view = render(state)
這裏把什麼賦值給view,在於咱們怎麼看。在虛擬dom那,就是個新的虛擬dom樹。咱們先無論虛擬dom,認爲這裏就是直接操做實際dom。
可是咱們的應用怎麼知道何時該從新執行這個更新函數onStateChanged?
let update const onStateChanged = _update => { update = _update } const setState = newState => { state = newState update() }
設置新的狀態的時候,調用update()方法。狀態變動的時候,更新。
一樣,這裏只是一段代碼示意。
在react裏:
onStateChanged(() => { view = render(state) }) setState({ a: 5 })
redux:
store.subscribe(() => { view = render(state) }) store.dispatch({ type: UPDATE_A, payload: 5 })
angularjs
$scope.$watch(() => { view = render($scope) }) $scope.a = 5 // auto-called in event handlers $scope.$apply()
angular2+:
ngOnChanges() { view = render(state) }) state.a = 5 // auto-called if in a zone Lifecycle.tick()
真實的框架裏確定不會這麼簡單,而是須要更新一顆複雜的組件樹。
如何實現的?是同步的仍是異步的?
髒檢查覈心代碼
(可具體看test_cast第30行用例講解)
Scope.prototype.$$digestOnce = function () { //digestOnce至少執行2次,並最多10次,ttl(Time To Live),能夠看test_case下gives up on the watches after 10 iterations的用例 var self = this; var newValue, oldValue, dirty; _.forEachRight(this.$$watchers, function (watcher) { try { if (watcher) { newValue = watcher.watchFn(self); oldValue = watcher.last; if (!self.$$areEqual(newValue, oldValue, watcher.valueEq)) { self.$$lastDirtyWatch = watcher; watcher.last = (watcher.valueEq ? _.cloneDeep(newValue) : newValue); watcher.listenerFn(newValue, (oldValue === initWatchVal ? newValue : oldValue), self); dirty = true; } else if (self.$$lastDirtyWatch === watcher) { return false; } } } catch (e) { // console.error(e); } }); return dirty; };
digest循環是同步進行。當觸發了angularjs的自定義事件,如ng-click,$http,$timeout等,就會同步觸發髒值檢查。(angularjs-demos/twowayBinding)
惟一優化就是經過lastDirtyWatch變量來減小watcher數組後續遍歷(這裏能夠看test_case:'ends the digest when the last watch is clean')。demo下有src
其實提供了一個異步更新的API叫$applyAsync。須要主動調用。
好比$http下設置useApplyAsync(true),就能夠合併處理幾乎在相同時間獲得的http響應。
angularjs爲何將會逐漸退出(注意不是angular),雖然目前仍然有大量的歷史項目仍在使用。
調和代碼
function reconcile(parentDom, instance, element) { //instance表明已經渲染到dom的元素對象,element是新的虛擬dom if (instance == null) { //1.若是instance爲null,就是新添加了元素,直接渲染到dom裏 // Create instance const newInstance = instantiate(element); parentDom.appendChild(newInstance.dom); return newInstance; } else if (element == null) { //2.element爲null,就是刪除了頁面的中的節點 // Remove instance parentDom.removeChild(instance.dom); return null; } else if (instance.element.type === element.type) { //3.類型一致,咱們就更新屬性,複用dom節點 // Update instance updateDomProperties(instance.dom, instance.element.props, element.props); instance.childInstances = reconcileChildren(instance, element); //調和子元素 instance.element = element; return instance; } else { //4.類型不一致,咱們就直接替換掉 // Replace instance const newInstance = instantiate(element); parentDom.replaceChild(newInstance.dom, instance.dom); return newInstance; } } //子元素調和的簡單版,沒有匹配子元素加了key的調和 //這個算法只會匹配子元素數組同一位置的子元素。它的弊端就是當兩次渲染時改變了子元素的排序,咱們將不能複用dom節點 function reconcileChildren(instance, element) { const dom = instance.dom; const childInstances = instance.childInstances; const nextChildElements = element.props.children || []; const newChildInstances = []; const count = Math.max(childInstances.length, nextChildElements.length); for (let i = 0; i < count; i++) { const childInstance = childInstances[I]; const childElement = nextChildElements[I]; const newChildInstance = reconcile(dom, childInstance, childElement); //遞歸調用調和算法 newChildInstances.push(newChildInstance); } return newChildInstances.filter(instance => instance != null); }
setState不會當即同步去調用頁面渲染(否則頁面就會一直在刷新了😭),setState經過引起一次組件的更新過程來引起從新繪製(一個事務裏).
源碼的setState在src/isomorphic/modern/class/ReactComponent.js下(15.0.0)
舉例:
this.state = { count:0 } function incrementMultiple() { const currentCount = this.state.count; this.setState({count: currentCount + 1}); this.setState({count: currentCount + 1}); this.setState({count: currentCount + 1}); }
上面的setState會被加上多少?
在React的setState函數實現中,會根據一個變量isBatchingUpdates判斷是直接更新this.state仍是放到隊列中回頭再說,而isBatchingUpdates默認是false,也就表示setState會同步更新this.state,可是,有一個函數batchedUpdates,這個函數會把isBatchingUpdates修改成true,而當React在調用事件處理函數以前就會調用這個batchedUpdates,形成的後果,就是由React控制的事件處理過程setState不會同步更新this.state。
但若是你寫個setTimeout或者使用addEventListener添加原生事件,setState後state就會被同步更新,而且更新後,當即執行render函數。
(示例在demo/setState-demo下)
那麼react會在何時統一更新呢,這就涉及到源碼裏的另外一個概念事務。事務這裏就不詳細展開了,咱們如今只要記住一點,點擊事件裏無論設置幾回state,都是處於同一個事務裏。
核心代碼:
export function defineReactive(obj, key, val) { var dep = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { // console.log('geter be called once!') var value = val if (Dep.target) { dep.depend() } return value }, set: function reactiveSetter(newVal) { // console.log('seter be called once!') var value = val if (newVal === value || (newVal !== newVal && value !== value)) { return } val = newVal dep.notify() } }) }
react的setState
vue的this.Obj.x = xxx
angular的state.x = x
優化方法
在vue中,組件的依賴是在渲染過程當中自動追蹤的,因此係統能精確知道哪一個組件確實須要被重渲染。你能夠理解爲每個組件都已經自動得到了shouldComponentUpdate,但依賴收集太過細粒度的時候,也是有必定的性能開銷。
MVP是MVC的變種
View與Model不發生聯繫,都經過Presenter傳遞。Model和View的徹底解耦
View很是薄,不部署任何業務邏輯,稱爲「被動視圖」,即沒有任何主動性,而Presenter很是厚,全部邏輯都在這裏。
Presenter調用View的方法去設置界面,仍然須要大量的、煩人的代碼,這實在是一件不舒服的事情。
能不能告訴View一個數據結構,而後View就能根據這個數據結構的變化而自動隨之變化呢?
因而ViewModel出現了,經過雙向綁定省去了不少在View層中寫不少case的狀況,只須要改變數據就行。(angularjs和vuejs都是典型的mvvm架構)
另外,MVC太經典了,目前在客戶端(IOS,Android)以及後端仍然普遍使用。
controller 和 view 層高耦合
下圖是view層和controller層在前端和服務端如何交互的,能夠看到,在服務端看來,view層和controller層只兩個交互。透過前端和後端的之間。
可是把mvc放到前端就有問題了,controller高度依賴view層。在某些框架裏,甚至是被view來建立的(好比angularjs的ng-controller)。controller要同時處理事件響應和業務邏輯,打破了單一職責原則,其後果多是controller層變得愈來愈臃腫。
過於臃腫的Model層
另外一方面,前端有兩種數據狀態須要處理,一個是服務端過來的應用狀態,一個是前端自己的UI狀態(按鈕置不置灰,圖標顯不顯示,)。一樣違背了Model層的單一職責。
組件就是: 視圖 + 事件處理+ UI狀態.
下圖能夠看到Flux要作的事,就是處理應用狀態和業務邏輯
很好的實現關注點分離
虛擬dom其實就是一個輕量的js對象。
好比這樣:
const element = { type: "div", props: { id: "container", children: [ { type: "input", props: { value: "foo", type: "text" } }, { type: "a", props: { href: "/bar" } }, { type: "span", props: {} } ] } };
對應於下面的dom:
<div id="container"> <input value="foo" type="text"> <a href="/bar"></a> <span></span> </div>
經過render方法(至關於ReactDOM.render)渲染到界面
function render(element, parentDom) { const { type, props } = element; const dom = document.createElement(type); const childElements = props.children || []; childElements.forEach(childElement => render(childElement, dom)); //遞歸 parentDom.appendChild(dom); // ``` 對其添加屬性和事件監聽 }
jsx
<div id="container"> <input value="foo" type="text" /> <a href="/bar">bar</a> <span onClick={e => alert("Hi")}>click me</span> </div>
一種語法糖,若是不這麼寫的話,咱們就要直接採用下面的函數調用寫法。
babel(一種預編譯工具)會把上面的jsx轉換成下面這樣:
const element = createElement( "div", { id: "container" }, createElement("input", { value: "foo", type: "text" }), createElement( "a", { href: "/bar" }, "bar" ), createElement( "span", { onClick: e => alert("Hi") }, "click me" ) );
createElement會返回上面的虛擬dom對象,也就是一開始的element
function createElement(type, config, ...args) { const props = Object.assign({}, config); const hasChildren = args.length > 0; props.children = hasChildren ? [].concat(...args) : []; return { type, props }; //...省略一些其餘處理 }
一樣,咱們在寫vue實例的時候通常這樣寫:
// template模板寫法(最經常使用的) new Vue({ data: { text: "before", }, template: ` <div> <span>text:</span> {{text}} </div>` }) // render函數寫法,相似react的jsx寫法 new Vue({ data: { text: "before", }, render (h) { return ( <div> <span>text:</span> {{text}} </div> ) } })
因爲vue2.x也引入了虛擬dom,他們會先被解析函數轉換成同一種表達方式
new Vue({ data: { text: "before", }, render(){ return this.__h__('div', {}, [ this.__h__('span', {}, [this.__toString__(this.text)]) ]) } })
這裏的this.__h__ 就和react下的creatElement方法一致。
最後,模板的裏的表達式都是怎麼變成頁面結果的?
舉個簡單的例子,好比在angular或者vue的模板裏寫上{{a+b}}
通過詞法分析(lexer)就會變成一些符號(Tokens)
[ {text: 'a', identifier: true}, {text: '+'}, {text: 'b', identifier: true} ]
而後通過(AST Builder)就轉化成抽象語法數(AST)
{ type: AST.BinaryExpression, operator: '+', left: { type: AST.Identifier, name: 'a' }, right: { type: AST.Identifier, name: 'b' } }
最後通過AST Compiler變成表達式函數
function(scope) { return scope.a + scope.b; }
(能夠看下angularjs源碼test_case下516行的'parses an addition',最後ASTCompiler.prototype.compile返回的函數)
響應式開發最流行的庫:rxjs
Netflix,google和微軟對reactivex項目的貢獻很大reactivex
RxJS是ReactiveX編程理念的JavaScript版本。ReactiveX來自微軟,它是一種針對異步數據流的編程。簡單來講,它將一切數據,包括HTTP請求,DOM事件或者普通數據等包裝成流的形式,而後用強大豐富的操做符對流進行處理,使你能以同步編程的方式處理異步數據,並組合不一樣的操做符來輕鬆優雅的實現你所須要的功能。
示例在demos/rxjs-demo下
響應式開發是趨勢,當前各個前端框架都有本身的響應式系統實現。另外,Observable應該會加入到ES標準裏,可能會在ES7+加入。
https://medium.freecodecamp.org/is-mvc-dead-for-the-frontend-35b4d1fe39ec?gi=3d39e0be4c84#.q25l7qkpu