UNPKG

14.2 kBJavaScriptView Raw
1import { getCurrentPageUrl } from '@tarojs/utils'
2import { commitAttachRef, detachAllRef, Current, eventCenter } from '@tarojs/taro'
3import { isEmptyObject, isFunction, isArray } from './util'
4import { mountComponent, updateComponent } from './lifecycle'
5import { cacheDataSet, cacheDataGet, cacheDataHas } from './data-cache'
6import propsManager from './propsManager'
7import nextTick from './next-tick'
8
9const anonymousFnNamePreffix = 'funPrivate'
10const preloadPrivateKey = '__preload_'
11const preloadInitedComponent = '$preloadComponent'
12const PRELOAD_DATA_KEY = 'preload'
13const pageExtraFns = ['onPullDownRefresh', 'onReachBottom', 'onShareAppMessage', 'onPageScroll', 'onTabItemTap']
14
15function bindProperties (weappComponentConf, ComponentClass, isPage) {
16 weappComponentConf.properties = {}
17 if (isPage) {
18 weappComponentConf.properties[preloadPrivateKey] = {
19 type: null,
20 value: null
21 }
22 }
23 weappComponentConf.properties.compid = {
24 type: null,
25 value: null,
26 observer (newVal, oldVal) {
27 // 头条基础库1.38.2后,太早 setData $taroCompReady 为 true 时,setData 虽然成功,但 slot 会不显示。
28 // 因此不在 observer 里 initComponent,在组件 attached 时 initComponent 吧。
29 // initComponent.apply(this, [ComponentClass, isPage])
30 if (oldVal && oldVal !== newVal) {
31 const { extraProps } = this.data
32 const component = this.$component
33 propsManager.observers[newVal] = {
34 component,
35 ComponentClass: component.constructor
36 }
37 const nextProps = filterProps(component.constructor.defaultProps, propsManager.map[newVal], component.props, extraProps || null)
38 this.$component.props = nextProps
39 nextTick(() => {
40 this.$component._unsafeCallUpdate = true
41 updateComponent(this.$component)
42 this.$component._unsafeCallUpdate = false
43 })
44 }
45 }
46 }
47}
48
49function bindBehaviors (weappComponentConf, ComponentClass) {
50 if (ComponentClass.behaviors) {
51 weappComponentConf.behaviors = ComponentClass.behaviors
52 }
53}
54
55function bindStaticOptions (weappComponentConf, ComponentClass) {
56 if (ComponentClass.options) {
57 weappComponentConf.options = ComponentClass.options
58 }
59}
60
61function bindStaticFns (weappComponentConf, ComponentClass) {
62 for (const key in ComponentClass) {
63 typeof ComponentClass[key] === 'function' && (weappComponentConf[key] = ComponentClass[key])
64 }
65 // 低版本 IOS 下部分属性不能直接访问
66 Object.getOwnPropertyNames(ComponentClass).forEach(key => {
67 const excludes = ['arguments', 'caller', 'length', 'name', 'prototype']
68 if (excludes.indexOf(key) < 0) {
69 typeof ComponentClass[key] === 'function' && (weappComponentConf[key] = ComponentClass[key])
70 }
71 })
72}
73
74function processEvent (eventHandlerName, obj) {
75 if (obj[eventHandlerName]) return
76
77 obj[eventHandlerName] = function (event) {
78 if (event) {
79 event.preventDefault = function () {}
80 event.stopPropagation = function () {}
81 event.currentTarget = event.currentTarget || event.target || {}
82 if (event.target) {
83 Object.assign(event.target, event.detail)
84 }
85 Object.assign(event.currentTarget, event.detail)
86 }
87
88 const scope = this.$component
89 if (!scope || !scope[eventHandlerName]) return
90
91 let callScope = scope
92 const isAnonymousFn = eventHandlerName.indexOf(anonymousFnNamePreffix) > -1
93 let realArgs = []
94 let detailArgs = []
95 let datasetArgs = []
96 let isScopeBinded = false
97 // 解析从dataset中传过来的参数
98 const dataset = event.currentTarget.dataset || {}
99 const bindArgs = {}
100 const eventType = event.type ? event.type.toLocaleLowerCase() : null
101
102 if (event.detail && event.detail.__detail) Object.assign(dataset, event.detail.__detail)
103
104 Object.keys(dataset).forEach(key => {
105 let keyLower = key.toLocaleLowerCase()
106 if (/^e/.test(keyLower)) {
107 // 小程序属性里中划线后跟一个下划线会解析成不同的结果
108 keyLower = keyLower.replace(/^e/, '')
109 if (keyLower.indexOf(eventType) >= 0) {
110 const argName = keyLower.replace(eventType, '')
111 if (/^(a[a-z]|so)$/.test(argName)) {
112 bindArgs[argName] = dataset[key]
113 }
114 }
115 }
116 })
117 // 如果是通过triggerEvent触发,并且带有参数
118 if (event.detail && event.detail.__arguments && event.detail.__arguments.length > 0) {
119 detailArgs = event.detail.__arguments
120 }
121 // 普通的事件(非匿名函数),会直接call
122 if (!isAnonymousFn) {
123 if ('so' in bindArgs) {
124 if (bindArgs['so'] !== 'this') {
125 callScope = bindArgs['so']
126 }
127 isScopeBinded = true
128 delete bindArgs['so']
129 }
130 if (detailArgs.length > 0) {
131 !isScopeBinded && detailArgs[0] && (callScope = detailArgs[0])
132 detailArgs.shift()
133 }
134 if (!isEmptyObject(bindArgs)) {
135 datasetArgs = Object.keys(bindArgs)
136 .sort()
137 .map(key => bindArgs[key])
138 }
139 realArgs = [...datasetArgs, ...detailArgs, event]
140 } else {
141 // 匿名函数,会将scope作为第一个参数
142 let _scope = null
143 if ('so' in bindArgs) {
144 if (bindArgs['so'] !== 'this') {
145 _scope = bindArgs['so']
146 }
147 isScopeBinded = true
148 delete bindArgs['so']
149 }
150 if (detailArgs.length > 0) {
151 !isScopeBinded && detailArgs[0] && (callScope = detailArgs[0])
152 detailArgs.shift()
153 }
154 if (!isEmptyObject(bindArgs)) {
155 datasetArgs = Object.keys(bindArgs)
156 .sort()
157 .map(key => bindArgs[key])
158 }
159 realArgs = [_scope, ...datasetArgs, ...detailArgs, event]
160 }
161 return scope[eventHandlerName].apply(callScope, realArgs)
162 }
163}
164
165function bindEvents (weappComponentConf, events, isPage) {
166 weappComponentConf.methods = weappComponentConf.methods || {}
167 const target = isPage ? weappComponentConf : weappComponentConf.methods
168 events.forEach(name => {
169 processEvent(name, target)
170 })
171}
172
173export function filterProps (defaultProps = {}, propsFromPropsManager = {}, curAllProps = {}) {
174 let newProps = Object.assign({}, curAllProps, propsFromPropsManager)
175
176 if (!isEmptyObject(defaultProps)) {
177 for (const propName in defaultProps) {
178 if (newProps[propName] === undefined) {
179 newProps[propName] = defaultProps[propName]
180 }
181 }
182 }
183 return newProps
184}
185
186export function componentTrigger (component, key, args) {
187 args = args || []
188
189 if (key === 'componentDidMount') {
190 if (component['$$hasLoopRef']) {
191 Current.current = component
192 Current.index = 0
193 component._disableEffect = true
194 component._createData(component.state, component.props, true)
195 component._disableEffect = false
196 Current.current = null
197 }
198
199 if (component['$$refs'] && component['$$refs'].length > 0) {
200 let refs = {}
201 const refComponents = []
202 component['$$refs'].forEach(ref => {
203 refComponents.push(new Promise((resolve, reject) => {
204 const query = tt.createSelectorQuery().in(component.$scope)
205 if (ref.type === 'component') {
206 component.$scope.selectComponent(`#${ref.id}`, target => {
207 resolve({
208 target: target ? target.$component || target : null,
209 ref
210 })
211 })
212 } else {
213 resolve({
214 target: query.select(`#${ref.id}`),
215 ref
216 })
217 }
218 }))
219 })
220 Promise.all(refComponents)
221 .then(targets => {
222 targets.forEach(({ ref, target }) => {
223 commitAttachRef(ref, target, component, refs, true)
224 ref.target = target
225 })
226 component.refs = Object.assign({}, component.refs || {}, refs)
227 // 此处执行componentDidMount
228 component[key] && typeof component[key] === 'function' && component[key](...args)
229 })
230 .catch(err => {
231 console.error(err)
232 component[key] && typeof component[key] === 'function' && component[key](...args)
233 })
234 // 此处跳过执行componentDidMount,在refComponents完成后再次执行
235 return
236 }
237 }
238
239 if (key === 'componentWillUnmount') {
240 const compid = component.$scope.data.compid
241 if (compid) propsManager.delete(compid)
242 }
243
244 component[key] && typeof component[key] === 'function' && component[key](...args)
245 if (key === 'componentWillUnmount') {
246 component._dirty = true
247 component._disable = true
248 component.$router = {
249 params: {},
250 path: ''
251 }
252 component._pendingStates = []
253 component._pendingCallbacks = []
254 // refs
255 detachAllRef(component)
256 }
257 if (key === 'componentWillMount') {
258 component._dirty = false
259 component._disable = false
260 component.state = component.getState()
261 }
262}
263
264let hasPageInited = false
265
266function initComponent (ComponentClass, isPage) {
267 if (!this.$component || this.$component.__isReady) return
268 // ready之后才可以setData,
269 // ready之前,小程序组件初始化时仍然会触发observer,__isReady为否的时候放弃处理observer
270 this.$component.__isReady = true
271
272 if (isPage && !hasPageInited) {
273 hasPageInited = true
274 }
275 // 页面Ready的时候setData更新,此时并未didMount,触发observer但不会触发子组件更新
276 // 小程序组件ready,但是数据并没有ready,需要通过updateComponent来初始化数据,setData完成之后才是真正意义上的组件ready
277 // 动态组件执行改造函数副本的时,在初始化数据前计算好props
278 if (hasPageInited && !isPage) {
279 const compid = this.data.compid
280 if (compid) {
281 propsManager.observers[compid] = {
282 component: this.$component,
283 ComponentClass
284 }
285 }
286 const nextProps = filterProps(ComponentClass.defaultProps, propsManager.map[compid], this.$component.props)
287 this.$component.props = nextProps
288 }
289 if (hasPageInited || isPage) {
290 mountComponent(this.$component)
291 }
292}
293
294function createComponent (ComponentClass, isPage) {
295 let initData = {}
296 const componentProps = filterProps(ComponentClass.defaultProps)
297 const componentInstance = new ComponentClass(componentProps)
298 componentInstance._constructor && componentInstance._constructor(componentProps)
299 try {
300 Current.current = componentInstance
301 Current.index = 0
302 componentInstance.state = componentInstance._createData() || componentInstance.state
303 } catch (err) {
304 if (isPage) {
305 console.warn(`[Taro warn] 请给页面提供初始 \`state\` 以提高初次渲染性能!`)
306 } else {
307 console.warn(`[Taro warn] 请给组件提供一个 \`defaultProps\` 以提高初次渲染性能!`)
308 }
309 console.warn(err)
310 }
311 initData = Object.assign({}, initData, componentInstance.props, componentInstance.state)
312
313 const weappComponentConf = {
314 data: initData,
315 created (options = {}) {
316 isPage && (hasPageInited = false)
317 if (isPage && cacheDataHas(preloadInitedComponent)) {
318 this.$component = cacheDataGet(preloadInitedComponent, true)
319 this.$component.$componentType = 'PAGE'
320 } else {
321 this.$component = new ComponentClass({}, isPage)
322 }
323 this.$component._init(this)
324 this.$component.render = this.$component._createData
325 this.$component.__propTypes = ComponentClass.propTypes
326 if (isPage) {
327 if (cacheDataHas(PRELOAD_DATA_KEY)) {
328 const data = cacheDataGet(PRELOAD_DATA_KEY, true)
329 this.$component.$router.preload = data
330 }
331 Object.assign(this.$component.$router.params, options)
332 if (cacheDataHas(options[preloadPrivateKey])) {
333 this.$component.$preloadData = cacheDataGet(options[preloadPrivateKey], true)
334 } else {
335 this.$component.$preloadData = {}
336 }
337 this.$component.$router.path = getCurrentPageUrl()
338 initComponent.apply(this, [ComponentClass, isPage])
339 }
340 },
341 attached () {
342 initComponent.apply(this, [ComponentClass, isPage])
343 },
344 ready () {
345 if (!this.$component.__mounted) {
346 this.$component.__mounted = true
347 componentTrigger(this.$component, 'componentDidMount')
348 }
349 },
350 detached () {
351 const component = this.$component
352 componentTrigger(component, 'componentWillUnmount')
353 component.hooks.forEach((hook) => {
354 if (isFunction(hook.cleanup)) {
355 hook.cleanup()
356 }
357 })
358 const events = component.$$renderPropsEvents
359 if (isArray(events)) {
360 events.forEach(e => eventCenter.off(e))
361 }
362 }
363 }
364 if (isPage) {
365 weappComponentConf['onLoad'] = weappComponentConf['created']
366 weappComponentConf['onReady'] = weappComponentConf['ready']
367 weappComponentConf['onUnload'] = weappComponentConf['detached']
368 weappComponentConf['onShow'] = function () {
369 componentTrigger(this.$component, 'componentDidShow')
370 }
371 weappComponentConf['onHide'] = function () {
372 componentTrigger(this.$component, 'componentDidHide')
373 }
374 pageExtraFns.forEach(fn => {
375 if (componentInstance[fn] && typeof componentInstance[fn] === 'function') {
376 weappComponentConf[fn] = function () {
377 const component = this.$component
378 if (component && component[fn] && typeof component[fn] === 'function') {
379 return component[fn](...arguments)
380 }
381 }
382 }
383 })
384 ComponentClass.$$componentPath && cacheDataSet(ComponentClass.$$componentPath, ComponentClass)
385 }
386 bindProperties(weappComponentConf, ComponentClass, isPage)
387 bindBehaviors(weappComponentConf, ComponentClass)
388 bindStaticFns(weappComponentConf, ComponentClass)
389 bindStaticOptions(weappComponentConf, ComponentClass)
390 ComponentClass['$$events'] && bindEvents(weappComponentConf, ComponentClass['$$events'], isPage)
391 if (ComponentClass['externalClasses'] && ComponentClass['externalClasses'].length) {
392 weappComponentConf['externalClasses'] = ComponentClass['externalClasses']
393 }
394 return weappComponentConf
395}
396
397export default createComponent