原始碼版本為2.0.0
接前文。
前文講到下面五個函式擴充套件了Vue的原型
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
複製程式碼
我畫了一個圖,是執行這幾個mixin之後,Vue原型掛載的方法
一個簡單的例子
window.app = new Vue({
data: {
msg: 'hello world',
},
render (h) {
return h('p', this.msg)
}
}).$mount('#root')
setTimeout(() => {
app.msg = 'hi world'
}, 2000)
複製程式碼
毫無疑問螢幕上會先渲染hello world,隔兩秒後變為hi world。
本文將從這個簡單的例子追本溯源,看看Vue究竟做了什麼。
init
我們沿著執行順序一步一步的看,上文已經找到了Vue的建構函式如下:
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
複製程式碼
所以執行new Vue()
的時候,例項(vm)會首先執行初始化方法vm._init(),_init方法如下:
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// console.log(resolveConstructorOptions(vm))
vm.$options = mergeOptions(
resolveConstructorOptions(vm),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
initRender(vm)
}
複製程式碼
由於本文是初步探索Vue,所以並沒有涉及到
元件
這個概念,但是我拷貝過來的程式碼中會經常出現與元件邏輯相關的程式碼,直接略過即可。
執行初始化操作首先給例項新增了幾個私有屬性,然後merge了options,vm.$options最終變為這樣
vm.$options = {
components: [..],
directives: [],
filters: [],
vm: vm,
data: {},
render: function() {}
}
複製程式碼
真正重要的操作是下面的幾個init函式
initLifecycle(vm) 初始化生命週期
initEvents(vm) 初始化事件系統(這裡面做的是父子元件通訊的工作,所以這篇文章暫時略過)
callHook(vm, 'beforeCreate') 執行beforeCreate鉤子
initState(vm) 初始化狀態(包括data、computed、methods、watch)
callHook(vm, 'created') 執行created鉤子
initRender(vm) 渲染頁面
複製程式碼
從上面可以看到,created鉤子執行的時機是在資料被observe之後(此時資料還沒有收集依賴)。看一下callHook函式:
export function callHook (vm: Component, hook: string) {
const handlers = vm.$options[hook]
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
handlers[i].call(vm)
}
}
vm.$emit('hook:' + hook)
}
複製程式碼
handle中的this繫結了vm
下面依次分析幾個初始化函式做的工作
initLifecycle
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
複製程式碼
這裡沒什麼好說的,vm._watcher和vm._isMounted後面會用到
initEvents
這裡做的是父子元件通訊的相關工作,不在本篇的討論範圍內。
initState
export function initState (vm: Component) {
vm._watchers = []
initProps(vm)
initData(vm)
initComputed(vm)
initMethods(vm)
initWatch(vm)
}
複製程式碼
initProps也是元件相關,因此剩下四個是我們關心的。核心initData完成了資料的observe
1) initData
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? data.call(vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object.',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
let i = keys.length
while (i--) {
// data中的欄位不能和props中的重複
if (props && hasOwn(props, keys[i])) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${keys[i]}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else {
// 代理
proxy(vm, keys[i])
}
}
// observe data
observe(data)
data.__ob__ && data.__ob__.vmCount++
}
複製程式碼
首先代理data裡面的欄位:
在vue中通常這樣訪問一個值
this.msg
而不是
this._data.msg
複製程式碼
正是因為proxy(vm, keys[i])
已經對key值做了代理,如下:
function proxy (vm: Component, key: string) {
if (!isReserved(key)) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
// 訪問vm[key]返回的事實上是vm._data[key]
return vm._data[key]
},
set: function proxySetter (val) {
// 設定vm[key]事實上給vm._data[key]賦值
vm._data[key] = val
}
})
}
}
複製程式碼
接下來就是對資料observe(本文暫不考慮陣列),資料的observe可以說是Vue的核心,網上很多文章已經介紹的十分詳細,這裡我把observe簡化一下如下:
export function observe (value) {
if (!isObject(value)) {
return
}
let ob = new Observer(value)
return ob
}
export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
this.walk(value)
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
export function defineReactive (obj, key, val) {
const dep = new Dep()
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 取值時給資料新增依賴
get: function reactiveGetter () {
const value = val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
return value
},
// 賦值時通知資料依賴更新
set: function reactiveSetter (newVal) {
const value = val
if (newVal === value) {
return
}
val = newVal
childOb = observe(newVal)
dep.notify()
}
})
}
複製程式碼
整個響應式系統的核心在於defineReactive這個函式,利用了一個閉包把資料的依賴收集起來,下文我們會看到Dep.target事實上是一個個watcher。
這裡有個需要注意的地方:
if (childOb) {
childOb.dep.depend()
}
複製程式碼
為什麼閉包裡的dep已經收集過了依賴,這裡還要加上這句程式碼?先看一個例子
data: {
name: {
first: 'zhang'
}
}
複製程式碼
假如資料是這樣,我們這樣改變資料
this.name.last = 'san'
複製程式碼
想一下這樣會出發依賴更新嗎?事實上是不會的,因為last並沒有被監聽。Vue給我們指明瞭正確的姿勢是:
this.$set('name', 'last', 'san')
複製程式碼
來看一下set的原始碼(為方便,我已把陣列相關的程式碼刪掉)
export function set (obj: Array<any> | Object, key: any, val: any) {
if (hasOwn(obj, key)) {
obj[key] = val
return
}
const ob = obj.__ob__
if (!ob) {
obj[key] = val
return
}
// 對新增的屬性進行監聽
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
複製程式碼
想一下,this.name變化時講道理是應該通知閉包內name的依賴更新,但是由於新增屬性並不會觸發defineReactive,而this.name.__ob__的依賴和name屬性的依賴是相同的,所以this.name.ob.notify()可達到相同的效果,這也是上面childOb.dep.depend()的原因。同理del也是如此:
export function del (obj: Object, key: string) {
const ob = obj.__ob__
if (!hasOwn(obj, key)) {
return
}
delete obj[key]
if (!ob) {
return
}
ob.dep.notify()
}
複製程式碼
2)initWatch
function initWatch (vm: Component) {
const watch = vm.$options.watch
if (watch) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
}
function createWatcher (vm: Component, key: string, handler: any) {
let options
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
vm.$watch(key, handler, options)
}
複製程式碼
可以看出來initWatch最終呼叫的是$watch
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: Function,
options?: Object
): Function {
const vm: Component = this
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
複製程式碼
最終例項化了一個Watcher,watcher可以分為兩種,一種是使用者定義的(我們在例項化Vue時傳入的watch選項),一種是Vue內部自己例項化的,後文會看到。
watcher的程式碼如下:
export default class Watcher {
constructor (vm, expOrFn, cb, options) {
this.vm = vm
vm._watchers.push(this)
// options
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.expression = expOrFn.toString()
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()
// parse expression for getter
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
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
const value = this.getter.call(this.vm, this.vm)
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
/**
* Add a dependency to this directive.
*/
addDep (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)
}
}
}
/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
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) {
process.env.NODE_ENV !== 'production' && warn(
`Error in watcher "${this.expression}"`,
this.vm
)
/* istanbul ignore else */
if (config.errorHandler) {
config.errorHandler.call(null, e, this.vm)
} else {
throw e
}
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
evaluate () {
this.value = this.get()
this.dirty = false
}
/**
* Depend on all deps collected by this watcher.
*/
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subcriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed or is performing a v-for
// re-render (the watcher list is then filtered by v-for).
if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
複製程式碼
程式碼蠻長的,慢慢看
watcher例項有一個getter方法,我們上文提到過watcher有兩種,當watcher是使用者建立時,此時的expOrFn就是一個expression,例如name
或者name.first
,此時它會被parsePath格式化為一個取值函式
const bailRE = /[^\w\.\$]/
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
} else {
const segments = path.split('.')
// obj為vue例項時 輸出的便是
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
}
複製程式碼
格式化完getter函式之後緊接著執行get方法,資料的依賴正是在watcher的get方法執行時收集的,可以說get是連線observer和watcher的橋樑
get () {
pushTarget(this)
const value = this.getter.call(this.vm, this.vm)
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
複製程式碼
get方法裡面執行了getter,前面已經說過getter是一個取值函式,這不禁令我們聯想到了資料的監聽,當取值時假如Dep.target存在那麼就可以收集依賴了,想想就激動。既然這樣,pushTarget
和popTarget
必然是定義Dep.target
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()
}
複製程式碼
如我們所想,pushTarget和popTarget定義了全域性唯一的Dep.target(即呼叫get的watcher)。這裡是需要思考的,原始碼的寫法顯然表明當getter函式呼叫時可能會觸發其他watcher的get方法,事實上當我們watch一個計算屬性或者渲染一個計算屬性時便會出現這種情況,我們本篇暫不討論。
getter執行後,data相應閉包中的dep會執行dep.depend()
,最終watcher會被新增到dep的訂閱subs
中,但data中的資料改變時,相應閉包中dep會notify
它的subs(即watcher)依次update
,最終呼叫watcher的run方法實現更新,看一下run方法:
run () {
if (this.active) {
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) {
process.env.NODE_ENV !== 'production' && warn(
`Error in watcher "${this.expression}"`,
this.vm
)
/* istanbul ignore else */
if (config.errorHandler) {
config.errorHandler.call(null, e, this.vm)
} else {
throw e
}
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
複製程式碼
run方法執行的時候會首先執行get方法,然後比較新的value的舊的value,如果不相同就執行watcher.cb
。至此Vue的響應式雛形基本完成。
3)initComputed
先看程式碼(簡化了)
function initComputed (vm) {
const computed = vm.$options.computed
if (computed) {
for (const key in computed) {
const userDef = computed[key]
computedSharedDefinition.get = makeComputedGetter(userDef, vm)
Object.defineProperty(vm, key, computedSharedDefinition)
}
}
}
function makeComputedGetter (getter, owner) {
const watcher = new Watcher(owner, getter, noop, {
lazy: true
})
return function computedGetter () {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
複製程式碼
從程式碼可以看到,計算屬性的值就是與之相關watcher的value。注意這裡options的lazy為true,這表明建立watcher(稱為a)的時候並不會執行get方法,也就是不會收集依賴。只有當我們取計算屬性的值的時候才會收集依賴,那麼什麼時候會取計算屬性的值呢?比如watch計算屬性或者把計算屬性寫進render函式中。因為此get是惰性的,所以依賴於其他watcher(稱為b)的喚醒,當執行完watcher.evaluate()
之後,會把a新增到計算屬性依賴資料dep的subs中,當執行完watcher.depend()
之後,會把這個b新增到計算屬性依賴資料dep的subs中。當依賴資料變化時,a和b(至少有這兩個)watcher均會update,並且a的update是靠前的,因為其id在前面,所以當b進行update時獲取到的計算屬性為更新後的。
這裡比較繞,多想想吧。
initMethods
function initMethods (vm: Component) {
const methods = vm.$options.methods
if (methods) {
for (const key in methods) {
if (methods[key] != null) {
vm[key] = bind(methods[key], vm)
} else if (process.env.NODE_ENV !== 'production') {
warn(`Method "${key}" is undefined in options.`, vm)
}
}
}
}
複製程式碼
這個沒什麼好說的,就是將方法掛載到例項上。
initRender
initRender裡面執行了首次渲染。
在進行下面的內容之前我們先說明一下例項的_render方法,這個方法是根據render函式返回虛擬dom,什麼是所謂的虛擬dom,看下Vue文件的解釋:
它所包含的資訊會告訴 Vue 頁面上需要渲染什麼樣的節點,及其子節點。我們把這樣的節點描述為“虛擬節點 (Virtual Node)”,也常簡寫它為“VNode”。“虛擬 DOM”是我們對由 Vue 元件樹建立起來的整個 VNode 樹的稱呼。
至於vnode的生成原理不在本文的討論範圍。
進入正題,看下initRender的程式碼:
export function initRender (vm: Component) {
// 對於元件適用 其在父樹的佔位
vm.$vnode = null // the placeholder node in parent tree
// 虛擬dom
vm._vnode = null // the root of the child tree
vm._staticTrees = null
vm._renderContext = vm.$options._parentVnode && vm.$options._parentVnode.context
vm.$slots = resolveSlots(vm.$options._renderChildren, vm._renderContext)
// bind the public createElement fn to this instance
// so that we get proper render context inside it.
// 這就是render函式裡面我們傳遞的那個引數
// 它的作用是生成vnode(虛擬dom)
vm.$createElement = bind(createElement, vm)
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
複製程式碼
initRender執行了例項的$mount,而$mount實際上是呼叫的內部方法_mount
,現在來看_mount(簡化了)
Vue.prototype._mount = function (el, hydrating) {
const vm = this
vm.$el = el
callHook(vm, 'beforeMount')
vm._watcher = new Watcher(vm, () => {
vm._update(vm._render(), hydrating)
}, noop)
hydrating = false
// root instance, call mounted on self
// mounted is called for child components in its inserted hook
// 假如vm是根例項 那麼其$root屬性就是其自身
if (vm.$root === vm) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
複製程式碼
_mount給我們提供了beforeMount
和mounted
兩個鉤子,可想而知例項化watcher的時候已經生成了虛擬dom,並且根據虛擬dom建立了真實dom並掛載到了頁面上。
上文我們已經講過watcher的建立過程,所以可知vm._watcher的getter函式即為
() => {
vm._update(vm._render(), hydrating)
}
複製程式碼
並且此watcher的get並非為惰性get,所以watcher例項化之後便會立即執行get方法,事實上是執行vm._render()
,並將獲得的vnode作為引數傳給vm._update
執行。思考一下_render()函式執行時會發生什麼,顯然會獲取data的值,此時便會觸發get攔截器,從而將vm._watcher新增至對應dep的subs中。
vm._update程式碼如下(簡化了):
Vue.prototype._update = function (vnode, hydrating) {
const vm = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevVnode = vm._vnode
vm._vnode = vnode
if (!prevVnode) {
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// 如果之前的虛擬dom不存在 說明是首次掛載
vm.$el = vm.__patch__(vm.$el, vnode, hydrating)
} else {
// 之前的虛擬dom存在 需要先對新舊虛擬dom對比 然後差異化更新
vm.$el = vm.__patch__(prevVnode, vnode)
}
if (vm._isMounted) {
callHook(vm, 'updated')
}
}
複製程式碼
可以看到_update的主要作用就是根據vnode形成真實dom節點。當data資料改變時,對應的dep會通知subs即vm._watcher進行update,update方法中會再次執行vm._watcher.get(),從而呼叫vm._update進行試圖的更新。
這裡有個地方值得我們思考,更新後的檢視可能不再依賴於上次的資料了,什麼意思呢
更新前 <p>{{this.a}}</p>
更新後 <p>{{this.b}}</p>
複製程式碼
也就是說需要清除掉a資料中watcher的依賴。看下Vue中的實現
dep.depend
並沒有我們想的那麼簡單,如下
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
複製程式碼
相應的watcher的addDep如下,他會把本次更新依賴的dep的id存起來,如果更新前的id列表不存在新的dep的id,說明檢視更新後依賴於這個dep,於是將vm._watcher新增到此dep的subs中
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
dep.addSub(this)
}
}
}
複製程式碼
假如之前dep的id列表存在存在某些id,這些id不存在與更新後dep的id列表,表明更新後的檢視不在依賴於這些id對應的dep,那麼需要將vm._watcher從這些dep中移除,這部分工作是在cleanupDeps
中完成的,如下:
cleanupDeps () {
let i = this.deps.length
// console.log(i)
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
複製程式碼
結語
這篇文章只是對Vue內部實現機制的簡單探索,很多地方沒有涉及到,比如元件機制、模板的編譯、虛擬dom樹的建立等等,希望這些能在以後慢慢搞清楚。