大概过了一遍 util 工具类后,开始看 Vue 实例的具体实现
原文地址
项目地址
init
src/instance/init.js 实现了 Vue 的 _init 初始化函数
import { mergeOptions } from '../../util/index'
let uid = 0
export default function (Vue) {
Vue.prototype._init = function (options) {
...
}
}
_init 方法会在实例创建的时候被调用:
function Vue(options) {
this._init(options);
}
init 初始化了 Vue 实例的共有属性如 $el, $parent, $root, $children, $refs, $els
还有一堆私有属性如_watchers, _directives, _uid, isVue, _events
等等,最后再是初始化实例状态、事件、生命周期等等
在实现 $root 上比较有趣:
this.$parent = options.parent
this.$root = this.$parent
? this.$parent.$root
: this
state
src/instance/state.js
/**
* Accessor for `$data` property, since setting $data
* requires observing the new object and updating
* proxied properties.
*/
Object.defineProperty(Vue.prototype, '$data', {
get () {
return this._data
},
set (newData) {
if (newData !== this._data) {
this._setData(newData)
}
}
})
_setData
使用 defineProperty 来实现对 Vue.$data 的 get 和 set
Vue.prototype._setData = function (newData) {
newData = newData || {}
var oldData = this._data
this._data = newData
var keys, key, i
// unproxy keys not present in new data
keys = Object.keys(oldData)
i = keys.length
while (i--) {
key = keys[i]
if (!(key in newData)) {
this._unproxy(key)
}
}
// proxy keys not already proxied,
// and trigger change for changed values
keys = Object.keys(newData)
i = keys.length
while (i--) {
key = keys[i]
if (!hasOwn(this, key)) {
// new property
this._proxy(key)
}
}
oldData.__ob__.removeVm(this)
observe(newData, this)
this._digest()
}
_setData 方法利用 Object.keys 获取对象的属性列表,在利用 key in obj
来判断是否存在属性 key,然后决定是否 proxy 或者 unproxy
_initComputed
/**
* Setup computed properties. They are essentially
* special getter/setters
*/
function noop () {}
Vue.prototype._initComputed = function () {
var computed = this.$options.computed
if (computed) {
for (var key in computed) {
var userDef = computed[key]
var def = {
enumerable: true,
configurable: true
}
if (typeof userDef === 'function') {
def.get = makeComputedGetter(userDef, this)
def.set = noop
} else {
def.get = userDef.get
? userDef.cache !== false
? makeComputedGetter(userDef.get, this)
: bind(userDef.get, this)
: noop
def.set = userDef.set
? bind(userDef.set, this)
: noop
}
Object.defineProperty(this, key, def)
}
}
}
function makeComputedGetter (getter, owner) {
var watcher = new Watcher(owner, getter, null, {
lazy: true
})
return function computedGetter () {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
初始化计算属性,即 computed 的实现,从文档可以看到我们既可以使用 function 来确定怎么获取某个值,也可以使用 get 和 set 对象来确定值的获取和更新,底层的实现是 watcher
methods
/**
* Setup instance methods. Methods must be bound to the
* instance since they might be passed down as a prop to
* child components.
*/
Vue.prototype._initMethods = function () {
var methods = this.$options.methods
if (methods) {
for (var key in methods) {
this[key] = bind(methods[key], this)
}
}
}
/**
* Initialize meta information like $index, $key & $value.
*/
Vue.prototype._initMeta = function () {
var metas = this.$options._meta
if (metas) {
for (var key in metas) {
defineReactive(this, key, metas[key])
}
}
}
watcher
import { extend, warn, isArray, isObject, nextTick } from './util/index'
import config from './config'
import Dep from './observer/dep'
import { parseExpression } from './parsers/expression'
import { pushWatcher } from './batcher'
let uid = 0
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 = isFn ? expOrFn.toString() : expOrFn
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = Object.create(null)
this.newDeps = null
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
}
/**
* Add a dependency to this directive.
*
* @param {Dep} dep
*/
Watcher.prototype.addDep = function (dep) {
var id = dep.id
if (!this.newDeps[id]) {
this.newDeps[id] = dep
if (!this.deps[id]) {
this.deps[id] = dep
dep.addSub(this)
}
}
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
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 + '". ' +
(config.debug
? ''
: 'Turn on debug mode to see stack trace.'
), e
)
}
}
// "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
}
/**
* Set the corresponding value with the setter.
*
* @param {*} value
*/
Watcher.prototype.set = function (value) {
var scope = this.scope || this.vm
if (this.filters) {
value = scope._applyFilters(
value, this.value, this.filters, true)
}
try {
this.setter.call(scope, scope, value)
} catch (e) {
if (
process.env.NODE_ENV !== 'production' &&
config.warnExpressionErrors
) {
warn(
'Error when evaluating setter "' +
this.expression + '"', e
)
}
}
// two-way sync for v-for alias
var forContext = scope.$forContext
if (forContext && forContext.alias === this.expression) {
if (forContext.filters) {
process.env.NODE_ENV !== 'production' && warn(
'It seems you are using two-way binding on ' +
'a v-for alias (' + this.expression + '), and the ' +
'v-for has filters. This will not work properly. ' +
'Either remove the filters or use an array of ' +
'objects and bind to object properties instead.'
)
return
}
forContext._withLock(function () {
if (scope.$key) { // original is an object
forContext.rawValue[scope.$key] = value
} else {
forContext.rawValue.$set(scope.$index, value)
}
})
}
}
/**
* Prepare for dependency collection.
*/
Watcher.prototype.beforeGet = function () {
Dep.target = this
this.newDeps = Object.create(null)
}
/**
* Clean up for dependency collection.
*/
Watcher.prototype.afterGet = function () {
Dep.target = null
var ids = Object.keys(this.deps)
var i = ids.length
while (i--) {
var id = ids[i]
if (!this.newDeps[id]) {
this.deps[id].removeSub(this)
}
}
this.deps = this.newDeps
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*
* @param {Boolean} shallow
*/
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)
}
}
/**
* Batcher job interface.
* Will be called by the batcher.
*/
Watcher.prototype.run = function () {
if (this.active) {
var value = this.get()
if (
value !== this.value ||
// Deep watchers and Array watchers 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).
((isArray(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
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
Watcher.prototype.evaluate = function () {
// avoid overwriting another watcher that is being
// collected.
var current = Dep.target
this.value = this.get()
this.dirty = false
Dep.target = current
}
/**
* Depend on all deps collected by this watcher.
*/
Watcher.prototype.depend = function () {
var depIds = Object.keys(this.deps)
var i = depIds.length
while (i--) {
this.deps[depIds[i]].depend()
}
}
/**
* Remove self from all dependencies' subcriber list.
*/
Watcher.prototype.teardown = function () {
if (this.active) {
// remove self from vm's watcher list
// we can skip this if the vm if being destroyed
// which can improve teardown performance.
if (!this.vm._isBeingDestroyed) {
this.vm._watchers.$remove(this)
}
var depIds = Object.keys(this.deps)
var i = depIds.length
while (i--) {
this.deps[depIds[i]].removeSub(this)
}
this.active = false
this.vm = this.cb = this.value = null
}
}
/**
* Recrusively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*
* @param {*} val
*/
function traverse (val) {
var i, keys
if (isArray(val)) {
i = val.length
while (i--) traverse(val[i])
} else if (isObject(val)) {
keys = Object.keys(val)
i = keys.length
while (i--) traverse(val[keys[i]])
}
}