上一篇 Vue 原理解析(五): 虛擬Dom到真實Dom的生成過程
vue 之所以能數據驅動視圖發生變更的關鍵就是:依賴它的響應式系統了。 響應式系統如果根據數據類型區分: 對象和數組兩者的實現會有所不同。 解釋響應式原理,需要從整體流程出發, 不在vue 組件化的整體流程中找到響應式原理的位置,對深刻理解響應式原理不太好。 接下來我們從整體流程出發, 試着站在巨人的肩膀上分別說明對象和數組的實現原理。
對象的響應式原理
對象響應式數據的創建
- 在數組的初始化階段, 將對傳入的狀態進行初始化, 以下以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() 後執行vm._init() 初始化過程中, 當執行到initState(vm)時就會對內部使用到的一些狀態, 如: props, data, computed, watch, methods 分別進行初始化, 再對data 進行初始化的最後有這麼一句:
function initData(vm) { //初始化data
...
observe(data) // info:{name:'cc',sex:'man'}
}
這個observer 就是將用戶定義的data變成響應式的數據, 接下來看看它的創建過程:
export function observe(value) {
if(!isObject(value)) { // 不是數組或對象,再見
return
}
return new Observer(value)
}
簡單理解這個observer 方法就是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]) // 只傳入了兩個參數
}
}
}
當執行new Observer 時, 首先將傳入的對象掛載到當前this 下, 然後遍歷當前對象的每一項, 執行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) {
... 派發更新
}
})
}
這個方法的作用就是使用Object.defineProperty創建響應式數據。 首先根據傳入的obj 和 key 計算出 val 具體的值; 如果val 還是對象, 那就使用observe 方法進行遞歸創建, 在遞歸的過程中使用Object.defindeProperty 將對象的每一個屬性都變成響應式數據:
...
data() {
return {
info: {
name: 'cc',
sex: 'man'
}
}
}
這段代碼就會有三個響應式數據:
info, info.name, info.sex
知識點: Object.defineProperty內的get 方法, 它的作用就是誰訪問到當前key 的值就用 defineReactive 內的dep 將它收集起來, 也就是依賴收集的意思。 set 方法的作用就是當前key 的值被賦值了, 就通知dep內收集到的依賴項, key的值發生了變更, 視圖請變更吧。
這個時候get 和 set 只是定義了, 並不會觸發。 什麼是依賴,我們接下來說明,首先看看下圖幫大家理清響應式數據的創建過程(這裏先摘用網上的一張圖):
依賴收集
什麼是依賴? 看下之前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 這個類實例化的, 只是參數不同, 具體實現我們之後章節說明, 這裏大家只是知道這個是用戶watcher即可。
計算 (computed) watcher
- 顧名思義, 這個是當定義計算屬性實例化出來的一種:
new Vue({
data: {
msg: 'hello'
},
computed() {
sayHi() { // 計算watcher
return this.msg + 'vue!'
}
}
})
渲染屬性 (render) watcher
- 只是用做視圖渲染而定義的Watcher 實例, 再組件實行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() // 實例化就會執行this.get()方法
}
get() {
pushTarget(this) // 添加
...
this.getter.call(this.vm, this.vm) // 執行vm._update(vm._render())
...
popTarget() // 移除
}
addDep(dep) {
...
dep.addSub(this) // 將當前watcher收集到dep實例中
}
}
當執行new Watcher 的時候內部會掛載一些屬性, 然後執行this.get()這個方法, 首先會執行一個全局的方法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 類的靜態屬性Dep.target 爲 null, 這是一個全局會用到的屬性, 保存的是當前組件對應渲染watcher 的實例; targetStack 內存儲的是再執行組件化的過程中每個組件對應的渲染watcher實例集合, 使用的是一個先進後出的形式來管理數組的數據, 這裏可能有點不太好懂, 稍等再看到最後的流程圖後自然就明白了;然後傳入的watcher實例賦值給全局屬性Dep.target , 再之後的依賴收集過程中就是收集的它。
watcher 的 get 這個方法然後會執行getter 這個方法, 它是new Watcher 時傳入的第二個參數, 這個參數就是之前的updateComponent 變量:
function mountComponent(vm, el) {
...
const updateComponent = function() { //第二個參數
vm._update(vm._render())
}
...
}
只要一執行就會執行當前組件實例上的vm._update(vm.render()) 將 render 函數轉爲VNode, 這個時候如果render 函數內有使用到data 中已經轉爲了響應式的數據,就會觸發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() { // 觸發依賴收集
if(Dep.target) { // 之前賦值的當前watcher實例
dep.depend() // 收集起來,放入到上面的dep依賴管理器內
...
}
return val
},
set(newVal) {
... 派發更新
}
})
}
這個時候我們知道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) // 執行watcher的addDep方法
}
}
}
----------------------------------------------------------
class Watcher{
...
addDep(dep) { // 將當前watcher實例添加到dep內
...
dep.addSub(this) // 執行dep的addSub方法
}
}
這個Dep 類的作用就是管理屬性對應watcher, 如添加/刪除/通知。 至此, 依賴收集的過程就算是完成了, 還是以一張圖片加深對過程的理解:
派發更新
如果只是收集依賴, 那其實是沒有任何意義的, 將收集到的依賴在數據發生變化時通知並引起視圖變化, 這樣纔有意義。 現在我們對數據重新賦值:
app.vue
export default { // app組件
...
methods: {
changeInfo() {
this.info.name = 'ww';
}
}
}
這個時候就會觸發創建響應式數據時的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) { // 派發更新
if(newVal === val) { // 相同
return
}
val = newVal // 賦值
observer(newVal) // 如果新值是對象也遞歸包裝
dep.notify() // 通知更新
}
})
}
當賦值觸發set 時, 首先會檢測新值和舊值, 不能相同; 然後將新值賦值給舊值; 如果新值是對象則將它變成響應式的; 最後讓對應屬性的依賴管理器使用dep.notify發出更新視圖的通知。來看下它的實現:
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() // 挨個觸發watcher的update方法
}
}
}
這裏做的事情只有一件, 將收集起來的watcher 挨個遍歷觸發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更新
}
執行update 方法時將當前watcher 實例傳入到定義的queueWatcher 方法內, 這個方法的作用是把將要執行更新的watcher收集到一個隊列queue之內,保證如果同一個watcher 內觸發了多次更新, 只會更新一次對應的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 推入到的隊列之後, 之後賦值觸發的set隊列內並不會添加任何渲染watcher; 當同時賦值多個屬性時也是, 因爲他們收集的都是同一個渲染watcher , 所以推入到隊列一次之後就不會添加了。
知識點: vue 還是很聰明的, 通過上面實例大家應該看出來, 派發更新通知的粒度是組件級別, 至於組件內是哪個屬性賦值了,派發更新並不關心, 而且怎麼高效更新這個視圖, 那是之後diff對比做的事情。
隊列有了, 執行nextTick(flushSchedulerQueue) 再下一次tick時更新它, 這裏的nextTick 就是我們經常使用的 this.$nextTick 方法的原始方法, 它們作用一致,實現原理之後章節說明。 看下參數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() // 觸發 beforeUpdate 鉤子
}
id = watcher.id
has[id] = null
watcher.run() // 真正的更新方法
...
}
}
原來是個函數, 再nextTick方法的內部會執行第一個參數。 首先會將queue這個隊列進行一次排序,依次是每次new Watcher 生成的 id, 以從小到大的順序。 當前示例只是做渲染, 而且隊列內只存在了一個渲染watcher, 所以是不存在順序的。 但是如果有定義user watcher 和 computed watcher 加上 render watcher 後 , 它們之間就會存在一個執行順序的問題了。
知識點: watcher 的執行順序是先父後子, 然後是從computed watcher 到user watcher 最後 render watcher , 這從它們的初始化順序就能看出來。
然後就是遍歷這個隊列, 因爲是渲染watcher, 所有是有before 屬性的, 執行傳入的before方法觸發beforeUpdate 鉤子。 最後執行watcher.run()方法, 執行真正的派發更新方法。 我們看下run幹了啥:
class Watcher {
...
run () {
if (this.active) {
this.getAndInvoke(this.cb) // 有一種要抓狂的感覺
}
}
getAndInvoke(cb) { // 渲染watcher的cb爲noop空函數
const value = this.get()
... 後面是用戶watcher邏輯
}
}
執行run 就是執行getAndInvoke方法, 因爲是渲染watcher, 參數cb是noop空函數。 看了這麼多, 其實… 就是重新執行一次 this.get()方法, 讓 vm._update(vm._render())在走一遍而已。 然後生成新舊VNode , 最後進行diff比對以更新視圖。
最後說下vue 基於Object.defineProperty響應式系統的一些不足。 比如:只能監聽到數據的變化, 所以有時data中要是定義一堆的初始值, 因爲加入了響應式系統後才能被感知到; 還有就是常規JavaScript操作對象的方式, 並不能監聽到增加以及刪除。如:
export default {
data() {
return {
info: {
name: 'cc'
}
}
},
methods: {
addInfo() { // 增加屬性
this.info.sex = 'man'
},
delInfo() { // 刪除屬性
delete info.name
}
}
}
數據是被賦值了, 但是視圖並不會發生變更。 vue爲了解決這個問題,提供了兩個API: $set 和 $delete, 它們又是怎麼辦到的? 原理我們之後章節分享。
最後我們以一個問題結束本章內容:
- 當前組件模板中用到的變量一定要定義在data裏麼?
解答:
- data 中的變量都會被代理到this下, 所以我們也可以在this下掛載屬性, 只要不重名即可。 而且定義在data中的變量在vue的內部會將它包裝成響應式的數據, 讓它擁有變更即可驅動視圖變化的能力。 但是如果這個數據不需要驅動視圖, 定義在created 或者 mounted 鉤子內也是可以的, 因爲不會執行響應式的包裝方法,對性能也是一種提升。
下一篇: Vue原理解析(七): 理解響應式原理(下)-數組