闲来无事,在 github 上 down 了一个简单的小程序源码,然后本地环境打包得到运行时源码,可以看到每一个 vue 文件都打包成了四个文件:js、json、wxml、wxss 另外还有个 map 暂且不算在内,类比一下,用 vue 开发 web 应用的时候是不是也是将 vue 打包成了 html、js、css 了?所以运行时就是看这几个文件,json 文件是属于配置文件可以先不看,先看看 wxml 和 wxss(我挑了其中内容最多的一个文件来,以下内容都围绕这个文件来解析,文件路径为src/pages/index/index.vue)
wxss
先说 wxss,index.vue打包出来的 wxss 路径为:dist/dev/mp-weixin/pages/index/index.wxss,以下是打包出来的代码,几乎没什么变化;
css
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
/* 文字基本颜色 */
/* 背景颜色 */
/* 边框颜色 */
/* 尺寸变量 */
/* 文字尺寸 */
/* 图片尺寸 */
/* Border Radius */
/* 水平间距 */
/* 垂直间距 */
/* 透明度 */
/* 文章场景相关 */
page {
background-color: #f7f7f7;
height: 100%;
overflow: hidden;
}
wxml
wxml变化也不是很大,主要就是把 Vue 的一些指令换成小程序的指令,谁叫小程序也是类 Vue 语法呢,转换就是很顺滑啊:
html
转换前
<template>
<CustomNav />
<XtxSwiper :banner-list="swiperData" />
<CategoryPanel :list="cateData" />
<HotPanel :list="mutliData" />
</template>
转换后
<custom-nav u-i="2d8575ed-0" bind:__l="__l"/>
<xtx-swiper wx:if="{{a}}" u-i="2d8575ed-1" bind:__l="__l" u-p="{{a}}"/>
<category-panel wx:if="{{b}}" u-i="2d8575ed-2" bind:__l="__l" u-p="{{b}}"/>
<hot-panel wx:if="{{c}}" u-i="2d8575ed-3" bind:__l="__l" u-p="{{c}}"/>
页面渲染逻辑
很好奇为啥不改名叫 wxjs,那不就成了一套了吗??js 是小程序运行时的关键环节,首先来看一下打包出来的代码吧:
js
"use strict";
const common_vendor = require("../../common/vendor.js");
const services_home = require("../../services/home.js");
require("../../utils/http.js");
require("../../stores/index.js");
require("../../stores/modules/member.js");
if (!Array) {
const _easycom_XtxSwiper2 = common_vendor.resolveComponent("XtxSwiper");
_easycom_XtxSwiper2();
}
const _easycom_XtxSwiper = () => "../../components/XtxSwiper.js";
if (!Math) {
(CustomNav + _easycom_XtxSwiper + CategoryPanel + HotPanel)();
}
const CustomNav = () => "./components/CustomNav.js";
const CategoryPanel = () => "./components/CategoryPanel.js";
const HotPanel = () => "./components/HotPanel.js";
const _sfc_main = /* @__PURE__ */ common_vendor.defineComponent({
__name: "index",
setup(__props) {
const swiperData = common_vendor.ref([]);
const getBanner = async () => {
let res = await services_home.getHomeSwiperAPI();
swiperData.value = res.result;
console.log("res", res);
};
const cateData = common_vendor.ref([]);
const getCategory = async () => {
let res = await services_home.getHomeCategoryAPI();
cateData.value = res.result;
console.log("res", res);
};
const mutliData = common_vendor.ref([]);
const getMutli = async () => {
let res = await services_home.getHomeMutliAPI();
mutliData.value = res.result;
console.log("res", res);
};
common_vendor.onLoad(() => {
getBanner();
getCategory();
getMutli();
});
return (_ctx, _cache) => {
return {
a: common_vendor.p({
["banner-list"]: swiperData.value
}),
b: common_vendor.p({
list: cateData.value
}),
c: common_vendor.p({
list: mutliData.value
})
};
};
}
});
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__file", "/Users/zhuxiansheng/Documents/work/sdi-track/packages/Uniapp-Store-heima/src/pages/index/index.vue"]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=index.js.map
看了上面的源代码之后会发现基本上和自己写的源代码没啥差别,都是在 onLoad 中请求一下数据,大致梳理一下有以下几个流程:
可以注意到前面三个步骤都是自己写的代码逻辑,真正的第四步才是突然冒出来的逻辑,着重看一下这个 wx.createPage,本来以为这是一个官方提供的原生方法,然后在官网搜了一大圈,居然搜不到!搜不到!搜不到!那这是怎么回事呢?那只能是在 uni 里面手动将 createPage 这个方法挂在了 wx 上面;然后找到了这一部分代码:
js
const createApp = initCreateApp();
const createPage = initCreatePage(parseOptions);
const createComponent = initCreateComponent(parseOptions);
const createPluginApp = initCreatePluginApp();
const createSubpackageApp = initCreateSubpackageApp();
{
wx.createApp = global.createApp = createApp;
wx.createPage = createPage;
wx.createComponent = createComponent;
wx.createPluginApp = global.createPluginApp = createPluginApp;
wx.createSubpackageApp = global.createSubpackageApp = createSubpackageApp;
}
顺藤摸瓜就找到了 initCreatePage 的逻辑:
js
function initCreatePage(parseOptions2) {
return function createPage2(vuePageOptions) {
return Component(parsePage(vuePageOptions, parseOptions2));
};
}
可以明确的是这个 initCreatePage 又与两个方法 Component 和 parsePage 有关,先来看看 parsePage:
js
function parsePage(vueOptions, parseOptions2) {
const { parse, mocks: mocks2, isPage: isPage2, initRelation: initRelation2, handleLink: handleLink2, initLifetimes: initLifetimes2 } = parseOptions2;
const miniProgramPageOptions = parseComponent(vueOptions, {
mocks: mocks2,
isPage: isPage2,
initRelation: initRelation2,
handleLink: handleLink2,
initLifetimes: initLifetimes2
});
initPageProps(miniProgramPageOptions, (vueOptions.default || vueOptions).props);
const methods = miniProgramPageOptions.methods;
methods.onLoad = function(query) {
this.options = query;
this.$page = {
fullPath: addLeadingSlash(this.route + stringifyQuery(query))
};
return this.$vm && this.$vm.$callHook(ON_LOAD, query);
};
initHooks(methods, PAGE_INIT_HOOKS);
{
initUnknownHooks(methods, vueOptions);
}
initRuntimeHooks(methods, vueOptions.__runtimeHooks);
initMixinRuntimeHooks(methods);
parse && parse(miniProgramPageOptions, { handleLink: handleLink2 });
return miniProgramPageOptions;
}
parsePage方法又主要调用了以下方法:
parseComponent:将 Vue 中的 Mixins 提取出来并合并到 VueOptions 对象中,组装mpComponentOptions的一些属性,包含options、lifetimes、pageLifetimes、methods、properties、observers(挂载了更新组件的方法)
initPageProps:将 Vue 的 props 转化成 小程序中的 properties,对比一下两者的差异;
Vue 中的 props 的格式是这样的:
ts
interface PropOptions<T = any, D = T> {
type?: PropType<T> | true | null;
required?: boolean;
default?: D | DefaultFactory<D> | null | undefined | object;
validator?(value: unknown, props: Data): boolean;
}
小程序 properties 定义:
定义段 | 类型 | 是否必填 | 描述 | 最低版本 |
---|---|---|---|---|
type | 是 | 属性的类型 | ||
optionalTypes | Array | 否 | 属性的类型(可以指定多个) | 2.6.5 |
value | 否 | 属性的初始值 | ||
observer | Function | 否 | 属性值变化时的回调函数 |
methods.onLoad:重写 onLoad 事件,为页面添加一些参数,例如 $page.fullPath
initHooks:用this.$vm.$callHook
调用底层的小程序钩子,其中包含这些钩子:onShow、onHide、onError、onThemeChange、onPageNotFound、onUnhandledRejection
initUnknownHooks:注册 uni-app 定义的一些钩子
initRuntimeHooks:注册onPageScroll、onShareAppMessage、onShareTimeline
parse:用户传入的自定义 parse 方法,如果不存在则不执行
截取一个 parsePage 之后得到的结果看一下(其中 todos 是我在组件中定义的 props,这里面涉及到的方法都没有打印出来,因为 JSON.stringify会忽略函数):
js
{
"options": {
"multipleSlots": true,
"addGlobalClass": true,
"pureDataPattern": {}
},
"lifetimes": {},
"pageLifetimes": {},
"methods": {},
"data": {},
"behaviors": [],
"properties": {
"eO": {
"type": null,
"value": ""
},
"uR": {
"type": null,
"value": ""
},
"uRIF": {
"type": null,
"value": ""
},
"uI": {
"type": null,
"value": ""
},
"uT": {
"type": null,
"value": ""
},
"uP": {
"type": null,
"value": ""
},
"uS": {
"type": null,
"value": []
},
"todos": {
"value": []
}
},
"observers": {}
}
把这个结果又传递给了 Component 方法,Component 方法经历了以下四个过程:
initMiniProgramHook:初始化 created 生命周期(注意这个 created 与 vue 中的 created 不一样,它不能调用 setData 更新数据)
MPComponent:MPComponent 就是小程序全局的 Component 方法,所以 wx.createPage归根结底就是调用了 Component 方法(跳转微信小程序查看 Component 的参数)
至此,首次渲染就完成了,那么当与页面交互时,比如说点击页面的计数器,此时是监听 setter 然后直接调用小程序实例的 setData 吗?
页面更新逻辑
并不是,它还是要走 Vue 的 diff 逻辑,然后将发生变化的变量去进行批量的 setData,这比多次调用 setData 性能更好;
简单地做个总结:原生的微信小程序的运行时逻辑是这样的,渲染层和逻辑层之间通过微信客户端进行通信,当逻辑层 setData 时会将最新数据传递给微信客户端,然后微信客户端会通知渲染层更新页面:
当使用了 uni-app 之后,在逻辑层前面多了一个 Vue 运行时,这个运行时主要是继承了 Vue 的响应式原理,通过 Porxy 创建了代理对象,然后对 get、set 进行监听,当触发 set 之后会进行 diff 然后得到 diff 之后的对象,这些值都是发生了改变的值,最后执行 setData 进行批量更新: