歡迎關注我的博客:https://github.com/wangweianger/myblog
Vue內部實現了一組觀察數組的變異方法,例如:push(),pop(),shift()等。
Object.definePropert只能把對象屬性改為getter/setter,而對於數組的方法就無能為力了,其內部巧妙的使用了數組的屬性來實現了數據的雙向綁定,下面我們來一步一步的實現一個簡單版。
下文大部分代碼摘自Vue源碼
首先我們來實現給定一個數組 調用相關方法時觸發自定義的函數
定義一個需要監聽變化的數組
let obarr = []
來copy一份數組的原型方法,防止污染原生數組方法
const arrayProto = Array.prototype const arrayMethods = Object.create(arrayProto)
我們先把arrayMethods對象上的push轉換為觀察者對象
Object.defineProperty(arrayMethods,'push',{
value:function mutator(){
console.log('obarr.push會走這里')
}
})
此時arrayMethods定義了一個push的新屬性,那么我們如何把它和 let obarr = [] 綁定起來呢,來看看下面的實現?
obarr.__proto__ = arrayMethods
使用arrayMethods覆蓋obarr的所有方法
到此現在完整代碼如下:
let obarr = []
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
Object.defineProperty(arrayMethods,'push',{
value:function mutator(){
console.log('obarr.push會走這里')
}
})
obarr.__proto__ = arrayMethods;
向obarr中push一個值看看,是不是走了console呢,肯定的答復你:yes 走了。
obarr.push(0)
針對於不支持__proto__的瀏覽器實現如下:
let obarr = []
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
Object.defineProperty(arrayMethods,'push',{
value:function mutator(){
console.log('obarr.push會走這里')
}
})
Object.defineProperty(obarr,'push',{
value:arrayMethods.push
})
來真正的為arr賦值代碼如下:
let obarr = []
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
Object.defineProperty(arrayMethods,'push',{
value:function mutator(){
//緩存原生方法,之后調用
const original = arrayProto['push']
let args = Array.from(arguments)
original.apply(this,args)
console.log(obarr)
}
})
obarr.__proto__ = arrayMethods;
現在每次執行obarr.push(0)時,obarr都會新增一項。
上面實現了push方法,其他的方法同理,我們只需要把所有需要實現的方法循環遍歷執行即可,升級后代碼如下:
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(item=>{
Object.defineProperty(arrayMethods,item,{
value:function mutator(){
//緩存原生方法,之后調用
const original = arrayProto[item]
let args = Array.from(arguments)
original.apply(this,args)
},
})
})
function protoAugment (target,src) {
target.__proto__ = src
}
// 調用
let obarr = []
protoAugment(obarr, arrayMethods)
來多試幾次吧:
obarr.push(1) obarr.push(2) obarr.push(3) obarr.push(4)
分析:
1、經過以上的代碼可以看出,只會更改我們給定數組(obarr)的相關方法,而不會污染Array的原生方法,因此其他普通數組不受影響。
2、從新賦值數組的__proto__屬性為arrayMethods,而arrayMethods我們從新定義了push,pop等相關屬性方法,因此當我們使用數組的push,pop等方法時會調用arrayMethods的相關屬性方法,達到監聽數組變化的能力。
3、對於不支持__proto__屬性的瀏覽器,直接使用Object.defineProperty從新定義相關屬性。
4、而Vue的實現方法正如上,更改我們需要監聽的Array數組屬性值(屬性值為函數),在監聽函數里執行數組的原生方法,並通知所有注冊的觀察者進行響應式處理。
下面來簡單的實現Vue對數組的依賴收集和通知更新
實現Vue的數據雙向綁定有3大核心:Observer,Dep,Watcher,來個簡單實現
首先來實現dep,dep主要負責依賴的收集,get時觸發收集,set時通知watcher通信:
class Dep{
constructor () {
// 存放所有的監聽watcher
this.subs = []
}
//添加一個觀察者對象
addSub (Watcher) {
this.subs.push(Watcher)
}
//依賴收集
depend () {
//Dep.target 作用只有需要的才會收集依賴
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 調用依賴收集的Watcher更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 為Dep.target 賦值
function pushTarget (Watcher) {
Dep.target = Watcher
}
再來簡單的實現Watcher,Watcher負責數據變更之后調用Vue的diff進行視圖的更新:
class Watcher{
constructor(vm,expOrFn,cb,options){
//傳進來的對象 例如Vue
this.vm = vm
//在Vue中cb是更新視圖的核心,調用diff並更新視圖的過程
this.cb = cb
//收集Deps,用於移除監聽
this.newDeps = []
this.getter = expOrFn
//設置Dep.target的值,依賴收集時的watcher對象
this.value =this.get()
}
get(){
//設置Dep.target值,用以依賴收集
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
return value
}
//添加依賴
addDep (dep) {
// 這里簡單處理,在Vue中做了重復篩選,即依賴只收集一次,不重復收集依賴
this.newDeps.push(dep)
dep.addSub(this)
}
//更新
update () {
this.run()
}
//更新視圖
run(){
//這里只做簡單的console.log 處理,在Vue中會調用diff過程從而更新視圖
console.log(`這里會去執行Vue的diff相關方法,進而更新數據`)
}
}
簡單實現Observer,Observer負責數據的雙向綁定,並把對象屬性改為getter/setter
//獲得arrayMethods對象上所有屬性的數組
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
class Observer{
constructor (value) {
this.value = value
// 增加dep屬性(處理數組時可以直接調用)
this.dep = new Dep()
//將Observer實例綁定到data的__ob__屬性上面去,后期如果oberve時直接使用,不需要從新Observer,
//處理數組是也可直接獲取Observer對象
def(value, '__ob__', this)
if (Array.isArray(value)) {
//處理數組
const augment = value.__proto__ ? protoAugment : copyAugment
//此處的 arrayMethods 就是上面使用Object.defineProperty處理過
augment(value, arrayMethods, arrayKeys)
// 循環遍歷數組children進行oberve
this.observeArray(value)
} else {
//處理對象
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
//此處我做了攔截處理,防止死循環,Vue中在oberve函數中進行的處理
if(keys[i]=='__ob__') return;
defineReactive(obj, keys[i], obj[keys[i]])
}
}
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
//數據重復Observer
function observe(value){
if(typeof(value) != 'object' ) return;
let ob = new Observer(value)
return ob;
}
// 把對象屬性改為getter/setter,並收集依賴
function defineReactive (obj,key,val) {
const dep = new Dep()
//處理children
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
console.log(`調用get獲取值,值為${val}`)
const value = val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
//此處是對Array數據類型的依賴收集
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
console.log(`調用了set,值為${newVal}`)
const value = val
val = newVal
//對新值進行observe
childOb = observe(newVal)
//通知dep調用,循環調用手機的Watcher依賴,進行視圖的更新
dep.notify()
}
})
}
//輔助方法
function def (obj, key, val) {
Object.defineProperty(obj, key, {
value: val,
enumerable: true,
writable: true,
configurable: true
})
}
//重新賦值Array的__proto__屬性
function protoAugment (target,src) {
target.__proto__ = src
}
//不支持__proto__的直接修改相關屬性方法
function copyAugment (target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
//收集數組的依賴
function dependArray (value) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
//循環遍歷chindren進行依賴收集
dependArray(e)
}
}
}
Observer中寫了一些相關需要的方法。
讓我們來修改下處理數組的相關方法,當使用Array.push相關方法時可以調用Watcher更新視圖
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(item=>{
Object.defineProperty(arrayMethods,item,{
value:function mutator(){
//緩存原生方法,之后調用
const original = arrayProto[item]
let args = Array.from(arguments)
original.apply(this,args)
const ob = this.__ob__
ob.dep.notify()
},
})
})
大功至此告成,把所有代碼整理完整如下:
/*----------------------------------------處理數組------------------------------------*/
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
].forEach(item=>{
Object.defineProperty(arrayMethods,item,{
value:function mutator(){
//緩存原生方法,之后調用
const original = arrayProto[item]
let args = Array.from(arguments)
original.apply(this,args)
const ob = this.__ob__
ob.dep.notify()
},
})
})
/*----------------------------------------Dep---------------------------------------*/
class Dep{
constructor () {
// 存放所有的監聽watcher
this.subs = []
}
//添加一個觀察者對象
addSub (Watcher) {
this.subs.push(Watcher)
}
//依賴收集
depend () {
//Dep.target 作用只有需要的才會收集依賴
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 調用依賴收集的Watcher更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 為Dep.target 賦值
function pushTarget (Watcher) {
Dep.target = Watcher
}
/*----------------------------------------Watcher------------------------------------*/
class Watcher{
constructor(vm,expOrFn,cb,options){
//傳進來的對象 例如Vue
this.vm = vm
//在Vue中cb是更新視圖的核心,調用diff並更新視圖的過程
this.cb = cb
//收集Deps,用於移除監聽
this.newDeps = []
this.getter = expOrFn
//設置Dep.target的值,依賴收集時的watcher對象
this.value =this.get()
}
get(){
//設置Dep.target值,用以依賴收集
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
return value
}
//添加依賴
addDep (dep) {
// 這里簡單處理,在Vue中做了重復篩選,即依賴只收集一次,不重復收集依賴
this.newDeps.push(dep)
dep.addSub(this)
}
//更新
update () {
this.run()
}
//更新視圖
run(){
//這里只做簡單的console.log 處理,在Vue中會調用diff過程從而更新視圖
console.log(`這里會去執行Vue的diff相關方法,進而更新數據`)
}
}
/*----------------------------------------Observer------------------------------------*/
//獲得arrayMethods對象上所有屬性的數組
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
class Observer{
constructor (value) {
this.value = value
// 增加dep屬性(處理數組時可以直接調用)
this.dep = new Dep()
//將Observer實例綁定到data的__ob__屬性上面去,后期如果oberve時直接使用,不需要從新Observer,
//處理數組是也可直接獲取Observer對象
def(value, '__ob__', this)
if (Array.isArray(value)) {
//處理數組
const augment = value.__proto__ ? protoAugment : copyAugment
//此處的 arrayMethods 就是上面使用Object.defineProperty處理過
augment(value, arrayMethods, arrayKeys)
// 循環遍歷數組children進行oberve
this.observeArray(value)
} else {
//處理對象
this.walk(value)
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
//此處我做了攔截處理,防止死循環,Vue中在oberve函數中進行的處理
if(keys[i]=='__ob__') return;
defineReactive(obj, keys[i], obj[keys[i]])
}
}
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
//數據重復Observer
function observe(value){
if(typeof(value) != 'object' ) return;
let ob = new Observer(value)
return ob;
}
// 把對象屬性改為getter/setter,並收集依賴
function defineReactive (obj,key,val) {
const dep = new Dep()
//處理children
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
console.log(`調用get獲取值,值為${val}`)
const value = val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
//此處是對Array數據類型的依賴收集
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
console.log(`調用了set,值為${newVal}`)
const value = val
val = newVal
//對新值進行observe
childOb = observe(newVal)
//通知dep調用,循環調用手機的Watcher依賴,進行視圖的更新
dep.notify()
}
})
}
//輔助方法
function def (obj, key, val) {
Object.defineProperty(obj, key, {
value: val,
enumerable: true,
writable: true,
configurable: true
})
}
//重新賦值Array的__proto__屬性
function protoAugment (target,src) {
target.__proto__ = src
}
//不支持__proto__的直接修改相關屬性方法
function copyAugment (target, src, keys) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
//收集數組的依賴
function dependArray (value) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
//循環遍歷chindren進行依賴收集
dependArray(e)
}
}
}
依賴收集流程圖:

測試:
定義一個data對象:
let data={
name:'zane',
blog:'https://blog.seosiwei.com/',
hobby:['basketball','football'],
list:[
{name:'zhangsan'},
{name:'lishi'}
]
}
調用watcher,並進行數據監聽
let getUpdates = (vm)=>{
console.log('默認調用一次,進行依賴收集')
}
new Watcher(this,getUpdates)
observe(data)
調用get收集依賴
//收集name依賴 data.name //收集hobby依賴 data.hobby
測試數據監聽
//都會打印這里會去執行Vue的diff相關方法,進而更新數據
data.name = 'zhangshan'
data.hobby.push('volleyball')
是不時出現可可愛的 這里會去執行Vue的diff相關方法,進而更新數據 日志呢。
沒進行依賴收集的屬性會打印日志嗎,來嘗試一下吧
//不會打印更新
data.blog = 'http://www.seosiwei.com/'
//不會調用每一個children的打印更新
data.list.push({name:'xiaowang'})
