vue源碼之響應式數據
分析vue是如何實現數據響應的.
前記
現在回顧一下看數據響應的原因. 之前看了vuex和vue-i18n的源碼, 他們都有自己內部的vm, 也就是vue實例. 使用的都是vue的響應式數據特性及$watch
api. 所以決定看一下vue的源碼, 了解vue是如何實現響應式數據.
本文敘事方式為樹藤摸瓜, 順著看源碼的邏輯走一遍, 查看的vue的版本為2.5.2.
目的
明確調查方向才能直至目標, 先說一下目標行為:
- vue中的數據改變, 視圖層面就能獲得到通知並進行渲染.
-
$watch
api監聽表達式的值, 在表達式中任何一個元素變化以後獲得通知並執行回調.
那麽準備開始以這個方向為目標從vue源碼的入口開始找答案.
入口開始
來到src/core/index.js
, 調了initGlobalAPI()
, 其他代碼是ssr相關, 暫不關心.
進入initGlobalAPI
方法, 做了一些暴露全局屬性和方法的事情, 最後有4個init, initUse是Vue的install方法, 前面vuex和vue-i18n的源碼分析已經分析過了. initMixin是我們要深入的部分.
在initMixin
前面部分依舊做了一些變量的處理, 具體的init動作為:
vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) // resolve injections before data/props initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created')
vue啟動的順序已經看到了: 加載生命周期/時間/渲染的方法 => beforeCreate鉤子 => 調用injection => 初始化state => 調用provide => created鉤子.
injection和provide都是比較新的api, 我還沒用過. 我們要研究的東西在initState中.
來到initState:
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) // 如果沒有data, _data效果一樣, 只是沒做代理 } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } }
做的事情很簡單: 如果有props就處理props, 有methods就處理methods, …, 我們直接看initData(vm)
.
initData
initData做了兩件事: proxy, observe.
先貼代碼, 前面做了小的事情寫在註釋裏了.
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function' // 如果data是函數, 用vm作為this執行函數的結果作為data
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) { // 過濾亂搞, data只接受對象, 如果亂搞會報警並且把data認為是空對象
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) { // 遍歷data
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) { // 判斷是否和methods重名
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) { // 判斷是否和props重名
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) { // 判斷key是否以_或$開頭
proxy(vm, `_data`, key) // 代理data
}
}
// observe data
observe(data, true /* asRootData */)
}
我們來看一下proxy和observe是幹嘛的.
proxy的參數: vue實例, _data
, 鍵.
作用: 把vm.key的setter和getter都代理到vm._data.key, 效果就是vm.a實際實際是vm._data.a, 設置vm.a也是設置vm._data.a.
代碼是:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
// 在initData中調用: proxy(vm, `_data`, key)
// target: vm, sourceKey: _data, key: key. 這裏的key為遍歷data的key
// 舉例: data為{a: 'a value', b: 'b value'}
// 那麽這裏執行的target: vm, sourceKey: _data, key: a
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key] // getter: vm._data.a
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val // setter: vm._data.a = val
}
Object.defineProperty(target, key, sharedPropertyDefinition) // 用Object.defineProperty來設置getter, setter
// 第一個參數是vm, 也就是獲取`vm.a`就獲取到了`vm._data.a`, 設置也是如此.
}
代理完成之後是本文的核心, initData最後調用了observe(data, true)
,來實現數據的響應.
observe
observe方法其實是一個濾空和單例的入口, 最後行為是創建一個observe對象放到observe目標的__ob__
屬性裏, 代碼如下:
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) { // 只能是監察對象, 過濾非法參數
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__ // 如果已被監察過, 返回存在的監察對象
} else if ( // 符合下面條件就新建一個監察對象, 如果不符合就返回undefined
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
那麽關鍵是new Observer(value)
了, 趕緊跳到Observe這個類看看是如何構造的.
以下是Observer的構造函數:
constructor (value: any) {
this.value = value // 保存值
this.dep = new Dep() // dep對象
this.vmCount = 0
def(value, '__ob__', this) // 自己的副本, 放到__ob__屬性下, 作為單例依據的緩存
if (Array.isArray(value)) { // 判斷是否為數組, 如果是數組的話劫持一些數組的方法, 在調用這些方法的時候進行通知.
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value) // 遍歷數組, 繼續監察數組的每個元素
} else {
this.walk(value) // 直到不再是數組(是對象了), 遍歷對象, 劫持每個對象來發出通知
}
}
做了幾件事:
- 建立內部Dep對象. (作用是之後在watcher中遞歸的時候把自己添加到依賴中)
- 把目標的
__ob__
屬性賦值成Observe對象, 作用是上面提過的單例. - 如果目標是數組, 進行方法的劫持. (下面來看)
- 如果是數組就observeArray, 否則walk.
那麽我們來看看observeArray和walk方法.
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]]) // 用'obj[keys[i]]'這種方式是為了在函數中直接給這個賦值就行了
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
我們發現, observeArray的作用是遞歸調用, 最後調用的方法是defineReactive
, 可以說這個方法是最終的核心了.
下面我們先看一下數組方法劫持的目的和方法, 之後再看defineReactive
的做法.
array劫持
之後會知道defineReactive的實現劫持的方法是Object.defineProperty
來劫持對象的getter, setter, 那麽數組的變化不會觸發這些劫持器, 所以vue劫持了數組的一些方法, 代碼比較零散就不貼了.
最後的結果就是: array.prototype.push = function () {…}, 被劫持的方法有[‘push‘, ‘pop‘, ‘shift‘, ‘unshift‘, ‘splice‘, ‘sort‘, ‘reverse‘]
, 也就是調用這些方法也會觸發響應. 具體劫持以後的方法是:
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args) // 調用原生的數組方法
const ob = this.__ob__ // 獲取observe對象
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted) // 繼續遞歸
// notify change
ob.dep.notify() // 出發notify
return result
})
做了兩件事:
- 遞歸調用
- 觸發所屬Dep的
notify()
方法.
接下來就說最終的核心方法, defineReactive, 這個方法最後也調用了notify().
defineReactive
這裏先貼整個代碼:
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
// 這個方法是劫持對象key的動作
// 這裏還是舉例: 對象為 {a: 'value a', b: 'value b'}, 當前遍歷到a
obj: Object, // {a: 'value a', b: 'value b'}
key: string, // a
val: any, // value a
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) { // 判斷當前key的操作權限
return
}
// cater for pre-defined getter/setters
// 獲取對象本來的getter setter
const getter = property && property.get
const setter = property && property.set
let childOb = !shallow && observe(val) // childOb是val的監察對象(就是new Observe(val), 也就是遞歸調用)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val // 如果本身有getter, 先調用
if (Dep.target) { // 如果有dep.target, 進行一些處理, 最後返回value, if裏的代碼我們之後去dep的代碼中研究
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 // 如果本身有getter, 先調用
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) { // 如果值不變就不去做通知了, (或是某個值為Nan?)
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter() // 根據"生產環境不執行"這個行為來看, 這個方法可能作用是log, 可能是保留方法, 還沒地方用?
}
if (setter) { // 如果本身有setter, 先調用, 沒的話就直接賦值
setter.call(obj, newVal)
} else {
val = newVal // 因為傳入參數的時候其實是'obj[keys[i]]', 所以就等於是'obj[key] = newVal'了
}
childOb = !shallow && observe(newVal) // 重新建立子監察
dep.notify() // 通知, 可以說是劫持的核心步驟
}
})
}
解釋都在註釋中了, 總結一下這個方法的做的幾件重要的事:
- 建立Dep對象. (下面會說調用的Dep的方法的具體作用)
- 遞歸調用. 可以說很大部分代碼都在遞歸調用, 分別在創建子observe對象, setter, getter中.
- getter中: 調用原來的getter, 收集依賴(Dep.depend(), 之後會解釋收集的原理), 同樣也是遞歸收集.
- setter中: 調用原來的setter, 並判斷是否需要通知, 最後調用
dep.notify()
.
總結一下, 總的來說就是, 進入傳入的data數據會被劫持, 在get的時候調用Dep.depend()
, 在set的時候調用Dep.notify()
. 那麽Dep是什麽, 這兩個方法又幹了什麽, 帶著疑問去看Dep對象.
Dep
Dep應該是dependencies的意思. dep.js整個文件只有62行, 所以貼一下:
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
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 () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
// 這是一個隊列, 因為不允許有多個watcher的get方法同時調用
Dep.target = null
const targetStack = []
export function pushTarget (_target: Watcher) {
// 設置target, 把舊的放進stack
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
// 從stack拿一個作為當前的
Dep.target = targetStack.pop()
}
首先來分析變量:
- 全局Target. 這個其實是用來跟watcher交互的, 也保證了普通get的時候沒有target就不設置依賴, 後面會解釋.
- id. 這是用來在watcher裏依賴去重的, 也要到後面解釋.
- subs: 是一個watcher數組. sub應該是subscribe的意思, 也就是當前dep(依賴)的訂閱者列表.
再來看方法:
- 構造: 設uid, subs. addSub: 添加wathcer, removeSub: 移除watcher. 這3個好無聊.
- depend: 如果有Dep.target, 就把自己添加到Dep.target中(調用了
Dep.target.addDep(this)
).那麽什麽時候有Dep.target呢, 就由
pushTarget()
和popTarget()
來操作了, 這些方法在Dep中沒有調用, 後面會分析是誰在操作Dep.target.(這個是重點) - notify: 這個是setter劫持以後調用的最終方法, 做了什麽: 把當前Dep訂閱中的每個watcher都調用
update()
方法.
Dep看完了, 我們的疑問都轉向了Watcher對象了. 現在看來有點糊塗, 看完Watcher就都明白了.
Watcher
watcher非常大(而且打watcher這個單詞也非常容易手誤, 心煩), 我們先從構造看起:
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm // 保存vm
vm._watchers.push(this) // 把watcher存到vm裏
// options
// 讀取配置 或 設置默認值
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production' // 非生產環境就記錄expOrFn
? expOrFn.toString()
: ''
// parse expression for getter
// 設置getter, parse字符串, 並濾空濾錯
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
// 調用get獲得值
this.value = this.lazy
? undefined
: this.get()
}
註釋都寫了, 我來高度總結一下構造器做了什麽事:
- 處理傳入的參數並設置成自己的屬性.
- parse表達式. watcher表達式接受2種: 方法/字符串. 如果是方法就設為getter, 如果是字符串會進行處理:
/**
* Parse simple path.
*/
const bailRE = /[^\w.$]/
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
// 這裏是vue如何分析watch的, 就是接受 '.' 分隔的變量.
// 如果鍵是'a.b.c', 也就等於function () {return this.a.b.c}
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
處理的效果寫在上面代碼的註釋裏.
- 調用
get()
方法.
下面說一下get方法. get()方法是核心, 看完了就能把之前的碎片都串起來了. 貼get()的代碼:
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
// 進入隊列, 把當前watcher設置為Dep.target
// 這樣下面調用getter的時候出發的dep.append() (最後調用Dep.target.addDep()) 就會調用這個watcher的addDep.
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
// 調用getter的時候會走一遍表達式,
// 如果是 this.a + this.b , 會在a和b的getter中調用Dep.target.addDep(), 最後結果就調用了當前watcher的addDep,
// 當前watcher就有了this.a的dep和this.b的dep
// addDep把當前watcher加入了dep的sub(subscribe)裏, dep的notify()調用就會運行本watcher的run()方法.
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
// 走到這裏已經通過了getter獲得到了value, 或者失敗為undefined, 這個值返回作為watcher的valule
// 處理deep選項 (待看)
if (this.deep) {
traverse(value)
}
popTarget() // 移除隊列
this.cleanupDeps() // 清理依賴(addDep加到newDep數組, 這步做整理動作)
}
return value
}
註釋都在代碼中了, 這段理解了就對整個響應系統理解了.
我來總結一下: (核心, 非常重要)
- dep方面: 傳入vue參數的data(實際是所有調用
defineReactive
的屬性)都會產生自己的Dep對象. - Watcher方面: 在所有new Watcher的地方產生Watcher對象.
-
dep與Watcher關系: Watcher的get方法建立了雙方關系:
把自己設為target, 運行watcher的表達式(即調用相關數據的getter), 因為getter有鉤子, 調用了Watcher的addDep, addDep方法把dep和Watcher互相推入互相的屬性數組(分別是deps和subs)
- dep與Watcher建立了多對多的關系: dep含有訂閱的watcher的數組, watcher含有所依賴的變量的數組
- 當dep的數據調動setter, 調用notify, 最終調用Watcher的update方法.
- 前面提到dep與Watcher建立關系是通過
get()
方法, 這個方法在3個地方出現: 構造方法, run方法, evaluate方法. 也就是說, notify了以後會重新調用一次get()方法. (所以在lifycycle中調用的時候把依賴和觸發方法都寫到getter方法中了).
那麽接下來要看一看watcher在什麽地方調用的.
找了一下, 一共三處:
-
initComputed的時候: (state.js)
``` watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions ) ``` -
$watch api: (state.js)
``` new Watcher(vm, expOrFn, cb, options) ``` -
lifecycle的mount階段: (lifecycle.js)
``` new Watcher(vm, updateComponent, noop) ```
總結
看完源碼就不神秘了, 寫得也算很清楚了. 當然還有很多細節沒寫, 因為沖著目標來.
總結其實都在上一節的粗體裏了.
甜點
我們只從data看了, 那麽props和computed應該也是這樣的, 因為props應該與組建相關, 下回分解吧, 我們來看看computed是咋回事吧.
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
// 循環每個computed
// ------------
// 格式濾錯濾空
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
// 為computed建立wathcer
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
// 因為沒有被代理, computed屬性是不能通過vm.xx獲得的, 如果可以獲得說明重復定義, 拋出異常.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
已註釋, 總結為:
- 遍歷每個computed鍵值, 過濾錯誤語法.
- 遍歷每個computed鍵值, 為他們建立watcher, options為
{ lazy: true}
. - 遍歷每個computed鍵值, 調用defineComputed.
那麽繼續看defineComputed.
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
// 因為computed除了function還有get set 字段的語法, 下面的代碼是做api的兼容
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
// 除非設置setter, computed屬性是不能被修改的, 拋出異常 (evan說改變了自由哲學, 要控制低級用戶)
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
`Computed property "${key}" was assigned to but it has no setter.`,
this
)
}
}
// 其實核心就下面這步... 上面步驟的作用是和data一樣添加一個getter, 增加append動作. 現在通過vm.xxx可以獲取到computed屬性啦!
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
因為computed可以設置getter, setter, 所以computed的值不一定是function, 可以為set和get的function, 很大部分代碼是做這些處理, 核心的事情有2件:
- 使用Object.defineProperty在vm上掛載computed屬性.
- 為屬性設置getter, getter做了和data一樣的事: depend. 但是多了一步:
watcher.evalueate()
.
看到這裏, computed註冊核心一共做了兩件事:
- 為每個computed建立watcher(lazy: true)
- 建立一個getter來depend, 並掛到vm上.
那麽dirty成了疑問, 我們回到watcher的代碼中去看, lazy和dirty和evaluate是幹什麽的.
精選相關代碼:
- (構造函數中)
this.dirty = this.lazy
- (構造函數中)
this.value = this.lazy ? undefined : this.get()
-
(evaluate函數)
``` evaluate () { this.value = this.get() this.dirty = false } ```
到這裏已經很清楚了. 因為還沒設置getter, 所以在建立watcher的時候不立即調用getter, 所以構造函數沒有馬上調用get, 在設置好getter以後調用evaluate來進行依賴註冊.
總結: computed是watch+把屬性掛到vm上的行為組合.
原文地址:https://segmentfault.com/a/1190000012559684
vue源碼之響應式數據