wepy 的运行原理探索

1,453 阅读4分钟

前言

刚搭建了一个博客,咋都得写上几篇文章,不然不就白搭了。

关于 博客

一开始打算用 hexo + gitalk + github 搭建一个,但是人太懒,总想一次完事,以后更新就写一篇 .md 文章就可以了,不想弄其他流程了,然后就用了 vue + github-issuse-api + gitalk + github 搭建了一个一劳永逸的办法,除非 github 挂了。

关于 wepy

  • 官方介绍
    是小程序【最早(wepy你已经很成熟了)】 的框架之一,是一款让小程序支持 【组件化开发(现在小程序已经支持npm构建,好像没啥优势)】 的框架,通过预编译的手段让开发者可以选择自己 【喜欢的开发风格(类 vue 语法,数据劫持,自动 setData)】 去开发小程序。框架的细节优化,Promise,【Async Functions(原生不支持)】的引入都是为了能让开发小程序项目变得更加简单,高效

  • 优缺点
      - 优点
          - vue 语法,更好的开发体验
          - 更好的组件化
          - 更多的组件库
          - 支持更多 ECMAscript 语法
      - 缺点
          - 官方维护不太积极,遇到问题解决太慢
          - 引入了额外的代码,增加了代码体积
    
  • 代码库
      - 研究源码版本 2.0.0-alpha.8
      - wepy 核心代码是 cli 和 core
      - wepy-cli 是文件转换打包的处理
      - core 是对数据绑定生命周期注入的处理
    

core 源码探索

  • wepy 核心代码分为 wepy-cli 和 core,这篇文章主要对 core 进行探索
  • 源码版本 2.0.0-alpha.8
  • 源码库 wepy/packages/core
  • 为了看的更清晰一些我删除了大部分占时没有用到代码
  • 每个 js 部分,头部我会写清对于源码文件路径
  • 牢记一点,运行环境和this是在运行时候确认,不然你看源码时候你会很懵,明明 this 上没有这个函数或属性啊,明明全局搜不到这个方法啊,就像下面这个 App() 你不和小程序环境关联起来,你全局都搜不到有这么一个函数
  • 代码里面注释很关键

wepy 是怎么编译之后运行到小程序中去的?

// core/weapp/apis/index.js
import { app, page, component } from '../native/index';

export function initGlobalAPI (wepy) {

 wepy.app = app; // return App({})
 wepy.page = page; // return Page({})
 wepy.component = component; // return Component({})

 return wepy;
}

写过 wepy2.0 的就知道,通过 wepy.page(), wepy.component(), 注入页面和组件,以及在app.wpy中的 wepy.app() 方法 ,通过这三个方法,分别会返回 App() Page() Component(),写过原生到小伙伴是不是很熟悉这三个 函数,分别是实现组册小程序,注册页面和组件,这样就和原生关联起来了

wepy 怎样实现生命周期绑定关联的了?

拿 app 为例, 首先这个函数做了什么?app 函数里运行 patchAppLifecycle 函数 并传入小程序运行环境 appConfig 中的对象【相当于 APP({}) 函数中的对象】,然后将 所有生命周期绑定上去,每次原生环境运行,触发原生生命周期,每个生命周期都会触发一个回调函数,在执行 .wey 文件中存在的生命周期函数逻辑,这样,小程序的生命周期和你页面的生命周期进行了关联

// core/weapp/native/app.js
export function app (option, rel) {
 let appConfig = {};
 patchAppLifecycle(appConfig, option, rel); // 注册和初始化生命周期
 return App(appConfig); // 这里最终运行于小程序中 appConfig 就是小程序js中的逻辑
}

// patchAppLifecycle 函数
// core/weapp/init/lifecycle
export function patchAppLifecycle (appConfig, options, rel = {}) {
 appConfig.onLaunch = function (...args) {
   let vm = new WepyApp();
   // 生命周期回调,触发页面生命周期函数
   return callUserMethod(vm, vm.$options, 'onLaunch', args);
 };
 // 相当于 App({onLaunch: function() {}})

 // 获取生命周期列表
 let lifecycle = getLifecycycle(WEAPP_APP_LIFECYCLE, rel, 'app');
 
 // 循环绑定生命周期
 lifecycle.forEach(k => {
  appConfig[k] = function (...args) {
   return callUserMethod(app, app.$options, k, args);
  };
 });
};

