vue变化侦测
一、Object的變化偵測
1. 什么是變化偵測
- Vue.js會自動通過狀態生成DOM,并將其輸出到頁面上顯示出來,這個過程叫做渲染。Vue.js的渲染過程是聲明式的,我們通過模板來描述狀態與DOM之間的映射關系。通常,在運行時應用內部的狀態會不斷發生變化,此時需要不停地重新渲染,如果確認狀態中發生了什么呢?這時候變化偵測的作用便顯示出來了。
- 變化偵測分為推和拉,Angular和React中的變化偵測屬于拉,即當狀態發生改變時,并不知道哪個狀態變了,只知道狀態有可能變了,然后發送一個信號告訴框架,框架內部受到信號后,會進行一個暴力對比來找出哪些DOM節點需要重新渲染。
- Vue.js中屬于推。當狀態發生變化時,Vue.js會立刻知道且在一定程度知道哪些狀態變了。它知道的信息越多,也便可以進行更細粒度的更新。更細粒度的更新是什么意思呢?實際上,一個狀態綁定了多個依賴,當該狀態改變時,會通知這些依賴(每個依賴表示一個具體的DOM節點),讓它們進行更新操作。既然Vue.js知道更多的內容,它也就要付出一定的代價。粒度越細,每個狀態綁定的依賴便越多,追蹤依賴在內存上的開銷也便會很大。故從Vue.js 2.0開始,引入了虛擬DOM,其將粒度調整為中等粒度,即一個狀態綁定的不再是一個個的DOM節點,而是一個個組件。當狀態發生變化時,通知依賴(即組件),組件內部再使用虛擬DOM進行對比。這可以大大降低依賴的數量,從而降低依賴追蹤所消耗的內存。
2. 如何追蹤變化
在js中,可以通過Object.defineProperty以及es6的proxy來偵測對象的變化。但由于proxy在瀏覽器支持度不是很理想,故Vue.js目前是以Object.defineProperty方式來偵測對象的變化的。
function defineReactive(data, key, val) {Object.defineProperty(data, key, {enumerable: true,configurable: true,get: function() {return val},set: function(newVal) {if(val === newVal) {return;}val = newVal}}) }使用上述defineReactive方法偵測對象data后,當data.key對應的值改變時,會觸發其中的set方法,當讀取data.key時,會觸發get方法。由此我們也可以得知,當某一個依賴第一次讀取data.key時,也便是觸發get時,便可將其加入到data.key的依賴列表中,當之后data.key發生變化時,也便是觸發set時,會通知其對應的所有依賴,讓依賴通知其內部的DOM元素,從而能夠更新DOM。那么接下來我們要做什么呢?我們需要做的便是為每一個對象的每一個屬性增加一個依賴列表(dep,即Dep實例),當觸發get方法時將讀取該屬性的依賴存放進依賴列表中,當觸發set方法時,通過該依賴列表中的所有依賴。
我們使用一個Dep來封裝依賴的各種行為
class Dep {constructor() {this.subs = [];}addSub(sub) {this.subs.push(sub)}//移除依賴removeSub(sub) {return this.subs.filter(item => item != sub);}//添加依賴depend() {if (window.target) {this.addSub(window.target)//window.target代表的當前正在讀取該對象該屬性值的依賴}}//修改時觸發所有依賴notify() {const subs = this.subs.slice();subs.forEach(item => {subs[i].update() //觸發每個依賴的更新方法})} }改變后的defineReactive方法:
function defineReactive(data, key, val) {//遞歸子屬性if (typeof val === 'object') {new Observer(val);}let dep = new Dep();Object.defineProperty(data, key, {enumerable: true,configurable: true,get: function() {dep.depend();return val},set: function(newVal) {if(val === newVal) {return;}val = newValdep.notify()}}) }class Dep {constructor() {this.subs = [];}addSub(sub) {this.subs.push(sub)}//移除依賴removeSub(sub) {return this.subs.filter(item => item != sub);}//添加依賴depend() {if (window.target) {this.addSub(window.target)//window.target代表的當前正在讀取該對象該屬性值的依賴}}//修改時觸發所有依賴notify() {const subs = this.subs.slice();subs.forEach(item => {subs[i].update() //觸發每個依賴的更新方法})} }我們現在已經知道該如何收集依賴并且依賴收集在哪里,但是我們一直說的依賴是誰呢?從上面代碼中我們可以看到,我們要收集的依賴是window.target,那它是誰呢?我們可以將其叫為Watcher。我們可以將其理解為一個中介的角色,當數據變化時,我們可以通過它通知到其他地方。當外界讀取相關屬性時,會先告知其相關Watcher實例,Watcher實例去讀取數據并將window.target設置為其本身,Watcher在屬性的get中被收集,當屬性值更新時,會通知所有讀取該屬性的Watcher實例,Watcher實例再通知到其可通知到的外界
class Watcher {constructor(vm, expOrFn, cb) {this.vm = vm;this.getter = parsePath(expOrFn);//expOrFn -> a.b.c (key) 返回的為該key對應的value值this.cb = cb;this.value = this.get();//讀取該key對應的最初的值并返回}//此時會觸發該key對應的defineReactive,便會將當前this作為window.target,存儲在該key對應的依賴中get() {window.target = this;let value = this.getter.call(this.vm, this.vm) window.target = undefined;return value;}//當當前key對應的屬性值改變時調用該方法update() {const oldValue = this.value;this.value = this.get();//將最新的值作為當前watcher的valuethis.cb.call(this.vm, this.value, oldValue)} }const bailRE = /[^\w.$]/ function parsePath(path) {if(bailRE.test(path)) return;const segments = path.split('.')return function (obj) {for(let i = 0 ; i < segments.length ; i++) {if (!obj) return;obj = obj[segments[i]];}return obj;} }對于對象的所有屬性,我們都需要將其轉換為響應式的,即需要用Object.defineProperty去監聽set以及get。我們采用Observer類來進行遞歸實現。Observer類會附加到每個被偵測的Object上。每一個Object都對應一個Observer實例。
export class Observer {constructor (value) {this.value = value;if (!Array.isArray(value)) {this.walk(value)}}/*walk會將每一個屬性都轉換成getter/setter的形式來偵測變化 該方法只有在數據類型為Object時被調用*/walk (obj) {const keys = Object.keys(obj);for (let i=0; i < keys.length; i++) {defineReactive(obj, keys[i], obj[keys[i]])}} }二、Array的變化偵測
1. Array的變化偵測和Object有什么不同?
當通過Array的各種操作方法(push、unshift、shift、pop、splice、reverse、sort)方法對數組進行操作時,無法在set中監聽到,故Object的getter/setter方法就不可行了。
2. 怎么辦?
我們可以通過做中間處理,當請求上述操作方法時,可以被我們監聽到。可是上述方法都是Array原型上的方法,那我們該如何監聽到呢?最直接的方法便是我們做一層封裝,當數組對象請求上述方法時實際上請求的是我們封裝的方法,我們在封裝的方法中能夠攔截到操作請求,我們在封裝的方法中先去請求Array原型上的方法,完成正常的請求,然后再通知依賴,數組對象做了什么操作,最后依賴再去通知到其他地方。
3. 攔截器
知道了解決方法后,我們便可以得知,我們需要實現的是一個攔截器,攔截器偽造成數組對象的原型,當數組對象請求對應操作時,實際上調用的時攔截器中的方法。這樣我們便可以監聽到上述操作方法的請求。
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) const methodsList = ['push', 'pop', 'unshift', 'shift', 'sort', 'splice', 'reverse'] methodsList.forEach (function (method) { //緩存原始方法const original = arrayProto[method]Object.defineProperty(arrayMethods, method, {//為arrayMethods增加上述方法value: function mutator (...args) { //當數組對象調用對應方法時,需要改變方法內部this指向,在此處我們也可以做一些其他事情,比如說發送變化通知return original.apply(this, args);},enumerable: false,writable: true,configurable:true})})4. 怎么覆蓋數組本身的方法: 將攔截器方法掛載到數組的屬性上!
攔截器已經存在,那我們該如何將攔截器和數組綁定呢?答案當然是將其作為數組的原型,當通過數組調用對應方法時,調用的為攔截器上的方法。那對于有些瀏覽器不支持__proto__怎么辦呢?Vue直接將這些方法掛載在數組上,使其稱為數組的不可遍歷的屬性。這樣的話,便可以實現了。
export class Observer {constructor (value) {this.value = value;if (Array.isArray(value)) {const augment = hasProto ? protoAugment : copyAugmentaugment(value, arrayMethods, arrayKeys)} else{this.walk(value)}}/*walk會將每一個屬性都轉換成getter/setter的形式來偵測變化 該方法只有在數據類型為Object時被調用*/walk (obj) {const keys = Object.keys(obj);for (let i=0; i < keys.length; i++) {defineReactive(obj, keys[i], obj[keys[i]])}} }function protoAugment (target, src, keys) { target.__proto__ = src}function copyAugment (target, src, keys) { //將arrayMethods上的方法直接添加給數組上for (let i = 0 ; i < keys.length ; i++) {const key = keys[i];def(target, key, src[key])//給數組本身添加不可遍歷的的屬性}}function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, {value: val,enumerable: !!enumerable,writable: true,configurable: true})}5. 怎么收集依賴?
我們已經實現了攔截器以及攔截器已經作為數組的原型,當調用對應方法時,會調用攔截器上的方法。我們創建攔截器本質上是為了得到一種能力,一種當數組的內容發生變化時通知所有依賴的能力。那我們現在有了這個能力,又去通知誰呢?通知依賴。那么依賴怎么收集呢?
舉例說明:this.arr,當我們要從this上讀取arr時,一定會觸發名字叫arr的屬性的getter,,故數組的收集依賴是在第一次讀取數組時進行的,與Object是一致的。
6. 依賴列表在哪?
我們已經知道在getter中收集依賴,那么在哪里觸發呢?答案當然是在攔截器中。那依賴列表保存在哪里呢?最直觀的想法應該是在getter以及攔截器都可以訪問到的地方。Vue將依賴列表存放在Observer中,為什么Observer可以被getter以及攔截器訪問到呢?Vue做了如下處理:
Vue將對象進行統一,當傳入的val為對象時,會去判斷其是否有不可枚舉的屬性__ob__,如果有,則返回賦值給childOb,并在setter中將依賴收集到childOb.dep上,如果沒有,則創建一個Observer實例,將其作為val的__ob__,故__ob__為一個Observer實例,它也可以作為當前val已經成為響應式數據的依據。(Object類型的數據雖然也會將依賴收集到__ob__上,但是它使用的仍為defineReactive中的dep依賴集)
function defineReactive(data, key, val) {let childOb = observer(val) //如果data.key的值,即val為一個對象,則為其創建Observer實例let dep = new Dep();Object.defineProperty(data, key, {enumerable: true,configurable: true,get: function() {dep.depend();if (childOb) {childOb.dep.depend();}return val},set: function(newVal) {if(val === newVal) {return;}val = newValdep.notify()}}) }function observer (value, asRootData) {if (!isObject(value)) {//如果不是對象,則不需要創建Observer實例return}let ob;if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {//是否已經偵聽過,如果偵聽過,則返回已經創建好的Observer實例ob = value.__ob__;} else {ob = new Observer(value)}return ob;}當我們在攔截器中告知所有依賴時,我們可以采用this.__ob__.dep的方式獲取到依賴集。故攔截器部分被修改成下面部分:
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) const methodsList = ['push', 'pop', 'unshift', 'shift', 'sort', 'splice', 'reverse'] methodsList.forEach (function (method) { //緩存原始方法const original = arrayProto[method]Object.defineProperty(arrayMethods, method, {//為arrayMethods增加上述方法value: function mutator (...args) { //當數組對象調用對應方法時,需要改變方法內部this指向,在此處我們也可以做一些其他事情,比如說發送變化通知const ob = this.__ob__;ob.dep.notify() //向依賴發送消息return original.apply(this, args);},enumerable: false,writable: true,configurable:true})})7. 初始子元素如果是數組/對象怎么辦?
如果當前需要被偵測的數組的子元素中存在數組/對象該怎么辦?答案當然是遞歸偵測所有子元素。所以我們為Observer新增了observerArray方法,遍歷其子元素,如果子元素也是對象/數組,則也將其變為響應式的。
export class Observer {constructor (value) {this.value = value;this.dep = new Dep();def(value,'__ob__',this)//將當前Observer實例作為value的__ob__(不可枚舉的屬性)。可通過在數組數據的__ob__上拿到Observer實例,也可標識該對象是否已經被轉換成了響應式數據if (Array.isArray(value)) {const augment = hasProto ? protoAugment : copyAugmentaugment(value, arrayMethods, arrayKeys)this.observerArray(value) //將數組的所有元素都變成響應式的} else{this.walk(value)}}/*walk會將每一個屬性都轉換成getter/setter的形式來偵測變化 該方法只有在數據類型為Object時被調用*/walk (obj) {const keys = Object.keys(obj);for (let i=0; i < keys.length; i++) {defineReactive(obj, keys[i], obj[keys[i]])}}/*偵測Array中的每一項*/observerArray (items) {for (let i = 0; o < items.length; i++) {observer(items[i])}} }8. 新增子元素是數組/對象怎么辦?
我們是可以對某一個數組進行新增元素的,如果新增的元素是數組/對象我們也是要對其進行偵聽的。那我們該怎么對其進行偵測呢?其實我們只需要對所有可以進行新增的方法進行監聽,當新增的元素是數組/對象時,我們調用observerArray方法,將其轉換為響應式數據。
我們將原始的攔截器修改成下述代碼(def在前面已經實現),
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto) const methodsList = ['push', 'pop', 'unshift', 'shift', 'sort', 'splice', 'reverse'] const hasProto = '__proto__' in {} const arrayKeys = Object.getOwnPropertyNames(arrayMethods) methodsList.forEach (function (method) { //緩存原始方法const original = arrayProto[method]def(arrayMethods, method, function mutator (...args) { const result = original.apply(this, args)const ob = this.__ob__;//獲取到this的__ob__,即當前數組對象let inserted;//保存新增內容switch (method) {case 'push':case 'unshift':inserted = args;break;case 'splice':inserted = args.slice(2)//splice第三個參數為新增元素,如果沒有第三個參數,則代表沒有在新增內容break;}if (inserted) ob.observerArray(inserted) //將新增內容轉換為響應式數據ob.dep.notify();return result;})})9. 總結:
我們首先為每個數組綁定一個Observer實例,如果數組元素也是數組,則也要為其綁定一個Observer實例,在創建Observer實例時,我們需要將數組轉換為響應式數據,從而能夠被Vue偵測到。除此之外,我們還需要在創建Observer實例時為數組進行攔截器設置,將攔截器對象作為數組的原型,而Array的原型作為攔截器對象的原型,這樣在調用各種操作方法時,調用的為攔截器中的方法,這樣的話我們便可以知曉什么時候該數組進行了更新,我們便可以通知所有依賴,從而完成更新操作。
Array的變化偵測中,依賴收集仍在setter中完成,但是是收集到各個數組綁定的Observer實例上(__ob__為數組的一個不可枚舉的屬性),這樣的話我們可以在攔截器中,通過this.__ob__方式獲取到該數組綁定的Observer實例,從而能夠獲取到依賴集。
除了Array原始元素要被轉換為響應式數據外,新增的元素也要被轉換為響應式數據,故我們需要在攔截器中對新增方法進行監聽,當有新增元素時,需要通過observerArray將其轉換為響應式數據。
三、VUE2.0中Object和Array變化偵測的弊端
Object變化偵測只能夠偵測到數據的變化,并不能偵測到數據的增加和刪除。需要通過vm.$set以及vm.$delete方法進行偵測。
Array變化偵測無法偵測到arr[0]/arr.length = 5這兩種方式的操作。
總結
- 上一篇: ssm+jsp计算机毕业设计郑财学生经验
- 下一篇: html5倒计时秒杀怎么做,vue 设