一、数据的初始化
这部分涉及到data数据的初始化的过程,以及后续涉及到的双向绑定的过程。
首先我们知道Vue的源头实际上是一个用Function实现的类。源码地址:vue/src/core/instance/index.js
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
当我们new Vue时,最后实际上执行的是this._init(options),而_init这个方法实际上是initMinin(Vue)初始化的,我们来看看这个函数。
vue/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._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 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 = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(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)
}
}
}
这个函数中我们把目光对焦到initstate函数上面,这个函数实际上处理的就是数据绑定相关的逻辑。
vue/src/core/instance/state.js
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (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)
}
}
一层一层下来我们终于看到了这个最关键的函数,initData
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {} //这一段代码就解释了为什么data是一个function,这里要注意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中不会有和data中重名的逻辑
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 data
observe(data, true /* asRootData */)
}
相关逻辑我已经注释在代码上,总而言之,initData解释了一些现象。
1、访问this.somedata时实际上访问的是this._data.somedata亦或者说是vm._data.somedata。
2、methods和props中的名字为什么不能和data中的变量名重名的问题。
3、访问data时被Vue自己的proxy劫持getter/setter。(use defineProperty,后续会提到)
ok,那么我们现在重点看一下这个proxy。
函数调用了proxy(vm, `_data`, key)
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
代码含义实际上很简单,当访问_data中的变量时就被sharedPropertyDefinition自己的get和set劫持了。
二、Vue实例挂载的实现
回到最初vue/src/core/instance/index.js中声明Vue函数的时候最后一句
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
这句就是挂载的入口了。mount方法实际上在Vue Runtime版本中有事先的定义
vue/src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element, //传入的可以是字符串或者dom
hydrating?: boolean
): Component {
el = el && query(el) //query方法实际上就是一个dom元素的selector
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) { //确保不是挂载到body或者html标签下,否则覆盖。。
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) { //如果没有render函数的分支
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) { //传入的template如果是一个dom的话那就拿innerHTMl
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) { //如果没有template则给他套一层div之后在返回
template = getOuterHTML(el)
}
if (template) { //后续就是template编译成render函数的过程
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}
相关解释我也注释在代码上了,根据这段代码我们可以解释官网LifeCycle的图了:

1、Vue会判断是否直接传入了render函数,如果没有则判断是否有template。
2、有template的情况下会直接将template编译成render函数。否则则会在外层套一个div作为template返回。当然,如果传入的template是一个dom的话则取innerHTMl
接下来我们仔细观察一下这个mount函数是怎么运作的。
上面这个函数的最初有这么一句话
const mount = Vue.prototype.$mount
这里实际上是对$mount做了一个缓存。$mount 最初的定义实际上在vue/src/platforms/web/runtime/index.js里,这里附上代码
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
而这段代码中的核心函数mountComponent则在lifecycle.js这个文件里,这里附上注释代码。
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) { //如果template没有编译成render函数则走入这个warning分支
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) { //这种情况实际上代表着检测到template但未被编译,可能是Runtime-only的仅运行版本的问题
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount') //调用beforeMount的hook函数
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => { //生产环境测速相关
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else { //这里实际上声明了一个updateComponent,后续需要用到
updateComponent = () => {
vm._update(vm._render(), hydrating) //这句实际上是最关键的渲染语句,后续会分析
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, { //new了一个渲染Watcher,
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
上述代码我觉得需要特别关注的是new了一个Watcher,这个watcher的具体构造函数可以去src\core\observer\watcher.js查看,实际上这个就是所谓的观察者模式了,传入的第二个参数updateComponent函数并不是只有在初始化mount的时候执行,就像他的名字一样,一旦数据发生了改变,就会执行这个更新函数,也就是这个背后是有一个依赖收集的逻辑的,具体的我们后续分析。我们现在聚焦到这里最重要的一行代码。
vm._update(vm._render(), hydrating)
篇幅原因, render函数看下一章节。
大佬