物有本末,事有终始,知所先后,则近道矣 ---《大学》
在分析Vue初始化之前,我们先看看Vue源码的目录结构:
其中我们重点关注的是compiler(编译部分)、core(核心模块)、platforms(平台相关的 我们重点看web),这三部分就已经包含了我们最为常用的功能。我们先从入口看起,不过在此之前,我们先从最简单的一个例子看起:
html:
<div id="demo">{{message}}</div>
js:
new Vue({
el: '#demo',
data: {
message:"hello Vue!"
}
})
执行完毕之后页面展示hello Vue,显然data里面的message和html似乎建立起了一种关联关系,当我们修改data里面的message时候便会发现html元素也会变化(甚至还有双向绑定---即html元素变化,data里面的message也会变化;这个后面单独拿一篇文章细聊实现)。这个思路和我们之前用Jquery操作完全不同,之前我们都是指哪儿打哪儿的,想修改dom直接选择dom节点操作,现在Vue不是这样了,他相当于给dom节点建立一个数据模型,我们不能直接动dom了,想修改dom操作数据模型就可以了。那我们就从入口说起,显然new Vue相当于实例化一个对象,那开头先执行的必定是构造函数了,如下所示(core/index.js):
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)
Vue源码中我们可能会碰到很多process这个对象,这个其实是nodejs中的对象,主要是npm在构建最终vue包时可以根据当前环境给一些提示,对我们理解源码其实没啥太大作用,所以实际上如果去掉process部分,上面Vue的构造函数就一行代码:this._init(options)
然后下面有五行***Mixin,这五个函数分别在Vue原型上添加了各种方法,比如我们调用this._init,这个__init方法为啥能调用呢?就是因为在initMixin中将__init方法加到Vue原型上了:
function initMixin (Vue) {
Vue.prototype._init = function (options) {
....
}
}
其他几个函数也是类似,在Vue.prototype上挂了很多方法,比如stateMixin中:
function stateMixin (Vue) {
...
Object.defineProperty(Vue.prototype, '$data', dataDef);
Object.defineProperty(Vue.prototype, '$props', propsDef);
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
Vue.prototype.$watch = function ()
....
}
我们可以看到在这里面在Vue原型挂了很多我们熟悉的方法,比如$set/$watch等等,所以我们经常才能这么用this.$watch(...)。
我们继续看this._init(options),那options是什么呢?在我们给的例子当中,options就是传入的:
{
el: '#demo',
data: {
message:"hello Vue!"
}
}
上文分析过this._init方法是在initMixin里面定义的,所以下一步就看core/instance/init.js中的_init函数了(如果是用VSCode看源码的话 按住Ctrl键 然后鼠标点击某个函数就进入了这个函数的实现):
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)
}
}
看上去很长,但是去掉process以及部分打日志的代码,其实这个函数干的事主要是:
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
)
}
vm._renderProxy = vm
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')
可以看到我们传入的options是没有_isComponent的,所以我们上文的例子会走到else里面,那啥时可以走到_isComponent里面呢?我们知道Vue里面还有一个强大的功能--组件,我们可以通过自己自定义一些基础组件然后通过拼装组件构成最终页面达到最高的代码复用率,这部分初始化我们将在下一篇文章中介绍。
我们接着说,上文例子中会走到else里面,可以看到这里面是将传入的options和Vue里面的options进行合并,毕竟传入的是用户想用设置的值,Vue里面肯定很多选项,如果用户没有传入,就会取默认值。上文我们的例子就传入el和data两个。然后下面又是一堆初始化函数,同时可以看到两个调用钩子的时间点:beforeCreate和created;在Vue官网文档中有一个很醒目的Vue声明周期的图:
可以看到前两步正和我们现在的代码相对应,initLifecycle是初始化生命周期(core/instance/lifecycle.js),initEvent是初始化事件处理机制(core/instance/events.js),这两步完成之后就认为是beforeCreate状态了,紧接着进行的是initInjections(core/instance/inject.js)、initState(core/instance/state.js)以及initProvide(core/instance/inject.js)了,其中initInjections和initProvide是跟父子组件通信相关的,这里不细说,而initState是重中之重,在这里面把data、props、watch、计算属性等等全部初始化完毕,所以这个initState是初始化过程要重点关注的一个函数,上代码:
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)
}
}
我们传入的目前只有data,所以会调用到initData(vm)这个方法中:
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 */)
}
这里开头判断data是一个对象还是函数,如果是函数,那么就调用这个函数得到一个对象。我们例子中传入的是对象,所以会继续走到while循环中,循环里面会判断一定data数据的key是否在methods或者props里面出现,毕竟不能重复嘛!我们data对象中只有一个"message":"hello Vue",key即message,没有重复,所以会走到isReserved(key)这个逻辑中,这个函数在core/utils/lang.js中:
export function isReserved (str: string): boolean {
const c = (str + '').charCodeAt(0)
return c === 0x24 || c === 0x5F
}
其中0x24以及0x5F分别是$和_的ASCII码,vue实例中有些变量会以$开头表示是特定用途的,比如$data/$props/$methods等等,而_在js中通常表示私有属性(private),所以我们自己在data中定义的数据的key不允许以上述两个ASCII码开头;而示例程序中的key为message显然isReserved判断之后返回false,再取非返回true,所以会走入proxy(vm, '_data', key)中,这里面是实现了代理模式,代码如下:
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)
}
我们上文已经介绍过Object.defineProperty了,这里实现的功能是啥呢?比如我们经常像这样在Vue里面访问data里面的数据--console.log(this.message),可是我们知道这个message明明是在data下面的,不应该是这么使用嘛---console.log(this.data.message)?确实这么调用肯定是可以的,不过太过繁琐,所以Vue这里实现了对message的代理,当我们访问this.message的时候,就会调用到
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
而这里返回的是this._data.key,在程序开头可以看到this._data就是data,因此我们便可以通过this.message直接访问data里面的message了,这里面其实就一个知识点:Object.defineProperty,搞明白这个就理解这个设计了。
接下来就走到了 observe(data, true /* asRootData */),observe函数如下所示:
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
其实核心代码就一句: ob = new Observer(value)
啥意思呢? 这里是利用观察者模式实现的数据变化然后通知视图(html)变化的,这里面涉及到Vue对观察者模式的实现,这部分代码说实话细节颇多,稍微比较难懂,我们单独拿出一次文章聊这个事情。我们现在继续我们的初始化流程。可能大家已经忘了上面是从哪儿走到这里了,其实这是看源码的一个困难所在。由于现在开源项目通常代码规模都比较高,在看源码时经常会出现读着读着已经不知道读到哪儿了这种问题,我觉得这个是正常的。我个人是这么解决这个问题的:
1.从上至下。我会先忽略与主流程无关的代码(在看源码前要知道这个软件干啥的,最起码对主流程很熟悉),先将主流程看明白,或者退一步,主流程上的代码看懂一句是一句,看不懂先略过。然后结合最简单的一个例子,通过debug的方式,先走一遍混个脸熟,我们在准备篇最后曾经给过一个Vue源码调试环境搭建的视频,按照视频做完就可以实现在Vue源码中任何一处加入断点调试Vue源码啦。
2.从下至上。有些源码虽然主流程好懂,可是顺着看就是看不懂或者说总是有不顺的地方。比如我在看Activemq的源码时,主链路总是缺一环。所以我采取的方式是从下至上,先看懂某个模块的代码,比如kahadb的实现,这部分代码相比总体源码代码量少很多,较容易看懂,在看懂这个模块的基础上,我再考虑这个模块在总体代码中的位置,逐渐去理解总体源码。
这两种方式,我通常更喜欢第一种,偶尔碰到困难就从第二种入手,Vue采用的就是第一种,Activemq或者Linux源码就是第二种。读者可以按照我说的试试,我感觉总会有收获的。
继续我们的初始化流程,刚才是_init里面的initState走到initData中的链路,其实_init还没有执行完毕呢,还有最后一句very重要的:
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
这里面el我们传入的是"#demo", 所以上面便是vm.$mount("#demo"),这里又有知识点了:如果你在Vue源码中查询Vue.prototype.$mount的话,会发现这个出现了多次:
platforms/web/runtime/index.js
platforms/weex/runtime/index.js
platforms/web/entry-runtime-with-compiler.js
platforms下面存储的都是和平台相关的,我们多数使用的web,所以只关注web目录下的js文件,依然还剩两个文件,这两个文件里面都有Vue.prototype.$mount = function(){....},而且两个函数还不一样,这到底是怎么回事?
Vue其实是有两个版本----Vue runtime only和Vue runtime + compiler,两个版本什么区别呢? 区别在于编译的时机不同。当我们真正用Vue进行开发时,如果采用Vue runtime + compiler的版本即带着编译功能代码的版本,那么编译的行为会发生在用户在浏览器访问网页的那时,显然编译还是比较浪费时间的;而Vue runtime only版本是不带编译代码的,这个时候模板编译为render函数这个行为是发生在打包那一刻(这也是为啥我们开发时会安装一个叫做vue-loader的插件),也就是说vue-loader这个插件在打包时会进行编译行为,最终生成的vue源码就不含编译代码了,这样用户访问网页就快了。 我们怎么验证呢? 读者可以用VSCode打开Vue源码之后,修改一下Vue源码路径下的scripts/config.js:
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
//entry: resolve('web/entry-runtime.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
}
我们在运行npm run dev的时候实际上这里可以选择带不带编译器,with-compiler自然是带了,下面的entry-runtime.js是不带的,那两者生成的最终的Vue.js有啥区别呢?见下图:
上面两张图所表示的代码是带有编译器打包之后的Vue.js源码,我们会发现这个时候Vue源码中出现过两次对Vue.prototype.$mount的定义,而如果不带编译器打包之后的Vue.js源码中是不会出现下面第二个图中的代码的-即整体代码里面只有一个Vue.prototype.$mount的定义。这里我们为了能够讲清楚Vue源码编译的过程,所以采用的是带有编译器的Vue版本。那问题来了,Vue.prototype.$mount定义了两次,那么执行的是哪一个呢?答案是:两个都执行了,且先执行的是第二个图的$mount,然后又调用了第一个图的$mount(而不带编译版本的Vue只会执行第一个图的$mount),为啥呢? 其实道理很简单,我们在用Vue之前,必然会先导入vue--<script src='...vue.js'>,在导入vue过程,程序必然是从上往下执行的,因为先会执行上面的第一个图,然后在执行第二个图的时候,有一句很关键的代码:
var mount=Vue.prototype.$mount
这里会先存一下之前的$mount函数,然后再在Vue.prototype上加了一个新的$mount函数,然后当我们前面vm.$mount调用时当然会进入到第二个新加的$mount之中(当然我是针对带有编译版本的Vue源码而言的),那第二个$mount干了啥呢?上代码(platforms/web/entry-runtime-with-compiler.js):
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 <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
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)
}
关键的这一句:
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
这句代码顺着走进去会走到一个非常清晰的地方(compiler/index.js):
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
编译三部曲: 1. 获得AST(抽象语法树) 2. 优化之 3.生成render函数
上面compileToFunctions是如何走到这个地方的,我是省略了N个字,实际上这里面是很有技巧的(函数柯里化),我们第一篇介绍过,这里留给读者自己玩味玩味。
那看看我们的例子里面生成的AST以及render函数是啥样子呢?
我们在Vue源码获取ast之前写上一行 debugger,然后将网页调试台打开,刷新一下网页便可以在断点处停住(当然需要设置一点点东西,具体设置参照https://www.bilibili.com/video/av20149603/),我们单步运行看看ast和code.render到底是啥东西?
AST(抽象语法树)其实就是个树型结构,由于我们的网页结构很简单,所以父节点是<div id="demo">,children是所有子节点,我们的子节点现在只有一个,就是{{message}}的文本节点。注意看我们上上张打断点那个图,在初始化流程走到此处时,网页显示还是“{{message}}”呢,这个时候其实Vue还没有起作用,下面流程就会将“{{message}}”替换为真正的值(在vm._update之中干的)。
上图便是生成的render函数字符串, with是为了下面_c、_v、_s这些函数调用的时候不用加this,否则我们要这么调用this._c、this._v、this._s(想了解js with用法可以简单看看这个文档:https://www.jb51.net/article/12326.htm)
这目前只是字符串,如何转变为render函数的呢? Vue源码中一句话搞定,new Function(...)(想了解Function可以参考文档:https://yq.aliyun.com/articles/589449)。那这里_c/_v/_s等等是啥呢?这个问题我打算在后面介绍编译的文章再细讲,这里作个类比吧,写java的人都知道字节码,其实字节码是固定的,那么javac这个程序需要将我们写的java程序转变为字节码;在这里可以将_c和_v和_s等看做是系统已经有的字节码,我们需要将静态html模板编译为这些"字节码"表示的形式(后续文章会细说这个事)。
我们继续我们的初始化流程,在这里获得render函数之后会走接着走(我们之前是在第二个$mount函数里),在第二个$mount函数里面最后一句是:
return mount.call(this, el, hydrating)
这句会走到第一个定义的$mount函数中,即走到下面这部分代码:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
这里显然el显然有定义,所以会调用query(el)根据该选择符获得真实的dom元素,其实query就是document.querySelector(el)的封装,调用完毕后,再会调用mountComponent函数(core/instance/lifecycle.js):
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
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 {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
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 = () => {
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) {
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
}
去掉无用代码可以简化为:
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
这里又可以看到三个生命周期的阶段:beforeMount、beforeUpdate以及mounted,看这段代码最好和Vue官网那一张vue生命周期图对比看看。
这段代码最重要的是vm._render生成Vnode以及vm._update负责新建、更新或者销毁dom节点,而下面这个new Watcher便是实例化一个watcher关注着数据的变化,只要数据变化便会再次调用vm._render以及vm._update两个函数去更新变化,当然第一次走到这里时会先调用一次-新建dom节点。vm._render这个函数是从哪儿来的呢?是在最初的core/index.js中初始化流程中有个renderMixin函数中定义的_render函数,如下所示:
Vue.prototype._render = function (): VNode {
const vm: Component = this
const { render, _parentVnode } = vm.$options
// reset _rendered flag on slots for duplicate slot check
if (process.env.NODE_ENV !== 'production') {
for (const key in vm.$slots) {
// $flow-disable-line
vm.$slots[key]._rendered = false
}
}
if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
}
// 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 {
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') {
if (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
}
} else {
vnode = vm._vnode
}
}
// 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
}
从源码中可以看到,vnode是通过render函数生成并返回的,而render函数自然是在上文所介绍的编译流程生成的(new Function(code.render)),调用这个生成的render函数会得到所写html即<div id="demo">{{message}}</div>的Vnode表示,我们加入断点看看生成的Vnode啥样:
注意看我加断点的位置,这个时候vnode已经生成了,但是网页还是显示的是{{message}},可能有的读者已经急了,咋还木有更新呀? 别着急,就是下一句了,在此之前,先看看生成的vnode啥样:
注意看tag为div是父Vnode,然后子节点有一个,text为"Hello Vue",注意这个时候网页还是{{message}},因为这个时候Vnode只是在内存里,还没有和真实dom建立关系,所以真实dom还没有变化,那啥时变化的呢? 当我们运行完vm._update之后,Vue便会根据Vnode去更新真实Dom,最终才展现出结果。
至此,初始化流程结束。但是我们有两个细节没有说透:
1、
updateComponent = () => {
debugger
const vnode = vm._render()
vm._update(vnode, hydrating)
}
代码走到上图代码时仅仅是定义了一个函数,这里并没有执行_render和_update,真正去执行的是在这一句代码的下一句:
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
这个new Watcher中才去调用的_render和_update,至于如何调用的,我们在观察者模式实现篇再细聊。
2、
由于第一次调用_render和_update的时候实际上是新建,这个时候其实并不是像有些人想的将节点中的{{message}}替换为Hello Vue,实际过程看看下图大家就明白了:
这部分将会在dom diff算法的时候再具体分析。
虽然洋洋洒洒写了数千字,但实际上初始化流程主要有三个方面:
虽然略显粗糙,但我们将逐步细化!
初始化上篇完结!