UNPKG

@tarojs/taro-quickapp

Version:

Taro quickapp framework

404 lines (378 loc) 13 kB
import camelCase from 'lodash/camelCase' import { Current, eventCenter } from '@tarojs/taro' import { isEmptyObject, addLeadingSlash, isArray, isFunction } from './util' import { cacheDataGet, cacheDataHas, cacheDataSet } from './data-cache' import { mountComponent } from './lifecycle' import appGlobal from './global' import propsManager from './propsManager' const anonymousFnNamePreffix = 'funPrivate' const routerParamsPrivateKey = '__key_' const preloadPrivateKey = 'quick$PriPreload' const PRELOAD_DATA_KEY = 'preload' const COMP_ID = 'compid' const preloadInitedComponent = 'quick$PriPreloadComponent' const pageExtraFns = ['onBackPress', 'onMenuPress', 'onRefresh'] function bindProperties (componentConf, ComponentClass, isPage) { componentConf.properties = ComponentClass.properties || {} const defaultProps = ComponentClass.defaultProps || {} for (const key in defaultProps) { if (defaultProps.hasOwnProperty(key)) { componentConf.properties[key] = { type: null, value: defaultProps[key] } } } if (isPage) { componentConf.properties[routerParamsPrivateKey] = { type: null, value: null } componentConf.properties[preloadPrivateKey] = { type: null, value: null } const defaultParams = ComponentClass.defaultParams || {} for (const key in defaultParams) { if (defaultParams.hasOwnProperty(key)) { componentConf.properties[key] = { type: null, value: null } } } } componentConf.props = [] Object.keys(componentConf.properties).forEach(item => { componentConf.props.push(item.toLocaleLowerCase()) }) componentConf.props.push(COMP_ID) componentConf.onCompidChange = function () { initComponent.apply(this, [ComponentClass, isPage]) } } export function filterProps (defaultProps = {}, propsFromPropsManager = {}, curAllProps = {}, extraProps) { let newProps = Object.assign({}, curAllProps, propsFromPropsManager) if (!isEmptyObject(defaultProps)) { for (const propName in defaultProps) { if (newProps[propName] === undefined) { newProps[propName] = defaultProps[propName] } } } if (extraProps) { newProps = Object.assign({}, newProps, extraProps) } Object.keys(newProps).forEach(propName => { const camelizePropName = camelCase(propName) if (camelizePropName !== propName) { Object.defineProperty(newProps, camelizePropName, { get () { return newProps[propName] } }) } }) return newProps } function processEvent (eventHandlerName, obj) { if (obj[eventHandlerName]) return obj[eventHandlerName] = function (event) { if (event) { let currentTarget = event.currentTarget const target = event.target Object.defineProperties(event, { target: { configurable: true, get () { return Object.assign(target || {}, event.detail) } }, currentTarget: { configurable: true, get () { return Object.assign(currentTarget || target || {}, event.detail) }, set (target) { currentTarget = target } } }) if (!event.stopPropagation) { Object.defineProperty(event, 'stopPropagation', { value: () => {} }) } if (!event.preventDefault) { Object.defineProperty(event, 'preventDefault', { value: () => {} }) } } const scope = this.$component let callScope = scope const isAnonymousFn = eventHandlerName.indexOf(anonymousFnNamePreffix) > -1 let realArgs = [] let detailArgs = [] let datasetArgs = [] let isScopeBinded = false // 解析从dataset中传过来的参数 const dataset = {} const currentTarget = event.currentTarget const vm = currentTarget._vm || (currentTarget._target ? currentTarget._target._vm : null) const attr = vm ? vm._externalBinding.template.attr : (currentTarget._attr || currentTarget.attr) if (attr) { Object.keys(attr).forEach(key => { if (/^data/.test(key)) { const item = attr[key] dataset[key.replace(/^data/, '')] = typeof item === 'function' ? item() : item } }) } const bindArgs = {} const eventType = `on${event.type}`.toLocaleLowerCase() if (event.detail && event.detail.__detail) Object.assign(dataset, event.detail.__detail) Object.keys(dataset).forEach(key => { let keyLower = key.toLocaleLowerCase() if (/^e/.test(keyLower)) { // 小程序属性里中划线后跟一个下划线会解析成不同的结果 keyLower = keyLower.replace(/^e/, '') if (keyLower.indexOf(eventType) >= 0) { const argName = keyLower.replace(eventType, '') if (/^(a[a-z]|so)$/.test(argName)) { bindArgs[argName] = dataset[key] } } } }) // 如果是通过triggerEvent触发,并且带有参数 if (event.detail && event.detail.__arguments && event.detail.__arguments.length > 0) { detailArgs = event.detail.__arguments } // 普通的事件(非匿名函数),会直接call if (!isAnonymousFn) { if ('so' in bindArgs) { if (bindArgs['so'] !== 'this') { callScope = bindArgs['so'] } isScopeBinded = true delete bindArgs['so'] } if (detailArgs.length > 0) { !isScopeBinded && detailArgs[0] && (callScope = detailArgs[0]) detailArgs.shift() } if (!isEmptyObject(bindArgs)) { datasetArgs = Object.keys(bindArgs) .sort() .map(key => bindArgs[key]) } realArgs = [...datasetArgs, ...detailArgs, event] } else { // 匿名函数,会将scope作为第一个参数 let _scope = null if ('so' in bindArgs) { if (bindArgs['so'] !== 'this') { _scope = bindArgs['so'] } isScopeBinded = true delete bindArgs['so'] } if (detailArgs.length > 0) { !isScopeBinded && detailArgs[0] && (callScope = detailArgs[0]) detailArgs.shift() } if (!isEmptyObject(bindArgs)) { datasetArgs = Object.keys(bindArgs) .sort() .map(key => bindArgs[key]) } realArgs = [_scope, ...datasetArgs, ...detailArgs, event] } return scope[eventHandlerName].apply(callScope, realArgs) } } function bindEvents (componentConf, events) { events.forEach(name => { processEvent(name, componentConf) }) } function bindStaticFns (componentConf, ComponentClass) { for (const key in ComponentClass) { typeof ComponentClass[key] === 'function' && (componentConf[key] = ComponentClass[key]) } // 低版本 IOS 下部分属性不能直接访问 Object.getOwnPropertyNames(ComponentClass).forEach(key => { const excludes = ['arguments', 'caller', 'length', 'name', 'prototype'] if (excludes.indexOf(key) < 0) { typeof ComponentClass[key] === 'function' && (componentConf[key] = ComponentClass[key]) } }) } function getPageUrlParams (url) { const taroRouterParamsCache = appGlobal.taroRouterParamsCache let params = {} if (taroRouterParamsCache && url) { url = addLeadingSlash(url) params = taroRouterParamsCache[url] || {} delete taroRouterParamsCache[url] } return params } let hasPageInited = false function initComponent (ComponentClass, isPage) { if (!this.$component || this.$component.__isReady) return this.$component.__isReady = true if (isPage && !hasPageInited) { hasPageInited = true } if (hasPageInited && !isPage) { const compid = this.compid if (compid) { propsManager.observers[compid] = { component: this.$component, ComponentClass } } const nextProps = filterProps(ComponentClass.defaultProps, propsManager.map[compid], this.$component.props) this.$component.props = nextProps } if (hasPageInited || isPage) { mountComponent(this.$component) } } export function componentTrigger (component, key, args) { args = args || [] if (key === 'componentDidMount') { if (component['$$hasLoopRef']) { Current.current = component Current.index = 0 component._disableEffect = true component._createData(component.state, component.props, true) component._disableEffect = false Current.current = null } } if (key === 'componentWillUnmount') { const compid = component.$scope.compid if (compid) propsManager.delete(compid) } component[key] && typeof component[key] === 'function' && component[key](...args) if (key === 'componentWillMount') { component._dirty = false component._disable = false component.state = component.getState() } if (key === 'componentWillUnmount') { component._dirty = true component._disable = true component.$router = { params: {}, path: '' } component._pendingStates = [] component._pendingCallbacks = [] } } export default function createComponent (ComponentClass, isPage) { let initData = { priTaroCompReady: false } const componentProps = filterProps(ComponentClass.defaultProps) const componentInstance = new ComponentClass(componentProps) componentInstance._constructor && componentInstance._constructor(componentProps) try { Current.current = componentInstance Current.index = 0 componentInstance.state = componentInstance._createData() || componentInstance.state } catch (err) { if (isPage) { console.warn(`[Taro warn] 请给页面提供初始 \`state\` 以提高初次渲染性能!`) } else { console.warn(`[Taro warn] 请给组件提供一个 \`defaultProps\` 以提高初次渲染性能!`) } console.warn(err) } initData = Object.assign({}, initData, componentInstance.props, componentInstance.state) const componentConf = { data: initData, onInit () { isPage && (hasPageInited = false) if (isPage && cacheDataHas(preloadInitedComponent)) { this.$component = cacheDataGet(preloadInitedComponent, true) } else { this.$component = new ComponentClass({}, isPage) } this.$component._init(this) this.$component.render = this.$component._createData this.$component.__propTypes = ComponentClass.propTypes if (isPage) { this.$component.$componentType = 'PAGE' if (cacheDataHas(PRELOAD_DATA_KEY)) { const data = cacheDataGet(PRELOAD_DATA_KEY, true) this.$component.$router.preload = data } const options = getPageUrlParams(isPage) Object.assign(this.$component.$router.params, options) this.$app.pageInstaceMap = this.$app.pageInstaceMap || {} this.$app.pageInstaceMap[isPage] = this.$component if (cacheDataHas(options[preloadPrivateKey])) { this.$component.$preloadData = cacheDataGet(options[preloadPrivateKey], true) } else { this.$component.$preloadData = {} } // this.$component.$router.path = getCurrentPageUrl() initComponent.apply(this, [ComponentClass, isPage]) } // 监听数据变化 this.$watch(COMP_ID, 'onCompidChange') }, onReady () { if (!isPage) { initComponent.apply(this, [ComponentClass, isPage]) } const component = this.$component if (!component.__mounted) { component.__mounted = true componentTrigger(component, 'componentDidMount') } }, onDestroy () { componentTrigger(this.$component, 'componentWillUnmount') const component = this.$component component.hooks.forEach((hook) => { if (isFunction(hook.cleanup)) { hook.cleanup() } }) const events = component.$$renderPropsEvents if (isArray(events)) { events.forEach(e => eventCenter.off(e)) } } } if (isPage) { componentConf['onShow'] = function () { componentTrigger(this.$component, 'componentDidShow') } componentConf['onHide'] = function () { componentTrigger(this.$component, 'componentDidHide') } pageExtraFns.forEach(fn => { if (componentInstance[fn] && typeof componentInstance[fn] === 'function') { componentConf[fn] = function () { const component = this.$component if (component[fn] && typeof component[fn] === 'function') { return component[fn](...arguments) } } } }) appGlobal.componentPath = isPage addLeadingSlash(isPage) && cacheDataSet(addLeadingSlash(isPage), ComponentClass) } bindStaticFns(componentConf, ComponentClass) bindProperties(componentConf, ComponentClass, isPage) ComponentClass['privateTaroEvent'] && bindEvents(componentConf, ComponentClass['privateTaroEvent']) return componentConf }