@mpxjs/core 2.7.52 → 2.8.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/@types/index.d.ts +342 -27
  2. package/package.json +10 -5
  3. package/src/convertor/convertor.js +2 -2
  4. package/src/convertor/mergeLifecycle.js +4 -4
  5. package/src/convertor/wxToAli.js +3 -4
  6. package/src/convertor/wxToSwan.js +2 -2
  7. package/src/convertor/wxToTt.js +1 -10
  8. package/src/convertor/wxToWeb.js +14 -7
  9. package/src/core/implement.js +2 -2
  10. package/src/core/injectMixins.js +1 -1
  11. package/src/core/innerLifecycle.js +15 -2
  12. package/src/core/mergeOptions.js +11 -5
  13. package/src/core/proxy.js +343 -229
  14. package/src/core/transferOptions.js +5 -2
  15. package/src/helper/const.js +10 -0
  16. package/src/index.js +73 -147
  17. package/src/observer/array.js +12 -17
  18. package/src/observer/computed.js +27 -56
  19. package/src/observer/dep.js +1 -1
  20. package/src/observer/effect.js +113 -0
  21. package/src/observer/effectScope.js +109 -0
  22. package/src/observer/{index.js → reactive.js} +74 -70
  23. package/src/observer/ref.js +97 -0
  24. package/src/observer/scheduler.js +171 -56
  25. package/src/observer/watch.js +163 -39
  26. package/src/platform/builtInMixins/i18nMixin.js +238 -31
  27. package/src/platform/builtInMixins/pageScrollMixin.web.js +4 -5
  28. package/src/platform/builtInMixins/pageStatusMixin.js +76 -54
  29. package/src/platform/builtInMixins/pageStatusMixin.web.js +35 -22
  30. package/src/platform/builtInMixins/proxyEventMixin.js +40 -22
  31. package/src/platform/builtInMixins/proxyEventMixin.web.js +16 -24
  32. package/src/platform/builtInMixins/refsMixin.js +82 -73
  33. package/src/platform/builtInMixins/refsMixin.web.js +0 -47
  34. package/src/platform/builtInMixins/relationsMixin.js +10 -9
  35. package/src/platform/builtInMixins/renderHelperMixin.js +1 -1
  36. package/src/platform/builtInMixins/showMixin.js +1 -1
  37. package/src/platform/createApp.js +5 -5
  38. package/src/platform/export/api.js +23 -0
  39. package/src/platform/export/api.web.js +26 -0
  40. package/src/platform/export/index.js +45 -0
  41. package/src/platform/export/index.web.js +36 -0
  42. package/src/platform/index.js +1 -5
  43. package/src/platform/patch/ali/getDefaultOptions.js +33 -31
  44. package/src/platform/patch/ali/lifecycle.js +21 -13
  45. package/src/platform/patch/builtInKeysMap.js +2 -1
  46. package/src/platform/patch/index.js +4 -9
  47. package/src/platform/patch/swan/getDefaultOptions.js +3 -3
  48. package/src/platform/patch/swan/lifecycle.js +17 -14
  49. package/src/platform/patch/web/getDefaultOptions.js +40 -16
  50. package/src/platform/patch/web/lifecycle.js +6 -3
  51. package/src/platform/patch/wx/getDefaultOptions.js +38 -31
  52. package/src/platform/patch/wx/lifecycle.js +18 -11
  53. package/src/runtime/createFactory.js +6 -2
  54. package/src/vue.web.js +3 -0
  55. package/src/vuePlugin.js +31 -0
  56. package/src/core/createStore.js +0 -241
  57. package/src/core/mapStore.js +0 -94
  58. package/src/helper/env.js +0 -20
  59. package/src/helper/getByPath.js +0 -127
  60. package/src/helper/log.js +0 -31
  61. package/src/helper/utils.js +0 -652
  62. package/src/observer/watcher.js +0 -244
package/src/core/proxy.js CHANGED
@@ -1,111 +1,157 @@
1
- import { observe } from '../observer/index'
2
- import Watcher from '../observer/watcher'
1
+ import { reactive } from '../observer/reactive'
2
+ import { ReactiveEffect } from '../observer/effect'
3
+ import { effectScope } from '../platform/export/index'
3
4
  import { watch } from '../observer/watch'
