前言
接著上一篇的初始化部分,我們細看initData
中做了什麼。
正文
initData
function initData (vm: Component) {
let data = vm.$options.data
// 獲得傳入的data.此處為{a:1, b:2}
data = vm._data = typeof data === `function`
? getData(data, vm)
: data || {}
// 如果data不是純物件,則列印警告資訊
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== `production` && warn(
`data functions should return an object:
` +
`https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function`,
vm
)
}
// 獲得data中所有的key,即a,b
const keys = Object.keys(data)
// 獲得元件中定義的props與methods
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
// data中的第i個k
const key = keys[i]
// 如果methods中定義過相同的key,報錯
if (process.env.NODE_ENV !== `production`) {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// 如果props中定義過相同的key,報錯
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== `production` && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
// 如果key不是保留關鍵字,即不是_或者$開頭的key。
} else if (!isReserved(key)) {
// 將屬性代理到例項的_data屬性上。
// 例如vm.a = 1實際上會被處理為vm._data.a = 1;
// vm.a 將返回 vm._data.a;
proxy(vm, `_data`, key)
}
}
// 讓data變為響應式資料,即資料改變時候,UI也能跟著變。
observe(data, true /* asRootData */)
}
observe
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果設定的引數value不是一個物件或者是一個虛擬dom。則直接返回。
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 如果這個value存在觀察者例項,則賦給返回值
if (hasOwn(value, `__ob__`) && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert && // 需要轉化
!isServerRendering() && // 非服務端渲染
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue // 是陣列或者物件且可以擴充套件屬性,且不是vue元件例項
) {
// 根據給定的值建立一個觀察者例項
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
// 返回觀察者例項
return ob
}
new Observer(value)
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
// 要被觀察的值
this.value = value
// new 一個用來收集依賴的物件
this.dep = new Dep()
this.vmCount = 0
// 將該Observer例項,賦值給value的__ob__屬性。
def(value, `__ob__`, this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
// 如果是陣列的話,需要對陣列的方法做特殊處理,
// 並且依次的讓陣列的每個物件元素變為可觀察的
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
// 否則是物件的話,依次遍歷每個屬性,並設定其
// getter與setter,支援後續的響應式變化。
this.walk(value)
}
}
/**
* 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]])
}
}
/**
* Observe a list of Array items.
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
observe 與 Observer簡單說明
observe使一個物件變為響應式物件,在其值改變時候,可觸發ui改變。其做法是為每一個value(非原始型別的)設定一個__ob__,即Observer物件例項。如果value是一個物件,會遍歷它的每一個key,然後為其設定setter/getter,如果是陣列會對陣列元素依次的遞迴observe。
假設輸入為data = {a: 1, b: 2}
經過observe後,會變成
data = {a: 1, b:2, __ob__: ObserverInstance, a的 getter,setter,b的getter, b的setter}。
其中ObserverInstance.dep用來收集對這個物件變更感興趣的watcher,
其中ObserverInstance.value指向這個物件data;
defineReactive
為了以最簡單的方式說明流程,這裡我們只看物件的處理情況,到了這裡,我們的邏輯進入walk
函式內部,遍歷物件的每個屬性,即a,b;分別定義為響應式屬性。
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 這裡的Dep還是Watcher收集器,用來收集對該變數感興趣的watcher
const dep = new Dep()
// 獲取屬性描述符,不熟悉的同學請查閱es6的文件
const property = Object.getOwnPropertyDescriptor(obj, key)
// 如果屬性描述符顯示該物件不可配置,
// 即無法設定getter,setter,也就無法處理為響應式屬性了,那直接返回。
// 一般我們定義的data裡很少設定屬性描述符,預設property => undefined
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
// 如果該obj的key,對應的val還是物件,則使其也變成可觀察的物件
// 這裡是一個遞迴處理,shallow標識是否深度處理該值。
// 類似深拷貝,淺拷貝中是否深拷貝的邏輯。
let childOb = !shallow && observe(val)
// 重點,設定getter,setter,這裡我們定義data.a的getter,setter,
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 這裡一般物件的屬性是沒有getter的,直接返回val
const value = getter ? getter.call(obj) : val
// 在特定的時機進行依賴收集。通過Dep.target
// 這裡在後期vue處理模板template的時候,會生成render函式,
// render函式執行生成虛擬dom時候會去讀取vm例項的屬性,比如vm.a這時候會觸發這個getter,
// 此時的Dep.target為一個watcher,
// 內容為vm._update(vm._render)這個函式,用來更新檢視用
// 將該函式新增到defineReactive內部定義的dep中。
// 接下來我們看後面的set部分
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 當我們觸發點選事件時候,this.a += 1;
// 此時newVal是value值加1,所以程式碼會繼續走
const value = getter ? getter.call(obj) : val
/* 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()
}
// 這裡設定了newVal為val
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 如果新值是一個物件,會繼續被當做可觀察的物件。
childOb = !shallow && observe(newVal)
// dep.notify就是挨個通知Dep中收集的watcher去搞事情。
// get函式內我提到,dep中被加入了一個watcher,
// 其watcher實際作用就是觸發檢視更新,get時候,被收集了,
// set時候就會觸發ui的更新。
dep.notify()
}
})
}
initData的完結
至此initData
完結,上文中defineReactive
的getter和setter的設定,在前期到不會觸發,這裡只是把規矩定下,真正用到的時候還需要跟模板結合。這個章節我們主要分析一下initData
對data
的處理。接下來我們看下模板的地方。
首先我們需要把程式碼跳出來看這裡,不要問我為什麼知道看這裡,因為我是看完一遍後有個印象,現在只不過在梳理流程。
this.init -> initState -> initData
this.init -> initState ->
this.init
this.init -> vm.$mount(vm.options.el)
這裡我們再貼一下init內部函式的程式碼,讓大家對大概的方位有個瞭解
Vue.prototype._init = function (options?: Object) {
// ***************************省略頂部程式碼
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, `created`)
/* istanbul ignore if */
if (process.env.NODE_ENV !== `production` && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
vm.$mount(vm.$options.el)
}
}
vm.$mount
接下來我們簡述一下vm.$mount內部的程式碼
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 根據id查詢dom元素
el = el && query(el)
// 巴拉巴拉省略魔法
// 如果沒有render函式則生成render函式
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
return mount.call(this, el, hydrating)
}
compileToFunctions生成的render函式
compileToFunctions是怎麼根據模板生成html的,這不本章節的重點,後面我會單獨去寫compileToFunctions的過程,本章節重點是render函式的結果,我以上一章節的html為例,這是render函式的結果。
(function anonymous() {
with (this) {
return _c(`div`, {
attrs: {
"id": "demo"
}
}, [_c(`div`, [_c(`p`, [_v("a:" + _s(a))]), _v(" "), _c(`p`, [_v("b: " + _s(b))]), _v(" "), _c(`p`, [_v("a+b: " + _s(total))]), _v(" "), _c(`button`, {
on: {
"click": addA
}
}, [_v("a+1")])])])
}
})
其中_c就是vue封裝的createElement,用來生成虛擬dom。_s就是toString方法,等等。這裡我們可以看到其中有些引數是變數,a,b,total。這與我們在js中定義的一致。接下來我們看下這個渲染函式執行的地方。
mount.call
首先還是接上面的程式碼。mount.call(this, el, hydrating)。
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
mountComponent
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 巴拉巴拉省略大法,去除無關程式碼
callHook(vm, `beforeMount`)
// 巴拉巴拉省略大法,去除無關程式碼
let updateComponent
/* istanbul ignore if */
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 核心就這一句話。new 一個Watcher,上文多次提到的傢伙。
vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, `mounted`)
}
return vm
}
new Watcher()
上個程式碼片段核心就這一句話。vm._watcher = new Watcher(vm, updateComponent, noop)
。
重點是這個Watcher第二個引數,updateComponent,很重要。
new Watcher我們著重看下建構函式內部的程式碼即可,如下是精簡過後的程式碼
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: ISet;
newDepIds: ISet;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
// 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
}
// 此時expOrFn為上述的updateComponent
this.getter = expOrFn
// 由於此時lazy是falsly值,觸發get
this.value = this.lazy
? undefined
: this.get()
}
get () {
// 該函式將Dep.target置為當前watcher。
pushTarget(this)
let value
const vm = this.vm
try {
// 呼叫getter實際是呼叫updateComponent
// 由於updateComponent會呼叫options.render,所以會觸發
// vm._render函式,而vm._render函式中的核心則是
// vnode = render.call(vm, vm.$createElement)
// 在 compileToFunctions生成的render函式 一節我們已經看到了一個rendre函式大概的面貌
// 此時render函式中有時候會取讀取vm.a的值。有時會取讀取vm.b的值。
// 當讀取vm.a或者b的時候,就會觸發對應屬性的getter
// 然後會將當前的Watcher加入屬性對應的dep中。
// 聯絡不起來的同學可以往回看,defineReactive中的dep收集的就是當前watcher了。
// 當使用者點選頁面的a+1按鈕時,則會觸發this.a += 1。
// 則會觸發defineReactive(obj, a, {get,set})中的set,
// set中會呼叫dep.notify。其實就是讓dep收集到的watcher挨個執行
// 下述中的run方法.
// 而run方法又觸發了當前的這個get方法,執行到getter.call的時候,介面就更新了。
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
}
run () {
if (this.active) {
// 關鍵一句就是獲取值,實際上這裡的獲取值就是
// get -> this.get -> updateComponent -> 虛擬節點中重新獲取
// 介面中需要的a,b的值。
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
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)
}
}
}
}
總結
此時一個大概的關於data是如何影響view的流程基本跑通了。以介面資料a為例,
核心思想就是defineReactive(data, a, {get,set})去設定屬性a的getter,setter。
getter將會在vue執行render函式生成虛擬dom的時候,將介面更新的watcher放入a的dep中。
當滑鼠單擊介面上的a+1按鈕觸發this.a += 1時候,會觸發a的setter函式,此時會將getter時收集的依賴————更新介面的watcher————觸發。watcher執行自身的run方法,即更新介面。
至此data -> view這一層算是通了,至於input中的v-model實際上是input + onInput事件的語法糖,監聽input,值改變時候通過事件修改vm.a的值,進一步觸發————更新介面的watcher,使介面更新。