Vue(v2.6.14)源码解毒(二)(初始化和挂载)

初始化流程 new Vue
我们在使用 Vue 的时候,首页就是先 new Vue(...) ;在上一章中通过分析构建流程,我们得出入口文件 src/platforms/web/entry-runtime-with-compiler.js ,通过入口文件,我们一步一步找到 Vue 构造函数定义所在:

// src/platforms/web/entry-runtime-with-compiler.js // ... import Vue from './runtime/index' // ...

// src/platforms/web/runtime/index.js import Vue from 'core/index' // ...

// src/core/index.js import Vue from './instance/index' import { initGlobalAPI } from './global-api/index' import { isServerRendering } from 'core/util/env' import { FunctionalRenderContext } from 'core/vdom/create-functional-component' // 初始化全局 API initGlobalAPI(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' // 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') } // 调用 Vue.prototype_init 方法,该方法是在 initMixin 中定义的 this._init(options) } // 定义 Vue.prototype_init 方法 initMixin(Vue) /** * 定义: *Vue.prototype.$data *Vue.prototype.$props *Vue.prototype.$set *Vue.prototype.$delete *Vue.prototype.$watch */ stateMixin(Vue) /** * 定义 事件相关的 方法: *Vue.prototype.$on *Vue.prototype.$once *Vue.prototype.$off *Vue.prototype.$emit */ eventsMixin(Vue) /** * 定义: *Vue.prototype._update *Vue.prototype.$forceUpdate *Vue.prototype.$destroy */ lifecycleMixin(Vue) /** * 定义: *Vue.prototype.$nextTick *Vue.prototype._render */ renderMixin(Vue) export default Vue

_init
// src/core/instance/init.js export function initMixin (Vue: Class) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // 每个实例都保存一个 _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 // 处理组件配置项 if (options && options._isComponent) { // 每个子组件初始化时走这里,这里只做了一些性能优化 // 将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率 initInternalComponent(vm, options) } else { // 合并选项,合并默认选项和自定义选项 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } // 设置代理,将 vm 实例上的属性代理到 vm._renderProxy /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm // 初始化实例关系属性,$parent、$children、$refs、$root等 initLifecycle(vm) // 初始化自定义事件,处理父组件传递的事件和回调 initEvents(vm) // 解析组件的插槽信息,得到 vm.$slot,处理渲染函数(_render),得到 vm.$createElement 方法,即 h 函数 initRender(vm) // 调用 beforeCreate 钩子函数 callHook(vm, 'beforeCreate') // 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象,然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例 initInjections(vm) // 数据响应式核心,处理 props、methods、data、computed、watch initState(vm) // 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上 initProvide(vm) // resolve provide after data/props // 调用 created 钩子函数 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) } } }

上面代码很清晰的看出初始化都做了哪些事情,在初始化的最后,如果有 el 属性,则会自动调用 vm.$mount 进行挂载,否则我们就需要手动调用 $mount。接下里就进入了挂载阶段。
Vue 实例挂载 $mount
入口文件 src/platforms/web/entry-runtime-with-compiler.js
/* @flow */ import config from 'core/config' import { warn, cached } from 'core/util/index' import { mark, measure } from 'core/util/perf' import Vue from './runtime/index' import { query } from './util/index' import { compileToFunctions } from './compiler/index' import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat' const idToTemplate = cached(id => { const el = query(id) return el && el.innerHTML }) /** * 编译器的入口 * 进行预编译,最终将模版编译成 render 函数 */ // 缓存原型上的方法 const mount = Vue.prototype.$mount // 重新定义该方法 Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) // 不能挂载在 body、html 这样的根节点上 if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue toor - mount to normal elements instead.` ) return this } const options = this.$options /** *若没有 render方法,则解析 template 和 el,并转换为 render 函数 *优先级:render > template > el */ if (!options.render) { let template = options.template // template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { // { template: '#app' },以 id 为 ‘app’ 的节点,作为挂载节点 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 是一个正常的元素,获取其 innerHtml 作为模版 template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { // 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, { // 在非生产环境下,编译时记录标签属性在模版字符串中开始和结束的位置索引 outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, // 界定符,默认 {{}} delimiters: options.delimiters, // 是否保留注释 comments: options.comments }, this) // 将两个渲染函数放到 this.$options 上 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) } /** * Get outerHTML of elements, taking care * of SVG elements in IE as well. */ function getOuterHTML (el: Element): string { if (el.outerHTML) { return el.outerHTML } else { const container = document.createElement('div') container.appendChild(el.cloneNode(true)) return container.innerHTML } } Vue.compile = compileToFunctions export default Vue

从上面代码可以看出,不管定义 render 方法还是 eltemplate 属性,最终的目的就是得到 render 渲染函数。然后保存在 options 上。
【Vue(v2.6.14)源码解毒(二)(初始化和挂载)】编译模板,得到 render 渲染函数,通过调用 compileToFunctions 方法,这个到编译器的时候再一块看。
最后调用原型上的 $mount ,定义在 src/platform/web/runtime/index.js
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }

实际调用 mountComponent ,定义在 src/core/instance/lifecycle.js
mountComponent
// src/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 { // 执行 vm._render() 函数,得到 虚拟 DOM,并将 vnode 传递给 _update 方法,接下来就该到 patch 阶段了 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 && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例 if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }

mountComponent 内定义了 updateComponent 方法,然后实例化一个Watcher,同时将 updateComponent 作为参数传入,在 Watcher 的回调函数中被调用。Watcher 在这里主要是初始化和数据变化时,执行回调函数。
最后设置 vm._isMounted = true ,表示实例已挂载。
updateComponent 的调用会执行 vm._updatevm._rendervm._render 获取虚拟DOM,vm._update 更新视图。
上面代码出现了三个生命周期钩子 beforeMountbeforeUpdatemounted ;也就是说,在执行 vm._render()之前,执行了 beforeMount 钩子函数;在执行完 vm._update()把虚拟DOM转换真实 DOM 后,执行 mounted 钩子函数;后续若数据变化时,通过 _isMounted 标记,表示已挂载则执行 beforeUpdate 钩子函数。
这里值得注意的是,在 mounted 钩子执行前有个判断,只有在父虚拟 Node 为 null 的时候执行。只有 new Vue 才会走到这里,如果是组件的话,它的父虚拟 Node 是存在的。组件的 mounted 在别的地方。
相关链接 Vue(v2.6.14)源码解毒(预):手写一个简易版Vue
Vue(v2.6.14)源码解毒(一):准备工作
Vue(v2.6.14)源码解毒(二):初始化和挂载
[Vue(v2.6.14)源码解毒(三):响应式原理(待续)]()
[Vue(v2.6.14)源码解毒(四):更新策略(待续)]()
[Vue(v2.6.14)源码解毒(五):render和VNode(待续)]()
[Vue(v2.6.14)源码解毒(六):update和patch(待续)]()
[Vue(v2.6.14)源码解毒(七):模板编译(待续)]()
如果觉得还凑合的话,给个赞吧!!!也可以来我的个人博客逛逛 https://www.mingme.net/

    推荐阅读