1. 思考
不知道大家是否思考过new Vue()这个过程中究竟做了些什么?
过程中是如何完成数据的绑定,又是如何将数据渲染到视图的等等
2. 源码解析
首先找到vue的构造函数
源码位置:node_modules/vue/src/core/instance/index.js(ps:找不到可以在node_modules目录下搜索,因为懒惰后边就不写node_modules/vue这两级目录了)
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) | |
} |
options
是用户传递过来的配置项,如data
、methods
等常用的方法
vue
构建函数调用_init
方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法
initMixin(Vue); // 定义 _init | |
stateMixin(Vue); // 定义 $set $get $delete $watch 等 | |
eventsMixin(Vue); // 定义事件 $on $once $off $emit | |
lifecycleMixin(Vue);// 定义 _update $forceUpdate $destroy | |
renderMixin(Vue); // 定义 _render 返回虚拟dom |
首先可以看initMixin
方法,发现该方法在Vue
原型上定义了_init
方法
源码位置:src\core\instance\init.js
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 | |
// 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法 | |
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 { // 合并vue属性 | |
vm.$options = mergeOptions( | |
resolveConstructorOptions(vm.constructor), | |
options || {}, | |
vm | |
) | |
} | |
/* istanbul ignore else */ | |
if (process.env.NODE_ENV !== 'production') { | |
// 初始化proxy拦截器 | |
initProxy(vm) | |
} else { | |
vm._renderProxy = vm | |
} | |
// expose real self | |
vm._self = vm | |
// 初始化组件生命周期标志位 | |
initLifecycle(vm) | |
// 初始化组件事件侦听 | |
initEvents(vm) | |
// 初始化渲染方法 | |
initRender(vm) | |
callHook(vm, 'beforeCreate') | |
// 初始化依赖注入内容,在初始化data、props之前 | |
initInjections(vm) // resolve injections before data/props | |
// 初始化props/data/method/watch/methods | |
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) | |
} | |
} |
仔细阅读上面的代码,我们得到以下结论:
- 在调用beforeCreate之前,数据初始化并未完成,像data、props这些属性无法访问到
- 到了created的时候,数据已经初始化完成,能够访问data、props这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素
- 挂载方法是调用vm.$mount方法
initState方法是完成props/data/method/watch/methods的初始化
源码位置:src\core\instance\state.js
export function initState (vm: Component) { | |
// 初始化组件的watcher列表 | |
vm._watchers = [] | |
const opts = vm.$options | |
// 初始化props | |
if (opts.props) initProps(vm, opts.props) | |
// 初始化methods方法 | |
if (opts.methods) initMethods(vm, opts.methods) | |
if (opts.data) { | |
// 初始化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) | |
} | |
} |
我们和这里主要看初始化data的方法为initData,它与initState在同一文件上
function initData (vm: Component) { | |
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 | |
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 | |
) | |
} | |
} | |
// 属性名不能与state名称重复 | |
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)) { // 验证key值的合法性 | |
// 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据 | |
proxy(vm, `_data`, key) | |
} | |
} | |
// observe data | |
// 响应式监听data是数据的变化 | |
observe(data, true /* asRootData */) | |
} |
仔细阅读上面的代码,我们可以得到以下结论:
- 初始化顺序:props、methods、data
- data定义的时候可选择函数形式或者对象形式(组件只能为函数形式)
关于数据响应式在这就不展开详细说明
上文提到挂载方法是调用vm.$mount方法
源码位置:/src/platforms/web/entry-runtime-with-compiler.js
Vue.prototype.$mount = function ( | |
el?: string | Element, | |
hydrating?: boolean | |
): Component { | |
// 获取或查询元素 | |
el = el && query(el) | |
/* istanbul ignore if */ | |
// vue 不允许直接挂载到body或页面文档上 | |
if (el === document.body || el === document.documentElement) { | |
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) { | |
let template = options.template | |
// 存在template模板,解析vue模板文件 | |
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') | |
} | |
/** | |
* 1.将temmplate解析ast tree | |
* 2.将ast tree转换成render语法字符串 | |
* 3.生成render方法 | |
*/ | |
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) | |
} |
阅读上面代码,我们能得到以下结论:
- 不要将根元素放到body或者html上
- 可以在对象中定义template/render或者直接使用template、el表示元素选择器
- 最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数
对template的解析步骤大致分为以下几步:
- 将html文档片段解析成ast描述符
- 将ast描述符解析成字符串
- 生成render函数
生成render函数,挂载到vm上后,会再次调用mount方法
源码位置: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渲染组件
export function mountComponent ( | |
vm: Component, | |
el: ?Element, | |
hydrating?: boolean | |
): Component { | |
vm.$el = el | |
// 如果没有获取解析的render函数,则会抛出警告 | |
// render是解析模板文件生成的 | |
if (!vm.$options.render) { | |
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) { | |
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 { | |
// 没有获取到vue的模板文件 | |
warn( | |
'Failed to mount component: template or render function not defined.', | |
vm | |
) | |
} | |
} | |
} | |
// 执行beforeMount钩子 | |
callHook(vm, 'beforeMount') | |
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 = () => { | |
// 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render | |
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, { | |
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 | |
} |
阅读上面代码,我们得到以下结论:
- 会触发beforeCreate钩子
- 定义updateComponent渲染页面视图的方法
- 监听组件数据,一旦发生变化,触发beforeUpdate生命钩子
updateComponent方法主要执行在vue初始化时声明的render,update方法
render的作用主要是生成vnode
源码位置:src\core\instance\render.js
// 定义vue 原型上的render方法 | |
Vue.prototype._render = function (): VNode { | |
const vm: Component = this | |
// render函数来自于组件的option | |
const { render, _parentVnode } = vm.$options | |
if (_parentVnode) { | |
vm.$scopedSlots = normalizeScopedSlots( | |
_parentVnode.data.scopedSlots, | |
vm.$slots, | |
vm.$scopedSlots | |
) | |
} | |
// set parent vnode. this allows render functions to have access | |
// to the data on the placeholder node. | |
vm.$vnode = _parentVnode | |
// render self | |
let vnode | |
try { | |
// There's no need to maintain a stack because all render fns are called | |
// separately from one another. Nested component's render fns are called | |
// when parent component is patched. | |
currentRenderingInstance = vm | |
// 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode | |
vnode = render.call(vm._renderProxy, vm.$createElement) | |
} catch (e) { | |
handleError(e, vm, `render`) | |
// return error render result, | |
// or previous vnode to prevent render error causing blank component | |
/* istanbul ignore else */ | |
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) { | |
try { | |
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) | |
} catch (e) { | |
handleError(e, vm, `renderError`) | |
vnode = vm._vnode | |
} | |
} else { | |
vnode = vm._vnode | |
} | |
} finally { | |
currentRenderingInstance = null | |
} | |
// if the returned array contains only a single node, allow it | |
if (Array.isArray(vnode) && vnode.length === 1) { | |
vnode = vnode[0] | |
} | |
// return empty vnode in case the render function errored out | |
if (!(vnode instanceof VNode)) { | |
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { | |
warn( | |
'Multiple root nodes returned from render function. Render function ' + | |
'should return a single root node.', | |
vm | |
) | |
} | |
vnode = createEmptyVNode() | |
} | |
// set parent | |
vnode.parent = _parentVnode | |
return vnode | |
} |
_update主要功能是调用patch,将vnode转换为真实DOM,并且更新到页面中
源码位置:src\core\instance\lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { | |
const vm: Component = this | |
const prevEl = vm.$el | |
const prevVnode = vm._vnode | |
// 设置当前激活的作用域 | |
const restoreActiveInstance = setActiveInstance(vm) | |
vm._vnode = vnode | |
// Vue.prototype.__patch__ is injected in entry points | |
// based on the rendering backend used. | |
if (!prevVnode) { | |
// initial render | |
// 执行具体的挂载逻辑 | |
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) | |
} else { | |
// updates | |
vm.$el = vm.__patch__(prevVnode, vnode) | |
} | |
restoreActiveInstance() | |
// update __vue__ reference | |
if (prevEl) { | |
prevEl.__vue__ = null | |
} | |
if (vm.$el) { | |
vm.$el.__vue__ = vm | |
} | |
// if parent is an HOC, update its $el as well | |
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { | |
vm.$parent.$el = vm.$el | |
} | |
// updated hook is called by the scheduler to ensure that children are | |
// updated in a parent's updated hook. | |
} |
3. 结论
- new Vue的时候调用会调用_init方法 定义 set、get 、delete、watch 等方法定义 on、off、emit、off等事件定义 _update、forceUpdate、destroy生命周期
- 调用$mount进行页面的挂载
- 挂载的时候主要是通过mountComponent方法
- 定义updateComponent更新函数
- 执行render生成虚拟DOM
- _update将虚拟DOM生成真实DOM结构,并且渲染到页面中