4
- import { initComputed } from '../observer/computed'
5
- import { queueWatcher } from '../observer/scheduler'
6
- import EXPORT_MPX from '../index'
5
+ import { computed } from '../observer/computed'
6
+ import { queueJob, nextTick } from '../observer/scheduler'
7
+ import Mpx from '../index'
7
8
  import {
8
9
  noop,
9
- proxy,
10
+ type,
11
+ isFunction,
12
+ isObject,
10
13
  isEmptyObject,
11
14
  isPlainObject,
12
- processUndefined,
13
- setByPath,
15
+ doGetByPath,
14
16
  getByPath,
15
- asyncLock,
17
+ setByPath,
16
18
  diffAndCloneA,
17
- preProcessRenderData,
18
- mergeData,
19
+ hasOwn,
20
+ proxy,
21
+ makeMap,
22
+ isString,
19
23
  aIsSubPathOfB,
24
+ mergeData,
25
+ processUndefined,
20
26
  getFirstKey,
21
- makeMap,
22
- hasOwn
23
- } from '../helper/utils'
24
- import _getByPath from '../helper/getByPath'
25
- import { getRenderCallBack } from '../platform/patch'
27
+ callWithErrorHandling,
28
+ warn,
29
+ error
30
+ } from '@mpxjs/utils'
26
31
  import {
27
32
  BEFORECREATE,
28
33
  CREATED,
29
34
  BEFOREMOUNT,
30
35
  MOUNTED,
36
+ BEFOREUPDATE,
31
37
  UPDATED,
32
- DESTROYED
38
+ BEFOREUNMOUNT,
39
+ UNMOUNTED,
40
+ ONLOAD,
41
+ ONSHOW,
42
+ ONHIDE,
43
+ ONRESIZE
33
44
  } from './innerLifecycle'
34
- import { warn, error } from '../helper/log'
35
45
 
36
46
  let uid = 0
37
47
 
