2.1 變化偵測是干什么的昂芜?
vue在渲染頁面時(shí)莹规,會(huì)根據(jù)數(shù)據(jù)的變化不停的進(jìn)行狀態(tài)的更替,也需要不停的對頁面進(jìn)行渲染泌神,變化偵測就是用來監(jiān)測狀態(tài)的變化良漱。
2.1.1 三大框架是如何監(jiān)測狀態(tài)的舞虱?
Angular&&React:當(dāng)狀態(tài)發(fā)生變化時(shí),發(fā)送信號(hào)告訴框架母市,框架接到信號(hào)后矾兜,通過暴力比對找到需要重新渲染的DOM節(jié)點(diǎn)。Angular中使用臟檢查患久,React中使用虛擬DOM椅寺。
Vue:狀態(tài)變化時(shí),vue立刻知道是誰發(fā)生了變化蒋失,(數(shù)據(jù)變化時(shí)主動(dòng)推送給框架)從而進(jìn)行dom 的更新返帕。
2.2 如何追蹤變化?
(本節(jié)暫只探討如何追蹤一個(gè)對象的變化)
JS中有兩種方式可以追蹤到變化:Object.defineProperty和ES6的Proxy篙挽。
vue中使用了Object.defineProperty來追蹤一個(gè)對象的變化:定義一個(gè)響應(yīng)式數(shù)據(jù)荆萤,每當(dāng)從data的key中讀取數(shù)據(jù)時(shí),get函數(shù)被觸發(fā)铣卡;每當(dāng)往data的key中設(shè)置數(shù)據(jù)是链韭,set函數(shù)被觸發(fā)。具體原理是:
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
}
})
}
2.3 依賴是什么算行?
vue的變化偵測具體是怎么發(fā)生的呢梧油?具體的說,一個(gè)狀態(tài)綁定著多個(gè)依賴州邢,每個(gè)依賴代表著一個(gè)具體的DOM節(jié)點(diǎn)儡陨,當(dāng)這個(gè)狀態(tài)發(fā)生變化時(shí),通過向這個(gè)狀態(tài)的所有依賴發(fā)送通知量淌,讓他們來更新DOM骗村。
那么依賴是什么呢?依賴就是當(dāng)屬性發(fā)生變化時(shí)呀枢,我們要通知的“用到數(shù)據(jù)的地方”胚股。
真實(shí)情況是,使用這個(gè)數(shù)據(jù)的地方有很多裙秋,類型也不一樣(有可能是一個(gè)模板琅拌,也有可能是用戶寫的一個(gè)watch),為了方便處理摘刑,我們需要抽象出一個(gè)能集中處理這些問題的類,然后我們在收集依賴階段只收集這個(gè)封裝好的類的實(shí)例枷恕,通知也只通知它一個(gè),讓它負(fù)責(zé)通知其他地方灾而,這里的“它”就被稱為“依賴”——Watcher旁趟。
2.4 如何收集依賴轻庆?
收集依賴是什么意思余爆?收集依賴就是把用到某一數(shù)據(jù)的地方收集起來的過程蛾方,等數(shù)據(jù)的屬性發(fā)生變化時(shí)上陕,把之前收集的依賴循環(huán)觸發(fā)一遍就好了释簿。
具體的說庶溶,在getter中收集依賴偏螺,在setter中觸發(fā)依賴。
2.5 依賴收集在哪里酿联?
最簡單的思路是:收集的依賴存在一個(gè)數(shù)組里贞让,在getter中將數(shù)據(jù)都push入這個(gè)數(shù)組喳张,然后在setter中循環(huán)這個(gè)數(shù)組來觸發(fā)所有依賴蹲姐。記這個(gè)數(shù)據(jù)為dep柴墩,那么:
function defineReactive(data,key,val){
let dep=[] //新增
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get:function(){
dep.push(window.target) //新增
return val
},
set:function(newVal){
if(val===newVal){
return
}
//新增
for(let i=0;i<dep.length,i++){
dep[i](val,newVal)
}
val=newVal
}
})
}
這樣的代碼耦合度過高江咳,現(xiàn)在我們把dep封裝成一個(gè)類:
export default class Dep(
constructor(){
this.subs=[]
}
addSub(sub){
this.subs.push(sub)
}
removeSub(sub){
remove(this.subs, sub)
}
depend(){
if(window.target){
this.addSub(window.target)
}
}
notify(){
const subs=this.subs.slice()
for(let i=0;i<subs.length;i++){
subs[i].update()
}
}
)
function remove(arr, item){
if(arr.length){
const index=arr.indexOf(item)
if(index > -1){
return arr.splice(index, 1)
}
}
},
function defineReactive(data,key,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=newVal
dep.notify()//新增
}
})
}
2.6 重新介紹watcher 是什么歼指?
白話:Watcher是一個(gè)中介的角色踩身,數(shù)據(jù)發(fā)生變化時(shí)通知它挟阻,然后它再通知其他地方附鸽。
Watcher的原理是把自己設(shè)置到全局唯一的指定位置(例如window.target)瞒瘸,然后讀取數(shù)據(jù)——>觸發(fā)數(shù)據(jù)的getter,在getter中就從全局唯一的那個(gè)位置讀取當(dāng)前正在讀取數(shù)據(jù)的Watcher省撑,并把這個(gè)Watcher收集到Dep丁侄,這樣朝巫,Watcher就可以訂閱任意一個(gè)數(shù)據(jù)的變化劈猿。
Watcher的經(jīng)典使用方法是:
// keypath
vm.$watch('a.b.c',function(newVal,oldVal){
// 做點(diǎn)什么
})
當(dāng)data.a.b.c發(fā)生變化時(shí)揪荣,觸發(fā)第二個(gè)參數(shù)中的函數(shù)
如何實(shí)現(xiàn)這個(gè)功能呢:把這個(gè)watcher實(shí)例添加到data.a.b.c屬性的Dep中,當(dāng)data.a.b.c的值發(fā)生變化時(shí)佛舱,通知Watcher,然后Watcher執(zhí)行參數(shù)中的這個(gè)回調(diào)函數(shù)订歪。代碼如下:
export default class Watcher{
constructor (vm,expOrFn,cb){
this.vm=vm
// 執(zhí)行this.getter()刷晋,就可以讀取data.a.b.c的內(nèi)容
this.getter=parsePath(expOrFn)
this.cb=cb
this.value=this.get()
}
get(){
window.target=this
let value=this.getter.call(this.vm,this.vm)
window.target=underfined
return value
}
update(){
const oldValue=this.value
this.value=this.get()
this.cb.call(this.vm,this.value,oldValue)
}
}
代碼解析:
- 在get()中眼虱,this指向當(dāng)前watcher實(shí)例捏悬,觸發(fā)了getter就會(huì)觸發(fā)收集依賴的機(jī)制润梯,即從window.target中讀取一個(gè)依賴并且添加到Dep中。
- 依賴注入到Dep之后抒和,每當(dāng)data.a.b.c的值發(fā)生變化時(shí)摧莽,就會(huì)讓依賴列表中所有的依賴循環(huán)觸發(fā)update方法顿痪,而update方法會(huì)執(zhí)行參數(shù)中的回調(diào)函數(shù)將value和oldValue傳到參數(shù)中蚁袭。
總結(jié):不管是用戶執(zhí)行vm.$watch('a.b.c',(value,oldValue)=>{})揩悄,還是模板中用到的data,都是通過Watcher來通知自己是否需要發(fā)生變化亏娜。
補(bǔ)充介紹1:parsePath是怎么讀取一個(gè)字符串的keypath的维贺?
//解析簡單路徑
const bailRE=/[^\w.$]/
export 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
}
}
代碼解析:
- 使用.分割keypath為一個(gè)數(shù)組溯泣,然后循環(huán)數(shù)組一層一層去讀數(shù)據(jù),最后拿到的obj就是keypath中想要讀的數(shù)據(jù)客给。
補(bǔ)充介紹2:keypath是什么?
- keypath(鍵路徑)是一個(gè)由 . 作為分隔符的鍵組成的字符串译仗,用于支撐一個(gè)連接在一起的對象性質(zhì)序列官觅。第一個(gè)鍵的性質(zhì)由先前的性質(zhì)決定,接下來每個(gè)鍵的值也是相對于其前面的性質(zhì)咱圆。
- keypath用于鍵值額觀察功氨,當(dāng)另一個(gè)對象的屬性發(fā)生變化時(shí)捷凄,可以直接通知對象跺涤。
- 通過keypath找到這個(gè)屬性的值,然后去檢查這個(gè)值是否有變化航唆,從而得知該對象是否發(fā)生了變化糯钙。
關(guān)于keypath的詳情介紹請點(diǎn)擊:http://www.reibang.com/p/e008f73a35ba
2.7 遞歸偵測所有key
前面介紹的代碼只能偵測數(shù)據(jù)中的某一個(gè)屬性换可,可以封裝一個(gè)肤晓?observer類來偵測數(shù)據(jù)中的所有屬性米碰。
export class Observer(){
constructor (value){
this.value=value
if(!Array.isArray(value)){
this.walk(value)
}
}
walk(obj){
const keys=Object.keys(obj)
for(let i=0;i<keys.length;i++){
defineReactive(obj,keys[i],obj[keys[i]])
}
}
}
function defineReactive(data,key,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=newVal
dep.notify()//新增
}
})
}
代碼解析:
1 Objecter類將一個(gè)正常的object轉(zhuǎn)換成被偵測的object漆诽,即將一個(gè)數(shù)據(jù)內(nèi)的所有屬性都轉(zhuǎn)換成getter/setter的形式厢拭,然后去追蹤它們的變化供鸠。
2 判斷數(shù)據(jù)的類型陨闹,只有Object類型的數(shù)據(jù)才會(huì)調(diào)用walk將每一個(gè)屬性轉(zhuǎn)換成getter/setter的形式來偵測變化寨闹。
3 早defineReactive中新增new Observer(val)來遞歸子屬性君账,這樣就完成了偵測所有屬性的功能。當(dāng)data中的屬性發(fā)生變化時(shí)帖蔓,與這個(gè)屬性相對應(yīng)的依賴就會(huì)接收到通知塑娇。
2.8 關(guān)于Object的問題
Vue.js通過Object.defineProoerty將對象的key轉(zhuǎn)換成getter/setter的形式來追蹤變化埋酬,但是getter/setter只能追蹤一個(gè)數(shù)據(jù)是否是修改写妥,不能追蹤到數(shù)據(jù)是新增還是刪除珍特,為了解決這個(gè)問題魔吐,Vue.js提供了兩個(gè)API:vm.delete扎筒,后文會(huì)介紹莱找。
像對象中新增屬性:
var vm=new Vue(){
el:'#el',
template:'#demo-template',
methods:{
action(){
//新增屬性值
this.obj.name='xiaoming'
}
},
data:{
obj:{}
}
}
在對象中刪除屬性:
var vm=new Vue(){
el:'#el',
template:'#demo-template',
methods:{
action(){
//刪除一個(gè)屬性
delete this.obj.name
}
},
data:{
obj:{
name:'xiaoming'
}
}
}
聲明:本文參考自:劉博文-深入淺出Vue.j