# 1. 初始化

# 1.1. app.js

app.js中直接引用了3个文件:

require('./common/runtime.js')
require('./common/vendor.js')
require('./common/main.js')

runtime.js中就是__webpack_require__一些相关定义,和普通的h5项目的运行时没什么差别。不懂webpack运行时的可以看这篇文章 (opens new window)

main.js中主要是项目main.js的编译产物。

# 1.2. vendor.js

vendor.js中包括uni-app中运行时代码、公共js文件等。

vendor.js中导出了 createPage、createComponent 等方法,是运行时的关键。我们写的组件比如xxx.vue会被编译成4个文件:

  • xxx.wxml
  • xxx.wxss
  • xxx.json
  • xxx.js

其中xxx.js主要是 script部分 的编译产物,它里面会引用 createPage 或 createComponent 方法,将 vue 配置转为小程序的配置,其实就很像Vant-weapp中的 VantComponent。

const MPPage = Page;
const MPComponent = Component;

if (!MPPage.__$wrappered) {
  MPPage.__$wrappered = true;
  Page = function (options = {}) {
    initHook('onLoad', options);
    return MPPage(options)
  };
  Page.after = MPPage.after;

  Component = function (options = {}) {
    initHook('created', options);
    return MPComponent(options)
  };
}

function initHook (name, options, isComponent) {
  const oldHook = options[name];
  if (!oldHook) {
    options[name] = function () {
      initTriggerEvent(this);
    };
  } else {
    options[name] = function (...args) {
      initTriggerEvent(this);
      return oldHook.apply(this, args)
    };
  }
}

function initTriggerEvent (mpInstance) {
  const oldTriggerEvent = mpInstance.triggerEvent;
  const newTriggerEvent = function (event, ...args) {
    return oldTriggerEvent.apply(mpInstance, [customize(event), ...args])
  };
  try {
    // 京东小程序 triggerEvent 为只读
    mpInstance.triggerEvent = newTriggerEvent;
  } catch (error) {
    mpInstance._triggerEvent = newTriggerEvent;
  }
}

上面是 uni-app 代理了 Page 和 Component 方法,分别处理了 onLoad 和 created 两个钩子函数,猜测是处理不同平台的参数,并合并用户手动添加的方法。

下面就是 uni-app 主要暴露的两个方法,createPage 和 createComponent。

function createPage (vuePageOptions) {
  {
    return Component(parsePage(vuePageOptions))
  }
}

function createComponent (vueOptions) {
  {
    return Component(parseComponent(vueOptions))
  }
}

可以看到 createPage 和 createComponent 都是调用了小程序的 Component 去生成组件。parsePage 和 parseComponent 就是处理 Vue文件 的配置,将其转为小程序需要的参数。

function parsePage (vuePageOptions) {
  return parseBasePage(vuePageOptions, {
    isPage,
    initRelation
  })
}

function parseComponent (vueComponentOptions) {
  return parseBaseComponent(vueComponentOptions, {
    isPage,
    initRelation
  })
}

# 1.3. parseBaseComponent

parseBaseComponent 是转化参数的核心,主要有:

  • 调用 initVueComponent,也就是 Vue.extend 初始化一个Vue实例
  • 初始化 options、data、behaviors、properties等
  • 在 attached 中,处理VueId,初始化父子关系,调用 this.$vm.$mount() 来触发首次 setData
  • 在 ready 中,调用 mounted 和 onReady 方法。
  • 提供两个方法: _l 和 _e
  • 声明了 pageLifetimes
function initVueComponent (Vue, vueOptions) {
  vueOptions = vueOptions.default || vueOptions;
  let VueComponent;
  if (isFn(vueOptions)) {
    VueComponent = vueOptions;
  } else {
    VueComponent = Vue.extend(vueOptions);
  }
  vueOptions = VueComponent.options;
  return [VueComponent, vueOptions]
}