38
- export default class MPXProxy {
39
- constructor (options, target) {
48
+ class RenderTask {
49
+ resolved = false
50
+
51
+ constructor (instance) {
52
+ instance.currentRenderTask = this
53
+ this.promise = new Promise((resolve) => {
54
+ this.resolve = resolve
55
+ }).then(() => {
56
+ this.resolved = true
57
+ })
58
+ }
59
+ }
60
+
61
+ /**
62
+ * process renderData, remove sub node if visit parent node already
63
+ * @param {Object} renderData
64
+ * @return {Object} processedRenderData
65
+ */
66
+ function preProcessRenderData (renderData) {
67
+ // method for get key path array
68
+ const processKeyPathMap = (keyPathMap) => {
69
+ const keyPath = Object.keys(keyPathMap)
70
+ return keyPath.filter((keyA) => {
71
+ return keyPath.every((keyB) => {
72
+ if (keyA.startsWith(keyB) && keyA !== keyB) {
73
+ const nextChar = keyA[keyB.length]
74
+ if (nextChar === '.' || nextChar === '[') {
75
+ return false
76
+ }
77
+ }
78
+ return true
79
+ })
80
+ })
81
+ }
82
+
83
+ const processedRenderData = {}
84
+ const renderDataFinalKey = processKeyPathMap(renderData)
85
+ Object.keys(renderData).forEach(item => {
86
+ if (renderDataFinalKey.indexOf(item) > -1) {
87
+ processedRenderData[item] = renderData[item]
88
+ }
89
+ })
90
+ return processedRenderData
91
+ }
92
+
93
+ export default class MpxProxy {
94
+ constructor (options, target, reCreated) {
40
95
  this.target = target
96
+ this.reCreated = reCreated
41
97
  this.uid = uid++
42
98
  this.name = options.name || ''
43
99
  this.options = options
44
- // beforeCreate -> created -> mounted -> destroyed
100
+ // beforeCreate -> created -> mounted -> unmounted
45
101
  this.state = BEFORECREATE
46
- this.lockTask = asyncLock()
47
- this.ignoreProxyMap = makeMap(EXPORT_MPX.config.ignoreProxyWhiteList)
102
+ this.ignoreProxyMap = makeMap(Mpx.config.ignoreProxyWhiteList)
103
+ // 收集setup中动态注册的hooks,小程序与web环境都需要
104
+ this.hooks = {}
48
105
  if (__mpx_mode__ !== 'web') {
49
- this._watchers = []
50
- this._namedWatchers = {}
51
- this._computedWatchers = {}
52
- this._watcher = null
53
- this.localKeysMap = {} // 非props key
54
- this.renderData = {} // 渲染函数中收集的数据
106
+ this.scope = effectScope(true)
107
+ // props响应式数据代理
108
+ this.props = {}
109
+ // data响应式数据代理
110
+ this.data = {}
111
+ // 非props key
112
+ this.localKeysMap = {}
113
+ // 渲染函数中收集的数据
114
+ this.renderData = {}
115
+ // 最小渲染数据
55
116
  this.miniRenderData = {}
56
- this.forceUpdateData = {} // 强制更新的数据
57
- this.forceUpdateAll = false // 下次是否需要强制更新全部渲染数据
58
- this.curRenderTask = null
117
+ // 强制更新的数据
118
+ this.forceUpdateData = {}
119
+ // 下次是否需要强制更新全部渲染数据
120
+ this.forceUpdateAll = false
121
+ this.currentRenderTask = null
59
122
  }
123
+ this.initApi()
124
+ this.callHook(BEFORECREATE)
60
125
  }
61
126
 
62
- created (params) {
63
- this.initApi()
64
- this.callUserHook(BEFORECREATE)
127
+ created () {
65
128
  if (__mpx_mode__ !== 'web') {
66
- this.initState()
129
+ setCurrentInstance(this)
130
+ this.initProps()
131
+ this.initSetup()
132
+ this.initData()
133
+ this.initComputed()
134
+ this.initWatch()
135
+ unsetCurrentInstance()
67
136
  }
137
+
68
138
  this.state = CREATED
69
- this.callUserHook(CREATED, params)
139
+ this.callHook(CREATED)
140
+
70
141
  if (__mpx_mode__ !== 'web') {
71
142
  this.initRender()
72
143
  }
73
- }
74
144
 
75
- reCreated (params) {
76
- const options = this.options
77
- this.state = BEFORECREATE
78
- this.callUserHook(BEFORECREATE)
79
- if (__mpx_mode__ !== 'web') {
80
- this.initComputed(options.computed, true)
81
- this.initWatch(options.watch)
82
- }
83
- this.state = CREATED
84
- this.callUserHook(CREATED, params)
85
- if (__mpx_mode__ !== 'web') {
86
- this.initRender()
145
+ if (this.reCreated) {
146
+ nextTick(this.mounted.bind(this))
87
147
  }
88
- this.nextTick(() => {
89
- this.mounted()
90
- })
91
148
  }
92
149
 
93
- renderTaskExecutor (isEmptyRender) {
94
- if ((!this.isMounted() && this.curRenderTask) || (this.isMounted() && isEmptyRender)) {
150
+ createRenderTask (isEmptyRender) {
151
+ if ((!this.isMounted() && this.currentRenderTask) || (this.isMounted() && isEmptyRender)) {
95
152
  return
96
153
  }
97
- this.curRenderTask = {
98
- state: 'pending'
99
- }
100
- const promise = new Promise(resolve => {
101
- this.curRenderTask.resolve = (res) => {
102
- this.curRenderTask.state = 'finished'
103
- resolve(res)
104
- }
105
- })
106
- this.curRenderTask.promise = promise
107
- // isMounted之前基于mounted触发,isMounted之后基于setData回调触发
108
- return this.isMounted() && this.curRenderTask.resolve
154
+ return new RenderTask(this)
109
155
  }
110
156
 
111
157
  isMounted () {
@@ -116,131 +162,132 @@ export default class MPXProxy {
116
162
  if (this.state === CREATED) {
117
163
  this.state = MOUNTED
118
164
  // 用于处理refs等前置工作
119
- this.callUserHook(BEFOREMOUNT)
120
- this.callUserHook(MOUNTED)
121
- this.curRenderTask && this.curRenderTask.resolve()
165
+ this.callHook(BEFOREMOUNT)
166
+ this.callHook(MOUNTED)
167
+ this.currentRenderTask && this.currentRenderTask.resolve()
122
168
  }
123
169
  }
124
170
 
125
- updated () {
126
- if (this.isMounted()) {
127
- this.callUserHook(UPDATED)
128
- }
171
+ propsUpdated () {
172
+ const updateJob = this.updateJob || (this.updateJob = () => {
173
+ // 只有当前没有渲染任务时,属性更新才需要单独触发updated,否则可以由渲染任务触发updated
174
+ if (this.currentRenderTask?.resolved && this.isMounted()) {
175
+ this.callHook(BEFOREUPDATE)
176
+ this.callHook(UPDATED)
177
+ }
178
+ })
179
+ nextTick(updateJob)
129
180
  }
130
181
 
131
- destroyed () {
132
- this.state = DESTROYED
133
- if (__mpx_mode__ !== 'web') {
134
- this.clearWatchers()
135
- }
136
- this.callUserHook(DESTROYED)
182
+ unmounted () {
183
+ this.callHook(BEFOREUNMOUNT)
184
+ this.scope?.stop()
185
+ this.callHook(UNMOUNTED)
186
+ this.state = UNMOUNTED
137
187
  }
138
188
 
139
- isDestroyed () {
140
- return this.state === DESTROYED
189
+ isUnmounted () {
190
+ return this.state === UNMOUNTED
141
191
  }
142
192
 
143
- initApi () {
144
- // 挂载扩展属性到实例上
145
- proxy(this.target, this.options.proto, Object.keys(this.options.proto), true, (key) => {
193
+ createProxyConflictHandler (owner) {
194
+ return (key) => {
146
195
  if (this.ignoreProxyMap[key]) {
147
- error(`The key [${key}] of mpx.prototype is a reserved keyword of miniprogram, please check and rename it!`, this.options.mpxFileResource)
196
+ !this.reCreated && error(`The ${owner} key [${key}] is a reserved keyword of miniprogram, please check and rename it.`, this.options.mpxFileResource)
148
197
  return false
149
198
  }
150
- error(`The key [${key}] of mpx.prototype exist in the component/page instance already, please check your plugins!`, this.options.mpxFileResource)
151
- })
199
+ !this.reCreated && error(`The ${owner} key [${key}] exist in the current instance already, please check and rename it.`, this.options.mpxFileResource)
200
+ }
201
+ }
202
+
203
+ initApi () {
204
+ // 挂载扩展属性到实例上
205
+ proxy(this.target, Mpx.prototype, undefined, true, this.createProxyConflictHandler('mpx.prototype'))
152
206
  // 挂载混合模式下createPage中的自定义属性,模拟原生Page构造器的表现
153
207
  if (this.options.__type__ === 'page' && !this.options.__pageCtor__) {
154
- proxy(this.target, this.options, this.options.mpxCustomKeysForBlend, undefined, (key) => {
155
- if (this.ignoreProxyMap[key]) {
156
- error(`The key [${key}] of page options is a reserved keyword of miniprogram, please check and rename it!`, this.options.mpxFileResource)
157
- return false
158
- }
159
- error(`The key [${key}] of page options exist in the page instance already, please check your page options!`, this.options.mpxFileResource)
160
- })
208
+ proxy(this.target, this.options, this.options.mpxCustomKeysForBlend, false, this.createProxyConflictHandler('page options'))
161
209
  }
162
210
  if (__mpx_mode__ !== 'web') {
163
211
  // 挂载$watch
164
- this.target.$watch = (...rest) => this.watch(...rest)
212
+ this.target.$watch = this.watch.bind(this)
165
213
  // 强制执行render
166
- this.target.$forceUpdate = (...rest) => this.forceUpdate(...rest)
167
- this.target.$nextTick = fn => this.nextTick(fn)
168
- this.target.$getPausableWatchers = () => this._watchers.filter(item => item.pausable)
169
- this.target.$getWatcherByName = (name) => {
170
- if (!this._namedWatchers) return null
171
- return this._namedWatchers[name] || null
172
- }
173
- this.target.$getRenderWatcher = () => this._watcher
214
+ this.target.$forceUpdate = this.forceUpdate.bind(this)
215
+ this.target.$nextTick = nextTick
216
+ // 挂载$refs对象
217
+ this.target.$refs = {}
174
218
  }
175
219
  }
176
220
 
177
- initState () {
178
- const options = this.options
179
- const proxyedKeys = this.initData(options.data, options.dataFn)
180
- const proxyedKeysMap = makeMap(proxyedKeys)
181
- this.initComputed(options.computed)
182
- // target的数据访问代理到将proxy的data
183
- proxy(this.target, this.data, undefined, undefined, (key) => {
184
- if (this.ignoreProxyMap[key]) {
185
- error(`The data/props/computed key [${key}] is a reserved keyword of miniprogram, please check and rename it!`, this.options.mpxFileResource)
186
- return false
187
- }
188
- if (!proxyedKeysMap[key]) error(`The data/props/computed key [${key}] exist in the component/page instance already, please check and rename it!`, this.options.mpxFileResource)
189
- })
190
- this.initWatch(options.watch)
221
+ initProps () {
222
+ this.props = diffAndCloneA(this.target.__getProps(this.options)).clone
223
+ reactive(this.props)
224
+ proxy(this.target, this.props, undefined, false, this.createProxyConflictHandler('props'))
191
225
  }
192
226
 
193
- initComputed (computedOpt, reInit) {
194
- if (computedOpt) {
195
- if (reInit) {
196
- // target传递null以跳过computed挂载,仅重新初始化watchers
197
- initComputed(this, null, computedOpt)
198
- } else {
199
- this.collectLocalKeys(computedOpt)
200
- initComputed(this, this.data, computedOpt)
227
+ initSetup () {
228
+ const setup = this.options.setup
229
+ if (setup) {
230
+ const setupResult = callWithErrorHandling(setup, this, 'setup function', [
231
+ this.props,
232
+ {
233
+ triggerEvent: this.target.triggerEvent.bind(this.target),
234
+ refs: this.target.$refs,
235
+ forceUpdate: this.forceUpdate.bind(this),
236
+ selectComponent: this.target.selectComponent.bind(this.target),
237
+ selectAllComponents: this.target.selectAllComponents.bind(this.target),
238
+ createSelectorQuery: this.target.createSelectorQuery.bind(this.target),
239
+ createIntersectionObserver: this.target.createIntersectionObserver.bind(this.target)
240
+ }
241
+ ])
242
+ if (!isObject(setupResult)) {
243
+ error(`Setup() should return a object, received: ${type(setupResult)}.`, this.options.mpxFileResource)
244
+ return
201
245
  }
246
+ proxy(this.target, setupResult, undefined, false, this.createProxyConflictHandler('setup result'))
247
+ this.collectLocalKeys(setupResult, (key, val) => !isFunction(val))
202
248
  }
203
249
  }
204
250
 
205
- // 构建响应式data
206
- initData (data, dataFn) {
207
- let proxyedKeys = []
208
- // 获取包含data/props在内的初始数据,包含初始原生微信转换支付宝时合并props进入data的逻辑
209
- const initialData = this.target.__getInitialData(this.options) || {}
251
+ initData () {
252
+ const data = this.options.data
253
+ const dataFn = this.options.dataFn
210
254
  // 之所以没有直接使用initialData,而是通过对原始dataOpt进行深clone获取初始数据对象,主要是为了避免小程序自身序列化时错误地转换数据对象,比如将promise转为普通object
211
255
  this.data = diffAndCloneA(data || {}).clone
212
- if (dataFn) {
213
- proxyedKeys = Object.keys(initialData)
214
- // 预先将initialData代理到this.target中,便于data函数访问
215
- proxy(this.target, initialData, proxyedKeys, undefined, (key) => {
216
- if (this.ignoreProxyMap[key]) {
217
- error(`The props/data key [${key}] is a reserved keyword of miniprogram, please check and rename it!`, this.options.mpxFileResource)
218
- return false
219
- }
220
- error(`The props/data key [${key}] exist in the component instance already, please check and rename it!`, this.options.mpxFileResource)
221
- })
222
- Object.assign(this.data, dataFn.call(this.target))
256
+ // 执行dataFn
257
+ if (isFunction(dataFn)) {
258
+ Object.assign(this.data, callWithErrorHandling(dataFn.bind(this.target), this, 'data function'))
223
259
  }
224
- // 此时data中不包括props数据
260
+ reactive(this.data)
261
+ proxy(this.target, this.data, undefined, false, this.createProxyConflictHandler('data'))
225
262
  this.collectLocalKeys(this.data)
226
- // 将props数据合并到data中
227
- Object.keys(initialData).forEach((key) => {
228
- if (!hasOwn(this.data, key)) {
229
- // 除了data函数返回的数据外深拷贝切断引用关系,避免后续watch由于小程序内部对data赋值重复触发watch
230
- this.data[key] = diffAndCloneA(initialData[key]).clone
231
- }
232
- })
233
- // mpxCid 解决支付宝环境selector为全局问题
234
- this.data.mpxCid = this.uid
235
- this.localKeysMap.mpxCid = true
236
- observe(this.data, true)
237
- return proxyedKeys
238
263
  }
239
264
 
240
- initWatch (watch) {
265
+ initComputed () {
266
+ const computedOpt = this.options.computed
267
+ if (computedOpt) {
268
+ const computedObj = {}
269
+ Object.entries(computedOpt).forEach(([key, opt]) => {
270
+ const get = isFunction(opt)
271
+ ? opt.bind(this.target)
272
+ : isFunction(opt.get)
273
+ ? opt.get.bind(this.target)
274
+ : noop
275
+
276
+ const set = !isFunction(opt) && isFunction(opt.set)
277
+ ? opt.set.bind(this.target)
278
+ : () => warn(`Write operation failed: computed property "${key}" is readonly.`, this.options.mpxFileResource)
279
+
280
+ computedObj[key] = computed({ get, set })
281
+ })
282
+ this.collectLocalKeys(computedObj)
283
+ proxy(this.target, computedObj, undefined, false, this.createProxyConflictHandler('computed'))
284
+ }
285
+ }
286
+
287
+ initWatch () {
288
+ const watch = this.options.watch
241
289
  if (watch) {
242
- for (const key in watch) {
243
- const handler = watch[key]
290
+ Object.entries(watch).forEach(([key, handler]) => {
244
291
  if (Array.isArray(handler)) {
245
292
  for (let i = 0; i < handler.length; i++) {
246
293
  this.watch(key, handler[i])
@@ -248,55 +295,66 @@ export default class MPXProxy {
248
295
  } else {
249
296
  this.watch(key, handler)
250
297
  }
251
- }
298
+ })
252
299
  }
253
300
  }
254
301
 
255
- collectLocalKeys (data) {
256
- for (let key in data) {
257
- if (hasOwn(data, key)) {
258
- this.localKeysMap[key] = true
259
- }
260
- }
261
- }
302
+ watch (source, cb, options) {
303
+ const target = this.target
304
+ const getter = isString(source)
305
+ ? () => getByPath(target, source)
306
+ : source.bind(target)
262
307
 
263
- nextTick (fn) {
264
- if (typeof fn === 'function') {
265
- queueWatcher(() => {
266
- this.curRenderTask ? this.curRenderTask.promise.then(fn) : fn()
267
- })
308
+ if (isObject(cb)) {
309
+ options = cb
310
+ cb = cb.handler
268
311
  }
269
- }
270
312
 
271
- callUserHook (hookName, params) {
272
- const hook = this.options[hookName] || this.target[hookName]
273
- if (typeof hook === 'function') {
274
- try {
275
- hook.apply(this.target, params)
276
- } catch (e) {
277
- if (typeof EXPORT_MPX.config.hookErrorHandler === 'function') {
278
- EXPORT_MPX.config.hookErrorHandler(e, this.target, hookName)
279
- } else {
280
- throw e
281
- }
282
- }
313
+ if (isString(cb) && target[cb]) {
314
+ cb = target[cb]
283
315
  }
316
+
317
+ cb = cb || noop
318
+
319
+ const cur = currentInstance
320
+ setCurrentInstance(this)
321
+
322
+ const res = watch(getter, cb.bind(target), options)
323
+
324
+ if (cur) setCurrentInstance(cur)
325
+ else unsetCurrentInstance()
326
+
327
+ return res
284
328
  }
285
329
 
286
- watch (expOrFn, cb, options) {
287
- return watch(this, expOrFn, cb, options)
330
+ collectLocalKeys (data, filter = () => true) {
331
+ Object.keys(data).filter((key) => filter(key, data[key])).forEach((key) => {
332
+ this.localKeysMap[key] = true
333
+ })
288
334
  }
289
335
 
290
- clearWatchers () {
291
- let i = this._watchers.length
292
- while (i--) {
293
- this._watchers[i].teardown()
336
+ callHook (hookName, params, hooksOnly) {
337
+ const hook = this.options[hookName]
338
+ const hooks = this.hooks[hookName] || []
339
+ let result
340
+ if (isFunction(hook) && !hooksOnly) {
341
+ result = callWithErrorHandling(hook.bind(this.target), this, `${hookName} hook`, params)
294
342
  }
295
- this._watchers.length = 0
343
+ hooks.forEach((hook) => {
344
+ result = params ? hook(...params) : hook()
345
+ })
346
+ return result
347
+ }
348
+
349
+ hasHook (hookName) {
350
+ return !!(this.options[hookName] || this.hooks[hookName])
296
351
  }
297
352
 
298
353
  render () {
299
- const renderData = this.data
354
+ const renderData = {}
355
+ Object.keys(this.localKeysMap).forEach((key) => {
356
+ renderData[key] = this.target[key]
357
+ })
300
358
  this.doRender(this.processRenderDataWithStrictDiff(renderData))
301
359
  }
302
360
 
@@ -315,7 +373,7 @@ export default class MPXProxy {
315
373
 
316
374
  processRenderDataWithStrictDiff (renderData) {
317
375
  const result = {}
318
- for (let key in renderData) {
376
+ for (const key in renderData) {
319
377
  if (hasOwn(renderData, key)) {
320
378
  const data = renderData[key]
321
379
  const firstKey = getFirstKey(key)
@@ -329,7 +387,7 @@ export default class MPXProxy {
329
387
  clone = localClone
330
388
  if (diff) {
331
389
  this.miniRenderData[key] = clone
332
- if (diffData && EXPORT_MPX.config.useStrictDiff) {
390
+ if (diffData && Mpx.config.useStrictDiff) {
333
391
  this.processRenderDataWithDiffData(result, key, diffData)
334
392
  } else {
335
393
  result[key] = clone
@@ -350,12 +408,12 @@ export default class MPXProxy {
350
408
  const subPath = aIsSubPathOfB(key, tarKey)
351
409
  if (subPath) {
352
410
  // setByPath 更新miniRenderData中的子数据
353
- _getByPath(this.miniRenderData[tarKey], subPath, (current, subKey, meta) => {
411
+ doGetByPath(this.miniRenderData[tarKey], subPath, (current, subKey, meta) => {
354
412
  if (meta.isEnd) {
355
413
  const { clone, diff, diffData } = diffAndCloneA(data, current[subKey])
356
414
  if (diff) {
357
415
  current[subKey] = clone
358
- if (diffData && EXPORT_MPX.config.useStrictDiff) {
416
+ if (diffData && Mpx.config.useStrictDiff) {
359
417
  this.processRenderDataWithDiffData(result, key, diffData)
360
418
  } else {
361
419
  result[key] = clone
@@ -377,7 +435,7 @@ export default class MPXProxy {
377
435
  const { clone, diff, diffData } = diffAndCloneA(data, localInitialData)
378
436
  this.miniRenderData[key] = clone
379
437
  if (diff) {
380
- if (diffData && EXPORT_MPX.config.useStrictDiff) {
438
+ if (diffData && Mpx.config.useStrictDiff) {
381
439
  this.processRenderDataWithDiffData(result, key, diffData)
382
440
  } else {
383
441
  result[key] = clone
@@ -408,7 +466,7 @@ export default class MPXProxy {
408
466
  }
409
467
 
410
468
  const isEmpty = isEmptyObject(data) && isEmptyObject(this.forceUpdateData)
411
- const resolve = this.renderTaskExecutor(isEmpty)
469
+ const renderTask = this.createRenderTask(isEmpty)
412
470
 
413
471
  if (isEmpty) {
414
472
  cb && cb()
@@ -427,16 +485,17 @@ export default class MPXProxy {
427
485
  */
428
486
  let callback = cb
429
487
  if (this.isMounted()) {
488
+ this.callHook(BEFOREUPDATE)
430
489
  callback = () => {
431
- getRenderCallBack(this)()
432
490
  cb && cb()
433
- resolve && resolve()
491
+ this.callHook(UPDATED)
492
+ renderTask && renderTask.resolve()
434
493
  }
435
494
  }
436
495
  data = processUndefined(data)
437
- if (typeof EXPORT_MPX.config.setDataHandler === 'function') {
496
+ if (typeof Mpx.config.setDataHandler === 'function') {
438
497
  try {
439
- EXPORT_MPX.config.setDataHandler(data, this.target)
498
+ Mpx.config.setDataHandler(data, this.target)
440
499
  } catch (e) {
441
500
  }
442
501
  }
@@ -446,64 +505,119 @@ export default class MPXProxy {
446
505
  initRender () {
447
506
  if (this.options.__nativeRender__) return this.doRender()
448
507
 
449
- if (this.target.__injectedRender) {
450
- this._watcher = new Watcher(this, () => {
508
+ this.effect = new ReactiveEffect(() => {
509
+ if (this.target.__injectedRender) {
451
510
  try {
452
511
  return this.target.__injectedRender()
453
512
  } catch (e) {
454
- warn(`Failed to execute render function, degrade to full-set-data mode.`, this.options.mpxFileResource, e)
513
+ warn('Failed to execute render function, degrade to full-set-data mode.', this.options.mpxFileResource, e)
455
514
  this.render()
456
515
  }
457
- }, noop, { pausable: true })
458
- } else {
459
- this._watcher = new Watcher(this, () => {
516
+ } else {
460
517
  this.render()
461
- }, noop, { pausable: true })
462
- }
518
+ }
519
+ }, () => queueJob(update), this.scope)
520
+
521
+ const update = this.effect.run.bind(this.effect)
522
+ update.id = this.uid
523
+ update()
463
524
  }
464
525
 
465
526
  forceUpdate (data, options, callback) {
466
- if (typeof data === 'function') {
527
+ if (this.isUnmounted()) return
528
+ if (isFunction(data)) {
467
529
  callback = data
468
530
  data = undefined
469
531
  }
470
532
 
471
533
  options = options || {}
472
534
 
473
- if (typeof options === 'function') {
535
+ if (isFunction(options)) {
474
536
  callback = options
475
537
  options = {}
476
538
  }
477
539
 
478
540
  if (isPlainObject(data)) {
479
- this.forceUpdateData = data
480
- Object.keys(this.forceUpdateData).forEach(key => {
541
+ Object.keys(data).forEach(key => {
481
542
  if (!this.options.__nativeRender__ && !this.localKeysMap[getFirstKey(key)]) {
482
- warn(`ForceUpdate data includes a props/computed key [${key}], which may yield a unexpected result.`, this.options.mpxFileResource)
543
+ warn(`ForceUpdate data includes a props key [${key}], which may yield a unexpected result.`, this.options.mpxFileResource)
483
544
  }
484
- setByPath(this.data, key, this.forceUpdateData[key])
545
+ setByPath(this.target, key, data[key])
485
546
  })
547
+ this.forceUpdateData = data
486
548
  } else {
487
549
  this.forceUpdateAll = true
488
550
  }
489
551
 
490
- if (callback) {
491
- callback = callback.bind(this.target)
492
- this.nextTick(callback)
493
- }
494
- if (this._watcher) {
495
- this._watcher.update(options.sync)
552
+ if (this.effect) {
553
+ options.sync ? this.effect.run() : this.effect.update()
496
554
  } else {
497
555
  if (this.forceUpdateAll) {
498
- Object.keys(this.data).forEach((key) => {
499
- if (this.localKeysMap[key]) {
500
- this.forceUpdateData[key] = diffAndCloneA(this.data[key]).clone
501
- }
556
+ Object.keys(this.localKeysMap).forEach((key) => {
557
+ this.forceUpdateData[key] = diffAndCloneA(this.target[key]).clone
502
558
  })
503
559
  }
504
- options.sync ? this.doRender() : queueWatcher(() => {
505
- this.doRender()
506
- })
560
+ options.sync ? this.doRender() : queueJob(this.doRender.bind(this))
561
+ }
562
+
563
+ if (callback) {
564
+ callback = callback.bind(this.target)
565
+ const doCallback = () => {
566
+ if (this.currentRenderTask?.resolved === false) {
567
+ this.currentRenderTask.promise.then(callback)
568
+ } else {
569
+ callback()
570
+ }
571
+ }
572
+ options.sync ? doCallback() : nextTick(doCallback)
507
573
  }
508
574
  }
509
575
  }
576
+
577
+ export let currentInstance = null
578
+
579
+ export const getCurrentInstance = () => currentInstance
580
+
581
+ export const setCurrentInstance = (instance) => {
582
+ currentInstance = instance
583
+ instance?.scope?.on()
584
+ }
585
+
586
+ export const unsetCurrentInstance = () => {
587
+ currentInstance?.scope?.off()
588
+ currentInstance = null
589
+ }
590
+
591
+ export const injectHook = (hookName, hook, instance = currentInstance) => {
592
+ if (instance) {
593
+ const wrappedHook = (...args) => {
594
+ if (instance.isUnmounted()) return
595
+ setCurrentInstance(instance)
596
+ const res = callWithErrorHandling(hook, instance, `${hookName} hook`, args)
597
+ unsetCurrentInstance()
598
+ return res
599
+ }
600
+ if (isFunction(hook)) (instance.hooks[hookName] || (instance.hooks[hookName] = [])).push(wrappedHook)
601
+ }
602
+ }
603
+
604
+ export const createHook = (hookName) => (hook, instance) => injectHook(hookName, hook, instance)
605
+ // 在代码中调用以下生命周期钩子时, 将生命周期钩子注入到mpxProxy实例上
606
+ export const onBeforeMount = createHook(BEFOREMOUNT)
607
+ export const onMounted = createHook(MOUNTED)
608
+ export const onBeforeUpdate = createHook(BEFOREUPDATE)
609
+ export const onUpdated = createHook(UPDATED)
610
+ export const onBeforeUnmount = createHook(BEFOREUNMOUNT)
611
+ export const onUnmounted = createHook(UNMOUNTED)
612
+ export const onLoad = createHook(ONLOAD)
613
+ export const onShow = createHook(ONSHOW)
614
+ export const onHide = createHook(ONHIDE)
615
+ export const onResize = createHook(ONRESIZE)
616
+ export const onPullDownRefresh = createHook('__onPullDownRefresh__')
617
+ export const onReachBottom = createHook('__onReachBottom__')
618
+ export const onShareAppMessage = createHook('__onShareAppMessage__')
619
+ export const onShareTimeline = createHook('__onShareTimeline__')
620
+ export const onAddToFavorites = createHook('__onAddToFavorites__')
621
+ export const onPageScroll = createHook('__onPageScroll__')
622
+ export const onTabItemTap = createHook('__onTabItemTap__')
623
+ export const onSaveExitState = createHook('__onSaveExitState__')