这是官网上一个最简单的例子
{{ message }}
接下来我通过在chrome中一步步运行代码来理清其内部逻辑。
首先当然是调用构造函数
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) // 主要就是这个步骤,调用原型中的_init方法
}
//下面的这些方法都是给Vue.prototype增加方法和属性的,具体意义看vue官网api就能了解
initMixin(Vue) // 给原型增加了_init()
stateMixin(Vue) // 增加$data,$props,$set(),$delete(),$watch()
eventsMixin(Vue) // 增加$on(),$once(),$off(),$emit()
lifecycleMixin(Vue) // 增加_update(),$forceUpdate(),$destroy()
renderMixin(Vue) // 增加_render()
然后就进入_init()方法了
var uid$1 = 0;
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this; // // 定义一个vm指向Vue实例(这里就是app)
// a uid
vm._uid = uid$1++;
var startTag, endTag;
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
startTag = "vue-perf-init:" + (vm._uid);
endTag = "vue-perf-end:" + (vm._uid);
mark(startTag);
}
// a flag to avoid this being observed
vm._isVue = true;
// merge options
if (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属性,这个属性是合并之后的options。详细内容看¹mergeOptions
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
/* istanbul ignore else */
{
initProxy(vm);
}
// expose real self
vm._self = vm;
// 下面这些都是和vue生命周期有关的一些操作了
initLifecycle(vm) // ²初始化生命周期
initEvents(vm) // ³初始化事件
initRender(vm) // ⁴初始化render
callHook(vm, 'beforeCreate') // ⁵beforeCreate
initInjections(vm) // ⁶初始化inject
initState(vm) // ⁷对data/props/computed/watch等进行监听
initProvide(vm) // ⁸初始化provide
callHook(vm, 'created') // ⁹create
/* istanbul ignore if */
if ("development" !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false);
mark(endTag);
measure(((vm._name) + " init"), startTag, endTag);
}
if (vm.$options.el) {
vm.$mount(vm.$options.el); // ¹⁰挂载
}
};
}
1.mergeOptions
这个方法顾名思义就是合并分支的。那我们先看没合并之前的vm
再看这个方法运行之后变成啥样了
明显能注意到的就是,vm.$options比之前的options多了components,directives,filters,_base,然后data变成一个方法了。因此我们大概知道这个方法就是添加vm.$options然后把基础的options和传入的options合并(其实看函数名就差不多知道了)
2.initLifecycle
对比很明显,增加了很多属性,来看一眼代码
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
}
看样子这个函数是很好理解的,主要目的是为了给vm增加$children、$parent、$refs、$root。然后以下划线开始的这些属性看命名就能才出来是与vue生命周期有关的一些变量(下面我把以$开头的称为实例属性,把_开头的称变量)。这里放一张官网上的生命周期图片,具体的在之后的内容中也会涉及到的
-
initEvents
看上去也就是多了几个变量,我们看看具体的函数执行情况吧
这么一张图看上去已经很清晰了,
vm.$options._parentListeners
未定义,因此之后的步骤也不会执行的,整个函数就新增了两个变量。不过这个函数本来想干的事,看作者注释的话是想添加父实例的事件,这个功能在我之后调试到相应例子之后会再进行扩散的
4.initRender
好害怕啊突然变长好多~总结一下就是又多了很多属性( $attrs、 $listeners、$slots、$scopedSlots、$vnode)和变量,还增加了方法($createElement)。先看一下执行过程
这里大致可以分为三个部分,第一又增加了变量和属性,同样我们在这个例子里看不出什么,比如说$slots这个实例属性,第二是给vm增加了一个$createElement方法,调用这个方法其实就是调用createElement方法,看命名就知道是一个生成元素的方法。第三个是调用了一个defineReactive方法,这个方法是什么意思呢
这些步骤基本是在做初始化
但是最重要的这里,事实上对于get,在我们这个例子中,Dep.target为null,因此get就是返回了value,而set也就是普通的设置了值而已,那么这么长的代码有什么用呢,这其实又和数据绑定有关。关于数据绑定我这里也先不再延伸。所以这第三部分就是在给$attrs、$listeners重写get和set
5.callHook(vm, 'beforeCreate')
function callHook (vm, hook) {
var handlers = vm.$options[hook];
if (handlers) {
for (var i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm); // 执行beforeCreate下的每个函数
} catch (e) {
handleError(e, vm, (hook + " hook"));
}
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook);
}
}
这个函数很简单,我们这个简单的vue实例也没有传beforeCreate这个选项,因此这步也可以忽略,只需要知道在create之前会调用这样一个东西
6.initInjections
function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm) //获得vm.$options.inject的所有属性
if (result) {
observerState.shouldConvert = false
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key]) // 把inject监听起来
}
})
observerState.shouldConvert = true
}
}
瞅瞅这个简单的代码,对于我们这个例子来说,又是可以跳过的。不过我们还需大致看看这个函数,它其实是对inject选项的初始化,这个眼熟的defineReactive,没错就是对inject的每个属性的get,set都重写了一下。
7.initState
这个函数明显是很重要的,因为它处理了props/methods/data/computed/watch等选项,而这些选项又是我们经常需要用到的。那我们从源码看看它怎么处理的
function initState (vm: Component) {
// 首先在vm上初始化一个_watchers数组,缓存这个vm上的所有watcher
vm._watchers = []
// 获取options,包括在new Vue传入的,同时还包括了Vue所继承的options
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)
}
}
这段代码从命名上看是很简单的,没有阅读障碍。那我们看看和我们相关的initData
。
function initData (vm: Component) {
// 获取data
let data = vm.$options.data
// 因为data有可能是个函数,这里给它转换了一下
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
// 收集data的自身属性名
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]
if (process.env.NODE_ENV !== 'production') {
// data的属性不能同时在method中
if (methods && hasOwn(methods, key)) {
warn(
`method "${key}" has already been defined as a data property.`,
vm
)
}
}
// data的属性不能同时在props中
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)) {
// 给data里的每个属性重写了get和set
proxy(vm, `_data`, key)
}
}
// 时刻观察data
// observe data
observe(data, true /* asRootData */)
}
这个observe函数实际上做了很多事情。
8.initProvide
function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
初始化provide选项。超简单的代码,谁都能看懂,加了个_provided变量,和我这个例子好像也没有什么关系
- callHook(vm, 'created')
和callHook(vm, 'beforeCreate')一样一样的,不再赘述了
10.mount
if (vm.$options.el) {
vm.$mount(vm.$options.el) //如果传入了el就挂载
}
像这段代码,和生命周期图结合起来看,就一目了然,如果有el选项的话就挂载
那么再来看看updateComponent
这里首先要执行vm._render()
回到_update
我们传了旧的和新的node进去
这步执行完页面上是这样的
直到下图执行完后,页面才算更新完毕
说的可能还有点不太完整。整个_init方法实际上只是到mount部分为止。真的要销毁的话得手动调用$destroy。还有很多函数没有仔细扩散,等之后调试到相关例子的时候会再补充。