function parseBaseComponent (vueComponentOptions, {
  isPage,
  initRelation
} = {}) {
  const [VueComponent, vueOptions] = initVueComponent(vue__WEBPACK_IMPORTED_MODULE_1__["default"], vueComponentOptions);

  const options = {
    multipleSlots: true,
    foo1GlobalClass: true,
    ...(vueOptions.options || {})
  };

  {
    // 微信 multipleSlots 部分情况有 bug,导致内容顺序错乱 如 u-list,提供覆盖选项
    if (vueOptions['mp-weixin'] && vueOptions['mp-weixin'].options) {
      Object.assign(options, vueOptions['mp-weixin'].options);
    }
  }

  const componentOptions = {
    options,
    data: initData(vueOptions, vue__WEBPACK_IMPORTED_MODULE_1__["default"].prototype),
    behaviors: initBehaviors(vueOptions, initBehavior),
    properties: initProperties(vueOptions.props, false, vueOptions.__file),
    lifetimes: {
      attached () {
        const properties = this.properties;

        const options = {
          mpType: isPage.call(this) ? 'page' : 'component',
          mpInstance: this,
          propsData: properties
        };

        initVueIds(properties.vueId, this);

        // 处理父子关系
        initRelation.call(this, {
          vuePid: this._$vuePid,
          vueOptions: options
        });

        // 初始化 vue 实例
        this.$vm = new VueComponent(options);

        // 处理$slots,$scopedSlots(暂不支持动态变化$slots)
        initSlots(this.$vm, properties.vueSlots);

        // 触发首次 setData
        this.$vm.$mount();
      },
      ready () {
        // 当组件 props 默认值为 true,初始化时传入 false 会导致 created,ready 触发, 但 attached 不触发
        // https://developers.weixin.qq.com/community/develop/doc/00066ae2844cc0f8eb883e2a557800
        if (this.$vm) {
          this.$vm._isMounted = true;
          this.$vm.__call_hook('mounted');
          this.$vm.__call_hook('onReady');
        }
      },
      detached () {
        this.$vm && this.$vm.$destroy();
      }
    },
    pageLifetimes: {
      show (args) {
        this.$vm && this.$vm.__call_hook('onPageShow', args);
      },
      hide () {
        this.$vm && this.$vm.__call_hook('onPageHide');
      },
      resize (size) {
        this.$vm && this.$vm.__call_hook('onPageResize', size);
      }
    },
    methods: {
      __l: handleLink,
      __e: handleEvent
    }
  };
  // externalClasses
  if (vueOptions.externalClasses) {
    componentOptions.externalClasses = vueOptions.externalClasses;
  }

  if (Array.isArray(vueOptions.wxsCallMethods)) {
    vueOptions.wxsCallMethods.forEach(callMethod => {
      componentOptions.methods[callMethod] = function (args) {
        return this.$vm[callMethod](args)
      };
    });
  }

  if (isPage) {
    return componentOptions
  }
  return [componentOptions, VueComponent]
}

Vue.extend 中会调用 initState、initData、observe 等方法,收集依赖。

this.$vm.$mount() 执行vue实例的mount,但是vue的template已经打包在了wxml中,所以不会触发render函数。

uni-app 改造了 patch 方法,会复制所有 data 和 computed,对比之前的data,和最新的mpData,然后触发小程序的 setData 方法。

var patch = function(oldVnode, vnode) {
  var data = Object.create(null);
  try {
    data = cloneWithData(this);
  } catch (err) {
    console.error(err);
  }
  data.__webviewId__ = mpInstance.data.__webviewId__;
  var mpData = Object.create(null);
  Object.keys(data).forEach(function (key) { //仅同步 data 中有的数据
    mpData[key] = mpInstance.data[key];
  });

  // ...
  mpInstance.setData(diffData, function () {
    this$1.__next_tick_pending = false;
    flushCallbacks$1(this$1);
  });
}

# 2. 页面交互

对于下面这个例子:

<template>
  <div @click="foo1(); foo2(2)" @touchstart="foo3($event)">{{ num }}</div>
</template>

会被转化成:

<view 
    data-event-opts="{{
        [
            ['tap',[['foo1'],['foo2',[2]]]],
            ['touchstart',[['foo3',['$event']]]]
        ]
    }}"
    bindtap="__e" 
    bindtouchstart="__e"
    class="_div">
    {{num}}
</view>

data-event-opts是一个二维数组,每个子数组代表一个事件类型。子数组有两个值,第一个表示事件类型名称,第二个表示触发事件函数的个数。事件函数又是一个数组,第一个值表述事件函数名称,第二个是参数个数。

