大概過了一遍 util 工具類後,開始看 Vue 實例的具體實現vue
src/instance/init.js 實現了 Vue 的 _init 初始化函數github
import { mergeOptions } from '../../util/index' let uid = 0 export default function (Vue) { Vue.prototype._init = function (options) { ... } }
_init 方法會在實例建立的時候被調用:express
function Vue(options) { this._init(options); }
init 初始化了 Vue 實例的共有屬性如 $el, $parent, $root, $children, $refs, $els
還有一堆私有屬性如_watchers, _directives, _uid, isVue, _events
等等,最後再是初始化實例狀態、事件、生命週期等等app
在實現 $root 上比較有趣:async
this.$parent = options.parent this.$root = this.$parent ? this.$parent.$root : this
src/instance/state.jside
/** * 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) } } })
使用 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工具
/** * 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 對象來肯定值的獲取和更新,底層的實現是 watcheroop
/** * 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]) } } }
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]]) } }