omi-mp-create是dntzhang写的小程序框架,主要功能是实现全局状态自动更新和页面间通信,传送门。
代码虽然简单但是注释不多读起来还是需要一点时间理解,因此在上面加入了个人理解的注释方便查看~
在里面还用到的一个监听更改库obaa
/*!
* omi-mp-create v0.1.0 by dntzhang
* Github: https://github.com/Tencent/omi
* MIT Licensed.
*/
import obaa from './obaa'
import mitt from './mitt'
// 页面构造器
// 在onload阶段执行下面的
// 把data深拷贝到oData
// 赋值store(这个应该没用的吧。。。除非主动改变了onLoad中的this指向,但是这有啥用)
function _Page(option) {
const onLoad = option.onLoad
option.onLoad = function (e) {
this.store = option.store //这行被我注释了
this.oData = JSON.parse(JSON.stringify(option.data))
observe(this)
onLoad && onLoad.call(this, e)
}
Page(option)
}
// 组件构造器
// 在ready阶段执行
// 把data深拷贝到oData
// 如果没有store的话,就从当前页面中的store里面取
function _Component(option) {
const ready = option.ready
option.ready = function () {
const page = getCurrentPages()[getCurrentPages().length - 1]
this.store = option.store || page.store
this.oData = JSON.parse(JSON.stringify(option.data))
observe(this)
ready && ready.call(this)
}
Component(option)
}
// 把类似 "#-a-0-c" 的字符串处理成 "a[0].c"这种能够用于小程序渲染页面的路径(索引)
// 大概因为这样写后面的代码就不用管是object还是Array都用 - 连接反正都能直接塞进去了
// 作者应该是为了使用 obaa 监听变更才需要这个函数把path转换小程序的格式
function fixPath(path) {
let mpPath = ''
const arr = path.replace('#-', '').split('-')
arr.forEach((item, index) => {
if (index) {
if (isNaN(parseInt(item))) {
mpPath += '.' + item
} else {
mpPath += '[' + item + ']'
}
} else {
mpPath += item
}
})
return mpPath
}
// 使用obaa监听 oData
// 让对 oData的修改能够自动使用setData更新到视图上
function observe(ele) {
let timeout = null
let patch = {} // patch 是要应用到视图上的变更
// 下面的索引指的是patch的key值
// 如 a[2].c 这样的能够符合小程序setData规范的key
obaa(ele.oData, (prop, value, old, path) => {
clearTimeout(timeout)
if (prop.indexOf('Array-push') === 0) {
// 如果是在数组上push元素
// 把新的值赋值到 patch上
let dl = value.length - old.length
for (let i = 0; i < dl; i++) {
patch[fixPath(path + '-' + (old.length + i))] = value[(old.length + i)]
}
} else if (prop.indexOf('Array-') === 0) {
// 如果是非 push 的其他数组方法
// 把索引用新值替代
patch[fixPath(path)] = value
} else {
// 不是数组操作就根据path-prop索引到对应的值进行赋值
patch[fixPath(path + '-' + prop)] = value
}
// 这里作者使用 setTimeout 0 的方法把setData操作放到异步队列中而不是立即同步执行
// 在setData后重新把patch置为空
// 配合上面的clearTimeout可以避免频繁setData的问题
// 同步修改数据的过程中所有的patch都是同一个对象
// 在修改一结束后才会使用 setData(patch) 一次性把所有的更改更新到视图上
timeout = setTimeout(() => {
ele.setData(patch)
patch = {}
}, 0)
})
}
// 一个全局的store的引用
// 等同于app.globalData.store
let globalStore = null
function create(store, option) {
// 页面构造函数
if (arguments.length === 2) {
// 如果option里面有data
// 把option.data深拷贝到store.data对象中
if (option.data && Object.keys(option.data).length > 0) {
Object.assign(store.data, JSON.parse(JSON.stringify(option.data)))
}
// 初始化store.instances
if (!store.instances) {
store.instances = {}
}
// 把store绑定在全局变量app.globalData上
getApp().globalData && (getApp().globalData.store = store)
// 函数中的globalStore也指向 store
globalStore = store
// 页面data绑定store.data
option.data = store.data
observeStore(store)
const onLoad = option.onLoad
option.onLoad = function (e) {
// 把全局的store绑定为页面的属性
this.store = store
// 以页面的路由作为key值
// 把当前页面的绑定在store.instances上
store.instances[this.route] = []
store.instances[this.route].push(this)
// 调用页面原有的onLoad
onLoad && onLoad.call(this, e)
}
Page(option)
}
// 组件构造函数(下面的store代表一个组件的option)
else {
const ready = store.ready
store.ready = function () {
// 找到组件对应的页面page
this.page = getCurrentPages()[getCurrentPages().length - 1]
// 把组件所属页面page的store绑定到当前组件上
this.store = this.page.store
// 用深拷贝把全局store.data 中的属性绑定给 组件的store.data
// 这里用深拷贝大概是因为作者认为不应该用修改this.store.data来影响全局
// 而应该使用全局的store.data直接修改
// 我觉得这个深拷贝可以直接去掉?那样就不用在组件内再引入一次store了吧
// 应该是为了遵循小程序不能够直接修改this.data的原则
store.data && Object.assign(this.store.data, JSON.parse(JSON.stringify(store.data)))
// 把糅合后的store.data赋值给组件的data
this.setData.call(this, this.store.data)
// 放入当前页面路由的监听队列
this.store.instances[this.page.route].push(this)
// 调用原有的ready
ready && ready.call(this)
}
Component(store)
}
}
// 监听全局的store
// 类似上面的observe方法,不同的是这里直接监听的store
// 而且使用 _update更新全局视图而不是 this.setData的简单更新
function observeStore(store) {
let timeout = null
let patch = {}
obaa(store.data, (prop, value, old, path) => {
clearTimeout(timeout)
if (prop.indexOf('Array-push') === 0) {
let dl = value.length - old.length
for (let i = 0; i < dl; i++) {
patch[fixPath(path + '-' + (old.length + i))] = value[(old.length + i)]
}
} else if (prop.indexOf('Array-') === 0) {
patch[fixPath(path)] = value
} else {
patch[fixPath(path + '-' + prop)] = value
}
timeout = setTimeout(() => {
_update(patch)
patch = {}
}, 0)
})
}
function _update(kv) {
for (let key in globalStore.instances) {
// 对 instances 中的所有页面的所有实例都调用setData更新视图
// 这里的instances中包含所有的页面
// 每个页面对应的数组中包含这个页面实例本身及它所包含的组件实例
globalStore.instances[key].forEach(ins => {
ins.setData.call(ins, kv)
})
}
// 这里还调用了一个全局的onChange方法
// 大概是留给开发者监听的store?
// 但是好像没啥用
globalStore.onChange && globalStore.onChange(kv)
}
create.Page = _Page
create.Component = _Component
create.obaa = obaa
// 提供一个mitt的引用
create.mitt = mitt
// 初始化一个mitt(供全局使用)
create.emitter = mitt()
export default create