diff --git a/packages/okam-core/src/ant/base/component.js b/packages/okam-core/src/ant/base/component.js index 6887ed8a..f2c14b7e 100644 --- a/packages/okam-core/src/ant/base/component.js +++ b/packages/okam-core/src/ant/base/component.js @@ -7,22 +7,7 @@ import componentBase from '../../base/component'; - /** - * Emit custom component event - * - * @param {...any} args the event arguments - */ -componentBase.methods.$emit = function (...args) { - this.__beforeEmit && this.__beforeEmit(args); - this.$listener.emit.apply(this.$listener, args); - - let eventProp = args[0]; - eventProp = 'on' + eventProp.charAt(0).toUpperCase() + eventProp.substr(1); - let eventHandler = this.props[eventProp]; - eventHandler.call(this, args[1]); -}; - -export default Object.assign(componentBase, { +const antComponent = Object.assign({}, componentBase, { didMount() { this.created(); this.attached(); @@ -38,3 +23,23 @@ export default Object.assign(componentBase, { this.detached(); } }); + +antComponent.methods = Object.assign({}, antComponent.methods, { + + /** + * Emit custom component event + * + * @param {...any} args the event arguments + */ + $emit(...args) { + this.__beforeEmit && this.__beforeEmit(args); + this.$listener.emit.apply(this.$listener, args); + + let eventProp = args[0]; + eventProp = 'on' + eventProp.charAt(0).toUpperCase() + eventProp.substr(1); + let eventHandler = this.props[eventProp]; + eventHandler.call(this, args[1]); + } +}); + +export default antComponent; diff --git a/packages/okam-core/src/base/page.js b/packages/okam-core/src/base/page.js index e53bd40f..25ccac3c 100644 --- a/packages/okam-core/src/base/page.js +++ b/packages/okam-core/src/base/page.js @@ -16,6 +16,7 @@ export default Object.assign({}, component, { * @param {Object} query the page query params */ onLoad(query) { + this.$isPage = true; this.$query = query || {}; // call component create life cycle method diff --git a/packages/okam-core/src/extend/data/observable/Observer.js b/packages/okam-core/src/extend/data/observable/Observer.js index 09378673..5b933807 100644 --- a/packages/okam-core/src/extend/data/observable/Observer.js +++ b/packages/okam-core/src/extend/data/observable/Observer.js @@ -47,11 +47,12 @@ export function proxyObject(observer, data, root) { * * @param {Observer} observer the observer to observe array * @param {Array} arr the array data to proxy + * @param {boolean} isPage whether is page component * @return {Array} */ -export function proxyArray(observer, arr) { +export function proxyArray(observer, arr, isPage) { let newArr = []; - makeArrayObservable(newArr, observer); + makeArrayObservable(newArr, observer, isPage); // XXX: copy array // we cannot proxy array element visited by index, so we should not proxy array element by default @@ -85,6 +86,41 @@ export default class Observer { this.selector = getDataSelector(this.paths); } + /** + * Set context data + * + * @private + * @param {*} value the value to set + */ + setContextData(value) { + let paths = this.paths; + let result = this.ctx.data; + let lastIdx = paths.length - 1; + for (let i = 0; i < lastIdx; i++) { + let p = paths[i]; + result = result[p]; + } + + /* istanbul ignore next */ + if (lastIdx >= 0) { + result[paths[lastIdx]] = value; + } + } + + /** + * Get context data + * + * @return {*} + */ + getContextData() { + let paths = this.paths; + let result = this.ctx.data; + for (let i = 0, len = paths.length; i < len; i++) { + result = result[paths[i]]; + } + return result; + } + /** * Add dependence to current target computed property * @@ -121,7 +157,7 @@ export default class Observer { if (Array.isArray(value)) { paths || (paths = this.getPaths(k)); let observer = new Observer(ctx, value, paths, this.isProps); - return (observeData[k] = proxyArray(observer, value)); + return (observeData[k] = proxyArray(observer, value, ctx.$isPage)); } else if (value && typeof value === 'object') { paths || (paths = this.getPaths(k)); @@ -189,6 +225,9 @@ export default class Observer { this.rawData[k] = val; } + else { + this.setContextData(this.rawData); + } this.ctx.$setData({[selector]: val}); this.notifyWatcher(val, oldVal, paths); diff --git a/packages/okam-core/src/extend/data/observable/ant/array.js b/packages/okam-core/src/extend/data/observable/ant/array.js new file mode 100644 index 00000000..5cd813db --- /dev/null +++ b/packages/okam-core/src/extend/data/observable/ant/array.js @@ -0,0 +1,81 @@ +/** + * @file Array data update proxy for ant application + * @author sparklewhy@gmail.com + */ + +'use strict'; + +/* eslint-disable fecs-export-on-declare */ + +const componentApi = { + __spliceData(path, spliceArgs, observer) { + let spliceInfo = {[path]: spliceArgs}; + this.$spliceData(spliceInfo, this.__nextTickCallback); + // update the cache raw data + observer.rawData = observer.getContextData(); + } +}; + +const observableArray = { + push(observer, rawPush, ...items) { + let {ctx, selector} = observer; + + ctx.__spliceData(selector, [this.length, 0, ...items], observer); + + return rawPush.apply(this, items); + }, + + shift(observer, rawShift) { + let {ctx, selector} = observer; + if (!this.length) { + return rawShift.call(this); + } + + ctx.__spliceData(selector, [0, 1], observer); + + return rawShift.call(this); + }, + + unshift(observer, rawUnshift, ...items) { + let {ctx, selector} = observer; + ctx.__spliceData(selector, [0, 0, ...items], observer); + + return rawUnshift.apply(this, items); + }, + + pop(observer, rawPop) { + let {ctx, selector} = observer; + let len = this.length; + if (!len) { + return rawPop.call(this); + } + ctx.__spliceData(selector, [len - 1, 1], observer); + + return rawPop.call(this); + }, + + splice(observer, rawSplice, ...args) { + let {ctx, selector} = observer; + ctx.__spliceData(selector, args, observer); + + return rawSplice.apply(this, args); + } +}; + +Object.keys(observableArray).forEach(k => { + let raw = observableArray[k]; + observableArray[k] = function (...args) { + let observer = args[0]; + observer.ctx.__dataUpTaskNum++; + let result = raw.apply(this, args); + + observer.notifyWatcher(this, this, observer.paths); + + return result; + }; +}); + +export { + observableArray as array, + componentApi as component +}; diff --git a/packages/okam-core/src/extend/data/observable/ant/index.js b/packages/okam-core/src/extend/data/observable/ant/index.js index acf5947b..32e4ae5a 100644 --- a/packages/okam-core/src/extend/data/observable/ant/index.js +++ b/packages/okam-core/src/extend/data/observable/ant/index.js @@ -5,8 +5,42 @@ 'use strict'; -import observable, {setPropDataKey} from '../index'; +import observable, {setObservableContext} from '../index'; +import {observableArray, overrideArrayMethods} from '../array'; +import {component as antApi, array as antArray} from './array'; -setPropDataKey('props'); +setObservableContext('props', true); + +Object.assign(observable.component.methods, antApi); + +let arrApis = Object.assign({}, observableArray, antArray); +overrideArrayMethods(arrApis, true); +overrideArrayMethods(arrApis, false); + +/** + * View update hook + * + * @private + * @param {Object} prevProps the previous property data before update + */ +observable.component.didUpdate = function (prevProps) { + let propObserver = this.__propsObserver; + if (!propObserver) { + return; + } + + let currProps = this.props; + // update the cache props data, as for the prop data will be override + // when prop change, it leads to the cache props data will not refer to + // the new props data + propObserver.rawData = currProps; + Object.keys(prevProps).forEach(k => { + let newVal = currProps[k]; + let oldVal = prevProps[k]; + if (newVal !== oldVal) { + propObserver.firePropValueChange(k, newVal, oldVal); + } + }); +}; export default observable; diff --git a/packages/okam-core/src/extend/data/observable/array.js b/packages/okam-core/src/extend/data/observable/array.js index e329aab0..12eb0d65 100644 --- a/packages/okam-core/src/extend/data/observable/array.js +++ b/packages/okam-core/src/extend/data/observable/array.js @@ -5,6 +5,13 @@ 'use strict'; +const hasProto = '__proto__' in {}; + +/** + * The default override Array APIs to proxy array data update + * + * @type {Object} + */ export const observableArray = { push(observer, rawPush, ...items) { let rawData = observer.rawData; @@ -80,6 +87,37 @@ export const observableArray = { } }; +/** + * The Page Array APIs to override + * + * @inner + * @type {Object} + */ +let overridePageArrApis = observableArray; + +/** + * The component Array APIs to override + * + * @inner + * @type {Object} + */ +let overrideComponentArrApis = observableArray; + +/** + * Extend the array operation methods + * + * @param {Object} arrApis the array methods to override + * @param {boolean} isPage whether is page Array APIs to override + */ +export function overrideArrayMethods(arrApis, isPage) { + if (isPage) { + overridePageArrApis = arrApis; + } + else { + overrideComponentArrApis = arrApis; + } +} + /** * Update array item value * @@ -103,41 +141,34 @@ function getArrayItem(observer, idx) { return observer.get(idx); } -let overrideArrApis = observableArray; - /** - * Extend the array operation methods + * Make array observable * - * @param {Object} extension the array method extension - * @param {boolean} isOverride whether override default array extend apis, - * by default false, for test purpose + * @param {Array} arr the array to observe + * @param {Observer} observer the observer + * @param {boolean} isPage whether is page Array APIs to override + * @return {Array} */ -export function extendArrayMethods(extension, isOverride) { +export default function makeArrayObservable(arr, observer, isPage) { + let arrayMethods; /* istanbul ignore next */ - if (isOverride) { - overrideArrApis = extension; + if (hasProto) { + arrayMethods = Object.create(Array.prototype); + /* eslint-disable no-proto */ + arr.__proto__ = arrayMethods; } else { - /* istanbul ignore next */ - Object.assign(overrideArrApis, extension); + arrayMethods = arr; } -} -/** - * Make array observable - * - * @param {Array} arr the array to observe - * @param {Observer} observer the observer - * @return {Array} - */ -export default function makeArrayObservable(arr, observer) { + let overrideArrApis = isPage ? overridePageArrApis : overrideComponentArrApis; Object.keys(overrideArrApis).forEach(method => { - let rawMethod = arr[method]; - arr[method] = overrideArrApis[method].bind(arr, observer, rawMethod); + let rawMethod = arrayMethods[method]; + arrayMethods[method] = overrideArrApis[method].bind(arr, observer, rawMethod); }); - arr.setItem = updateArrayItem.bind(arr, observer); - arr.getItem = getArrayItem.bind(arr, observer); + arrayMethods.setItem = updateArrayItem.bind(arr, observer); + arrayMethods.getItem = getArrayItem.bind(arr, observer); return arr; } diff --git a/packages/okam-core/src/extend/data/observable/index.js b/packages/okam-core/src/extend/data/observable/index.js index a206f769..6c704efb 100644 --- a/packages/okam-core/src/extend/data/observable/index.js +++ b/packages/okam-core/src/extend/data/observable/index.js @@ -20,6 +20,13 @@ import {getSetDataPaths} from './setData'; */ let propDataKey = 'data'; +/** + * Whether skip the `beforeUpdate`/`updated` hook + * + * @type {boolean} + */ +let isSkipUpdateHook = false; + /** * Make computed props observable * @@ -63,7 +70,12 @@ function makePropsObservable(ctx) { return; } - let observer = new Observer(ctx, ctx[propDataKey] || {}, null, true); + let observer = new Observer( + ctx, + ctx[propDataKey] || /* istanbul ignore next */ {}, + null, + true + ); let propsObj = {}; Object.keys(props).reduce((last, item) => { @@ -103,58 +115,40 @@ function makeDataObservable(ctx) { } /** - * Initialize the props to add observer to the prop to listen the prop change. - * - * @inner - * @param {Object} ctx the component definition context - * @param {boolean} isPage whether is page component - */ -function initProps(ctx, isPage) { - // cache the raw props information because the mini program will merge data - // and props later on. - let props = ctx.props; - if (!props) { - return; - } - - let rawProps = Object.assign({}, props); - ctx.rawProps = rawProps; - normalizeExtendProp(ctx, 'rawProps', '$rawProps', isPage); - - Object.keys(props).forEach(p => { - let value = props[p]; - let rawObserver = value.observer; - value.observer = function (newVal, oldVal, changePath) { - rawObserver && rawObserver.call(this, newVal, oldVal, changePath); - let propObserver = this.__propsObserver; - propObserver && propObserver.firePropValueChange(p, newVal, oldVal); - }; - }); -} - -/** - * Set the component property data key + * Set observable context setting * * @param {string} key the prop data key + * @param {boolean} ignoreUpdateHook whether skip update hook */ -export function setPropDataKey(key) { - key && (propDataKey = key); +export function setObservableContext(key, ignoreUpdateHook) { + propDataKey = key; + isSkipUpdateHook = !!ignoreUpdateHook; } export default { component: { /** - * The instance initialization before the instance is normalized and created. + * Initialize the props to add observer to the prop to listen the prop change. * * @param {boolean} isPage whether is page component - * @private */ $init(isPage) { - initProps(this, isPage); - // normalize extend computed property normalizeExtendProp(this, 'computed', '$rawComputed', isPage); + + // cache the raw props information because the mini program will merge data + // and props later on. + let props = this.props; + if (!props) { + return; + } + + let rawProps = Object.assign({}, props); + this._rawProps = rawProps; + normalizeExtendProp(this, '_rawProps', '$rawProps', isPage); + + this.__initProps && this.__initProps(); }, /** @@ -187,7 +181,7 @@ export default { // init computed data computedObserver.initComputedPropValues(); - this.afterObserverInit && this.afterObserverInit(); + this.__afterObserverInit && this.__afterObserverInit(); }, /** @@ -245,7 +239,7 @@ export default { } // call lifecycle updated hook - this.updated && this.updated(); + isSkipUpdateHook || (this.updated && this.updated()); }, /** @@ -263,7 +257,7 @@ export default { if (queues) { // TODO optimize value update: merge operations // call lifecycle beforeUpdate hook - this.beforeUpdate && this.beforeUpdate(); + isSkipUpdateHook || (this.beforeUpdate && this.beforeUpdate()); this.setData(getSetDataPaths(queues), this.__nextTickCallback); this.$upQueues = null; } diff --git a/packages/okam-core/src/extend/data/observable/initProps.js b/packages/okam-core/src/extend/data/observable/initProps.js new file mode 100644 index 00000000..6a81db41 --- /dev/null +++ b/packages/okam-core/src/extend/data/observable/initProps.js @@ -0,0 +1,22 @@ +/** + * @file Props initialization helper + * @author sparklewhy@gmail.com + */ + +'use strict'; + +/** + * Initialize the props to add observer to the prop to listen the prop change. + */ +export default function initProps() { + let props = this.props; + Object.keys(props).forEach(p => { + let value = props[p]; + let rawObserver = value.observer; + value.observer = function (newVal, oldVal, changePath) { + rawObserver && rawObserver.call(this, newVal, oldVal, changePath); + let propObserver = this.__propsObserver; + propObserver && propObserver.firePropValueChange(p, newVal, oldVal); + }; + }); +} diff --git a/packages/okam-core/src/extend/data/observable/wx/index.js b/packages/okam-core/src/extend/data/observable/wx/index.js new file mode 100644 index 00000000..feeed31e --- /dev/null +++ b/packages/okam-core/src/extend/data/observable/wx/index.js @@ -0,0 +1,13 @@ +/** + * @file Make component support data operation like Vue for weixin mini program + * @author sparklewhy@gmail.com + */ + +'use strict'; + +import observable from '../index'; +import initProps from '../initProps'; + +observable.component.__initProps = initProps; + +export default observable; diff --git a/packages/okam-core/src/extend/data/watch.js b/packages/okam-core/src/extend/data/watch.js index 92fd40ad..44388806 100644 --- a/packages/okam-core/src/extend/data/watch.js +++ b/packages/okam-core/src/extend/data/watch.js @@ -128,7 +128,7 @@ export default { * * @private */ - afterObserverInit() { + __afterObserverInit() { let watch = this.$rawWatch; if (typeof watch === 'function') { watch = this.$rawWatch(); diff --git a/packages/okam-core/src/helper/methods.js b/packages/okam-core/src/helper/methods.js index 512f3a7d..8773162d 100644 --- a/packages/okam-core/src/helper/methods.js +++ b/packages/okam-core/src/helper/methods.js @@ -18,7 +18,7 @@ const extendPropMethods = [ ]; /** - * Normalize extended property + * Normalize extended property: convert the extended prop to methods prop * * @param {Object} component the component instance * @param {string} propName the extended property name