摘要:響應式數據響應式數據不是憑空出現的。對于對象而言,如果是之前不存在的屬性,首先可以將進行響應化處理比如調用,然后將對具體屬性定義監聽比如調用函數,最后再去做賦值,可能具體的處理過程千差萬別,但是內部實現的原理應該就是如此僅僅是猜測。
前言
首先歡迎大家關注我的Github博客,也算是對我的一點鼓勵,畢竟寫東西沒法獲得變現,能堅持下去也是靠的是自己的熱情和大家的鼓勵。
國內前端算是屬于Vue與React兩分天下,提到Vue,最令人印象深刻的就是雙向綁定了,想要深入的理解雙向綁定,最重要的就是明白響應式數據的原理。這篇文章不會去一字一句的分析Vue中是如何實現響應式數據的,我們只會從原理的角度去考量如何實現一個簡單的響應式模塊,希望能對你有些許的幫助。
響應式數據不是憑空出現的。對于前端工程而言,數據模型Model都是普通的JavsScript對象。View是Model的體現,借助JavaScript的事件響應,View對Model的修改非常容易,比如:
var model = { click: false }; var button = document.getElementById("button"); button.addEventListener("click", function(){ model.click = !model.click; })
但是想要在修改Model時,View也可以對應刷新,相對比較困難的。在這方面,React和View提供了兩個不同的解決方案,具體可以參考這篇文章。其中響應式數據提供了一種可實現的思路。什么是響應式數據?在我看來響應式數據就是修改數據的時候,可以按照你設定的規則觸發一系列其他的操作。我們想實現的其實就是下面的效果:
var model = { name: "javascript" }; // 使傳入的數據變成響應式數據 observify(model); //監聽數據修改 watch(model, "name", function(newValue, oldValue){ console.log("name newValue: ", newValue, ", oldValue: ", oldValue); }); model.name = "php"; // languange newValue: php, oldValue: javascript
從上面效果中我們可以看出來,我們需要劫持修改數據的過程。好在ES5提供了描述符屬性,通過方法Object.defineProperty我們可以設置訪問器屬性。但是包括IE8在內的低版本瀏覽器是沒有實現Object.defineProperty并且也不能通過polyfill實現(其實IE8是實現了該功能,只不過只能對DOM對象使用,并且非常受限),因此在低版本瀏覽器中沒法實現該功能。這也就是為什么Vue不支持IE8及其以下的瀏覽的原因。通過Object.defineProperty我們可以實現:
Object.defineProperty(obj, "prop", { enumerable: true, configurable: true, set: function(value){ //劫持修改的過程 }, get: function(){ //劫持獲取的過程 } });數據響應化
根據上面的思路我們去考慮如何實現observify函數,如果我們想要將一個對象響應化,我們則需要遍歷對象中的每個屬性,并且需要對每個屬性對應的值同樣進行響應化。代碼如下:
// 數據響應化 // 使用lodash function observify(model){ if(_.isObject(model)){ _.each(model, function(value, key){ defineReactive(model, key, value); }); } } //定義對象的單個響應式屬性 function defineReactive(obj, key, value){ observify(value); Object.defineProperty(obj, key, { configurable: true, enumerable: true, set: function(newValue){ var oldValue = value; value = newValue; //可以在修改數據時觸發其他的操作 console.log("newValue: ", newValue, " oldValue: ", oldValue); }, get: function(){ return value; } }); }
上面的函數observify就實現了對象的響應化處理,例如:
var model = { name: "MrErHu", message: { languange: "javascript" } }; observify(model); model.name = "mrerhu" //newValue: mrerhu oldValue: MrErHu model.message.languange = "php" //newValue: php oldValue: javascript model.message = { db: "MySQL" } //newValue: {db: "MySQL"} oldValue: {languange:"javascript"}
我們知道在JavaScript中經常使用的不僅僅是對象,數組也是非常重要的一部分。并且中還有非常的多的方法能夠改變數組本身,那么我們如何能夠監聽到數組的方法對數組帶來的變化呢?為了解決這個問題我們能夠一種替代的方式,將原生的函數替換成我們自定義的函數,并且在自定義的函數中調用原生的數組方法,就可以達到我們想要的目的。我們接著改造我們的defineReactive函數。
function observifyArray(array){ //需要變異的函數名列表 var methods = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"]; var arrayProto = Object.create(Array.prototype); _.each(methods, function(method){ arrayProto[method] = function(...args){ // 劫持修改數據 var ret = Array.prototype[method].apply(this, args); //可以在修改數據時觸發其他的操作 console.log("newValue: ", this); return ret; } }); Object.setPrototypeOf(array, arrayProto); } //定義對象的單個響應式屬性 function defineReactive(obj, key, value){ if(_.isArray(value)){ observifyArray(value, dep); }else { observify(value); } Object.defineProperty(obj, key, { // 省略...... }); }
我們可以看到我們將數組原生的原型替換成自定義的原型,然后調用數組的變異方法時就會調用我們自定義的函數。例如:
var model = [1,2,3]; observify(model); model.push(4); //newValue: [1, 2, 3, 4]
到目前為止我們已經實現了我們的需求,其實我寫到這里的時候,我考慮到是否需要實現對數組的鍵值進行監聽,其實作為使用過Vue的用戶一定知道,當你利用索引直接設置一個項時,是不會監聽到數組的變化的。比如:
vm.items[indexOfItem] = newValue
如果你想要實現上面的效果,可以通過下面的方式實現:
vm.items.splice(indexOfItem, 1, newValue);
首先考慮這個是否能實現。答案是顯而易見的了。當然是可以,數組其實可以看做特殊的數組,而其實對于數組而言,數值類型的索引都會被最終解析成字符串類型,比如下面的代碼:
var array = [0,1,2]; array["0"] = 1; //array: [1,1,2]
那要實現對數值索引對應的數據進行修改,其實也是可以通過Object.defineProperty函數去實現,比如:
var array = [0]; Object.defineProperty(array, 0, { set: function(newValue){ console.log("newValue: ", newValue); } }); array[0] = 1;//newValue: 1
可以實現但卻沒有實現該功能,想來主要原因可能就是基于性能方面的考慮(我的猜測)。但是Vue提供了另一個全局的函數,Vue.set可以實現
Vue.set(vm.array, indexOfItem, newValue)
我們可以大致猜測一下Vue.set內部怎么實現的,對于數組而言,只需要對newValue做響應化處理并將其賦值到數組中,然后通知數組改變。對于對象而言,如果是之前不存在的屬性,首先可以將newValue進行響應化處理(比如調用observify(newValue)),然后將對具體屬性定義監聽(比如調用函數defineReactive),最后再去做賦值,可能具體的處理過程千差萬別,但是內部實現的原理應該就是如此(僅僅是猜測)。
不僅如此,在上面的實現中我們可以發現,我們并不能監聽到對象不能檢測對象屬性的添加或刪除,因此如果如果你要監聽某個屬性的值,而一開始這個屬性并不存在,最好是在數據初始化的時候就給其一個默認值,從而能監聽到該屬性的變化。
依賴收集上面我們講了這么多,希望大家不要被帶偏了,我們上面所做的都是希望能在數據發生變化時得到通知。回到我們最初的問題。我們希望的是,在Model層數據發生改變的時候,View層的數據相應發生改變,我們已經能夠監聽到數據的改變了,接下來要考慮的就是View的改變。
對于Vue而言,即使你使用的是Template描述View層,最終都會被編譯成render函數。比如,模板中描述了:
{{ name }}
其實最后會被編譯成:
render: function (createElement) { return createElement("h1", this.name); }
那現在就存在下面這個一個問題,假如我的Model是下面這個樣子的:
var model = { name: "MrErHu", age: 23, sex: "man" }
事實上render函數中就只用到了屬性name,但是Model中卻存在其他的屬性,當數據改變的時候,我們怎么知道什么時候才需要重新調用render函數呢。你可能會想,哪里需要那么麻煩,每次數據改變都去刷新render函數不就行了嗎。這樣當然可以,其實如果朝著這個思路走,我們就朝著React方向走了。事實上如果不借助虛擬DOM的前提下,如果每次屬性改變都去調用render效率必然是低下的,這時候我們就引入了依賴收集,如果我們能知道render依賴了那些屬性,那么在這些屬性修改的時候,我們再精準地調用render函數,那么我們的目的不就達到了嗎?這就是我們所稱的依賴收集。
依賴收集的原理非常的簡單,在響應式數據中我們一直利用的都是屬性描述符中的set方法,而我們知道當調用某個對象的屬性時,會觸發屬性描述符的get方法,當get方法調用時,我們將調用get的方法收集起來就能完成我們的依賴收集的任務。
首先我們可以思考要一下,如果是自己寫一個響應式數據帶依賴收集的模塊,我們會去怎么設計。首先我們想要達到的類似效果就是:
var model = { name: "MrErHu", program: { language: "Javascript" }, favorite: ["React"] }; //數據響應化 observify(model); //監聽 watch(function(){ return "" + (model.name) + "
" }, function(){ console.log("name: ", model.name); }); watch(function(){ return "" + (model.program.language) + "
" }, function(){ console.log("language: ", model.program.language); }); watch(function(){ return "" + (model.favorite) + "
" }, function(){ console.log("favorite: ", model.favorite); }); model.name = "mrerhu"; //name: mrerhu model.program.language = "php"; //language: php model.favorite.push("Vue"); //favorite: [React, Vue]
我們所需要實現的watch函數的第一個參數可以認為是render函數,通過執行render函數我們可以收集到render函數內部使用了那些響應式數據屬性。然后在對應的響應式數據屬性改變的時候,觸發我們注冊的第二個函數。這樣看我們監聽屬性的粒度就是響應數據的每一個屬性。按照單一職責的概念,我們將監聽訂閱與通知發布的職責分離出去,由多帶帶的Dep類負責。由于監聽的粒度是響應式數據的每一個屬性,因此我們會為每一個屬性維護一個Dep。與此相對應,我們創建Watcher類,負責向Dep注冊,并在收到通知后調用回調函數。如下圖所示:
首先我們實現Dep和Watcher類:
//引入lodash庫 class Dep { constructor(){ this.listeners = []; } // 添加Watcher addWatcher(watcher){ var find = _.find(this.listeners, v => v === watcher); if(!find){ //防止重復注冊 this.listeners.push(watcher); } } // 移除Watcher removeWatcher(watcher){ var find = _.findIndex(this.listeners, v => v === fn); if(find !== -1){ this.listeners.splice(watcher, 1); } } // 通知 notify(){ _.each(this.listeners, function(watcher){ watcher.update(); }); } } Dep.target = null; class Watcher { constructor(callback){ this.callback = callback; } //得到Dep通知調用相應的回調函數 update(){ this.callback(); } }
接著我們創建watcher函數并且改造之前響應式相關的函數:
// 數據響應化 function observify(model){ if(_.isObject(model)){ _.each(model, function(value, key){ defineReactive(model, key, value); }); } } //定義對象的單個響應式屬性 function defineReactive(obj, key, value){ var dep = new Dep(); if(_.isArray(value)){ observifyArray(value, dep); }else { observify(value); } Object.defineProperty(obj, key, { configurable: true, enumerable: true, set: function(newValue){ observify(value); var oldValue = value; value = newValue; //可以在修改數據時觸發其他的操作 dep.notify(value); }, get: function(){ if(!_.isNull(Dep.target)){ dep.addWatcher(Dep.target); } return value; } }); } // 數據響應化 function observify(model){ if(_.isObject(model)){ _.each(model, function(value, key){ defineReactive(model, key, value); }); } } //定義對象的單個響應式屬性 function defineReactive(obj, key, value){ var dep = new Dep(); if(_.isArray(value)){ observifyArray(value, dep); }else { observify(value); } Object.defineProperty(obj, key, { configurable: true, enumerable: true, set: function(newValue){ observify(value); var oldValue = value; value = newValue; //可以在修改數據時觸發其他的操作 dep.notify(value); }, get: function(){ if(!_.isNull(Dep.target)){ dep.addWatcher(Dep.target); } return value; } }); } function observifyArray(array, dep){ //需要變異的函數名列表 var methods = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"]; var arrayProto = Object.create(Array.prototype); _.each(methods, function(method){ arrayProto[method] = function(...args){ var ret = Array.prototype[method].apply(this, args); dep.notify(this); return ret; } }); Object.setPrototypeOf(array, arrayProto); } function watch(render, callback){ var watcher = new Watcher(callback); Dep.target = watcher; render(); Dep.target = null; }
接下來我們就可以實驗一下我們的watch函數了:
var model = { name: "MrErHu", message: { languange: "javascript" }, love: ["Vue"] }; observify(model); watch(function(){ return "" + (model.name) + "
" }, function(){ console.log("name: ", model.name); }); watch(function(){ return "" + (model.message.languange) + "
" }, function(){ console.log("message: ", model.message); }); watch(function(){ return "" + (model.love) + "
" }, function(){ console.log("love: ", model.love); }); model.name = "mrerhu"; // name: mrerhu model.message.languange = "php"; // message: { languange: "php"} model.message = { target: "javascript" }; // message: { languange: "php"} model.love.push("React"); // love: ["Vue", "React"]
到此為止我們已經基本實現了我們想要的效果,當然上面的例子并不完備,但是也基本能展示出響應式數據與數據依賴的基本原理。當然上面僅僅只是采用ES5的數據描述符實現的,隨著ES6的普及,我們也可以用Proxy(代理)和Reflect(反射)去實現。作為本系列的第一篇文章,還有其他的點沒有一一列舉出來,大家可以關注我的Github博客繼續關注,如果有講的不準確的地方,歡迎大家指正。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107790.html
摘要:接下來,我們就一起深入了解的數據響應式原理,搞清楚響應式的實現機制。回調函數只是打印出新的得到的新的值,由執行后生成。及異步更新相信讀過前文,你應該對響應式原理有基本的認識。 前言 Vue.js 的核心包括一套響應式系統。 響應式,是指當數據改變后,Vue 會通知到使用該數據的代碼。例如,視圖渲染中使用了數據,數據改變后,視圖也會自動更新。 舉個簡單的例子,對于模板: {{ name ...
寫文章不容易,點個贊唄兄弟專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請點擊 下面鏈接 或者 拉到 下面關注公眾號也可以吧 【Vue原理】Props - 源碼版 今天記錄 Props 源碼流程,哎,這東西,就算是研究過了,也真是會隨著時間慢慢忘記的。 幸好我做...
摘要:當東西發售時,就會打你的電話通知你,讓你來領取完成更新。其中涉及的幾個步驟,按上面的例子來轉化一下你買東西,就是你要使用數據你把電話給老板,電話就是你的,用于通知老板記下電話在電話本,就是把保存在中。剩下的步驟屬于依賴更新 寫文章不容易,點個贊唄兄弟專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內部詳情,讓我們一起學習吧研究基于 Vue版本 【...
摘要:其成員函數最主要的是和,前者用來設置某個的依賴,后者則用來通知與這個依賴相關的來運行其回調函數。也就是進行數據更新操作。 vue框架的兩個抽象核心:虛擬DOM和相應式數據原理 關于虛擬DOM的核心算法,我們上一章已經基本解析過了,詳細的見React && VUE Virtual Dom的Diff算法統一之路 snabbdom.js解讀 關于響應式數據原理,我們先看張圖你 ‘ (4).p...
摘要:響應式原理之之前簡單介紹了和類的代碼和作用,現在來介紹一下類和。對于數組,響應式的實現稍有不同。不存在時,說明不是響應式數據,直接更新。如果對象是響應式的,確保刪除能觸發更新視圖。 Vue響應式原理之Observer 之前簡單介紹了Dep和Watcher類的代碼和作用,現在來介紹一下Observer類和set/get。在Vue實例后再添加響應式數據時需要借助Vue.set/vm.$se...
閱讀 1944·2021-10-12 10:12
閱讀 3072·2019-08-30 15:44
閱讀 843·2019-08-30 15:43
閱讀 2994·2019-08-30 14:02
閱讀 2076·2019-08-30 12:54
閱讀 3497·2019-08-26 17:05
閱讀 1980·2019-08-26 13:34
閱讀 1051·2019-08-26 11:54