wepy 的运行原理探索
loo41 opened this issue · 0 comments
前言
刚搭建了一个博客,咋都得写上几篇文章,不然不就白搭了。
关于 博客
一开始打算用 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({}, {});