其他章节请看:
vue 快速入门 系列
书接上文,每次调用 new Vue() 都会执行 Vue.prototype._init() 方法。倘若你看过 jQuery 的源码,你会发现每次调用 jQuery() 也会执行一个初始化的方法(即 jQuery.fn.init())。两者在执行初始化方法后都会返回一个实例(vue 实例或 jQuery 实例),而且在初始化过程中,都会做许多事情。本篇就和大家一起来看一下 vue 实例的初始化过程。
Tip:本篇亦叫 Vue.prototype._init() 的源码解读。解读顺序和源码的顺序保持一致。
function Vue (options) { ... this._init(options)}// 核心代码Vue.prototype._init = function (options?: Object) { const vm: Component = this // 合并参数 if (options && options._isComponent) { initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // 初始化生命周期、初始化事件... initLifecycle(vm) initEvents(vm) initRender(vm) // 触发生命钩子:beforeCreate callHook(vm, 'beforeCreate') // resolve injections before data/props // 我们可以使用的顺序:inject -> data/props -> provide initInjections(vm) initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') if (vm.$options.el) { // 挂载 vm.$mount(vm.$options.el) } }export function initLifecycle (vm: Component) { const options = vm.$options // locate first non-abstract parent // 定位第一个非抽象父节点 let parent = options.parent // 有 parent,并且自己不是抽象的,则找到最近一级的非抽象 parent,并将自己放入其 $children 数组中 if (parent && !options.abstract) { // 如果 parent 是抽象的(abstract),则继续往上级找 parent,直到 parent 不是抽象的为止 while (parent.$options.abstract && parent.$parent) { parent = parent.$parent } // 将 vm 放入 parent 中 parent.$children.push(vm) } // 初始化实例 property:vm.$parent、vm.$root、$children、vm.$refs vm.$parent = parent // 父实例,如果当前实例有的话 vm.$root = parent ? parent.$root : vm // 当前组件树的根 Vue 实例 vm.$children = [] // 当前实例的直接子组件 vm.$refs = {} // 一个对象,持有注册过 ref attribute 的所有 DOM 元素和组件实例。 // 以下划线(_)开头的应该是私有实例属性 vm._watcher = null vm._inactive = null vm._directInactive = false vm._isMounted = false vm._isDestroyed = false vm._isBeingDestroyed = false}initLifecycle() 方法做了一下几件事:
这个方法所做的事太简单了!和我的猜测不一致。
最初我认为初始化生命周期(initLifecycle()),应该和官网的生命周期图相关。现在在来看一下这张图,发现 _init() 中的代码仅仅对应这张图的前一半而已。
export function initEvents (vm: Component) { // 创建一个没有原型的对象,赋值给 _events vm._events = Object.create(null) // 是否有钩子事件 vm._hasHookEvent = false // init parent attached events // 初始化父组件附加的事件 const listeners = vm.$options._parentListeners if (listeners) { // 更新组件监听器 updateComponentListeners(vm, listeners) }}initEvents() 好像没干啥事。
但一个方法总得干点事,所以如果实在要说这个函数哪里做了点事,应该就和 _parentListeners 有关。
export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object) { target = vm // 更新监听器 // 第一个参数是父组件的监听器,第二个是父组件监听器的老版本 // 之后就是 add、remove,很简单,即注册事件和删除事件 updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm) target = undefined}initEvents() 做的事情就是,更新父组件给子组件注册的事件。这里有两个关键词:父子组件、注册。
Tip:updateListeners() 就两个逻辑:
listeners。如果老的版本上没有定义,说明是新增。里面用 on;新老版本不一致,以新版本为准。oldListeners。新的版本没有定义,说明删除了。里面用 removeexport function updateListeners ( on: Object, oldOn: Object, add: Function, remove: Function, createOnceHandler: Function, vm: Component) { let name, def, cur, old, event // 遍历新的监听器 for (name in on) { def = cur = on[name] old = oldOn[name] event = normalizeEvent(name) ... if (isUndef(cur)) { ... // 老的版本上没有定义,说明是新增。里面用 on } 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 = cur on[name] = old } } // 遍历旧的监听器 for (name in oldOn) { // 新的版本没有定义,说明删除了。里面用 remove if (isUndef(on[name])) { event = normalizeEvent(name) remove(event.name, oldOn[name], event.capture) } }}我们通过一个实验来了解一下父组件和其中的子组件的创建过程。
定义父子两个组件,并都有4个生命周期钩子函数 beforeCreate、created、beforeMount、mounted:
// WelComeButton.vue - 子组件<template> <div> <button v-on:click="$emit('welcome')">Click me to be welcomed</button> </div></template><script>export default { beforeCreate () { console.log('beforeCreate') }, created () { console.log('created') }, beforeMount () { console.log('beforeMount') }, mounted () { console.log('mounted') }}</script>// About.vue - 父组件<template> <div > <welcome-button></welcome-button> </div></template><script>import WelcomeButton from './WelComeButton.vue'export default { components: { WelcomeButton }, beforeCreate () { console.log('parent beforeCreate') }, created () { console.log('parent created') }, beforeMount () { console.log('parent beforeMount') }, mounted () { console.log('parent mounted') }}</script>// 浏览器输出parent beforeCreateparent createdparent beforeMountbeforeCreatecreatedbeforeMountmountedparent mounted父子组件的创建过程如下:
于是我们知道包含子组件的组件,它的落地(更新到真实 dom)过程:
提到注册事件,我们会想到 v-on。
v-on 用在普通元素上时,监听原生 DOM 事件。用在自定义元素组件上时,监听子组件触发的自定义事件。
// v-on 用于普通元素<button v-on:click="greet">Greet</button>// v-on 用于自定义元素组件<div id='app'> <!-- 父组件给子组件注册了事件 chang-count,事件的回调方法是 changCount --> <button-counter v-on:chang-count='changCount'></button-counter></div>我们通常使用模板,并在其上注册事件,模板会编译生成渲染函数,接着就到了虚拟 DOM,每次执行渲染函数都会生成一份新的 vNode,新的 vNode 和旧的 vNode 对比,查找出需要更新的dom 节点,最后就更新 dom。这个过程会创建一些元素,此时才会去判断到底是组件还是原生的元素(或平台标签)。
为什么得在创建元素的时候才去判断到底是组件还是原生的元素?
笔者猜测:在前面做这个判断从技术上是可以做到的,因为这个逻辑(组件 or 原生的元素)判断不复杂;所以另一种可能就是将这个逻辑放在更新 dom 的时候更加合理。如果是普通元素,直接创建,如果是组件,则创建组件,如果包含子组件,则先创建(或实例化)子组件,并会传递一些参数,其中就包含通过 v-on 注册的事件。
export function initRender (vm: Component) { // 重置子树的根 vm._vnode = null // 重置 _staticTrees。 // once 只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。 vm._staticTrees = null // v-once cached trees // 用于当前 Vue 实例的初始化选项 const options = vm.$options // 父树中的占位符节点 const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree // 渲染上下文,即父节点的上下文 const renderContext = parentVnode && parentVnode.context // vm.$slots,用来访问被插槽分发的内容 vm.$slots = resolveSlots(options._renderChildren, renderContext) // vm.$scopedSlots,用来访问作用域插槽 vm.$scopedSlots = emptyObject // 定义 vm._c。创建元素类型的 vNode // 渲染函数中会使用这个方法。 vm._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) // 定义 vm.$attrs、vm.$listeners // vm.$attrs,包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 // vm.$listeners,包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器 const parentData = parentVnode && parentVnode.data if (process.env.NODE_ENV !== 'production') { ... } else { defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true) defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true) }}此函数的功能有些零散,但至少我们知道该方法定义了 6 个实例属性:vm.$slots、vm.$scopedSlots、vm._c、vm.$createElement、vm.$attrs、vm.$listeners。
此函数将会创建 vnode。这个我们可以通关源码来验证:
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)export function createElement ( context: Component, tag: any, data: any, children: any, normalizationType: any, alwaysNormalize: boolean): VNode | Array<VNode> { ... return _createElement(context, tag, data, children, normalizationType)}真正起作用的是 _createElement:
export function _createElement ( context: Component, tag?: string | Class<Component> | Function | Object, data?: VNodeData, children?: any, normalizationType?: number): VNode | Array<VNode> { if (isDef(data) && isDef((data: any).__ob__)) { ... return createEmptyVNode() } ... if (!tag) { // in case of component :is set to falsy value return createEmptyVNode() } ... // 定义 vnode let vnode, ns if (typeof tag === 'string') { ... } else { vnode = createComponent(tag, data, context, children) } if (Array.isArray(vnode)) { return vnode } else if (isDef(vnode)) { if (isDef(ns)) applyNS(vnode, ns) if (isDef(data)) registerDeepBindings(data) return vnode } else { return createEmptyVNode() }}重点看一下返回值(return),都是 vnode。
在模板一文中,我们知道模板编译成渲染函数,执行渲染函数就会生成一份 vNode。
callHook(vm, 'beforeCreate') 会触发 beforeCreated 对应的回调。请看源码:
// 将钩子拿出来,触发export function callHook(vm: Component, hook: string) { // #7573 调用生命周期钩子时禁用 dep 收集 pushTarget() // 是一个数组。比如可以通过 Vue.mixin 注入一个 created,这样就能有两个 created。 const handlers = vm.$options[hook] const info = `${hook} 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()}callHook() 真正调用钩子的方法是 invokeWithErrorHandling()。请看源码:
// handler 指回调export function invokeWithErrorHandling ( handler: Function, context: any, args: null | any[], vm: any, info: string) { let res try { // res,指回调的结果 res = args ? handler.apply(context, args) : handler.call(context) // {1} if (res && !res._isVue && isPromise(res) && !res._handled) { res.catch(e => handleError(e, vm, info + ` (Promise/async)`)) // issue #9511 // avoid catch triggering multiple times when nested calls res._handled = true } } catch (e) { handleError(e, vm, info) } return res}其中 回调的结果(行{1})放在 try...catch 中,如果报错,则会进入处理错误逻辑,即 handleError()。看父组件、父父组件...(一直往上找),如果没能捕获错误,则进入全局错误处理(globalHandleError)。请看源码:
export function handleError (err: Error, vm: any, info: string) { pushTarget() try { if (vm) { let cur = vm // 依次找父组件,父父组件...,如果定义了错误捕获(errorCaptured),并能捕获错误,则退出函数 // 否则进入全局错误处理 while ((cur = cur.$parent)) { const hooks = cur.$options.errorCaptured if (hooks) { // 依次迭代错误捕获 for (let i = 0; i < hooks.length; i++) { try { // 如果错误捕获返回 false,则视为已捕获,结束函数 const capture = hooks[i].call(cur, err, vm, info) === false if (capture) return } catch (e) { globalHandleError(e, cur, 'errorCaptured hook') } } } } } // 全局处理错误 globalHandleError(err, vm, info) } finally { popTarget() }}inject 就是给子孙组件注入属性或方法。就像这样:
// 父级组件提供 'foo'var Provider = { provide: { foo: 'bar' }, // ...}// 子组件注入 'foo'var Child = { inject: ['foo'], created () { console.log(this.foo) // => "bar" } // ...}执行 initInjections() 方法,首先获取 vm 中注入的 inject(包含注入的 key 和对应的属性或方法),然后将 inject 绑定到 vm 上,期间会关闭响应。
// 初始化注入export function initInjections(vm: Component) { // 拿到注入的key以及对应的属性或方法。数据结构是 [{key: provideProperyOrFunction},...] const result = resolveInject(vm.$options.inject, vm) // 若有注入 if (result) { // 官网:provide 和 inject 绑定并不是可响应的。 // 关闭响应 toggleObserving(false) Object.keys(result).forEach(key => { /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { ... } else { // 访问 key 时,其实就会访问 result[key],即调用注入的函数 defineReactive(vm, key, result[key]) } }) toggleObserving(true) }}Tip:resolveInject() 会返回一个包含对象的数组,里面是 inject 属性以及对应的值:
export function resolveInject(inject: any, vm: Component): ?Object { if (inject) { const result = Object.create(null) const keys = hasSymbol ? Reflect.ownKeys(inject) : Object.keys(inject) // 依次遍历 inject 的每个 key,从当前 vm 开始找 provide,若没有则依次往上一级找 // 如果找到,则注册到一个空对象(result)中 for (let i = 0; i < keys.length; i++) { const key = keys[i] // #6574 in case the inject object is observed... if (key === '__ob__') continue const provideKey = inject[key].from let source = vm while (source) { if (source._provided && hasOwn(source._provided, provideKey)) { // 找到inject对应的 provide,存入 result 对象中 // _provided 在下文的 initProvide 中被初始化 result[key] = source._provided[provideKey] break } // 找上一级 source = source.$parent } // source 为假值,说明一直找到顶部,都找到 if (!source) { ... } } return result }}初始化状态,即初始化 props、methods、data、computed 和 watch。
export function initState(vm: Component) { vm._watchers = [] // 用于当前 Vue 实例的初始化选项 const opts = vm.$options // 初始化 props if (opts.props) initProps(vm, opts.props) // 初始化 methods if (opts.methods) initMethods(vm, opts.methods) // 初始化 data if (opts.data) { initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } // computed if (opts.computed) initComputed(vm, opts.computed) // watch if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) }}定义组件时,我们可以通过 props 定义父组件传来的属性。就像这样:
// 接收父组件传来的 title 属性Vue.component('blog-post', { props: ['title'], template: '<h3>{{ title }}</h3>'})initProps() 会将 prop 和对应的属性或方法加入 vm._props 中,并将 prop 代理到 vm._props。如果访问 props,例如 vm.titlexx,其实访问的是 vm._props.titlexx。请看源码:
function initProps(vm: Component, propsOptions: Object) { // propsData,创建实例时传递 props const propsData = vm.$options.propsData || {} // 下面会将 prop 和对应的属性或方法绑定到此对象中 const props = vm._props = {} const keys = vm.$options._propKeys = [] ... // propsOptions 是 vm.$options.props for (const key in propsOptions) { keys.push(key) // value 是 prop 对应的属性或方法 const value = validateProp(key, propsOptions, propsData, vm) // Tip:直接看生成环境的逻辑即可 if (process.env.NODE_ENV !== 'production') { ... } else { // defineReactive 将数据转为响应式。给 props 添加 key 和对应的 value。 defineReactive(props, key, value) } // 如果 key 不在 vm 中,则将 key 代理到 _props // 就是说,如果访问props,例如 vm.titlexx,其实访问的是 vm._props.titlexx if (!(key in vm)) { proxy(vm, `_props`, key) } } toggleObserving(true)}initMethods() 会将我们定义的方法放到 vm 中。开发环境下会检查方法名,比如不能和 prop 中重复,不能和现有 Vue 实例方法冲突。
function initMethods (vm: Component, methods: Object) { const props = vm.$options.props for (const key in methods) { if (process.env.NODE_ENV !== 'production') { ... if (props && hasOwn(props, key)) { // `方法 "${key}" 已经被定义为一个 prop。`, warn( `Method "${key}" has already been defined as a prop.`, vm ) } if ((key in vm) && isReserved(key)) { // `方法 "${key}" 与现有的 Vue 实例方法冲突。 ` + // `避免定义以_或$开头的组件方法。` warn( `Method "${key}" conflicts with an existing Vue instance method. ` + `Avoid defining component methods that start with _ or $.` ) } } // bind(methods[key], vm),将 methods[key] 方法绑定到 vm 中 vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm) }}initData() 首先会取得 data,并放入 vm._data 中。依次将 data 中的 key 代理到 vm._data 中,期间会检查 key 是否与 methods 或 props 中 key 相同。如果访问 data,例如访问 vm.age,其实访问的是 vm._data.age。请看源码:
function initData(vm: Component) { // 取得数据 vm.$options.data let data = vm.$options.data // 如果数据是函数,则调用 getData(即 data.call(vm, vm))返回数据,每个实例都有一份 // 如果data不是函数,则每个实例公用这份 data // vm._data 指向 data,后面会做一个代理。访问 vm.age,其实访问的是 vm._data.age data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} // data 如果不是一个对象,开发环境则发出警告:数据函数应该返回一个对象 if (!isPlainObject(data)) { ... } // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] // key 不能和 methods 中相同 if (process.env.NODE_ENV !== 'production') { ... } // key 不能和 props 中相同 if (props && hasOwn(props, key)) { ... // 没有被预定,则将 key 代理到 _data } else if (!isReserved(key)) { proxy(vm, `_data`, key) } } // observe data observe(data, true /* asRootData */)}computed 用法如下:
computed: { aDouble: vm => vm.a * 2}initComputed() 会依次迭代我们定义的 computed,给每一个 key 都会创建一个 Watcher,并给 Watcher 传入 key 对应的回调方法,最后在 vm 上定义计算属性(defineComputed(vm, key, userDef))。请看源码:
function initComputed(vm: Component, computed: Object) { // 创建一个空对象给 vm._computedWatchers,是计算属性的 watcher const watchers = vm._computedWatchers = Object.create(null) // 是否是服务端渲染 const isSSR = isServerRendering() // 迭代 computed for (const key in computed) { // 取得 key 对应的方法 const userDef = computed[key] // computed 还支持 get、set const getter = typeof userDef === 'function' ? userDef : userDef.get ... // 非服务端渲染 if (!isSSR) { // create internal watcher for the computed property. // 为计算属性创建内部观察者。访问 vm['计算属性'] 时会使用 watchers[key] = new Watcher( vm, // 取值(watcher.value)时会用到 getter || noop, // {1} noop, computedWatcherOptions ) } // 定义计算属性 if (!(key in vm)) { // userDef,即 key 对应的回调 defineComputed(vm, key, userDef) } else if (process.env.NODE_ENV !== 'production') { // 计算属性不能在 data、props和methods中 ... } }}如果你想知道 defineComputed(vm, key, userDef) 做了什么?请继续看。
defineComputed() 的核心功能在最后一句:
// 定义计算属性export function defineComputed( target: any, key: string, userDef: Object | Function) { // 不是服务端渲染,则需要缓存 const shouldCache = !isServerRendering() if (typeof userDef === 'function') { sharedPropertyDefinition.get = shouldCache ? createComputedGetter(key) : createGetterInvoker(userDef) sharedPropertyDefinition.set = noop } else { // 计算属性可以有 get、set ... } ... // target 是 vm // 访问 vm[key] 就会访问 sharedPropertyDefinition Object.defineProperty(target, key, sharedPropertyDefinition)}我们这里不是服务端渲染,所以进入 createComputedGetter():
function createComputedGetter(key) { return function computedGetter() { // 取得在 initComputed() 中定义的 watcher const watcher = this._computedWatchers && this._computedWatchers[key] if (watcher) { // 计算属性是有缓存的(官网:计算属性是基于它们的响应式依赖进行缓存的) // 脏的(比如说计算属性依赖的某个数据值变了,就是脏的),则重新求值 if (watcher.dirty) { watcher.evaluate() } if (Dep.target) { watcher.depend() } // 取得 watcher 的值。会访问 initComputed() 方法中的 getter(行{1}) return watcher.value } }}defineComputed(vm, key, userDef) 做什么事情,它的名字其实已经告诉我们了(即定义计算属性)。比如访问一个计算属性,会取得对应计算属性的 Watcher,然会从 watcher 中取得对应的值。其中 watcher 的 dirty 与缓存有关。
Tip:有关 Water 的介绍可以看 侦测数据的变化
用法如下:
watch: { a: function (val, oldVal) { console.log('new: %s, old: %s', val, oldVal) }}initWatch() 会依次迭代我们传入的 watch,并通过 createWatcher 创建 Watcher。请看源码:
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) } }}createWatcher() 的本质是 vm.$watch()。
function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object) { if (isPlainObject(handler)) { options = handler handler = handler.handler } if (typeof handler === 'string') { handler = vm[handler] } // hander 是函数 // vm.$watch() 方法赋予我们监听实例上数据变化的能力 return vm.$watch(expOrFn, handler, options)}provide 就是提供给子孙组件注入属性或方法。就像这样:
// 父级组件提供 'foo'var Provider = { provide: { foo: 'bar' }, // ...}initProvide 与上文的 initInjections 对应。
initProvide() 主要就是将用户传入的 provide 保存到 vm._provided,后续给 inject 使用。请看源码:
export function initProvide(vm: Component) { const provide = vm.$options.provide // 存起来,供子孙组件使用 if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide }}_init() 的末尾就是挂载(vm.$mount()):
if (vm.$options.el) { vm.$mount(vm.$options.el) }因为这些 key 最后都绑定在 vm 上,所以不能相同。请看源码:
// propsproxy(vm, `_props`, key)// dataproxy(vm, `_data`, key)// methodsvm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)// compueddefineComputed(vm, key, userDef)在 initState() 中有如下代码:
// 初始化 propsif (opts.props) initProps(vm, opts.props)...// 初始化 dataif (opts.data) { initData(vm)} else { observe(vm._data = {}, true /* asRootData */)}由于 props 先初始化,所以在 data 中可以使用 props。请看示例:
// 父组件<welcome-button name="peng"></welcome-button>// WelcomeButton.vue<template> <div> name={{ name }} <br /> myName={{ myName }} </div></template><script>export default { props: ['name'], data () { return { myName: this.name + 'jiali' } }}</script>浏览器输出:
name=pengmyName=pengjialiTip:props 中使用 data 却是不可以的,因为 data 初始化在 props 后面。
请问下面这段代码,控制台输出什么:
<template> <div> <!-- 读取三个属性 --> {{ doubleAge }} {{ age }} {{ name }} </div></template><script>export default { data () { return { age: 18, name: 'peng' } }, computed: { doubleAge: function (vm) { const result = this.age * 2 console.log('computed') return result } }, watch: { age: { handler: function (val, oldVal) { console.log('watch age') }, immediate: !true }, name: { handler: function (val, oldVal) { console.log('watch name') }, // 立即执行 immediate: true } }, created () { setTimeout(() => this.age++, 5000) }}</script>watch namecomputed// 过5秒watch agecomputed虽然在 initState() 中先初始化 computed,再初始化 watch,但在这个例子中,却是先执行 watch,后执行 computed。
// computedif (opts.computed) initComputed(vm, opts.computed)// watchif (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch)}其他章节请看:
vue 快速入门 系列