https://ustbhuangyi.github.io/vue-analysis/v2/data-driven/new-vue.html
如下源码,VUe 初始化主要做了以下几件事情:
- 初始化内部组件,合并配置
- 初始化生命周期
- 初始化事件中心
- 初始化渲染
- 调用 beforeCreate 钩子
- 初始化props、methods、data、computed、watch 等
调用 create 钩子
export function initMixin (Vue: Class<Component>) {Vue.prototype._init = function (options?: Object) {const vm: Component = this// a uidvm._uid = uid++let startTag, endTag/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {startTag = `vue-perf-start:${vm._uid}`endTag = `vue-perf-end:${vm._uid}`mark(startTag)}// a flag to avoid this being observedvm._isVue = true// merge optionsif (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 {vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)}/* istanbul ignore else */if (process.env.NODE_ENV !== 'production') {initProxy(vm)} else {vm._renderProxy = vm}// expose real selfvm._self = vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(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)}}}
export function initEvents (vm: Component) {vm._events = Object.create(null)vm._hasHookEvent = false// init parent attached eventsconst listeners = vm.$options._parentListenersif (listeners) {updateComponentListeners(vm, listeners)}}
初始化生命周期
```javascript 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 = null vm._directInactive = false vm._isMounted = false vm._isDestroyed = false vm._isBeingDestroyed = false }
<a name="tdfoa"></a>## 初始化事件中心```javascriptexport function initState (vm: Component) {vm._watchers = []const opts = vm.$optionsif (opts.props) initProps(vm, opts.props)if (opts.methods) initMethods(vm, opts.methods)if (opts.data) {initData(vm)} else {observe(vm._data = {}, true /* asRootData */)}if (opts.computed) initComputed(vm, opts.computed)if (opts.watch && opts.watch !== nativeWatch) {initWatch(vm, opts.watch)}}
该部分核心源码在 src/core/vdom/helpers/update-listeners.js 中
export function updateListeners (on: Object,oldOn: Object,add: Function,remove: Function,createOnceHandler: Function,vm: Component) {let name, def, cur, old, eventfor (name in on) {def = cur = on[name]old = oldOn[name]event = normalizeEvent(name)/* istanbul ignore if */if (__WEEX__ && isPlainObject(def)) {cur = def.handlerevent.params = def.params}if (isUndef(cur)) {process.env.NODE_ENV !== 'production' && warn(`Invalid handler for event "${event.name}": got ` + String(cur),vm)} else if (isUndef(old)) {if (isUndef(cur.fns)) {cur = on[name] = createFnInvoker(cur, vm)}if (isTrue(event.once)) {cur = on[name] = createOnceHandler(event.name, cur, event.capture)}add(event.name, cur, event.capture, event.passive, event.params)} else if (cur !== old) {old.fns = curon[name] = old}}for (name in oldOn) {if (isUndef(on[name])) {event = normalizeEvent(name)remove(event.name, oldOn[name], event.capture)}}}
初始化渲染
export function initRender (vm: Component) {vm._vnode = null // the root of the child treevm._staticTrees = null // v-once cached treesconst options = vm.$optionsconst parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent treeconst renderContext = parentVnode && parentVnode.contextvm.$slots = resolveSlots(options._renderChildren, renderContext)vm.$scopedSlots = emptyObject// bind the createElement fn to this instance// so that we get proper render context inside it.// args order: tag, data, children, normalizationType, alwaysNormalize// internal version is used by render functions compiled from templatesvm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)// normalization is always applied for the public version, used in// user-written render functions.vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)// $attrs & $listeners are exposed for easier HOC creation.// they need to be reactive so that HOCs using them are always updatedconst parentData = parentVnode && parentVnode.data/* istanbul ignore else */if (process.env.NODE_ENV !== 'production') {defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)}, true)defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)}, true)} else {defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)}}
callHook
export function callHook (vm: Component, hook: string) {// #7573 disable dep collection when invoking lifecycle hookspushTarget()const handlers = vm.$options[hook]const info = `${hook} hook`if (handlers) {for (let i = 0, j = handlers.length; i < j; i++) {invokeWithErrorHandling(handlers[i], vm, null, vm, info)}}if (vm._hasHookEvent) {vm.$emit('hook:' + hook)}popTarget()}
初始化 prop
function initProps (vm: Component, propsOptions: Object) {const propsData = vm.$options.propsData || {}const props = vm._props = {}// cache prop keys so that future props updates can iterate using Array// instead of dynamic object key enumeration.const keys = vm.$options._propKeys = []const isRoot = !vm.$parent// root instance props should be convertedif (!isRoot) {toggleObserving(false)}for (const key in propsOptions) {keys.push(key)const value = validateProp(key, propsOptions, propsData, vm)/* istanbul ignore else */if (process.env.NODE_ENV !== 'production') {const hyphenatedKey = hyphenate(key)if (isReservedAttribute(hyphenatedKey) ||config.isReservedAttr(hyphenatedKey)) {warn(`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,vm)}defineReactive(props, key, value, () => {if (!isRoot && !isUpdatingChildComponent) {warn(`Avoid mutating a prop directly since the value will be ` +`overwritten whenever the parent component re-renders. ` +`Instead, use a data or computed property based on the prop's ` +`value. Prop being mutated: "${key}"`,vm)}})} else {defineReactive(props, key, value)}// static props are already proxied on the component's prototype// during Vue.extend(). We only need to proxy props defined at// instantiation here.if (!(key in vm)) {proxy(vm, `_props`, key)}}toggleObserving(true)}
初始化 methods
function initMethods (vm: Component, methods: Object) {const props = vm.$options.propsfor (const key in methods) {if (process.env.NODE_ENV !== 'production') {if (typeof methods[key] !== 'function') {warn(`Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +`Did you reference the function correctly?`,vm)}if (props && hasOwn(props, key)) {warn(`Method "${key}" has already been defined as a prop.`,vm)}if ((key in vm) && isReserved(key)) {warn(`Method "${key}" conflicts with an existing Vue instance method. ` +`Avoid defining component methods that start with _ or $.`)}}vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)}}
初始化 data
function initData (vm: Component) {let data = vm.$options.datadata = vm._data = typeof data === 'function'? getData(data, vm): data || {}if (!isPlainObject(data)) {data = {}process.env.NODE_ENV !== 'production' && warn('data functions should return an object:\n' +'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm)}// proxy data on instanceconst keys = Object.keys(data)const props = vm.$options.propsconst methods = vm.$options.methodslet i = keys.lengthwhile (i--) {const key = keys[i]if (process.env.NODE_ENV !== 'production') {if (methods && hasOwn(methods, key)) {warn(`Method "${key}" has already been defined as a data property.`,vm)}}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)} else if (!isReserved(key)) {proxy(vm, `_data`, key)}}// observe dataobserve(data, true /* asRootData */)}
初始化 computed
const computedWatcherOptions = { computed: true }function initComputed (vm: Component, computed: Object) {// $flow-disable-lineconst watchers = vm._computedWatchers = Object.create(null)// computed properties are just getters during SSRconst isSSR = isServerRendering()for (const key in computed) {const userDef = computed[key]const getter = typeof userDef === 'function' ? userDef : userDef.getif (process.env.NODE_ENV !== 'production' && getter == null) {warn(`Getter is missing for computed property "${key}".`,vm)}if (!isSSR) {// create internal watcher for the computed property.watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions)}// component-defined computed properties are already defined on the// component prototype. We only need to define computed properties defined// at instantiation here.if (!(key in vm)) {defineComputed(vm, key, userDef)} else if (process.env.NODE_ENV !== 'production') {if (key in vm.$data) {warn(`The computed property "${key}" is already defined in data.`, vm)} else if (vm.$options.props && key in vm.$options.props) {warn(`The computed property "${key}" is already defined as a prop.`, vm)} else if (vm.$options.methods && key in vm.$options.methods) {warn(`The computed property "${key}" is already defined as a method.`, vm)}}}}
函数首先创建 vm._computedWatchers 为一个空对象,接着对 computed 对象做遍历,拿到计算属性的每一个 userDef,然后尝试获取这个 userDef 对应的 getter 函数,拿不到则在开发环境下报警告。接下来为每一个 getter 创建一个 watcher,这个 watcher 和渲染 watcher 有一点很大的不同,它是一个 computed watcher,因为 const computedWatcherOptions = { computed: true }。computed watcher 和普通 watcher 的差别我稍后会介绍。最后对判断如果 key 不是 vm 的属性,则调用 defineComputed(vm, key, userDef),否则判断计算属性对于的 key 是否已经被 data 或者 prop 所占用,如果是的话则在开发环境报相应的警告。
那么接下来需要重点关注 defineComputed 的实现:
export function defineComputed (target: any,key: string,userDef: Object | Function) {const shouldCache = !isServerRendering()if (typeof userDef === 'function') {sharedPropertyDefinition.get = shouldCache? createComputedGetter(key): userDefsharedPropertyDefinition.set = noop} else {sharedPropertyDefinition.get = userDef.get? shouldCache && userDef.cache !== false? createComputedGetter(key): userDef.get: noopsharedPropertyDefinition.set = userDef.set? userDef.set: noop}if (process.env.NODE_ENV !== 'production' &&sharedPropertyDefinition.set === noop) {sharedPropertyDefinition.set = function () {warn(`Computed property "${key}" was assigned to but it has no setter.`,this)}}Object.defineProperty(target, key, sharedPropertyDefinition)}
这段逻辑很简单,其实就是利用 Object.defineProperty 给计算属性对应的 key 值添加 getter 和 setter,setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少,我们重点关注一下 getter 部分,缓存的配置也先忽略,最终 getter 对应的是 createComputedGetter(key) 的返回值,来看一下它的定义:
function createComputedGetter (key) {return function computedGetter () {const watcher = this._computedWatchers && this._computedWatchers[key]if (watcher) {watcher.depend()return watcher.evaluate()}}}
createComputedGetter 返回一个函数 computedGetter,它就是计算属性对应的 getter。
整个计算属性的初始化过程到此结束,我们知道计算属性是一个 computed watcher,它和普通的 watcher 有什么区别呢,为了更加直观,接下来来我们来通过一个例子来分析 computed watcher 的实现。
var vm = new Vue({data: {firstName: 'Foo',lastName: 'Bar'},computed: {fullName: function () {return this.firstName + ' ' + this.lastName}}})
当初始化这个 computed watcher 实例的时候,构造函数部分逻辑稍有不同:
constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: ?Object,isRenderWatcher?: boolean) {// ...if (this.computed) {this.value = undefinedthis.dep = new Dep()} else {this.value = this.get()}
可以发现 computed watcher 会并不会立刻求值,同时持有一个 dep 实例。
然后当我们的 render 函数执行访问到 this.fullName 的时候,就触发了计算属性的 getter,它会拿到计算属性对应的 watcher,然后执行 watcher.depend(),来看一下它的定义:
/*** Depend on this watcher. Only for computed property watchers.*/depend () {if (this.dep && Dep.target) {this.dep.depend()}}
注意,这时候的 Dep.target 是渲染 watcher,所以 this.dep.depend() 相当于渲染 watcher 订阅了这个 computed watcher 的变化。
然后再执行 watcher.evaluate() 去求值,来看一下它的定义
/*** Evaluate and return the value of the watcher.* This only gets called for computed property watchers.*/evaluate () {if (this.dirty) {this.value = this.get()this.dirty = false}return this.value}
evaluate 的逻辑非常简单,判断 this.dirty,如果为 true 则通过 this.get() 求值,然后把 this.dirty 设置为 false。在求值过程中,会执行 value = this.getter.call(vm, vm),这实际上就是执行了计算属性定义的 getter 函数,在我们这个例子就是执行了 return this.firstName + ‘ ‘ + this.lastName。
这里需要特别注意的是,由于 this.firstName 和 this.lastName 都是响应式对象,这里会触发它们的 getter,根据我们之前的分析,它们会把自身持有的 dep 添加到当前正在计算的 watcher 中,这个时候 Dep.target 就是这个 computed watcher。
最后通过 return this.value 拿到计算属性对应的值。我们知道了计算属性的求值过程,那么接下来看一下它依赖的数据变化后的逻辑。
一旦我们对计算属性依赖的数据做修改,则会触发 setter 过程,通知所有订阅它变化的 watcher 更新,执行 watcher.update() 方法:
/* istanbul ignore else */if (this.computed) {// A computed property watcher has two modes: lazy and activated.// It initializes as lazy by default, and only becomes activated when// it is depended on by at least one subscriber, which is typically// another computed property or a component's render function.if (this.dep.subs.length === 0) {// In lazy mode, we don't want to perform computations until necessary,// so we simply mark the watcher as dirty. The actual computation is// performed just-in-time in this.evaluate() when the computed property// is accessed.this.dirty = true} else {// In activated mode, we want to proactively perform the computation// but only notify our subscribers when the value has indeed changed.this.getAndInvoke(() => {this.dep.notify()})}} else if (this.sync) {this.run()} else {queueWatcher(this)}
那么对于计算属性这样的 computed watcher,它实际上是有 2 种模式,lazy 和 active。如果 this.dep.subs.length === 0 成立,则说明没有人去订阅这个 computed watcher 的变化,仅仅把 this.dirty = true,只有当下次再访问这个计算属性的时候才会重新求值。在我们的场景下,渲染 watcher 订阅了这个 computed watcher 的变化,那么它会执行:
this.getAndInvoke(() => {this.dep.notify()})getAndInvoke (cb: Function) {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 valueconst oldValue = this.valuethis.value = valuethis.dirty = falseif (this.user) {try {cb.call(this.vm, value, oldValue)} catch (e) {handleError(e, this.vm, `callback for watcher "${this.expression}"`)}} else {cb.call(this.vm, value, oldValue)}}}
getAndInvoke 函数会重新计算,然后对比新旧值,如果变化了则执行回调函数,那么这里这个回调函数是 this.dep.notify(),在我们这个场景下就是触发了渲染 watcher 重新渲染。
通过以上的分析,我们知道计算属性本质上就是一个 computed watcher,也了解了它的创建过程和被访问触发 getter 以及依赖更新的过程,其实这是最新的计算属性的实现,之所以这么设计是因为 Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化才会触发渲染 watcher 重新渲染,本质上是一种优化。
接下来我们来分析一下侦听属性 watch 是怎么实现的。
初始化 watch
function initWatch (vm: Component, watch: Object) {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)}}}
这里就是对 watch 对象做遍历,拿到每一个 handler,因为 Vue 是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,调用 createWatcher 方法,否则直接调用 createWatcher:
function createWatcher (vm: Component,expOrFn: string | Function,handler: any,options?: Object) {if (isPlainObject(handler)) {options = handlerhandler = handler.handler}if (typeof handler === 'string') {handler = vm[handler]}return vm.$watch(expOrFn, handler, options)}
这里的逻辑也很简单,首先对 hanlder 的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options) 函数,$watch 是 Vue 原型上的方法,它是在执行 stateMixin 的时候定义的:
Vue.prototype.$watch = function (expOrFn: string | Function,cb: any,options?: Object): Function {const vm: Component = thisif (isPlainObject(cb)) {return createWatcher(vm, expOrFn, cb, options)}options = options || {}options.user = trueconst watcher = new Watcher(vm, expOrFn, cb, options)if (options.immediate) {cb.call(vm, watcher.value)}return function unwatchFn () {watcher.teardown()}}
也就是说,侦听属性 watch 最终会调用 $watch 方法,这个方法首先判断 cb 如果是一个对象,则调用 createWatcher 方法,这是因为 $watch 方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行 const watcher = new Watcher(vm, expOrFn, cb, options) 实例化了一个 watcher,这里需要注意一点这是一个 user watcher,因为 options.user = true。通过实例化 watcher 的方式,一旦我们 watch 的数据发送变化,它最终会执行 watcher 的 run 方法,执行回调函数 cb,并且如果我们设置了 immediate 为 true,则直接会执行回调函数 cb。最后返回了一个 unwatchFn 方法,它会调用 teardown 方法去移除这个 watcher。
所以本质上侦听属性也是基于 Watcher 实现的,它是一个 user watcher。
Watcher 的构造函数对 options 做的了处理,代码如下:
if (options) {this.deep = !!options.deepthis.user = !!options.userthis.computed = !!options.computedthis.sync = !!options.sync// ...} else {this.deep = this.user = this.computed = this.sync = false}
所以 Watcher 总共有四种类型:
- deep Watcher
- user Watcher
- computed Watcher
- sync Watcher
deep Watcher
通常,如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:
这个时候是不会 log 任何数据的,因为我们是 watch 了 a 对象,只触发了 a 的 getter,并没有触发 a.b 的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。var vm = new Vue({data() {a: {b: 1}},watch: {a: {handler(newVal) {console.log(newVal)}}}})vm.a.b = 2
而我们只需要对代码做稍稍修改,就可以观测到这个变化了
这样就创建了一个 deep watcher 了,在 watcher 执行 get 求值的过程中有一段逻辑:watch: {a: {deep: true,handler(newVal) {console.log(newVal)}}}
在对 watch 的表达式或者函数求值后,会调用 traverse 函数,它的定义在 src/core/observer/traverse.js 中: ```javascript import { _Set as Set, isObject } from ‘../util/index’ import type { SimpleSet } from ‘../util/index’ import VNode from ‘../vdom/vnode’get() {let value = this.getter.call(vm, vm)// ...if (this.deep) {traverse(value)}}
const seenObjects = new Set()
/**
- Recursively traverse an object to evoke all converted
- getters, so that every nested property inside the object
- is collected as a “deep” dependency. */ export function traverse (val: any) { _traverse(val, seenObjects) seenObjects.clear() }
function traverse (val: any, seen: SimpleSet) { let i, keys const isA = Array.isArray(val) if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } if (val.ob) { const depId = val._ob.dep.id if (seen.has(depId)) { return } seen.add(depId) } if (isA) { i = val.length while (i—) _traverse(val[i], seen) } else { keys = Object.keys(val) i = keys.length while (i—) _traverse(val[keys[i]], seen) } }
traverse 的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id 记录到 seenObjects,避免以后重复访问。<br />那么在执行了 traverse 后,我们再对 watch 的对象内部任何一个值做修改,也会调用 watcher 的回调函数了。<br />对 deep watcher 的理解非常重要,今后工作中如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep 为 true,但是因为设置了 deep 后会执行 traverse 函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。<a name="UEvDz"></a>### user Watcher前面我们分析过,通过 vm.$watch 创建的 watcher 是一个 user watcher,其实它的功能很简单,在对 watcher 求值以及在执行回调函数的时候,会处理一下错误,如下:```javascriptget() {if (this.user) {handleError(e, vm, `getter for watcher "${this.expression}"`)} else {throw e}},getAndInvoke() {// ...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)}}
handleError 在 Vue 中是一个错误捕获并且暴露给用户的一个利器。
computed Watcher
computed watcher 几乎就是为计算属性量身定制的,我们刚才已经对它做了详细的分析,这里不再赘述了。
sync Watcher
不把更新watcher放到nextTick队列 而是立即执行更新
在我们之前对 setter 的分析过程知道,当响应式数据发送变化后,触发了 watcher.update(),只是把这个 watcher 推送到一个队列中,在 nextTick 后才会真正执行 watcher 的回调函数。而一旦我们设置了 sync,就可以在当前 Tick 中同步执行 watcher 的回调函数。
update () {if (this.computed) {// ...} else if (this.sync) {this.run()} else {queueWatcher(this)}}
只有当我们需要 watch 的值的变化到执行 watcher 的回调函数是一个同步过程的时候才会去设置该属性为 true。
immediate Watcher
将实例作为参数传入立即执行回调函数。
参考链接
https://ustbhuangyi.github.io/vue-analysis/v2/reactive/computed-watcher.html#watch
