diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a5353cb1e..8f7b4c8f330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,63 @@ +# [3.0.0-alpha.6](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.5...v3.0.0-alpha.6) (2020-02-22) + + +### Bug Fixes + +* **compiler-core:** should alias name in helperString ([#743](https://github.com/vuejs/vue-next/issues/743)) ([7b987d9](https://github.com/vuejs/vue-next/commit/7b987d9450fc7befcd0946a0d53991d27ed299ec)), closes [#740](https://github.com/vuejs/vue-next/issues/740) +* **compiler-dom:** properly stringify class/style bindings when hoisting static strings ([1b9b235](https://github.com/vuejs/vue-next/commit/1b9b235663b75db040172d2ffbee1dd40b4db032)) +* **reactivity:** should trigger all effects when array length is mutated ([#754](https://github.com/vuejs/vue-next/issues/754)) ([5fac655](https://github.com/vuejs/vue-next/commit/5fac65589b4455b98fd4e2f9eb3754f0acde97bb)) +* **sfc:** inherit parent scopeId on child root ([#756](https://github.com/vuejs/vue-next/issues/756)) ([9547c2b](https://github.com/vuejs/vue-next/commit/9547c2b93d6d8f469314cfe055960746a3e3acbe)) +* **types:** improve ref typing, close [#759](https://github.com/vuejs/vue-next/issues/759) ([627b9df](https://github.com/vuejs/vue-next/commit/627b9df4a293ae18071009d9cac7a5e995d40716)) +* **types:** update setup binding unwrap types for 6b10f0c ([a840e7d](https://github.com/vuejs/vue-next/commit/a840e7ddf0b470b5da27b7b2b8b5fcf39a7197a2)), closes [#738](https://github.com/vuejs/vue-next/issues/738) + + +### Code Refactoring + +* preserve refs in reactive arrays ([775a7c2](https://github.com/vuejs/vue-next/commit/775a7c2b414ca44d4684badb29e8e80ff6b5d3dd)), closes [#737](https://github.com/vuejs/vue-next/issues/737) + + +### Features + +* **reactivity:** expose unref and shallowRef ([e9024bf](https://github.com/vuejs/vue-next/commit/e9024bf1b7456b9cf9b913c239502593364bc773)) +* **runtime-core:** add watchEffect API ([99a2e18](https://github.com/vuejs/vue-next/commit/99a2e18c9711d3d1f79f8c9c59212880efd058b9)) + + +### Performance Improvements + +* **effect:** optimize effect trigger for array length mutation ([#761](https://github.com/vuejs/vue-next/issues/761)) ([76c7f54](https://github.com/vuejs/vue-next/commit/76c7f5426919f9d29a303263bc54a1e42a66e94b)) +* **reactivity:** only trigger all effects on Array length mutation if new length is shorter than old length ([33622d6](https://github.com/vuejs/vue-next/commit/33622d63600ba0f18ba4dae97bda882c918b5f7d)) + + +### BREAKING CHANGES + +* **runtime-core:** replace `watch(fn, options?)` with `watchEffect` + + The `watch(fn, options?)` signature has been replaced by the new + `watchEffect` API, which has the same usage and behavior. `watch` + now only supports the `watch(source, cb, options?)` signature. + +* **reactivity:** reactive arrays no longer unwraps contained refs + + When reactive arrays contain refs, especially a mix of refs and + plain values, Array prototype methods will fail to function + properly - e.g. sort() or reverse() will overwrite the ref's value + instead of moving it (see #737). + + Ensuring correct behavior for all possible Array methods while + retaining the ref unwrapping behavior is exceedingly complicated; In + addition, even if Vue handles the built-in methods internally, it + would still break when the user attempts to use a 3rd party utility + function (e.g. lodash) on a reactive array containing refs. + + After this commit, similar to other collection types like Map and + Set, Arrays will no longer automatically unwrap contained refs. + + The usage of mixed refs and plain values in Arrays should be rare in + practice. In cases where this is necessary, the user can create a + computed property that performs the unwrapping. + + + # [3.0.0-alpha.5](https://github.com/vuejs/vue-next/compare/v3.0.0-alpha.4...v3.0.0-alpha.5) (2020-02-18) @@ -40,7 +100,7 @@ ### Code Refactoring -* **watch:** adjsut watch API behavior ([9571ede](https://github.com/vuejs/vue-next/commit/9571ede84bb6949e13c25807cc8f016ace29dc8a)) +* **watch:** adjust watch API behavior ([9571ede](https://github.com/vuejs/vue-next/commit/9571ede84bb6949e13c25807cc8f016ace29dc8a)) ### Features @@ -85,7 +145,7 @@ behaves exactly the same as 2.x. - When using the effect signature or `{ immediate: true }`, the - intital execution is now performed synchronously instead of + initial execution is now performed synchronously instead of deferred until the component is mounted. This is necessary for certain use cases to work properly with `async setup()` and Suspense. diff --git a/package.json b/package.json index 967c8b9d54e..2d3d9325da4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "workspaces": [ "packages/*" ], diff --git a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts index b62a27a7557..cc3cbd480a5 100644 --- a/packages/compiler-core/__tests__/transforms/transformElement.spec.ts +++ b/packages/compiler-core/__tests__/transforms/transformElement.spec.ts @@ -623,7 +623,7 @@ describe('compiler: element transform', () => { test(`props merging: style`, () => { const { node } = parseWithElementTransform( - `
`, + `
`, { nodeTransforms: [transformStyle, transformElement], directiveTransforms: { @@ -646,7 +646,7 @@ describe('compiler: element transform', () => { elements: [ { type: NodeTypes.SIMPLE_EXPRESSION, - content: `_hoisted_1`, + content: `{"color":"green"}`, isStatic: false }, { diff --git a/packages/compiler-core/package.json b/packages/compiler-core/package.json index e39ddd43400..141a0a96cfd 100644 --- a/packages/compiler-core/package.json +++ b/packages/compiler-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-core", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "description": "@vue/compiler-core", "main": "index.js", "module": "dist/compiler-core.esm-bundler.js", diff --git a/packages/compiler-core/src/parse.ts b/packages/compiler-core/src/parse.ts index f856d06027d..dbadeff771b 100644 --- a/packages/compiler-core/src/parse.ts +++ b/packages/compiler-core/src/parse.ts @@ -694,7 +694,7 @@ function parseAttributeValue( if (!match) { return undefined } - let unexpectedChars = /["'<=`]/g + const unexpectedChars = /["'<=`]/g let m: RegExpExecArray | null while ((m = unexpectedChars.exec(match[0])) !== null) { emitError( diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index a25691643cf..3d2afb6caa4 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -126,9 +126,9 @@ export function isStaticNode( return false } } - // only svg/foeignObject could be block here, however if they are static + // only svg/foreignObject could be block here, however if they are static // then they don't need to be blocks since there will be no nested - // udpates. + // updates. if (codegenNode.isBlock) { codegenNode.isBlock = false } diff --git a/packages/compiler-dom/__tests__/__snapshots__/index.spec.ts.snap b/packages/compiler-dom/__tests__/__snapshots__/index.spec.ts.snap index b429387fd6d..40e14d51f18 100644 --- a/packages/compiler-dom/__tests__/__snapshots__/index.spec.ts.snap +++ b/packages/compiler-dom/__tests__/__snapshots__/index.spec.ts.snap @@ -2,9 +2,6 @@ exports[`compile should contain standard transforms 1`] = ` "const _Vue = Vue -const { createVNode: _createVNode } = _Vue - -const _hoisted_1 = {} return function render(_ctx, _cache) { with (_ctx) { @@ -14,7 +11,7 @@ return function render(_ctx, _cache) { _createVNode(\\"div\\", { textContent: text }, null, 8 /* PROPS */, [\\"textContent\\"]), _createVNode(\\"div\\", { innerHTML: html }, null, 8 /* PROPS */, [\\"innerHTML\\"]), _createVNode(\\"div\\", null, \\"test\\"), - _createVNode(\\"div\\", { style: _hoisted_1 }, \\"red\\"), + _createVNode(\\"div\\", { style: {\\"color\\":\\"red\\"} }, \\"red\\"), _createVNode(\\"div\\", { style: {color: 'green'} }, null, 4 /* STYLE */) ], 64 /* STABLE_FRAGMENT */)) } diff --git a/packages/compiler-dom/__tests__/index.spec.ts b/packages/compiler-dom/__tests__/index.spec.ts index d40bf36fe2c..db43ce905cf 100644 --- a/packages/compiler-dom/__tests__/index.spec.ts +++ b/packages/compiler-dom/__tests__/index.spec.ts @@ -5,7 +5,7 @@ describe('compile', () => { const { code } = compile(`
test
-
red
+
red
`) expect(code).toMatchSnapshot() diff --git a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts index d1095d3f425..cc8b53f245d 100644 --- a/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/stringifyStatic.spec.ts @@ -77,8 +77,8 @@ describe('stringify static html', () => { test('serliazing constant bindings', () => { const { ast } = compileWithStringify( - `
${repeat( - `{{ 1 }} + {{ false }}`, + `
${repeat( + `{{ 1 }} + {{ false }}`, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT )}
` ) @@ -89,8 +89,8 @@ describe('stringify static html', () => { callee: CREATE_STATIC, arguments: [ JSON.stringify( - `
${repeat( - `1 + false`, + `
${repeat( + `1 + false`, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT )}
` ) diff --git a/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts b/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts index 89121c10a26..6eee9f8ba38 100644 --- a/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts +++ b/packages/compiler-dom/__tests__/transforms/transformStyle.spec.ts @@ -26,17 +26,8 @@ function transformWithStyleTransform( } describe('compiler: style transform', () => { - test('should transform into directive node and hoist value', () => { - const { root, node } = transformWithStyleTransform( - `
` - ) - expect(root.hoists).toMatchObject([ - { - type: NodeTypes.SIMPLE_EXPRESSION, - content: `{"color":"red"}`, - isStatic: false - } - ]) + test('should transform into directive node', () => { + const { node } = transformWithStyleTransform(`
`) expect(node.props[0]).toMatchObject({ type: NodeTypes.DIRECTIVE, name: `bind`, @@ -47,7 +38,7 @@ describe('compiler: style transform', () => { }, exp: { type: NodeTypes.SIMPLE_EXPRESSION, - content: `_hoisted_1`, + content: `{"color":"red"}`, isStatic: false } }) @@ -71,7 +62,7 @@ describe('compiler: style transform', () => { }, value: { type: NodeTypes.SIMPLE_EXPRESSION, - content: `_hoisted_1`, + content: `{"color":"red"}`, isStatic: false } } diff --git a/packages/compiler-dom/package.json b/packages/compiler-dom/package.json index 380f71148fd..2820c7c1c71 100644 --- a/packages/compiler-dom/package.json +++ b/packages/compiler-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-dom", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "description": "@vue/compiler-dom", "main": "index.js", "module": "dist/compiler-dom.esm-bundler.js", @@ -34,6 +34,6 @@ }, "homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-dom#readme", "dependencies": { - "@vue/compiler-core": "3.0.0-alpha.5" + "@vue/compiler-core": "3.0.0-alpha.6" } } diff --git a/packages/compiler-dom/src/transforms/stringifyStatic.ts b/packages/compiler-dom/src/transforms/stringifyStatic.ts index a2efa56eec0..a4aa7745f8e 100644 --- a/packages/compiler-dom/src/transforms/stringifyStatic.ts +++ b/packages/compiler-dom/src/transforms/stringifyStatic.ts @@ -14,7 +14,10 @@ import { isString, isSymbol, escapeHtml, - toDisplayString + toDisplayString, + normalizeClass, + normalizeStyle, + stringifyStyle } from '@vue/shared' // Turn eligible hoisted static trees into stringied static nodes, e.g. @@ -84,8 +87,15 @@ function stringifyElement( } } else if (p.type === NodeTypes.DIRECTIVE && p.name === 'bind') { // constant v-bind, e.g. :foo="1" + let evaluated = evaluateConstant(p.exp as SimpleExpressionNode) + const arg = p.arg && (p.arg as SimpleExpressionNode).content + if (arg === 'class') { + evaluated = normalizeClass(evaluated) + } else if (arg === 'style') { + evaluated = stringifyStyle(normalizeStyle(evaluated)) + } res += ` ${(p.arg as SimpleExpressionNode).content}="${escapeHtml( - evaluateConstant(p.exp as ExpressionNode) + evaluated )}"` } } @@ -151,7 +161,7 @@ function evaluateConstant(exp: ExpressionNode): string { if (c.type === NodeTypes.TEXT) { res += c.content } else if (c.type === NodeTypes.INTERPOLATION) { - res += evaluateConstant(c.content) + res += toDisplayString(evaluateConstant(c.content)) } else { res += evaluateConstant(c) } diff --git a/packages/compiler-dom/src/transforms/transformStyle.ts b/packages/compiler-dom/src/transforms/transformStyle.ts index 3c232db4357..765fab778e9 100644 --- a/packages/compiler-dom/src/transforms/transformStyle.ts +++ b/packages/compiler-dom/src/transforms/transformStyle.ts @@ -17,12 +17,11 @@ export const transformStyle: NodeTransform = (node, context) => { node.props.forEach((p, i) => { if (p.type === NodeTypes.ATTRIBUTE && p.name === 'style' && p.value) { // replace p with an expression node - const exp = context.hoist(parseInlineCSS(p.value.content, p.loc)) node.props[i] = { type: NodeTypes.DIRECTIVE, name: `bind`, arg: createSimpleExpression(`style`, true, p.loc), - exp, + exp: parseInlineCSS(p.value.content, p.loc), modifiers: [], loc: p.loc } @@ -45,5 +44,5 @@ function parseInlineCSS( tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim()) } }) - return createSimpleExpression(JSON.stringify(res), false, loc) + return createSimpleExpression(JSON.stringify(res), false, loc, true) } diff --git a/packages/compiler-sfc/package.json b/packages/compiler-sfc/package.json index 9bafa05aff0..71fdc411500 100644 --- a/packages/compiler-sfc/package.json +++ b/packages/compiler-sfc/package.json @@ -1,6 +1,6 @@ { "name": "@vue/compiler-sfc", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "description": "@vue/compiler-sfc", "main": "dist/compiler-sfc.cjs.js", "types": "dist/compiler-sfc.d.ts", @@ -27,11 +27,11 @@ }, "homepage": "https://github.com/vuejs/vue/tree/dev/packages/compiler-sfc#readme", "peerDependencies": { - "vue": "3.0.0-alpha.5" + "vue": "3.0.0-alpha.6" }, "dependencies": { - "@vue/compiler-core": "3.0.0-alpha.5", - "@vue/compiler-dom": "3.0.0-alpha.5", + "@vue/compiler-core": "3.0.0-alpha.6", + "@vue/compiler-dom": "3.0.0-alpha.6", "consolidate": "^0.15.1", "hash-sum": "^2.0.0", "lru-cache": "^5.1.1", diff --git a/packages/compiler-ssr/__tests__/ssrElement.spec.ts b/packages/compiler-ssr/__tests__/ssrElement.spec.ts index c99d13c543a..c81d6ccb10e 100644 --- a/packages/compiler-ssr/__tests__/ssrElement.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrElement.spec.ts @@ -101,7 +101,7 @@ describe('ssr: element', () => { expect( getCompiledString(`
`) ).toMatchInlineSnapshot( - `"\`
\`"` + `"\`
\`"` ) }) @@ -184,7 +184,7 @@ describe('ssr: element', () => { ) ).toMatchInlineSnapshot(` "\`
\`" `) }) diff --git a/packages/compiler-ssr/__tests__/ssrVShow.spec.ts b/packages/compiler-ssr/__tests__/ssrVShow.spec.ts index 2738bc22617..8a99a52c7d3 100644 --- a/packages/compiler-ssr/__tests__/ssrVShow.spec.ts +++ b/packages/compiler-ssr/__tests__/ssrVShow.spec.ts @@ -16,11 +16,9 @@ describe('ssr: v-show', () => { .toMatchInlineSnapshot(` "const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\") - const _hoisted_1 = {\\"color\\":\\"red\\"} - return function ssrRender(_ctx, _push, _parent) { _push(\`
\`) }" @@ -48,11 +46,9 @@ describe('ssr: v-show', () => { ).toMatchInlineSnapshot(` "const { ssrRenderStyle: _ssrRenderStyle } = require(\\"@vue/server-renderer\\") - const _hoisted_1 = {\\"color\\":\\"red\\"} - return function ssrRender(_ctx, _push, _parent) { _push(\`
\`) @@ -69,12 +65,10 @@ describe('ssr: v-show', () => { "const { mergeProps: _mergeProps } = require(\\"vue\\") const { ssrRenderAttrs: _ssrRenderAttrs } = require(\\"@vue/server-renderer\\") - const _hoisted_1 = {\\"color\\":\\"red\\"} - return function ssrRender(_ctx, _push, _parent) { _push(\` { expect(fnSpy).toHaveBeenCalledTimes(1) }) + it('should trigger all effects when array lenght is set 0', () => { + const observed: any = reactive([1]) + let dummy, record + effect(() => { + dummy = observed.length + }) + effect(() => { + record = observed[0] + }) + expect(dummy).toBe(1) + expect(record).toBe(1) + + observed[1] = 2 + expect(observed[1]).toBe(2) + + observed.unshift(3) + expect(dummy).toBe(3) + expect(record).toBe(3) + + observed.length = 0 + expect(dummy).toBe(0) + expect(record).toBeUndefined() + }) + it('should handle self dependency mutations', () => { const count = ref(0) effect(() => { diff --git a/packages/reactivity/__tests__/reactive.spec.ts b/packages/reactivity/__tests__/reactive.spec.ts index 1b18dc8055a..5d8876843b1 100644 --- a/packages/reactivity/__tests__/reactive.spec.ts +++ b/packages/reactivity/__tests__/reactive.spec.ts @@ -26,51 +26,6 @@ describe('reactivity/reactive', () => { expect(Object.keys(observed)).toEqual(['foo']) }) - test('Array', () => { - const original = [{ foo: 1 }] - const observed = reactive(original) - expect(observed).not.toBe(original) - expect(isReactive(observed)).toBe(true) - expect(isReactive(original)).toBe(false) - expect(isReactive(observed[0])).toBe(true) - // get - expect(observed[0].foo).toBe(1) - // has - expect(0 in observed).toBe(true) - // ownKeys - expect(Object.keys(observed)).toEqual(['0']) - }) - - test('cloned reactive Array should point to observed values', () => { - const original = [{ foo: 1 }] - const observed = reactive(original) - const clone = observed.slice() - expect(isReactive(clone[0])).toBe(true) - expect(clone[0]).not.toBe(original[0]) - expect(clone[0]).toBe(observed[0]) - }) - - test('Array identity methods should work with raw values', () => { - const raw = {} - const arr = reactive([{}, {}]) - arr.push(raw) - expect(arr.indexOf(raw)).toBe(2) - expect(arr.indexOf(raw, 3)).toBe(-1) - expect(arr.includes(raw)).toBe(true) - expect(arr.includes(raw, 3)).toBe(false) - expect(arr.lastIndexOf(raw)).toBe(2) - expect(arr.lastIndexOf(raw, 1)).toBe(-1) - - // should work also for the observed version - const observed = arr[2] - expect(arr.indexOf(observed)).toBe(2) - expect(arr.indexOf(observed, 3)).toBe(-1) - expect(arr.includes(observed)).toBe(true) - expect(arr.includes(observed, 3)).toBe(false) - expect(arr.lastIndexOf(observed)).toBe(2) - expect(arr.lastIndexOf(observed, 1)).toBe(-1) - }) - test('nested reactives', () => { const original = { nested: { @@ -97,25 +52,6 @@ describe('reactivity/reactive', () => { expect('foo' in original).toBe(false) }) - test('observed value should proxy mutations to original (Array)', () => { - const original: any[] = [{ foo: 1 }, { bar: 2 }] - const observed = reactive(original) - // set - const value = { baz: 3 } - const reactiveValue = reactive(value) - observed[0] = value - expect(observed[0]).toBe(reactiveValue) - expect(original[0]).toBe(value) - // delete - delete observed[0] - expect(observed[0]).toBeUndefined() - expect(original[0]).toBeUndefined() - // mutating methods - observed.push(value) - expect(observed[2]).toBe(reactiveValue) - expect(original[2]).toBe(value) - }) - test('setting a property with an unobserved value should wrap with reactive', () => { const observed = reactive<{ foo?: object }>({}) const raw = {} diff --git a/packages/reactivity/__tests__/reactiveArray.spec.ts b/packages/reactivity/__tests__/reactiveArray.spec.ts new file mode 100644 index 00000000000..b61116e28b1 --- /dev/null +++ b/packages/reactivity/__tests__/reactiveArray.spec.ts @@ -0,0 +1,111 @@ +import { reactive, isReactive, toRaw } from '../src/reactive' +import { ref, isRef } from '../src/ref' +import { effect } from '../src/effect' + +describe('reactivity/reactive/Array', () => { + test('should make Array reactive', () => { + const original = [{ foo: 1 }] + const observed = reactive(original) + expect(observed).not.toBe(original) + expect(isReactive(observed)).toBe(true) + expect(isReactive(original)).toBe(false) + expect(isReactive(observed[0])).toBe(true) + // get + expect(observed[0].foo).toBe(1) + // has + expect(0 in observed).toBe(true) + // ownKeys + expect(Object.keys(observed)).toEqual(['0']) + }) + + test('cloned reactive Array should point to observed values', () => { + const original = [{ foo: 1 }] + const observed = reactive(original) + const clone = observed.slice() + expect(isReactive(clone[0])).toBe(true) + expect(clone[0]).not.toBe(original[0]) + expect(clone[0]).toBe(observed[0]) + }) + + test('observed value should proxy mutations to original (Array)', () => { + const original: any[] = [{ foo: 1 }, { bar: 2 }] + const observed = reactive(original) + // set + const value = { baz: 3 } + const reactiveValue = reactive(value) + observed[0] = value + expect(observed[0]).toBe(reactiveValue) + expect(original[0]).toBe(value) + // delete + delete observed[0] + expect(observed[0]).toBeUndefined() + expect(original[0]).toBeUndefined() + // mutating methods + observed.push(value) + expect(observed[2]).toBe(reactiveValue) + expect(original[2]).toBe(value) + }) + + test('Array identity methods should work with raw values', () => { + const raw = {} + const arr = reactive([{}, {}]) + arr.push(raw) + expect(arr.indexOf(raw)).toBe(2) + expect(arr.indexOf(raw, 3)).toBe(-1) + expect(arr.includes(raw)).toBe(true) + expect(arr.includes(raw, 3)).toBe(false) + expect(arr.lastIndexOf(raw)).toBe(2) + expect(arr.lastIndexOf(raw, 1)).toBe(-1) + + // should work also for the observed version + const observed = arr[2] + expect(arr.indexOf(observed)).toBe(2) + expect(arr.indexOf(observed, 3)).toBe(-1) + expect(arr.includes(observed)).toBe(true) + expect(arr.includes(observed, 3)).toBe(false) + expect(arr.lastIndexOf(observed)).toBe(2) + expect(arr.lastIndexOf(observed, 1)).toBe(-1) + }) + + test('Array identity methods should be reactive', () => { + const obj = {} + const arr = reactive([obj, {}]) + + let index: number = -1 + effect(() => { + index = arr.indexOf(obj) + }) + expect(index).toBe(0) + arr.reverse() + expect(index).toBe(1) + }) + + describe('Array methods w/ refs', () => { + let original: any[] + beforeEach(() => { + original = reactive([1, ref(2)]) + }) + + // read + copy + test('read only copy methods', () => { + const res = original.concat([3, ref(4)]) + const raw = toRaw(res) + expect(isRef(raw[1])).toBe(true) + expect(isRef(raw[3])).toBe(true) + }) + + // read + write + test('read + write mutating methods', () => { + const res = original.copyWithin(0, 1, 2) + const raw = toRaw(res) + expect(isRef(raw[0])).toBe(true) + expect(isRef(raw[1])).toBe(true) + }) + + test('read + indentity', () => { + const ref = original[1] + expect(ref).toBe(toRaw(original)[1]) + expect(original.indexOf(ref)).toBe(1) + }) + }) +}) diff --git a/packages/reactivity/__tests__/ref.spec.ts b/packages/reactivity/__tests__/ref.spec.ts index b822e801bbf..6620f7fd9ce 100644 --- a/packages/reactivity/__tests__/ref.spec.ts +++ b/packages/reactivity/__tests__/ref.spec.ts @@ -1,5 +1,14 @@ -import { ref, effect, reactive, isRef, toRefs, Ref } from '../src/index' +import { + ref, + effect, + reactive, + isRef, + toRefs, + Ref, + isReactive +} from '../src/index' import { computed } from '@vue/runtime-dom' +import { shallowRef, unref } from '../src/ref' describe('reactivity/ref', () => { it('should hold a value', () => { @@ -33,28 +42,36 @@ describe('reactivity/ref', () => { expect(dummy).toBe(2) }) + it('should work without initial value', () => { + const a = ref() + let dummy + effect(() => { + dummy = a.value + }) + expect(dummy).toBe(undefined) + a.value = 2 + expect(dummy).toBe(2) + }) + it('should work like a normal property when nested in a reactive object', () => { const a = ref(1) const obj = reactive({ a, b: { - c: a, - d: [a] + c: a } }) let dummy1: number let dummy2: number - let dummy3: number effect(() => { dummy1 = obj.a dummy2 = obj.b.c - dummy3 = obj.b.d[0] }) const assertDummiesEqualTo = (val: number) => - [dummy1, dummy2, dummy3].forEach(dummy => expect(dummy).toBe(val)) + [dummy1, dummy2].forEach(dummy => expect(dummy).toBe(val)) assertDummiesEqualTo(1) a.value++ @@ -63,8 +80,6 @@ describe('reactivity/ref', () => { assertDummiesEqualTo(3) obj.b.c++ assertDummiesEqualTo(4) - obj.b.d[0]++ - assertDummiesEqualTo(5) }) it('should unwrap nested ref in types', () => { @@ -84,15 +99,14 @@ describe('reactivity/ref', () => { expect(typeof (c.value.b + 1)).toBe('number') }) - it('should properly unwrap ref types nested inside arrays', () => { + it('should NOT unwrap ref types nested inside arrays', () => { const arr = ref([1, ref(1)]).value - // should unwrap to number[] - arr[0]++ - arr[1]++ + ;(arr[0] as number)++ + ;(arr[1] as Ref).value++ const arr2 = ref([1, new Map(), ref('1')]).value const value = arr2[0] - if (typeof value === 'string') { + if (isRef(value)) { value + 'foo' } else if (typeof value === 'number') { value + 1 @@ -120,8 +134,28 @@ describe('reactivity/ref', () => { tupleRef.value[2].a++ expect(tupleRef.value[2].a).toBe(2) expect(tupleRef.value[3]()).toBe(0) - tupleRef.value[4]++ - expect(tupleRef.value[4]).toBe(1) + tupleRef.value[4].value++ + expect(tupleRef.value[4].value).toBe(1) + }) + + test('unref', () => { + expect(unref(1)).toBe(1) + expect(unref(ref(1))).toBe(1) + }) + + test('shallowRef', () => { + const sref = shallowRef({ a: 1 }) + expect(isReactive(sref.value)).toBe(false) + + let dummy + effect(() => { + dummy = sref.value.a + }) + expect(dummy).toBe(1) + + sref.value = { a: 2 } + expect(isReactive(sref.value)).toBe(false) + expect(dummy).toBe(2) }) test('isRef', () => { diff --git a/packages/reactivity/package.json b/packages/reactivity/package.json index f7c36b0b492..59ada495c01 100644 --- a/packages/reactivity/package.json +++ b/packages/reactivity/package.json @@ -1,6 +1,6 @@ { "name": "@vue/reactivity", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "description": "@vue/reactivity", "main": "index.js", "module": "dist/reactivity.esm-bundler.js", diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 43b6df903bd..0aaeade83e4 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/packages/reactivity/src/baseHandlers.ts @@ -16,20 +16,21 @@ const shallowReactiveGet = /*#__PURE__*/ createGetter(false, true) const readonlyGet = /*#__PURE__*/ createGetter(true) const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true) -const arrayIdentityInstrumentations: Record = {} +const arrayInstrumentations: Record = {} ;['includes', 'indexOf', 'lastIndexOf'].forEach(key => { - arrayIdentityInstrumentations[key] = function( - value: unknown, - ...args: any[] - ): any { - return toRaw(this)[key](toRaw(value), ...args) + arrayInstrumentations[key] = function(...args: any[]): any { + const arr = toRaw(this) as any + for (let i = 0, l = (this as any).length; i < l; i++) { + track(arr, TrackOpTypes.GET, i + '') + } + return arr[key](...args.map(toRaw)) } }) function createGetter(isReadonly = false, shallow = false) { return function get(target: object, key: string | symbol, receiver: object) { - if (isArray(target) && hasOwn(arrayIdentityInstrumentations, key)) { - return Reflect.get(arrayIdentityInstrumentations, key, receiver) + if (isArray(target) && hasOwn(arrayInstrumentations, key)) { + return Reflect.get(arrayInstrumentations, key, receiver) } const res = Reflect.get(target, key, receiver) if (isSymbol(key) && builtInSymbols.has(key)) { @@ -40,7 +41,8 @@ function createGetter(isReadonly = false, shallow = false) { // TODO strict mode that returns a shallow-readonly version of the value return res } - if (isRef(res)) { + // ref unwrapping, only for Objects, not for Arrays. + if (isRef(res) && !isArray(target)) { return res.value } track(target, TrackOpTypes.GET, key) @@ -79,7 +81,7 @@ function createSetter(isReadonly = false, shallow = false) { const oldValue = (target as any)[key] if (!shallow) { value = toRaw(value) - if (isRef(oldValue) && !isRef(value)) { + if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } @@ -91,20 +93,10 @@ function createSetter(isReadonly = false, shallow = false) { const result = Reflect.set(target, key, value, receiver) // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { - /* istanbul ignore else */ - if (__DEV__) { - const extraInfo = { oldValue, newValue: value } - if (!hadKey) { - trigger(target, TriggerOpTypes.ADD, key, extraInfo) - } else if (hasChanged(value, oldValue)) { - trigger(target, TriggerOpTypes.SET, key, extraInfo) - } - } else { - if (!hadKey) { - trigger(target, TriggerOpTypes.ADD, key) - } else if (hasChanged(value, oldValue)) { - trigger(target, TriggerOpTypes.SET, key) - } + if (!hadKey) { + trigger(target, TriggerOpTypes.ADD, key, value) + } else if (hasChanged(value, oldValue)) { + trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result @@ -116,12 +108,7 @@ function deleteProperty(target: object, key: string | symbol): boolean { const oldValue = (target as any)[key] const result = Reflect.deleteProperty(target, key) if (result && hadKey) { - /* istanbul ignore else */ - if (__DEV__) { - trigger(target, TriggerOpTypes.DELETE, key, { oldValue }) - } else { - trigger(target, TriggerOpTypes.DELETE, key) - } + trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) } return result } diff --git a/packages/reactivity/src/collectionHandlers.ts b/packages/reactivity/src/collectionHandlers.ts index cb90cf0e3c2..af4ca05d17f 100644 --- a/packages/reactivity/src/collectionHandlers.ts +++ b/packages/reactivity/src/collectionHandlers.ts @@ -51,12 +51,7 @@ function add(this: SetTypes, value: unknown) { const hadKey = proto.has.call(target, value) const result = proto.add.call(target, value) if (!hadKey) { - /* istanbul ignore else */ - if (__DEV__) { - trigger(target, TriggerOpTypes.ADD, value, { newValue: value }) - } else { - trigger(target, TriggerOpTypes.ADD, value) - } + trigger(target, TriggerOpTypes.ADD, value, value) } return result } @@ -69,20 +64,10 @@ function set(this: MapTypes, key: unknown, value: unknown) { const hadKey = proto.has.call(target, key) const oldValue = proto.get.call(target, key) const result = proto.set.call(target, key, value) - /* istanbul ignore else */ - if (__DEV__) { - const extraInfo = { oldValue, newValue: value } - if (!hadKey) { - trigger(target, TriggerOpTypes.ADD, key, extraInfo) - } else if (hasChanged(value, oldValue)) { - trigger(target, TriggerOpTypes.SET, key, extraInfo) - } - } else { - if (!hadKey) { - trigger(target, TriggerOpTypes.ADD, key) - } else if (hasChanged(value, oldValue)) { - trigger(target, TriggerOpTypes.SET, key) - } + if (!hadKey) { + trigger(target, TriggerOpTypes.ADD, key, value) + } else if (hasChanged(value, oldValue)) { + trigger(target, TriggerOpTypes.SET, key, value, oldValue) } return result } @@ -96,12 +81,7 @@ function deleteEntry(this: CollectionTypes, key: unknown) { // forward the operation before queueing reactions const result = proto.delete.call(target, key) if (hadKey) { - /* istanbul ignore else */ - if (__DEV__) { - trigger(target, TriggerOpTypes.DELETE, key, { oldValue }) - } else { - trigger(target, TriggerOpTypes.DELETE, key) - } + trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue) } return result } @@ -117,12 +97,7 @@ function clear(this: IterableCollections) { // forward the operation before queueing reactions const result = getProto(target).clear.call(target) if (hadItems) { - /* istanbul ignore else */ - if (__DEV__) { - trigger(target, TriggerOpTypes.CLEAR, void 0, { oldTarget }) - } else { - trigger(target, TriggerOpTypes.CLEAR) - } + trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget) } return result } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index f4cac6d4edc..010aea0020a 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -165,7 +165,9 @@ export function trigger( target: object, type: TriggerOpTypes, key?: unknown, - extraInfo?: DebuggerEventExtraInfo + newValue?: unknown, + oldValue?: unknown, + oldTarget?: Map | Set ) { const depsMap = targetMap.get(target) if (depsMap === void 0) { @@ -175,10 +177,17 @@ export function trigger( const effects = new Set() const computedRunners = new Set() if (type === TriggerOpTypes.CLEAR) { - // collection being cleared, trigger all effects for target + // collection being cleared + // trigger all effects for target depsMap.forEach(dep => { addRunners(effects, computedRunners, dep) }) + } else if (key === 'length' && isArray(target)) { + depsMap.forEach((dep, key) => { + if (key === 'length' || key >= (newValue as number)) { + addRunners(effects, computedRunners, dep) + } + }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { @@ -195,7 +204,19 @@ export function trigger( } } const run = (effect: ReactiveEffect) => { - scheduleRun(effect, target, type, key, extraInfo) + scheduleRun( + effect, + target, + type, + key, + __DEV__ + ? { + newValue, + oldValue, + oldTarget + } + : undefined + ) } // Important: computed effects must be run first so that computed getters // can be invalidated before any normal effects that depend on them are run. diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 040dd8c673d..21a9eae8f8c 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -1,4 +1,4 @@ -export { ref, isRef, toRefs, Ref, UnwrapRef } from './ref' +export { ref, unref, shallowRef, isRef, toRefs, Ref, UnwrapRef } from './ref' export { reactive, isReactive, diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index f97a1e6f2d4..5f8aa579725 100644 --- a/packages/reactivity/src/reactive.ts +++ b/packages/reactivity/src/reactive.ts @@ -9,7 +9,7 @@ import { mutableCollectionHandlers, readonlyCollectionHandlers } from './collectionHandlers' -import { UnwrapRef, Ref } from './ref' +import { UnwrapRef, Ref, isRef } from './ref' import { makeMap } from '@vue/shared' // WeakMaps that store {raw <-> observed} pairs. @@ -50,6 +50,9 @@ export function reactive(target: object) { if (readonlyValues.has(target)) { return readonly(target) } + if (isRef(target)) { + return target + } return createReactiveObject( target, rawToReactive, diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 65e1b8dd016..f88631130e7 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -23,19 +23,30 @@ export interface Ref { const convert = (val: T): T => isObject(val) ? reactive(val) : val -export function isRef(r: Ref | T): r is Ref +export function isRef(r: Ref | unknown): r is Ref export function isRef(r: any): r is Ref { return r ? r._isRef === true : false } -export function ref(value: T): T -export function ref(value: T): Ref +export function ref(value: T): T extends Ref ? T : Ref export function ref(): Ref export function ref(value?: unknown) { + return createRef(value) +} + +export function shallowRef(value: T): T extends Ref ? T : Ref +export function shallowRef(): Ref +export function shallowRef(value?: unknown) { + return createRef(value, true) +} + +function createRef(value: unknown, shallow = false) { if (isRef(value)) { return value } - value = convert(value) + if (!shallow) { + value = convert(value) + } const r = { _isRef: true, get value() { @@ -43,7 +54,7 @@ export function ref(value?: unknown) { return value }, set value(newVal) { - value = convert(newVal) + value = shallow ? newVal : convert(newVal) trigger( r, TriggerOpTypes.SET, @@ -55,6 +66,10 @@ export function ref(value?: unknown) { return r } +export function unref(ref: T): T extends Ref ? V : T { + return isRef(ref) ? (ref.value as any) : ref +} + export function toRefs( object: T ): { [K in keyof T]: Ref } { @@ -83,8 +98,6 @@ function toProxyRef( } as any } -type UnwrapArray = { [P in keyof T]: UnwrapRef } - // corner case when use narrows type // Ex. type RelativePath = string & { __brand: unknown } // RelativePath extends object -> true @@ -94,7 +107,7 @@ type BaseTypes = string | number | boolean export type UnwrapRef = { cRef: T extends ComputedRef ? UnwrapRef : T ref: T extends Ref ? UnwrapRef : T - array: T extends Array ? Array> & UnwrapArray : T + array: T object: { [K in keyof T]: UnwrapRef } }[T extends ComputedRef ? 'cRef' diff --git a/packages/runtime-core/__tests__/apiSetupContext.spec.ts b/packages/runtime-core/__tests__/apiSetupContext.spec.ts index bc3c2065034..8f7ab078bbe 100644 --- a/packages/runtime-core/__tests__/apiSetupContext.spec.ts +++ b/packages/runtime-core/__tests__/apiSetupContext.spec.ts @@ -6,7 +6,7 @@ import { render, serializeInner, nextTick, - watch, + watchEffect, defineComponent, triggerEvent, TestElement @@ -55,7 +55,7 @@ describe('api: setup context', () => { const Child = defineComponent({ setup(props: { count: number }) { - watch(() => { + watchEffect(() => { dummy = props.count }) return () => h('div', props.count) @@ -88,7 +88,7 @@ describe('api: setup context', () => { }, setup(props) { - watch(() => { + watchEffect(() => { dummy = props.count }) return () => h('div', props.count) diff --git a/packages/runtime-core/__tests__/apiTemplateRef.spec.ts b/packages/runtime-core/__tests__/apiTemplateRef.spec.ts index 8ff6b2c59e1..68b14dd2b28 100644 --- a/packages/runtime-core/__tests__/apiTemplateRef.spec.ts +++ b/packages/runtime-core/__tests__/apiTemplateRef.spec.ts @@ -142,7 +142,7 @@ describe('api: template refs', () => { foo: ref(null), bar: ref(null) } - const refKey: Ref = ref('foo') + const refKey = ref('foo') as Ref const Comp = { setup() { diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index ef05452fd33..36db04b1996 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1,4 +1,12 @@ -import { watch, reactive, computed, nextTick, ref, h } from '../src/index' +import { + watch, + watchEffect, + reactive, + computed, + nextTick, + ref, + h +} from '../src/index' import { render, nodeOps, serializeInner } from '@vue/runtime-test' import { ITERATE_KEY, @@ -13,10 +21,10 @@ import { mockWarn } from '@vue/shared' describe('api: watch', () => { mockWarn() - it('watch(effect)', async () => { + it('effect', async () => { const state = reactive({ count: 0 }) let dummy - watch(() => { + watchEffect(() => { dummy = state.count }) expect(dummy).toBe(0) @@ -117,10 +125,10 @@ describe('api: watch', () => { expect(dummy).toMatchObject([[2, true], [1, false]]) }) - it('stopping the watcher', async () => { + it('stopping the watcher (effect)', async () => { const state = reactive({ count: 0 }) let dummy - const stop = watch(() => { + const stop = watchEffect(() => { dummy = state.count }) expect(dummy).toBe(0) @@ -132,11 +140,32 @@ describe('api: watch', () => { expect(dummy).toBe(0) }) + it('stopping the watcher (with source)', async () => { + const state = reactive({ count: 0 }) + let dummy + const stop = watch( + () => state.count, + count => { + dummy = count + } + ) + + state.count++ + await nextTick() + expect(dummy).toBe(1) + + stop() + state.count++ + await nextTick() + // should not update + expect(dummy).toBe(1) + }) + it('cleanup registration (effect)', async () => { const state = reactive({ count: 0 }) const cleanup = jest.fn() let dummy - const stop = watch(onCleanup => { + const stop = watchEffect(onCleanup => { onCleanup(cleanup) dummy = state.count }) @@ -187,7 +216,7 @@ describe('api: watch', () => { const Comp = { setup() { - watch(() => { + watchEffect(() => { assertion(count.value) }) return () => count.value @@ -221,7 +250,7 @@ describe('api: watch', () => { const Comp = { setup() { - watch( + watchEffect( () => { assertion(count.value, count2.value) }, @@ -263,7 +292,7 @@ describe('api: watch', () => { const Comp = { setup() { - watch( + watchEffect( () => { assertion(count.value) }, @@ -363,14 +392,14 @@ describe('api: watch', () => { expect(spy).toHaveBeenCalledTimes(3) }) - it('warn immediate option when using effect signature', async () => { + it('warn immediate option when using effect', async () => { const count = ref(0) let dummy - // @ts-ignore - watch( + watchEffect( () => { dummy = count.value }, + // @ts-ignore { immediate: false } ) expect(dummy).toBe(0) @@ -381,6 +410,24 @@ describe('api: watch', () => { expect(dummy).toBe(1) }) + it('warn and not respect deep option when using effect', async () => { + const arr = ref([1, [2]]) + let spy = jest.fn() + watchEffect( + () => { + spy() + return arr + }, + // @ts-ignore + { deep: true } + ) + expect(spy).toHaveBeenCalledTimes(1) + ;(arr.value[1] as Array)[0] = 3 + await nextTick() + expect(spy).toHaveBeenCalledTimes(1) + expect(`"deep" option is only respected`).toHaveBeenWarned() + }) + it('onTrack', async () => { const events: DebuggerEvent[] = [] let dummy @@ -388,7 +435,7 @@ describe('api: watch', () => { events.push(e) }) const obj = reactive({ foo: 1, bar: 2 }) - watch( + watchEffect( () => { dummy = [obj.foo, 'bar' in obj, Object.keys(obj)] }, @@ -423,7 +470,7 @@ describe('api: watch', () => { events.push(e) }) const obj = reactive({ foo: 1 }) - watch( + watchEffect( () => { dummy = obj.foo }, diff --git a/packages/runtime-core/__tests__/components/Suspense.spec.ts b/packages/runtime-core/__tests__/components/Suspense.spec.ts index a530532f077..6a115e39c25 100644 --- a/packages/runtime-core/__tests__/components/Suspense.spec.ts +++ b/packages/runtime-core/__tests__/components/Suspense.spec.ts @@ -9,6 +9,7 @@ import { nextTick, onMounted, watch, + watchEffect, onUnmounted, onErrorCaptured } from '@vue/runtime-test' @@ -163,7 +164,7 @@ describe('Suspense', () => { // extra tick needed for Node 12+ deps.push(p.then(() => Promise.resolve())) - watch(() => { + watchEffect(() => { calls.push('immediate effect') }) @@ -265,7 +266,7 @@ describe('Suspense', () => { const p = new Promise(r => setTimeout(r, 1)) deps.push(p) - watch(() => { + watchEffect(() => { calls.push('immediate effect') }) diff --git a/packages/runtime-core/__tests__/errorHandling.spec.ts b/packages/runtime-core/__tests__/errorHandling.spec.ts index 6396c6b35a4..f6907949b1f 100644 --- a/packages/runtime-core/__tests__/errorHandling.spec.ts +++ b/packages/runtime-core/__tests__/errorHandling.spec.ts @@ -7,7 +7,8 @@ import { watch, ref, nextTick, - defineComponent + defineComponent, + watchEffect } from '@vue/runtime-test' import { setErrorRecovery } from '../src/errorHandling' import { mockWarn } from '@vue/shared' @@ -241,7 +242,7 @@ describe('error handling', () => { expect(fn).toHaveBeenCalledWith(err, 'ref function') }) - test('in watch (effect)', () => { + test('in effect', () => { const err = new Error('foo') const fn = jest.fn() @@ -257,7 +258,7 @@ describe('error handling', () => { const Child = { setup() { - watch(() => { + watchEffect(() => { throw err }) return () => null @@ -268,7 +269,7 @@ describe('error handling', () => { expect(fn).toHaveBeenCalledWith(err, 'watcher callback') }) - test('in watch (getter)', () => { + test('in watch getter', () => { const err = new Error('foo') const fn = jest.fn() @@ -298,7 +299,7 @@ describe('error handling', () => { expect(fn).toHaveBeenCalledWith(err, 'watcher getter') }) - test('in watch (callback)', async () => { + test('in watch callback', async () => { const err = new Error('foo') const fn = jest.fn() @@ -332,7 +333,7 @@ describe('error handling', () => { expect(fn).toHaveBeenCalledWith(err, 'watcher callback') }) - test('in watch cleanup', async () => { + test('in effect cleanup', async () => { const err = new Error('foo') const count = ref(0) const fn = jest.fn() @@ -349,7 +350,7 @@ describe('error handling', () => { const Child = { setup() { - watch(onCleanup => { + watchEffect(onCleanup => { count.value onCleanup(() => { throw err diff --git a/packages/runtime-core/__tests__/helpers/scopeId.spec.ts b/packages/runtime-core/__tests__/helpers/scopeId.spec.ts index 08f7b1e7d50..b14d3acbc44 100644 --- a/packages/runtime-core/__tests__/helpers/scopeId.spec.ts +++ b/packages/runtime-core/__tests__/helpers/scopeId.spec.ts @@ -17,6 +17,27 @@ describe('scopeId runtime support', () => { expect(serializeInner(root)).toBe(`
`) }) + test('should attach scopeId to components in parent component', () => { + const Child = { + __scopeId: 'child', + render: withChildId(() => { + return h('div') + }) + } + const App = { + __scopeId: 'parent', + render: withParentId(() => { + return h('div', [h(Child)]) + }) + } + + const root = nodeOps.createElement('div') + render(h(App), root) + expect(serializeInner(root)).toBe( + `
` + ) + }) + test('should work on slots', () => { const Child = { __scopeId: 'child', @@ -41,7 +62,7 @@ describe('scopeId runtime support', () => { // - scopeId from parent // - slotted scopeId (with `-s` postfix) from child (the tree owner) expect(serializeInner(root)).toBe( - `
` + `
` ) }) }) diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json index fe69d4ff9e2..2ba19068880 100644 --- a/packages/runtime-core/package.json +++ b/packages/runtime-core/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-core", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "description": "@vue/runtime-core", "main": "index.js", "module": "dist/runtime-core.esm-bundler.js", @@ -31,6 +31,6 @@ }, "homepage": "https://github.com/vuejs/vue/tree/dev/packages/runtime-core#readme", "dependencies": { - "@vue/reactivity": "3.0.0-alpha.5" + "@vue/reactivity": "3.0.0-alpha.6" } } diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index da3df3033f9..71cb485a533 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -71,6 +71,14 @@ export type StopHandle = () => void const invoke = (fn: Function) => fn() +// Simple effect. +export function watchEffect( + effect: WatchEffect, + options?: BaseWatchOptions +): StopHandle { + return doWatch(effect, null, options) +} + // initial value for watchers to trigger on undefined initial values const INITIAL_WATCHER_VALUE = {} @@ -110,6 +118,13 @@ export function watch( // watch(source, cb) return doWatch(effectOrSource, cbOrOptions, options) } else { + // TODO remove this in the next release + __DEV__ && + warn( + `\`watch(fn, options?)\` signature has been moved to a separate API. ` + + `Use \`watchEffect(fn, options?)\` instead. \`watch\` will only ` + + `support \`watch(source, cb, options?) signature in the next release.` + ) // watch(effect) return doWatch(effectOrSource, null, cbOrOptions) } @@ -124,13 +139,13 @@ function doWatch( if (immediate !== undefined) { warn( `watch() "immediate" option is only respected when using the ` + - `watch(source, callback) signature.` + `watch(source, callback, options?) signature.` ) } if (deep !== undefined) { warn( `watch() "deep" option is only respected when using the ` + - `watch(source, callback) signature.` + `watch(source, callback, options?) signature.` ) } } @@ -171,7 +186,7 @@ function doWatch( } } - if (deep) { + if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) } diff --git a/packages/runtime-core/src/componentProxy.ts b/packages/runtime-core/src/componentProxy.ts index 9e451110523..a69d33da140 100644 --- a/packages/runtime-core/src/componentProxy.ts +++ b/packages/runtime-core/src/componentProxy.ts @@ -13,7 +13,8 @@ import { isRef, isReactive, Ref, - ComputedRef + ComputedRef, + unref } from '@vue/reactivity' import { warn } from './warning' import { Slots } from './componentSlots' @@ -84,8 +85,6 @@ const enum AccessTypes { OTHER } -const unwrapRef = (val: unknown) => (isRef(val) ? val.value : val) - export const PublicInstanceProxyHandlers: ProxyHandler = { get(target: ComponentInternalInstance, key: string) { // fast path for unscopables when using `with` block @@ -115,7 +114,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { case AccessTypes.DATA: return data[key] case AccessTypes.CONTEXT: - return unwrapRef(renderContext[key]) + return unref(renderContext[key]) case AccessTypes.PROPS: return propsProxy![key] // default: just fallthrough @@ -125,7 +124,7 @@ export const PublicInstanceProxyHandlers: ProxyHandler = { return data[key] } else if (hasOwn(renderContext, key)) { accessCache![key] = AccessTypes.CONTEXT - return unwrapRef(renderContext[key]) + return unref(renderContext[key]) } else if (type.props != null) { // only cache other properties when instance has declared (this stable) // props diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 145a3697c5c..6b3be672792 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/packages/runtime-core/src/componentRenderUtils.ts @@ -39,6 +39,7 @@ export function renderComponentRoot( ): VNode { const { type: Component, + parent, vnode, proxy, withProxy, @@ -102,6 +103,11 @@ export function renderComponentRoot( if (vnodeHooks !== EMPTY_OBJ) { result = cloneVNode(result, vnodeHooks) } + // inherit scopeId + const parentScopeId = parent && parent.type.__scopeId + if (parentScopeId) { + result = cloneVNode(result, { [parentScopeId]: '' }) + } // inherit directives if (vnode.dirs != null) { if (__DEV__ && !isElementRoot(result)) { @@ -127,6 +133,7 @@ export function renderComponentRoot( result = createVNode(Comment) } currentRenderingInstance = null + return result } diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index b26c9a8f987..634d53b424b 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -3,6 +3,8 @@ export const version = __VERSION__ export { ref, + unref, + shallowRef, isRef, toRefs, reactive, @@ -15,7 +17,7 @@ export { markNonReactive } from '@vue/reactivity' export { computed } from './apiComputed' -export { watch } from './apiWatch' +export { watch, watchEffect } from './apiWatch' export { onBeforeMount, onMounted, diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 9d1c2a414a3..ab5b58cecf2 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -128,7 +128,7 @@ export interface RendererInternals { c: ProcessTextOrCommentFn } -// These functions are created inside a closure and therefore there types cannot +// These functions are created inside a closure and therefore their types cannot // be directly exported. In order to avoid maintaining function signatures in // two places, we declare them once here and use them inside the closure. type PatchFn = ( @@ -651,7 +651,6 @@ function baseCreateRenderer< // generated by the compiler and can take the fast path. // in this path old node and new node are guaranteed to have the same shape // (i.e. at the exact same position in the source template) - if (patchFlag & PatchFlags.FULL_PROPS) { // element props contain dynamic keys, full diff needed patchProps( diff --git a/packages/runtime-dom/__tests__/modules/style.spec.ts b/packages/runtime-dom/__tests__/modules/style.spec.ts index 28eac3b426b..08667275081 100644 --- a/packages/runtime-dom/__tests__/modules/style.spec.ts +++ b/packages/runtime-dom/__tests__/modules/style.spec.ts @@ -21,7 +21,7 @@ describe(`module style`, () => { it('remove if falsy value', () => { const el = document.createElement('div') - patchStyle(el, { color: 'red' }, { color: null }) + patchStyle(el, { color: 'red' }, { color: undefined }) expect(el.style.cssText.replace(/\s/g, '')).toBe('') }) diff --git a/packages/runtime-dom/package.json b/packages/runtime-dom/package.json index 59245312ffe..fefe3ae9208 100644 --- a/packages/runtime-dom/package.json +++ b/packages/runtime-dom/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-dom", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "description": "@vue/runtime-dom", "main": "index.js", "module": "dist/runtime-dom.esm-bundler.js", @@ -37,7 +37,7 @@ }, "homepage": "https://github.com/vuejs/vue/tree/dev/packages/runtime-dom#readme", "dependencies": { - "@vue/runtime-core": "3.0.0-alpha.5", + "@vue/runtime-core": "3.0.0-alpha.6", "csstype": "^2.6.8" } } diff --git a/packages/runtime-test/package.json b/packages/runtime-test/package.json index b1c62e1e5ff..4b1a83ab1bd 100644 --- a/packages/runtime-test/package.json +++ b/packages/runtime-test/package.json @@ -1,6 +1,6 @@ { "name": "@vue/runtime-test", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "description": "@vue/runtime-test", "private": true, "main": "index.js", @@ -30,6 +30,6 @@ }, "homepage": "https://github.com/vuejs/vue/tree/dev/packages/runtime-test#readme", "dependencies": { - "@vue/runtime-core": "3.0.0-alpha.5" + "@vue/runtime-core": "3.0.0-alpha.6" } } diff --git a/packages/server-renderer/__tests__/renderToString.spec.ts b/packages/server-renderer/__tests__/renderToString.spec.ts index 2ac929ac1c4..c8bb79c8746 100644 --- a/packages/server-renderer/__tests__/renderToString.spec.ts +++ b/packages/server-renderer/__tests__/renderToString.spec.ts @@ -562,7 +562,7 @@ describe('ssr: renderToString', () => { } expect(await renderToString(h(Parent))).toBe( - `
slot
` + `
slot
` ) }) }) diff --git a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts index 87521a49aae..38ca843c4db 100644 --- a/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts +++ b/packages/server-renderer/__tests__/ssrRenderAttrs.spec.ts @@ -26,6 +26,14 @@ describe('ssr: renderAttrs', () => { ).toBe(` id="foo" title="bar"`) }) + test('empty value attrs', () => { + expect( + ssrRenderAttrs({ + 'data-v-abc': '' + }) + ).toBe(` data-v-abc`) + }) + test('escape attrs', () => { expect( ssrRenderAttrs({ diff --git a/packages/server-renderer/package.json b/packages/server-renderer/package.json index b159c111fd7..6af7ff5d5b1 100644 --- a/packages/server-renderer/package.json +++ b/packages/server-renderer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/server-renderer", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "description": "@vue/server-renderer", "main": "index.js", "types": "dist/server-renderer.d.ts", @@ -27,9 +27,9 @@ }, "homepage": "https://github.com/vuejs/vue/tree/dev/packages/server-renderer#readme", "peerDependencies": { - "vue": "3.0.0-alpha.5" + "vue": "3.0.0-alpha.6" }, "dependencies": { - "@vue/compiler-ssr": "3.0.0-alpha.5" + "@vue/compiler-ssr": "3.0.0-alpha.6" } } diff --git a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts index 4a1dcb8df5f..d17c0dd11d6 100644 --- a/packages/server-renderer/src/helpers/ssrRenderAttrs.ts +++ b/packages/server-renderer/src/helpers/ssrRenderAttrs.ts @@ -1,11 +1,9 @@ -import { escapeHtml } from '@vue/shared' +import { escapeHtml, stringifyStyle } from '@vue/shared' import { normalizeClass, normalizeStyle, propsToAttrMap, - hyphenate, isString, - isNoUnitNumericStyleProp, isOn, isSSRSafeAttrName, isBooleanAttr, @@ -55,7 +53,9 @@ export function ssrRenderDynamicAttr( if (isBooleanAttr(attrKey)) { return value === false ? `` : ` ${attrKey}` } else if (isSSRSafeAttrName(attrKey)) { - return ` ${attrKey}="${escapeHtml(value)}"` + return value === '' + ? ` ${attrKey}` + : ` ${attrKey}="${escapeHtml(value)}"` } else { console.warn( `[@vue/server-renderer] Skipped rendering unsafe attribute name: ${attrKey}` @@ -93,17 +93,5 @@ export function ssrRenderStyle(raw: unknown): string { return escapeHtml(raw) } const styles = normalizeStyle(raw) - let ret = '' - for (const key in styles) { - const value = styles[key] - const normalizedKey = key.indexOf(`--`) === 0 ? key : hyphenate(key) - if ( - isString(value) || - (typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey)) - ) { - // only render valid values - ret += `${normalizedKey}:${value};` - } - } - return escapeHtml(ret) + return escapeHtml(stringifyStyle(styles)) } diff --git a/packages/server-renderer/src/renderToString.ts b/packages/server-renderer/src/renderToString.ts index 2753c345538..0aac04cd14a 100644 --- a/packages/server-renderer/src/renderToString.ts +++ b/packages/server-renderer/src/renderToString.ts @@ -67,9 +67,11 @@ function createBuffer() { let hasAsync = false const buffer: SSRBuffer = [] return { - buffer, - hasAsync() { - return hasAsync + getBuffer(): ResolvedSSRBuffer | Promise { + // If the current component's buffer contains any Promise from async children, + // then it must return a Promise too. Otherwise this is a component that + // contains only sync children so we can avoid the async book-keeping overhead. + return hasAsync ? Promise.all(buffer) : (buffer as ResolvedSSRBuffer) }, push(item: SSRBufferItem) { const isStringItem = isString(item) @@ -104,28 +106,19 @@ export async function renderToString( input: App | VNode, context: SSRContext = {} ): Promise { - let buffer: ResolvedSSRBuffer if (isVNode(input)) { // raw vnode, wrap with app (for context) return renderToString(createApp({ render: () => input }), context) - } else { - // rendering an app - const vnode = createVNode(input._component, input._props) - vnode.appContext = input._context - // provide the ssr context to the tree - input.provide(ssrContextKey, context) - buffer = await renderComponentVNode(vnode) } - // resolve portals - if (context.__portalBuffers) { - context.portals = context.portals || {} - for (const key in context.__portalBuffers) { - // note: it's OK to await sequentially here because the Promises were - // created eagerly in parallel. - context.portals[key] = unrollBuffer(await context.__portalBuffers[key]) - } - } + // rendering an app + const vnode = createVNode(input._component, input._props) + vnode.appContext = input._context + // provide the ssr context to the tree + input.provide(ssrContextKey, context) + const buffer = await renderComponentVNode(vnode) + + await resolvePortals(context) return unrollBuffer(buffer) } @@ -201,7 +194,7 @@ function renderComponentSubTree( instance: ComponentInternalInstance ): ResolvedSSRBuffer | Promise { const comp = instance.type as Component - const { buffer, push, hasAsync } = createBuffer() + const { getBuffer, push } = createBuffer() if (isFunction(comp)) { renderVNode(push, renderComponentRoot(instance), instance) } else { @@ -225,10 +218,7 @@ function renderComponentSubTree( ) } } - // If the current component's buffer contains any Promise from async children, - // then it must return a Promise too. Otherwise this is a component that - // contains only sync children so we can avoid the async book-keeping overhead. - return hasAsync() ? Promise.all(buffer) : (buffer as ResolvedSSRBuffer) + return getBuffer() } function renderVNode( @@ -349,7 +339,7 @@ function renderPortal( return [] } - const { buffer, push, hasAsync } = createBuffer() + const { getBuffer, push } = createBuffer() renderVNodeChildren( push, vnode.children as VNodeArrayChildren, @@ -360,7 +350,17 @@ function renderPortal( ] as SSRContext const portalBuffers = context.__portalBuffers || (context.__portalBuffers = {}) - portalBuffers[target] = hasAsync() - ? Promise.all(buffer) - : (buffer as ResolvedSSRBuffer) + + portalBuffers[target] = getBuffer() +} + +async function resolvePortals(context: SSRContext) { + if (context.__portalBuffers) { + context.portals = context.portals || {} + for (const key in context.__portalBuffers) { + // note: it's OK to await sequentially here because the Promises were + // created eagerly in parallel. + context.portals[key] = unrollBuffer(await context.__portalBuffers[key]) + } + } } diff --git a/packages/shared/package.json b/packages/shared/package.json index b897788f6c9..cd8125af310 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,5 +1,5 @@ { "name": "@vue/shared", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "private": true } diff --git a/packages/shared/src/normalizeProp.ts b/packages/shared/src/normalizeProp.ts index 96678e8bc94..201e49806d3 100644 --- a/packages/shared/src/normalizeProp.ts +++ b/packages/shared/src/normalizeProp.ts @@ -1,4 +1,5 @@ -import { isArray, isString, isObject } from './' +import { isArray, isString, isObject, hyphenate } from './' +import { isNoUnitNumericStyleProp } from './domAttrConfig' export function normalizeStyle( value: unknown @@ -19,6 +20,27 @@ export function normalizeStyle( } } +export function stringifyStyle( + styles: Record | undefined +): string { + let ret = '' + if (!styles) { + return ret + } + for (const key in styles) { + const value = styles[key] + const normalizedKey = key.indexOf(`--`) === 0 ? key : hyphenate(key) + if ( + isString(value) || + (typeof value === 'number' && isNoUnitNumericStyleProp(normalizedKey)) + ) { + // only render valid values + ret += `${normalizedKey}:${value};` + } + } + return ret +} + export function normalizeClass(value: unknown): string { let res = '' if (isString(value)) { diff --git a/packages/shared/src/patchFlags.ts b/packages/shared/src/patchFlags.ts index 374a22b5218..9e499abf71f 100644 --- a/packages/shared/src/patchFlags.ts +++ b/packages/shared/src/patchFlags.ts @@ -55,7 +55,7 @@ export const enum PatchFlags { // Indicates an element that only needs non-props patching, e.g. ref or // directives (onVnodeXXX hooks). since every patched vnode checks for refs - // and onVnodeXXX hooks, itt simply marks the vnode so that a parent block + // and onVnodeXXX hooks, it simply marks the vnode so that a parent block // will track it. NEED_PATCH = 1 << 9, diff --git a/packages/size-check/package.json b/packages/size-check/package.json index 5f8702111a4..9589206a3a0 100644 --- a/packages/size-check/package.json +++ b/packages/size-check/package.json @@ -1,6 +1,6 @@ { "name": "@vue/size-check", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "private": true, "buildOptions": { "name": "Vue", diff --git a/packages/template-explorer/package.json b/packages/template-explorer/package.json index c592497bdb7..f35f72597ca 100644 --- a/packages/template-explorer/package.json +++ b/packages/template-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@vue/template-explorer", - "version": "3.0.0-alpha.5", + "version": "3.0.0-alpha.6", "private": true, "buildOptions": { "formats": [ diff --git a/packages/vue/examples/composition/commits.html b/packages/vue/examples/composition/commits.html index 270662fe386..711cbc67039 100644 --- a/packages/vue/examples/composition/commits.html +++ b/packages/vue/examples/composition/commits.html @@ -22,7 +22,7 @@

Latest Vue.js Commits