数据绑定

如果你了解过 vue 的数据绑定那这个实现原理也和 vue 一样 (虚!!我怀疑是抄的,连函数名都一样),也是实现了一个观察者模式,只是,vue 做的更新处理是 patch 函数 diff 虚拟 dom 实现更新 web,而,wepy做调是调用 setData 更新数据,话不多说,上代码。

// core/weapp/init/lifecycle
export function patchLifecycle (output, options, rel, isComponent) {
  // output 小程序的运行环境
  const initLifecycle = function (...args) {
    let vm = new initClass();
    // 将小程序 this 绑定到 vm.$wx 上去
    vm.$wx = this;
    // 初始化数据,进行数据绑定,进行绑定对数据只是 data 中的数据
    initData(vm, output.data, isComponent);
    // 进行依赖收集,做 setData
    initRender(vm, Object.keys(vm._data).concat(Object.keys(vm._props)).concat(Object.keys(vm._computedWatchers || {})));
    return callUserMethod(vm, vm.$options, 'created', args);
  };
 
  // 绑定到小程序到生命周期中去,做点事
  // 相当于 Page({created: initLifecycle(){}})
  output.created = initLifecycle;
};

来看看 initData 做了啥, 对 data 进行了处理,代理到 _data 属性上去,进行观察

export function initData (vm, data) {
  vm._data = _data;
  Object.keys(_data).forEach(key => {proxy(vm, '_data', key)});

  observe({
    vm: vm,
    key: '',
    value: _data,
    parent: '',
    root: true
  });
}

observe 函数做了啥

// core/weapp/observer/index.js
export function observe ({vm, key, value, parent, root}) {
  // 判断是否为观察者 __ob__ 属性定义
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  }
  // 观察数据
  ob = new Observer({vm: vm, key: key, value: value,  parent});
  return ob
}

class Observer {
    // 如果是数组,则做特殊处理,因为数组 push, pop 等不能劫持,所以需要特殊处理
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(key, value);
    } else {
      // 否则劫持数据
      this.walk(key, value);
    }
}

walk (key, obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive({ vm: this.vm, obj: obj, key: keys[i], value: obj[keys[i]], parent: obj });
    }
}
defineReactive () {
   Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const val = getter ? getter.call(obj) : value
      // 依赖收集
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(val)) {
            dependArray(val)
          }
        }
      }
      return val
    },
    set: function reactiveSetter (newVal) {
      const val = getter ? getter.call(obj) : value
      // 更新数据
      value = newVal
      // 通知数据更新
      dep.notify();
    }
  })
}

如果是数组需要做特殊处理,数据,push,pop等方式改变等数据不能被劫持,如果不是,就做数据劫持 getter setter,数据劫持的初始化已经做完了,现在在来看如进行等依赖收集,回到 initRender() 上了,进行依赖收集,就得触发一次数据到 getter, initRender 就是干这个的,watch 观察每个页面实例,触发一次 data getter 进行依赖收集,每次 setter 到时候就会触发这个函数 setData

// core/weapp/init/render.js
export function initRender (vm, keys) {
 return new Watcher(vm, function () {
     // 会触发数据到 getter 进行依赖收集
     Object.keys(keys).forEach(key => clone(vm[key]))
     // 调用 setData 进行数据更新,vm.$wx 在页面 created 钩子到时候进行绑定,写在了前面,是小程序中的 this
     vm.$wx.setData(dirty, renderFlushCallbacks)
   }
 }, function () {

 }, null, true);
};

编译结果

可以看到最后编译出来就是执行最开始导出的三个函数,对应到不同的小程序页中去

// app.js
// vendor.js 为编译后的核心代码,相当于,导出的 wepy
var _core = _interopRequireDefault(require('vendor.js')(1));

// 调用 app 函数,return App({}) 完成对小程序的注册
_core["default"].app({}, {a: 1})
// page.js
// vendor.js 为编译后的核心代码,相当于,导出的 wepy
var _core = _interopRequireDefault(require('../vendor.js')(1));

// 调用 page 函数,return Page({}) 完成对小程序页面的注册
_core["default"].page({}, {});

总结

  • 纸上得来终于觉浅,得知原理需读源
  • 重点 ---- 勿喷 ----
  • Blog