做者:滴滴公共前端團隊 - 黃軼javascript
本文來自《Vue.js 權威指南》源碼篇的一個章節,如今分享出來給你們html
Vue.js 最顯著的功能就是響應式系統,它是一個典型的 MVVM 框架,模型(Model)只是普通的 JavaScript 對象,修改它則視圖(View)會自動更新。這種設計讓狀態管理變得很是簡單而直觀,不過理解它的原理也很重要,能夠避免一些常見問題。下面讓咱們深挖 Vue.js 響應式系統的細節,來看一看 Vue.js 是如何把模型和視圖創建起關聯關係的。前端
咱們先來看一個簡單的例子。代碼示例以下:vue
<div id="main">
<h1>count: {{times}}</h1>
</div>
<script src="vue.js"></script>
<script> var vm = new Vue({ el: '#main', data: function () { return { times: 1 }; }, created: function () { var me = this; setInterval(function () { me.times++; }, 1000); } }); </script>複製代碼
運行後,咱們能夠從頁面中看到,count 後面的 times 每隔 1s 遞增 1,視圖一直在更新。在代碼中僅僅是經過 setInterval 方法每隔 1s 來修改 vm.times 的值,並無任何 DOM 操做。那麼 Vue.js 是如何實現這個過程的呢?咱們能夠經過一張圖來看一下,以下圖所示:
java
圖中的模型(Model)就是 data 方法返回的{times:1},視圖(View)是最終在瀏覽器中顯示的DOM。模型經過Observer、Dep、Watcher、Directive等一系列對象的關聯,最終和視圖創建起關係。概括起來,Vue.js在這裏主要作了三件事:node
接下來咱們就結合 Vue.js 的源碼來詳細介紹這三個過程。react
首先來看一下 Vue.js 是如何給 data 對象添加 Observer 的。咱們知道,Vue 實例建立的過程會有一個生命週期,其中有一個過程就是調用 vm.initData 方法處理 data 選項。initData 方法的源碼定義以下:ios
<!-源碼目錄:src/instance/internal/state.js-->
Vue.prototype._initData = function () {
var dataFn = this.$options.data
var data = this._data = dataFn ? dataFn() : {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object.',
this
)
}
var props = this._props
// proxy data on instance
var keys = Object.keys(data)
var i, key
i = keys.length
while (i--) {
key = keys[i]
// there are two scenarios where we can proxy a data key:
// 1. it's not already defined as a prop
// 2. it's provided via a instantiation option AND there are no
// template prop present
if (!props || !hasOwn(props, key)) {
this._proxy(key)
} else if (process.env.NODE_ENV !== 'production') {
warn(
'Data field "' + key + '" is already defined ' +
'as a prop. To provide default value for a prop, use the "default" ' +
'prop option; if you want to pass prop values to an instantiation ' +
'call, use the "propsData" option.',
this
)
}
}
// observe data
observe(data, this)
}複製代碼
在 initData 中咱們要特別注意 proxy 方法,它的功能就是遍歷 data 的 key,把 data 上的屬性代理到 vm 實例上。_proxy 方法的源碼定義以下:git
<!-源碼目錄:src/instance/internal/state.js-->
Vue.prototype._proxy = function (key) {
if (!isReserved(key)) {
// need to store ref to self here
// because these getter/setters might
// be called by child scopes via
// prototype inheritance.
var self = this
Object.defineProperty(self, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return self._data[key]
},
set: function proxySetter (val) {
self._data[key] = val
}
})
}
}複製代碼
proxy 方法主要經過 Object.defineProperty 的 getter 和 setter 方法實現了代理。在前面的例子中,咱們調用 vm.times 就至關於訪問了 vm.data.times。github
在 _initData 方法的最後,咱們調用了 observe(data, this) 方法來對 data 作監聽。observe 方法的源碼定義以下:
<!-源碼目錄:src/observer/index.js-->
export function observe (value, vm) {
if (!value || typeof value !== 'object') {
return
}
var ob
if (
hasOwn(value, '__ob__') &&
value.__ob__ instanceof Observer
) {
ob = value.__ob__
} else if (
shouldConvert &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (ob && vm) {
ob.addVm(vm)
}
return ob
}複製代碼
observe 方法首先判斷 value 是否已經添加了 ob 屬性,它是一個 Observer 對象的實例。若是是就直接用,不然在 value 知足一些條件(數組或對象、可擴展、非 vue 組件等)的狀況下建立一個 Observer 對象。接下來咱們看一下 Observer 這個類,它的源碼定義以下:
<!-源碼目錄:src/observer/index.js-->
export function Observer (value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}複製代碼
Observer 類的構造函數主要作了這麼幾件事:首先建立了一個 Dep 對象實例(關於 Dep 對象咱們稍後做介紹);而後把自身 this 添加到 value 的 ob 屬性上;最後對 value 的類型進行判斷,若是是數組則觀察數組,不然觀察單個元素。其實 observeArray 方法就是對數組進行遍歷,遞歸調用 observe 方法,最終都會調用 walk 方法觀察單個元素。接下來咱們看一下 walk 方法,它的源碼定義以下:
<!-源碼目錄:src/observer/index.js-->
Observer.prototype.walk = function (obj) {
var keys = Object.keys(obj)
for (var i = 0, l = keys.length; i < l; i++) {
this.convert(keys[i], obj[keys[i]])
}
}複製代碼
walk 方法是對 obj 的 key 進行遍歷,依次調用 convert 方法,對 obj 的每個屬性進行轉換,讓它們擁有 getter、setter 方法。只有當 obj 是一個對象時,這個方法才能被調用。接下來咱們看一下 convert 方法,它的源碼定義以下:
<!-源碼目錄:src/observer/index.js-->
Observer.prototype.convert = function (key, val) {
defineReactive(this.value, key, val)
}複製代碼
convert 方法很簡單,它調用了 defineReactive 方法。這裏 this.value 就是要觀察的 data 對象,key 是 data 對象的某個屬性,val 則是這個屬性的值。defineReactive 的功能是把要觀察的 data 對象的每一個屬性都賦予 getter 和 setter 方法。這樣一旦屬性被訪問或者更新,咱們就能夠追蹤到這些變化。接下來咱們看一下 defineReactive 方法,它的源碼定義以下:
<!-源碼目錄:src/observer/index.js-->
export function defineReactive (obj, key, val) {
var dep = new Dep()
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get
var setter = property && property.set
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (isArray(value)) {
for (var e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
}複製代碼
defineReactive 方法最核心的部分就是經過調用 Object.defineProperty 給 data 的每一個屬性添加 getter 和setter 方法。當 data 的某個屬性被訪問時,則會調用 getter 方法,判斷當 Dep.target 不爲空時調用 dep.depend 和 childObj.dep.depend 方法作依賴收集。若是訪問的屬性是一個數組,則會遍歷這個數組收集數組元素的依賴。當改變 data 的屬性時,則會調用 setter 方法,這時調用 dep.notify 方法進行通知。這裏咱們提到了 dep,它是 Dep 對象的實例。接下來咱們看一下 Dep 這個類,它的源碼定義以下:
<!-源碼目錄:src/observer/dep.js-->
export default function Dep () {
this.id = uid++
this.subs = []
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null複製代碼
Dep 類是一個簡單的觀察者模式的實現。它的構造函數很是簡單,初始化了 id 和 subs。其中 subs 用來存儲全部訂閱它的 Watcher,Watcher 的實現稍後咱們會介紹。Dep.target 表示當前正在計算的 Watcher,它是全局惟一的,由於在同一時間只能有一個 Watcher 被計算。
前面提到了在 getter 和 setter 方法調用時會分別調用 dep.depend 方法和 dep.notify 方法,接下來依次介紹這兩個方法。depend 方法的源碼定義以下:
<!-源碼目錄:src/observer/dep.js-->
Dep.prototype.depend = function () {
Dep.target.addDep(this)
}複製代碼
depend 方法很簡單,它經過 Dep.target.addDep(this) 方法把當前 Dep 的實例添加到當前正在計算的Watcher 的依賴中。接下來咱們看一下 notify 方法,它的源碼定義以下:
<!-源碼目錄:src/observer/dep.js-->
Dep.prototype.notify = function () {
// stablize the subscriber list first
var subs = toArray(this.subs)
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}複製代碼
notify 方法也很簡單,它遍歷了全部的訂閱 Watcher,調用它們的 update 方法。
至此,vm 實例中給 data 對象添加 Observer 的過程就結束了。接下來咱們看一下 Vue.js 是如何進行指令解析的。
Vue 指令類型不少,限於篇幅,咱們不會把全部指令的解析過程都介紹一遍,這裏結合前面的例子只介紹 v-text 指令的解析過程,其餘指令的解析過程也大同小異。
前面咱們提到了 Vue 實例建立的生命週期,在給 data 添加 Observer 以後,有一個過程是調用 vm.compile 方法對模板進行編譯。compile 方法的源碼定義以下:
<!-源碼目錄:src/instance/internal/lifecycle.js-->
Vue.prototype._compile = function (el) {
var options = this.$options
// transclude and init element
// transclude can potentially replace original
// so we need to keep reference; this step also injects
// the template and caches the original attributes
// on the container node and replacer node.
var original = el
el = transclude(el, options)
this._initElement(el)
// handle v-pre on root node (#2026)
if (el.nodeType === 1 && getAttr(el, 'v-pre') !== null) {
return
}
// root is always compiled per-instance, because
// container attrs and props can be different every time.
var contextOptions = this._context && this._context.$options
var rootLinker = compileRoot(el, options, contextOptions)
// resolve slot distribution
resolveSlots(this, options._content)
// compile and link the rest
var contentLinkFn
var ctor = this.constructor
// component compilation can be cached
// as long as it's not using inline-template
if (options._linkerCachable) {
contentLinkFn = ctor.linker
if (!contentLinkFn) {
contentLinkFn = ctor.linker = compile(el, options)
}
}
// link phase
// make sure to link root with prop scope!
var rootUnlinkFn = rootLinker(this, el, this._scope)
var contentUnlinkFn = contentLinkFn
? contentLinkFn(this, el)
: compile(el, options)(this, el)
// register composite unlink function
// to be called during instance destruction
this._unlinkFn = function () {
rootUnlinkFn()
// passing destroying: true to avoid searching and
// splicing the directives
contentUnlinkFn(true)
}
// finally replace original
if (options.replace) {
replace(original, el)
}
this._isCompiled = true
this._callHook('compiled')
}複製代碼
咱們能夠經過下圖來看一下這個方法編譯的主要流程:
這個過程經過 el = transclude(el, option) 方法把 template 編譯成一段 document fragment,拿到 el 對象。而指令解析部分就是經過 compile(el, options) 方法實現的。接下來咱們看一下 compile 方法的實現,它的源碼定義以下:
<!-源碼目錄:src/compiler/compile.js-->
export function compile (el, options, partial) {
// link function for the node itself.
var nodeLinkFn = partial || !options._asComponent
? compileNode(el, options)
: null
// link function for the childNodes
var childLinkFn =
!(nodeLinkFn && nodeLinkFn.terminal) &&
!isScript(el) &&
el.hasChildNodes()
? compileNodeList(el.childNodes, options)
: null
/** * A composite linker function to be called on a already * compiled piece of DOM, which instantiates all directive * instances. * * @param {Vue} vm * @param {Element|DocumentFragment} el * @param {Vue} [host] - host vm of transcluded content * @param {Object} [scope] - v-for scope * @param {Fragment} [frag] - link context fragment * @return {Function|undefined} */
return function compositeLinkFn (vm, el, host, scope, frag) {
// cache childNodes before linking parent, fix #657
var childNodes = toArray(el.childNodes)
// link
var dirs = linkAndCapture(function compositeLinkCapturer () {
if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag)
if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag)
}, vm)
return makeUnlinkFn(vm, dirs)
}
}複製代碼
compile 方法主要經過 compileNode(el, options) 方法完成節點的解析,若是節點擁有子節點,則調用 compileNodeList(el.childNodes, options) 方法完成子節點的解析。compileNodeList 方法其實就是遍歷子節點,遞歸調用 compileNode 方法。由於 DOM 元素自己就是樹結構,這種遞歸方法也就是常見的樹的深度遍歷方法,這樣就能夠完成整個 DOM 樹節點的解析。接下來咱們看一下 compileNode 方法的實現,它的源碼定義以下:
<!-源碼目錄:src/compiler/compile.js-->
function compileNode (node, options) {
var type = node.nodeType
if (type === 1 && !isScript(node)) {
return compileElement(node, options)
} else if (type === 3 && node.data.trim()) {
return compileTextNode(node, options)
} else {
return null
}
}複製代碼
compileNode 方法對節點的 nodeType 作判斷,若是是一個非 script 普通的元素(div、p等);則調用 compileElement(node, options) 方法解析;若是是一個非空的文本節點,則調用 compileTextNode(node, options) 方法解析。咱們在前面的例子中解析的是非空文本節點 count: {{times}},這其實是 v-text 指令,它的解析是經過 compileTextNode 方法實現的。接下來咱們看一下 compileTextNode 方法,它的源碼定義以下:
<!-源碼目錄:src/compiler/compile.js-->
function compileTextNode (node, options) {
// skip marked text nodes
if (node._skip) {
return removeText
}
var tokens = parseText(node.wholeText)
if (!tokens) {
return null
}
// mark adjacent text nodes as skipped,
// because we are using node.wholeText to compile
// all adjacent text nodes together. This fixes
// issues in IE where sometimes it splits up a single
// text node into multiple ones.
var next = node.nextSibling
while (next && next.nodeType === 3) {
next._skip = true
next = next.nextSibling
}
var frag = document.createDocumentFragment()
var el, token
for (var i = 0, l = tokens.length; i < l; i++) {
token = tokens[i]
el = token.tag
? processTextToken(token, options)
: document.createTextNode(token.value)
frag.appendChild(el)
}
return makeTextNodeLinkFn(tokens, frag, options)
}複製代碼
compileTextNode 方法首先調用了 parseText 方法對 node.wholeText 作解析。主要經過正則表達式解析 count: {{times}} 部分,咱們看一下解析結果,以下圖所示:
解析後的 tokens 是一個數組,數組的每一個元素則是一個 Object。若是是 count: 這樣的普通文本,則返回的對象只有 value 字段;若是是 {{times}} 這樣的插值,則返回的對象包含 html、onTime、tag、value 等字段。
接下來建立 document fragment,遍歷 tokens 建立 DOM 節點插入到這個 fragment 中。在遍歷過程當中,若是 token 無 tag 字段,則調用 document.createTextNode(token.value) 方法建立 DOM 節點;不然調用processTextToken(token, options) 方法建立 DOM 節點和擴展 token 對象。咱們看一下調用後的結果,以下圖所示:
能夠看到,token 字段多了一個 descriptor 屬性。這個屬性包含了幾個字段,其中 def 表示指令相關操做的對象,expression 爲解析後的表達式,filters 爲過濾器,name 爲指令的名稱。
在compileTextNode 方法的最後,調用 makeTextNodeLinkFn(tokens, frag, options) 並返回該方法執行的結果。接下來咱們看一下 makeTextNodeLinkFn 方法,它的源碼定義以下:
<!-源碼目錄:src/compiler/compile.js-->
function makeTextNodeLinkFn (tokens, frag) {
return function textNodeLinkFn (vm, el, host, scope) {
var fragClone = frag.cloneNode(true)
var childNodes = toArray(fragClone.childNodes)
var token, value, node
for (var i = 0, l = tokens.length; i < l; i++) {
token = tokens[i]
value = token.value
if (token.tag) {
node = childNodes[i]
if (token.oneTime) {
value = (scope || vm).$eval(value)
if (token.html) {
replace(node, parseTemplate(value, true))
} else {
node.data = _toString(value)
}
} else {
vm._bindDir(token.descriptor, node, host, scope)
}
}
}
replace(el, fragClone)
}
}複製代碼
makeTextNodeLinkFn 這個方法什麼也沒作,它僅僅是返回了一個新的方法 textNodeLinkFn。往前回溯,這個方法最終做爲 compileNode 的返回值,被添加到 compile 方法生成的 childLinkFn 中。
咱們回到 compile 方法,在 compile 方法的最後有這樣一段代碼:
<!-源碼目錄:src/compiler/compile.js-->
return function compositeLinkFn (vm, el, host, scope, frag) {
// cache childNodes before linking parent, fix #657
var childNodes = toArray(el.childNodes)
// link
var dirs = linkAndCapture(function compositeLinkCapturer () {
if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag)
if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag)
}, vm)
return makeUnlinkFn(vm, dirs)
}複製代碼
compile 方法返回了 compositeLinkFn,它在 Vue.prototype._compile 方法執行時,是經過 compile(el, options)(this, el) 調用的。compositeLinkFn 方法執行了 linkAndCapture 方法,它的功能是經過調用 compile 過程當中生成的 link 方法建立指令對象,再對指令對象作一些綁定操做。linkAndCapture 方法的源碼定義以下:
<!-源碼目錄:src/compiler/compile.js-->
function linkAndCapture (linker, vm) {
/* istanbul ignore if */
if (process.env.NODE_ENV === 'production') {
// reset directives before every capture in production
// mode, so that when unlinking we don't need to splice
// them out (which turns out to be a perf hit).
// they are kept in development mode because they are
// useful for Vue's own tests.
vm._directives = []
}
var originalDirCount = vm._directives.length
linker()
var dirs = vm._directives.slice(originalDirCount)
dirs.sort(directiveComparator)
for (var i = 0, l = dirs.length; i < l; i++) {
dirs[i]._bind()
}
return dirs
}複製代碼
linkAndCapture 方法首先調用了 linker 方法,它會遍歷 compile 過程當中生成的全部 linkFn 並調用,本例中會調用到以前定義的 textNodeLinkFn。這個方法會遍歷 tokens,判斷若是 token 的 tag 屬性值爲 true 且 oneTime 屬性值爲 false,則調用 vm.bindDir(token.descriptor, node, host, scope) 方法建立指令對象。 vm._bindDir 方法的源碼定義以下:
<!-源碼目錄:src/instance/internal/lifecycle.js-->
Vue.prototype._bindDir = function (descriptor, node, host, scope, frag) {
this._directives.push(
new Directive(descriptor, this, node, host, scope, frag)
)
}複製代碼
Vue.prototype._bindDir 方法就是根據 descriptor 實例化不一樣的 Directive 對象,並添加到 vm 實例 directives 數組中的。到這一步,Vue.js 從解析模板到生成 Directive 對象的步驟就完成了。接下來回到 linkAndCapture 方法,它對建立好的 directives 進行排序,而後遍歷 directives 調用 dirs[i]._bind 方法對單個directive作一些綁定操做。dirs[i]._bind方法的源碼定義以下:
<!-源碼目錄:src/directive.js-->
Directive.prototype._bind = function () {
var name = this.name
var descriptor = this.descriptor
// remove attribute
if (
(name !== 'cloak' || this.vm._isCompiled) &&
this.el && this.el.removeAttribute
) {
var attr = descriptor.attr || ('v-' + name)
this.el.removeAttribute(attr)
}
// copy def properties
var def = descriptor.def
if (typeof def === 'function') {
this.update = def
} else {
extend(this, def)
}
// setup directive params
this._setupParams()
// initial bind
if (this.bind) {
this.bind()
}
this._bound = true
if (this.literal) {
this.update && this.update(descriptor.raw)
} else if (
(this.expression || this.modifiers) &&
(this.update || this.twoWay) &&
!this._checkStatement()
) {
// wrapped updater for context
var dir = this
if (this.update) {
this._update = function (val, oldVal) {
if (!dir._locked) {
dir.update(val, oldVal)
}
}
} else {
this._update = noop
}
var preProcess = this._preProcess
? bind(this._preProcess, this)
: null
var postProcess = this._postProcess
? bind(this._postProcess, this)
: null
var watcher = this._watcher = new Watcher(
this.vm,
this.expression,
this._update, // callback
{
filters: this.filters,
twoWay: this.twoWay,
deep: this.deep,
preProcess: preProcess,
postProcess: postProcess,
scope: this._scope
}
)
// v-model with inital inline value need to sync back to
// model instead of update to DOM on init. They would
// set the afterBind hook to indicate that.
if (this.afterBind) {
this.afterBind()
} else if (this.update) {
this.update(watcher.value)
}
}
}複製代碼
Directive.prototype._bind 方法的主要功能就是作一些指令的初始化操做,如混合 def 屬性。def 是經過 this.descriptor.def 得到的,this.descriptor 是對指令進行相關描述的對象,而 this.descriptor.def 則是包含指令相關操做的對象。好比對於 v-text 指令,咱們能夠看一下它的相關操做,源碼定義以下:
<!-源碼目錄:src/directives/public/text.js-->
export default {
bind () {
this.attr = this.el.nodeType === 3
? 'data'
: 'textContent'
},
update (value) {
this.el[this.attr] = _toString(value)
}
}複製代碼
v-text 的 def 包含了 bind 和 update 方法,Directive 在初始化時經過 extend(this, def) 方法能夠對實例擴展這兩個方法。Directive 在初始化時還定義了 this.update 方法,並建立了 Watcher,把 this.update 方法做爲 Watcher 的回調函數。這裏把 Directive 和 Watcher 作了關聯,當 Watcher 觀察到指令表達式值變化時,會調用 Directive 實例的 _update 方法,最終調用 v-text 的 update 方法更新 DOM 節點。
至此,vm 實例中編譯模板、解析指令、綁定 Watcher 的過程就結束了。接下來咱們看一下 Watcher 的實現,瞭解 Directive 和 Observer 之間是如何經過 Watcher 關聯的。
咱們先來看一下 Watcher 類的實現,它的源碼定義以下:
<!-源碼目錄:src/watcher.js-->
export default function Watcher (vm, expOrFn, cb, options) {
// mix in options
if (options) {
extend(this, options)
}
var isFn = typeof expOrFn === 'function'
this.vm = vm
vm._watchers.push(this)
this.expression = expOrFn
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.prevError = null // for async error stacks
// parse expression for getter/setter
if (isFn) {
this.getter = expOrFn
this.setter = undefined
} else {
var res = parseExpression(expOrFn, this.twoWay)
this.getter = res.get
this.setter = res.set
}
this.value = this.lazy
? undefined
: this.get()
// state for avoiding false triggers for deep and Array
// watchers during vm._digest()
this.queued = this.shallow = false
}複製代碼
Directive 實例在初始化 Watche r時,會傳入指令的 expression。Watcher 構造函數會經過 parseExpression(expOrFn, this.twoWay) 方法對 expression 作進一步的解析。在前面的例子中, expression 是times,passExpression 方法的功能是把 expression 轉換成一個對象,以下圖所示:
能夠看到 res 有兩個屬性,其中 exp 爲表達式字符串;get 是經過 new Function 生成的匿名方法,能夠把它打印出來,以下圖所示:
能夠看到 res.get 方法很簡單,它接受傳入一個 scope 變量,返回 scope.times。對於傳入的 scope 值,稍後咱們會進行介紹。在 Watcher 構造函數的最後調用了 this.get 方法,它的源碼定義以下:
<!-源碼目錄:src/watcher.js-->
Watcher.prototype.get = function () {
this.beforeGet()
var scope = this.scope || this.vm
var value
try {
value = this.getter.call(scope, scope)
} catch (e) {
if (
process.env.NODE_ENV !== 'production' &&
config.warnExpressionErrors
) {
warn(
'Error when evaluating expression ' +
'"' + this.expression + '": ' + e.toString(),
this.vm
)
}
}
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
if (this.preProcess) {
value = this.preProcess(value)
}
if (this.filters) {
value = scope._applyFilters(value, null, this.filters, false)
}
if (this.postProcess) {
value = this.postProcess(value)
}
this.afterGet()
return value
}複製代碼
Watcher.prototype.get 方法的功能就是對當前 Watcher 進行求值,收集依賴關係。它首先執行 this.beforeGet 方法,源碼定義以下:
<!-源碼目錄:src/watcher.js-->
Watcher.prototype.beforeGet = function () {
Dep.target = this
}複製代碼
Watcher.prototype.beforeGet 很簡單,設置 Dep.target 爲當前 Watcher 實例,爲接下來的依賴收集作準備。咱們回到 get 方法,接下來執行 this.getter.call(scope, scope) 方法,這裏的 scope 是 this.vm,也就是當前 Vue 實例。這個方法實際上至關於獲取 vm.times,這樣就觸發了對象的 getter。在第一小節咱們給 data 添加 Observer 時,經過 Object.defineProperty 給 data 對象的每個屬性添加 getter 和 setter。回顧一下代碼:
<!-源碼目錄:src/observer/index.js-->
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (isArray(value)) {
for (var e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
}
}
}
return value
},
…
})複製代碼
當獲取 vm.times 時,會執行到 get 方法體內。因爲咱們在以前已經設置了 Dep.target 爲當前 Watcher 實例,因此接下來就調用 dep.depend() 方法完成依賴收集。它其實是執行了 Dep.target.addDep(this),至關於執行了 Watcher 實例的 addDep 方法,把 Dep 實例添加到 Watcher 實例的依賴中。addDep 方法的源碼定義以下:
<!-源碼目錄:src/watcher.js-->
Watcher.prototype.addDep = function (dep) {
var 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.prototype.addDep 方法就是把 dep 添加到 Watcher 實例的依賴中,同時又經過 dep.addSub(this) 把 Watcher 實例添加到 dep 的訂閱者中。addSub 方法的源碼定義以下:
<!-源碼目錄:src/observer/dep.js-->
Dep.prototype.addSub = function (sub) {
this.subs.push(sub)
}複製代碼
至此,指令完成了依賴收集,而且經過 Watcher 完成了對數據變化的訂閱。
接下來咱們看一下,當 data 發生變化時,視圖是如何自動更新的。在前面的例子中,咱們經過 setInterval 每隔 1s 執行一次 vm.times++,數據改變會觸發對象的 setter,執行 set 方法體的代碼。回顧一下代碼:
<!-源碼目錄:src/observer/index.js-->
Object.defineProperty(obj, key, {
…
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})複製代碼
這裏會調用 dep.notify() 方法,它會遍歷全部的訂閱者,也就是 Watcher 實例。而後調用 Watcher 實例的 update 方法,源碼定義以下:
<!-源碼目錄:src/watcher.js-->
Watcher.prototype.update = function (shallow) {
if (this.lazy) {
this.dirty = true
} else if (this.sync || !config.async) {
this.run()
} else {
// if queued, only overwrite shallow with non-shallow,
// but not the other way around.
this.shallow = this.queued
? shallow
? this.shallow
: false
: !!shallow
this.queued = true
// record before-push error stack in debug mode
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.debug) {
this.prevError = new Error('[vue] async stack trace')
}
pushWatcher(this)
}
}複製代碼
Watcher.prototype.update 方法在知足某些條件下會直接調用 this.run 方法。在多數狀況下會調用 pushWatcher(this) 方法把 Watcher 實例推入隊列中,延遲 this.run 調用的時機。pushWatcher 方法的源碼定義以下:
<!-源碼目錄:src/batcher.js-->
export function pushWatcher (watcher) {
const id = watcher.id
if (has[id] == null) {
// push watcher into appropriate queue
const q = watcher.user
? userQueue
: queue
has[id] = q.length
q.push(watcher)
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushBatcherQueue)
}
}
}複製代碼
pushWatcher 方法把 Watcher 推入隊列中,經過 nextTick 方法在下一個事件循環週期處理 Watcher 隊列,這是 Vue.j s的一種性能優化手段。由於若是同時觀察的數據屢次變化,好比同步執行 3 次 vm.time++,同步調用 watcher.run 就會觸發 3 次 DOM 操做。而推入隊列中等待下一個事件循環週期再操做隊列裏的 Watcher,由於是同一個 Watcher,它只會調用一次 watcher.run,從而只觸發一次 DOM 操做。接下來咱們看一下 flushBatcherQueue 方法,它的源碼定義以下:
<!-源碼目錄:src/batcher.js-->
function flushBatcherQueue () {
runBatcherQueue(queue)
runBatcherQueue(userQueue)
// user watchers triggered more watchers,
// keep flushing until it depletes
if (queue.length) {
return flushBatcherQueue()
}
// dev tool hook
/* istanbul ignore if */
if (devtools && config.devtools) {
devtools.emit('flush')
}
resetBatcherState()
}複製代碼
flushBatcherQueue 方法經過調用 runBatcherQueue 來 run Watcher。這裏咱們看到 Watcher 隊列分爲內部 queue 和 userQueue,其中 userQueue 是經過 $watch() 方法註冊的 Watcher。咱們優先 run 內部queue 來保證指令和 DOM 節點優先更新,這樣當用戶自定義的 Watcher 的回調函數觸發時 DOM 已更新完畢。接下來咱們看一下 runBatcherQueue 方法,它的源碼定義以下:
<!-源碼目錄:src/batcher.js-->
function runBatcherQueue (queue) {
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (let i = 0; i < queue.length; i++) {
var watcher = queue[i]
var id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > config._maxUpdateCount) {
warn(
'You may have an infinite update loop for watcher ' +
'with expression "' + watcher.expression + '"',
watcher.vm
)
break
}
}
}
queue.length = 0
}複製代碼
runBatcherQueued 的功能就是遍歷 queue 中 Watcher 的 run 方法。接下來咱們看一下 Watcher 的 run 方法,它的源碼定義以下:
<!-源碼目錄:src/watcher.js-->
Watcher.prototype.run = function () {
if (this.active) {
var 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; but only do so if this is a
// non-shallow update (caused by a vm digest).
((isObject(value) || this.deep) && !this.shallow)
) {
// set new value
var oldValue = this.value
this.value = value
// in debug + async mode, when a watcher callbacks
// throws, we also throw the saved before-push error
// so the full cross-tick stack trace is available.
var prevError = this.prevError
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' &&
config.debug && prevError) {
this.prevError = null
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
nextTick(function () {
throw prevError
}, 0)
throw e
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
this.queued = this.shallow = false
}
}複製代碼
Watcher.prototype.run 方法再次對 Watcher 求值,從新收集依賴。接下來判斷求值結果和以前 value 的關係。若是不變則什麼也不作,若是變了則調用 this.cb.call(this.vm, value, oldValue) 方法。這個方法是 Directive 實例建立 Watcher 時傳入的,它對應相關指令的 update 方法來真實更新 DOM。這樣就完成了數據更新到對應視圖的變化過程。 Watcher 巧妙地把 Observer 和 Directive 關聯起來,實現了數據一旦更新,視圖就會自動變化的效果。儘管 Vue.js 利用 Object.defineProperty 這個核心技術實現了數據和視圖的綁定,但仍然會存在一些數據變化檢測不到的問題,接下來咱們看一下這部份內容。
今天咱們就講到這裏,更多精彩內容關注咱們的書籍《Vue.js 權威指南》
歡迎關注DDFE
GITHUB:github.com/DDFE
微信公衆號:微信搜索公衆號「DDFE」或掃描下面的二維碼