okam-core
Version:
The extension for small program framework
302 lines (265 loc) • 9.21 kB
JavaScript
/**
* @file Make component support data operation like Vue
* @author sparklewhy@gmail.com
*/
'use strict';
import watchDataChange from './watch';
/**
* Proxy data getter
*
* @inner
* @param {Object} ctx the component instance
* @param {string} prop the property name
*/
function proxyDataGetter(ctx, prop) {
let proxyProps = ctx.__proxyProps;
proxyProps || (proxyProps = ctx.__proxyProps = {});
if (proxyProps[prop]) {
return;
}
proxyProps[prop] = true;
let descriptor = Object.getOwnPropertyDescriptor(ctx, prop);
if (descriptor && descriptor.configurable) {
let newDescriptor = Object.assign({}, descriptor, {
get() {
ctx.__deps && ctx.__deps.push(prop);
return descriptor.get && descriptor.get.call(ctx);
}
});
Object.defineProperty(ctx, prop, newDescriptor);
}
else {
console.warn('cannot configure the data prop descriptor info:', prop);
}
}
/**
* Get inner watcher name
*
* @inner
* @param {string} prop the watch prop name
* @return {string}
*/
function getInnerWatcher(prop) {
return `__watcher_${prop}`;
}
/**
* Add data change watcher
*
* @inner
* @param {Object} ctx the component instance
* @param {string} prop the data prop name
* @param {boolean=} deep whether watch deep
*/
function addDataChangeWatcher(ctx, prop, deep) {
// in quick app, the watch handler does not support dynamic added methods
// let handler = '__handleDataChange$' + prop;
// ctx[handler] = (newVal, oldVal) => ctx.__handleDataChange(
// prop, newVal, oldVal
// );
// FIXME: array cannot support deep watch in quick app when change array item info
let handlerName = getInnerWatcher(prop);
watchDataChange.call(ctx, prop, handlerName, {deep});
}
/**
* Collect the computed prop dependence data props
*
* @inner
* @param {Object} ctx the component instance
* @param {string} prop the computed prop name
* @param {Function} getter the computed getter
* @return {*}
*/
function collectComputedPropDeps(ctx, prop, getter) {
ctx.__deps = [];
let value = getter.call(ctx);
ctx.__computedDeps[prop] = ctx.__deps;
ctx.__deps = null;
return value;
}
/**
* Find the changed computed props
*
* @inner
* @param {Object} allDeps all computed props deps info
* @param {string} changeProp the changed prop name
* @return {Array.<string>}
*/
function findChangeComputedProps(allDeps, changeProp) {
let result = [];
Object.keys(allDeps).forEach(k => {
let depList = allDeps[k];
if (k !== changeProp && depList.indexOf(changeProp) !== -1) {
result.push(k);
}
});
return result;
}
export default {
component: {
/**
* Initialize the props to add observer to the prop to listen the prop change.
*
* @param {boolean} isPage whether is page component
*/
$init(isPage) {
let computed = this.computed;
if (computed) {
this.$rawComputed = () => computed;
delete this.computed;
}
// collect all data, props and computed keys
let data = this.data;
if (typeof data === 'function') {
data = data();
}
let dataKeys = data ? Object.keys(data) : [];
let props = this.props;
if (props) {
Object.keys(props).forEach(k => {
if (dataKeys.indexOf(k) === -1) {
dataKeys.push(k);
}
});
}
computed && Object.keys(computed).forEach(k => {
if (dataKeys.indexOf(k) === -1) {
dataKeys.push(k);
}
});
// init watcher
// we must declare all watcher callback before defining component
// as for it does not support dynamic watcher callback declaration
// in quick app.
dataKeys.forEach(k => {
this[getInnerWatcher(k)] = function (newVal, oldVal) {
this.__handleDataChange(k, newVal, oldVal);
};
});
this.__allDataKeys = () => dataKeys;
},
/**
* The created hook
*
* @private
*/
created() {
this.__originalWatch = this.__originalWatch || this.$watch;
let computedInfo = this.$rawComputed;
if (typeof computedInfo === 'function') {
this.$rawComputed = computedInfo = computedInfo();
}
// watch all data keys
this.__allDataKeys = this.__allDataKeys();
this.__allDataKeys.forEach(k => {
let isComputedProp = computedInfo && computedInfo[k];
addDataChangeWatcher(this, k, !isComputedProp);
if (!isComputedProp) {
proxyDataGetter(this, k);
}
});
// override $set API
let rawSet = this.$set;
this.$set = (...args) => {
let k = args[0];
if (this.__allDataKeys.indexOf(k) === -1) {
this.__allDataKeys.push(k);
// addDataChangeWatcher(this, k);
}
let result = rawSet.apply(this, args);
proxyDataGetter(this, k);
return result;
};
// add computed data
this.__computedDeps = {};
computedInfo && Object.keys(computedInfo).forEach(k => {
let getter = computedInfo[k];
let value = getter.call(this);
this.$set(k, value);
});
// collect computed props deps
computedInfo && Object.keys(computedInfo).forEach(
k => collectComputedPropDeps(this, k, computedInfo[k])
);
},
/**
* Watch data change which support deep and immediate options
*
* @param {string} expression the expression to watch
* @param {string} handlerName the callback handler name to execute when the
* expression value changes
* @param {Object=} options watch options
* @param {boolean=} options.immediate whether trigger the callback
* immediately with the current value of the expression or function
* optional, by default false
* @param {boolean=} optional.deep whether watch object nested value
* optional, by default false
* @return {*}
*/
__watchDataChange: watchDataChange,
/**
* Add anonymous computed prop
*
* @protected
* @param {Function} getter the computed getter
* @return {string} the anonymous computed prop name
*/
__addComputedProp(getter) {
if (!this.__computedCounter) {
this.__computedCounter = 1;
}
let prop = `__c${this.__computedCounter++}`;
this.$rawComputed[prop] = getter;
let value = getter.call(this);
this.$set(prop, value);
collectComputedPropDeps(this, prop, getter);
return prop;
},
/**
* Handle data change
*
* @private
* @param {string} prop the changed prop name
*/
__handleDataChange(prop) {
let computedInfo = this.$rawComputed;
if (!computedInfo) {
return;
}
let computedGetter = computedInfo[prop];
if (computedGetter) {
// recollect computed prop deps
collectComputedPropDeps(this, prop, computedGetter);
}
// up changed computed props
let changeProps = findChangeComputedProps(
this.__computedDeps, prop
);
changeProps.forEach(k => {
let getter = computedInfo[k];
let value = getter.call(this);
this[k] = value;
});
},
/**
* Update computed property value
*
* @param {string} p the computed property name to update
* @param {Function=} shouldUpdate whether should update the computed property
*/
__updateComputed(p, shouldUpdate) {
let old = this[p];
// lazy computed is not supported
let computedGetter = this.$rawComputed[p];
let value = collectComputedPropDeps(this, p, computedGetter);
// maybe the computed value is a reference of the dependence data,
// so if the old === value && typeof old === 'object', it'll also need
// to update view
let neeUpdate = typeof shouldUpdate === 'function'
? shouldUpdate(old, value, p)
: (old !== value || (typeof old === 'object'));
if (neeUpdate) {
this[p] = value;
}
}
}
};