vue之所以能數(shù)據(jù)驅(qū)動(dòng)視圖發(fā)生變更的關(guān)鍵,就是依賴(lài)它的響應(yīng)式系統(tǒng)了。響應(yīng)式系統(tǒng)如果根據(jù)數(shù)據(jù)類(lèi)型區(qū)分,對(duì)象和數(shù)組它們的實(shí)現(xiàn)會(huì)有所不同;解釋響應(yīng)式原理,如果只是為了說(shuō)明響應(yīng)式原理而說(shuō),但不是從整體流程出發(fā),不在vue組件化的整體流程中找到響應(yīng)式原理的位置,對(duì)深刻理解響應(yīng)式原理并不太好。接下來(lái)筆者會(huì)從整體流程出發(fā),試著站在巨人的肩膀上分別說(shuō)明對(duì)象和數(shù)組的實(shí)現(xiàn)原理。
對(duì)象的響應(yīng)式原理
對(duì)象響應(yīng)式數(shù)據(jù)的創(chuàng)建
- 在組件的初始化階段,將對(duì)傳入的狀態(tài)進(jìn)行初始化,以下以
data為例,會(huì)將傳入的數(shù)據(jù)包裝為響應(yīng)式的數(shù)據(jù)。
對(duì)象示例:
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' // 即使是響應(yīng)式數(shù)據(jù),沒(méi)被使用就不會(huì)進(jìn)行依賴(lài)收集
}
}
}
}
接下來(lái)的分析將以上面代碼為示例,這種結(jié)構(gòu)其實(shí)是一個(gè)嵌套組件,只不過(guò)根組件一般定義的參數(shù)比較少而已,理解這個(gè)還是很重要的。
在組件new Vue()后的執(zhí)行vm._init()初始化過(guò)程中,當(dāng)執(zhí)行到initState(vm)時(shí)就會(huì)對(duì)內(nèi)部使用到的一些狀態(tài),如props、data、computed、watch、methods分別進(jìn)行初始化,再對(duì)data進(jìn)行初始化的最后有這么一句:
function initData(vm) { //初始化data
...
observe(data) // info:{name:'cc',sex:'man'}
}
這個(gè)observe就是將用戶(hù)定義的data變成響應(yīng)式的數(shù)據(jù),接下來(lái)看下它的創(chuàng)建過(guò)程:
export function observe(value) {
if(!isObject(value)) { // 不是數(shù)組或?qū)ο螅僖?jiàn)
return
}
return new Observer(value)
}
簡(jiǎn)單理解這個(gè)observe方法就是Observer這個(gè)類(lèi)的工廠方法,所以還是要看下Observer這個(gè)類(lèi)的定義:
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]) // 只傳入了兩個(gè)參數(shù)
}
}
}
當(dāng)執(zhí)行new Observer時(shí),首先將傳入的對(duì)象掛載到當(dāng)前this下,然后遍歷當(dāng)前對(duì)象的每一項(xiàng),執(zhí)行defineReactive這個(gè)方法,看下它的定義:
export function defineReactive(obj, key, val) {
const dep = new Dep() // 依賴(lài)管理器
val = obj[key] // 計(jì)算出對(duì)應(yīng)key的值
observe(val) // 遞歸包裝對(duì)象的嵌套屬性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
... 收集依賴(lài)
},
set(newVal) {
... 派發(fā)更新
}
})
}
這個(gè)方法的作用就是使用Object.defineProperty創(chuàng)建響應(yīng)式數(shù)據(jù)。首先根據(jù)傳入的obj和key計(jì)算出val具體的值;如果val還是對(duì)象,那就使用observe方法進(jìn)行遞歸創(chuàng)建,在遞歸的過(guò)程中使用Object.defineProperty將對(duì)象的每一個(gè)屬性都變成響應(yīng)式數(shù)據(jù):
...
data() {
return {
info: {
name: 'cc',
sex: 'man'
}
}
}
這段代碼就會(huì)有三個(gè)響應(yīng)式數(shù)據(jù):
info, info.name, info.sex
知識(shí)點(diǎn):
Object.defineProperty內(nèi)的get方法,它的作用就是誰(shuí)訪問(wèn)到當(dāng)前key的值就用defineReactive內(nèi)的dep將它收集起來(lái),也就是依賴(lài)收集的意思。set方法的作用就是當(dāng)前key的值被賦值了,就通知dep內(nèi)收集到的依賴(lài)項(xiàng),key的值發(fā)生了變更,視圖請(qǐng)變更吧~
這個(gè)時(shí)候get和set只是定義了,并不會(huì)觸發(fā)。什么是依賴(lài)我們接下來(lái)說(shuō)明,首先還是用一張圖幫大家理清響應(yīng)式數(shù)據(jù)的創(chuàng)建過(guò)程:
依賴(lài)收集
什么是依賴(lài)了?我們看下之前mountComponent的定義:
function mountComponent(vm, el) {
...
const updateComponent = function() {
vm._update(vm._render())
}
new Watcher(vm, updateComponent, noop, { // 渲染watcher
...
}, true) // true為標(biāo)志,表示是否是渲染watcher
...
}
我們首先說(shuō)明下這個(gè)Watcher類(lèi),它類(lèi)似與之前的VNode類(lèi),根據(jù)傳入的參數(shù)不同,可以分別實(shí)例化出三種不同的Watcher實(shí)例,它們分別是用戶(hù)watcher,計(jì)算watcher以及渲染watcher:
用戶(hù)
(user) watcher
- 也就是用戶(hù)自己定義的,如:
new Vue({
data {
msg: 'hello Vue!'
}
created() {
this.$watch('msg', cb()) // 定義用戶(hù)watcher
},
watch: {
msg() {...} // 定義用戶(hù)watcher
}
})
這里的兩種方式內(nèi)部都是使用Watcher這個(gè)類(lèi)實(shí)例化的,只是參數(shù)不同,具體實(shí)現(xiàn)我們之后章節(jié)說(shuō)明,這里大家只用知道這個(gè)是用戶(hù)watcher即可。
計(jì)算
(computed) watcher
- 顧名思義,這個(gè)是當(dāng)定義計(jì)算屬性實(shí)例化出來(lái)的一種:
new Vue({
data: {
msg: 'hello'
},
computed() {
sayHi() { // 計(jì)算watcher
return this.msg + 'vue!'
}
}
})
渲染
(render) watcher
- 只是用做視圖渲染而定義的
Watcher實(shí)例,再組件執(zhí)行vm.$mount的最后會(huì)實(shí)例化Watcher類(lèi),這個(gè)時(shí)候就是以渲染watcher的格式定義的,收集的就是當(dāng)前渲染watcher的實(shí)例,我們來(lái)看下它內(nèi)部是如何定義的:
class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
if(isRenderWatcher) { // 是否是渲染watcher
vm._watcher = this // 當(dāng)前組件下掛載vm._watcher屬性
}
vm._watchers.push(this) //vm._watchers是之前初始化initState時(shí)定義的[]
this.before = options.before // 渲染watcher特有屬性
this.getter = expOrFn // 第二個(gè)參數(shù)
this.get() // 實(shí)例化就會(huì)執(zhí)行this.get()方法
}
get() {
pushTarget(this) // 添加
...
this.getter.call(this.vm, this.vm) // 執(zhí)行vm._update(vm._render())
...
popTarget() // 移除
}
addDep(dep) {
...
dep.addSub(this) // 將當(dāng)前watcher收集到dep實(shí)例中
}
}
當(dāng)執(zhí)行new Watcher的時(shí)候內(nèi)部會(huì)掛載一些屬性,然后執(zhí)行this.get()這個(gè)方法,首先會(huì)執(zhí)行一個(gè)全局的方法pushTarget(this),傳入當(dāng)前watcher的實(shí)例,我們看下這個(gè)方法定義的地方:
Dep.target = null
const targetStack = [] // 組件從父到子對(duì)應(yīng)的watcher實(shí)例集合
export function pushTarget (_target) { // 添加
if (Dep.target) {
targetStack.push(Dep.target) // 添加到集合內(nèi)
}
Dep.target = _target // 當(dāng)前的watcher實(shí)例
}
export function popTarget() { // 移除
targetStack.pop() // 移除數(shù)組最后一項(xiàng)
Dep.target = targetStack[targetStack.length - 1] // 賦值為數(shù)組最后一項(xiàng)
}
首先會(huì)定義一個(gè)Dep類(lèi)的靜態(tài)屬性Dep.target為null,這是一個(gè)全局會(huì)用到的屬性,保存的是當(dāng)前組件對(duì)應(yīng)渲染watcher的實(shí)例;targetStack內(nèi)存儲(chǔ)的是再執(zhí)行組件化的過(guò)程中每個(gè)組件對(duì)應(yīng)的渲染watcher實(shí)例集合,使用的是一個(gè)先進(jìn)后出的形式來(lái)管理數(shù)組的數(shù)據(jù),這里可能有點(diǎn)不太好懂,稍等再看到最后的流程圖后自然就明白了;然后將傳入的watcher實(shí)例賦值給全局屬性Dep.target,再之后的依賴(lài)收集過(guò)程中就是收集的它。
watcher的get這個(gè)方法然后會(huì)執(zhí)行getter這個(gè)方法,它是new Watcher時(shí)傳入的第二個(gè)參數(shù),這個(gè)參數(shù)就是之前的updateComponent變量:
function mountComponent(vm, el) {
...
const updateComponent = function() { //第二個(gè)參數(shù)
vm._update(vm._render())
}
...
}
只要一執(zhí)行就會(huì)執(zhí)行當(dāng)前組件實(shí)例上的vm._update(vm._render())將render函數(shù)轉(zhuǎn)為VNode,這個(gè)時(shí)候如果render函數(shù)內(nèi)有使用到data中已經(jīng)轉(zhuǎn)為了響應(yīng)式的數(shù)據(jù),就會(huì)觸發(fā)get方法進(jìn)行依賴(lài)的收集,補(bǔ)全之前依賴(lài)收集的邏輯:
export function defineReactive(obj, key, val) {
const dep = new Dep() // 依賴(lài)管理器
val = obj[key] // 計(jì)算出對(duì)應(yīng)key的值
observe(val) // 遞歸的轉(zhuǎn)化對(duì)象的嵌套屬性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() { // 觸發(fā)依賴(lài)收集
if(Dep.target) { // 之前賦值的當(dāng)前watcher實(shí)例
dep.depend() // 收集起來(lái),放入到上面的dep依賴(lài)管理器內(nèi)
...
}
return val
},
set(newVal) {
... 派發(fā)更新
}
})
}
這個(gè)時(shí)候我們知道watcher是個(gè)什么東西了,簡(jiǎn)單理解就是數(shù)據(jù)和組件之間一個(gè)通信工具的封裝,當(dāng)某個(gè)數(shù)據(jù)被組件讀取時(shí),就將依賴(lài)數(shù)據(jù)的組件使用Dep這個(gè)類(lèi)給收集起來(lái)。
當(dāng)前例子data內(nèi)的屬性是只有一個(gè)渲染watcher的,因?yàn)闆](méi)有被其他組件所使用。但如果該屬性被其他組件使用到,也會(huì)將使用它的組件收集起來(lái),例如作為了props傳遞給了子組件,再dep的數(shù)組內(nèi)就會(huì)存在多個(gè)渲染watcher。我們來(lái)看下Dep類(lèi)這個(gè)依賴(lài)管理器的定義:
let uid = 0
export default class Dep {
constructor() {
this.id = uid++
this.subs = [] // 對(duì)象某個(gè)key的依賴(lài)集合
}
addSub(sub) { // 添加watcher實(shí)例到數(shù)組內(nèi)
this.subs.push(sub)
}
depend() {
if(Dep.target) { // 已經(jīng)被賦值為了watcher的實(shí)例
Dep.target.addDep(this) // 執(zhí)行watcher的addDep方法
}
}
}
----------------------------------------------------------
class Watcher{
...
addDep(dep) { // 將當(dāng)前watcher實(shí)例添加到dep內(nèi)
...
dep.addSub(this) // 執(zhí)行dep的addSub方法
}
}
這個(gè)Dep類(lèi)的作用就是管理屬性對(duì)應(yīng)的watcher,如添加/刪除/通知。至此,依賴(lài)收集的過(guò)程算是完成了,還是以一張圖片加深對(duì)過(guò)程的理解:
派發(fā)更新
如果只是收集依賴(lài),那其實(shí)是沒(méi)任何意義的,將收集到的依賴(lài)在數(shù)據(jù)發(fā)生變化時(shí)通知到并引起視圖變化,這樣才有意義。如現(xiàn)在我們對(duì)數(shù)據(jù)重新賦值:
app.vue
export default { // app組件
...
methods: {
changeInfo() {
this.info.name = 'ww';
}
}
}
這個(gè)時(shí)候就會(huì)觸發(fā)創(chuàng)建響應(yīng)式數(shù)據(jù)時(shí)的set方法了,我們?cè)傺a(bǔ)全那里的邏輯:
export function defineReactive(obj, key, val) {
const dep = new Dep() // 依賴(lài)管理器
val = obj[key] // 計(jì)算出對(duì)應(yīng)key的值
observe(val) // 遞歸轉(zhuǎn)化對(duì)象的嵌套屬性
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
... 依賴(lài)收集
},
set(newVal) { // 派發(fā)更新
if(newVal === val) { // 相同
return
}
val = newVal // 賦值
observer(newVal) // 如果新值是對(duì)象也遞歸包裝
dep.notify() // 通知更新
}
})
}
當(dāng)賦值觸發(fā)set時(shí),首先會(huì)檢測(cè)新值和舊值,不能相同;然后將新值賦值給舊值;如果新值是對(duì)象則將它變成響應(yīng)式的;最后讓對(duì)應(yīng)屬性的依賴(lài)管理器使用dep.notify發(fā)出更新視圖的通知。我們看下它的實(shí)現(xiàn):
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() // 挨個(gè)觸發(fā)watcher的update方法
}
}
}
這里做的事情只有一件,將收集起來(lái)的watcher挨個(gè)遍歷觸發(fā)update方法:
class Watcher{
...
update() {
queueWatcher(this)
}
}
---------------------------------------------------------
const queue = []
let has = {}
function queueWatcher(watcher) {
const id = watcher.id
if(has[id] == null) { // 如果某個(gè)watcher沒(méi)有被推入隊(duì)列
...
has[id] = true // 已經(jīng)推入
queue.push(watcher) // 推入到隊(duì)列
}
...
nextTick(flushSchedulerQueue) // 下一個(gè)tick更新
}
執(zhí)行update方法時(shí)將當(dāng)前watcher實(shí)例傳入到定義的queueWatcher方法內(nèi),這個(gè)方法的作用是把將要執(zhí)行更新的watcher收集到一個(gè)隊(duì)列queue之內(nèi),保證如果同一個(gè)watcher內(nèi)觸發(fā)了多次更新,只會(huì)更新一次對(duì)應(yīng)的watcher,我們舉兩個(gè)小示例:
export default {
data() {
return { // 都被模板引用了
num: 0,
name: 'cc',
sex: 'man'
}
},
methods: {
changeNum() { // 賦值100次
for(let i = 0; i < 100; i++) {
this.num++
}
},
changeInfo() { // 一次賦值多個(gè)屬性的值
this.name = 'ww'
this.sex = 'woman'
}
}
}
這里的三個(gè)響應(yīng)式屬性它們收集都是同一個(gè)渲染watcher。所以當(dāng)賦值100次的情況出現(xiàn)時(shí),再將當(dāng)前的渲染watcher推入到的隊(duì)列之后,之后賦值觸發(fā)的set隊(duì)列內(nèi)并不會(huì)添加任何渲染watcher;當(dāng)同時(shí)賦值多個(gè)屬性時(shí)也是,因?yàn)樗鼈兪占亩际峭粋€(gè)渲染watcher,所以推入到隊(duì)列一次之后就不會(huì)添加了。
知識(shí)點(diǎn):
vue還是挺聰明的,通過(guò)這兩個(gè)實(shí)例大家也看出來(lái)了,派發(fā)更新通知的粒度是組件級(jí)別,至于組件內(nèi)是哪個(gè)屬性賦值了,派發(fā)更新并不關(guān)心,而且怎么高效更新這個(gè)視圖,那是之后diff比對(duì)做的事情。
隊(duì)列有了,執(zhí)行nextTick(flushSchedulerQueue)再下一次tick時(shí)更新它,這里的nextTick就是我們經(jīng)常使用的this.$nextTick方法的原始方法,它們作用一致,實(shí)現(xiàn)原理之后章節(jié)說(shuō)明??聪聟?shù)flushSchedulerQueue是個(gè)啥?
let index = 0
function flushSchedulerQueue() {
let watcher, id
queue.sort((a, b) => a.id - b.id) // watcher 排序
for(index = 0; index < queue.length; index++) { // 遍歷隊(duì)列
watcher = queue[index]
if(watcher.before) { // 渲染watcher獨(dú)有屬性
watcher.before() // 觸發(fā) beforeUpdate 鉤子
}
id = watcher.id
has[id] = null
watcher.run() // 真正的更新方法
...
}
}
原來(lái)是個(gè)函數(shù),再nextTick方法的內(nèi)部會(huì)執(zhí)行第一個(gè)參數(shù)。首先會(huì)將queue這個(gè)隊(duì)列進(jìn)行一次排序,依據(jù)是每次new Watcher生成的id,以從小到大的順序。當(dāng)前示例只是做渲染,而且隊(duì)列內(nèi)只存在了一個(gè)渲染watcher,所以是不存在順序的。但是如果有定義user watcher和computed watcher加上render watcher后,它們之間就會(huì)存在一個(gè)執(zhí)行順序的問(wèn)題了。
知識(shí)點(diǎn):
watcher的執(zhí)行順序是先父后子,然后是從computed watcher到user watcher最后render watcher,這從它們的初始化順序就能看出。
然后就是遍歷這個(gè)隊(duì)列,因?yàn)槭卿秩?code>watcher,所有是有before屬性的,執(zhí)行傳入的before方法觸發(fā)beforeUpdate鉤子。最后執(zhí)行watcher.run()方法,執(zhí)行真正的派發(fā)更新方法。我們?nèi)タ聪?code>run干了啥:
class Watcher {
...
run () {
if (this.active) {
this.getAndInvoke(this.cb) // 有一種要抓狂的感覺(jué)
}
}
getAndInvoke(cb) { // 渲染watcher的cb為noop空函數(shù)
const value = this.get()
... 后面是用戶(hù)watcher邏輯
}
}
執(zhí)行run就是執(zhí)行getAndInvoke方法,因?yàn)槭卿秩?code>watcher,參數(shù)cb是noop空函數(shù)??戳诉@么多,其實(shí)...就是重新執(zhí)行一次this.get()方法,讓vm._update(vm._render())再走一遍而已。然后生成新舊VNode,最后進(jìn)行diff比對(duì)以更新視圖。
最后我們來(lái)說(shuō)下vue基于Object.defineProperty響應(yīng)式系統(tǒng)的一些不足。如只能監(jiān)聽(tīng)到數(shù)據(jù)的變化,所以有時(shí)data中要定義一堆的初始值,因?yàn)榧尤肓隧憫?yīng)式系統(tǒng)后才能被感知到;還有就是常規(guī)JavaScript操作對(duì)象的方式,并不能監(jiān)聽(tīng)到增加以及刪除,例如:
export default {
data() {
return {
info: {
name: 'cc'
}
}
},
methods: {
addInfo() { // 增加屬性
this.info.sex = 'man'
},
delInfo() { // 刪除屬性
delete info.name
}
}
}
數(shù)據(jù)是被賦值了,但是視圖并不會(huì)發(fā)生變更。vue為了解決這個(gè)問(wèn)題,提供了兩個(gè)API:$set和$delete,它們又是怎么辦到的了?原理之后章節(jié)分析。
最后慣例的面試問(wèn)答就扯扯最近工作中遇到趣事吧。對(duì)于一個(gè)數(shù)據(jù)不會(huì)變更的列表,筆者把它定義再了created鉤子內(nèi),很少結(jié)對(duì)編程,這次例外。
created() {
this.list = [...]
}
旁邊的妹子接過(guò)后:
妹子: 這個(gè)列表怎么data里沒(méi)有阿?在哪定義的?
我:我定義在created鉤子里了。
妹子:你怎么定義在這了?
我:因?yàn)樗遣粫?huì)被變更的,所以不需要... 算了,那你移到data里吧。
妹子:嗯?。?好。 小聲說(shuō)道:我還是第一次看見(jiàn)這么寫(xiě)的。
我:...有種被嫌棄了的感覺(jué)
面試官微笑而又不失禮貌的問(wèn)道:
- 當(dāng)前組件模板中用到的變量一定要定義在
data里么?
懟回去:
-
data中的變量都會(huì)被代理到當(dāng)前this下,所以我們也可以在this下掛載屬性,只要不重名即可。而且定義在data中的變量在vue的內(nèi)部會(huì)將它包裝成響應(yīng)式的數(shù)據(jù),讓它擁有變更即可驅(qū)動(dòng)視圖變化的能力。但是如果這個(gè)數(shù)據(jù)不需要驅(qū)動(dòng)視圖,定義在created或mounted鉤子內(nèi)也是可以的,因?yàn)椴粫?huì)執(zhí)行響應(yīng)式的包裝方法,對(duì)性能也是一種提升。
順手點(diǎn)個(gè)贊或關(guān)注唄,找起來(lái)也方便~
分享一個(gè)筆者自己寫(xiě)的組件庫(kù),哪天可能會(huì)用的上了 ~ ↓