vue
之所以能數據驅動視圖發(fā)生變更的關鍵荣恐,就是依賴它的響應式系統了。響應式系統如果根據數據類型區(qū)分,對象和數組它們的實現會有所不同;解釋響應式原理响逢,如果只是為了說明響應式原理而說,但不是從整體流程出發(fā)棕孙,不在vue
組件化的整體流程中找到響應式原理的位置舔亭,對深刻理解響應式原理并不太好。接下來筆者會從整體流程出發(fā)蟀俊,試著站在巨人的肩膀上分別說明對象和數組的實現原理钦铺。
對象的響應式原理
對象響應式數據的創(chuàng)建
- 在組件的初始化階段,將對傳入的狀態(tài)進行初始化肢预,以下以
data
為例矛洞,會將傳入的數據包裝為響應式的數據。
對象示例:
main.js
new Vue({ // 根組件
render: h => h(App)
})
--------------------------------------------------------------------------------------
app.vue
<template>
<div>{{info.name}}</div> // 只用了info.name屬性
</template>
export default { // app組件
data() {
return {
info: {
name: 'cc',
sex: 'man' // 即使是響應式數據烫映,沒被使用就不會進行依賴收集
}
}
}
}
接下來的分析將以上面代碼為示例沼本,這種結構其實是一個嵌套組件,只不過根組件一般定義的參數比較少而已窑邦,理解這個還是很重要的擅威。
在組件new Vue()
后的執(zhí)行vm._init()
初始化過程中壕探,當執(zhí)行到initState(vm)
時就會對內部使用到的一些狀態(tài)冈钦,如props
、data
李请、computed
瞧筛、watch
、methods
分別進行初始化导盅,再對data
進行初始化的最后有這么一句:
function initData(vm) { //初始化data
...
observe(data) // info:{name:'cc',sex:'man'}
}
這個observe
就是將用戶定義的data
變成響應式的數據较幌,接下來看下它的創(chuàng)建過程:
export function observe(value) {
if(!isObject(value)) { // 不是數組或對象,再見
return
}
return new Observer(value)
}
簡單理解這個observe
方法就是Observer
這個類的工廠方法白翻,所以還是要看下Observer
這個類的定義:
export class Observer {
constructor(value) {
this.value = value
this.walk(value) // 遍歷value
}
walk(obj) {
const keys = Object.keys(obj)
for(let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // 只傳入了兩個參數
}
}
}
當執(zhí)行new Observer
時乍炉,首先將傳入的對象掛載到當前this
下绢片,然后遍歷當前對象的每一項,執(zhí)行defineReactive
這個方法岛琼,看下它的定義:
export function defineReactive(obj, key, val) {
const dep = new Dep() // 依賴管理器
val = obj[key] // 計算出對應key的值
observe(val) // 遞歸包裝對象的嵌套屬性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
... 收集依賴
},
set(newVal) {
... 派發(fā)更新
}
})
}
這個方法的作用就是使用Object.defineProperty
創(chuàng)建響應式數據底循。首先根據傳入的obj
和key
計算出val
具體的值;如果val
還是對象槐瑞,那就使用observe
方法進行遞歸創(chuàng)建熙涤,在遞歸的過程中使用Object.defineProperty
將對象的每一個屬性都變成響應式數據:
...
data() {
return {
info: {
name: 'cc',
sex: 'man'
}
}
}
這段代碼就會有三個響應式數據:
info, info.name, info.sex
知識點:
Object.defineProperty
內的get
方法,它的作用就是誰訪問到當前key
的值就用defineReactive
內的dep
將它收集起來困檩,也就是依賴收集的意思祠挫。set
方法的作用就是當前key
的值被賦值了,就通知dep
內收集到的依賴項悼沿,key
的值發(fā)生了變更等舔,視圖請變更吧~
這個時候get
和set
只是定義了,并不會觸發(fā)糟趾。什么是依賴我們接下來說明软瞎,首先還是用一張圖幫大家理清響應式數據的創(chuàng)建過程:
依賴收集
什么是依賴了?我們看下之前mountComponent
的定義:
function mountComponent(vm, el) {
...
const updateComponent = function() {
vm._update(vm._render())
}
new Watcher(vm, updateComponent, noop, { // 渲染watcher
...
}, true) // true為標志拉讯,表示是否是渲染watcher
...
}
我們首先說明下這個Watcher
類涤浇,它類似與之前的VNode
類,根據傳入的參數不同魔慷,可以分別實例化出三種不同的Watcher
實例只锭,它們分別是用戶watcher
,計算watcher
以及渲染watcher
:
用戶
(user) watcher
- 也就是用戶自己定義的院尔,如:
new Vue({
data {
msg: 'hello Vue!'
}
created() {
this.$watch('msg', cb()) // 定義用戶watcher
},
watch: {
msg() {...} // 定義用戶watcher
}
})
這里的兩種方式內部都是使用Watcher
這個類實例化的蜻展,只是參數不同,具體實現我們之后章節(jié)說明邀摆,這里大家只用知道這個是用戶watcher
即可纵顾。
計算
(computed) watcher
- 顧名思義,這個是當定義計算屬性實例化出來的一種:
new Vue({
data: {
msg: 'hello'
},
computed() {
sayHi() { // 計算watcher
return this.msg + 'vue!'
}
}
})
渲染
(render) watcher
- 只是用做視圖渲染而定義的
Watcher
實例栋盹,再組件執(zhí)行vm.$mount
的最后會實例化Watcher
類施逾,這個時候就是以渲染watcher
的格式定義的,收集的就是當前渲染watcher
的實例例获,我們來看下它內部是如何定義的:
class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
if(isRenderWatcher) { // 是否是渲染watcher
vm._watcher = this // 當前組件下掛載vm._watcher屬性
}
vm._watchers.push(this) //vm._watchers是之前初始化initState時定義的[]
this.before = options.before // 渲染watcher特有屬性
this.getter = expOrFn // 第二個參數
this.get() // 實例化就會執(zhí)行this.get()方法
}
get() {
pushTarget(this) // 添加
...
this.getter.call(this.vm, this.vm) // 執(zhí)行vm._update(vm._render())
...
popTarget() // 移除
}
addDep(dep) {
...
dep.addSub(this) // 將當前watcher收集到dep實例中
}
}
當執(zhí)行new Watcher
的時候內部會掛載一些屬性汉额,然后執(zhí)行this.get()
這個方法,首先會執(zhí)行一個全局的方法pushTarget(this)
榨汤,傳入當前watcher
的實例蠕搜,我們看下這個方法定義的地方:
Dep.target = null
const targetStack = [] // 組件從父到子對應的watcher實例集合
export function pushTarget (_target) { // 添加
if (Dep.target) {
targetStack.push(Dep.target) // 添加到集合內
}
Dep.target = _target // 當前的watcher實例
}
export function popTarget() { // 移除
targetStack.pop() // 移除數組最后一項
Dep.target = targetStack[targetStack.length - 1] // 賦值為數組最后一項
}
首先會定義一個Dep
類的靜態(tài)屬性Dep.target
為null
,這是一個全局會用到的屬性收壕,保存的是當前組件對應渲染watcher
的實例妓灌;targetStack
內存儲的是再執(zhí)行組件化的過程中每個組件對應的渲染watcher
實例集合轨蛤,使用的是一個先進后出的形式來管理數組的數據,這里可能有點不太好懂虫埂,稍等再看到最后的流程圖后自然就明白了俱萍;然后將傳入的watcher
實例賦值給全局屬性Dep.target
,再之后的依賴收集過程中就是收集的它告丢。
watcher
的get
這個方法然后會執(zhí)行getter
這個方法枪蘑,它是new Watcher
時傳入的第二個參數,這個參數就是之前的updateComponent
變量:
function mountComponent(vm, el) {
...
const updateComponent = function() { //第二個參數
vm._update(vm._render())
}
...
}
只要一執(zhí)行就會執(zhí)行當前組件實例上的vm._update(vm._render())
將render
函數轉為VNode
岖免,這個時候如果render
函數內有使用到data
中已經轉為了響應式的數據岳颇,就會觸發(fā)get
方法進行依賴的收集,補全之前依賴收集的邏輯:
export function defineReactive(obj, key, val) {
const dep = new Dep() // 依賴管理器
val = obj[key] // 計算出對應key的值
observe(val) // 遞歸的轉化對象的嵌套屬性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() { // 觸發(fā)依賴收集
if(Dep.target) { // 之前賦值的當前watcher實例
dep.depend() // 收集起來颅湘,放入到上面的dep依賴管理器內
...
}
return val
},
set(newVal) {
... 派發(fā)更新
}
})
}
這個時候我們知道watcher
是個什么東西了话侧,簡單理解就是數據和組件之間一個通信工具的封裝,當某個數據被組件讀取時闯参,就將依賴數據的組件使用Dep
這個類給收集起來瞻鹏。
當前例子data
內的屬性是只有一個渲染watcher
的,因為沒有被其他組件所使用鹿寨。但如果該屬性被其他組件使用到新博,也會將使用它的組件收集起來,例如作為了props
傳遞給了子組件脚草,再dep
的數組內就會存在多個渲染watcher
赫悄。我們來看下Dep
類這個依賴管理器的定義:
let uid = 0
export default class Dep {
constructor() {
this.id = uid++
this.subs = [] // 對象某個key的依賴集合
}
addSub(sub) { // 添加watcher實例到數組內
this.subs.push(sub)
}
depend() {
if(Dep.target) { // 已經被賦值為了watcher的實例
Dep.target.addDep(this) // 執(zhí)行watcher的addDep方法
}
}
}
----------------------------------------------------------
class Watcher{
...
addDep(dep) { // 將當前watcher實例添加到dep內
...
dep.addSub(this) // 執(zhí)行dep的addSub方法
}
}
這個Dep
類的作用就是管理屬性對應的watcher
,如添加/刪除/通知馏慨。至此埂淮,依賴收集的過程算是完成了,還是以一張圖片加深對過程的理解:
派發(fā)更新
如果只是收集依賴写隶,那其實是沒任何意義的倔撞,將收集到的依賴在數據發(fā)生變化時通知到并引起視圖變化,這樣才有意義慕趴。如現在我們對數據重新賦值:
app.vue
export default { // app組件
...
methods: {
changeInfo() {
this.info.name = 'ww';
}
}
}
這個時候就會觸發(fā)創(chuàng)建響應式數據時的set
方法了痪蝇,我們再補全那里的邏輯:
export function defineReactive(obj, key, val) {
const dep = new Dep() // 依賴管理器
val = obj[key] // 計算出對應key的值
observe(val) // 遞歸轉化對象的嵌套屬性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
... 依賴收集
},
set(newVal) { // 派發(fā)更新
if(newVal === val) { // 相同
return
}
val = newVal // 賦值
observer(newVal) // 如果新值是對象也遞歸包裝
dep.notify() // 通知更新
}
})
}
當賦值觸發(fā)set
時,首先會檢測新值和舊值秩贰,不能相同霹俺;然后將新值賦值給舊值柔吼;如果新值是對象則將它變成響應式的毒费;最后讓對應屬性的依賴管理器使用dep.notify
發(fā)出更新視圖的通知。我們看下它的實現:
let uid = 0
class Dep{
constructor() {
this.id = uid++
this.subs = []
}
notify() { // 通知
const subs = this.subs.slice()
for(let i = 0, i < subs.length; i++) {
subs[i].update() // 挨個觸發(fā)watcher的update方法
}
}
}
這里做的事情只有一件愈魏,將收集起來的watcher
挨個遍歷觸發(fā)update
方法:
class Watcher{
...
update() {
queueWatcher(this)
}
}
---------------------------------------------------------
const queue = []
let has = {}
function queueWatcher(watcher) {
const id = watcher.id
if(has[id] == null) { // 如果某個watcher沒有被推入隊列
...
has[id] = true // 已經推入
queue.push(watcher) // 推入到隊列
}
...
nextTick(flushSchedulerQueue) // 下一個tick更新
}
執(zhí)行update
方法時將當前watcher
實例傳入到定義的queueWatcher
方法內觅玻,這個方法的作用是把將要執(zhí)行更新的watcher
收集到一個隊列queue
之內想际,保證如果同一個watcher
內觸發(fā)了多次更新,只會更新一次對應的watcher
溪厘,我們舉兩個小示例:
export default {
data() {
return { // 都被模板引用了
num: 0,
name: 'cc',
sex: 'man'
}
},
methods: {
changeNum() { // 賦值100次
for(let i = 0; i < 100; i++) {
this.num++
}
},
changeInfo() { // 一次賦值多個屬性的值
this.name = 'ww'
this.sex = 'woman'
}
}
}
這里的三個響應式屬性它們收集都是同一個渲染watcher
胡本。所以當賦值100次的情況出現時,再將當前的渲染watcher
推入到的隊列之后畸悬,之后賦值觸發(fā)的set
隊列內并不會添加任何渲染watcher
侧甫;當同時賦值多個屬性時也是,因為它們收集的都是同一個渲染watcher
蹋宦,所以推入到隊列一次之后就不會添加了披粟。
知識點:
vue
還是挺聰明的,通過這兩個實例大家也看出來了冷冗,派發(fā)更新通知的粒度是組件級別守屉,至于組件內是哪個屬性賦值了,派發(fā)更新并不關心蒿辙,而且怎么高效更新這個視圖拇泛,那是之后diff
比對做的事情。
隊列有了思灌,執(zhí)行nextTick(flushSchedulerQueue)
再下一次tick
時更新它俺叭,這里的nextTick
就是我們經常使用的this.$nextTick
方法的原始方法,它們作用一致泰偿,實現原理之后章節(jié)說明绪颖。看下參數flushSchedulerQueue
是個啥甜奄?
let index = 0
function flushSchedulerQueue() {
let watcher, id
queue.sort((a, b) => a.id - b.id) // watcher 排序
for(index = 0; index < queue.length; index++) { // 遍歷隊列
watcher = queue[index]
if(watcher.before) { // 渲染watcher獨有屬性
watcher.before() // 觸發(fā) beforeUpdate 鉤子
}
id = watcher.id
has[id] = null
watcher.run() // 真正的更新方法
...
}
}
原來是個函數柠横,再nextTick
方法的內部會執(zhí)行第一個參數。首先會將queue
這個隊列進行一次排序课兄,依據是每次new Watcher
生成的id
牍氛,以從小到大的順序。當前示例只是做渲染烟阐,而且隊列內只存在了一個渲染watcher
搬俊,所以是不存在順序的。但是如果有定義user watcher
和computed watcher
加上render watcher
后蜒茄,它們之間就會存在一個執(zhí)行順序的問題了唉擂。
知識點:
watcher
的執(zhí)行順序是先父后子,然后是從computed watcher
到user watcher
最后render watcher
檀葛,這從它們的初始化順序就能看出玩祟。
然后就是遍歷這個隊列,因為是渲染watcher
屿聋,所有是有before
屬性的空扎,執(zhí)行傳入的before
方法觸發(fā)beforeUpdate
鉤子藏鹊。最后執(zhí)行watcher.run()
方法,執(zhí)行真正的派發(fā)更新方法转锈。我們去看下run
干了啥:
class Watcher {
...
run () {
if (this.active) {
this.getAndInvoke(this.cb) // 有一種要抓狂的感覺
}
}
getAndInvoke(cb) { // 渲染watcher的cb為noop空函數
const value = this.get()
... 后面是用戶watcher邏輯
}
}
執(zhí)行run
就是執(zhí)行getAndInvoke
方法盘寡,因為是渲染watcher
,參數cb
是noop
空函數撮慨「吞担看了這么多,其實...就是重新執(zhí)行一次this.get()
方法砌溺,讓vm._update(vm._render())
再走一遍而已菇曲。然后生成新舊VNode
,最后進行diff
比對以更新視圖抚吠。
最后我們來說下vue
基于Object.defineProperty
響應式系統的一些不足常潮。如只能監(jiān)聽到數據的變化,所以有時data
中要定義一堆的初始值楷力,因為加入了響應式系統后才能被感知到喊式;還有就是常規(guī)JavaScript
操作對象的方式,并不能監(jiān)聽到增加以及刪除萧朝,例如:
export default {
data() {
return {
info: {
name: 'cc'
}
}
},
methods: {
addInfo() { // 增加屬性
this.info.sex = 'man'
},
delInfo() { // 刪除屬性
delete info.name
}
}
}
數據是被賦值了岔留,但是視圖并不會發(fā)生變更。vue
為了解決這個問題检柬,提供了兩個API
:$set
和$delete
献联,它們又是怎么辦到的了?原理之后章節(jié)分析何址。
最后慣例的面試問答就扯扯最近工作中遇到趣事吧里逆。對于一個數據不會變更的列表,筆者把它定義再了created
鉤子內用爪,很少結對編程原押,這次例外。
created() {
this.list = [...]
}
旁邊的妹子接過后:
妹子: 這個列表怎么data里沒有阿偎血?在哪定義的诸衔?
我:我定義在created鉤子里了。
妹子:你怎么定義在這了颇玷?
我:因為它是不會被變更的笨农,所以不需要... 算了,那你移到data里吧谒亦。
妹子:嗯!? 好羞延。 小聲說道:我還是第一次看見這么寫的渣淳。
我:...有種被嫌棄了的感覺
面試官微笑而又不失禮貌的問道:
- 當前組件模板中用到的變量一定要定義在
data
里么?
懟回去:
-
data
中的變量都會被代理到當前this
下入愧,所以我們也可以在this
下掛載屬性,只要不重名即可棺蛛。而且定義在data
中的變量在vue
的內部會將它包裝成響應式的數據怔蚌,讓它擁有變更即可驅動視圖變化的能力。但是如果這個數據不需要驅動視圖旁赊,定義在created
或mounted
鉤子內也是可以的桦踊,因為不會執(zhí)行響應式的包裝方法,對性能也是一種提升终畅。
順手點個贊或關注唄籍胯,找起來也方便~
分享一個筆者自己寫的組件庫,哪天可能會用的上了 ~ ↓