声明式渲染
用模板语法声明式地将数据渲染至DOM
<div id="app">
{{ message }}
</div>
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
数据和DOM建立关联,数据是响应式的。
new Vue 发生了什么
new关键字将创建一个实例,所以Vue是一个构造函数。
我们打开vue源文件,找到core/instance/index.js文件,可以看到以下代码,这就是找的Vue函数。
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方法。Vue方法首先判断当前是否是生产环境,判断有没有用new关键字创建Vue实例(如果new了实例,this会指向实例,实例instanceof Vue 返回true)。然后调用_init方法。_init中的options参数就是上述{el: '#app',data: {message: 'Hello Vue!'}}
部分。
调用initMixin方法,查看initMixin方法,看到之前flow语法,详情:vuejs源码讲解从flow.js开始——一个JavaScript 静态类型检测工具
initMixin接收一个类作为参数,在类(Vue)的原型上添加了_init方法。_init接收可选的接口为Object的options参数。在这个_init方法内,主要是初始化(vm、生命周期、事件等)和合并options。
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)
}
// 避免被观察的标志
vm._isVue = true
// 合并 options
if (options && options._isComponent) {
//优化内部组件实例化
//因为动态选项合并非常慢,
//内部组件选项需要特殊处理。
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) // 在data/pops之前注入
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) { // 判断el,执行$mount 挂载Dom
vm.$mount(vm.$options.el)
}
}
}
假设在vue项目初始化的时候我们选择了runtime版本,首先来看entry-runtime-with-compiler.js,在这里我们可以看到判断el将实例挂载到#app节点上的操作, m o u n t 。 mount。 mount。mount在Vue原型上创建,判断el是不是body和HTML标签,如果是而且在生产环境则警告用户不能将模板挂载在body和HTML标签上。如果options上没有render就开始检测如果el传进来的是字符串,那么检测是不是#开头,再根据环境和存不存在这个DOM元素来判断有没有模板。如果el不是字符串,那就检测用户编写的模板是不是DOM节点( document.getElementByID(’#id’))。处理最后的结果都是赋值给template变量,为了转化为render函数。【详情看下一篇render】
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)+
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to or - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
if (!options.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 = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
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)
}
上述idToTemplate方法中判断DOM元素存不存在调用的是query方法,该方法主要使用querySelector查找的dom元素。
export function query (el: string | Element): Element {
if (typeof el === 'string') {
const selected = document.querySelector(el)
if (!selected) {
process.env.NODE_ENV !== 'production' && warn(
'Cannot find element: ' + el
)
return document.createElement('div')
}
return selected
} else {
return el
}
}
我们找到initState方法,initState方法调用了一些初始化props、methods、data、computed、watch的方法。
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方法,该方法首先判断data是不是函数,因为我们既可以data(){}添加data数据,也可以用data:{}形式添加。如果是函数则调用getData方法,接收一个接口为Function的data参数和Vue实例,改变data中this上下文,返回data。
function initData (vm: Component) {
let data = vm.$options.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
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') {
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 */)
}
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
至此我们知道了执行new Vue主要发生了什么。
相关链接:
vuejs源码讲解从flow.js开始——一个JavaScript 静态类型检测工具
vue源码讲解系列之二------ render()函数、VNode虚拟节点的实现