From 5c04dcddca3b63fa86305d37a6ffa286516ab210 Mon Sep 17 00:00:00 2001 From: liximomo Date: Tue, 9 Jul 2019 14:08:30 +0800 Subject: [PATCH 001/551] test: createComponent --- package.json | 2 +- test/createComponent.spec.ts | 26 ++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 test/createComponent.spec.ts diff --git a/package.json b/package.json index 2b7d8ad7..6c6ca221 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "^.+\\.[jt]s$": "/test/tsTransform.js" }, "testMatch": [ - "/test/**/*.spec.js" + "/test/**/*.spec.{js,ts}" ] }, "prettier": { diff --git a/test/createComponent.spec.ts b/test/createComponent.spec.ts new file mode 100644 index 00000000..e409216b --- /dev/null +++ b/test/createComponent.spec.ts @@ -0,0 +1,26 @@ +const Vue = require('vue/dist/vue.common.js'); +import { plugin, createComponent, value, PropType } from '../src'; + +Vue.use(plugin); + +it('should work', () => { + const Child = createComponent({ + template: `{{ localMsg }}`, + props: (['msg'] as any) as PropType<{ msg: string }>, + setup(props) { + return { localMsg: props.msg }; + }, + }); + + const App = createComponent({ + template: `
`, + setup() { + return { msg: value('hello') }; + }, + components: { + Child, + }, + }); + const vm = new Vue(App).$mount(); + expect(vm.$el.querySelector('span').textContent).toBe('hello'); +}); From d6a444e3e34b3500fa1b634302dd29746fcfef05 Mon Sep 17 00:00:00 2001 From: TWithers Date: Thu, 1 Aug 2019 20:07:43 -0700 Subject: [PATCH 002/551] doc(README.md): fix grammar --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 648dfcb4..29c1332c 100644 --- a/README.md +++ b/README.md @@ -342,8 +342,8 @@ Full properties list: # Misc - `vue-function-api` will keep updated with `Vue3.x` API. When `3.0` released, you can replace this library seamlessly. -- `vue-function-api` only relies on `Vue2.x` itself. Wheather `Vue3.x` is released or not, it's not affect you using this library. -- Due the the limitation of `Vue2.x`'s public API. `vue-function-api` inevitably introduce some extract workload. It doesn't concern you if you are now working on extreme environment. +- `vue-function-api` only relies on `Vue2.x` itself. Whether you decide to upgrade `Vue3.x` or not, using this library will not impact `Vue2.x` functionality. +- Due the the limitation of `Vue2.x`'s public API. `vue-function-api` inevitably introduces some extra workload. This shouldn't concern you unless are already pushing your environment to the extreme. [wrapper]: https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#why-do-we-need-value-wrappers From 3a788a47a098a52b120ce6c6ff93dff128487263 Mon Sep 17 00:00:00 2001 From: IwYvI Date: Fri, 2 Aug 2019 18:39:00 +0800 Subject: [PATCH 003/551] chore: replace custom transform with ts-jest custom transform will give the wrong line number when debugging --- package.json | 7 +++--- test/tsTransform.js | 11 --------- yarn.lock | 56 +++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 54 insertions(+), 20 deletions(-) delete mode 100644 test/tsTransform.js diff --git a/package.json b/package.json index 2b7d8ad7..4eda799f 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "rollup-plugin-replace": "^2.2.0", "rollup-plugin-terser": "^4.0.4", "rollup-plugin-typescript2": "^0.21.0", + "ts-jest": "^24.0.2", "typescript": "^3.4.5", "vue": "^2.5.22" }, @@ -73,12 +74,10 @@ "ts", "js" ], - "transform": { - "^.+\\.[jt]s$": "/test/tsTransform.js" - }, "testMatch": [ "/test/**/*.spec.js" - ] + ], + "preset": "ts-jest" }, "prettier": { "printWidth": 100, diff --git a/test/tsTransform.js b/test/tsTransform.js deleted file mode 100644 index 603d94fb..00000000 --- a/test/tsTransform.js +++ /dev/null @@ -1,11 +0,0 @@ -const tsc = require('typescript'); -const tsConfig = require('../tsconfig.json'); - -module.exports = { - process(src, path) { - if (path.endsWith('.ts')) { - return tsc.transpile(src, { ...tsConfig.compilerOptions, module: 'commonjs' }, path, []); - } - return src; - }, -}; diff --git a/yarn.lock b/yarn.lock index ae21da06..e2c45073 100644 --- a/yarn.lock +++ b/yarn.lock @@ -691,6 +691,13 @@ browser-resolve@^1.11.3: dependencies: resolve "1.1.7" +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + bser@^2.0.0: version "2.0.0" resolved "https://registry.npm.taobao.org/bser/download/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" @@ -698,7 +705,7 @@ bser@^2.0.0: dependencies: node-int64 "^0.4.0" -buffer-from@^1.0.0: +buffer-from@1.x, buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.npm.taobao.org/buffer-from/download/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" integrity sha1-MnE7wCj3XAL9txDXx7zsHyxgcO8= @@ -747,6 +754,11 @@ callsites@^3.0.0: resolved "https://registry.npm.taobao.org/callsites/download/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha1-s2MKvYlDQy9Us/BRkjjjPNffL3M= +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= + camelcase@^5.0.0: version "5.3.1" resolved "https://registry.npm.taobao.org/camelcase/download/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" @@ -1270,7 +1282,7 @@ fast-deep-equal@^2.0.1: resolved "https://registry.npm.taobao.org/fast-deep-equal/download/fast-deep-equal-2.0.1.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ffast-deep-equal%2Fdownload%2Ffast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= -fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: version "2.0.0" resolved "https://registry.npm.taobao.org/fast-json-stable-stringify/download/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= @@ -2390,7 +2402,7 @@ json-stringify-safe@~5.0.1: resolved "https://registry.npm.taobao.org/json-stringify-safe/download/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json5@^2.1.0: +json5@2.x, json5@^2.1.0: version "2.1.0" resolved "https://registry.npm.taobao.org/json5/download/json5-2.1.0.tgz#e7a0c62c48285c628d20a10b85c89bb807c32850" integrity sha1-56DGLEgoXGKNIKELhcibuAfDKFA= @@ -2622,6 +2634,11 @@ make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" +make-error@1.x: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== + makeerror@1.0.x: version "1.0.11" resolved "https://registry.npm.taobao.org/makeerror/download/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" @@ -2757,7 +2774,7 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@0.x, mkdirp@^0.5.0, mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.npm.taobao.org/mkdirp/download/mkdirp-0.5.1.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmkdirp%2Fdownload%2Fmkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= @@ -3475,6 +3492,13 @@ resolve@1.10.1: dependencies: path-parse "^1.0.6" +resolve@1.x: + version "1.12.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" + integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== + dependencies: + path-parse "^1.0.6" + resolve@^1.10.0, resolve@^1.11.0, resolve@^1.3.2: version "1.11.1" resolved "https://registry.npm.taobao.org/resolve/download/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e" @@ -3624,7 +3648,7 @@ semver-compare@^1.0.0: resolved "https://registry.npm.taobao.org/semver-compare/download/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= -"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.6.0: version "5.7.0" resolved "https://registry.npm.taobao.org/semver/download/semver-5.7.0.tgz?cache=0&sync_timestamp=1559063729249&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fsemver%2Fdownload%2Fsemver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" integrity sha1-eQp89v6lRZuslhELKbYEEtyP+Ws= @@ -4079,6 +4103,21 @@ trim-right@^1.0.1: resolved "https://registry.npm.taobao.org/trim-right/download/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= +ts-jest@^24.0.2: + version "24.0.2" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.0.2.tgz#8dde6cece97c31c03e80e474c749753ffd27194d" + integrity sha512-h6ZCZiA1EQgjczxq+uGLXQlNgeg02WWJBbeT8j6nyIBRQdglqbvzDoHahTEIiS6Eor6x8mK6PfZ7brQ9Q6tzHw== + dependencies: + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + json5 "2.x" + make-error "1.x" + mkdirp "0.x" + resolve "1.x" + semver "^5.5" + yargs-parser "10.x" + tslib@1.9.3, tslib@^1.9.3: version "1.9.3" resolved "https://registry.npm.taobao.org/tslib/download/tslib-1.9.3.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Ftslib%2Fdownload%2Ftslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" @@ -4336,6 +4375,13 @@ yallist@^3.0.0, yallist@^3.0.3: resolved "https://registry.npm.taobao.org/yallist/download/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" integrity sha1-tLBJ4xS+VF486AIjbWzSLNkcPek= +yargs-parser@10.x: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + yargs-parser@^11.1.1: version "11.1.1" resolved "https://registry.npm.taobao.org/yargs-parser/download/yargs-parser-11.1.1.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fyargs-parser%2Fdownload%2Fyargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" From cd88dd50703a017b7da8e0d69d7b2f16aa4c09fc Mon Sep 17 00:00:00 2001 From: liximomo Date: Sun, 4 Aug 2019 16:50:39 +0800 Subject: [PATCH 004/551] fix(reactivity): don't set flag on non-extensible objects --- src/reactivity/observable.ts | 30 ++++++++++++++++++++---------- test/functions/watch.spec.js | 35 ++++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/reactivity/observable.ts b/src/reactivity/observable.ts index e5a9f553..5e6021a1 100644 --- a/src/reactivity/observable.ts +++ b/src/reactivity/observable.ts @@ -2,11 +2,14 @@ import { AnyObject } from '../types/basic'; import { getCurrentVue } from '../runtimeContext'; import { isObject, def, hasOwn } from '../utils'; import { isWrapper } from '../wrappers'; -import { ObservableIdentifierKey, AccessControIdentifierlKey } from '../symbols'; +import { AccessControIdentifierlKey, ObservableIdentifierKey } from '../symbols'; const AccessControlIdentifier = {}; const ObservableIdentifier = {}; - +/** + * Proxing property access of target. + * We can do unwrapping and other things here. + */ function setupAccessControl(target: AnyObject) { if (!isObject(target) || isWrapper(target)) { return; @@ -19,13 +22,21 @@ function setupAccessControl(target: AnyObject) { return; } - def(target, AccessControIdentifierlKey, AccessControlIdentifier); + if (Object.isExtensible(target)) { + def(target, AccessControIdentifierlKey, AccessControlIdentifier); + } const keys = Object.keys(target); for (let i = 0; i < keys.length; i++) { defineAccessControl(target, keys[i]); } } +function isObservable(obj: any): boolean { + return ( + hasOwn(obj, ObservableIdentifierKey) && obj[ObservableIdentifierKey] === ObservableIdentifier + ); +} + /** * Auto unwrapping when acccess property */ @@ -74,12 +85,9 @@ export function defineAccessControl(target: AnyObject, key: any, val?: any) { }); } -export function isObservable(obj: any): boolean { - return ( - hasOwn(obj, ObservableIdentifierKey) && obj[ObservableIdentifierKey] === ObservableIdentifier - ); -} - +/** + * Make obj reactivity + */ export function observable(obj: T): T { if (!isObject(obj) || isObservable(obj)) { return obj; @@ -101,7 +109,9 @@ export function observable(obj: T): T { observed = vm._data.$$state; } - def(observed, ObservableIdentifierKey, ObservableIdentifier); + if (Object.isExtensible(observed)) { + def(observed, ObservableIdentifierKey, ObservableIdentifier); + } setupAccessControl(observed); return observed; } diff --git a/test/functions/watch.spec.js b/test/functions/watch.spec.js index 48a98e29..23e10b54 100644 --- a/test/functions/watch.spec.js +++ b/test/functions/watch.spec.js @@ -183,7 +183,7 @@ describe('Hooks watch', () => { obj.a = 2; waitForUpdate(() => { expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenCalledWith(2, 1); + expect(spy).toHaveBeenLastCalledWith(2, 1); }).then(done); }); @@ -220,8 +220,8 @@ describe('Hooks watch', () => { }).$mount(); a.value = 2; waitForUpdate(() => { - expect(spy1).toHaveBeenCalledWith(2, 1); - expect(spy).toHaveBeenCalledWith(2, 1); + expect(spy1).toHaveBeenLastCalledWith(2, 1); + expect(spy).toHaveBeenLastCalledWith(2, 1); }).then(done); }); @@ -240,7 +240,7 @@ describe('Hooks watch', () => { expect(spy).not.toHaveBeenCalled(); vm.a = 2; waitForUpdate(() => { - expect(spy).toHaveBeenCalledWith(2, 1); + expect(spy).toHaveBeenLastCalledWith(2, 1); }).then(done); }); @@ -261,11 +261,11 @@ describe('Hooks watch', () => { vm.a.b = 2; expect(spy).not.toHaveBeenCalled(); waitForUpdate(() => { - expect(spy).toHaveBeenCalledWith(vm.a, vm.a); + expect(spy).toHaveBeenLastCalledWith(vm.a, vm.a); vm.a = { b: 3 }; }) .then(() => { - expect(spy).toHaveBeenCalledWith(vm.a, oldA); + expect(spy).toHaveBeenLastCalledWith(vm.a, oldA); }) .then(done); }); @@ -293,7 +293,7 @@ describe('Hooks watch', () => { vm.a = 2; waitForUpdate(() => { expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenCalledWith(2, 1); + expect(spy).toHaveBeenLastCalledWith(2, 1); }).then(done); }); @@ -320,7 +320,7 @@ describe('Hooks watch', () => { vm.a = 2; waitForUpdate(() => { expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenCalledWith(2, 1); + expect(spy).toHaveBeenLastCalledWith(2, 1); }).then(done); }); @@ -339,9 +339,9 @@ describe('Hooks watch', () => { }).$mount(); expect(spy).not.toHaveBeenCalled(); vm.a = 2; - expect(spy).toHaveBeenCalledWith(2, 1); + expect(spy).toHaveBeenLastCalledWith(2, 1); vm.a = 3; - expect(spy).toHaveBeenCalledWith(3, 2); + expect(spy).toHaveBeenLastCalledWith(3, 2); waitForUpdate(() => { expect(spy.mock.calls.length).toBe(2); }).then(done); @@ -365,7 +365,20 @@ describe('Hooks watch', () => { vm['数据'] = 2; expect(spy).not.toHaveBeenCalled(); waitForUpdate(() => { - expect(spy).toHaveBeenCalledWith(2, 1); + expect(spy).toHaveBeenLastCalledWith(2, 1); }).then(done); }); + + it('should allow to be triggered in setup', () => { + new Vue({ + setup() { + const count = value(0); + watch(count, spy, { flush: 'sync' }); + count.value++; + }, + }); + expect(spy.mock.calls.length).toBe(2); + expect(spy).toHaveBeenNthCalledWith(1, 0, undefined); + expect(spy).toHaveBeenNthCalledWith(2, 1, 0); + }); }); From f0d99759283307ec001388e7f11c7b73fb8be3ca Mon Sep 17 00:00:00 2001 From: liximomo Date: Sun, 4 Aug 2019 17:24:46 +0800 Subject: [PATCH 005/551] refactor: call setup in the wrapper function of "data" option --- src/functions/inject.ts | 7 +-- src/setup.ts | 102 ++++---------------------------- src/symbols.ts | 2 - src/utils.ts | 17 ++++++ src/wrappers/AbstractWrapper.ts | 4 +- src/wrappers/ComputedWrapper.ts | 5 +- test/setup.spec.js | 46 ++++---------- tsconfig.json | 2 +- 8 files changed, 50 insertions(+), 135 deletions(-) diff --git a/src/functions/inject.ts b/src/functions/inject.ts index 2b26e2b7..d6850e9c 100644 --- a/src/functions/inject.ts +++ b/src/functions/inject.ts @@ -1,9 +1,8 @@ import Vue from 'vue'; -import { getCurrentVue } from '../runtimeContext'; import { state } from '../functions/state'; import { isWrapper, Wrapper, ComputedWrapper } from '../wrappers'; import { ensureCurrentVMInFn } from '../helper'; -import { hasOwn } from '../utils'; +import { hasOwn, warn } from '../utils'; const UNRESOLVED_INJECT = {}; export interface Key extends Symbol {} @@ -45,10 +44,10 @@ export function inject(key: Key): Wrapper | void { return new ComputedWrapper({ read: () => reactiveVal, write() { - getCurrentVue().util.warn(`The injectd value can't be re-assigned`, vm); + warn(`The injectd value can't be re-assigned`, vm); }, }); } else if (process.env.NODE_ENV !== 'production') { - getCurrentVue().util.warn(`Injection "${String(key)}" not found`, vm); + warn(`Injection "${String(key)}" not found`, vm); } } diff --git a/src/setup.ts b/src/setup.ts index a3b5e1c7..67fa37a3 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -1,89 +1,11 @@ import VueInstance, { VueConstructor } from 'vue'; import { SetupContext } from './types/vue'; import { isWrapper } from './wrappers'; -import { SetupHookEvent } from './symbols'; import { setCurrentVM } from './runtimeContext'; -import { isPlainObject, assert, proxy, noopFn } from './utils'; +import { isPlainObject, assert, proxy, warn, logError } from './utils'; import { value } from './functions/state'; -import { watch } from './functions/watch'; - -let disableSetup = false; - -// `cb` should be called right after props get resolved -function waitPropsResolved(vm: VueInstance, cb: (v: VueInstance, props: Record) => void) { - const safeRunCb = (props: Record) => { - // Running `cb` under the scope of a dep.Target, otherwise the `Observable` - // in `cb` will be unexpectedly colleced by the current dep.Target. - const dispose = watch( - () => { - cb(vm, props); - }, - noopFn, - { lazy: false, deep: false, flush: 'sync' } - ); - dispose(); - }; - - const opts = vm.$options; - let methods = opts.methods; - - if (!methods) { - opts.methods = { [SetupHookEvent]: noopFn }; - // This will get invoked when assigning to `SetupHookEvent` property of vm. - vm.$once(SetupHookEvent, () => { - // restore `opts` object - delete opts.methods; - safeRunCb(vm.$props); - }); - return; - } - - // Find the first method will re resovled. - // The order will be stable, since we never modify the `methods` object. - let firstMedthodName: string | undefined; - for (const key in methods) { - firstMedthodName = key; - break; - } - - // `methods` is an empty object - if (!firstMedthodName) { - methods[SetupHookEvent] = noopFn; - vm.$once(SetupHookEvent, () => { - // restore `methods` object - delete methods![SetupHookEvent]; - safeRunCb(vm.$props); - }); - return; - } - - proxy(vm, firstMedthodName, { - set(val: any) { - safeRunCb(vm.$props); - - // restore `firstMedthodName` to a noraml property - Object.defineProperty(vm, firstMedthodName!, { - configurable: true, - enumerable: true, - writable: true, - value: val, - }); - }, - }); -} export function mixin(Vue: VueConstructor) { - // We define the setup hook on prototype, - // which avoids Object.defineProperty calls for each instance created. - proxy(Vue.prototype, SetupHookEvent, { - get() { - return 'hook'; - }, - set(this: VueInstance) { - this.$emit(SetupHookEvent); - }, - }); - Vue.mixin({ beforeCreate: functionApiInit, }); @@ -93,15 +15,15 @@ export function mixin(Vue: VueConstructor) { */ function functionApiInit(this: VueInstance) { const vm = this; - const { setup } = vm.$options; + const $options = vm.$options; + const { setup } = $options; - if (disableSetup || !setup) { + if (!setup) { return; } - if (typeof setup !== 'function') { if (process.env.NODE_ENV !== 'production') { - Vue.util.warn( + warn( 'The "setup" option should be a function that returns a object in component definitions.', vm ); @@ -109,7 +31,12 @@ export function mixin(Vue: VueConstructor) { return; } - waitPropsResolved(vm, initSetup); + const { data } = $options; + // wapper the data option, so we can invoke setup before data get resolved + $options.data = function wrappedData() { + initSetup(vm, vm.$props); + return typeof data === 'function' ? data.call(vm, vm) : data || {}; + }; } function initSetup(vm: VueInstance, props: Record = {}) { @@ -120,10 +47,7 @@ export function mixin(Vue: VueConstructor) { try { binding = setup(props, ctx); } catch (err) { - if (process.env.NODE_ENV !== 'production') { - Vue.util.warn(`there is an error occuring in "setup"`, vm); - } - throw err; + logError(err, vm, 'setup()'); } finally { setCurrentVM(null); } @@ -160,7 +84,7 @@ export function mixin(Vue: VueConstructor) { proxy(ctx, key, { get: () => vm[`$${key}`], set() { - Vue.util.warn(`Cannot assign to '${key}' because it is a read-only property`, vm); + warn(`Cannot assign to '${key}' because it is a read-only property`, vm); }, }); }); diff --git a/src/symbols.ts b/src/symbols.ts index 6792f3b3..7b4aebd0 100644 --- a/src/symbols.ts +++ b/src/symbols.ts @@ -8,5 +8,3 @@ export const WatcherPreFlushQueueKey = createSymbol('vfa.key.preFlushQueue'); export const WatcherPostFlushQueueKey = createSymbol('vfa.key.postFlushQueue'); export const AccessControIdentifierlKey = createSymbol('vfa.key.accessControIdentifier'); export const ObservableIdentifierKey = createSymbol('vfa.key.observableIdentifier'); -// event name should be a string -export const SetupHookEvent = 'vfa.key.setupHookEvent'; diff --git a/src/utils.ts b/src/utils.ts index fa76af32..2f425ba9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import Vue from 'vue'; + const toString = (x: any) => Object.prototype.toString.call(x); export const hasSymbol = typeof Symbol === 'function' && Symbol.for; @@ -50,3 +52,18 @@ export function isPlainObject(x: unknown): x is T { export function isFunction(x: unknown): x is Function { return typeof x === 'function'; } + +export function warn(msg: string, vm?: Vue) { + Vue.util.warn(msg, vm); +} + +export function logError(err: Error, vm: Vue, info: string) { + if (process.env.NODE_ENV !== 'production') { + warn(`Error in ${info}: "${err.toString()}"`, vm); + } + if (typeof window !== 'undefined' && typeof console !== 'undefined') { + console.error(err); + } else { + throw err; + } +} diff --git a/src/wrappers/AbstractWrapper.ts b/src/wrappers/AbstractWrapper.ts index 525fdd6a..87b00ab2 100644 --- a/src/wrappers/AbstractWrapper.ts +++ b/src/wrappers/AbstractWrapper.ts @@ -1,6 +1,5 @@ import Vue from 'vue'; -import { getCurrentVue } from '../runtimeContext'; -import { proxy, hasOwn, def } from '../utils'; +import { proxy, hasOwn, def, warn } from '../utils'; export default abstract class AbstractWrapper { protected _propName?: string; @@ -12,7 +11,6 @@ export default abstract class AbstractWrapper { def(this, '_propName', propName); const props = vm.$options.props; - const warn = getCurrentVue().util.warn; if (!(propName in vm) && !(props && hasOwn(props, propName))) { proxy(vm, propName, { get: () => this.value, diff --git a/src/wrappers/ComputedWrapper.ts b/src/wrappers/ComputedWrapper.ts index 06689b21..340fd00e 100644 --- a/src/wrappers/ComputedWrapper.ts +++ b/src/wrappers/ComputedWrapper.ts @@ -1,5 +1,4 @@ -import { getCurrentVue } from '../runtimeContext'; -import { proxy, def } from '../utils'; +import { proxy, def, warn } from '../utils'; import AbstractWrapper from './AbstractWrapper'; interface ComputedInternal { @@ -22,7 +21,7 @@ export default class ComputedWrapper extends AbstractWrapper { set value(val: V) { if (!this._internal.write) { if (process.env.NODE_ENV !== 'production') { - getCurrentVue().util.warn( + warn( 'Computed property' + (this._propName ? ` "${this._propName}"` : '') + ' was assigned to but it has no setter.', diff --git a/test/setup.spec.js b/test/setup.spec.js index 81fb9578..44b76e35 100644 --- a/test/setup.spec.js +++ b/test/setup.spec.js @@ -11,66 +11,46 @@ describe('setup', () => { warn.mockRestore(); }); - it('should be called before `methods` gets resolved(no methods option)', () => { + it('should works', () => { const vm = new Vue({ setup() { return { a: value(1), }; }, - data() { - return { - b: this.a, - }; - }, }).$mount(); expect(vm.a).toBe(1); - expect(vm.b).toBe(1); }); - it('should be called before `methods` gets resolved(empty methods option)', () => { + it('should be overrided by data option of plain object', () => { const vm = new Vue({ setup() { return { a: value(1), }; }, - data() { - return { - b: this.a, - }; + data: { + a: 2, }, - methods: {}, }).$mount(); - expect(vm.a).toBe(1); - expect(vm.b).toBe(1); + expect(vm.a).toBe(2); }); - it('should be called before `methods` gets resolved(multiple methods)', () => { + it("should access setup's value in data", () => { const vm = new Vue({ setup() { return { - a: value(0), + a: value(1), }; }, - created() { - this.m1(); - this.m2(); - this.m3(); - }, - methods: { - m1() { - this.a++; - }, - m2() { - this.a++; - }, - m3() { - this.a++; - }, + data() { + return { + b: this.a, + }; }, }).$mount(); - expect(vm.a).toBe(3); + expect(vm.a).toBe(1); + expect(vm.b).toBe(1); }); it('should work with `methods` and `data` options', done => { diff --git a/tsconfig.json b/tsconfig.json index 1bc69413..2807b4d5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "module": "esnext", "moduleResolution": "node", "skipLibCheck": true, - "allowSyntheticDefaultImports": true, + "esModuleInterop": true, "strict": true, "strictNullChecks": true, "strictFunctionTypes": true, From f7cd71bc9825bac3699f765df8fc53af1d3c1d13 Mon Sep 17 00:00:00 2001 From: liximomo Date: Mon, 5 Aug 2019 10:11:15 +0800 Subject: [PATCH 006/551] fix: keep "currentThis" in nested setup call Resolves: #38 --- src/runtimeContext.ts | 2 +- src/setup.ts | 5 +++-- test/setup.spec.js | 20 +++++++++++++++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/runtimeContext.ts b/src/runtimeContext.ts index 744b5e51..2e0cc208 100644 --- a/src/runtimeContext.ts +++ b/src/runtimeContext.ts @@ -16,7 +16,7 @@ export function setCurrentVue(vue: VueConstructor) { currentVue = vue; } -export function getCurrentVM() { +export function getCurrentVM(): Vue | null { return currentVM; } diff --git a/src/setup.ts b/src/setup.ts index 67fa37a3..17d22ee8 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -1,7 +1,7 @@ import VueInstance, { VueConstructor } from 'vue'; import { SetupContext } from './types/vue'; import { isWrapper } from './wrappers'; -import { setCurrentVM } from './runtimeContext'; +import { getCurrentVM, setCurrentVM } from './runtimeContext'; import { isPlainObject, assert, proxy, warn, logError } from './utils'; import { value } from './functions/state'; @@ -43,13 +43,14 @@ export function mixin(Vue: VueConstructor) { const setup = vm.$options.setup!; const ctx = createSetupContext(vm); let binding: any; + let preVm = getCurrentVM(); setCurrentVM(vm); try { binding = setup(props, ctx); } catch (err) { logError(err, vm, 'setup()'); } finally { - setCurrentVM(null); + setCurrentVM(preVm); } if (!binding) return; diff --git a/test/setup.spec.js b/test/setup.spec.js index 44b76e35..9e325ab9 100644 --- a/test/setup.spec.js +++ b/test/setup.spec.js @@ -1,5 +1,5 @@ const Vue = require('vue/dist/vue.common.js'); -const { plugin, value, computed } = require('../src'); +const { plugin, value, computed, onCreated } = require('../src'); Vue.use(plugin); @@ -309,4 +309,22 @@ describe('setup', () => { expect(vm.$el.textContent).toBe('2, 3'); }).then(done); }); + + it('current vue should exist in nested setup call', () => { + const spy = jest.fn(); + new Vue({ + setup() { + new Vue({ + setup() { + onCreated(() => spy(1)); + }, + }); + + onCreated(() => spy(2)); + }, + }); + expect(spy.mock.calls.length).toBe(2); + expect(spy).toHaveBeenNthCalledWith(1, 1); + expect(spy).toHaveBeenNthCalledWith(2, 2); + }); }); From da7a0411911d02510832fda509c3e2e162080931 Mon Sep 17 00:00:00 2001 From: liximomo Date: Mon, 5 Aug 2019 14:25:19 +0800 Subject: [PATCH 007/551] feat: mirror context.slots to scopeSlots Resolves: #26 --- src/setup.ts | 32 +++++++++++++++++++++++--------- test/setup.spec.js | 26 ++++++++++++-------------- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/setup.ts b/src/setup.ts index 17d22ee8..ddb9a65c 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -79,27 +79,41 @@ export function mixin(Vue: VueConstructor) { function createSetupContext(vm: VueInstance & { [x: string]: any }): SetupContext { const ctx = {} as SetupContext; - const props = ['parent', 'root', 'refs', 'slots', 'attrs']; + const props: Array = [ + 'root', + 'parent', + 'refs', + ['slots', 'scopedSlots'], + 'attrs', + ]; const methodReturnVoid = ['emit']; props.forEach(key => { - proxy(ctx, key, { - get: () => vm[`$${key}`], + let targetKey: string; + let srcKey: string; + if (Array.isArray(key)) { + [targetKey, srcKey] = key; + } else { + targetKey = srcKey = key; + } + srcKey = `$${srcKey}`; + proxy(ctx, targetKey, { + get: () => vm[srcKey], set() { - warn(`Cannot assign to '${key}' because it is a read-only property`, vm); + warn(`Cannot assign to '${targetKey}' because it is a read-only property`, vm); }, }); }); - methodReturnVoid.forEach(key => + methodReturnVoid.forEach(key => { + const srcKey = `$${key}`; proxy(ctx, key, { get() { - const vmKey = `$${key}`; return (...args: any[]) => { - const fn: Function = vm[vmKey]; + const fn: Function = vm[srcKey]; fn.apply(vm, args); }; }, - }) - ); + }); + }); if (process.env.NODE_ENV === 'test') { (ctx as any)._vm = vm; } diff --git a/test/setup.spec.js b/test/setup.spec.js index 9e325ab9..900697a8 100644 --- a/test/setup.spec.js +++ b/test/setup.spec.js @@ -114,23 +114,21 @@ describe('setup', () => { expect(props.a).toBe(1); }); - it('should reveive context second params', done => { - new Vue({ + it('should reveive context second params', () => { + let context; + const vm = new Vue({ setup(_, ctx) { - expect(ctx).toBeDefined(); - expect('parent' in ctx).toBe(true); - expect(ctx).toEqual( - expect.objectContaining({ - root: expect.any(Object), - refs: expect.any(Object), - slots: expect.any(Object), - attrs: expect.any(Object), - emit: expect.any(Function), - }) - ); - done(); + context = ctx; }, }); + expect(context).toBeDefined(); + expect('parent' in context).toBe(true); + expect(context.root).toBe(vm.$root); + expect(context.parent).toBe(vm.$parent); + expect(context.refs).toBe(vm.$refs); + expect(context.slots).toBe(vm.$scopedSlots); + expect(context.attrs).toBe(vm.$attrs); + expect(typeof context.emit === 'function').toBe(true); }); it('warn for existing props', () => { From 37c648f78c86602313bb5feab24cb827ba0dad41 Mon Sep 17 00:00:00 2001 From: liximomo Date: Mon, 5 Aug 2019 16:16:51 +0800 Subject: [PATCH 008/551] fix: onErrorCaptured not triggered Not all hooks have an corresponding "hook:" event, so we inject hooks to $options. Resolves: #25 --- src/functions/lifecycle.ts | 16 +++++++- src/functions/watch.ts | 8 ++-- src/helper.ts | 4 +- src/types/vue.ts | 4 +- test/functions/lifecycle.spec.js | 69 ++++++++++++++++++++++++++++++++ 5 files changed, 92 insertions(+), 9 deletions(-) diff --git a/src/functions/lifecycle.ts b/src/functions/lifecycle.ts index d0e1b913..c29c2a2a 100644 --- a/src/functions/lifecycle.ts +++ b/src/functions/lifecycle.ts @@ -1,20 +1,32 @@ +import { VueConstructor } from 'vue'; +import { VueInstance } from '../types/vue'; +import { getCurrentVue } from '../runtimeContext'; import { ensureCurrentVMInFn } from '../helper'; const genName = (name: string) => `on${name[0].toUpperCase() + name.slice(1)}`; function createLifeCycle(lifeCyclehook: string) { return (callback: Function) => { const vm = ensureCurrentVMInFn(genName(lifeCyclehook)); - vm.$on(`hook:${lifeCyclehook}`, callback); + injectHookOption(getCurrentVue(), vm, lifeCyclehook, callback); }; } function createLifeCycles(lifeCyclehooks: string[], name: string) { return (callback: Function) => { + const currentVue = getCurrentVue(); const vm = ensureCurrentVMInFn(name); - lifeCyclehooks.forEach(lifeCyclehook => vm.$on(`hook:${lifeCyclehook}`, callback)); + lifeCyclehooks.forEach(lifeCyclehook => + injectHookOption(currentVue, vm, lifeCyclehook, callback) + ); }; } +function injectHookOption(Vue: VueConstructor, vm: VueInstance, hook: string, val: Function) { + const options = vm.$options as any; + const mergeFn = Vue.config.optionMergeStrategies[hook]; + options[hook] = mergeFn(options[hook], val); +} + export const onCreated = createLifeCycle('created'); export const onBeforeMount = createLifeCycle('beforeMount'); export const onMounted = createLifeCycle('mounted'); diff --git a/src/functions/watch.ts b/src/functions/watch.ts index 7ff766a6..be188dbc 100644 --- a/src/functions/watch.ts +++ b/src/functions/watch.ts @@ -1,4 +1,4 @@ -import Vue, { VueConstructor } from 'vue'; +import { VueInstance } from '../types/vue'; import { Wrapper } from '../wrappers'; import { isArray, assert } from '../utils'; import { isWrapper } from '../wrappers'; @@ -22,7 +22,7 @@ interface WatcherContext { watcherStopHandle: Function; } -let fallbackVM: Vue; +let fallbackVM: VueInstance; function hasWatchEnv(vm: any) { return vm[WatcherPreFlushQueueKey] !== undefined; @@ -81,7 +81,7 @@ function flushWatcherCallback(vm: any, fn: Function, mode: FlushMode) { } function createSingleSourceWatcher( - vm: InstanceType, + vm: VueInstance, source: watchedValue, cb: watcherCallBack, options: WatcherOption @@ -128,7 +128,7 @@ function createSingleSourceWatcher( } function createMuiltSourceWatcher( - vm: InstanceType, + vm: VueInstance, sources: Array>, cb: watcherCallBack, options: WatcherOption diff --git a/src/helper.ts b/src/helper.ts index 9753f8dc..8225c5a5 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,8 +1,8 @@ -import { VueConstructor } from 'vue'; +import { VueInstance } from './types/vue'; import { getCurrentVue, getCurrentVM } from './runtimeContext'; import { assert } from './utils'; -export function ensureCurrentVMInFn(hook: string): InstanceType { +export function ensureCurrentVMInFn(hook: string): VueInstance { const vm = getCurrentVM(); if (process.env.NODE_ENV !== 'production') { assert(vm, `"${hook}" get called outside of "setup()"`); diff --git a/src/types/vue.ts b/src/types/vue.ts index e52ad0a6..3c381541 100644 --- a/src/types/vue.ts +++ b/src/types/vue.ts @@ -1,4 +1,6 @@ -import Vue, { VNode } from 'vue/'; +import Vue, { VueConstructor, VNode } from 'vue/'; + +export type VueInstance = InstanceType; export interface SetupContext { readonly parent: Vue; diff --git a/test/functions/lifecycle.spec.js b/test/functions/lifecycle.spec.js index 1ae33e63..78446037 100644 --- a/test/functions/lifecycle.spec.js +++ b/test/functions/lifecycle.spec.js @@ -8,12 +8,41 @@ const { onUpdated, onBeforeDestroy, onDestroyed, + onErrorCaptured, } = require('../../src'); Vue.use(plugin); describe('Hooks lifecycle', () => { describe('created', () => { + it('work with created option', () => { + const spy = jest.fn(); + new Vue({ + created() { + spy('option'); + }, + setup() { + onCreated(() => spy('hook')); + }, + }); + expect(spy.mock.calls.length).toBe(2); + expect(spy).toHaveBeenNthCalledWith(1, 'option'); + expect(spy).toHaveBeenNthCalledWith(2, 'hook'); + }); + + it('can register multiple callbacks', () => { + const spy = jest.fn(); + new Vue({ + setup() { + onCreated(() => spy('first')); + onCreated(() => spy('second')); + }, + }); + expect(spy.mock.calls.length).toBe(2); + expect(spy).toHaveBeenNthCalledWith(1, 'first'); + expect(spy).toHaveBeenNthCalledWith(2, 'second'); + }); + it('should have completed observation', () => { const spy = jest.fn(); new Vue({ @@ -335,4 +364,44 @@ describe('Hooks lifecycle', () => { expect(spy.mock.calls.length).toBe(1); }); }); + + describe('errorCaptured', () => { + let globalSpy; + + beforeEach(() => { + globalSpy = Vue.config.errorHandler = jest.fn(); + }); + + afterEach(() => { + Vue.config.errorHandler = null; + }); + + it('should capture error from child component', () => { + const spy = jest.fn(); + + let child; + let err; + const Child = { + setup(_, { _vm }) { + child = _vm; + onCreated(() => { + err = new Error('child'); + throw err; + }); + }, + render() {}, + }; + + new Vue({ + setup() { + onErrorCaptured(spy); + }, + render: h => h(Child), + }).$mount(); + + expect(spy).toHaveBeenCalledWith(err, child, 'created hook'); + // should propagate by default + expect(globalSpy).toHaveBeenCalledWith(err, child, 'created hook'); + }); + }); }); From e6e7c5e9743639503ac12be4bedec6d5aa651f82 Mon Sep 17 00:00:00 2001 From: liximomo Date: Mon, 5 Aug 2019 17:23:22 +0800 Subject: [PATCH 009/551] feat: "provide" can be called multiple times per component Resolves: #24 --- src/functions/inject.ts | 14 +++++++++++--- src/types/basic.ts | 2 +- test/functions/inject.spec.js | 5 ++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/functions/inject.ts b/src/functions/inject.ts index d6850e9c..ea2c6a7c 100644 --- a/src/functions/inject.ts +++ b/src/functions/inject.ts @@ -1,8 +1,9 @@ import Vue from 'vue'; +import { AnyObject } from '../types/basic'; import { state } from '../functions/state'; import { isWrapper, Wrapper, ComputedWrapper } from '../wrappers'; import { ensureCurrentVMInFn } from '../helper'; -import { hasOwn, warn } from '../utils'; +import { hasOwn, warn, isObject } from '../utils'; const UNRESOLVED_INJECT = {}; export interface Key extends Symbol {} @@ -21,12 +22,19 @@ function resolveInject(provideKey: Key, vm: Vue): any { return UNRESOLVED_INJECT; } -export function provide(key: Key, value: T | Wrapper) { +export function provide(data: AnyObject): void; +export function provide(key: Key, value: T | Wrapper): void; +export function provide(keyOrData: Key | AnyObject, value?: T | Wrapper): void { const vm: any = ensureCurrentVMInFn('provide'); if (!vm._provided) { vm._provided = {}; } - vm._provided[key as any] = value; + + if (isObject(keyOrData)) { + Object.assign(vm._provided, keyOrData); + } else { + vm._provided[keyOrData] = value; + } } export function inject(key: Key): Wrapper | void { diff --git a/src/types/basic.ts b/src/types/basic.ts index 504d6bea..f0f441ca 100644 --- a/src/types/basic.ts +++ b/src/types/basic.ts @@ -1 +1 @@ -export type AnyObject = Record; +export type AnyObject = Record; diff --git a/test/functions/inject.spec.js b/test/functions/inject.spec.js index dc8e0627..79ae3a06 100644 --- a/test/functions/inject.spec.js +++ b/test/functions/inject.spec.js @@ -33,7 +33,10 @@ describe('Hooks provide/inject', () => { new Vue({ template: ``, setup() { - provide('foo', 1); + const count = value(1); + provide({ + foo: count, + }); provide('bar', false); }, components: { From 676ed114bb8c2e28f5346da5515bc799e61e4dce Mon Sep 17 00:00:00 2001 From: liximomo Date: Mon, 5 Aug 2019 17:41:12 +0800 Subject: [PATCH 010/551] chore: add author email --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 646b1a09..f34d4f07 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,10 @@ "umd:main": "dist/vue-function-api.umd.js", "module": "dist/vue-function-api.module.js", "typings": "dist/index.d.ts", - "author": "liximomo", + "author": { + "name": "liximomo", + "email": "liximomo@gmail.com" + }, "license": "MIT", "sideEffects": false, "files": [ From 8250be2a52daa9f6c4eeea2759f36b43da858a44 Mon Sep 17 00:00:00 2001 From: liximomo Date: Mon, 5 Aug 2019 18:42:58 +0800 Subject: [PATCH 011/551] chore: update `provide` signature --- README.md | 10 +++------- README.zh-CN.md | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 29c1332c..ce335709 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ > [Function-based Component API RFC](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md) -Future-Oriented Programming, `vue-function-api` provides function api from `Vue3.x` to `Vue2.x` for developing next-generation Vue applications. +`vue-function-api` provides a way to use **function api** from `Vue3` in `Vue2.x`. [**中文文档**](./README.zh-CN.md) @@ -281,7 +281,7 @@ const MyComponent = { ``` ## provide, inject -▸ **provide**(value: *`Object`*) +▸ **provide**(key: *`string` | `symbol`*, value: *`any`*) ▸ **inject**(key: *`string` | `symbol`*) @@ -298,9 +298,7 @@ const Ancestor = { setup() { // providing a value can make it reactive const count = value(0) - provide({ - [CountSymbol]: count - }) + provide(CountSymbol, count) } } @@ -341,8 +339,6 @@ Full properties list: # Misc -- `vue-function-api` will keep updated with `Vue3.x` API. When `3.0` released, you can replace this library seamlessly. -- `vue-function-api` only relies on `Vue2.x` itself. Whether you decide to upgrade `Vue3.x` or not, using this library will not impact `Vue2.x` functionality. - Due the the limitation of `Vue2.x`'s public API. `vue-function-api` inevitably introduces some extra workload. This shouldn't concern you unless are already pushing your environment to the extreme. diff --git a/README.zh-CN.md b/README.zh-CN.md index f5552f65..3b153a09 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,7 +2,7 @@ > [通过基于函数的 API 来复用组件逻辑](https://zhuanlan.zhihu.com/p/68477600) -面向未来编程(Future-Oriented Programming),`vue-function-api` 提供 Vue3 中的组件逻辑复用机制帮助开发者开发下一代 vue 应用程序,允许开发者利用 Vue3 的响应性 API 建设未来 Vue 生态。 +`vue-function-api` 使开发者们可以在 `Vue2.x` 中使用 `Vue3` 引入的**基于函数**的**逻辑复用机制**。 [**English Version**](./README.md) @@ -284,7 +284,7 @@ const MyComponent = { ``` ## provide, inject -▸ **provide**(value: *`Object`*) +▸ **provide**(key: *`string` | `symbol`*, value: *`any`*) ▸ **inject**(key: *`string` | `symbol`*) @@ -301,9 +301,7 @@ const Ancestor = { setup() { // providing a value can make it reactive const count = value(0) - provide({ - [CountSymbol]: count - }) + provide(CountSymbol, count) } } @@ -405,6 +403,4 @@ console.log(count.value) // 2 # 其他 -- `vue-function-api` 会一直保持与 `Vue3.x` 的兼容性,当 `3.0` 发布时,您可以无缝替换掉本库。 -- `vue-function-api` 的实现只依赖 `Vue2.x` 本身,不论 `Vue3.x` 的发布与否,都不会影响您正常使用本库。 - 由于 `Vue2.x` 的公共 API 限制,`vue-function-api` 无法避免的会产生一些额外的内存负载。如果您的应用并不工作在极端内存环境下,无需关心此项。 From 43262a226af4bed82a5d42d56c885e04bfca1557 Mon Sep 17 00:00:00 2001 From: Jordan Pittman Date: Tue, 6 Aug 2019 07:05:38 -0400 Subject: [PATCH 012/551] test: add test for mixins breaking computed properties --- test/functions/computed.spec.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/functions/computed.spec.js b/test/functions/computed.spec.js index aebc4dab..5c7a3a14 100644 --- a/test/functions/computed.spec.js +++ b/test/functions/computed.spec.js @@ -166,4 +166,31 @@ describe('Hooks computed', () => { }); expect(() => vm.a).toThrowError('rethrow'); }); + + it('Mixins should not break computed properties', () => { + const ExampleComponent = Vue.extend({ + props: ['test'], + render: h => h('div'), + setup: props => ({ example: computed(() => props.test) }), + }); + + Vue.mixin({ + computed: { + foobar() { + return 'test'; + }, + }, + }); + + const app = new Vue({ + render: h => + h('div', [ + h(ExampleComponent, { props: { test: 'A' } }), + h(ExampleComponent, { props: { test: 'B' } }), + ]), + }).$mount(); + + expect(app.$children[0].example).toBe('A'); + expect(app.$children[1].example).toBe('B'); + }); }); From 84f92d8a69a5f38ad42713040f0fd3a7ba0f4d1c Mon Sep 17 00:00:00 2001 From: liximomo Date: Tue, 6 Aug 2019 19:07:28 +0800 Subject: [PATCH 013/551] chore: update commets --- src/wrappers/AbstractWrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wrappers/AbstractWrapper.ts b/src/wrappers/AbstractWrapper.ts index 87b00ab2..bb8a2332 100644 --- a/src/wrappers/AbstractWrapper.ts +++ b/src/wrappers/AbstractWrapper.ts @@ -19,7 +19,7 @@ export default abstract class AbstractWrapper { }, }); if (process.env.NODE_ENV !== 'production') { - // after data has resolved, expose bindings to vm._data. + // expose bindings after state has been resolved to prevent repeated works vm.$nextTick(() => { this.exposeToDevtool(); }); From b862bf9c0e72d3e28a0f9fa2653cf1984d08b005 Mon Sep 17 00:00:00 2001 From: liximomo Date: Mon, 5 Aug 2019 18:46:57 +0800 Subject: [PATCH 014/551] build: release 2.1.1 --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f408cf09..ee6bb127 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +# 2.1.1 +* Add a new signature of `provide`: `provide(key, value)`. +* Fix multiple `provide` invoking per component. +* Fix order of `setup` invoking. +* `onErrorCaptured` not triggered ([#25](/vuejs/vue-function-api/issues/25)). +* Fix `this` losing in nested setup call ([#38](/vuejs/vue-function-api/issues/38)). +* Fix some edge cases of unwarpping. +* Change `context.slots`'s value. It now proxies to `$scopeSlots` instead of `$slots`. + # 2.0.6 ## Fixed * watch callback is called repeatedly with multi-sources diff --git a/package.json b/package.json index f34d4f07..e8f42905 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-function-api", - "version": "2.0.6", + "version": "2.1.1", "description": "Provide logic composition capabilities for Vue.", "keywords": [ "vue", From 53dd384ad40365c736e247de4415e60cb71cff9e Mon Sep 17 00:00:00 2001 From: liximomo Date: Tue, 6 Aug 2019 23:27:59 +0800 Subject: [PATCH 015/551] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee6bb127..bc6f0d60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,5 @@ # 2.1.1 +* Export `set` from `vue-function-api`. Using exported `set` whenever you need to use [Vue.set](https://vuejs.org/v2/api/#Vue-set) or [vm.$set](https://vuejs.org/v2/api/#vm-set). The custom `set` ensures that auto-unwrapping works for the new property. * Add a new signature of `provide`: `provide(key, value)`. * Fix multiple `provide` invoking per component. * Fix order of `setup` invoking. From 1987efa76f7c3355a67de0c2d538609ca87ab3f8 Mon Sep 17 00:00:00 2001 From: liximomo Date: Thu, 8 Aug 2019 11:47:29 +0800 Subject: [PATCH 016/551] perf: skip AccessControl setup for vue's Observer --- src/reactivity/observable.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/reactivity/observable.ts b/src/reactivity/observable.ts index 5e6021a1..8b91bead 100644 --- a/src/reactivity/observable.ts +++ b/src/reactivity/observable.ts @@ -41,6 +41,8 @@ function isObservable(obj: any): boolean { * Auto unwrapping when acccess property */ export function defineAccessControl(target: AnyObject, key: any, val?: any) { + if (key === '__ob__') return; + let getter: (() => any) | undefined; let setter: ((x: any) => void) | undefined; const property = Object.getOwnPropertyDescriptor(target, key); @@ -50,7 +52,9 @@ export function defineAccessControl(target: AnyObject, key: any, val?: any) { } getter = property.get; setter = property.set; - val = target[key]; + if ((!getter || setter) /* not only have getter */ && arguments.length === 2) { + val = target[key]; + } } setupAccessControl(val); From 96e391fbda8f68efb78f8f77178c4107d160c734 Mon Sep 17 00:00:00 2001 From: liximomo Date: Sat, 10 Aug 2019 10:08:19 +0800 Subject: [PATCH 017/551] fix: fix `value` breaking array. Remove Auto-Unwrapping for array. Currently, we can't let this work without using `Proxy`. Resolves: #53 --- src/reactivity/observable.ts | 2 +- src/reactivity/set.ts | 2 - test/functions/state.spec.js | 99 ++++++++++++------------------------ 3 files changed, 34 insertions(+), 69 deletions(-) diff --git a/src/reactivity/observable.ts b/src/reactivity/observable.ts index 8b91bead..a019f11f 100644 --- a/src/reactivity/observable.ts +++ b/src/reactivity/observable.ts @@ -11,7 +11,7 @@ const ObservableIdentifier = {}; * We can do unwrapping and other things here. */ function setupAccessControl(target: AnyObject) { - if (!isObject(target) || isWrapper(target)) { + if (!isObject(target) || Array.isArray(target) || isWrapper(target)) { return; } diff --git a/src/reactivity/set.ts b/src/reactivity/set.ts index e6e95466..8d82307c 100644 --- a/src/reactivity/set.ts +++ b/src/reactivity/set.ts @@ -34,8 +34,6 @@ export function set(target: any, key: any, val: T): T { } if (isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key); - // IMPORTANT: define access control before trigger watcher - defineAccessControl(target, key, val); target.splice(key, 1, val); return val; } diff --git a/test/functions/state.spec.js b/test/functions/state.spec.js index bfccdb17..1a331a39 100644 --- a/test/functions/state.spec.js +++ b/test/functions/state.spec.js @@ -4,48 +4,18 @@ const { plugin, state, value, watch, set } = require('../../src'); Vue.use(plugin); describe('Hooks value', () => { - it('should proxy and be reactive', done => { - const vm = new Vue({ + it('should work with array', () => { + let arr; + new Vue({ setup() { - return { - name: value(null), - msg: value('foo'), - }; - }, - template: '
{{name}}, {{ msg }}
', - }).$mount(); - vm.name = 'foo'; - vm.msg = 'bar'; - waitForUpdate(() => { - expect(vm.$el.textContent).toBe('foo, bar'); - }).then(done); - }); -}); - -describe('Hooks state', () => { - it('should work', done => { - const app = new Vue({ - setup() { - return { - state: state({ - count: 0, - }), - }; + arr = value([2]); + arr.value.push(3); + arr.value.unshift(1); }, - render(h) { - return h('div', [h('span', this.state.count)]); - }, - }).$mount(); - - expect(app.$el.querySelector('span').textContent).toBe('0'); - app.state.count++; - waitForUpdate(() => { - expect(app.$el.querySelector('span').textContent).toBe('1'); - }).then(done); + }); + expect(arr.value).toEqual([1, 2, 3]); }); -}); -describe('reactivity/value', () => { it('should hold a value', () => { const a = value(1); expect(a.value).toBe(1); @@ -80,39 +50,58 @@ describe('reactivity/value', () => { a.value.count = 2; expect(dummy).toBe(2); }); +}); +describe('Hooks state', () => { + it('should work', done => { + const app = new Vue({ + setup() { + return { + state: state({ + count: 0, + }), + }; + }, + render(h) { + return h('div', [h('span', this.state.count)]); + }, + }).$mount(); + + expect(app.$el.querySelector('span').textContent).toBe('0'); + app.state.count++; + waitForUpdate(() => { + expect(app.$el.querySelector('span').textContent).toBe('1'); + }).then(done); + }); +}); + +describe('value/unwrapping', () => { it('should work like a normal property when nested in an observable(same ref)', () => { const a = value(1); const obj = state({ a, b: { c: a, - d: [a], }, }); let dummy1; let dummy2; - let dummy3; watch( () => obj, () => { dummy1 = obj.a; dummy2 = obj.b.c; - dummy3 = obj.b.d[0]; }, { deep: true } ); expect(dummy1).toBe(1); expect(dummy2).toBe(1); - expect(dummy3).toBe(1); a.value++; expect(dummy1).toBe(2); expect(dummy2).toBe(2); - expect(dummy3).toBe(2); obj.a++; expect(dummy1).toBe(3); expect(dummy2).toBe(3); - expect(dummy3).toBe(3); }); it('should work like a normal property when nested in an observable(different ref)', () => { @@ -197,26 +186,4 @@ describe('reactivity/value', () => { obj.a.foo++; expect(dummy).toBe(3); }); - - it('should work like a normal property when nested in an observable(new property of array)', () => { - const count = value(1); - const obj = state({ - a: [], - }); - let dummy; - watch( - () => obj, - () => { - dummy = obj.a[0]; - }, - { deep: true } - ); - expect(dummy).toBe(undefined); - set(obj.a, 0, count); - expect(dummy).toBe(1); - count.value++; - expect(dummy).toBe(2); - obj.a[0]++; - expect(dummy).toBe(3); - }); }); From 4f68e60fff6dcb247842919869a4f9c41490f6e3 Mon Sep 17 00:00:00 2001 From: liximomo Date: Sat, 10 Aug 2019 10:12:26 +0800 Subject: [PATCH 018/551] docs: add docs for TypeScript --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index ce335709..1b475c67 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ - [Todo App Compare with Vue2 API](https://codesandbox.io/s/todo-example-6d7ep) - [CodePen Live Demo](https://codepen.io/liximomo/pen/dBOvgg) - [Single-File Component](#single-file-Component) +- [TypeScript](#TypeScript) - [API](#API) - [setup](#setup) - [value](#value) @@ -112,6 +113,23 @@ After installing the plugin you can use the new [function API](#API) to compose ``` + +# TypeScript +To let TypeScript properly infer types inside Vue component options, you need to define components with `Vue.component`、`Vue.extend` or `createComponent`: + +```ts +import Vue from 'vue' + +const Component = createComponent({ + // type inference enabled +}) + +const Component = { + // this will NOT have type inference, + // because TypeScript can't tell this is options for a Vue component. +} +``` + # API ## setup From 1869b537148598fffafd36436e821f0e6e1fed20 Mon Sep 17 00:00:00 2001 From: liximomo Date: Sat, 10 Aug 2019 10:16:49 +0800 Subject: [PATCH 019/551] chore: update changelog --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6f0d60..42186a15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,13 @@ +# 2.1.2 +* Remove auto-unwrapping for Array ([#53](https://github.com/vuejs/vue-function-api/issues/53)). + # 2.1.1 * Export `set` from `vue-function-api`. Using exported `set` whenever you need to use [Vue.set](https://vuejs.org/v2/api/#Vue-set) or [vm.$set](https://vuejs.org/v2/api/#vm-set). The custom `set` ensures that auto-unwrapping works for the new property. * Add a new signature of `provide`: `provide(key, value)`. * Fix multiple `provide` invoking per component. * Fix order of `setup` invoking. -* `onErrorCaptured` not triggered ([#25](/vuejs/vue-function-api/issues/25)). -* Fix `this` losing in nested setup call ([#38](/vuejs/vue-function-api/issues/38)). +* `onErrorCaptured` not triggered ([#25](https://github.com/vuejs/vue-function-api/issues/25)). +* Fix `this` losing in nested setup call ([#38](https://github.com/vuejs/vue-function-api/issues/38)). * Fix some edge cases of unwarpping. * Change `context.slots`'s value. It now proxies to `$scopeSlots` instead of `$slots`. From 62bb55becd49bbfa36ccdc44542106eb892485d2 Mon Sep 17 00:00:00 2001 From: liximomo Date: Sat, 10 Aug 2019 10:23:38 +0800 Subject: [PATCH 020/551] build: release v2.1.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e8f42905..dffa9c60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-function-api", - "version": "2.1.1", + "version": "2.1.2", "description": "Provide logic composition capabilities for Vue.", "keywords": [ "vue", From 6ce30bfb6452984fb8b33df9ed633d3375e9b406 Mon Sep 17 00:00:00 2001 From: Septian A Tama Date: Sun, 11 Aug 2019 06:53:49 +0700 Subject: [PATCH 021/551] chore: fix some grammatical errors --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1b475c67..01beaf6b 100644 --- a/README.md +++ b/README.md @@ -357,7 +357,7 @@ Full properties list: # Misc -- Due the the limitation of `Vue2.x`'s public API. `vue-function-api` inevitably introduces some extra workload. This shouldn't concern you unless are already pushing your environment to the extreme. +- Due to the limitation of `Vue2.x`'s public API, `vue-function-api` inevitably introduces some extra workload. This shouldn't concern you unless you are already pushing your environment to the extreme. [wrapper]: https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#why-do-we-need-value-wrappers From e86b855fa058f67afd9be5187f26694239ea6395 Mon Sep 17 00:00:00 2001 From: liximomo Date: Wed, 14 Aug 2019 11:16:28 +0800 Subject: [PATCH 022/551] fix(types): allow string keys in provide/inject Resolves: #56 --- src/functions/inject.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/functions/inject.ts b/src/functions/inject.ts index ea2c6a7c..c61d08ee 100644 --- a/src/functions/inject.ts +++ b/src/functions/inject.ts @@ -23,8 +23,8 @@ function resolveInject(provideKey: Key, vm: Vue): any { } export function provide(data: AnyObject): void; -export function provide(key: Key, value: T | Wrapper): void; -export function provide(keyOrData: Key | AnyObject, value?: T | Wrapper): void { +export function provide(key: Key | string, value: T | Wrapper): void; +export function provide(keyOrData: Key | string | AnyObject, value?: T | Wrapper): void { const vm: any = ensureCurrentVMInFn('provide'); if (!vm._provided) { vm._provided = {}; @@ -37,13 +37,13 @@ export function provide(keyOrData: Key | AnyObject, value?: T | Wrapper } } -export function inject(key: Key): Wrapper | void { +export function inject(key: Key | string): Wrapper | void { if (!key) { return; } const vm = ensureCurrentVMInFn('inject'); - const val = resolveInject(key, vm); + const val = resolveInject(key as Key, vm); if (val !== UNRESOLVED_INJECT) { if (isWrapper(val)) { return val; From 6d56ff45db6dcbff897ecd23b9deff4826a66bc0 Mon Sep 17 00:00:00 2001 From: liximomo Date: Wed, 14 Aug 2019 11:23:29 +0800 Subject: [PATCH 023/551] fix: skip setting access control for Vue instance --- src/helper.ts | 6 +++++- src/reactivity/observable.ts | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/helper.ts b/src/helper.ts index 8225c5a5..f8ecdff2 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,5 +1,5 @@ import { VueInstance } from './types/vue'; -import { getCurrentVue, getCurrentVM } from './runtimeContext'; +import { currentVue, getCurrentVue, getCurrentVM } from './runtimeContext'; import { assert } from './utils'; export function ensureCurrentVMInFn(hook: string): VueInstance { @@ -27,3 +27,7 @@ export function compoundComputed(computed: { Vue.config.silent = silent; return reactive; } + +export function isVueInstance(obj: any) { + return currentVue && obj instanceof currentVue; +} diff --git a/src/reactivity/observable.ts b/src/reactivity/observable.ts index a019f11f..bed93ea1 100644 --- a/src/reactivity/observable.ts +++ b/src/reactivity/observable.ts @@ -1,6 +1,7 @@ import { AnyObject } from '../types/basic'; import { getCurrentVue } from '../runtimeContext'; import { isObject, def, hasOwn } from '../utils'; +import { isVueInstance } from '../helper'; import { isWrapper } from '../wrappers'; import { AccessControIdentifierlKey, ObservableIdentifierKey } from '../symbols'; @@ -11,7 +12,7 @@ const ObservableIdentifier = {}; * We can do unwrapping and other things here. */ function setupAccessControl(target: AnyObject) { - if (!isObject(target) || Array.isArray(target) || isWrapper(target)) { + if (!isObject(target) || Array.isArray(target) || isWrapper(target) || isVueInstance(target)) { return; } From 2ffa07fa1e8b0a9508e2665daf1e78a5780dd0ce Mon Sep 17 00:00:00 2001 From: liximomo Date: Wed, 14 Aug 2019 11:31:10 +0800 Subject: [PATCH 024/551] chore(test): add test environment setup scripts --- package.json | 3 +++ test/functions/computed.spec.js | 4 +--- test/functions/inject.spec.js | 4 +--- test/functions/lifecycle.spec.js | 3 --- test/functions/state.spec.js | 4 +--- test/functions/watch.spec.js | 4 +--- test/setup.spec.js | 4 +--- test/setupTest.js | 6 ++++++ 8 files changed, 14 insertions(+), 18 deletions(-) create mode 100644 test/setupTest.js diff --git a/package.json b/package.json index dffa9c60..5e9e11b3 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,9 @@ }, "jest": { "verbose": true, + "setupFiles": [ + "/test/setupTest.js" + ], "setupFilesAfterEnv": [ "/test/helpers/wait-for-update.js" ], diff --git a/test/functions/computed.spec.js b/test/functions/computed.spec.js index 5c7a3a14..1e084173 100644 --- a/test/functions/computed.spec.js +++ b/test/functions/computed.spec.js @@ -1,7 +1,5 @@ const Vue = require('vue/dist/vue.common.js'); -const { plugin, value, computed } = require('../../src'); - -Vue.use(plugin); +const { value, computed } = require('../../src'); describe('Hooks computed', () => { beforeEach(() => { diff --git a/test/functions/inject.spec.js b/test/functions/inject.spec.js index 79ae3a06..825d7e1b 100644 --- a/test/functions/inject.spec.js +++ b/test/functions/inject.spec.js @@ -1,7 +1,5 @@ const Vue = require('vue/dist/vue.common.js'); -const { plugin, inject, provide, value } = require('../../src'); - -Vue.use(plugin); +const { inject, provide, value } = require('../../src'); let injected; const injectedComp = { diff --git a/test/functions/lifecycle.spec.js b/test/functions/lifecycle.spec.js index 78446037..9683185e 100644 --- a/test/functions/lifecycle.spec.js +++ b/test/functions/lifecycle.spec.js @@ -1,6 +1,5 @@ const Vue = require('vue/dist/vue.common.js'); const { - plugin, onCreated, onBeforeMount, onMounted, @@ -11,8 +10,6 @@ const { onErrorCaptured, } = require('../../src'); -Vue.use(plugin); - describe('Hooks lifecycle', () => { describe('created', () => { it('work with created option', () => { diff --git a/test/functions/state.spec.js b/test/functions/state.spec.js index 1a331a39..9d466e42 100644 --- a/test/functions/state.spec.js +++ b/test/functions/state.spec.js @@ -1,7 +1,5 @@ const Vue = require('vue/dist/vue.common.js'); -const { plugin, state, value, watch, set } = require('../../src'); - -Vue.use(plugin); +const { state, value, watch, set } = require('../../src'); describe('Hooks value', () => { it('should work with array', () => { diff --git a/test/functions/watch.spec.js b/test/functions/watch.spec.js index 23e10b54..d14600e1 100644 --- a/test/functions/watch.spec.js +++ b/test/functions/watch.spec.js @@ -1,7 +1,5 @@ const Vue = require('vue/dist/vue.common.js'); -const { plugin, value, state, watch } = require('../../src'); - -Vue.use(plugin); +const { value, state, watch } = require('../../src'); describe('Hooks watch', () => { let spy; diff --git a/test/setup.spec.js b/test/setup.spec.js index 900697a8..3eb90667 100644 --- a/test/setup.spec.js +++ b/test/setup.spec.js @@ -1,7 +1,5 @@ const Vue = require('vue/dist/vue.common.js'); -const { plugin, value, computed, onCreated } = require('../src'); - -Vue.use(plugin); +const { value, computed, onCreated } = require('../src'); describe('setup', () => { beforeEach(() => { diff --git a/test/setupTest.js b/test/setupTest.js new file mode 100644 index 00000000..6ea8e898 --- /dev/null +++ b/test/setupTest.js @@ -0,0 +1,6 @@ +const Vue = require('vue/dist/vue.common'); +const { plugin } = require('../src'); + +Vue.config.productionTip = false; +Vue.config.devtools = false; +Vue.use(plugin); From 096d53121b9d5a7f1caa597d6be130f3b3435012 Mon Sep 17 00:00:00 2001 From: liximomo Date: Thu, 4 Jul 2019 16:10:04 +0800 Subject: [PATCH 025/551] feat(setup): return a render function --- src/setup.ts | 44 +++++++++++++++------------- test/setup.spec.js | 71 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 19 deletions(-) diff --git a/src/setup.ts b/src/setup.ts index ddb9a65c..559e5ad8 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -2,7 +2,7 @@ import VueInstance, { VueConstructor } from 'vue'; import { SetupContext } from './types/vue'; import { isWrapper } from './wrappers'; import { getCurrentVM, setCurrentVM } from './runtimeContext'; -import { isPlainObject, assert, proxy, warn, logError } from './utils'; +import { isPlainObject, assert, proxy, warn, logError, isFunction } from './utils'; import { value } from './functions/state'; export function mixin(Vue: VueConstructor) { @@ -54,27 +54,33 @@ export function mixin(Vue: VueConstructor) { } if (!binding) return; - if (!isPlainObject(binding)) { - if (process.env.NODE_ENV !== 'production') { - assert( - false, - `"setup" must return a "Object", get "${Object.prototype.toString - .call(binding) - .slice(8, -1)}"` - ); - } + + if (isFunction(binding)) { + vm.$options.render = () => binding(vm.$props, ctx); return; } - Object.keys(binding).forEach(name => { - let bindingValue = binding[name]; - // make plain value reactive - if (!isWrapper(bindingValue)) { - bindingValue = value(bindingValue); - } - // bind to vm - bindingValue.setVmProperty(vm, name); - }); + if (isPlainObject(binding)) { + Object.keys(binding).forEach(name => { + let bindingValue = binding[name]; + // make plain value reactive + if (!isWrapper(bindingValue)) { + bindingValue = value(bindingValue); + } + // bind to vm + bindingValue.setVmProperty(vm, name); + }); + return; + } + + if (process.env.NODE_ENV !== 'production') { + assert( + false, + `"setup" must return a "Object" or a "Function", get "${Object.prototype.toString + .call(binding) + .slice(8, -1)}"` + ); + } } function createSetupContext(vm: VueInstance & { [x: string]: any }): SetupContext { diff --git a/test/setup.spec.js b/test/setup.spec.js index 3eb90667..b782251a 100644 --- a/test/setup.spec.js +++ b/test/setup.spec.js @@ -323,4 +323,75 @@ describe('setup', () => { expect(spy).toHaveBeenNthCalledWith(1, 1); expect(spy).toHaveBeenNthCalledWith(2, 2); }); + + it('inline render function should receive proper params', () => { + let p, c; + const vm = new Vue({ + template: ``, + components: { + child: { + name: 'child', + props: ['msg'], + setup() { + return (props, ctx) => { + p = props; + c = ctx; + return null; + }; + }, + }, + }, + }).$mount(); + expect(p).toEqual({ + msg: 'foo', + }); + expect(c).toBeDefined(); + expect(c.root).toBe(vm); + expect(c.attrs).toEqual({ + a: '1', + b: '2', + }); + }); + + it('inline render function should work', done => { + const vm = new Vue({ + props: ['msg'], + template: '
1
', + setup(_, { _vm }) { + const h = _vm.$createElement; + const count = value(0); + const increment = () => { + count.value++; + }; + + return props => + h('div', [ + h('span', props.msg), + h( + 'button', + { + on: { + click: increment, + }, + }, + count.value + ), + ]); + }, + propsData: { + msg: 'foo', + }, + }).$mount(); + expect(vm.$el.querySelector('span').textContent).toBe('foo'); + expect(vm.$el.querySelector('button').textContent).toBe('0'); + vm.$el.querySelector('button').click(); + waitForUpdate(() => { + expect(vm.$el.querySelector('button').textContent).toBe('1'); + vm.msg = 'bar'; + }) + .then(() => { + expect(vm.$el.querySelector('span').textContent).toBe('bar'); + }) + .then(done); + }); }); From eb94237f0e2239c6e19980b28f01f2ab85c49d71 Mon Sep 17 00:00:00 2001 From: liximomo Date: Wed, 14 Aug 2019 18:26:59 +0800 Subject: [PATCH 026/551] imporve: imporve type infer of "createComponent" Resolves: #15 --- src/functions/computed.ts | 17 ++++---- src/functions/lifecycle.ts | 4 +- src/functions/watch.ts | 15 +++---- src/helper.ts | 34 +++++++--------- src/index.ts | 10 ++--- src/reactivity/observable.ts | 14 ++++--- src/runtimeContext.ts | 13 +++--- src/setup.ts | 10 ++--- src/ts-api/component.ts | 78 ++++++++++++++++++++++++++++++++++++ src/ts-api/componentProps.ts | 46 +++++++++++++++++++++ src/ts-api/index.ts | 24 +---------- src/types/vue.ts | 13 ------ src/wrappers/index.ts | 75 ++++++++++++++++++++++++++++++++++ 13 files changed, 256 insertions(+), 97 deletions(-) create mode 100644 src/ts-api/component.ts create mode 100644 src/ts-api/componentProps.ts delete mode 100644 src/types/vue.ts diff --git a/src/functions/computed.ts b/src/functions/computed.ts index 6cd0c6cd..8072596d 100644 --- a/src/functions/computed.ts +++ b/src/functions/computed.ts @@ -1,19 +1,22 @@ -import { compoundComputed } from '../helper'; +import { getCurrentVue } from '../runtimeContext'; +import { createComponentInstance } from '../helper'; import { Wrapper, ComputedWrapper } from '../wrappers'; export function computed(getter: () => T, setter?: (x: T) => void): Wrapper { - const computedHost = compoundComputed({ - $$state: { - get: getter, - set: setter, + const computedHost = createComponentInstance(getCurrentVue(), { + computed: { + $$state: { + get: getter, + set: setter, + }, }, }); return new ComputedWrapper({ - read: () => computedHost.$$state, + read: () => (computedHost as any).$$state, ...(setter && { write: (v: T) => { - computedHost.$$state = v; + (computedHost as any).$$state = v; }, }), }); diff --git a/src/functions/lifecycle.ts b/src/functions/lifecycle.ts index c29c2a2a..12b59e25 100644 --- a/src/functions/lifecycle.ts +++ b/src/functions/lifecycle.ts @@ -1,5 +1,5 @@ import { VueConstructor } from 'vue'; -import { VueInstance } from '../types/vue'; +import { ComponentInstance } from '../ts-api'; import { getCurrentVue } from '../runtimeContext'; import { ensureCurrentVMInFn } from '../helper'; @@ -21,7 +21,7 @@ function createLifeCycles(lifeCyclehooks: string[], name: string) { }; } -function injectHookOption(Vue: VueConstructor, vm: VueInstance, hook: string, val: Function) { +function injectHookOption(Vue: VueConstructor, vm: ComponentInstance, hook: string, val: Function) { const options = vm.$options as any; const mergeFn = Vue.config.optionMergeStrategies[hook]; options[hook] = mergeFn(options[hook], val); diff --git a/src/functions/watch.ts b/src/functions/watch.ts index be188dbc..739d9be3 100644 --- a/src/functions/watch.ts +++ b/src/functions/watch.ts @@ -1,6 +1,7 @@ -import { VueInstance } from '../types/vue'; +import { ComponentInstance } from '../ts-api'; import { Wrapper } from '../wrappers'; import { isArray, assert } from '../utils'; +import { createComponentInstance } from '../helper'; import { isWrapper } from '../wrappers'; import { getCurrentVM, getCurrentVue } from '../runtimeContext'; import { WatcherPreFlushQueueKey, WatcherPostFlushQueueKey } from '../symbols'; @@ -22,7 +23,7 @@ interface WatcherContext { watcherStopHandle: Function; } -let fallbackVM: VueInstance; +let fallbackVM: ComponentInstance; function hasWatchEnv(vm: any) { return vm[WatcherPreFlushQueueKey] !== undefined; @@ -81,7 +82,7 @@ function flushWatcherCallback(vm: any, fn: Function, mode: FlushMode) { } function createSingleSourceWatcher( - vm: VueInstance, + vm: ComponentInstance, source: watchedValue, cb: watcherCallBack, options: WatcherOption @@ -128,7 +129,7 @@ function createSingleSourceWatcher( } function createMuiltSourceWatcher( - vm: VueInstance, + vm: ComponentInstance, sources: Array>, cb: watcherCallBack, options: WatcherOption @@ -245,11 +246,7 @@ export function watch( let vm = getCurrentVM(); if (!vm) { if (!fallbackVM) { - const Vue = getCurrentVue(); - const silent = Vue.config.silent; - Vue.config.silent = true; - fallbackVM = new Vue(); - Vue.config.silent = silent; + fallbackVM = createComponentInstance(getCurrentVue()); } vm = fallbackVM; opts.flush = 'sync'; diff --git a/src/helper.ts b/src/helper.ts index f8ecdff2..74ea72b5 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,8 +1,9 @@ -import { VueInstance } from './types/vue'; -import { currentVue, getCurrentVue, getCurrentVM } from './runtimeContext'; +import Vue, { ComponentOptions, VueConstructor } from 'vue'; +import { ComponentInstance } from './ts-api'; +import { currentVue, getCurrentVM } from './runtimeContext'; import { assert } from './utils'; -export function ensureCurrentVMInFn(hook: string): VueInstance { +export function ensureCurrentVMInFn(hook: string): ComponentInstance { const vm = getCurrentVM(); if (process.env.NODE_ENV !== 'production') { assert(vm, `"${hook}" get called outside of "setup()"`); @@ -10,24 +11,17 @@ export function ensureCurrentVMInFn(hook: string): VueInstance { return vm!; } -export function compoundComputed(computed: { - [key: string]: - | (() => any) - | { - get?: () => any; - set?: (v: any) => void; - }; -}) { - const Vue = getCurrentVue(); - const silent = Vue.config.silent; - Vue.config.silent = true; - const reactive = new Vue({ - computed, - }); - Vue.config.silent = silent; - return reactive; +export function createComponentInstance( + Ctor: VueConstructor, + options: ComponentOptions = {} +) { + const silent = Ctor.config.silent; + Ctor.config.silent = true; + const vm = new Ctor(options); + Ctor.config.silent = silent; + return vm; } -export function isVueInstance(obj: any) { +export function isComponentInstance(obj: any) { return currentVue && obj instanceof currentVue; } diff --git a/src/index.ts b/src/index.ts index 2db00fab..c116349a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import Vue, { VueConstructor } from 'vue'; -import { SetupContext } from './types/vue'; +import { SetupFunction } from './ts-api'; import { currentVue } from './runtimeContext'; import { Wrapper } from './wrappers'; import { install } from './install'; @@ -7,11 +7,7 @@ import { mixin } from './setup'; declare module 'vue/types/options' { interface ComponentOptions { - setup?: ( - this: void, - props: { [x: string]: any }, - context: SetupContext - ) => object | null | undefined | void; + setup?: SetupFunction<{}, {}>; } } @@ -27,7 +23,7 @@ if (currentVue && typeof window !== 'undefined' && window.Vue) { export { plugin, Wrapper }; export { set } from './reactivity'; -export * from './ts-api'; +export { createComponent, PropType } from './ts-api'; export * from './functions/state'; export * from './functions/lifecycle'; export * from './functions/watch'; diff --git a/src/reactivity/observable.ts b/src/reactivity/observable.ts index bed93ea1..0d7a7a8e 100644 --- a/src/reactivity/observable.ts +++ b/src/reactivity/observable.ts @@ -1,7 +1,7 @@ import { AnyObject } from '../types/basic'; import { getCurrentVue } from '../runtimeContext'; import { isObject, def, hasOwn } from '../utils'; -import { isVueInstance } from '../helper'; +import { isComponentInstance, createComponentInstance } from '../helper'; import { isWrapper } from '../wrappers'; import { AccessControIdentifierlKey, ObservableIdentifierKey } from '../symbols'; @@ -12,7 +12,12 @@ const ObservableIdentifier = {}; * We can do unwrapping and other things here. */ function setupAccessControl(target: AnyObject) { - if (!isObject(target) || Array.isArray(target) || isWrapper(target) || isVueInstance(target)) { + if ( + !isObject(target) || + Array.isArray(target) || + isWrapper(target) || + isComponentInstance(target) + ) { return; } @@ -103,14 +108,11 @@ export function observable(obj: T): T { if (Vue.observable) { observed = Vue.observable(obj); } else { - const silent = Vue.config.silent; - Vue.config.silent = true; - const vm = new Vue({ + const vm = createComponentInstance(Vue, { data: { $$state: obj, }, }); - Vue.config.silent = silent; observed = vm._data.$$state; } diff --git a/src/runtimeContext.ts b/src/runtimeContext.ts index 2e0cc208..f9b74ada 100644 --- a/src/runtimeContext.ts +++ b/src/runtimeContext.ts @@ -1,8 +1,9 @@ -import Vue, { VueConstructor } from 'vue'; +import { VueConstructor } from 'vue'; +import { ComponentInstance } from './ts-api'; import { assert } from './utils'; let currentVue: VueConstructor | null = null; -let currentVM: Vue | null = null; +let currentVM: ComponentInstance | null = null; export function getCurrentVue(): VueConstructor { if (process.env.NODE_ENV !== 'production') { @@ -16,12 +17,12 @@ export function setCurrentVue(vue: VueConstructor) { currentVue = vue; } -export function getCurrentVM(): Vue | null { +export function getCurrentVM(): ComponentInstance | null { return currentVM; } -export function setCurrentVM(vue: Vue | null) { - currentVM = vue; +export function setCurrentVM(vm: ComponentInstance | null) { + currentVM = vm; } -export { currentVue }; +export { currentVue, currentVM }; diff --git a/src/setup.ts b/src/setup.ts index 559e5ad8..f026da22 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -1,5 +1,5 @@ -import VueInstance, { VueConstructor } from 'vue'; -import { SetupContext } from './types/vue'; +import { VueConstructor } from 'vue'; +import { ComponentInstance, SetupContext } from './ts-api'; import { isWrapper } from './wrappers'; import { getCurrentVM, setCurrentVM } from './runtimeContext'; import { isPlainObject, assert, proxy, warn, logError, isFunction } from './utils'; @@ -13,7 +13,7 @@ export function mixin(Vue: VueConstructor) { /** * Vuex init hook, injected into each instances init hooks list. */ - function functionApiInit(this: VueInstance) { + function functionApiInit(this: ComponentInstance) { const vm = this; const $options = vm.$options; const { setup } = $options; @@ -39,7 +39,7 @@ export function mixin(Vue: VueConstructor) { }; } - function initSetup(vm: VueInstance, props: Record = {}) { + function initSetup(vm: ComponentInstance, props: Record = {}) { const setup = vm.$options.setup!; const ctx = createSetupContext(vm); let binding: any; @@ -83,7 +83,7 @@ export function mixin(Vue: VueConstructor) { } } - function createSetupContext(vm: VueInstance & { [x: string]: any }): SetupContext { + function createSetupContext(vm: ComponentInstance & { [x: string]: any }): SetupContext { const ctx = {} as SetupContext; const props: Array = [ 'root', diff --git a/src/ts-api/component.ts b/src/ts-api/component.ts new file mode 100644 index 00000000..a3ff0c9c --- /dev/null +++ b/src/ts-api/component.ts @@ -0,0 +1,78 @@ +// import Vue, { VueConstructor, VNode, ComponentOptions as Vue2ComponentOptions } from 'vue'; +import { VueConstructor, VNode, ComponentOptions as Vue2ComponentOptions } from 'vue'; +import { ComponentPropsOptions, ExtractPropTypes } from './componentProps'; +import { UnwrapValue } from '../wrappers'; + +export type Data = { [key: string]: unknown }; + +export type ComponentInstance = InstanceType; + +// public properties exposed on the proxy, which is used as the render context +// in templates (as `this` in the render option) +type ComponentRenderProxy

= { + $data: S; + $props: PublicProps; + $attrs: Data; + $refs: Data; + $slots: Data; + $root: ComponentInstance | null; + $parent: ComponentInstance | null; + $emit: (event: string, ...args: unknown[]) => void; +} & P & + S; + +// for Vetur and TSX support +type VueConstructorProxy = { + new (): ComponentRenderProxy< + ExtractPropTypes, + UnwrapValue, + ExtractPropTypes + >; +}; + +type VueProxy = Vue2ComponentOptions< + never, + UnwrapValue, + never, + never, + PropsOptions, + ExtractPropTypes +> & + VueConstructorProxy; + +export interface SetupContext { + readonly parent: ComponentInstance; + readonly root: ComponentInstance; + readonly refs: { [key: string]: ComponentInstance | Element | ComponentInstance[] | Element[] }; + readonly slots: { [key: string]: VNode[] | undefined }; + readonly attrs: Record; + + emit(event: string, ...args: any[]): void; +} + +type RenderFunction = (props: Props, ctx: SetupContext) => VNode; + +export type SetupFunction = ( + this: void, + props: Props, + ctx: SetupContext +) => RawBindings | RenderFunction; + +export interface ComponentOptions< + PropsOptions = ComponentPropsOptions, + RawBindings = Data, + Props = ExtractPropTypes +> { + props?: PropsOptions; + setup?: SetupFunction; +} + +// object format with object props declaration +// see `ExtractPropTypes` in ./componentProps.ts +export function createComponent( + options: ComponentOptions +): VueProxy; +// implementation, close to no-op +export function createComponent(options: any) { + return options as any; +} diff --git a/src/ts-api/componentProps.ts b/src/ts-api/componentProps.ts new file mode 100644 index 00000000..6e1015f7 --- /dev/null +++ b/src/ts-api/componentProps.ts @@ -0,0 +1,46 @@ +import { Data } from './component'; + +export type ComponentPropsOptions

= { + [K in keyof P]: Prop | null; +}; + +type Prop = PropOptions | PropType; + +interface PropOptions { + type?: PropType | null; + required?: boolean; + default?: T | null | undefined | (() => T | null | undefined); + validator?(value: any): boolean; +} + +export type PropType = PropConstructor | PropConstructor[]; + +type PropConstructor = { new (...args: any[]): T & object } | { (): T }; + +type RequiredKeys = { + [K in keyof T]: T[K] extends + | { required: true } + | (MakeDefautRequired extends true ? { default: any } : never) + ? K + : never; +}[keyof T]; + +type OptionalKeys = Exclude>; + +// prettier-ignore +type InferPropType = T extends null + ? any // null & true would fail to infer + : T extends { type: null } + ? any // somehow `ObjectContructor` when inferred from { (): T } becomes `any` + : T extends ObjectConstructor | { type: ObjectConstructor } + ? { [key: string]: any } + : T extends Prop + ? V + : T; + +// prettier-ignore +export type ExtractPropTypes = { + readonly [K in RequiredKeys]: InferPropType; +} & { + readonly [K in OptionalKeys]?: InferPropType; +}; diff --git a/src/ts-api/index.ts b/src/ts-api/index.ts index 5b742ac3..d746f350 100644 --- a/src/ts-api/index.ts +++ b/src/ts-api/index.ts @@ -1,22 +1,2 @@ -// import Vue from 'vue'; -import Vue, { ComponentOptions } from 'vue'; -import { SetupContext } from '../types/vue'; - -export type PropType = T; - -// type FullPropType = T extends { required: boolean } ? T : T | undefined; -type Omit = Pick>; -type ComponentOptionsWithSetup = Omit, 'props' | 'setup'> & { - props?: Props; - setup?: ( - this: undefined, - props: { [K in keyof Props]: Props[K] }, - context: SetupContext - ) => object | null | undefined | void; -}; - -export function createComponent( - compOpions: ComponentOptionsWithSetup -): ComponentOptions { - return (compOpions as any) as ComponentOptions; -} +export { createComponent, SetupFunction, SetupContext, ComponentInstance } from './component'; +export { PropType } from './componentProps'; diff --git a/src/types/vue.ts b/src/types/vue.ts deleted file mode 100644 index 3c381541..00000000 --- a/src/types/vue.ts +++ /dev/null @@ -1,13 +0,0 @@ -import Vue, { VueConstructor, VNode } from 'vue/'; - -export type VueInstance = InstanceType; - -export interface SetupContext { - readonly parent: Vue; - readonly root: Vue; - readonly refs: { [key: string]: Vue | Element | Vue[] | Element[] }; - readonly slots: { [key: string]: VNode[] | undefined }; - readonly attrs: Record; - - emit(event: string, ...args: any[]): void; -} diff --git a/src/wrappers/index.ts b/src/wrappers/index.ts index 907e112e..95618ceb 100644 --- a/src/wrappers/index.ts +++ b/src/wrappers/index.ts @@ -9,3 +9,78 @@ export function isWrapper(obj: any): obj is AbstractWrapper { return obj instanceof AbstractWrapper; } export { ValueWrapper, ComputedWrapper, AbstractWrapper }; + +type Value = Wrapper; + +type BailTypes = Function | Map | Set | WeakMap | WeakSet; + +// prettier-ignore +// Recursively unwraps nested value bindings. +// Unfortunately TS cannot do recursive types, but this should be enough for +// practical use cases... +export type UnwrapValue = T extends Value + ? UnwrapValue2 + : T extends BailTypes + ? T // bail out on types that shouldn't be unwrapped + : T extends object ? { [K in keyof T]: UnwrapValue2 } : T + +// prettier-ignore +type UnwrapValue2 = T extends Value + ? UnwrapValue3 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue3 } : T + +// prettier-ignore +type UnwrapValue3 = T extends Value + ? UnwrapValue4 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue4 } : T + +// prettier-ignore +type UnwrapValue4 = T extends Value + ? UnwrapValue5 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue5 } : T + +// prettier-ignore +type UnwrapValue5 = T extends Value + ? UnwrapValue6 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue6 } : T + +// prettier-ignore +type UnwrapValue6 = T extends Value + ? UnwrapValue7 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue7 } : T + +// prettier-ignore +type UnwrapValue7 = T extends Value + ? UnwrapValue8 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue8 } : T + +// prettier-ignore +type UnwrapValue8 = T extends Value + ? UnwrapValue9 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue9 } : T + +// prettier-ignore +type UnwrapValue9 = T extends Value + ? UnwrapValue10 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue10 } : T + +// prettier-ignore +type UnwrapValue10 = T extends Value + ? V // stop recursion + : T From 465a9ffc0f39b5d4f971d82e82829a749fde5072 Mon Sep 17 00:00:00 2001 From: liximomo Date: Wed, 14 Aug 2019 18:28:22 +0800 Subject: [PATCH 027/551] feat: createElement --- src/createElement.ts | 21 +++++++++++++++++++++ src/index.ts | 1 + test/setup.spec.js | 6 +++--- 3 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 src/createElement.ts diff --git a/src/createElement.ts b/src/createElement.ts new file mode 100644 index 00000000..44bb2dd1 --- /dev/null +++ b/src/createElement.ts @@ -0,0 +1,21 @@ +import Vue from 'vue'; +import { currentVM, getCurrentVue } from './runtimeContext'; +import { createComponentInstance } from './helper'; + +type CreateElement = Vue['$createElement']; + +let fallbackCreateElement: CreateElement; + +const createElement: CreateElement = function createElement(...args: any[]) { + if (!currentVM) { + if (!fallbackCreateElement) { + fallbackCreateElement = createComponentInstance(getCurrentVue()).$createElement; + } + + return fallbackCreateElement.apply(null, args as any); + } + + return currentVM.$createElement.apply(null, args as any); +} as any; + +export default createElement; diff --git a/src/index.ts b/src/index.ts index c116349a..20be93d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,7 @@ if (currentVue && typeof window !== 'undefined' && window.Vue) { _install(window.Vue); } +export { default as createElement } from './createElement'; export { plugin, Wrapper }; export { set } from './reactivity'; export { createComponent, PropType } from './ts-api'; diff --git a/test/setup.spec.js b/test/setup.spec.js index b782251a..29b73972 100644 --- a/test/setup.spec.js +++ b/test/setup.spec.js @@ -1,5 +1,5 @@ const Vue = require('vue/dist/vue.common.js'); -const { value, computed, onCreated } = require('../src'); +const { value, computed, onCreated, createElement: h } = require('../src'); describe('setup', () => { beforeEach(() => { @@ -354,11 +354,11 @@ describe('setup', () => { }); it('inline render function should work', done => { + // let createELement; const vm = new Vue({ props: ['msg'], template: '

1
', - setup(_, { _vm }) { - const h = _vm.$createElement; + setup() { const count = value(0); const increment = () => { count.value++; From 306da2d304ba9f1fa9d5efe8e5222dda08393ec0 Mon Sep 17 00:00:00 2001 From: liximomo Date: Thu, 15 Aug 2019 10:49:02 +0800 Subject: [PATCH 028/551] fix(types): change return type of "state" from T to UnwrapValue --- src/functions/state.ts | 8 ++-- src/reactivity/observable.ts | 8 ++-- src/wrappers/AbstractWrapper.ts | 79 +++++++++++++++++++++++++++++++ src/wrappers/index.ts | 82 +-------------------------------- test/functions/state.spec.js | 28 +++++++++-- 5 files changed, 114 insertions(+), 91 deletions(-) diff --git a/src/functions/state.ts b/src/functions/state.ts index 89786a23..59226075 100644 --- a/src/functions/state.ts +++ b/src/functions/state.ts @@ -1,10 +1,10 @@ -import { Wrapper, ValueWrapper } from '../wrappers'; +import { Wrapper, ValueWrapper, UnwrapValue } from '../wrappers'; import { observable } from '../reactivity'; -export function state(value: T): T { +export function state(value: T): UnwrapValue { return observable(value); } -export function value(value: T): Wrapper { - return new ValueWrapper(state({ $$state: value })); +export function value(value: T): Wrapper> { + return (new ValueWrapper(state({ $$state: value })) as any) as Wrapper>; } diff --git a/src/reactivity/observable.ts b/src/reactivity/observable.ts index 0d7a7a8e..597a52e4 100644 --- a/src/reactivity/observable.ts +++ b/src/reactivity/observable.ts @@ -2,7 +2,7 @@ import { AnyObject } from '../types/basic'; import { getCurrentVue } from '../runtimeContext'; import { isObject, def, hasOwn } from '../utils'; import { isComponentInstance, createComponentInstance } from '../helper'; -import { isWrapper } from '../wrappers'; +import { isWrapper, UnwrapValue } from '../wrappers'; import { AccessControIdentifierlKey, ObservableIdentifierKey } from '../symbols'; const AccessControlIdentifier = {}; @@ -98,9 +98,9 @@ export function defineAccessControl(target: AnyObject, key: any, val?: any) { /** * Make obj reactivity */ -export function observable(obj: T): T { +export function observable(obj: T): UnwrapValue { if (!isObject(obj) || isObservable(obj)) { - return obj; + return obj as UnwrapValue; } const Vue = getCurrentVue(); @@ -120,5 +120,5 @@ export function observable(obj: T): T { def(observed, ObservableIdentifierKey, ObservableIdentifier); } setupAccessControl(observed); - return observed; + return observed as UnwrapValue; } diff --git a/src/wrappers/AbstractWrapper.ts b/src/wrappers/AbstractWrapper.ts index bb8a2332..d3d5b2ae 100644 --- a/src/wrappers/AbstractWrapper.ts +++ b/src/wrappers/AbstractWrapper.ts @@ -1,6 +1,85 @@ import Vue from 'vue'; import { proxy, hasOwn, def, warn } from '../utils'; +export interface Wrapper { + value: V; +} + +type Value = Wrapper; + +type BailTypes = Function | Map | Set | WeakMap | WeakSet; + +// prettier-ignore +// Recursively unwraps nested value bindings. +// Unfortunately TS cannot do recursive types, but this should be enough for +// practical use cases... +export type UnwrapValue = T extends Value + ? UnwrapValue2 + : T extends BailTypes + ? T // bail out on types that shouldn't be unwrapped + : T extends object ? { [K in keyof T]: UnwrapValue2 } : T + +// prettier-ignore +type UnwrapValue2 = T extends Value + ? UnwrapValue3 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue3 } : T + +// prettier-ignore +type UnwrapValue3 = T extends Value + ? UnwrapValue4 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue4 } : T + +// prettier-ignore +type UnwrapValue4 = T extends Value + ? UnwrapValue5 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue5 } : T + +// prettier-ignore +type UnwrapValue5 = T extends Value + ? UnwrapValue6 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue6 } : T + +// prettier-ignore +type UnwrapValue6 = T extends Value + ? UnwrapValue7 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue7 } : T + +// prettier-ignore +type UnwrapValue7 = T extends Value + ? UnwrapValue8 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue8 } : T + +// prettier-ignore +type UnwrapValue8 = T extends Value + ? UnwrapValue9 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue9 } : T + +// prettier-ignore +type UnwrapValue9 = T extends Value + ? UnwrapValue10 + : T extends BailTypes + ? T + : T extends object ? { [K in keyof T]: UnwrapValue10 } : T + +// prettier-ignore +type UnwrapValue10 = T extends Value + ? V // stop recursion + : T + export default abstract class AbstractWrapper { protected _propName?: string; protected _vm?: Vue; diff --git a/src/wrappers/index.ts b/src/wrappers/index.ts index 95618ceb..0ba12bf5 100644 --- a/src/wrappers/index.ts +++ b/src/wrappers/index.ts @@ -1,86 +1,8 @@ -import AbstractWrapper from './AbstractWrapper'; +import AbstractWrapper, { Wrapper, UnwrapValue } from './AbstractWrapper'; import ValueWrapper from './ValueWrapper'; import ComputedWrapper from './ComputedWrapper'; -export interface Wrapper { - value: V; -} export function isWrapper(obj: any): obj is AbstractWrapper { return obj instanceof AbstractWrapper; } -export { ValueWrapper, ComputedWrapper, AbstractWrapper }; - -type Value = Wrapper; - -type BailTypes = Function | Map | Set | WeakMap | WeakSet; - -// prettier-ignore -// Recursively unwraps nested value bindings. -// Unfortunately TS cannot do recursive types, but this should be enough for -// practical use cases... -export type UnwrapValue = T extends Value - ? UnwrapValue2 - : T extends BailTypes - ? T // bail out on types that shouldn't be unwrapped - : T extends object ? { [K in keyof T]: UnwrapValue2 } : T - -// prettier-ignore -type UnwrapValue2 = T extends Value - ? UnwrapValue3 - : T extends BailTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapValue3 } : T - -// prettier-ignore -type UnwrapValue3 = T extends Value - ? UnwrapValue4 - : T extends BailTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapValue4 } : T - -// prettier-ignore -type UnwrapValue4 = T extends Value - ? UnwrapValue5 - : T extends BailTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapValue5 } : T - -// prettier-ignore -type UnwrapValue5 = T extends Value - ? UnwrapValue6 - : T extends BailTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapValue6 } : T - -// prettier-ignore -type UnwrapValue6 = T extends Value - ? UnwrapValue7 - : T extends BailTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapValue7 } : T - -// prettier-ignore -type UnwrapValue7 = T extends Value - ? UnwrapValue8 - : T extends BailTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapValue8 } : T - -// prettier-ignore -type UnwrapValue8 = T extends Value - ? UnwrapValue9 - : T extends BailTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapValue9 } : T - -// prettier-ignore -type UnwrapValue9 = T extends Value - ? UnwrapValue10 - : T extends BailTypes - ? T - : T extends object ? { [K in keyof T]: UnwrapValue10 } : T - -// prettier-ignore -type UnwrapValue10 = T extends Value - ? V // stop recursion - : T +export { Wrapper, UnwrapValue, ValueWrapper, ComputedWrapper }; diff --git a/test/functions/state.spec.js b/test/functions/state.spec.js index 9d466e42..6d66285f 100644 --- a/test/functions/state.spec.js +++ b/test/functions/state.spec.js @@ -74,6 +74,28 @@ describe('Hooks state', () => { }); describe('value/unwrapping', () => { + it('should work', () => { + const obj = state({ + a: value(0), + }); + const objWrapper = value(obj); + let dummy; + watch( + () => obj, + () => { + dummy = obj.a; + }, + { deep: true } + ); + expect(dummy).toBe(0); + expect(obj.a).toBe(0); + expect(objWrapper.value.a).toBe(0); + obj.a++; + expect(dummy).toBe(1); + objWrapper.value.a++; + expect(dummy).toBe(2); + }); + it('should work like a normal property when nested in an observable(same ref)', () => { const a = value(1); const obj = state({ @@ -155,11 +177,11 @@ describe('value/unwrapping', () => { { deep: true, lazy: true } ); expect(dummy).toBeUndefined(); - const wrapperC = value(1); + const wrapperC = value(2); obj.a.b = wrapperC; - expect(dummy).toBe(1); - obj.a.b++; expect(dummy).toBe(2); + obj.a.b++; + expect(dummy).toBe(3); }); it('should work like a normal property when nested in an observable(new property of object)', () => { From c3362a6daf09962f6be0a474c5f47b02e9b1cdb9 Mon Sep 17 00:00:00 2001 From: liximomo Date: Thu, 15 Aug 2019 17:41:37 +0800 Subject: [PATCH 029/551] improve: improve "watch" usages outsied of component --- src/functions/watch.ts | 177 ++++++++++--------- test/functions/state.spec.js | 22 ++- test/functions/watch.spec.js | 319 ++++++++++++++++++++--------------- 3 files changed, 297 insertions(+), 221 deletions(-) diff --git a/src/functions/watch.ts b/src/functions/watch.ts index 739d9be3..77030fb5 100644 --- a/src/functions/watch.ts +++ b/src/functions/watch.ts @@ -6,8 +6,8 @@ import { isWrapper } from '../wrappers'; import { getCurrentVM, getCurrentVue } from '../runtimeContext'; import { WatcherPreFlushQueueKey, WatcherPostFlushQueueKey } from '../symbols'; -const initValue = {}; -type InitValue = typeof initValue; +const INIT_VALUE = {}; +type InitValue = typeof INIT_VALUE; type watcherCallBack = (newVal: T, oldVal: T) => void; type watchedValue = Wrapper | (() => T); type FlushMode = 'pre' | 'post' | 'sync'; @@ -25,6 +25,14 @@ interface WatcherContext { let fallbackVM: ComponentInstance; +function flushPreQueue(this: any) { + flushQueue(this, WatcherPreFlushQueueKey); +} + +function flushPostQueue(this: any) { + flushQueue(this, WatcherPostFlushQueueKey); +} + function hasWatchEnv(vm: any) { return vm[WatcherPreFlushQueueKey] !== undefined; } @@ -32,14 +40,8 @@ function hasWatchEnv(vm: any) { function installWatchEnv(vm: any) { vm[WatcherPreFlushQueueKey] = []; vm[WatcherPostFlushQueueKey] = []; - vm.$on('hook:beforeUpdate', createFlusher(WatcherPreFlushQueueKey)); - vm.$on('hook:updated', createFlusher(WatcherPostFlushQueueKey)); -} - -function createFlusher(key: any) { - return function(this: any) { - flushQueue(this, key); - }; + vm.$on('hook:beforeUpdate', flushPreQueue); + vm.$on('hook:updated', flushPostQueue); } function flushQueue(vm: any, key: any) { @@ -50,34 +52,36 @@ function flushQueue(vm: any, key: any) { queue.length = 0; } -function flushWatcherCallback(vm: any, fn: Function, mode: FlushMode) { - // flush all when beforeUpdate and updated are not fired - function fallbackFlush() { - vm.$nextTick(() => { - if (vm[WatcherPreFlushQueueKey].length) { - flushQueue(vm, WatcherPreFlushQueueKey); - } - if (vm[WatcherPostFlushQueueKey].length) { - flushQueue(vm, WatcherPostFlushQueueKey); - } - }); - } +function scheduleFlush(vm: any, fn: Function, mode: Exclude) { + if (vm === fallbackVM) { + // no render pipeline, ignore flush mode + fn(); + } else { + // flush all when beforeUpdate and updated are not fired + const fallbackFlush = () => { + vm.$nextTick(() => { + if (vm[WatcherPreFlushQueueKey].length) { + flushQueue(vm, WatcherPreFlushQueueKey); + } + if (vm[WatcherPostFlushQueueKey].length) { + flushQueue(vm, WatcherPostFlushQueueKey); + } + }); + }; - switch (mode) { - case 'pre': - fallbackFlush(); - vm[WatcherPreFlushQueueKey].push(fn); - break; - case 'post': - fallbackFlush(); - vm[WatcherPostFlushQueueKey].push(fn); - break; - case 'sync': - fn(); - break; - default: - assert(false, `flush must be one of ["post", "pre", "sync"], but got ${mode}`); - break; + switch (mode) { + case 'pre': + fallbackFlush(); + vm[WatcherPreFlushQueueKey].push(fn); + break; + case 'post': + fallbackFlush(); + vm[WatcherPostFlushQueueKey].push(fn); + break; + default: + assert(false, `flush must be one of ["post", "pre", "sync"], but got ${mode}`); + break; + } } } @@ -94,6 +98,8 @@ function createSingleSourceWatcher( getter = source as () => T; } + // `callbackRef` is used to handle firty sync callbck. + // The subsequent callbcks will redirect to `flush`. let callbackRef = (n: T, o: T) => { callbackRef = flush; @@ -104,15 +110,19 @@ function createSingleSourceWatcher( } }; - const flush = (n: T, o: T) => { - flushWatcherCallback( - vm, - () => { - cb(n, o); - }, - options.flush - ); - }; + const flushMode = options.flush; + const flush = + flushMode === 'sync' + ? (n: T, o: T) => cb(n, o) + : (n: T, o: T) => { + scheduleFlush( + vm, + () => { + cb(n, o); + }, + flushMode + ); + }; return vm.$watch( getter, @@ -123,7 +133,7 @@ function createSingleSourceWatcher( immediate: !options.lazy, deep: options.deep, // @ts-ignore - sync: options.flush === 'sync', + sync: flushMode === 'sync', } ); } @@ -134,27 +144,30 @@ function createMuiltSourceWatcher( cb: watcherCallBack, options: WatcherOption ): () => void { - let execCallbackAfterNumRun: false | number = options.lazy ? false : sources.length; - let pendingCallback = false; const watcherContext: Array> = []; - - function execCallback() { + const execCallback = () => { cb.apply( vm, watcherContext.reduce<[T[], T[]]>( (acc, ctx) => { - acc[0].push((ctx.value === initValue ? ctx.getter() : ctx.value) as T); - acc[1].push((ctx.oldValue === initValue ? undefined : ctx.oldValue) as T); + const newVal: T = (ctx.value = (ctx.value === INIT_VALUE + ? ctx.getter() + : ctx.value) as any); + const oldVal: T = (ctx.oldValue === INIT_VALUE ? newVal : ctx.oldValue) as any; + ctx.oldValue = newVal; + acc[0].push(newVal); + acc[1].push(oldVal); return acc; }, [[], []] ) ); - } - function stop() { - watcherContext.forEach(ctx => ctx.watcherStopHandle()); - } + }; + const stop = () => watcherContext.forEach(ctx => ctx.watcherStopHandle()); + let execCallbackAfterNumRun: false | number = options.lazy ? false : sources.length; + // `callbackRef` is used to handle firty sync callbck. + // The subsequent callbcks will redirect to `flush`. let callbackRef = () => { if (execCallbackAfterNumRun !== false) { if (--execCallbackAfterNumRun === 0) { @@ -168,21 +181,26 @@ function createMuiltSourceWatcher( } }; - const flush = () => { - if (!pendingCallback) { - pendingCallback = true; - vm.$nextTick(() => { - flushWatcherCallback( - vm, - () => { - pendingCallback = false; - execCallback(); - }, - options.flush - ); - }); - } - }; + let pendingCallback = false; + const flushMode = options.flush; + const flush = + flushMode === 'sync' + ? execCallback + : () => { + if (!pendingCallback) { + pendingCallback = true; + vm.$nextTick(() => { + scheduleFlush( + vm, + () => { + pendingCallback = false; + execCallback(); + }, + flushMode + ); + }); + } + }; sources.forEach(source => { let getter: () => T; @@ -193,8 +211,8 @@ function createMuiltSourceWatcher( } const watcherCtx = { getter, - value: initValue, - oldValue: initValue, + value: INIT_VALUE, + oldValue: INIT_VALUE, } as WatcherContext; // must push watcherCtx before create watcherStopHandle watcherContext.push(watcherCtx); @@ -203,8 +221,10 @@ function createMuiltSourceWatcher( getter, (n: T, o: T) => { watcherCtx.value = n; - watcherCtx.oldValue = o; - + // only update oldValue at frist, susquent updates at execCallback + if (watcherCtx.oldValue === INIT_VALUE) { + watcherCtx.oldValue = o; + } callbackRef(); }, { @@ -249,11 +269,10 @@ export function watch( fallbackVM = createComponentInstance(getCurrentVue()); } vm = fallbackVM; - opts.flush = 'sync'; + } else if (!hasWatchEnv(vm)) { + installWatchEnv(vm); } - if (!hasWatchEnv(vm)) installWatchEnv(vm); - if (isArray(source)) { return createMuiltSourceWatcher(vm, source, cb as watcherCallBack, opts); } diff --git a/test/functions/state.spec.js b/test/functions/state.spec.js index 6d66285f..96e6228c 100644 --- a/test/functions/state.spec.js +++ b/test/functions/state.spec.js @@ -21,7 +21,7 @@ describe('Hooks value', () => { expect(a.value).toBe(2); }); - it('should be reactive', () => { + it('should be reactive', done => { const a = value(1); let dummy; watch(a, () => { @@ -29,10 +29,12 @@ describe('Hooks value', () => { }); expect(dummy).toBe(1); a.value = 2; - expect(dummy).toBe(2); + waitForUpdate(() => { + expect(dummy).toBe(2); + }).then(done); }); - it('should make nested properties reactive', () => { + it('should make nested properties reactive', done => { const a = value({ count: 1, }); @@ -46,7 +48,9 @@ describe('Hooks value', () => { ); expect(dummy).toBe(1); a.value.count = 2; - expect(dummy).toBe(2); + waitForUpdate(() => { + expect(dummy).toBe(2); + }).then(done); }); }); @@ -85,7 +89,7 @@ describe('value/unwrapping', () => { () => { dummy = obj.a; }, - { deep: true } + { deep: true, flush: 'sync' } ); expect(dummy).toBe(0); expect(obj.a).toBe(0); @@ -112,7 +116,7 @@ describe('value/unwrapping', () => { dummy1 = obj.a; dummy2 = obj.b.c; }, - { deep: true } + { deep: true, flush: 'sync' } ); expect(dummy1).toBe(1); expect(dummy2).toBe(1); @@ -142,7 +146,7 @@ describe('value/unwrapping', () => { dummy1 = obj.a; dummy2 = obj.b.c; }, - { deep: true } + { deep: true, flush: 'sync' } ); expect(dummy1).toBe(1); expect(dummy2).toBe(1); @@ -174,7 +178,7 @@ describe('value/unwrapping', () => { () => { dummy = obj.a.b; }, - { deep: true, lazy: true } + { deep: true, lazy: true, flush: 'sync' } ); expect(dummy).toBeUndefined(); const wrapperC = value(2); @@ -196,7 +200,7 @@ describe('value/unwrapping', () => { () => { dummy = obj.a.foo; }, - { deep: true } + { deep: true, flush: 'sync' } ); expect(dummy).toBe(undefined); set(obj.a, 'foo', count); diff --git a/test/functions/watch.spec.js b/test/functions/watch.spec.js index d14600e1..5a146bdc 100644 --- a/test/functions/watch.spec.js +++ b/test/functions/watch.spec.js @@ -11,12 +11,11 @@ describe('Hooks watch', () => { spy.mockReset(); }); - it('basic usage(value warpper)', done => { + it('should work', done => { const vm = new Vue({ setup() { const a = value(1); - watch(a, spy, { flush: 'pre' }); - + watch(a, spy); return { a, }; @@ -26,18 +25,19 @@ describe('Hooks watch', () => { expect(spy.mock.calls.length).toBe(1); expect(spy).toHaveBeenLastCalledWith(1, undefined); vm.a = 2; + vm.a = 3; expect(spy.mock.calls.length).toBe(1); waitForUpdate(() => { expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith(2, 1); + expect(spy).toHaveBeenLastCalledWith(3, 1); }).then(done); }); - it('basic usage(function)', done => { + it('basic usage(value warpper)', done => { const vm = new Vue({ setup() { const a = value(1); - watch(() => a.value, spy); + watch(a, spy, { flush: 'pre' }); return { a, @@ -55,150 +55,28 @@ describe('Hooks watch', () => { }).then(done); }); - it('basic usage(multiple sources, lazy=false, flush=none-sync)', done => { + it('basic usage(function)', done => { const vm = new Vue({ setup() { const a = value(1); - const b = value(1); - watch([a, b], spy, { lazy: false, flush: 'post' }); + watch(() => a.value, spy); return { a, - b, }; }, - template: `
{{a}} {{b}}
`, + template: `
{{a}}
`, }).$mount(); expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith([1, 1], [undefined, undefined]); - vm.a = 2; - expect(spy.mock.calls.length).toBe(1); - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]); - vm.a = 3; - vm.b = 3; - }) - .then(() => { - expect(spy.mock.calls.length).toBe(3); - expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); - }) - .then(done); - }); - - it('basic usage(multiple sources, lazy=true, flush=none-sync)', done => { - const vm = new Vue({ - setup() { - const a = value(1); - const b = value(1); - watch([a, b], spy, { lazy: true, flush: 'post' }); - - return { - a, - b, - }; - }, - template: `
{{a}} {{b}}
`, - }).$mount(); - vm.a = 2; - expect(spy).not.toHaveBeenCalled(); - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]); - vm.a = 3; - vm.b = 3; - }) - .then(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); - }) - .then(done); - }); - - it('basic usage(multiple sources, lazy=false, flush=sync)', done => { - const vm = new Vue({ - setup() { - const a = value(1); - const b = value(1); - watch([a, b], spy, { lazy: false, flush: 'sync' }); - - return { - a, - b, - }; - }, - }); - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith([1, 1], [undefined, undefined]); - vm.a = 2; - expect(spy.mock.calls.length).toBe(1); - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]); - vm.a = 3; - vm.b = 3; - }) - .then(() => { - expect(spy.mock.calls.length).toBe(3); - expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); - }) - .then(done); - }); - - it('basic usage(multiple sources, lazy=true, flush=sync)', done => { - const vm = new Vue({ - setup() { - const a = value(1); - const b = value(1); - watch([a, b], spy, { lazy: true, flush: 'sync' }); - - return { - a, - b, - }; - }, - }); + expect(spy).toHaveBeenLastCalledWith(1, undefined); vm.a = 2; - expect(spy).not.toHaveBeenCalled(); - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith([2, 1], [1, undefined]); - vm.a = 3; - vm.b = 3; - }) - .then(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); - }) - .then(done); - }); - - it('out of setup', done => { - const obj = state({ a: 1 }); - watch(() => obj.a, spy); expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith(1, undefined); - obj.a = 2; waitForUpdate(() => { expect(spy.mock.calls.length).toBe(2); expect(spy).toHaveBeenLastCalledWith(2, 1); }).then(done); }); - it('out of setup(multiple sources)', done => { - const obj1 = state({ a: 1 }); - const obj2 = state({ a: 2 }); - watch([() => obj1.a, () => obj2.a], spy); - expect(spy.mock.calls.length).toBe(1); - expect(spy).toHaveBeenLastCalledWith([1, 2], [undefined, undefined]); - obj1.a = 2; - obj2.a = 3; - waitForUpdate(() => { - expect(spy.mock.calls.length).toBe(2); - expect(spy).toHaveBeenLastCalledWith([2, 3], [1, 2]); - }).then(done); - }); - it('multiple cbs (after option merge)', done => { const spy1 = jest.fn(); const a = value(1); @@ -269,6 +147,7 @@ describe('Hooks watch', () => { }); it('should flush after render', done => { + let rerenderText; const vm = new Vue({ setup() { const a = value(1); @@ -276,7 +155,7 @@ describe('Hooks watch', () => { a, (newVal, oldVal) => { spy(newVal, oldVal); - expect(vm.$el.textContent).toBe('2'); + rerenderText = vm.$el.textContent; }, { lazy: true } ); @@ -288,8 +167,10 @@ describe('Hooks watch', () => { return h('div', this.a); }, }).$mount(); + expect(spy).not.toHaveBeenCalled(); vm.a = 2; waitForUpdate(() => { + expect(rerenderText).toBe('2'); expect(spy.mock.calls.length).toBe(1); expect(spy).toHaveBeenLastCalledWith(2, 1); }).then(done); @@ -379,4 +260,176 @@ describe('Hooks watch', () => { expect(spy).toHaveBeenNthCalledWith(1, 0, undefined); expect(spy).toHaveBeenNthCalledWith(2, 1, 0); }); + + describe('Multiple sources', () => { + let obj1, obj2; + it('do not store the intermediate state', done => { + new Vue({ + setup() { + obj1 = state({ a: 1 }); + obj2 = state({ a: 2 }); + watch([() => obj1.a, () => obj2.a], spy); + return { + obj1, + obj2, + }; + }, + template: `
{{obj1.a}} {{obj2.a}}
`, + }).$mount(); + expect(spy.mock.calls.length).toBe(1); + expect(spy).toHaveBeenLastCalledWith([1, 2], [undefined, undefined]); + obj1.a = 2; + obj2.a = 3; + + obj1.a = 3; + obj2.a = 4; + waitForUpdate(() => { + expect(spy.mock.calls.length).toBe(2); + expect(spy).toHaveBeenLastCalledWith([3, 4], [1, 2]); + obj2.a = 5; + obj2.a = 6; + }) + .then(() => { + expect(spy.mock.calls.length).toBe(3); + expect(spy).toHaveBeenLastCalledWith([3, 6], [3, 4]); + }) + .then(done); + }); + + it('basic usage(lazy=false, flush=none-sync)', done => { + const vm = new Vue({ + setup() { + const a = value(1); + const b = value(1); + watch([a, b], spy, { lazy: false, flush: 'post' }); + + return { + a, + b, + }; + }, + template: `
{{a}} {{b}}
`, + }).$mount(); + expect(spy.mock.calls.length).toBe(1); + expect(spy).toHaveBeenLastCalledWith([1, 1], [undefined, undefined]); + vm.a = 2; + expect(spy.mock.calls.length).toBe(1); + waitForUpdate(() => { + expect(spy.mock.calls.length).toBe(2); + expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]); + vm.a = 3; + vm.b = 3; + }) + .then(() => { + expect(spy.mock.calls.length).toBe(3); + expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); + }) + .then(done); + }); + + it('basic usage(lazy=true, flush=none-sync)', done => { + const vm = new Vue({ + setup() { + const a = value(1); + const b = value(1); + watch([a, b], spy, { lazy: true, flush: 'post' }); + + return { + a, + b, + }; + }, + template: `
{{a}} {{b}}
`, + }).$mount(); + vm.a = 2; + expect(spy).not.toHaveBeenCalled(); + waitForUpdate(() => { + expect(spy.mock.calls.length).toBe(1); + expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]); + vm.a = 3; + vm.b = 3; + }) + .then(() => { + expect(spy.mock.calls.length).toBe(2); + expect(spy).toHaveBeenLastCalledWith([3, 3], [2, 1]); + }) + .then(done); + }); + + it('basic usage(lazy=false, flush=sync)', () => { + const vm = new Vue({ + setup() { + const a = value(1); + const b = value(1); + watch([a, b], spy, { lazy: false, flush: 'sync' }); + + return { + a, + b, + }; + }, + }); + expect(spy.mock.calls.length).toBe(1); + expect(spy).toHaveBeenLastCalledWith([1, 1], [undefined, undefined]); + vm.a = 2; + expect(spy.mock.calls.length).toBe(2); + expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]); + vm.a = 3; + vm.b = 3; + expect(spy.mock.calls.length).toBe(4); + expect(spy).toHaveBeenNthCalledWith(3, [3, 1], [2, 1]); + expect(spy).toHaveBeenNthCalledWith(4, [3, 3], [3, 1]); + }); + + it('basic usage(lazy=true, flush=sync)', () => { + const vm = new Vue({ + setup() { + const a = value(1); + const b = value(1); + watch([a, b], spy, { lazy: true, flush: 'sync' }); + + return { + a, + b, + }; + }, + }); + expect(spy).not.toHaveBeenCalled(); + vm.a = 2; + expect(spy.mock.calls.length).toBe(1); + expect(spy).toHaveBeenLastCalledWith([2, 1], [1, 1]); + vm.a = 3; + vm.b = 3; + expect(spy.mock.calls.length).toBe(3); + expect(spy).toHaveBeenNthCalledWith(2, [3, 1], [2, 1]); + expect(spy).toHaveBeenNthCalledWith(3, [3, 3], [3, 1]); + }); + }); + + describe('Out of setup', () => { + it('basic', done => { + const obj = state({ a: 1 }); + watch(() => obj.a, spy); + expect(spy).toHaveBeenLastCalledWith(1, undefined); + obj.a = 2; + waitForUpdate(() => { + expect(spy.mock.calls.length).toBe(2); + expect(spy).toHaveBeenLastCalledWith(2, 1); + }).then(done); + }); + + it('multiple sources', done => { + const obj1 = state({ a: 1 }); + const obj2 = state({ a: 2 }); + watch([() => obj1.a, () => obj2.a], spy); + expect(spy.mock.calls.length).toBe(1); + expect(spy).toHaveBeenLastCalledWith([1, 2], [undefined, undefined]); + obj1.a = 2; + obj2.a = 3; + waitForUpdate(() => { + expect(spy.mock.calls.length).toBe(2); + expect(spy).toHaveBeenLastCalledWith([2, 3], [1, 2]); + }).then(done); + }); + }); }); From 609c561870f0725a58c2cb0d8892f844e090e893 Mon Sep 17 00:00:00 2001 From: liximomo Date: Thu, 15 Aug 2019 17:54:19 +0800 Subject: [PATCH 030/551] feat: export "SetupContext" --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 20be93d5..565b568c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import Vue, { VueConstructor } from 'vue'; -import { SetupFunction } from './ts-api'; +import { SetupFunction, SetupContext } from './ts-api'; import { currentVue } from './runtimeContext'; import { Wrapper } from './wrappers'; import { install } from './install'; @@ -22,7 +22,7 @@ if (currentVue && typeof window !== 'undefined' && window.Vue) { } export { default as createElement } from './createElement'; -export { plugin, Wrapper }; +export { plugin, Wrapper, SetupContext }; export { set } from './reactivity'; export { createComponent, PropType } from './ts-api'; export * from './functions/state'; From f5ae2bce234e911bf0c2e971fb39a6bc5033bce8 Mon Sep 17 00:00:00 2001 From: liximomo Date: Thu, 15 Aug 2019 18:26:08 +0800 Subject: [PATCH 031/551] fix: do not convert inject value to reactivity --- src/functions/inject.ts | 4 +--- test/functions/inject.spec.js | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/functions/inject.ts b/src/functions/inject.ts index c61d08ee..32f09ab8 100644 --- a/src/functions/inject.ts +++ b/src/functions/inject.ts @@ -1,6 +1,5 @@ import Vue from 'vue'; import { AnyObject } from '../types/basic'; -import { state } from '../functions/state'; import { isWrapper, Wrapper, ComputedWrapper } from '../wrappers'; import { ensureCurrentVMInFn } from '../helper'; import { hasOwn, warn, isObject } from '../utils'; @@ -48,9 +47,8 @@ export function inject(key: Key | string): Wrapper | void { if (isWrapper(val)) { return val; } - const reactiveVal = state(val); return new ComputedWrapper({ - read: () => reactiveVal, + read: () => val, write() { warn(`The injectd value can't be re-assigned`, vm); }, diff --git a/test/functions/inject.spec.js b/test/functions/inject.spec.js index 825d7e1b..37228b17 100644 --- a/test/functions/inject.spec.js +++ b/test/functions/inject.spec.js @@ -1,5 +1,5 @@ const Vue = require('vue/dist/vue.common.js'); -const { inject, provide, value } = require('../../src'); +const { inject, provide, value, state } = require('../../src'); let injected; const injectedComp = { @@ -81,7 +81,7 @@ describe('Hooks provide/inject', () => { const app = new Vue({ template: ``, setup() { - provide(State, { msg: 'foo' }); + provide(State, state({ msg: 'foo' })); }, components: { child: { From 80ee075b27bee1fccf88b0419b6ffc23655fee53 Mon Sep 17 00:00:00 2001 From: liximomo Date: Thu, 15 Aug 2019 23:48:36 +0800 Subject: [PATCH 032/551] docs: update readme and changelog --- CHANGELOG.md | 7 ++ README.md | 233 +++++++++++++++++++++++++++++---------------------- package.json | 2 +- 3 files changed, 140 insertions(+), 102 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42186a15..686a7935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# 2.2.0 +* Improve typescript support. +* Export `createElement`. +* Export `SetupContext`. +* Support returning a render function from `setup`. +* Allow string keys in `provide`/`inject`. + # 2.1.2 * Remove auto-unwrapping for Array ([#53](https://github.com/vuejs/vue-function-api/issues/53)). diff --git a/README.md b/README.md index 1b475c67..51de5ac0 100644 --- a/README.md +++ b/README.md @@ -19,23 +19,25 @@ - [Single-File Component](#single-file-Component) - [TypeScript](#TypeScript) - [API](#API) - - [setup](#setup) - - [value](#value) - - [state](#state) - - [computed](#computed) - - [watch](#watch) - - [lifecycle](#lifecycle) - - [provide, inject](#provide-inject) + - [setup](#setup) + - [value](#value) + - [state](#state) + - [computed](#computed) + - [watch](#watch) + - [lifecycle](#lifecycle) + - [provide, inject](#provide-inject) - [Misc](#Misc) # Installation **npm** + ```bash npm install vue-function-api --save ``` **yarn** + ```bash yarn add vue-function-api ``` @@ -45,18 +47,18 @@ yarn add vue-function-api ```html ``` -By using the global variable `window.vueFunctionApi` +By using the global variable `window.vueFunctionApi` # Usage You must explicitly install `vue-function-api` via `Vue.use()`: ```js -import Vue from 'vue' -import { plugin } from 'vue-function-api' +import Vue from 'vue'; +import { plugin } from 'vue-function-api'; -Vue.use(plugin) +Vue.use(plugin); ``` After installing the plugin you can use the new [function API](#API) to compose your component. @@ -68,7 +70,8 @@ After installing the plugin you can use the new [function API](#API) to compose ## [CodePen Live Demo](https://codepen.io/liximomo/pen/dBOvgg) ## Single-File Component -``` html + +```html