mpvue 生命周期源码分析
mpvue 源码调试
通过 mpvue 的 webpack 打包配置我们知道 mpvue 及其他一些库文件被打包入 dist/wx/common/vendor.js 文件中,所以实际调试的是 vendor.js,这里我们有两种调试选择:在代码中加入 debugger 指令或在 vendor.js 中加入断点。
debugger 调试
我们可以在 JS 脚本中加入断点指定 debugger 进入调试流程,然后从 debugger 的位置再跳入 mpvue 的源码,如我们在 main.js 中加入 debugger:
/* main.js */
import Vue from 'vue'
import App from './App'
Vue.config.productionTip = false
App.mpType = 'app'
debugger
const app = new Vue(App)
app.$mount()
小程序运行后,会在执行 const app = new Vue(App) 之前被中断:
我们可以看到 main.js 被 webpack 编译成了 app.js,并且源码内容也进行了一些变更,造成这些变化的主要原因是 webpack 采用原生 js 的语法实现了模块化编译导致的,这点并不会影响我们调试程序,我们只需要关注自身的源码逻辑即可。此时我们可以按 F11 或点击调试面板中的 step into 按钮进入 Vue 的构造函数代码中:
注:直接点击 step into 会跳入 webpack 的模块化加载方法中,此时继续点击 step into 或 step over 执行即可,直到进入 Vue 的代码逻辑中
进入 Vue 的构造函数后,我们可以看到 Vue 的源码被集成进 vendor.js 中,同时在右侧的 Call Stack 程序执行栈中也可以看到我们从 app.js 跳入了 vendor.js 的 Vue 构造函数中了:
至此我们就可以愉快地调试 mpvue 源码了。
断点调试
我们还可以直接在 vendor.js 中加入断点的方式来调试 mpvue 源码。首先在微信小程序开发工具 Sources 标签下,找到 vendor.js 源码中 Vue 构造函数的位置,然后在代码前加入断点:
这样在所有调用 Vue 实例化的源码处,都会自动中断,我们可以对代码进行调试。
mpvue 生命周期源码分析
下面我们通过调试的方式了解 mpvue 的执行阶段,这样有助于我们进一步了解 mpvue 的生命周期。Vue 的实例化从 _init 方法开始:
function Vue$3 (options) {
if (false
) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}
_init 方法完成了整个 Vue 的实例化过程,它的核心源码如下:
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');
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
整个 mpvue 的生命周期就包含在上述代码中。
beforeCreate
beforeCreate 之前主要指向了一些初始化操作,为 Vue 实例一些必要属性和方法,这部分逻辑较少,值得注意的是 initRender (vm) 方法中对 render 函数中使用的方法进行了定义:
function initRender (vm) {
// ...
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
// ...
}
vm.$createElement 是在 Web 开发下 Vue 渲染界面的核心方法,但是在小程序中这一方法并不需要,因为渲染的逻辑方法了改变,小程序通过 wxml 对界面进行渲染。当 beforeCreate 之前的方法被执行完毕后,Vue 会调用我们自定义的 beforeCreate 生命周期函数:callHook(vm, 'beforeCreate'),如果定义了多个 beforeCreate 则会依次进行执行:
created
created 之前会执行 initInjections 和 initProvide 方法,这两个方法是为了初始化 Vue 的组件化通信机制:inject 和 provide 特性,我们可以先不关注,重点是看 initState 方法:
function initState (vm) {
vm._watchers = [];
var 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);
}
}
initState 方法非常重要,因为它完成了 props、methods、data、computed 和 watch 5 个属性的初始化,这也是为什么在 created 生命周期函数执行时,可以通过如 this.data 或 this.props 获得属性值的原因。在以上三个初始化方法执行后,Vue 会调用我们自定义的 created 生命周期函数。
小程序生命周期
由于我们实例化 Vue 时未传入 el 参数,所以下列代码不会执行:
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
此时 Vue 实例化已经完成,程序继续执行 main.js 中的 $mount 方法:
const app = new Vue(App)
// Vue 已经实例化完成,将执行 $mount 方法
app.$mount()
$mount 是 Vue 原型上的方法,也就是说必须实例化后才能访问,所以我们需要先实例化 Vue 之后才能访问该方法,它的源码如下:
Vue$3.prototype.$mount = function (el, hydrating) {
var this$1 = this;
var options = this.$options;
if (options && (options.render || options.mpType)) {
var mpType = options.mpType; if ( mpType === void 0 ) mpType = 'page';
return this._initMP(mpType, function () {
return mountComponent(this$1, undefined, undefined)
})
} else {
return mountComponent(this, undefined, undefined)
}
}
通过上述源码可以看到,$mount 方法的主要用途判断 mpType 的类型,并且实例化 App 或 Page,这是 mpvue 和 Vue 不同之处,mpvue 将不执行界面的更新动作,都将交由小程序框架程序。实例化 Page 后小程序框架会完成界面的渲染。首先看下面一行代码:
var mpType = options.mpType; if ( mpType === void 0 ) mpType = 'page';
它从 options 中获取 mpType 参数,如果 mpType 为 undefined(void 0),则 mpType 的默认值为 page,也就是说 App 类型的 Vue 实例,必须手动指定 mpType,上述 main.js 源码中指定 mpType 为 app,所以它是一个 App 实例:
/* main.js */
App.mpType = 'app' // 指定 App.mpType = 'app',表明这是一个 App 实例
const app = new Vue(App)
app.$mount()
明确 mpType 后就执行 _initMp 方法:
return this._initMP(mpType, function () {
return mountComponent(this$1, undefined, undefined)
})
_initMp 方法包含两个参数:mpType 和一个回调函数,它的核心源码如下,由于 _initMp 方法源码较多,所以这里只展示与生命周期相关的内容:
function initMP (mpType, next) {
var rootVueVM = this.$root;
if (!rootVueVM.$mp) {
rootVueVM.$mp = {};
}
var mp = rootVueVM.$mp;
mp.mpType = mpType;
if (mpType === 'app') {
global.App({
onLaunch: function onLaunch (options) {
callHook$1(rootVueVM, 'onLaunch', options);
next();
},
onShow: function onShow (options) {
callHook$1(rootVueVM, 'onShow', options);
}
});
} else {
var app = global.getApp();
global.Page({
onLoad: function onLoad (query) {
callHook$1(rootVueVM, 'onLoad', query);
},
onShow: function onShow () {
callHook$1(rootVueVM, 'onShow');
},
onReady: function onReady () {
callHook$1(rootVueVM, 'onReady');
next();
},
// 省略了其他生命周期函数定义
});
}
}
上述源码核心有两点:
- 第一,当 mpType 为 app 时,调用 global.App 方法实例化了一个 App 对象,在 App 构造器对象中执行了在 Vue 实例中定义的 onLaunch 和 onShow 生命周期函数。在 onLaunch 执行完毕后会调用我们传入的回调函数 next,继续完成 Vue 的生命周期。
- 第二,当 mpType 为 page 时,调用 global.Page 方法实例化了一个 Page 对象,在 Page 构造器对象中执行了在 Vue 实例中定义的 onLoad、onShow、onReady 等生命周期函数。在 onReady 执行完毕后会调用我们传入的回调函数 next,继续完成 Vue 的生命周期。
需要注意的是虽然这些生命周期函数定义在 Vue 中,但执行过程仍有小程序进行控制。
breforeMount
在 next 回调函数执行后,会继续执行 mountComponent 方法:
return this._initMP(mpType, function () {
return mountComponent(this$1, undefined, undefined)
})
mountComponent 方法核心源码如下:
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
}
callHook(vm, 'beforeMount');
var updateComponent = function () {
vm._update(vm._render(), hydrating);
};
vm._watcher = new Watcher(vm, updateComponent, noop);
hydrating = false;
if (vm.$vnode == null) {
vm._isMounted = true;
callHook(vm, 'mounted');
}
return vm
}
可以看到在小程序生命周期执行完毕后会进入 mountComponent 方法,随后就会执行 beforeMount 生命周期函数。
mounted
我们继续分析 mountComponent 方法,在 beforeMount 后,Vue 会执行一个关键步骤实例化渲染 Watcher:
vm._watcher = new Watcher(vm, updateComponent, noop);
这一步是实现 mpvue 响应式的关键,虽然 mpvue 并不依赖 Vue 进行渲染,但是仍然需要完成响应式功能,即修改 data、props 等对象的属性时,能够动态触发界面变更,这一步仍然需要 Vue 的特性来实现,这样我们才能使用 Vue 中的各种优质特性。实例化渲染 Watcher 后会调用 mounted 生命周期函数。至此整个 mpvue 的实例化加渲染过程完成。