1. 入口
由于我本地调试代码是通过 npm run dev
构建的,所以可以在 package.json
找到真实运行的命令
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev" 复制代码
通过我们之前开篇分析,我们可以透过 TARGET:web-full-dev
在 scripts/config.js
中找到编译入口
// Runtime+compiler development build (Browser) 'web-full-dev': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.js'), format: 'umd', env: 'development', alias: { he: './entity-decoder' }, banner } 复制代码
我们就此来到 web/entry-runtime-with-compiler.js
文件,在最后一行我们找到
export default Vue 复制代码
这个 Vue
便是我们在 Vue
中最终输出的构造函数了,通过溯源我们可以找到它是哪里来的
entry-runtime-with-compiler.js -> web/runtime/index -> core/index -> core/instance/index
终于找到你,还好没放弃
import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index' 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) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue 复制代码
在次我们可以看到 Vue
的真实面目,是个构造函数,但是这个函数有点简单,就一句代码初始化 this._init(options)
,我们甚至没看到 _init
方法是在哪里定义的。
这样做的好处是将构造函数的逻辑拆分成不同的部分,按照逻辑解耦分离,不同的 Mixin 函数为 Vue 提供不同的功能方法,其中 _init
方法就是在 initMixin
定义的。
2. 初始化函数
我们接着看看 _init
函数做了些什么,代码比较多,我就直接在其上面写注释了,我们主要关注主流程即可。
let uid = 0 export function initMixin (Vue: Class<Component>) { 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) { // 组件实例 暂时跳过 initInternalComponent(vm, options) } else { // resolveConstructorOptions 是从构造函数的父类继承方法 // 这边一般返回 vm.constructor.options {components: {}, directives: {}, filters: {}} // 其中vm.constructor.options => Vue.options 的定义是在 core/index.js 中的 initGlobalAPI(Vue) // initGlobalAPI 详见2.1 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { // proxy代理vm,主要用于提示错误和键名规范,如果 key is not defined on the instance but initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm // 初始化生命周期及相关数据 如vm._isMounted vm._isDestroyed vm._inactive vm.$parent vm.$children 等 // 详见2.2 initLifecycle(vm) // 初始化事件 注册在组件中定义的事件如 <hello @confirm="xx" /> // 详见2.3 initEvents(vm) // 初始化渲染函数 暂不分析 initRender(vm) // 调用组件的beforeCreate钩子 callHook(vm, 'beforeCreate') // inject 暂时跳过 initInjections(vm) // resolve injections before data/props // 初始化数据 详见2.4 initState(vm) // provide 暂时跳过 initProvide(vm) // resolve provide after data/props // 调用组件created钩子 可以看出来 beforeCreate 和 created 的差别在于中间几个数据初始化函数 特别是 initSate callHook(vm, 'created') // 挂载节点 if (vm.$options.el) { vm.$mount(vm.$options.el) } } } 复制代码
现在我们对构造函数中的唯一调用方法 this._init
方法进行了学习,基本可以知道实例化的大致逻辑流程了。因为函数中调用的方法已经高度封装,所以代码量虽少,但逻辑还是挺多的其实,下面我们将对上述流程中的部分方法进行学习,进一步搞清初始化逻辑。
2.1 initGlobalAPI
我们在前面的初始化函数中有提到 vm.contructor.options
,但是我们没看到在何处有定义它的地方,实际上我们在前面找构造函数 Vue
的定义的时候,曾经溯源了几个文件 entry-runtime-with-compiler.js -> web/runtime/index -> core/index -> core/instance/index
,各个文件不是单纯的去引入输出,而是在各自的范围内进行了一系列操作,其中在 core/index
就有这么一行代码
initGlobalAPI(Vue) 复制代码
我们来看看 core/global-api/index.js
中 initGlobalAPI
的具体逻辑
export function initGlobalAPI (Vue: GlobalAPI) { // config const configDef = {} configDef.get = () => config Object.defineProperty(Vue, 'config', configDef) // 定义了Vue的静态工具函数 但不属于开放API 我们尽量不去使用 Vue.util = { warn, extend, mergeOptions, defineReactive } // 原来set delete nextTick 在这边初始化 Vue.set = set Vue.delete = del Vue.nextTick = nextTick // 2.6 explicit observable API Vue.observable = <T>(obj: T): T => { observe(obj) return obj } // 我们的主角options出场 Vue.options = Object.create(null) // ASSET_TYPES的定义在 share/constants 中 ASSET_TYPES = ['component', 'directive', 'filter'] ASSET_TYPES.forEach(type => { Vue.options[type + 's'] = Object.create(null) }) // 不知 先跳过 Vue.options._base = Vue // KeepAlive逻辑 先跳过 extend(Vue.options.components, builtInComponents) // 一些常用的方法定义,通过解耦思想在函数中实现 use mixin extend 方法 initUse(Vue) initMixin(Vue) initExtend(Vue) // 比较特殊 通过遍历ASSET_TYPES分别定义 Vue.directive Vue.component Vue.filter initAssetRegisters(Vue) } 复制代码
2.2 initLifecycle
我们再来看看在 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) } // 初始化实例属性$parent $root $children $refs 等 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 } 复制代码
实际上,可以看出来 initLifecycle
函数比我们想的简单一些,它就是定义了实例和父子节点相关的属性及生命周期描述相关属性。这其实是可以理解的,因为生命周期钩子函数我们在组件中定义,在初始化和渲染更新中触发,所以理论上调用钩子函数也应该在实际渲染的各个过程中才对,就如我们前面在 _init
函数中调用的 callHook(vm, 'created')
,在 created
钩子调用中再去修改生命周期相关属性及调用函数。
2.3 initEvents
initEvents
函数本身比较简单,主要是为组件上定义的函数调用 updateComponentListeners
进行注册处理,关于 updateComponentListeners
我们就先不分析了,后面有机会再进行分析
export function initEvents (vm: Component) { vm._events = Object.create(null) vm._hasHookEvent = false // init parent attached events const listeners = vm.$options._parentListeners if (listeners) { updateComponentListeners(vm, listeners) } } 复制代码
2.4 initState
我们最后再来分析下 initState
,这是个比较重要的函数,我们可以从中分析得出框架在 beforeCreate
和 created
之间做了哪些数据处理
export function initState (vm: Component) { vm._watchers = [] // 这段代码真是短小精悍 短短的几行代码 分别初始化了 props methods data computed 我们来分别看看 const opts = vm.$options // 详见2.4.1 if (opts.props) initProps(vm, opts.props) // 详见2.4.2 if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { // 详见2.4.3 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) } } 复制代码
2.4.1 initProps
initProps
到底是如何初始化的,我们来看看
function initProps (vm: Component, propsOptions: Object) { // propsOptions为组件Props定义形如{name: String} // propsData为组件接收到的数据形如{name: 'Joke'} 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 converted if (!isRoot) { toggleObserving(false) } // 遍历prop定义 for (const key in propsOptions) { keys.push(key) // validateProp用于获取prop的值(propsData数据或默认值) const value = validateProp(key, propsOptions, propsData, vm) 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 ) } // 这边能遇见我们的错误提示老朋友 不允许重新父组件传来的prop值 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 { // 为props添加响应式 其中前面toggleObserving(false)的作用就在这边 // 当value为对象时不会进一步递归添加响应式 defineReactive(props, key, value) } // vm代理_props值 也就是我们的props if (!(key in vm)) { proxy(vm, `_props`, key) } } toggleObserving(true) } 复制代码
2.4.2 initMethods
相比之下 initMethods
就简单很多了
function initMethods (vm: Component, methods: Object) { const props = vm.$options.props for (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 $.` ) } } // 上面都是开发环境的校验和错误提示 // 正式环境只有这句实际逻辑 将methods下的方法都复制给vm 并将this指定为vm vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm) } } 复制代码
2.4.3 initData
我们继续看 initData
的逻辑
function initData (vm: Component) { let data = vm.$options.data // 如果是函数则将this指向vm并执行函数 data = 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 instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { // 校验是否和methods和props键重复 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)) { // vm代理_data proxy(vm, `_data`, key) } } // 响应式数据 后面文章单独分析 // observe data observe(data, true /* asRootData */) } 复制代码
2.4.4 initComputed和initWatch
涉及watch,我们后面再讲
3. 结语
前面我们大致分析了 Vue
函数实例化的流程。
- 通过
entry-runtime-with-compiler.js
找到入口,再通过溯源找到 Vue 函数的定义 _init
函数的流程
初始化生命周期数据 -> 初始化组件事件 -> 初始化渲染 -> 调用beforeCreate -> 初始化数据 -> 调用created -> 挂载节点
- Vue静态方法的初始化
initGlobalAPI
逻辑 - 实例化流程相关函数
initLifecycle initEvents initState
逻辑 initState
中props methods data
的初始化逻辑
还有些流程没有分析的,我将在后面的文章继续
initRender
初始化渲染initState
初始化数据中的computed watch
初始化initData
中的observe(data)
数据监测
最后啰嗦一句,贴的代码比较多。分析的有不对的地方希望帮忙指正,有不清楚的地方也可以提出来,大家一起交流~