['tap',[['foo1'],['foo2',[2]]]]表示事件类型为tap,触发函数有两个,一个为foo1函数且无参数,一个为foo2且参数为2。

['touchstart',[['foo3',['$event']]]]表示事件类型为 touchstart,触发函数有一个为foo3,参数为$event对象。


function handleEvent (event) {
  event = wrapper$1(event);

  // [['tap',[['handle',[1,2,a]],['handle1',[1,2,a]]]]]
  const dataset = (event.currentTarget || event.target).dataset;
  if (!dataset) {
    return console.warn('事件信息不存在')
  }
  const eventOpts = dataset.eventOpts || dataset['event-opts']; // 支付宝 web-view 组件 dataset 非驼峰
  if (!eventOpts) {
    return console.warn('事件信息不存在')
  }

  // [['handle',[1,2,a]],['handle1',[1,2,a]]]
  const eventType = event.type;

  const ret = [];

  eventOpts.forEach(eventOpt => {
    let type = eventOpt[0];
    const eventsArray = eventOpt[1];

    const isCustom = type.charAt(0) === CUSTOM;
    type = isCustom ? type.slice(1) : type;
    const isOnce = type.charAt(0) === ONCE;
    type = isOnce ? type.slice(1) : type;

    if (eventsArray && isMatchEventType(eventType, type)) {
      eventsArray.forEach(eventArray => {
        const methodName = eventArray[0];
        if (methodName) {
          let handlerCtx = this.$vm;
          if (handlerCtx.$options.generic) { // mp-weixin,mp-toutiao 抽象节点模拟 scoped slots
            handlerCtx = getContextVm(handlerCtx) || handlerCtx;
          }
          if (methodName === '$emit') {
            handlerCtx.$emit.apply(handlerCtx,
              processEventArgs(
                this.$vm,
                event,
                eventArray[1],
                eventArray[2],
                isCustom,
                methodName
              ));
            return
          }
          const handler = handlerCtx[methodName];
          if (!isFn(handler)) {
            throw new Error(` _vm.${methodName} is not a function`)
          }
          if (isOnce) {
            if (handler.once) {
              return
            }
            handler.once = true;
          }
          let params = processEventArgs(
            this.$vm,
            event,
            eventArray[1],
            eventArray[2],
            isCustom,
            methodName
          );
          params = Array.isArray(params) ? params : [];
          // 参数尾部增加原始事件对象用于复杂表达式内获取额外数据
          if (/=\s*\S+\.eventParams\s*\|\|\s*\S+\[['"]event-params['"]\]/.test(handler.toString())) {
            // eslint-disable-next-line no-sparse-arrays
            params = params.concat([, , , , , , , , , , event]);
          }
          ret.push(handler.apply(handlerCtx, params));
        }
      });
    }
  });

  if (
    eventType === 'input' &&
    ret.length === 1 &&
    typeof ret[0] !== 'undefined'
  ) {
    return ret[0]
  }
}

从上面我们知道__e对应的就是 handleEvent,handleEvent 处理流程如下:

  1. 拿到点击元素上的 data-event-opts 属性:[['tap',[['foo1'],['foo2',[2]]]],['touchstart',[['foo3',['$event']]]]]
  2. 根据点击类型获取相应数组,比如 bindTap 就取['tap',[['foo1'],['foo2',[2]]]],bindtouchstart 就取['touchstart',[['foo3',['$event']]]]
  3. 依次调用相应事件类型的函数,并传入参数,比如 tap 调用this.foo1();this.foo2(2)

# 3. 对比Vue

Vue 初次渲染过程

  • 解析模板为render函数(或在开发环境已完成,vue-loader)
  • 触发响应式,监听getter、setter
  • 执行render函数,生成vnode
  • patch(vnode)

更新过程

  • 修改data,触发setter
  • 重新执行render函数,生成newVnode
  • patch(vnode,newVnode)

Uni-app初次渲染过程

  • Vue.extend 初始化实例、收集依赖
  • 调用 $mount
  • 调用 patch
  • 调用 setData,交给小程序处理

更新过程

  • 修改data,触发 setter
  • 触发 Vue.prototype._update 方法
  • 调用 patch
  • 调用 setData,交给小程序处理