Vue.js 是一个以数据驱动为设计原理的框架。
所谓数据驱动,是指视图是由数据驱动生成的,比起视图如何生成,开发者更关注数据模型和数据流转。
接下来,我们会从源码角度来分析 Vue 是如何实现的,分析过程会以主线代码为主,重要的分支逻辑会放在之后单独分析。
先来看一个熟悉的 demo
熟悉的 demo
大家对这简单的案例应该都不会陌生,在我还是萌新的那会常看这个demo
demo的代码很简单,功能是点击数字然后累增
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title></title> <script src="/path/to/dist/vue.min.js"></script> </head> <body> <div id="app"> <div @click="num++"> {{ num }} </div> </div> <script> var vm1 = new Vue({ el: '#app', data: { num: 1, } }) </script> </body> </html>
|
现在回想起来,这一切似乎就从这个 new Vue()
开始了:)
带着些许怀旧的心情,我们从 new vue()
开始一探究竟。
new Vue() 做了什么
new Vue() 本质上是创建了一个 Vue 的实例对象,那么 Vue 是怎么定义的呢
Vue 的定义
Vue本质是一个构造函数,只能通过 new 操作符去创建一个 Vue 的实例
1
| const app = new Vue({ });
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
function Vue (options) { this._init(options) }
initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue)
|
Vue 本身很简单,就只做了一件事,就是调用初始化方法 this._init
,并发参数传入
Vue 实例的函数并没直接写在 Vue 构造函数内,而是通过各种 mixin 挂载到了 Vue.prototype
其实这些函数都可以直接在当前文件中书写,但是拆开到不同模块分别挂载可以降低当前文件的耦合度,更利于代码的维护
initMixin 初始化
initMixin
用于挂载初始化相关的函数到 Vue.prototype
_init
函数则是通过 initMixin
注入到 Vue 的原型链上
1 2 3 4 5 6 7
|
export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { } }
|
stateMixin 数据状态
stateMixin
用于挂载 控制数据状态 相关的函数到 Vue.prototype
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| export function stateMixin (Vue: Class<Component>) { const dataDef = {} dataDef.get = function () { return this._data } const propsDef = {} propsDef.get = function () { return this._props } Object.defineProperty(Vue.prototype, '$data', dataDef) Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set Vue.prototype.$delete = del
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function { } }
|
stateMixin
比较简单纯粹,主要做了这几件事
- 初始化 $data 和 $props 的 descriptor
- 注入 $set 和 $delete 方法
- 注入手动 $watch api
eventsMixin 事件发布订阅
eventsMixin
用于挂载 事件发布订阅 相关的函数 到 Vue.prototype
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export function eventsMixin (Vue: Class<Component>) { Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component { } Vue.prototype.$once = function (event: string, fn: Function): Component { }
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { }
Vue.prototype.$emit = function (event: string): Component { } }
|
eventsMixin
主要做了这几件事
- 挂载 事件订阅 函数
- 挂载 只触发一次的事件订阅 函数
- 挂载 取消订阅 函数
- 挂载 触发事件 函数
lifecycleMixin 生命周期
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| export function lifecycleMixin (Vue: Class<Component>) { Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { }
Vue.prototype.$forceUpdate = function () { }
Vue.prototype.$destroy = function () { } }
|
renderMixin 渲染VNode
renderMixin
用于挂载 控制渲染执行 相关的函数到 Vue.prototype
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export function renderMixin (Vue: Class<Component>) { installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) }
Vue.prototype._render = function (): VNode { } }
|
renderMixin
只做了三件事
- 注入运行时的一些辅助函数,供内部使用,目的是为了简化操作
- 挂载我们熟悉的
$nextTick
- 挂载内部API
_render
从上述各种 mixin 中可以观察到,这里挂载到 prototype 上的方法分为两种
函数类型 |
备注 |
以 _ 开头的私有方法 |
非公开暴露的API,仅提供给 vue 内部去使用,不建议外部使用这类私有方法,因为在后续是有可能会发生变化的 |
以 $ 开发的公有方法 |
已经被公开暴露的 API,可以安心使用 |
以上就是 Vue 的构造函数定义,其中的函数简化了许多,目的是为了先从宏观的角度上对 Vue 初始化有个大概了解
初始化 _init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
|
Vue.prototype._init = function (options?: Object) { const vm: Component = this vm._uid = uid++
vm._isVue = true if (options && options._isComponent) { initInternalComponent(vm, options) } else {
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } vm._self = vm initLifecycle(vm) initEvents(vm) initRender(vm) callHook(vm, 'beforeCreate') initInjections(vm) initState(vm) initProvide(vm) callHook(vm, 'created')
if (vm.$options.el) { vm.$mount(vm.$options.el) } }
|
好家伙,上面一串代码看起来是那么的简单清晰‼️大致浏览后便对 Vue _init
阶段有了一个轮廓
感慨完后,我们把其中各部分简单拆解一下,下面对它们做解释
合并配置 mergeOptions
1 2 3 4 5 6 7 8 9
| if (options && options._isComponent) { initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) }
|
可以看到这里有两种场景的配置合并,不同场景合并逻辑也不一样
为了方便梳理,这里会改造一下最初的案例,后面会用到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import Vue from 'vue'
var ChildComp = { template: '<div>{{msg}}</div>', created() { console.log('child created') }, mounted() { console.log('child mounted') }, data() { return { msg: 'Hello World' } } }
Vue.mixin({ created() { console.log('mixin created') } })
var vm1 = new Vue({ el: '#app', data: { num: 1, }, render: h => h(childComp) })
|
按照我们的思路,在执行 this._init
后,代码进入到这个场景 手动调用场景
所谓 手动调用场景
其实是指我们手动创建的 Vue 组件实例,用于区分平时我们 import xxxComponent from '../path/your/vue-component'
,也就是 组件场景
;
手动调用场景
1 2 3 4 5
| vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
|
这里合并的options来源有三个
1、合并 vue 构造器上的通用 options
2、参数传入的 options,加载优先级大于通用的options
3、当前vm实例,这个会在加载策略中用到
这里通过调用 mergeOptions 方法来合并,它实际上就是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合并。
options来源 |
说明 |
resolveConstructorOptions(vm.constructor) |
通用的配置,Vue.options,initGlobalAPI时挂载 |
options |
new Vue时传入的参数 |
vm |
组件实例 |
Vue.options
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| export function initGlobalAPI (Vue: GlobalAPI) { const configDef = {} configDef.get = () => config Object.defineProperty(Vue, 'config', configDef)
Vue.util = { warn, extend, mergeOptions, defineReactive } Vue.set = set Vue.delete = del Vue.nextTick = nextTick
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => { Vue.options[type + 's'] = Object.create(null) })
Vue.options._base = Vue }
|
可以看到 Vue.options 实际是给 components
,filters
,directives
设置默认值
resolveConstructorOptions 获取Vue构造器的options
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| export function resolveConstructorOptions (Ctor: Class<Component>) { let options = Ctor.options if (Ctor.super) { const superOptions = resolveConstructorOptions(Ctor.super) const cachedSuperOptions = Ctor.superOptions if (superOptions !== cachedSuperOptions) { Ctor.superOptions = superOptions const modifiedOptions = resolveModifiedOptions(Ctor) if (modifiedOptions) { extend(Ctor.extendOptions, modifiedOptions) } options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions) if (options.name) { options.components[options.name] = Ctor } } } return options }
|
上面一串代码简单来说
- 获取当前构造器的 options
- 如果构造器继承了别的构造器,则把当前构造器和父构造器的options合并
mergeOptions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object {
normalizeProps(child, vm) normalizeInject(child, vm) normalizeDirectives(child) if (!child._base) { if (child.extends) { parent = mergeOptions(parent, child.extends, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } } const options = {} let key for (key in parent) { mergeField(key) } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } } function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) } return options }
|
mergeOptions 主要功能就是把 parent 和 child 这两个对象根据一些合并策略,合并成一个新对象并返回。比较核心的几步,先递归把 extends 和 mixins 合并到 parent 上,然后遍历 parent,调用 mergeField,然后再遍历 child,如果 key 不在 parent 的自身属性上,则调用 mergeField
mergeField 中用到了策略模式,对于特定的 key,会有特殊的处理,mergeOptions中的 vm
参数也是用于此
merge strate 合并策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
|
function mergeHook ( parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function> ): ?Array<Function> { const res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal return res ? dedupeHooks(res) : res }
LIFECYCLE_HOOKS.forEach(hook => { strats[hook] = mergeHook })
|
不难看到,合并策略里的特殊 key 其实都是 Vue 的生命周期 key
这个函数的实现也非常有意思,就是
1、如果没有 childVal ,就返回 parentVal;
2、如果有 dhildval, 再判断是否存在 parentVal
3、如果存在 parentVal 就把 childVal 添加到 parentVal 后返回新数组;否则返回 childVal 的数组。
最后回到 mergeOptions 函数,一旦 parent 和 child 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。
这样做的目的是为了把同个组件内的相同的声明周期定义从上往下顺序执行
手动调用场景的结果
当我们走完上述流程,案例中的 Vue 实例的 options 最后会变成这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| vm.$options = { components: { }, created: [ function created() { console.log('mixin created') }, function created() { console.log('child created') }, ], mounted: [ function mounted() { console.log('child mounted') }, ], directives: { }, filters: { }, _base: function Vue(options) { }, el: "#app", render: function (h) { } }
|
组件场景
所谓 组件场景
是指通过全局组件注册 Vue.component
或者 局部组件注册 components: ['xxx']
所有注册的组件都会经过 createComponent
-> Vue.extend
处理
Vue.extend 组件创建和继承
我们来看看 Vue.extend
是怎么实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
|
Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this const SuperId = Super.cid const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } const name = extendOptions.name || Super.options.name const Sub = function VueComponent (options) { this._init(options) } Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.cid = cid++ Sub.options = mergeOptions( Super.options, extendOptions ) Sub['super'] = Super
if (Sub.options.props) { initProps(Sub) } if (Sub.options.computed) { initComputed(Sub) }
Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.use = Super.use
ASSET_TYPES.forEach(function (type) { Sub[type] = Super[type] }) if (name) { Sub.options.components[name] = Sub }
Sub.superOptions = Super.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options)
cachedCtors[SuperId] = Sub return Sub }
|
Vue.extend
主要做了这几件事
- 缓存 在当前父节点下 时期的 当前构造函数
- 组件名赋值
- 定义了一个 VueComponent 构造函数
- 基于prototype继承 Vue
- 增加 cid 标识
- 标识父构造函数的别名
- 把 props 代理到了
prototype._props
,computed 代理到 prototype._computed
- 继承父组件的component,directive,filter属性
- 允许组件使用自己,也就是递归使用自身
- 保存对各个配置的引用。sub.options 才是当前组件真正的配置
Vue.extend
会在 createComponent
中被调用,用于获取真正的VueComponent构造函数
createComponent 创建组件VNode
我们先不讨论他们是在何时怎么流转到 createComponent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { const baseCtor = context.$options._base
if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } data = data || {}
resolveConstructorOptions(Ctor)
installComponentHooks(data) const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) return vnode }
|
createComponent
最后返回了一个初始的 VNode,这个VNode的componentInstance其实还是空的,那么真正的组件实例会在什么时候创建呢?
这里关系到 installComponentHooks
,这里简单提一下,在父组件patch过程中,会对触发子组件的 init
钩子,此时子组件才会去真正创建组件实例
这个init
钩子做了什么呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if ( vnode.componentInstance && !vnode.componentInstance._isDestroyed && vnode.data.keepAlive ) { const mountedNode: any = vnode componentVNodeHooks.prepatch(mountedNode, mountedNode) } else { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, }
|
createComponentInstanceForVnode 创建组件实例并挂载到VNode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| export function createComponentInstanceForVnode ( vnode: any, parent: any ): Component { const options: InternalComponentOptions = { _isComponent: true, _parentVnode: vnode, parent } return new vnode.componentOptions.Ctor(options) }
|
initInternalComponent
组件场景的关键函数都已经梳理好了,我们回过头看看 组件场景
的关键处理函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| export function initInternalComponent (vm: Component, options: InternalComponentOptions) { const opts = vm.$options = Object.create(vm.constructor.options) const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag
if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } }
|
首先执行把组件的保存到当前实例 vm.$options
接着又把实例化子组件传入的子组件父 VNode 实例 parentVnode、子组件的父 Vue 实例 parent 保存到 vm.$options 中,另外还保留了 parentVnode 配置中的如 propsData 等其它的属性。
这样看来,相较于 手动调用场景
,initInternalComponent 只是做了简单一层对象赋值,并不涉及到递归、合并策略等复杂逻辑。
组件场景的结果
经过上述代码执行,VueComponent 实例的options会变成情况
核心的地方在于我们设置的组件配置都在 __proto__
上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| vm.$options = { parent: Vue , propsData: undefined, _componentTag: undefined, _parentVnode: VNode , _renderChildren: undefined, __proto__: { components: { }, directives: { }, filters: { }, _base: function Vue(options) { }, _Ctor: {}, created: [ function created() { console.log('mixin created') }, function created() { console.log('child created') } ], mounted: [ function mounted() { console.log('child mounted') } ], data() { return { msg: 'Hello Vue' } }, template: '<div>{{msg}}</div>' } }
|
总结
在new Vue()过程中,主要做了这几件事:
- 合并传入的options和通用的options,把options挂载到vm.$options
- 初始化生命周期标识中的一些变量,把自身vm绑定到parent.$children上
- 更新父组件的attached事件监听器
- 绑定生成Component和Element的函数到实例上,$attrs和$listeners的响应式,创建$slots,初始化_vnode、_staticTrees等虚拟dom和dom tree的关键信息
- 触发组件的 beforeCreate 事件
- data/props/computed的响应式,绑定methods到实例this,初始化watch
- 触发组件的 created 事件
- 准备就绪后,把vdom生成的模板挂载到对应父节点上
在 _init
过程中,做了很多模块的初始化
- 初始化生命周期 initLifecycle
- 初始化事件 initEvents
- 初始化渲染VNode initRender
- 初始化数据注入 initInjections
- 初始化数据状态 initState
- 初始化数据暴露 initProvide
- 挂载 vm.$mount
- 注册组件函数钩子 installComponentHooks
- …等等
这些将放在后面的篇章中一一细读