原文連接:www.yuque.com/wuhaosky/vu…javascript
數據驅動視圖是MVVM框架的顯著特色,MVVM框架的出現將前端開發者從繁雜的、「鳥巢」般的dom操做中解放出來,開發體驗比jQuery/underscore模板提高了不知道幾個層次。前端
想要實現數據驅動視圖,須要解決兩個問題,一是框架要知道數據何時變動、二是框架如何把變動後的數據更新到視圖。對於解決第一個問題,React是經過開發者手動執行this.setState方法實現的,Vue框架是經過自身數據響應式系統實現的;對於解決第二個問題,React和Vue都是經過patch函數和virtual dom diff算法實現的。vue
這篇文章裏,咱們只關注Vue的響應式系統是如何實現的。不管是Vue2仍是Vue3,二者的響應式系統都是基於觀察者模式(發佈訂閱模式)實現的,因此都涉及這麼幾個概念:目標對象(target)、依賴收集器(Dep)、觀察者(Watcher)。依賴收集器收集目標對象的觀察者,當目標對象的狀態發生改變,全部的觀察者都將獲得通知。示意圖以下:java
咱們先總體看下Vue2響應式系統是怎麼運做的,有個大致的概念,而後再拆分每一部分,看下每部分的實現。react
目標對象通過observe函數,新增__ob__屬性,這個屬性是一個Observer實例,這個Observer實例含有dep屬性,dep屬性指向依賴收集者。而後,對目標對象的每個屬性執行defineReactive函數,將屬性轉換成訪問器屬性,這樣咱們就能夠對屬性的讀寫操做進行攔截。這個過程稱之爲「數據劫持」。算法
當執行觀察者get方法時,會觸發目標對象屬性的getter方法,在getter方法裏收集觀察者,這個過程就是「收集觀察者」。express
當目標對象屬性變動時,會觸發目標對象的setter方法,在setter方法裏執行觀察者的update方法,這個過程就是「通知觀察者」。數組
observe函數和defineReactive函數的做用是把目標對象屬性轉換成訪問器屬性。微信
咱們看下,這兩個函數是怎麼實現的。首先看下observe函數,observe函數建立一個Observer實例,在Observer構造函數裏作了三件事:閉包
1.首先new了一個依賴收集器,這個dep的做用是,當目標對象增刪屬性時,通知對目標對象「感興趣」的觀察者;
2.給目標對象添加不可枚舉的__ob__屬性,指向Observer實例;
3.最後遍歷對象屬性,並執行defineReactive函數。
export function observe (value: any): Observer | void {
let ob: Observer | void
ob = new Observer(value)
return ob
}
export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
// ...
} else {
this.walk(value)
}
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}複製代碼
js對象裏目前存在的屬性描述符有兩種主要形式:數據描述符和訪問器描述符。defineReactive函數的做用,就是把目標對象屬性設置爲訪問器屬性,這樣能夠在getter/setter方法中攔截屬性的讀寫操做。若是屬性是對象或數組,則遞歸執行observe函數,使目標對象深度可偵測。defineReactive函數裏作了三件事:
1.建立了一個dep實例,這個dep 在訪問器屬性的 getter/setter 中被閉包引用,這個dep的做用是當目標對象屬性發生寫操做時,通知「感興趣」的觀察者;
2.若是屬性是對象或者數組,則調用observe函數並把這個屬性當作實參,目的是使目標對象深度可偵測;
3.使用Object.defineProperty函數把目標對象屬性轉成訪問器屬性,在getter方法裏,經過執行dep.depend方法,收集對當前屬性「感興趣」的觀察者;在setter方法裏,執行observe(newVal),把新增長的屬性值變成可偵測的,並執行dep.notify(),通知對此屬性「感興趣」的全部觀察者。
export function defineReactive ( obj: Object, key: string, val: any ) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
// cater for pre-defined getter/setters
const getter = property && property.get
let val;
if (!getter) {
val = obj[key]
}
const setter = property && property.set
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
observe(newVal)
dep.notify()
}
})
}
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}複製代碼
顧名思義,Dep依賴收集器的做用就是收集觀察者的。
咱們來看下Dep的實現,
1.Dep有個靜態屬性target,當觀察者初始化時,會在觀察者的構造方法裏,執行觀察者的get方法,在觀察者的get方法裏,觀察者會把本身賦值給Dep.target,意味着當前的觀察者是本身;
2.dep.addSub方法把當前的觀察者收集,存儲到subs屬性中;
3.dep.notify方法會調用全部觀察者的update方法。
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
const targetStack = []
export function pushTarget (_target: ?Watcher) {
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}複製代碼
觀察者的做用是監聽目標對象的變化。觀察者構造方法中的參數expOrFn,能夠是表達式,若是是表達式的話,只接受鍵路徑,例如"a.b.c";對於更復雜的表達式,可使用一個函數替代。
咱們來看下Watcher的實現,
1.Watcher的構造方法裏執行get方法裏,get方法裏執行expOrFn,expOrFn中對目標對象進行求值,觸發Dep收集觀察者;
2.當目標對象更新時,會調用觀察者的update方法,若是是同步更新則接着調用run方法,若是是異步更新則執行queueWatcher方法,但不管是同步更新仍是異步更新,最終都會執行run方法;
3.在run方法裏,執行get方法,從新求expOrFn的值,若是有cb參數,則調用cb函數,把新值和舊值當作實參傳入。
let uid = 0
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
if (options) {
this.deep = !!options.deep
} else {
this.deep = false
}
this.cb = cb
this.id = ++uid
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
throw e
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
}
return value
}
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}複製代碼
Object.defineProperty並不能攔截對象增刪屬性,Vue是經過Vue.set和Vue.delete實現對象增刪屬性攔截的。set方法裏,首先將新加的屬性設置爲訪問器屬性,使其變爲響應式,而後調用target.__ob__.dep.notify方法,通知觀察者。del方法裏,首先將屬性從對象裏刪除,而後調用target.__ob__.dep.notify方法,通知觀察者。
export function set (target: Object, key: any, val: any): any {
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
const ob = (target: any).__ob__
if (!ob) {
target[key] = val
return val
}
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
export function del (target: Object, key: any) {
const ob = (target: any).__ob__
if (!hasOwn(target, key)) {
return
}
delete target[key]
if (!ob) {
return
}
ob.dep.notify()
}複製代碼
在defineReactive函數裏,若是目標對象屬性爲數組,則對數組調用observe方法進行偵測。
let childOb = observe(val)複製代碼
對數組的偵測,首先重寫數組的原型爲arrayMethods;而後遍歷數組,對每個元素調用observe函數。何爲arrayMethods?首先設置arrayMethods的原型爲Array.prototype;而後往arrayMethods上定義7個屬性,這7個屬性實際上是重寫的7個數組變異方法。有的數組變異方法是能夠新增元素的,要把新增長的元素變成響應式的;在全部的變異方法裏都會調用數組的__ob__.dep.notify方法通知觀察者。示意圖以下:
export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
value.__proto__ = arrayMethods
this.observeArray(value)
} else {
// ...
}
}
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
})
})複製代碼
而後把目標對象屬性設置爲訪問器屬性,在訪問器屬性的get方法裏,則執行childOb.dep.depend(),收集對此數組「感興趣」的觀察者;並調用dependArray,每一個數組元素一樣把對此數組「感興趣」的觀察者收集爲依賴,這樣保證每一個數組元素變動時,會通知到對此數組「感興趣」的觀察者。
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
function dependArray (value: Array<any>) {
for (let e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
if (Array.isArray(e)) {
dependArray(e)
}
}
}複製代碼
對象屬性的寫操做;
非根級響應式對象的增刪屬性操做;
數組7個變異方法的攔截。
Vue 不容許動態添加根級響應式屬性,因此你必須在初始化實例前聲明全部根級響應式屬性;
使用array[index] = item方式給數組元素賦值;
使用array.length = newLength方式改變數組長度。
咱們先總體看下Vue3響應式系統是怎麼運做的,有個大致的概念,而後再拆分每一部分,看下每部分的實現。
目標對象通過reactive函數,生成Proxy代理對象,能夠對5種操做進行攔截。這個過程就是「數據劫持」。示意圖:
Vue3的觀察者不叫Watcher,而是叫effect,它是基於ReactiveEffect接口實現的。effect初始化時,執行它的入參fn,fn裏執行proxy對象的值,觸發get/has/ownKeys trap。在get/has/ownKeys trap 裏執行track方法,將目標對象屬性和觀察者存儲到依賴收集表。這個過程就是「收集觀察者」。示意圖:
當proxy對象的值發生改變,觸發deleteProperty/set trap。在deleteProperty/set trap 裏執行trigger方法,從依賴收集表中找出目標對象屬性對應的觀察者set集合,遍歷全部的觀察者,執行run方法,最終會執行effect的入參fn函數。這個過程就是「通知觀察者」。示意圖:
Vue3響應式系統總體工做過程(鑑於掘金不支持視頻,而gif最大支持5M,因此我把視頻傳到了B站):
reactive函數的做用就是生成目標對象的proxy代理對象。mutableHandlers包含proxy 攔截方法。rawToReactive、reactiveToRaw存儲目標對象和proxy對象的映射關係。
export function reactive(target: object) {
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}
function createReactiveObject( target: unknown, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) {
const handlers = baseHandlers
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}複製代碼
Vue3使用Proxy攔截了5個方法,在get/has/ownKeys trap 裏經過track方法收集依賴,在deleteProperty/set trap 裏經過trigger方法觸發通知。
createGetter函數中,只有在用到某個對象時,才執行reactive函數對其進行數據劫持,生成proxy對象。
export const mutableHandlers: ProxyHandler<object> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
function createGetter(isReadonly: boolean, shallow = false) {
return function get(target: object, key: string | symbol, receiver: object) {
let res = Reflect.get(target, key, receiver)
track(target, TrackOpTypes.GET, key)
return isObject(res)
? reactive(res)
: res
}
}
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
track(target, TrackOpTypes.HAS, key)
return result
}
function ownKeys(target: object): (string | number | symbol)[] {
track(target, TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.ownKeys(target)
}
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key]
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key)
}
}
return result
}
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key)
}
return result
}複製代碼
Vue3的觀察者不叫Watcher,而是叫effect,它是基於ReactiveEffect接口實現的。effect初始化時,執行它的入參fn,fn裏執行proxy對象的值,觸發get/has/ownKeys trap。
export function effect<T = any>( fn: () => T, // 須要監聽的函數 options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> {
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect() // 非懶計算,則當即執行effect函數,effect函數內部執行run方法
}
return effect
}
function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
return run(effect, fn, args)
} as ReactiveEffect
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}
function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
if (!effectStack.includes(effect)) {
cleanup(effect) // 把當前觀察者,從依賴收集表中刪除,並把當前觀察者的deps字段設置爲空數組
try {
effectStack.push(effect) // 進棧
return fn(...args)
} finally {
effectStack.pop() // 出棧
}
}
}複製代碼
track方法的做用是收集觀察者到依賴收集表;trigger方法的做用是從依賴收集表中找到effect,並執行effect,最終會執行effect的實參,也就是fn函數。
export function track(target: object, type: TrackOpTypes, key: unknown) {
const effect = effectStack[effectStack.length - 1]
let depsMap = targetMap.get(target)
let dep = depsMap.get(key)
if (!dep.has(effect)) {
dep.add(effect) // 將觀察者添加到依賴收集表的合適位置
effect.deps.push(dep) // 將依賴收集表的Dep添加到觀察者的deps數組中
}
}
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, extraInfo?: DebuggerEventExtraInfo ) {
const depsMap = targetMap.get(target)
const effects = depsMap.get(key)
const run = (effect: ReactiveEffect) => {
effect()
}
effects.forEach(run)
}複製代碼
Vue3的響應式系統和Vue2同樣,也是觀察者模式(發佈訂閱者模式)。因此,Vue3的響應式系統一樣包含三個階段,1.數據劫持(變更偵測);2.收集依賴(觀察者);3.通知依賴(觀察者)。
Vue3的數據劫持是經過Proxy實現的,而Vue2是經過Object.defineProperty實現的;長遠來看JS引擎會繼續優化Proxy,但Object.defineProperty不會再有針對性的優化,因此Proxy性能上總體優於Object.defineProperty;
總結:Vue3比Vue2有更快的性能。
Vue3支持Object、Array、Map、WeakMap、Set、WeakSet六種數據類型的數據劫持,而Vue2只支持Object、Array兩種數據類型;而且Vue3能夠劫持對象的屬性增刪和數組的索引操做。
總結:Vue3支持更多數據類型的數據劫持。
Vue3在目標對象進行get/has/iterate三種操做時,進行依賴收集;而Vue2只在目標對象的屬性進行get操做時,進行依賴收集;
Vue3在目標對象進行set/add/delete/clear四種操做時,觸發通知依賴;而Vue2只在對目標對象的屬性進行set操做時,觸發通知依賴。
總結:Vue3支持更多的時機來進行依賴收集和觸發通知。
Vue2會把整個data進行遞歸數據劫持,而Vue3只有在用到某個對象時,纔對其進行數據劫持,因此Vue3響應式系統更快而且佔用內存更小。想象下,一個很龐大的對象,咱們並非須要對其全部屬性進行變更偵測,Vue2的方式就會致使無用的內存消耗和性能消耗。
總結:數據劫持方面,Vue3作到了「精準數據」的數據劫持,Vue3比Vue2佔用更小的內存。
Vue3經過一個WeakMap做爲全局的依賴收集器,Vue3依賴收集器的結構是:
Vue2則是經過被閉包引用的dep和經過observer實例引用的dep來做爲依賴收集器;
總結:Vue3的依賴收集器更容易維護,能夠方便的找到或者移除目標對象的依賴。
Vue3響應式系統顯著優勢是:有更快的性能、佔用更小的內存、支持Vue根數據增刪屬性的攔截、支持數組的攔截。
須要技術交流能夠加微信。