摘要:總結(jié)最后我們依照下圖參考深入淺出,再來回顧下整個(gè)過程在后,會(huì)調(diào)用函數(shù)進(jìn)行初始化,也就是過程,在這個(gè)過程通過轉(zhuǎn)換成了的形式,來對(duì)數(shù)據(jù)追蹤變化,當(dāng)被設(shè)置的對(duì)象被讀取的時(shí)候會(huì)執(zhí)行函數(shù),而在當(dāng)被賦值的時(shí)候會(huì)執(zhí)行函數(shù)。
前言
Vue 最獨(dú)特的特性之一,是其非侵入性的響應(yīng)式系統(tǒng)。數(shù)據(jù)模型僅僅是普通的 JavaScript 對(duì)象。而當(dāng)你修改它們時(shí),視圖會(huì)進(jìn)行更新。這使得狀態(tài)管理非常簡(jiǎn)單直接,不過理解其工作原理同樣重要,這樣你可以避開一些常見的問題。----官方文檔
本文將針對(duì)響應(yīng)式原理做一個(gè)詳細(xì)介紹,并且?guī)銓?shí)現(xiàn)一個(gè)基礎(chǔ)版的響應(yīng)式系統(tǒng)。本文的代碼請(qǐng)猛戳Github博客
我們先來看個(gè)例子:
Price :¥{{ price }}Total:¥{{ price * quantity }}Taxes: ¥{{ totalPriceWithTax }}
var app = new Vue({ el: "#app", data() { return { price: 5.0, quantity: 2 }; }, computed: { totalPriceWithTax() { return this.price * this.quantity * 1.03; } }, methods: { changePrice() { this.price = 10; } } })
上例中當(dāng)price 發(fā)生變化的時(shí)候,Vue就知道自己需要做三件事情:
更新頁面上price的值
計(jì)算表達(dá)式 price*quantity 的值,更新頁面
調(diào)用totalPriceWithTax 函數(shù),更新頁面
數(shù)據(jù)發(fā)生變化后,會(huì)重新對(duì)頁面渲染,這就是Vue響應(yīng)式,那么這一切是怎么做到的呢?
想完成這個(gè)過程,我們需要:
偵測(cè)數(shù)據(jù)的變化
收集視圖依賴了哪些數(shù)據(jù)
數(shù)據(jù)變化時(shí),自動(dòng)“通知”需要更新的視圖部分,并進(jìn)行更新
對(duì)應(yīng)專業(yè)俗語分別是:
數(shù)據(jù)劫持 / 數(shù)據(jù)代理
依賴收集
發(fā)布訂閱模式
如何偵測(cè)數(shù)據(jù)的變化首先有個(gè)問題,在Javascript中,如何偵測(cè)一個(gè)對(duì)象的變化?
其實(shí)有兩種辦法可以偵測(cè)到變化:使用Object.defineProperty和ES6的Proxy,這就是進(jìn)行數(shù)據(jù)劫持或數(shù)據(jù)代理。這部分代碼主要參考珠峰架構(gòu)課。
Vue通過設(shè)定對(duì)象屬性的 setter/getter 方法來監(jiān)聽數(shù)據(jù)的變化,通過getter進(jìn)行依賴收集,而每個(gè)setter方法就是一個(gè)觀察者,在數(shù)據(jù)變更的時(shí)候通知訂閱者更新視圖。
function render () { console.log("模擬視圖渲染") } let data = { name: "浪里行舟", location: { x: 100, y: 100 } } observe(data) function observe (obj) { // 我們來用它使對(duì)象變成可觀察的 // 判斷類型 if (!obj || typeof obj !== "object") { return } Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) function defineReactive (obj, key, value) { // 遞歸子屬性 observe(value) Object.defineProperty(obj, key, { enumerable: true, //可枚舉(可以遍歷) configurable: true, //可配置(比如可以刪除) get: function reactiveGetter () { console.log("get", value) // 監(jiān)聽 return value }, set: function reactiveSetter (newVal) { observe(newVal) //如果賦值是一個(gè)對(duì)象,也要遞歸子屬性 if (newVal !== value) { console.log("set", newVal) // 監(jiān)聽 render() value = newVal } } }) } } data.location = { x: 1000, y: 1000 } //set {x: 1000,y: 1000} 模擬視圖渲染 data.name // get 浪里行舟
上面這段代碼的主要作用在于:observe這個(gè)函數(shù)傳入一個(gè) obj(需要被追蹤變化的對(duì)象),通過遍歷所有屬性的方式對(duì)該對(duì)象的每一個(gè)屬性都通過 defineReactive 處理,以此來達(dá)到實(shí)現(xiàn)偵測(cè)對(duì)象變化。值得注意的是,observe 會(huì)進(jìn)行遞歸調(diào)用。
那我們?nèi)绾蝹蓽y(cè)Vue中data 中的數(shù)據(jù),其實(shí)也很簡(jiǎn)單:
class Vue { /* Vue構(gòu)造類 */ constructor(options) { this._data = options.data; observer(this._data); } }
這樣我們只要 new 一個(gè) Vue 對(duì)象,就會(huì)將 data 中的數(shù)據(jù)進(jìn)行追蹤變化。
不過這種方式有幾個(gè)注意點(diǎn)需補(bǔ)充說明:
無法檢測(cè)到對(duì)象屬性的添加或刪除(如data.location.a=1)。
這是因?yàn)?Vue 通過Object.defineProperty來將對(duì)象的key轉(zhuǎn)換成getter/setter的形式來追蹤變化,但getter/setter只能追蹤一個(gè)數(shù)據(jù)是否被修改,無法追蹤新增屬性和刪除屬性。如果是刪除屬性,我們可以用vm.$delete實(shí)現(xiàn),那如果是新增屬性,該怎么辦呢?
1)可以使用 Vue.set(location, a, 1) 方法向嵌套對(duì)象添加響應(yīng)式屬性;
2)也可以給這個(gè)對(duì)象重新賦值,比如data.location = {...data.location,a:1}
Object.defineProperty 不能監(jiān)聽數(shù)組的變化,需要進(jìn)行數(shù)組方法的重寫,具體代碼如下:
function render() { console.log("模擬視圖渲染") } let obj = [1, 2, 3] let methods = ["pop", "shift", "unshift", "sort", "reverse", "splice", "push"] // 先獲取到原來的原型上的方法 let arrayProto = Array.prototype // 創(chuàng)建一個(gè)自己的原型 并且重寫methods這些方法 let proto = Object.create(arrayProto) methods.forEach(method => { proto[method] = function() { // AOP arrayProto[method].call(this, ...arguments) render() } }) function observer(obj) { // 把所有的屬性定義成set/get的方式 if (Array.isArray(obj)) { obj.__proto__ = proto return } if (typeof obj == "object") { for (let key in obj) { defineReactive(obj, key, obj[key]) } } } function defineReactive(data, key, value) { observer(value) Object.defineProperty(data, key, { get() { return value }, set(newValue) { observer(newValue) if (newValue !== value) { render() value = newValue } } }) } observer(obj) function $set(data, key, value) { defineReactive(data, key, value) } obj.push(123, 55) console.log(obj) //[1, 2, 3, 123, 55]
這種方法將數(shù)組的常用方法進(jìn)行重寫,進(jìn)而覆蓋掉原生的數(shù)組方法,重寫之后的數(shù)組方法需要能夠被攔截。但有些數(shù)組操作Vue時(shí)攔截不到的,當(dāng)然也就沒辦法響應(yīng),比如:
obj.length-- // 不支持?jǐn)?shù)組的長(zhǎng)度變化 obj[0]=1 // 修改數(shù)組中第一個(gè)元素,也無法偵測(cè)數(shù)組的變化
ES6提供了元編程的能力,所以有能力攔截,Vue3.0可能會(huì)用ES6中Proxy 作為實(shí)現(xiàn)數(shù)據(jù)代理的主要方式。
方法2.Proxy實(shí)現(xiàn)Proxy 是 JavaScript 2015 的一個(gè)新特性。Proxy 的代理是針對(duì)整個(gè)對(duì)象的,而不是對(duì)象的某個(gè)屬性,因此不同于 Object.defineProperty 的必須遍歷對(duì)象每個(gè)屬性,Proxy 只需要做一層代理就可以監(jiān)聽同級(jí)結(jié)構(gòu)下的所有屬性變化,當(dāng)然對(duì)于深層結(jié)構(gòu),遞歸還是需要進(jìn)行的。此外Proxy支持代理數(shù)組的變化。
function render() { console.log("模擬視圖的更新") } let obj = { name: "前端工匠", age: { age: 100 }, arr: [1, 2, 3] } let handler = { get(target, key) { // 如果取的值是對(duì)象就在對(duì)這個(gè)對(duì)象進(jìn)行數(shù)據(jù)劫持 if (typeof target[key] == "object" && target[key] !== null) { return new Proxy(target[key], handler) } return Reflect.get(target, key) }, set(target, key, value) { if (key === "length") return true render() return Reflect.set(target, key, value) } } let proxy = new Proxy(obj, handler) proxy.age.name = "浪里行舟" // 支持新增屬性 console.log(proxy.age.name) // 模擬視圖的更新 浪里行舟 proxy.arr[0] = "浪里行舟" //支持?jǐn)?shù)組的內(nèi)容發(fā)生變化 console.log(proxy.arr) // 模擬視圖的更新 ["浪里行舟", 2, 3 ] proxy.arr.length-- // 無效
以上代碼不僅精簡(jiǎn),而且還是實(shí)現(xiàn)一套代碼對(duì)對(duì)象和數(shù)組的偵測(cè)都適用。不過Proxy兼容性不太好!
為什么要收集依賴我們之所以要觀察數(shù)據(jù),其目的在于當(dāng)數(shù)據(jù)的屬性發(fā)生變化時(shí),可以通知那些曾經(jīng)使用了該數(shù)據(jù)的地方。比如第一例子中,模板中使用了price 數(shù)據(jù),當(dāng)它發(fā)生變化時(shí),要向使用了它的地方發(fā)送通知。那如果多個(gè)Vue實(shí)例中共用一個(gè)變量,如下面這個(gè)例子:
let globalData = { text: "浪里行舟" }; let test1 = new Vue({ template: `{{text}}`, data: globalData }); let test2 = new Vue({ template: `{{text}}`, data: globalData });如果我們執(zhí)行下面這條語句:
globalData.text = "前端工匠";此時(shí)我們需要通知 test1 以及 test2 這兩個(gè)Vue實(shí)例進(jìn)行視圖的更新,我們只有通過收集依賴才能知道哪些地方依賴我的數(shù)據(jù),以及數(shù)據(jù)更新時(shí)派發(fā)更新。那依賴收集是如何實(shí)現(xiàn)的?其中的核心思想就是“事件發(fā)布訂閱模式”。接下來我們先介紹兩個(gè)重要角色-- 訂閱者 Dep和觀察者 Watcher ,然后闡述收集依賴的如何實(shí)現(xiàn)的。
訂閱者 Dep 1.為什么引入 Dep收集依賴需要為依賴找一個(gè)存儲(chǔ)依賴的地方,為此我們創(chuàng)建了Dep,它用來收集依賴、刪除依賴和向依賴發(fā)送消息等。
于是我們先來實(shí)現(xiàn)一個(gè)訂閱者 Dep 類,用于解耦屬性的依賴收集和派發(fā)更新操作,說得具體點(diǎn),它的主要作用是用來存放 Watcher 觀察者對(duì)象。我們可以把Watcher理解成一個(gè)中介的角色,數(shù)據(jù)發(fā)生變化時(shí)通知它,然后它再通知其他地方。
2. Dep的簡(jiǎn)單實(shí)現(xiàn)class Dep { constructor () { /* 用來存放Watcher對(duì)象的數(shù)組 */ this.subs = []; } /* 在subs中添加一個(gè)Watcher對(duì)象 */ addSub (sub) { this.subs.push(sub); } /* 通知所有Watcher對(duì)象更新視圖 */ notify () { this.subs.forEach((sub) => { sub.update(); }) } }以上代碼主要做兩件事情:
用 addSub 方法可以在目前的 Dep 對(duì)象中增加一個(gè) Watcher 的訂閱操作;
用 notify 方法通知目前 Dep 對(duì)象的 subs 中的所有 Watcher 對(duì)象觸發(fā)更新操作。
所以當(dāng)需要依賴收集的時(shí)候調(diào)用 addSub,當(dāng)需要派發(fā)更新的時(shí)候調(diào)用 notify。調(diào)用也很簡(jiǎn)單:
let dp = new Dep() dp.addSub(() => { console.log("emit here") }) dp.notify()觀察者 Watcher 1.為什么引入WatcherVue 中定義一個(gè) Watcher 類來表示觀察訂閱依賴。至于為啥引入Watcher,《深入淺出vue.js》給出了很好的解釋:
當(dāng)屬性發(fā)生變化后,我們要通知用到數(shù)據(jù)的地方,而使用這個(gè)數(shù)據(jù)的地方有很多,而且類型還不一樣,既有可能是模板,也有可能是用戶寫的一個(gè)watch,這時(shí)需要抽象出一個(gè)能集中處理這些情況的類。然后,我們?cè)谝蕾囀占A段只收集這個(gè)封裝好的類的實(shí)例進(jìn)來,通知也只通知它一個(gè),再由它負(fù)責(zé)通知其他地方。
依賴收集的目的是將觀察者 Watcher 對(duì)象存放到當(dāng)前閉包中的訂閱者 Dep 的 subs 中。形成如下所示的這樣一個(gè)關(guān)系(圖參考《剖析 Vue.js 內(nèi)部運(yùn)行機(jī)制》)。
2.Watcher的簡(jiǎn)單實(shí)現(xiàn)class Watcher { constructor(obj, key, cb) { // 將 Dep.target 指向自己 // 然后觸發(fā)屬性的 getter 添加監(jiān)聽 // 最后將 Dep.target 置空 Dep.target = this this.cb = cb this.obj = obj this.key = key this.value = obj[key] Dep.target = null } update() { // 獲得新值 this.value = this.obj[this.key] // 我們定義一個(gè) cb 函數(shù),這個(gè)函數(shù)用來模擬視圖更新,調(diào)用它即代表更新視圖 this.cb(this.value) } }以上就是 Watcher 的簡(jiǎn)單實(shí)現(xiàn),在執(zhí)行構(gòu)造函數(shù)的時(shí)候?qū)?Dep.target 指向自身,從而使得收集到了對(duì)應(yīng)的 Watcher,在派發(fā)更新的時(shí)候取出對(duì)應(yīng)的 Watcher ,然后執(zhí)行 update 函數(shù)。
收集依賴所謂的依賴,其實(shí)就是Watcher。至于如何收集依賴,總結(jié)起來就一句話,在getter中收集依賴,在setter中觸發(fā)依賴。先收集依賴,即把用到該數(shù)據(jù)的地方收集起來,然后等屬性發(fā)生變化時(shí),把之前收集好的依賴循環(huán)觸發(fā)一遍就行了。
具體來說,當(dāng)外界通過Watcher讀取數(shù)據(jù)時(shí),便會(huì)觸發(fā)getter從而將Watcher添加到依賴中,哪個(gè)Watcher觸發(fā)了getter,就把哪個(gè)Watcher收集到Dep中。當(dāng)數(shù)據(jù)發(fā)生變化時(shí),會(huì)循環(huán)依賴列表,把所有的Watcher都通知一遍。
最后我們對(duì) defineReactive 函數(shù)進(jìn)行改造,在自定義函數(shù)中添加依賴收集和派發(fā)更新相關(guān)的代碼,實(shí)現(xiàn)了一個(gè)簡(jiǎn)易的數(shù)據(jù)響應(yīng)式。
function observe (obj) { // 判斷類型 if (!obj || typeof obj !== "object") { return } Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) function defineReactive (obj, key, value) { observe(value) // 遞歸子屬性 let dp = new Dep() //新增 Object.defineProperty(obj, key, { enumerable: true, //可枚舉(可以遍歷) configurable: true, //可配置(比如可以刪除) get: function reactiveGetter () { console.log("get", value) // 監(jiān)聽 // 將 Watcher 添加到訂閱 if (Dep.target) { dp.addSub(Dep.target) // 新增 } return value }, set: function reactiveSetter (newVal) { observe(newVal) //如果賦值是一個(gè)對(duì)象,也要遞歸子屬性 if (newVal !== value) { console.log("set", newVal) // 監(jiān)聽 render() value = newVal // 執(zhí)行 watcher 的 update 方法 dp.notify() //新增 } } }) } } class Vue { constructor(options) { this._data = options.data; observer(this._data); /* 新建一個(gè)Watcher觀察者對(duì)象,這時(shí)候Dep.target會(huì)指向這個(gè)Watcher對(duì)象 */ new Watcher(); console.log("模擬視圖渲染"); } }當(dāng) render function 被渲染的時(shí)候,讀取所需對(duì)象的值,會(huì)觸發(fā) reactiveGetter 函數(shù)把當(dāng)前的 Watcher 對(duì)象(存放在 Dep.target 中)收集到 Dep 類中去。之后如果修改對(duì)象的值,則會(huì)觸發(fā) reactiveSetter 方法,通知 Dep 類調(diào)用 notify 來觸發(fā)所有 Watcher 對(duì)象的 update 方法更新對(duì)應(yīng)視圖。
總結(jié)最后我們依照下圖(參考《深入淺出vue.js》),再來回顧下整個(gè)過程:
在 new Vue() 后, Vue 會(huì)調(diào)用 _init 函數(shù)進(jìn)行初始化,也就是init 過程,在 這個(gè)過程Data通過Observer轉(zhuǎn)換成了getter/setter的形式,來對(duì)數(shù)據(jù)追蹤變化,當(dāng)被設(shè)置的對(duì)象被讀取的時(shí)候會(huì)執(zhí)行getter 函數(shù),而在當(dāng)被賦值的時(shí)候會(huì)執(zhí)行 setter函數(shù)。
當(dāng)render function 執(zhí)行的時(shí)候,因?yàn)闀?huì)讀取所需對(duì)象的值,所以會(huì)觸發(fā)getter函數(shù)從而將Watcher添加到依賴中進(jìn)行依賴收集。
在修改對(duì)象的值的時(shí)候,會(huì)觸發(fā)對(duì)應(yīng)的setter, setter通知之前依賴收集得到的 Dep 中的每一個(gè) Watcher,告訴它們自己的值改變了,需要重新渲染視圖。這時(shí)候這些 Watcher就會(huì)開始調(diào)用 update 來更新視圖。
vue系列文章抄送門從頭開始學(xué)習(xí)Vuex
從頭開始學(xué)習(xí)vue-router
vue組件三大核心概念
vue組件間通信六種方式(完整版)
vue計(jì)算屬性和watch的區(qū)別
揭秘Vue中的Virtual Dom
給大家推薦一個(gè)好用的BUG監(jiān)控工具Fundebug,歡迎免費(fèi)試用!
歡迎關(guān)注公眾號(hào):前端工匠,你的成長(zhǎng)我們一起見證!
參考文章和書籍珠峰架構(gòu)課(強(qiáng)烈推薦)
剖析 Vue.js 內(nèi)部運(yùn)行機(jī)制
深入淺出Vue.js
Vue官方文檔
前端面試之道
前端開發(fā)核心知識(shí)進(jìn)階
Javascript響應(yīng)式的最通俗易懂的解釋(譯)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/105453.html
相關(guān)文章
淺究Vue響應(yīng)式原理
摘要:在讀取訪問器屬性時(shí),就會(huì)調(diào)用函數(shù),該函數(shù)負(fù)責(zé)返回有效的值在寫入訪問器屬性時(shí),會(huì)調(diào)用函數(shù)并傳入新值,該函數(shù)負(fù)責(zé)決定如何處理數(shù)據(jù),但是這兩個(gè)函數(shù)不一定非要同時(shí)存在。 前言 Vue最明顯的特性之一便是它的響應(yīng)式系統(tǒng),其數(shù)據(jù)模型即是普通的 JavaScript 對(duì)象。而當(dāng)你讀取或?qū)懭胨鼈儠r(shí),視圖便會(huì)進(jìn)行響應(yīng)操作。文章簡(jiǎn)要闡述下其實(shí)現(xiàn)原理,如有錯(cuò)誤,還請(qǐng)不吝指正。個(gè)人博客鏈接:hiybm.cn ...
Vue 數(shù)據(jù)響應(yīng)式原理
摘要:接下來,我們就一起深入了解的數(shù)據(jù)響應(yīng)式原理,搞清楚響應(yīng)式的實(shí)現(xiàn)機(jī)制?;卣{(diào)函數(shù)只是打印出新的得到的新的值,由執(zhí)行后生成。及異步更新相信讀過前文,你應(yīng)該對(duì)響應(yīng)式原理有基本的認(rèn)識(shí)。 前言 Vue.js 的核心包括一套響應(yīng)式系統(tǒng)。 響應(yīng)式,是指當(dāng)數(shù)據(jù)改變后,Vue 會(huì)通知到使用該數(shù)據(jù)的代碼。例如,視圖渲染中使用了數(shù)據(jù),數(shù)據(jù)改變后,視圖也會(huì)自動(dòng)更新。 舉個(gè)簡(jiǎn)單的例子,對(duì)于模板: {{ name ...
[Vue.js進(jìn)階]從源碼角度剖析計(jì)算屬性的原理
摘要:前言最近在學(xué)習(xí)計(jì)算屬性的源碼,發(fā)現(xiàn)和普通的響應(yīng)式變量?jī)?nèi)部的實(shí)現(xiàn)還有一些不同,特地寫了這篇博客,記錄下自己學(xué)習(xí)的成果文中的源碼截圖只保留核心邏輯完整源碼地址可能需要了解一些響應(yīng)式的原理版本計(jì)算屬性的概念一般的計(jì)算屬性值是一個(gè)函數(shù),這個(gè)函數(shù)showImg(https://user-gold-cdn.xitu.io/2019/5/6/16a8b98f1361f6f6); 前言 最近在學(xué)習(xí)Vue計(jì)...
【Vue原理】響應(yīng)式原理 - 白話版
摘要:所以我今后打算把每一個(gè)內(nèi)容分成白話版和源碼版。有什么錯(cuò)誤的地方,感謝大家能夠指出響應(yīng)式系統(tǒng)我們都知道,只要在實(shí)例中聲明過的數(shù)據(jù),那么這個(gè)數(shù)據(jù)就是響應(yīng)式的。什么是響應(yīng)式,也即是說,數(shù)據(jù)發(fā)生改變的時(shí)候,視圖會(huì)重新渲染,匹配更新為最新的值。 寫文章不容易,點(diǎn)個(gè)贊唄兄弟專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內(nèi)部詳情,讓我們一起學(xué)習(xí)吧研究基于 V...
發(fā)表評(píng)論
0條評(píng)論
yiliang
男|高級(jí)講師
TA的文章
閱讀更多
『學(xué)了就忘』Linux軟件包管理 — 45、yum源文件詳細(xì)說明
閱讀 2631·2021-11-17 17:00
Vue devtools工具安裝 Chrome安裝Vue devtools 教程步驟
閱讀 1884·2021-10-11 10:57
ReliableSite:美國(guó)服務(wù)器租用(紐約/邁阿密/洛杉磯);E5-1650v3/128GB/2
閱讀 3751·2021-09-09 11:33
一篇文章帶你了解如何用Planting 為測(cè)試工程師開發(fā)的部署框架
閱讀 921·2021-09-09 09:33
BFC深入理解
閱讀 3558·2019-08-30 14:20
好用的sublime text 插件(有空的時(shí)候持續(xù)更新)
閱讀 3324·2019-08-29 11:25
nginx 常用命令
閱讀 2809·2019-08-26 13:48
javascript高級(jí)程序設(shè)計(jì)(第三版)學(xué)習(xí)摘錄下
閱讀 747·2019-08-26 11:52