uni-app 运行时源码解析

闲来无事,在 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,以下是打包出来的代码,几乎没什么变化;

/**
 * 这里是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 语法呢,转换就是很顺滑啊:

转换前
<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 是小程序运行时的关键环节,首先来看一下打包出来的代码吧:

"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 中请求一下数据,大致梳理一下有以下几个流程:

graph TD
首先引入了若干文件 --> 定义了一个名叫_sfc_main的组件 ---> 导出_sfc_main ---> wx.createPage,渲染页面

可以注意到前面三个步骤都是自己写的代码逻辑,真正的第四步才是突然冒出来的逻辑,着重看一下这个 wx.createPage,本来以为这是一个官方提供的原生方法,然后在官网搜了一大圈,居然搜不到!搜不到!搜不到!那这是怎么回事呢?那只能是在 uni 里面手动将 createPage 这个方法挂在了 wx 上面;然后找到了这一部分代码:

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 的逻辑:

function initCreatePage(parseOptions2) {
  return function createPage2(vuePageOptions) {
    return Component(parsePage(vuePageOptions, parseOptions2));
  };
}

可以明确的是这个 initCreatePage 又与两个方法 Component 和 parsePage 有关,先来看看 parsePage:

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方法又主要调用了以下方法:

graph TD
parseComponent --> initPageProps---> methods.onLoad---> initHooks ---> initUnknownHooks ---> initRuntimeHooks ---> parse

parseComponent:将 Vue 中的 Mixins 提取出来并合并到 VueOptions 对象中,组装mpComponentOptions的一些属性,包含options、lifetimes、pageLifetimes、methods、properties、observers(挂载了更新组件的方法)

initPageProps:将 Vue 的 props 转化成 小程序中的 properties,对比一下两者的差异;

Vue 中的 props 的格式是这样的:

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会忽略函数):

{
    "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 方法经历了以下四个过程:

graph TD
initMiniProgramHook ---> MPComponent

initMiniProgramHook:初始化 created 生命周期(注意这个 created 与 vue 中的 created 不一样,它不能调用 setData 更新数据)

MPComponent:MPComponent 就是小程序全局的 Component 方法,所以 wx.createPage归根结底就是调用了 Component 方法(跳转微信小程序查看 Component 的参数

至此,首次渲染就完成了,那么当与页面交互时,比如说点击页面的计数器,此时是监听 setter 然后直接调用小程序实例的 setData 吗?

页面更新逻辑

并不是,它还是要走 Vue 的 diff 逻辑,然后将发生变化的变量去进行批量的 setData,这比多次调用 setData 性能更好;

简单地做个总结:原生的微信小程序的运行时逻辑是这样的,渲染层和逻辑层之间通过微信客户端进行通信,当逻辑层 setData 时会将最新数据传递给微信客户端,然后微信客户端会通知渲染层更新页面:

uni-app 运行时源码解析

当使用了 uni-app 之后,在逻辑层前面多了一个 Vue 运行时,这个运行时主要是继承了 Vue 的响应式原理,通过 Porxy 创建了代理对象,然后对 get、set 进行监听,当触发 set 之后会进行 diff 然后得到 diff 之后的对象,这些值都是发生了改变的值,最后执行 setData 进行批量更新:

uni-app 运行时源码解析

原文链接:https://juejin.cn/post/7323251349301313599 作者:蚂小蚁

(0)
上一篇 2024年1月14日 下午5:02
下一篇 2024年1月14日 下午5:13

相关推荐

发表回复

登录后才能评论