上一篇:Vue原理解析(五):完全搞懂虛擬Dom到真實Dom的生成過程html
vue
之因此能數據驅動視圖發生變動的關鍵,就是依賴它的響應式系統了。響應式系統若是根據數據類型區分,對象和數組它們的實現會有所不一樣;解釋響應式原理,若是隻是爲了說明響應式原理而說,但不是從總體流程出發,不在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'}
}
複製代碼
這個observe
就是將用戶定義的data
變成響應式的數據,接下來看下它的建立過程:數組
export function observe(value) {
if(!isObject(value)) { // 不是數組或對象,再見
return
}
return new Observer(value)
}
複製代碼
簡單理解這個observe
方法就是Observer
這個類的工廠方法,因此仍是要看下Observer
這個類的定義:bash
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
這個方法,看下它的定義:app
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.defineProperty
將對象的每個屬性都變成響應式數據:函數
...
data() {
return {
info: {
name: 'cc',
sex: 'man'
}
}
}
這段代碼就會有三個響應式數據:
info, info.name, info.sex
複製代碼
知識點:
Object.defineProperty
內的get
方法,它的做用就是誰訪問到當前key
的值就用defineReactive
內的dep
將它收集起來,也就是依賴收集的意思。set
方法的做用就是當前key
的值被賦值了,就通知dep
內收集到的依賴項,key
的值發生了變動,視圖請變動吧~工具
這個時候get
和set
只是定義了,並不會觸發。什麼是依賴咱們接下來講明,首先仍是用一張圖幫你們理清響應式數據的建立過程:oop
依賴收集
什麼是依賴了?咱們看下以前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
,它們又是怎麼辦到的了?原理以後章節分析。
最後慣例的面試問答就扯扯最近工做中遇到趣事吧。對於一個數據不會變動的列表,筆者把它定義再了created
鉤子內,不多結對編程,此次例外。
created() {
this.list = [...]
}
複製代碼
旁邊的妹子接事後:
妹子: 這個列表怎麼data裏沒有阿?在哪定義的?
我:我定義在created鉤子裏了。
妹子:你怎麼定義在這了?
我:由於它是不會被變動的,因此不須要... 算了,那你移到data裏吧。
妹子:嗯!? 好。 小聲說道:我仍是第一次看見這麼寫的。
我:...有種被嫌棄了的感受
複製代碼
面試官微笑而又不失禮貌的問道:
data
裏麼?懟回去:
data
中的變量都會被代理到當前this
下,因此咱們也能夠在this
下掛載屬性,只要不重名便可。並且定義在data
中的變量在vue
的內部會將它包裝成響應式的數據,讓它擁有變動便可驅動視圖變化的能力。可是若是這個數據不須要驅動視圖,定義在created
或mounted
鉤子內也是能夠的,由於不會執行響應式的包裝方法,對性能也是一種提高。順手點個贊或關注唄,找起來也方便~