@aimwhy/vue-function-api
Version:
Vue2 plugin for the function-based RFC
506 lines (426 loc) • 11.6 kB
JavaScript
;
var noopFn = _ => _;
var toString = x => Object.prototype.toString.call(x);
var isArray = x => toString(x) === `[object Array]`;
var isPlainObject = x => toString(x) === `[object Object]`;
var hasOwn = (obj, key) =>
!!obj && Object.prototype.hasOwnProperty.call(obj, key);
function assert(condition, msg) {
if (!condition) {
throw new Error(`[vue-function-api] ${msg}`);
}
}
function proxy(target, source, targetKey, sourceKey = targetKey) {
Object.defineProperty(target, targetKey, {
enumerable: true,
configurable: false,
get: function proxyGetter() {
return source[sourceKey];
},
set: function proxySetter(val) {
source[sourceKey] = val;
}
});
}
var currentVue = null;
function getCurrentVue() {
assert(currentVue, `must call Vue.use(plugin) before using any function.`);
return currentVue;
}
function setCurrentVue(vue) {
currentVue = vue;
}
function vueWarn(msg, vm) {
getCurrentVue().util.warn(msg, vm);
}
var currentVM = null;
function getCurrentVM() {
return currentVM;
}
function setCurrentVM(vm) {
currentVM = vm;
}
function ensureCurrentVMInFn(hook) {
var vm = getCurrentVM();
assert(vm, `"${hook}" get called outside of "setup()"`);
return vm;
}
// createComponent
export function createComponent(compOpts) {
return typeof compOpts === `function` ? { setup: compOpts } : compOpts;
}
// For state / value / state
function ValueWrapper(v) {
this.observe = v;
}
Object.defineProperty(ValueWrapper.prototype, `value`, {
enumerable: true,
configurable: true,
get() {
return this.observe.$$value;
},
set(v) {
this.observe.$$value = v;
}
});
function isValueWrapper(obj) {
return obj instanceof ValueWrapper;
}
function unProxy(obj) {
if (obj) {
var keys = Object.keys(obj);
for (var index = 0; index < keys.length; index++) {
var key = keys[index];
var value = obj[key];
if (isValueWrapper(value)) {
proxy(obj, value.observe, key, `$$value`);
} else if (
(isPlainObject(value) || isArray(value)) &&
!hasOwn(value, `__ob__`)
) {
obj[key] = unProxy(value);
}
}
}
return obj;
}
function observable(obj) {
var Vue = getCurrentVue();
if (Vue.observable) {
return Vue.observable(obj);
} else {
var silent = Vue.config.silent;
Vue.config.silent = true;
var vm = new Vue({ data: { $$state: obj } });
Vue.config.silent = silent;
return vm._data.$$state;
}
}
// state
export function state(value) {
return observable(
isArray(value) || isPlainObject(value) ? unProxy(value) : value
);
}
// value
export function value(value) {
return new ValueWrapper(
observable({
$$value: isArray(value) || isPlainObject(value) ? unProxy(value) : value
})
);
}
// computed
function compoundComputed(computed) {
var Vue = getCurrentVue();
var silent = Vue.config.silent;
Vue.config.silent = true;
var reactive = new Vue({ computed: computed });
Vue.config.silent = silent;
return reactive;
}
export function computed(getter, setter) {
var computedHost = compoundComputed({
$$value: { get: getter, set: setter }
});
return new ValueWrapper(computedHost);
}
// lifeCycle
var genName = function(name) {
return `on${name[0].toUpperCase()}${name.slice(1)}`;
};
function createLifeCycle(lifeCycleHook) {
return function(callback) {
var vm = ensureCurrentVMInFn(genName(lifeCycleHook));
vm.$on(`hook:${lifeCycleHook}`, callback);
};
}
function createLifeCycles(lifeCycleHooks, name) {
return function(callback) {
var vm = ensureCurrentVMInFn(genName(name));
lifeCycleHooks.forEach(function(lifeCycleHook) {
return vm.$on(`hook:${lifeCycleHook}`, callback);
});
};
}
export const onCreated = createLifeCycle(`created`);
export const onBeforeMount = createLifeCycle(`beforeMount`);
export const onMounted = createLifeCycle(`mounted`);
export const onBeforeUpdate = createLifeCycle(`beforeUpdate`);
export const onUpdated = createLifeCycle(`updated`);
export const onActivated = createLifeCycle(`activated`);
export const onDeactivated = createLifeCycle(`deactivated`);
export const onBeforeDestroy = createLifeCycle(`beforeDestroy`);
export const onDestroyed = createLifeCycle(`destroyed`);
export const onErrorCaptured = createLifeCycle(`errorCaptured`);
export const onUnmounted = createLifeCycles(
[`destroyed`, `deactivated`],
`unmounted`
);
// watch
var WatcherPreFlushQueueKey = `vfa.key.preFlushQueue`;
var WatcherPostFlushQueueKey = `vfa.key.postFlushQueue`;
var fallbackVM;
function installWatchEnv(vm) {
if (!vm[WatcherPreFlushQueueKey]) {
vm[WatcherPreFlushQueueKey] = [];
vm[WatcherPostFlushQueueKey] = [];
vm.$on(`hook:beforeUpdate`, createFlusher(WatcherPreFlushQueueKey));
vm.$on(`hook:updated`, createFlusher(WatcherPostFlushQueueKey));
}
}
function createFlusher(key) {
return function flushQueueWrap() {
flushQueue(this, key);
};
}
function flushQueue(vm, key) {
var queue = vm[key];
for (var index = 0; index < queue.length; index++) {
queue[index]();
}
queue.length = 0;
}
function fallbackFlush(vm) {
vm.$nextTick(function() {
if (vm[WatcherPreFlushQueueKey].length) {
flushQueue(vm, WatcherPreFlushQueueKey);
}
if (vm[WatcherPostFlushQueueKey].length) {
flushQueue(vm, WatcherPostFlushQueueKey);
}
});
}
function flushWatcherCallback(vm, fn, mode) {
switch (mode) {
case `pre`:
fallbackFlush(vm);
return vm[WatcherPreFlushQueueKey].push(fn);
case `post`:
fallbackFlush(vm);
return vm[WatcherPostFlushQueueKey].push(fn);
case `sync`:
return fn();
default:
return assert(
false,
`flush must be one of ["post", "pre", "sync"], but got ${mode}`
);
}
}
function createSingleWatcher(vm, source, cb, options) {
var getter = isValueWrapper(source) ? _ => source.observe.$$value : source;
let cleanUp = noopFn;
let cbWrap = function(n, o) {
cleanUp();
cb(n, o, function(v) {
cleanUp = v;
});
};
var callbackRef = function(n, o) {
callbackRef = flush;
return !options.lazy ? cbWrap(n, o) : flush(n, o);
};
var flush = function(n, o) {
flushWatcherCallback(vm, _ => cbWrap(n, o), options.flush);
};
var unwatch = vm.$watch(getter, callbackRef, {
immediate: !options.lazy,
deep: options.deep,
sync: options.flush === `sync`
});
return function stop() {
cleanUp();
unwatch();
};
}
function createMultiWatcher(vm, sources, cb, options) {
var pre = Array(sources.length);
var cur = Array(sources.length);
let cleanUp = noopFn;
let cbWrap = function(n, o) {
cleanUp();
cb(n, o, function(v) {
cleanUp = v;
});
};
var unwatchArr = sources.map(function(source, i) {
return (function(_source, _i) {
return createSingleWatcher(
vm,
_source,
function(n, v) {
if (cur[_i] !== n) {
pre[_i] = v;
cur[_i] = n;
cbWrap(cur, pre);
}
},
options
);
})(source, i);
});
return function stop() {
cleanUp();
unwatchArr.forEach(v => v());
};
}
export function watch(source, cb, options = {}) {
var opts = Object.assign(
{ lazy: false, deep: false, flush: `post` },
options
);
var vm = getCurrentVM();
if (!vm) {
if (!fallbackVM) {
var Vue_1 = getCurrentVue();
var silent = Vue_1.config.silent;
Vue_1.config.silent = true;
fallbackVM = new Vue_1();
Vue_1.config.silent = silent;
}
vm = fallbackVM;
opts.flush = `sync`;
}
installWatchEnv(vm);
return (isArray(source) ? createMultiWatcher : createSingleWatcher)(
vm,
source,
cb,
opts
);
}
// provide
export function provide(provideOpts) {
if (provideOpts) {
var vm = ensureCurrentVMInFn(`provide`);
vm._provided =
typeof provideOpts === `function` ? provideOpts.call(vm) : provideOpts;
}
}
// inject
export function inject(injectKey) {
if (injectKey) {
var vm = ensureCurrentVMInFn(`inject`);
var source = vm;
while (source) {
if (source._provided && hasOwn(source._provided, injectKey)) {
return source._provided[injectKey];
}
source = source.$parent;
}
vueWarn(`Injection "${injectKey}" not found`, vm);
}
}
// plugin
function _install(Vue, mixin) {
if (currentVue && currentVue === Vue) {
return assert(
false,
`already installed. Vue.use(plugin) should be called only once`
);
}
Vue.config.optionMergeStrategies.setup =
Vue.config.optionMergeStrategies.data;
setCurrentVue(Vue);
mixin(Vue);
}
function checkData(vm, propName) {
var props = vm.$options.props;
var methods = vm.$options.methods;
var computed = vm.$options.computed;
var msgPrefix = `The setup binding property "${propName}" is already declared`;
var msgSuffix = `.`;
if (hasOwn(vm.$data, propName)) {
msgSuffix = `as a data.`;
} else if (props && hasOwn(props, propName)) {
msgSuffix = `as a prop.`;
} else if (methods && hasOwn(methods, propName)) {
msgSuffix = `as a method.`;
} else if (computed && hasOwn(computed, propName)) {
msgSuffix = `as a computed.`;
}
if (msgSuffix !== `.`) {
vueWarn(msgPrefix + msgSuffix, vm);
}
}
function mixin(Vue) {
Vue.mixin({ created: setupMix });
function setupMix() {
var vm = this;
var setup = vm.$options.setup;
if (!setup) {
return;
}
if (typeof setup !== `function`) {
return vueWarn(`The "setup" should be a function`, vm);
}
var binding;
var ctx = createContext(vm);
setCurrentVM(vm);
try {
binding = setup(vm.$props || {}, ctx);
} catch (err) {
vueWarn(`there is an error occuring in "setup"`, vm);
console.log(err);
} finally {
setCurrentVM(null);
}
if (!binding) {
return;
}
if (typeof binding === `function`) {
return (vm.$options.render = function(h) {
return binding(ctx.props, ctx.slots, ctx.attrs);
});
}
if (!isPlainObject(binding)) {
return assert(
false,
`"setup" must return a "Object", get "${toString(binding)}"`
);
}
Object.keys(binding).forEach(name => checkData(vm, name));
vm._data2 = observable(unProxy(binding));
Object.keys(binding).forEach(key => proxy(vm, vm._data2, key));
}
function createContext(vm) {
var ctx = { vm };
var props = [`props`, `parent`, `root`, `refs`, `slots`, `attrs`];
var methodWithoutReturn = [`emit`];
props.forEach(function(key) {
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: function() {
return vm[`$${key}`];
},
set: function() {
vueWarn(`Cannot assign for read-only property "${key}"`, vm);
}
});
});
methodWithoutReturn.forEach(function(key) {
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: function() {
return function() {
var args = [];
for (var _i = 0; _i < arguments.length; _i++) {
args[_i] = arguments[_i];
}
vm[`$${key}`].apply(vm, args);
};
},
set: noopFn
});
});
return ctx;
}
}
export const plugin = {
install: function install(Vue) {
return _install(Vue, mixin);
}
};