詳解Vue數(shù)據(jù)驅(qū)動原理
前言
Vue區(qū)別于傳統(tǒng)的JS庫,例如JQuery,其中一個最大的特點就是不用手動去操作DOM,只需要對數(shù)據(jù)進行變更之后,視圖也會隨之更新。 比如你想修改div#app里的內(nèi)容:
/// JQuery
<div id="app"></div>
<script>
$('#app').text('lxb')
</script>
<template>
<div id="app">{{ message }}</div>
<button @click="change">點擊修改message</button>
</template>
<script>
export default {
data () {
return {
message: 'lxb'
}
},
methods: {
change () {
this.message = 'lxb1' // 觸發(fā)視圖更新
}
}
}
</script>
在代碼層面上的最大區(qū)別就是,JQuery直接對DOM進行了操作,而Vue則對數(shù)據(jù)進行了操作,接下來我們通過分析源碼來進一步分析,Vue是如何做到數(shù)據(jù)驅(qū)動的,而數(shù)據(jù)驅(qū)動主要分成兩個部分依賴收集和派發(fā)更新。
數(shù)據(jù)驅(qū)動
// _init方法中 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初始化會執(zhí)行_init方法,并調(diào)用initState方法. initState相關(guān)代碼在src/core/instance/state.js下
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) // 初始化Props
if (opts.methods) initMethods(vm, opts.methods) // 初始化方法
if (opts.data) {
initData(vm) // 初始化data
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed) // 初始化computed
if (opts.watch && opts.watch !== nativeWatch) { // 初始化watch
initWatch(vm, opts.watch)
}
}
我們具體看看initData是如何定義的。
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function' // 把data掛載到了vm._data上
? getData(data, vm) // 執(zhí)行 data.call(vm)
: data || {}
if (!isPlainObject(data)) {
data = {} // 這也是為什么 data函數(shù)需要返回一個object不然就會報這個警告
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) // 取到data中所有的key值所組成的數(shù)組
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) { // 避免方法名與data的key重復(fù)
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) { // 避免props的key與data的key重復(fù)
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)) { // 判斷是不是保留字段
proxy(vm, `_data`, key) // 代理
}
}
// observe data
observe(data, true /* asRootData */) // 響應(yīng)式處理
}
其中有兩個重要的函數(shù)分別是proxy跟observe,在往下閱讀之前,如果還有不明白Object.defineProperty作用的同學(xué),可以點擊這里進行了解,依賴收集跟派發(fā)更新都需要依靠這個函數(shù)進行實現(xiàn)。
proxy
proxy分別傳入vm,'_data',data中的key值,定義如下:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
proxy函數(shù)的邏輯很簡單,就是對vm._data上的數(shù)據(jù)進行代理,vm._data上保存的就是data數(shù)據(jù)。通過代理的之后我們就可以直接通過this.xxx訪問到data上的數(shù)據(jù),實際上訪問的就是this._data.xxx。
observe
oberse定義在src/core/oberse/index.js下,關(guān)于數(shù)據(jù)驅(qū)動的文件都存放在src/core/observe這個目錄中:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) { // 判斷是否是對象或者是VNode
return
}
let ob: Observer | void
// 是否擁有__ob__屬性 有的話證明已經(jīng)監(jiān)聽過了,直接返回該屬性
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve && // 能否被觀察
!isServerRendering() && // 是否是服務(wù)端渲染
(Array.isArray(value) || isPlainObject(value)) && // 是否是數(shù)組、對象、能否被擴展、是否是Vue函數(shù)
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value) // 對value進行觀察
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
observe函數(shù)會對傳入的value進行判斷,在我們初始化過程會走到new Observer(value),其他情況可以看上面的注釋。
Observer類
export class Observer {
value: any; // 觀察的數(shù)據(jù)
dep: Dep; // dep實例用于 派發(fā)更新
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 把__ob__變成不可枚舉的,因為沒有必要改變watcher本身
def(value, '__ob__', this) 會執(zhí)行 value._ob_ = this(watcher實例)操作
if (Array.isArray(value)) { // 當(dāng)value是數(shù)組
if (hasProto) {
protoAugment(value, arrayMethods) // 重寫Array.prototype的相關(guān)方法
} else {
copyAugment(value, arrayMethods, arrayKeys) // 重寫Array.prototype的相關(guān)方法
}
this.observeArray(value)
} else {
this.walk(value) // 當(dāng)value為對象
}
}
/**
* Walk through all properties 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]) // 對數(shù)據(jù)進行響應(yīng)式處理
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) // 遍歷value數(shù)組的每一項并調(diào)用observe函數(shù),進行響應(yīng)式處理
}
}
}
Observe類要做的事情通過查看源碼也是清晰明了,對數(shù)據(jù)進行響應(yīng)式處理,并對數(shù)組的原型方法進行重寫!defineReactive函數(shù)就是實現(xiàn)依賴收集和派發(fā)更新的核心函數(shù)了,實現(xiàn)代碼如下。
依賴收集
defineReactive
export function defineReactive (
obj: Object, // data數(shù)據(jù)
key: string, // data中對應(yīng)的key值
val: any, // 給data[key] 賦值 可選
customSetter?: ?Function, // 自定義setter 可選
shallow?: boolean // 是否對data[key]為對象的值進行observe遞歸 可選
) {
const dep = new Dep() // Dep實例 **每一個key對應(yīng)一個Dep實例**
const property = Object.getOwnPropertyDescriptor(obj, key) // 拿到對象的屬性描述
if (property && property.configurable === false) { // 判斷對象是否可配置
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) { // 沒有g(shù)etter或者有setter,并且傳入的參數(shù)有兩個
val = obj[key]
}
let childOb = !shallow && observe(val) // 根據(jù)shallow,遞歸遍歷val對象,相當(dāng)于val當(dāng)做data傳入
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) { // 當(dāng)前的全部的Watcher實例
dep.depend() // 把當(dāng)前的Dep.target加入到dep.subs數(shù)組中
if (childOb) { // 如果val是對象,
childOb.dep.depend() // 會在value._ob_的dep.subs數(shù)組中加入Dep.target, 忘記ob實例屬性的同學(xué)可往回翻一番
if (Array.isArray(value)) {
dependArray(value) // 定義如下,邏輯也比較簡單
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// ....
}
})
}
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() // 如果e是響應(yīng)式數(shù)據(jù),則往e._ob_.dep.subs數(shù)組中加入Dep.target
if (Array.isArray(e)) {
dependArray(e) // 遞歸遍歷
}
}
}
代碼中多次用到了Dep類和Dep.target,理解清楚了它們的作用,我們就離Vue數(shù)據(jù)驅(qū)動的原理更近一步了,相關(guān)的代碼如下:
Dep
let uid = 0
/**
* 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++ // 每一個dep都有一個唯一的ID
this.subs = [] // 存放watcher實例的數(shù)組
}
addSub (sub: Watcher) {
this.subs.push(sub) // 往this.subs加入watcher
}
removeSub (sub: Watcher) {
remove(this.subs, sub) // 刪除this.subs對應(yīng)的watcher
}
depend () {
if (Dep.target) {
// watcher.addDep(this) actually
Dep.target.addDep(this) // 在watcher類中查看
}
}
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
// subs aren't sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort((a, b) => a.id - b.id) // 根據(jù)watcher的id進行排序
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // 遍歷subs數(shù)組中的每一個watcher執(zhí)行update方法
}
}
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null // Dep.target 代表當(dāng)前全局的watcher
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target // 賦值
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1] // 賦值
}
Dep的定義還是非常清晰的,代碼注釋如上,很明顯Dep跟Watcher就跟捆綁銷售一樣,互相依賴。我們在分析denfineReactive的時候,在對數(shù)據(jù)進行響應(yīng)式操作的時候,通過Object.defineProperty重寫了getter函數(shù)。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) { // 當(dāng)前的全部的Watcher實例
dep.depend() // 把當(dāng)前的Dep.target加入到dep.subs數(shù)組中
// ..
}
return value
},
其中的dep.depend()實際上就是執(zhí)行了Dep.target.addDep(this),this指向Dep實例,而Dep.target是一個Watcher實例,即執(zhí)行watcher.addDep(this)函數(shù)。我們接下來在看看這個函數(shù)做了什么:
class Watcher {
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep) //
if (!this.depIds.has(id)) {
dep.addSub(this) // 會把watcher插入到dep.subs數(shù)組中
}
}
}
}
可以通過下圖以便理解data、Dep、Watcher的關(guān)系:

回到代碼中,其中dep.addSub(this)就是會把當(dāng)前的wathcer實例插入到dep.subs的數(shù)組中,為之后的派發(fā)更新做好準(zhǔn)備,這樣依賴收集就完成了。但是到現(xiàn)在為止,我們只分析了依賴收集是怎么實現(xiàn)的,但是依賴收集的時機又是在什么時候呢?什么時候會觸發(fā)getter函數(shù)進而實現(xiàn)依賴收集的?在進行依賴收集的時候,Dep.tagrget對應(yīng)wathcer又是什么呢?
Watcher大致可以分為三類: * 渲染W(wǎng)atcher: 每一個實例對應(yīng)唯一的一個(有且只有一個) * computed Watcher: 每一個實例可以有多個,由computed屬性生成的(computed有多少個keyy,實例就有多少個computedWatcher) * user Watcher: 每一個實例可以有多個,由watch屬性生成的(同computed一樣,userWatcher的數(shù)量由key數(shù)量決定) 為避免混淆,我們接下來說的Watcher都是渲染W(wǎng)atcher。我們知道在Vue初始化的過程中,在執(zhí)行mountComponent函數(shù)的時候,會執(zhí)行new Watcher(vm, updateComponent, {}, true),這里的Watcher就是渲染W(wǎng)atcher
class Wachter {
get () {
pushTarget(this) // Dep.target = this
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 更新視圖
} 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
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
}
new Watcher對于渲染watcher而言,會直接執(zhí)行this.get()方法,然后執(zhí)行pushTarget(this),所以當(dāng)前的Dep.target為渲染watcher(用于更新視圖)。 而在我們執(zhí)行this.getter的時候,會調(diào)用render函數(shù),此時會讀取vm實例上的data數(shù)據(jù),這個時候就觸發(fā)了getter函數(shù)了,從而進行了依賴收集,這就是依賴收集的時機,比如
{{ message }} // 會讀取vm._data.message, 觸發(fā)getters函數(shù)
派發(fā)更新
我們繼續(xù)來看defineReactive函數(shù)里
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
// ..
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ..
},
set: function reactiveSetter (newVal) {
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)https://cn.vuejs.org//images/data.png
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
dep.notify() // 遍歷dep.subs數(shù)組,取出所有的wathcer執(zhí)行update操作
}
})
}
當(dāng)我們修改數(shù)據(jù)的時候,會觸發(fā)setter函數(shù),這個時候會執(zhí)行dep.notify,dep.subs中所有的watcher都會執(zhí)行update方法,對于渲染W(wǎng)atcher而言,就是執(zhí)行this.get()方法,及更新視圖。這樣一來,就實現(xiàn)了數(shù)據(jù)驅(qū)動。 到這里,Vue的數(shù)據(jù)驅(qū)動原理我們就分析完了,如果還對這個流程不大清楚的,可以結(jié)合參考官方給的圖解:

總結(jié)
- 通過Object.defineProperty函數(shù)改寫了數(shù)據(jù)的getter和setter函數(shù),來實現(xiàn)依賴收集和派發(fā)更新。
- 一個key值對應(yīng)一個Dep實例,一個Dep實例可以包含多個Watcher,一個Wathcer也可以包含多個Dep。
- Dep用于依賴的收集與管理,并通知對應(yīng)的Watcher執(zhí)行相應(yīng)的操作。
- 依賴收集的時機是在執(zhí)行render方法的時候,讀取vm上的數(shù)據(jù),觸發(fā)getter函數(shù)。而派發(fā)更新即在變更數(shù)據(jù)的時候,觸發(fā)setter函數(shù),通過dep.notify(),通知到所收集的watcher,執(zhí)行相應(yīng)操作。
以上就是詳解Vue數(shù)據(jù)驅(qū)動原理的詳細內(nèi)容,更多關(guān)于Vue數(shù)據(jù)驅(qū)動原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue router+vuex實現(xiàn)首頁登錄驗證判斷邏輯
這篇文章主要介紹了vue router+vuex實現(xiàn)首頁登錄判斷邏輯,用于判斷是否登錄首頁,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05

