诗号:六道同坠,魔劫万千,引渡如来。

/img/bdx/yiyeshu-001.jpg

>=3.2.0 更新日志 vue-next CHANGELOG, 只列出重要或和开发时相关的更新。

3.2 All Important Changes

  • v-on 支持 async...await handler 🔗

  • vm = createApp({ ...}).mount(...) 返回的 vm 上取不到 expose({ foo: 1 }) 出来的 属性, vm.foo === undefined 🔗

  • defineProps 支持解构操作 🔗

    [3.2.4] slots.default(a,b,c,…) 插槽使用支持多个参数

slot.default(…args) 支持多个参数。

如: h('div', null, slots.default('a', 'b', 'c'))

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<template>
<demo>
  <template #default="a,b,c">{{a}}, {{b}}, {{c}}</template>
</demo>
</template>

// demo.js
export default {
name: "Demo",
props: { ... },
render() {
  return h('h1', null, this.$slots.default("a", "b", "c"))
}
}

FIX:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
packages/runtime-core/src/componentSlots.ts
@@ -63,15 +63,15 @@ const normalizeSlot = (
rawSlot: Function,
ctx: ComponentInternalInstance | null | undefined
): Slot => {
-  const normalized = withCtx((props: any) => {
+  const normalized = withCtx((...args: any[]) => {
  if (__DEV__ && currentInstance) {
    warn(
      `Slot "${key}" invoked outside of the render function: ` +
        `this will not track dependencies used in the slot. ` +
        `Invoke the slot function inside the render function instead.`
    )
  }
-    return normalizeSlotValue(rawSlot(props))
+    return normalizeSlotValue(rawSlot(...args))
}, ctx) as Slot
// NOT a compiled slot
;(normalized as ContextualRenderFn)._c = false

v-bind style/class 被解析成了 [object Object] ?

问题(SFC playground):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<template>
<button :class="{ btn: true }">1</button>
<button v-bind="{ disabled: true }" :class="{ btn: true }">2</button>
<button :class="{ btn: true }" v-bind="{ disabled: true }">3</button>
</template>

<style>
.btn {
  color: red;
}
</style>

第 1,2 个按钮能正常解析出 btn 类名,但是第三个按钮的 class 变成了 [object Object]

这要看下 runtime-core/src/vnode.ts:mergeProps 中是如何对属性进行合并的:

它首先将第一个属性 extend 出来了,做为了一个基准对象,比如 btn3 就有了:

ret = { class: { btn: true } }

然后进入 for 循环处理剩下的属性 {disabled: true}, 最后发现走了最后一个

else if (key !== '') 所以最后的 ret = { class: { btn: true }, disabled: true }

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export function mergeProps(...args: (Data & VNodeProps)[]) {
const ret = extend({}, args[0])
for (let i = 1; i < args.length; i++) {
  const toMerge = args[i]
  for (const key in toMerge) {
    if (key === 'class') {
      if (ret.class !== toMerge.class) {
        ret.class = normalizeClass([ret.class, toMerge.class])
      }
    } else if (key === 'style') {
      ret.style = normalizeStyle([ret.style, toMerge.style])
    } else if (isOn(key)) {
      const existing = ret[key]
      const incoming = toMerge[key]
      if (existing !== incoming) {
        ret[key] = existing
          ? [].concat(existing as any, incoming as any)
          : incoming
      }
    } else if (key !== '') {
      ret[key] = toMerge[key]
    }
  }
}
return ret
}

runtime-core 阶段只是对 props 进行了 normalize 处理,真正 patch 的流程是:

runtime-core: renderer.ts:patchProps -> hostPatchProp = runtime-dom:patchProp.ts

最后 class 的 patch 是 runtime-dom/src/modules/class.ts:patchClass

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// compiler should normalize class + :class bindings on the same element
// into a single binding ['staticClass', dynamic]
export function patchClass(el: Element, value: string | null, isSVG: boolean) {
// directly setting className should be faster than setAttribute in theory
// if this is an element during a transition, take the temporary transition
// classes into account.
const transitionClasses = (el as ElementWithTransition)._vtc
if (transitionClasses) {
  value = (value
    ? [value, ...transitionClasses]
    : [...transitionClasses]
  ).join(' ')
}
if (value == null) {
  el.removeAttribute('class')
} else if (isSVG) {
  el.setAttribute('class', value)
} else {
  el.className = value
}
}

而这里面直接就是 setAttribute('class', value)el.className = value 这就等于 是说一开始的 ret = { class: { btn: true }, disable: true } 中的 class: { btn: true } 到这里 value = { btn: true } 最终被自动转成了 [obect Object]

以上就是第三个按钮为什么 class 被解析成了 [object Object] 的由来。

那么解决办法就是在 runtime-core 的 normalize 阶段就将 class 值对象处理成字符串, 如 commit 所示。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
packages/runtime-core/src/vnode.ts
@@ -778,8 +778,8 @@ export function normalizeChildren(vnode: VNode, children: unknown) {
}

export function mergeProps(...args: (Data & VNodeProps)[]) {
-  const ret = extend({}, args[0])
-  for (let i = 1; i < args.length; i++) {
+  const ret: Data = {}
+  for (let i = 0; i < args.length; i++) {
  const toMerge = args[i]
  for (const key in toMerge) {
    if (key === 'class') {

不用 extend 将第一个 prop 放入基准对象,都直接进入 for 被处理掉。

这个时候第一个 {class: { btn: true } } 会进入 normalizeClass([ret.class, toMerge.class])

shared/src/normalizeProp.ts:normalizeClass(value: unknown)

此时检测到 class 是个对象最后被处理成 "btn" 字符串

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
export function normalizeClass(value: unknown): string {
let res = ''
if (isString(value)) {
  res = value
} else if (isArray(value)) {
  for (let i = 0; i < value.length; i++) {
    const normalized = normalizeClass(value[i])
    if (normalized) {
      res += normalized + ' '
    }
  }
} else if (isObject(value)) {
  for (const name in value) {
    if (value[name]) {
      res += name + ' '
    }
  }
}
return res.trim()
}

TODO EffectScope ?

deferredComputed

WARNING

没有被暴露的 api ,只限 vue 内部 @vue/reactivity 使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const url = process.env.VNEXT_PKG_RC +'/../reactivity/dist/reactivity.cjs.js'
const value = require(url.replace('stb-', ''))
const { reactive, effect, ref, deferredComputed } = value

;(async function () {
const tick = Promise.resolve()
const src = ref(0)
const c = deferredComputed(() => src.value)
let i = 0
const spy = (val) => {
console.log("i = " + i++ + ', val = ' + val)
}
effect(() => spy(c.value))

src.value = 1
src.value = 2
src.value = 3

console.log('1: i = ' + i)
await tick // to flush jobs
console.log('2: i = ' + i)
}())

return ''
i = 0, val = 0
1: i = 1
''i = 1, val = 3
2: i = 2

看个正常 computed 的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const url = process.env.VNEXT_PKG_RC +'/../reactivity/dist/reactivity.cjs.js'
const value = require(url.replace('stb-', ''))
const { reactive, effect, ref, computed } = value

;(async function () {
const tick = Promise.resolve()
const src = ref(0)
const c = computed(() => src.value)
let i = 0
const spy = (val) => {
console.log("i = " + i++ + ', val = ' + val)
}
effect(() => spy(c.value))

src.value = 1
src.value = 2
src.value = 3

console.log('1: i = ' + i)
await tick // to flush jobs
console.log(': i = ' + i)
}())

return 2
i = 0, val = 0
i = 1, val = 1
i = 2, val = 2
i = 3, val = 3
1: i = 4
2: i = 4

对比两个结果会发现:

正常的 computed 在 src.value 改变时每次都会执行 spy, 这是因为 computed(() => src.value) 操作让 src track 了这个 () => src.value 因此只要值发生改变就会立即 执行它,而对于计算属性 c 又依赖了 src.value 因此触发 c 重新计算,从而调用 spy。

而在 deferredComputed 的实现中将 effect 加入到了 scheduler 异步队列中去执行,导 致同步的代码没有执行结束之前是不会执行的,只要不重新计算 c.value 就不会改变。那 么 effect spy 也就不会被执行,从而导致 spy 不会在 src.value 改变时被立即调用。

但是在后面调用了 await nextTick() 之后会立即将 scheduler 的队列 flush 掉,此时才 会去执行 compute 重新计算 c.value 的值,得到的也就是最后一次 src.value 的值(要 清楚一点 await 之前 src.value 是会发生改变的,只是不会触发重新计算),然后 c.value 的改变会触发 effect(() => spy(c.value)) 去执行。

关于 scheduler 和 nextTick 可阅读这两文:

Vue3 功能拆解② Scheduler 渲染机制

Vue3 源码头脑风暴之 7 ☞ runtime-core(1) - 若叶知秋 - scheduler 任务调度机制

deferredComputed 源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 只会将 effect 加入 job 队列,不会立即执行
const scheduler = (fn: any) => {
  queue.push(fn)
  if (!queued) {
    queued = true
    tick.then(flush)
  }
}

class DeferredComputedRefImpl<T> {
  constructor(getter: ComputedGetter<T>) {
    let compareTarget: any
    let hasCompareTarget = false
    let scheduled = false
    this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
      if (this.dep) {
        if (computedTrigger) {
          compareTarget = this._value
          hasCompareTarget = true
        } else if (!scheduled) {
          const valueToCompare = hasCompareTarget ? compareTarget : this._value
          scheduled = true
          hasCompareTarget = false
          scheduler(() => {
            if (this.effect.active && this._get() !== valueToCompare) {
              triggerRefValue(this)
            }
            scheduled = false
          })
        }
        // chained upstream computeds are notified synchronously to ensure
        // value invalidation in case of sync access; normal effects are
        // deferred to be triggered in scheduler.
        for (const e of this.dep) {
          if (e.computed) {
            e.scheduler!(true /* computedTrigger */)
          }
        }
      }
      this._dirty = true
    })
    this.effect.computed = true
  }

  private _get() {
    if (this._dirty) {
      this._dirty = false
      return (this._value = this.effect.run()!)
    }
    return this._value
  }

  get value() {
    trackRefValue(this)
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    return toRaw(this)._get()
  }

}

而对于 computed 就没那么多操作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T
  private _dirty = true
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean
  ) {

    this.effect = new ReactiveEffect(getter, () => { // scheduler
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self)
    if (self._dirty) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

ReactiveEffect 从函数变成了一个 class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = []

  // can be attached after creation
  computed?: boolean
  allowRecurse?: boolean
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope | null
  ) {
    recordEffectScope(this, scope)
  }

  run() {
    if (!this.active) {
      return this.fn()
    }
    if (!effectStack.includes(this)) {
      try {
        effectStack.push((activeEffect = this))
        enableTracking()

        trackOpBit = 1 << ++effectTrackDepth

        if (effectTrackDepth <= maxMarkerBits) {
          initDepMarkers(this)
        } else {
          cleanupEffect(this)
        }
        return this.fn()
      } finally {
        if (effectTrackDepth <= maxMarkerBits) {
          finalizeDepMarkers(this)
        }

        trackOpBit = 1 << --effectTrackDepth

        resetTracking()
        effectStack.pop()
        const n = effectStack.length
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }

  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

依赖收集的时候:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }

  // 1. new instance
  const _effect = new ReactiveEffect(fn)
  if (options) {
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    // 2. run
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

新增 ref 语法糖($ref, $raw)

新增 ref 语法糖:

  1. $ref() 被解析成 _ref()

    如: let foo = $ref(1) => let foo = _ref(1)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    const url =
      process.env.VNEXT_PKG_RC + "/../compiler-sfc/dist/compiler-sfc.cjs.js";
    
    const value = require(url.replace("stb-", ""));
    const { compileScript, parse } = value;
    
    function compileSFCScript(src, options) {
    const { descriptor } = parse(src)
    return compileScript(descriptor, {
    ...options,
    id: 'xxxxxxx'
    })
    }
    
    function compileWithRefSugar(src) {
    return compileSFCScript(src, { refSugar: true })
    }
    
    const _ = (title, src) => {
    const { content } = compileWithRefSugar(src)
    console.log(title, '\n', content)
    }
    
    _('$ref declarations > ', `<script setup>
    let foo = $ref()
    let a = $ref(1)
    let b = $ref({
      count: 0
    })
    let c = () => {}
    let d
    </script>`)
    
    return 0;
    
    $ref declarations >
    import { ref as _ref } from 'vue'
    
    export default {
        setup(__props, { expose }) {
        expose()
    
        let foo = _ref()
        let a = _ref(1)
        let b = _ref({
            count: 0
        })
        let c = () => {}
        let d
    
    const __returned__ = { foo, a, b, c, d }
    Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
    return __returned__
    }
    
    }
    0
    

3.2.23~29(2021-11-16 ~ 2022-01-23)

Bug Fixes [1/1]

  • runtime-core: handle initial undefined attrs (#5017) (6d887aa), closes #5016

    值为 undefined 的 attrs 在值发生变化时会被解析到 props 中去。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    packages/runtime-core/src/componentProps.ts
    @@ -369,7 +369,7 @@ function setFullProps(
              continue
            }
          }
    -        if (value !== attrs[key]) {
    +        if (!(key in attrs) || value !== attrs[key]) {
            attrs[key] = value
            hasAttrsChanged = true
          }
    

Features [3/3]

  • reactivity: support default value in toRef() (2db9c90)

    支持设置默认值: toRef(obj, 'x', 1)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    packages/reactivity/src/ref.ts
    
    class ObjectRefImpl<T extends object, K extends keyof T> {
    -  constructor(private readonly _object: T, private readonly _key: K) {}
    +  constructor(
    +    private readonly _object: T,
    +    private readonly _key: K,
    +    private readonly _defaultValue?: T[K]
    +  ) {}
    
    get value() {
    -    return this._object[this._key]
    +    const val = this._object[this._key]
    +    return val === undefined ? (this._defaultValue as T[K]) : val
    }
    
  • support ref in v-for, remove compat deprecation warnings (41c18ef)

  • types/script-setup: add generic type to defineExpose (#5035) (34985fe) OLD:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    <script setup lang="ts">
      import type { IMyComponent } from './MyComponent';
    
      const publicAPI: IMyComponent = {
          doSomething() {
              // ...
          },
      };
    
      defineExpose(publicAPI);
    </script>

    NEW:

    1
    2
    3
    4
    5
    
    defineExpose<IMyComponent>({
      doSomething() {
          // ...
      },
    });
    

DONE 3.2.14~22 (2021-09-22 ~ 2021-11-15)

CLOSED: [2021-11-25 Thu 22:08]

这段时间的更新还是以修复 bug 为主的,另外更新了一个主要的特性 defineProps 支持解 构。

Bug Fixes [29/29]

  • compiler-core: generate TS-cast safe assignment code for v-model (686d014), closes #4655

  • compiler-core: more robust member expression check in Node (6257ade)

  • compiler-sfc: fix local var access check for bindings in normal script (6d6cc90), closes #4644

    解决 scriptscript setup 一起用的时候找不到变量问题。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    expect(() =>
      compile(`
          <script>const bar = 1</script>
          <script setup>
          defineProps({
            foo: {
              default: () => bar
            }
          })
          </script>`)
    ).not.toThrow(`cannot reference locally declared variables`)
    
  • devtools: fix prod devtools detection + handle late devtools hook injection (#4651) (2476eaa)

  • compiler-ssr: import ssr helpers from updated path (d74f21a)

  • ssr: fix ssr runtime helper import in module mode (8e05b7f)

  • build: avoid importing @babel/parser in esm-bundler build (fc85ad2), closes #4665

  • compiler-core: should treat attribute key as expression (#4658) (7aa0ea0)

    <template v-for="a in b" key="c"/> 中的 c 被解析成了变量 _ctx.c 问题。

  • server-renderer: respect compilerOptions during runtime template compilation (#4631) (50d9d34)

  • compiler-sfc: fix props codegen w/ leading import (d4c04e9), closes #4764

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    <script setup>import { ref } from 'vue' // this line will cause an error
    const props=defineProps({
     foo:String
    })
    
    const msg = ref('Hello World!')
    </script>
    
    <template>
    <h1>{{ msg }}</h1>
    <input v-model="msg">
    </template>
    
  • compiler-sfc: support runtime Enum in normal script (#4698) (f66d456)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
     test('runtime Enum in normal script', () => {
        const { content, bindings } = compile(
          `<script lang="ts">
            export enum D { D = "D" }
            const enum C { C = "C" }
            enum B { B = "B" }
          </script>
          <script setup lang="ts">
          enum Foo { A = 123 }
          </script>`
        )
        assertCode(content)
        expect(bindings).toStrictEqual({
          D: BindingTypes.SETUP_CONST,
          C: BindingTypes.SETUP_CONST,
          B: BindingTypes.SETUP_CONST,
          Foo: BindingTypes.SETUP_CONST
        })
      })
    
  • devtools: clear devtools buffer after timeout (f4639e0), closes #4738

  • hmr: fix hmr for components with no active instance yet (9e3d773), closes #4757

  • types: ensure that DeepReadonly handles Ref type properly (#4714) (ed0071a)

  • types: make toRef return correct type(fix #4732) (#4734) (925bc34)

  • custom-element: fix custom element props access on initial render (4b7f76e), closes #4792

    1
    2
    3
    4
    5
    
    packages/runtime-dom/src/apiCustomElement.ts
    -      asyncDef().then(resolve)
    +      asyncDef()
    +        .then(resolve)
    +        .then(() => this._update())
    
  • custom-element: fix initial attr type casting for programmtically created elements (3ca8317), closes #4772

    codesandbox.io

  • devtools: avoid open handle in non-browser env (6916d72), closes #4815

  • devtools: fix memory leak when devtools is not installed (#4833) (6b32f0d), closes #4829

  • runtime-core: add v-memo to built-in directives check (#4787) (5eb7263)

    memo 视为 vue 内置指令:

    1
    2
    3
    4
    
    const isBuiltInDirective = /*#__PURE__*/ makeMap(
    -  'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text'
    +  'bind,cloak,else-if,else,for,html,if,model,on,once,pre,show,slot,text,memo'
    )
    
  • runtime-dom: fix behavior regression for v-show + style display binding (3f38d59), closes #4768

  • types: fix ref unwrapping type inference for nested shallowReactive & shallowRef (20a3615), closes #4771

  • compiler-sfc: add type for props include Function in prod mode (#4938) (9c42a1e)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    <script setup lang='ts'>
    
    import { onMounted } from 'vue'
    
    var props = withDefaults(defineProps<{f?: Function}>(),{f:()=>[1,2,3]})
    
    onMounted(()=>{
    console.log("f:",props.f)
    })
    
    </script>
  • compiler-sfc: add type for props's properties in prod mode (#4790) (090df08), closes #4783

  • compiler-sfc: externalRE support automatic http/https prefix url pattern (#4922) (574070f), closes #4920

    1
    2
    3
    4
    5
    
    <template>
    <!-- other stuff -->
    <img src="//via.placeholder.com/150" />
    <!-- other stuff -->
    </template>
  • compiler-sfc: fix expose codegen edge case (#4919) (31fd590), closes #4917

  • devtool: improve devtools late injection browser env detection (#4890) (fa2237f)

  • runtime-core: improve dedupe listeners when attr fallthrough (#4912) (b4eb7e3), closes #4859

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    packages/runtime-core/src/vnode.ts
    @@ -791,7 +791,10 @@ export function mergeProps(...args: (Data & VNodeProps)[]) {
        } else if (isOn(key)) {
          const existing = ret[key]
          const incoming = toMerge[key]
    -        if (existing !== incoming) {
    +        if (
    +          existing !== incoming &&
    +          !(isArray(existing) && existing.includes(incoming))
    +        ) {
            ret[key] = existing
              ? [].concat(existing as any, incoming as any)
              : incoming
    
  • types/sfc: fix withDefaults type inference when using union types (#4925) (04e5835)

Features [1/1]

  • [3.2.20]compiler-sfc: <script setup> defineProps destructure transform (#4690) (467e113)

    defineProps 支持解构操作。

    <script setup>const { foo, bar } = defineProps(['foo', 'bar'])</script>

DONE 3.2.13 (2021-09-21)

CLOSED: [2021-10-20 Wed 16:39]

Bug Fixes [1/1]

  • runtime-core: return the exposeProxy from mount (#4606) (5aa4255)

    问题:在 createApp().mount 返回的 vm 上找不到 vm.foo

    FIX:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    export function getExposeProxy(instance: ComponentInternalInstance) {
    if (instance.exposed) {
      return (
        instance.exposeProxy ||
        (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
          get(target, key: string) {
            if (key in target) {
              return target[key]
            } else if (key in publicPropertiesMap) {
              return publicPropertiesMap[key](instance)
            }
          }
        }))
      )
    }
    }
    

    test:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    test('with mount', () => {
      const Component = defineComponent({
        setup(_, { expose }) {
          expose({
            foo: 1
          })
          return {
            bar: 2
          }
        },
        render() {
          return h('div')
        }
      })
      const root = nodeOps.createElement('div')
      const vm = createApp(Component).mount(root) as any
      expect(vm.foo).toBe(1)
      expect(vm.bar).toBe(undefined)
    })
    

DONE 3.2.12(2021-09-17)

CLOSED: [2021-10-20 Wed 16:24]

Bug Fixes [2/2]

  • compiler-sfc: support nested await statements (#4458) (ae942cd), closes #4448

    <script>await (await 1)</script>

  • compiler-core: v-on inline async function expression handler (#4569) (fc968d6), closes #4568

    <div @click="async () => await foo()" />

DONE 3.2.4(2021-08-17)

CLOSED: [2021-10-20 Wed 16:00]

DONE 3.2.0 (2021-08-09)

CLOSED: [2021-09-08 Wed 15:52]

Compatibility Notes

This release contains no public API breakage. However, there are a few compatibility related notes:

没有 API 的破坏更新。

Due to usage of new runtime helpers, code generated by the template compiler in >= 3.2 will not be compatible with runtime < 3.2.

3.2 之后模板编译与之前的不兼容。

This only affects cases where there is a version mismatch between the compiler and the runtime. The most common case is libraries that ship pre-compiled Vue components. If you are a library author and ship code pre-compiled by Vue >= 3.2, your library will be only compatible Vue >= 3.2.

This release ships TypeScript typings that rely on Template Literal Types and requires TS >= 4.1.

Performance Improvements [7/7]

DONE 3.2.0-beta.8 (2021-08-07)

CLOSED: [2021-09-08 Wed 14:52]

Important

  • FIX v-memo 在 v-for 中使用时支持常量表达式 <div v-for="v in list" v-memo="[count < 2 ? true : count]"/>

Bug Fixes [8/8]

  • compiler-core: detected forwarded slots in nested components (#4268) (abb3a81), closes #4244

  • compiler-sfc: fix ref sugar rewrite for identifiers in ts casting expressions (865b84b), closes #4254

  • core: typing of key in VNodeProps (#4242) (d045055), closes #4240

  • runtime-core: component effect scopes should be detached (6aa871e)

  • runtime-dom: fix shadowRoot instanceof check in unsupported browsers (#4238) (bc7dd93)

  • types: remove explicit return type annotation requirement for this inference in computed options (#4221) (d3d5ad2)

  • v-memo: ensure track block when returning cached vnode (#4270) (a211e27), closes #4253

  • v-memo: should work on v-for with constant expression (#4272) (3b60358), closes #4246

    v-memo 应用在 v-for 中加入表达式的支持

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    packages/runtime-core/src/helpers/renderList.ts
    @@ -71,7 +71,7 @@ export function renderList(
      }
      ret = new Array(source)
      for (let i = 0; i < source; i++) {
    -      ret[i] = renderItem(i + 1, i)
    +      ret[i] = renderItem(i + 1, i, undefined, cached && cached[i])
      }
    } else if (isObject(source)) {
      if (source[Symbol.iterator as any]) {
    

    test:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    test('on v-for /w constant expression ', async () => {
      const [el, vm] = mount({
        template: `<div v-for="item in 3"  v-memo="[count < 2 ? true : count]">
            {{count}}
          </div>`,
        data: () => ({
          count: 0
        })
      })
      expect(el.innerHTML).toBe(`<div>0</div><div>0</div><div>0</div>`)
    
      vm.count = 1
      await nextTick()
      // should not update
      expect(el.innerHTML).toBe(`<div>0</div><div>0</div><div>0</div>`)
    
      vm.count = 2
      await nextTick()
      // should update
      expect(el.innerHTML).toBe(`<div>2</div><div>2</div><div>2</div>`)
    })
    

DONE 3.2.0-beta.7 (2021-07-29)

CLOSED: [2021-09-08 Wed 14:24]

Bug Fixes [4/4]

  • reactivity: dereference nested effect scopes on manual stop (1867591)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    packages/reactivity/src/effectScope.ts
    @@ -1,3 +1,4 @@
    + import { remove } from '@vue/shared'
    import { ReactiveEffect } from './effect'
    import { warn } from './warning'
    
    @@ -8,10 +9,12 @@ export class EffectScope {
    active = true
    effects: (ReactiveEffect | EffectScope)[] = []
    cleanups: (() => void)[] = []
    +  parent: EffectScope | undefined
    
    constructor(detached = false) {
      if (!detached) {
        recordEffectScope(this)
    +      this.parent = activeEffectScope
      }
    }
    
    @@ -42,11 +45,14 @@ export class EffectScope {
      }
    }
    
    -  stop() {
    +  stop(fromParent = false) {
      if (this.active) {
    -      this.effects.forEach(e => e.stop())
    +      this.effects.forEach(e => e.stop(true))
        this.cleanups.forEach(cleanup => cleanup())
        this.active = false
    +      if (!fromParent && this.parent) {
    +        remove(this.parent.effects, this)
    +      }
      }
    }
    }
    

    test:

    1
    2
    3
    4
    5
    6
    7
    
    it('should derefence child scope from parent scope after stopping child scope (no memleaks)', async () => {
      const parent = new EffectScope()
      const child = parent.run(() => new EffectScope())!
      expect(parent.effects.includes(child)).toBe(true)
      child.stop()
      expect(parent.effects.includes(child)).toBe(false)
    })
    
  • sfc/style-vars: improve ignore style variable bindings in comments (#4202) (771635b)

  • shared: support custom .toString() in text interpolation again (#4210) (9d5fd33), closes #3944 使用插值时候支持自定义的 toString()

  • suspense: fix dynamicChildren tracking when suspense root is a block itself (51ee84f), closes #4183 #4198

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    packages/runtime-core/src/components/Suspense.ts
    @@ -749,7 +749,7 @@ function normalizeSuspenseSlot(s: any) {
      s = singleChild
    }
    s = normalizeVNode(s)
    -  if (block) {
    +  if (block && !s.dynamicChildren) {
      s.dynamicChildren = block.filter(c => c !== s)
    }
    return s
    

Features [2/2]

  • server-renderer: decouple esm build from Node + improve stream API (0867222), closes #3467 #3111 #3460

    移除 renderToSTream, 添加 renderToNodeStream, renderToWebStream, renderToSimpleStream

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    
    export function renderToSimpleStream<T extends SimpleReadable>(
    input: App | VNode,
    context: SSRContext,
    stream: T
    ): T {
    if (isVNode(input)) {
      // raw vnode, wrap with app (for context)
      return renderToSimpleStream(
        createApp({ render: () => input }),
        context,
        stream
      )
    }
    
    // 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)
    
    Promise.resolve(renderComponentVNode(vnode))
      .then(buffer => unrollBuffer(buffer, stream))
      .then(() => {
        stream.push(null)
      })
      .catch(error => {
        stream.destroy(error)
      })
    
    return stream
    }
    
    // node 环境
    export function renderToNodeStream(
    input: App | VNode,
    context: SSRContext = {}
    ): Readable {
    const stream: Readable = __NODE_JS__
      ? new (require('stream').Readable)()
      : null
    
    return renderToSimpleStream(input, context, stream)
    }
    
    // web 环境
    export function renderToWebStream(
    input: App | VNode,
    context: SSRContext = {}
    ): ReadableStream {
      // check
    
    const encoder = new TextEncoder()
    let cancelled = false
    
    return new ReadableStream({
      start(controller) {
        renderToSimpleStream(input, context, {
          push(content) {
            if (cancelled) return
            if (content != null) {
              controller.enqueue(encoder.encode(content))
            } else {
              controller.close()
            }
          },
          destroy(err) {
            controller.error(err)
          }
        })
      },
      cancel() {
        cancelled = true
      }
    })
    }
    

    测试:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    import { createApp, h, defineAsyncComponent } from 'vue'
    import { ReadableStream } from 'stream/web'
    import { renderToWebStream } from '../src'
    
    test('should work', async () => {
    const Async = defineAsyncComponent(() =>
      Promise.resolve({
        render: () => h('div', 'async')
      })
                                      )
    const App = {
      render: () => [h('div', 'parent'), h(Async)]
    }
    
    const stream = renderToWebStream(createApp(App), {}, ReadableStream)
    
    const reader = stream.getReader()
    
    let res = ''
    await reader.read().then(function read({ done, value }): any {
      if (!done) {
        res += value
        return reader.read().then(read)
      }
    })
    
    expect(res).toBe(`<!--[--><div>parent</div><div>async</div><!--]-->`)
    })
    
  • sfc: remove experimental status for sfc style v-bind (3b38c9a)

DONE 3.2.0-beta.6 (2021-07-27)

CLOSED: [2021-09-08 Wed 13:54]

Important

  • FIX inject 的时候要能自动 unref, provide + inject 实际上是原型链的实现

Bug Fixes [3/3]

  • inject: should auto unwrap injected refs (561e210), closes #4196

    在 child 组件中使用 inject 时候要检测是不是 ref 类型,如果是要自动 unref 下, 即:

    在 parent 中

    1
    2
    3
    4
    5
    
    defineComponent({
    provide(): {
      return { n: ref(0) }
    }
    })
    

    在 child 中

    1
    2
    3
    4
    5
    6
    7
    
    defineComponent({
    inject: ['n'],
    render() {
      // 这里要能直接使用,而不是需要 this.n.value
      return this.n
    }
    })
    

    所以 vue 内部要将 this.n.value 隐藏掉,从而能直接 this.n 使用。

    runtime-core/src/componentOptions.ts:resolveInjections

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    
    export function resolveInjections(
    injectOptions: ComponentInjectOptions,
    ctx: any,
    checkDuplicateProperties = NOOP as any,
    unwrapRef = false
    ) {
    if (isArray(injectOptions)) {
      injectOptions = normalizeInject(injectOptions)!
    }
    for (const key in injectOptions) {
      const opt = (injectOptions as ObjectInjectOptions)[key]
      let injected: unknown
      if (isObject(opt)) {
        if ('default' in opt) {
          injected = inject(
            opt.from || key,
            opt.default,
            true /* treat default function as factory */
          )
        } else {
          injected = inject(opt.from || key)
        }
      } else {
        injected = inject(opt)
      }
      if (isRef(injected)) {
        // TODO remove the check in 3.3
        if (unwrapRef) {
          Object.defineProperty(ctx, key, {
            enumerable: true,
            configurable: true,
            get: () => (injected as Ref).value,
            set: v => ((injected as Ref).value = v)
          })
        } else {
          ctx[key] = injected
        }
      } else {
        ctx[key] = injected
      }
      if (__DEV__) {
        checkDuplicateProperties!(OptionTypes.INJECT, key)
      }
    }
    }
    
  • runtime-core: expose ssrUtils in esm-bundler build (ee4cbae), closes #4199

  • sfc/style-vars: should ignore style variable bindings in comments (#4188) (3a75d5d), closes #4185

    过滤掉 css 注释里面的 v-bind

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    packages/compiler-sfc/src/cssVars.ts
    @@ -37,7 +37,9 @@ export function parseCssVars(sfc: SFCDescriptor): string[] {
    const vars: string[] = []
    sfc.styles.forEach(style => {
      let match
    -    while ((match = cssVarRE.exec(style.content))) {
    +    // ignore v-bind() in comments /* ... */
    +    const content = style.content.replace(/\/\*[\s\S]*\*\//g, '')
    +    while ((match = cssVarRE.exec(content))) {
        const variable = match[1] || match[2] || match[3]
        if (!vars.includes(variable)) {
          vars.push(variable)
    

Features [1/1]

  • unwrap refs in toDisplayString (f994b97)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    packages/shared/src/toDisplayString.ts
    @@ -12,8 +12,11 @@ export const toDisplayString = (val: unknown): string => {
      : String(val)
    }
    
    - const replacer = (_key: string, val: any) => {
    -  if (isMap(val)) {
    + const replacer = (_key: string, val: any): any => {
    +  // can't use isRef here since @vue/shared has no deps
    +  if (val && val.__v_isRef) {
    +    return replacer(_key, val.value)
    +  } else if (isMap(val)) {
      return {
        [`Map(${val.size})`]: [...val.entries()].reduce((entries, [key, val]) => {
          ;(entries as any)[`${key} =>`] = val
    

DONE 3.2.0-beta.5 (2021-07-23)

CLOSED: [2021-09-08 Wed 13:39]

Bug Fixes [4/4]

  • hmr: fix custom elements hmr edge cases (bff4ea7)

  • hmr: fix hmr when global mixins are used (db3f57a), closes #4174

  • types: fix types for readonly ref (2581cfb), closes #4180

  • v-model: avoid resetting value of in-focus & lazy input (ac74e1d), closes #4182

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    packages/runtime-dom/src/directives/vModel.ts
    @@ -80,11 +80,14 @@ export const vModelText: ModelDirective<
    mounted(el, { value }) {
      el.value = value == null ? '' : value
    },
    -  beforeUpdate(el, { value, modifiers: { trim, number } }, vnode) {
    +  beforeUpdate(el, { value, modifiers: { lazy, trim, number } }, vnode) {
      el._assign = getModelAssigner(vnode)
      // avoid clearing unresolved text. #2302
      if ((el as any).composing) return
      if (document.activeElement === el) {
    +      if (lazy) {
    +        return
    +      }
        if (trim && el.value.trim() === value) {
          return
        }
    

Features [4/4]

  • compiler-sfc: avoid exposing imports not used in template (5a3ccfd), closes #3183

    避免导出 <template> 中没有用到的 <script setup> 中引入的变量。

  • runtime-dom: hmr for custom elements (7a7e1d8)

    支持自定义元素在开发时的热更新。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    if (__DEV__) {
    instance.appContext.reload = () => {
      render(this._createVNode(), this.shadowRoot!)
      this.shadowRoot!.querySelectorAll('style').forEach(s => {
        this.shadowRoot!.removeChild(s)
      })
      this._applyStyles()
    }
    }
    
  • runtime-dom: support passing initial props to custom element constructor (5b76843) 支持给自定义元素传递默认属性值。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    describe('mounting/unmount', () => {
      const E = defineCustomElement({
        render: () => h('div', 'hello')
        props: {
          msg: {
            type: String,
            default: 'hello'
          }
        },
        render() {
          return h('div', this.msg)
        }
      })
      customElements.define('my-element', E)
    
      // ...
    
      test('should work w/ manual instantiation', () => {
        const e = new E({ msg: 'inline' })
        // should lazy init
        expect(e._instance).toBe(null)
        // should initialize on connect
        container.appendChild(e)
        expect(e._instance).toBeTruthy()
        expect(e.shadowRoot!.innerHTML).toBe(`<div>inline</div>`)
      })
    

    feat:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    
    packages/runtime-dom/src/apiCustomElement.ts
    @@ -23,8 +23,8 @@ import {
    import { camelize, extend, hyphenate, isArray, toNumber } from '@vue/shared'
    import { hydrate, render } from '.'
    
    - type VueElementConstructor<P = {}> = {
    -  new (): VueElement & P
    + export type VueElementConstructor<P = {}> = {
    +  new (initialProps?: Record<string, any>): VueElement & P
    }
    
    // defineCustomElement provides the same type inference as defineComponent
    @@ -134,8 +134,8 @@ export function defineCustomElement(
      static get observedAttributes() {
        return attrKeys
      }
      constructor() {
    -      super(Comp, attrKeys, propKeys, hydate)
    +    constructor(initialProps?: Record<string, any>) {
    +      super(Comp, initialProps, attrKeys, propKeys, hydate)
      }
    }
    
    @@ -163,10 +163,6 @@ const BaseClass = (
    ) as typeof HTMLElement
    
    export class VueElement extends BaseClass {
    /**
     * @internal
     */
    @@ -178,6 +174,7 @@ export class VueElement extends BaseClass {
    
    constructor(
      private _def: ComponentOptions & { styles?: string[] },
    +    private _props: Record<string, any> = {},
      private _attrKeys: string[],
      private _propKeys: string[],
      hydrate?: RootHydrateFunction
    
  • runtime-dom: support specifying shadow dom styles in defineCustomElement (a7fa4ac)

    给自定义元素增加 styles 支持。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    packages/runtime-dom/src/apiCustomElement.ts
    
    // overload 5: defining a custom element from the returned value of
    @@ -176,7 +176,7 @@ export class VueElement extends BaseClass {
    _connected = false
    
    constructor(
    -    private _def: Component,
    +    private _def: ComponentOptions & { styles?: string[] },
      private _attrKeys: string[],
      private _propKeys: string[],
      hydrate?: RootHydrateFunction
    @@ -192,6 +192,13 @@ export class VueElement extends BaseClass {
          )
        }
        this.attachShadow({ mode: 'open' })
    +      if (_def.styles) {
    +        _def.styles.forEach(css => {
    +          const s = document.createElement('style')
    +          s.textContent = css
    +          this.shadowRoot!.appendChild(s)
    +        })
    +      }
      }
    }
    

    测试:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    describe('styles', () => {
      test('should attach styles to shadow dom', () => {
        const Foo = defineCustomElement({
          styles: [`div { color: red; }`],
          render() {
            return h('div', 'hello')
          }
        })
        customElements.define('my-el-with-styles', Foo)
        container.innerHTML = `<my-el-with-styles></my-el-with-styles>`
        const el = container.childNodes[0] as VueElement
        const style = el.shadowRoot?.querySelector('style')!
        expect(style.textContent).toBe(`div { color: red; }`)
      })
    })
    

DONE 3.2.0-beta.4 (2021-07-21)

CLOSED: [2021-09-08 Wed 11:24]

Bug Fixes [2/2]

  • runtime-core: ensure setupContext.attrs reactivity when used in child slots (8560005), closes #4161

    setup 中的 attrs 使用 proxy 代理

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    @@ -859,15 +874,13 @@ export function createSetupContext(
    } else {
      return {
    -      attrs: instance.attrs,
    +      get attrs() {
    +        return attrs || (attrs = createAttrsProxy(instance))
    +      },
        slots: instance.slots,
        emit: instance.emit,
        expose
    
  • runtime-dom: defer setting value (ff0c810), closes #2325 #4024

Performance Improvements [1/1]

  • skip patch on same vnode (d13774b)

    优化:不对同一个节点进行 patch 过程。

    1
    2
    3
    4
    5
    6
    7
    8
    
    packages/runtime-core/src/renderer.ts
    @@ -470,6 +470,10 @@ function baseCreateRenderer(
      slotScopeIds = null,
      optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
    ) => {
    +    if (n1 === n2) {
    +      return
    +    }
    

DONE 3.2.0-beta.3 (2021-07-20)

CLOSED: [2021-09-08 Wed 10:50]

Important

  • ADD watchSyncEffect 同步 watch effect,回调会在值变更之前被调用

  • ADD 添加 deferredComputed 支持计算属性异步功能,修改之后取值不会立即计算,而是 在 next tick 之后 flush scheduler 队列的时候通过 effect 去触发重新计算。

  • FIX 修复 <button class="{btn:true}" v-bind="{disabled:true}"> 中的 class 被解 析成了 [object Object] 的问题。

Bug Fixes [4/4]

Features [2/2]

  • reactivity: deferredComputed (14ca881)

  • runtime-core: watchSyncEffect (d87d059)

    watch options flush -> sync

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    export function watchSyncEffect(
    effect: WatchEffect,
    options?: DebuggerOptions
    ) {
    return doWatch(
      effect,
      null,
      (__DEV__
        ? Object.assign(options || {}, { flush: 'sync' })
        : { flush: 'sync' }) as WatchOptionsBase
    )
    }
    

    test:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    
    const url = process.env.VNEXT_PKG_RC +'/../runtime-test/dist/runtime-test.cjs.js'
    const value = require(url.replace('stb-', ''))
    const { nodeOps, render, nextTick, h, serializeInner: s, defineComponent, ref, watchSyncEffect } = value
    const count = ref(0)
    const count2 = ref(0)
    let result1, result2, callCount = 0
    const assertion = count => {
    console.log('called ' + ++callCount)
    // on mount, watch callback 应该在 DOM 渲染之前被调用
    // on update, 应该在 count 更新之前被调用
    // 因为是同步 effect
    const expectedDOM = callCount === 1 ? '' : `${count - 1}`
    result1 = s(root) === expectedDOM
    
    // 在同步回调中,在第2次调用时,state mutation 还不会被执行,但是在第3次调用时被执行
    const expectedState = callCount <3 ? 0 : 1
    result2 = count2.value === expectedState
    }
    
    const Comp = {
    setup() {
      watchSyncEffect(() => {
        assertion(count.value)
      })
      return () => count.value
    }
    }
    
    const root = nodeOps.createElement('div')
    render(h(Comp), root)
    console.log('before set, result1 = ' + result1)
    console.log('before set, result2 = ' + result2)
    
    count.value++
    count2.value++
    nextTick().then(() => {
    console.log('\nafter set, result1 = ' + result1)
    console.log('after set, result2 = ' + result2)
    })
    

    源码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    
    // apiWatch.ts -> doWatch(...)
    let scheduler: EffectScheduler
    // 如果是 flush : 'sync', 这里会直接给 sheduler,这个
    // scheduler 会在值发生变更 trigger -> triggerEffect 中执行
    if (flush === 'sync') {
    scheduler = job as any // the scheduler function gets called directly
    }
    
    // ...
    // getter 已经上面测试中的 watchSyncEffect(fn) 的 fn 函数
    const effect = new ReactiveEffect(getter, scheduler)
    
    // ...
    // initial run
    if (cb) {
      // ...
    } else if (flush === 'post') {
      // ...
    } else {
      // on mount 时执行,
      // 会进入这里直接的执行 run, 即立即执行一次 watchSyncEffect(fn) 的 fn
      effect.run()
    }
    
    
    // effect.ts -> trigger -> triggerEffects
    // on update 执行的: trigger 的时候如果有 scheduler 会直接执行
    export function triggerEffects(
    dep: Dep | ReactiveEffect[],
    debuggerEventExtraInfo?: DebuggerEventExtraInfo
    ) {
    // spread into array for stabilization
    for (const effect of isArray(dep) ? dep : [...dep]) {
      if (effect !== activeEffect || effect.allowRecurse) {
        if (__DEV__ && effect.onTrigger) {
          effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
        }
        if (effect.scheduler) {
          effect.scheduler()
        } else {
          effect.run()
        }
      }
    }
    }
    

DONE 3.2.0-beta.2 (2021-07-19)

CLOSED: [2021-09-03 Fri 16:33]

Important

  1. ADD: 支持 <script setup lang="ts"> 中使用 const enum Foo { A: 100 }, const enum

  2. FIX: 支持 <div :style="color: `${value}`"/> 使用

  3. FIX: 修复 watch([a,b], ([newA, newB], [oldA, oldB]) => {})undefined -> [oldA, oldB] 解构问题

Bug Fixes [11/11]

  • compiler-core: fix self-closing tags with v-pre (a21ca3d)

  • compiler-sfc: defineProps infer TSParenthesizedType (#4147) (f7607d3)

  • compiler-sfc: expose correct range for empty blocks (b274b08)

  • compiler-sfc: fix whitespace preservation when block contains single self-closing tag (ec6abe8)

  • compiler-sfc: support const enum (93a950d)

    支持 <script setup lang="ts"> 中使用 const enum Foo { A: 100 }

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    const url =
        process.env.VNEXT_PKG_RC + "/../compiler-sfc/dist/compiler-sfc.cjs.js";
    const value = require(url.replace("stb-", ""));
    const { compileScript, parse } = value;
    
    function compileSFCScript(src, options) {
    const { descriptor } = parse(src)
    return compileScript(descriptor, {
      ...options,
      id: 'xxxxxxx'
    })
    }
    
    function compileWithRefSugar(src) {
    return compileSFCScript(src, { refSugar: true })
    }
    
    const _ = (title, src) => {
    const { content } = compileWithRefSugar(src)
    console.log(title, '\n', content)
    }
    
    _('const enum >> ', `
    <script setup lang="ts">
    const enum Foo { A = 123 }
    </script>`)
    
    const enum >>
     import { defineComponent as _defineComponent } from 'vue'
    const enum Foo { A = 123 }
    
    export default _defineComponent({
      setup(__props, { expose }) {
      expose()
    
    
    const __returned__ = { Foo }
    Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
    return __returned__
    }
    
    })
    undefined
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    packages/compiler-sfc/src/compileScript.ts
    @@ -1008,7 +1008,7 @@ export function compileScript(
    
      if (isTS) {
        // runtime enum
    -      if (node.type === 'TSEnumDeclaration' && !node.const) {
    +      if (node.type === 'TSEnumDeclaration') {
          registerBinding(setupBindings, node.id, BindingTypes.SETUP_CONST)
        }
    
  • reactivity: computed should not trigger scheduler if stopped (6eb47f0), closes #4149

    组件 deactivated 之后不应该再执行 compute 计算,3.2.1 中好像又改回去了?

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    
    (async function () {
    const url = process.env.VNEXT_PKG_RC +'/../reactivity/dist/reactivity.cjs.js'
    const value = require(url.replace('stb-', ''))
    const { reactive, effect, computed, ref } = value
    const tick = Promise.resolve()
    const queue = []
    let queued = false
    const schedule = fn => {
      queue.push(fn)
      if (!queued) {
        queued = true
        tick.then(flush)
      }
    }
    
    const flush = () => {
      for (let i = 0; i < queue.length; i++) {
        queue[i]()
      }
      queue.length = 0
      queued = false
    }
    
    let i = 0
    const c1Spy = () => {
      i++
      console.log('xxx');
    }
    const src = ref(0)
    const c1 = computed(() => {
      c1Spy()
      return src.value % 2
    })
    effect(() => c1.value)
    console.log(`c1Spy called ${i} times`)
    
    schedule(() => {
      console.log('\nstopped');
      c1.effect.stop()
    })
    
    src.value++
    
    await tick
    console.log(`c1Spy called ${i} times`)
    
    }())
    
    return
    
    xxx
    c1Spy called 1 times
    xxx
    undefined
    stopped
    c1Spy called 2 times
    
  • runtime-core: fix null type in required + multi-type prop declarations (bbf6ca9), closes #4146 #4147 支持多种类型时 null 声明。

    test:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    test('support null in required + multiple-type declarations', () => {
      const Comp = {
        props: {
          foo: { type: [Function, null], required: true }
        },
        render() {}
      }
      const root = nodeOps.createElement('div')
      expect(() => {
        render(h(Comp, { foo: () => {} }), root)
      }).not.toThrow()
    
      expect(() => {
        render(h(Comp, { foo: null }), root)
      }).not.toThrow()
    })
    

    FIX:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    
    packages/runtime-core/src/componentProps.ts
    @@ -529,7 +529,7 @@ function validatePropName(key: string) {
    // so that it works across vms / iframes.
    function getType(ctor: Prop<any>): string {
    const match = ctor && ctor.toString().match(/^\s*function (\w+)/)
    -  return match ? match[1] : ''
    +  return match ? match[1] : ctor === null ? 'null' : ''
    }
    
    function isSameType(a: Prop<any>, b: Prop<any>): boolean {
    @@ -637,6 +637,8 @@ function assertType(value: unknown, type: PropConstructor): AssertionResult {
      valid = isObject(value)
    } else if (expectedType === 'Array') {
      valid = isArray(value)
    +  } else if (expectedType === 'null') {
    +    valid = value === null
    } else {
      valid = value instanceof type
    }
    @@ -656,7 +658,7 @@ function getInvalidTypeMessage(
    ): string {
    let message =
      `Invalid prop: type check failed for prop "${name}".` +
    -    ` Expected ${expectedTypes.map(capitalize).join(', ')}`
    +    ` Expected ${expectedTypes.map(capitalize).join(' | ')}`
    const expectedType = expectedTypes[0]
    const receivedType = toRawType(value)
    const expectedValue = styleValue(value, expectedType)
    
  • scheduler: fix insertion for id-less job (d810a1a), closes #4148

    scheduler 调试 job 过程中是按照 job.id 的大小来进行排序的,比如,队列中有三个 job: job1{id:5}, job4, job2{id:1}, job5, job3{id:3} 最后当前队列中会有: [job2, job1, job3, job4, job5] 如果一个任务没有 id,会直接按照调用顺序逐个追加 到队列末尾,如 job4, job5。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    
    packages/runtime-core/src/scheduler.ts
    @@ -10,6 +10,7 @@ setComputedScheduler(queueJob)
    export interface SchedulerJob extends Function {
    id?: number
    active?: boolean
    +  computed?: boolean
    /**
     * Indicates whether the effect is allowed to recursively trigger itself
     * when managed by the scheduler.
    @@ -70,16 +71,15 @@ export function nextTick<T = void>(
    // Use binary-search to find a suitable position in the queue,
    // so that the queue maintains the increasing order of job's id,
    // which can prevent the job from being skipped and also can avoid repeated patching.
    - function findInsertionIndex(job: SchedulerJob) {
    + function findInsertionIndex(id: number) {
    // the start index should be `flushIndex + 1`
    let start = flushIndex + 1
    let end = queue.length
    -  const jobId = getId(job)
    
    while (start < end) {
      const middle = (start + end) >>> 1
      const middleJobId = getId(queue[middle])
    -    middleJobId < jobId ? (start = middle + 1) : (end = middle)
    +    middleJobId < id ? (start = middle + 1) : (end = middle)
    }
    
    return start
    @@ -100,11 +100,10 @@ export function queueJob(job: SchedulerJob) {
        )) &&
      job !== currentPreFlushParentJob
    ) {
    -    const pos = findInsertionIndex(job)
    -    if (pos > -1) {
    -      queue.splice(pos, 0, job)
    -    } else {
    +    if (job.id == null) {
        queue.push(job)
    +    } else {
    +      queue.splice(findInsertionIndex(job.id), 0, job)
      }
      queueFlush()
    }
    @@ -253,6 +252,7 @@ function flushJobs(seen?: CountMap) {
          if (__DEV__ && checkRecursiveUpdates(seen!, job)) {
            continue
          }
    +        // console.log(`running:`, job.id)
          callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
        }
      }
    
  • shared: normalizeStyle should handle strings (a8c3a8a), closes #4138

    问题: <h1 :style="`color: ${x};`" style="">Hello World!</h1>

    修复:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    packages/shared/src/normalizeProp.ts
    @@ -18,6 +18,8 @@ export function normalizeStyle(value: unknown): NormalizedStyle | undefined {
        }
      }
      return res
    +  } else if (isString(value)) {
    +    return parseStringStyle(value)
    } else if (isObject(value)) {
      return value
    }
    

    源码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    const listDelimiterRE = /;(?![^(]*\))/g
    const propertyDelimiterRE = /:(.+)/
    
    export function parseStringStyle(cssText: string): NormalizedStyle {
    const ret: NormalizedStyle = {}
    cssText.split(listDelimiterRE).forEach(item => {
      if (item) {
        const tmp = item.split(propertyDelimiterRE)
        tmp.length > 1 && (ret[tmp[0].trim()] = tmp[1].trim())
      }
    })
    return ret
    }
    
  • ssr: update initial old value to watch callback in ssr usage (#4103) (20b6619) 问题: 指定 immediate: true 时候会立即执行一次,然而此时 oldValue 是 undefined 会导致 callback([…], [oldA, oldB]) 解构错误(undefined -> [oldA, oldB])

    1
    2
    3
    4
    5
    6
    7
    
    setup(){
    const a = ref(1)
    const b = ref(2)
    watch([a, b], ([newA, newB], [oldA, oldB]) => {
      // ...
    }, { deep: true, immediate: true })
    }
    

    修复: 检查被 watch 的源数据,如果是数据 oldValue 初始化成 []

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    packages/runtime-core/src/apiWatch.ts
    @@ -265,7 +265,7 @@ function doWatch(
      } else if (immediate) {
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          getter(),
    -        undefined,
    +        isMultiSource ? [] : undefined,
          onInvalidate
        ])
      }
    
  • v-model: properly detect input type=number (3056e9b), closes #3813

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    packages/runtime-dom/src/directives/vModel.ts
    @@ -49,7 +49,8 @@ export const vModelText: ModelDirective<
    > = {
    created(el, { modifiers: { lazy, trim, number } }, vnode) {
      el._assign = getModelAssigner(vnode)
    -    const castToNumber = number || el.type === 'number'
    +    const castToNumber =
    +      number || (vnode.props && vnode.props.type === 'number')
      addEventListener(el, lazy ? 'change' : 'input', e => {
        if ((e.target as any).composing) return
        let domValue: string | number = el.value
    

Features [3/3]

  • compiler: allow 'comments' option to affect comment inclusion in dev (#4115) (dd0f9d1), closes #3392 #3395

    __DEV__ 值决定 comments 是否保留。

  • compiler-sfc: add ignoreEmpty option for sfc parse method (8dbecfc)

    支持 sfc parse(src, { ignoreEmpty: true/false }) 来决定是否忽略空的 script 和 style

  • types: map declared emits to onXXX props in inferred prop types (#3926) (69344ff)

    emits 事件绑定的函数类型推导。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    packages/runtime-core/src/componentEmits.ts
    + export type EmitsToProps<T extends EmitsOptions> = T extends string[]
    +  ? {
    +      [K in string & `on${Capitalize<T[number]>}`]?: (...args: any[]) => any
    +    }
    +  : T extends ObjectEmitsOptions
    +  ? {
    +      [K in string &
    +        `on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}`
    +        ? T[Uncapitalize<C>] extends null
    +          ? (...args: any[]) => any
    +          : T[Uncapitalize<C>]
    +        : never
    +    }
    +  : {}
    

    test:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    const MyComponent = defineComponent({
      mixins: [MixinA, MixinB, MixinC, MixinD],
    +    emits: ['click'],
      props: {
        // required should make property non-void
        z: {
    @@ -552,6 +554,9 @@ describe('with mixins', () => {
      setup(props) {
        expectType<string>(props.z)
        // props
    +      expectType<((...args: any[]) => any) | undefined>(props.onClick)
        // from Base
    +      expectType<((...args: any[]) => any) | undefined>(props.onBar)
        expectType<string>(props.aP1)
        expectType<boolean | undefined>(props.aP2)
        expectType<any>(props.bP1)
    

Performance Improvements [1/1]

  • compiler-sfc: ignore empty blocks (#3520) (b771fdb)

    忽略 SFC 中的空标签。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    packages/compiler-sfc/src/parse.ts
    @@ -162,7 +162,8 @@ export function parse(
      if (node.type !== NodeTypes.ELEMENT) {
        return
      }
    -    if (!node.children.length && !hasSrc(node) && node.tag !== 'template') {
    +    // we only want to keep the nodes that are not empty (when the tag is not a template)
    +    if (node.tag !== 'template' && isEmpty(node) && !hasSrc(node)) {
        return
      }
      switch (node.tag) {
    @@ -415,3 +416,15 @@ function hasSrc(node: ElementNode) {
      return p.name === 'src'
    })
    }
    +
    + /**
    + * Returns true if the node has no children
    + * once the empty text nodes (trimmed content) have been filtered out.
    + */
    + function isEmpty(node: ElementNode) {
    +  return (
    +    node.children.filter(
    +      child => child.type !== NodeTypes.TEXT || child.content.trim() !== ''
    +    ).length === 0
    +  )
    + }
    

    测试:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    const url =
        process.env.VNEXT_PKG_RC + "/../compiler-sfc/dist/compiler-sfc.cjs.js";
    const value = require(url.replace("stb-", ""));
    const { compileScript, parse } = value;
    
    const _ = (title, src) => {
    const { descriptor: { script, styles, template } } = parse(src)
    console.log(title, '\n', script, styles, template.content)
    }
    
    
    _('empty tag', `<template>
    <h1>{{ msg }}</h1>
    </template>
    
    <script setup>
    
    </script>
    
    <style scoped>
    
    </style>`)
    
    empty tag
     null []
      <h1>{{ msg }}</h1>
    
    undefined
    

DONE 3.2.0-beta.1 (2021-07-16)

CLOSED: [2021-09-03 Fri 14:27]

Important

  1. ADD: defineCustomElement 结合 window.customElements 来定义元素 🔗

  2. ADD: v-memo 指令可以指定哪些条件下组件需要更新 🔗

  3. ADD: watchPostEffect 等价于 doWatch(effect, null/*cb*/, { flush: 'post' }) 🔗

  4. ADD: effectScope 🔗

  5. ADD: ref 新语法糖 $ref() 等价于 ref(), 只是不再需要手动从 vue import 了

    之前: <script setup>import { ref } from 'vue'; var val = ref(1);</script>

    之后: <script setup>var val = $ref(1);</script>

  6. FIX: 使用了 MutationObserver 来解决 cssVar + transition + v-if 时 cssVar 不正 常生效问题

  7. CHG: ReactiveEffect 改成了 class 来实现,因此 effect 不再是函数,而是一个 ReactiveEffect 实例对象。

Code Refactoring(代码重构) [1/1]

  • remove deprecated scopeId codegen (f596e00)

    生成的 render 没有 scope id 了 ?

    1
    2
    
    - export const render = /*#__PURE__*/_withId((_ctx, _cache) => {
    + export function render(_ctx, _cache) {
    

Bug Fixes [4/4]

  • sfc/style-vars: properly re-apply style vars on component root elements change (49dc2dd), closes #3894

    在使用 <transition>v-if 时, SFC <style> 中的 v-bind(color) 没起作用?

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    // packages/runtime-dom/src/helpers/useCssVars.ts
    // @@ -27,8 +27,12 @@ export function useCssVars(getter: (ctx: any) => Record<string, string>) {
    const setVars = () =>
      setVarsOnVNode(instance.subTree, getter(instance.proxy!))
    -  onMounted(() => watchEffect(setVars, { flush: 'post' }))
    -  onUpdated(setVars)
    +  watchPostEffect(setVars)
    +  onMounted(() => {
    +    const ob = new MutationObserver(setVars)
    +    ob.observe(instance.subTree.el!.parentNode, { childList: true })
    +    onUnmounted(() => ob.disconnect())
    +  })
    }
    

    涉及函数: watchPostEffect(setVars)MutationObserver(setVars) 的使用。

    watchPostEffect 是监听 instance.subTree 状态的变化时执行 setVars -> setVarsOnVNode

    MutationObserver 是 JavaScript 的原生 API ,详情可查看此文 JavaScript API - MutationObserver

  • ensure customElements API ssr compatibility (de32cfa), closes #4129

    解决 SSR 服务端渲染时不支持 HTMLElement 的问题。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    // packages/runtime-dom/src/apiCustomElement.ts
    @@ -157,7 +157,11 @@ export const defineSSRCustomElement = ((options: any) => {
    - export class VueElement extends HTMLElement {
    + const BaseClass = (typeof HTMLElement !== 'undefined'
    +  ? HTMLElement
    +  : class {}) as typeof HTMLElement
    
    + export class VueElement extends BaseClass {
    /**
     ,* @internal
     ,*/
    
  • runtime-core: fix default shapeFlag for fragments (2a310df)

    1
    2
    3
    4
    
    dynamicProps: string[] | null = null,
    -  shapeFlag = ShapeFlags.ELEMENT,
    +  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
    isBlockNode = false,
    
  • ignore .prop/.attr modifiers in ssr (29732c2)

    忽略 SSR 中的 .prop/.attr 因为这两个的作用是决定该属性是做为 DOM 元素的 attribute 存在还是以 element.prop = value 元素对象的属性存在。不管是哪种情况都 和实际的 DOM 元素有关。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    // packages/compiler-core/src/transforms/vBind.ts
    @@ -37,12 +37,13 @@ export const transformBind: DirectiveTransform = (dir, _node, context) => {
    -  if (modifiers.includes('prop')) {
    -    injectPrefix(arg, '.')
    -  }
    
    -  if (modifiers.includes('attr')) {
    -    injectPrefix(arg, '^')
    +  if (!context.inSSR) {
    +    if (modifiers.includes('prop')) {
    +      injectPrefix(arg, '.')
    +    }
    +    if (modifiers.includes('attr')) {
    +      injectPrefix(arg, '^')
    +    }
    

Features [10/10]

  • sfc: (experimental) new ref sugar (562bddb)

  • sfc: support namespaced component tags when using <script setup> (e5a4412)

  • custom element reflection, casting and edge cases (00f0b3c)

  • remove experimental status of <script setup> (27104ea)

    正式发布 <script setup>

  • support v-bind .prop & .attr modifiers (1c7d737)

  • runtime-dom: defineCustomElement (8610e1c)

    runtime-core/src/component.ts:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    export interface ComponentInternalInstance {
    /**
     ,* is custom element?
     ,*/
    isCE?: boolean
    // ...
    }
    
    export function createComponentInstance(/*...*/) {
    // ...
    // 交给 vnode.ce 去处理
    // apply custom element special handling
    if (vnode.ce) {
      vnode.ce(instance)
    }
    }
    

    runtime-core/src/helpers/renderSlot.ts

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    export function renderSlot(/*...*/) {
    if (currentRenderingInstance!.isCE) {
      return createVNode(
        'slot',
        name === 'default' ? null : { name },
        fallback && fallback()
      )
    }
    // ...
    }
    
    测试结果
  • v-memo 可以指定什么条件下组件会被重新渲染,否则使用缓存结果 (3b64508)

    compiler-core/src/transforms/vFor.ts 中增加的核心代码:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    
    // v-memo
    if (memo) {
    const loop = createFunctionExpression(
      createForLoopParams(forNode.parseResult, [
        createSimpleExpression(`_cached`)
      ])
    )
    loop.body = createBlockStatement([
      createCompoundExpression([`const _memo = (`, memo.exp!, `)`]),
      createCompoundExpression([
        `if (_cached`,
        ...(keyExp ? [` && _cached.key === `, keyExp] : []),
        ` && ${context.helperString(
    IS_MEMO_SAME
    )}(_cached.memo, _memo)) return _cached`
      ]),
      createCompoundExpression([`const _item = `, childBlock as any]),
      createSimpleExpression(`_item.memo = _memo`),
      createSimpleExpression(`return _item`)
    ])
    renderExp.arguments.push(
      loop as ForIteratorExpression,
      createSimpleExpression(`_cache`),
      createSimpleExpression(String(context.cached++))
    )
    } else {
    renderExp.arguments.push(createFunctionExpression(
      createForLoopParams(forNode.parseResult),
      childBlock,
      true /* force newline */
    ) as ForIteratorExpression)
    }
    

    如:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    const url = process.env.VNEXT_PKG_RC +'/../compiler-core/dist/compiler-core.cjs.js'
    const value = require(url.replace('stb-', ''))
    const { baseCompile } = value
    
    const compile = c => baseCompile(`<div>${c}</div>`, {
    mode: "module",
    prefixIdentifiers: true
    }).code
    
    function test(title, code, options) {
    console.log('// > ' + title)
    console.log(compile(code))
    }
    
    console.log('// > on root element')
    console.log(  baseCompile(`<div v-memo="[x]"></div>`, {
    mode: 'module',
    prefixIdentifiers: true
    }).code)
    
    test('on normal element', `<div v-memo="[x]"></div>`)
    test('on template v-for', `<template v-for="{ x, y } in list" :key="x" v-memo="[x, y === z]">
            <span>foobar</span>
          </template>`)
    return 0
    
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    // > on root element
    import { openBlock as _openBlock, createElementBlock as _createElementBlock, withMemo as _withMemo } from "vue"
    
    export function render(_ctx, _cache) {
      return _withMemo([_ctx.x], () => (_openBlock(), _createElementBlock("div")), _cache, 0)
    }
    // > on normal element
    import { openBlock as _openBlock, createElementBlock as _createElementBlock, withMemo as _withMemo } from "vue"
    
    export function render(_ctx, _cache) {
      return (_openBlock(), _createElementBlock("div", null, [
        _withMemo([_ctx.x], () => (_openBlock(), _createElementBlock("div")), _cache, 0)
      ]))
    }
    // > on template v-for
    import { renderList as _renderList, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock, isMemoSame as _isMemoSame, withMemo as _withMemo } from "vue"
    
    export function render(_ctx, _cache) {
      return (_openBlock(), _createElementBlock("div", null, [
        (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.list, ({ x, y }, __, ___, _cached) => {
          const _memo = ([x, y === z])
          if (_cached && _cached.key === x && _isMemoSame(_cached, _memo)) return _cached
          const _item = (_openBlock(), _createElementBlock("span", { key: x }, "foobar"))
          _item.memo = _memo
          return _item
        }, _cache, 0), 128 /* KEYED_FRAGMENT */))
      ]))
    }
    0
    

    _withMemo -> runtime-core/src/helpers/withMemo.ts:withMemo

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    export function withMemo(
    memo: any[],
    render: () => VNode<any, any>,
    cache: any[],
    index: number
    ) {
    const cached = cache[index] as VNode | undefined
    if (cached && isMemoSame(cached, memo)) {
      return cached
    }
    const ret = render()
    
    // shallow clone
    ret.memo = memo.slice()
    return (cache[index] = ret)
    }
    

    判断不重新渲染条件(memo 长度和元素的值必须一致):

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    export function isMemoSame(cached: VNode, memo: any[]) {
    const prev: any[] = cached.memo!
    if (prev.length != memo.length) {
      return false
    }
    for (let i = 0; i < prev.length; i++) {
      if (prev[i] !== memo[i]) {
        return false
      }
    }
    
    // make sure to let parent block track it when returning cached
    if (isBlockTreeEnabled > 0 && currentBlock) {
      currentBlock.push(cached)
    }
    return true
    }
    
  • watchPostEffect (42ace95)

    1
    2
    3
    4
    5
    6
    7
    8
    
    export function watchPostEffect(
    effect: WatchEffect,
    options?: DebuggerOptions
    ) {
    return doWatch(effect, null, (__DEV__
      ? Object.assign(options || {}, { flush: 'post' })
      : { flush: 'post' }) as WatchOptionsBase)
    }
    

    测试:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    (async function () {
    const url = process.env.VNEXT_PKG_RC +'/../runtime-test/dist/runtime-test.cjs.js'
    const value = require(url.replace('stb-', ''))
    const { render, ref,
            reactive, nextTick, serializeInner, h, nodeOps,
            watchPostEffect
          } = value
    
    const count = ref(0)
    let result, n = 0
    const assertion = count => {
      result = serializeInner(root) === `${count}`
      n++
    }
    
    const Comp = {
      setup() {
        watchPostEffect(() => assertion(count.value))
        return () => count.value
      }
    }
    
    const root = nodeOps.createElement('div')
    try {
      render(h(Comp), root)
    } catch(e) {
      console.log(e.message);
    }
    console.log('1. result = ' + result + ', n = ' + n)
    
    count.value++
    
    await nextTick()
    console.log('\n2. result = ' + result + ', n = ' + n)
    }());
    return ''
    
    1. result = true, n = 1
    ''
    2. result = true, n = 2
    
  • reactivity: new effectScope API (#2195) (f5617fc)

    RFC: vuejs/rfcs#212

    新增的 APIs

    1. EffectScope (class)

    2. getCurrentScope

    3. onScopeDispose

  • reactivity: support onTrack/onTrigger debug options for computed (5cea9a1)

    支持 DEV 模式下分别在 track 和 trigger 的时候调用 onTrack 和 onTrigger。

    onTrack -> effect.ts:trackEffects:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    
    if (shouldTrack) {
     dep.add(activeEffect!)
     activeEffect!.deps.push(dep)
     if (__DEV__ && activeEffect!.onTrack) {
       activeEffect!.onTrack(
         Object.assign(
           {
             effect: activeEffect!
           },
           debuggerEventExtraInfo
         )
       )
     }
    }
    

    onTrigger -> effect.ts:triggerEffects:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    for (const effect of isArray(dep) ? dep : [...dep]) {
     if (effect !== activeEffect || effect.allowRecurse) {
       if (__DEV__ && effect.onTrigger) {
         effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
       }
       if (effect.scheduler) {
         effect.scheduler()
       } else {
         effect.run()
       }
     }
    }
    
    // onTrigger 参数: { effect } & DebuggerEventExtraInfo
    export type DebuggerEventExtraInfo = {
     target: object
     type: TrackOpTypes | TriggerOpTypes
     key: any
     newValue?: any
     oldValue?: any
     oldTarget?: Map<any, any> | Set<any>
    }
    

    使用:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    
    const url = process.env.VNEXT_PKG_RC +'/../reactivity/dist/reactivity.cjs.js'
    const value = require(url.replace('stb-', ''))
    const { reactive, effect, computed } = value
    
    const obj = reactive({ foo: 1 })
    function onTrack(eventInfo) {
     console.log('TrackEventArg=', eventInfo);
    }
    function onTrigger(eventInfo) {
     console.log('TriggerEventArg=', eventInfo);
    }
    const c = computed(() => obj.foo, { onTrigger, onTrack })
    
    c.value;
    obj.foo++
    console.log('c.value = ' + c.value)
    return obj
    
    TrackEventArg= {
      effect: ReactiveEffect {
        fn: [Function (anonymous)],
        scheduler: [Function (anonymous)],
        active: true,
        deps: [ [Set] ],
        onTrack: [Function: onTrack],
        onTrigger: [Function: onTrigger]
      },
      target: { foo: 1 },
      type: 'get',
      key: 'foo'
    }
    TriggerEventArg= {
      effect: ReactiveEffect {
        fn: [Function (anonymous)],
        scheduler: [Function (anonymous)],
        active: true,
        deps: [ [Set] ],
        onTrack: [Function: onTrack],
        onTrigger: [Function: onTrigger]
      },
      target: { foo: 2 },
      type: 'set',
      key: 'foo',
      newValue: 2,
      oldValue: 1,
      oldTarget: undefined
    }
    c.value = 2
    { foo: 2 }
    

Performance improvements [7/7]

  • also hoist all-static children array (b7ea7c1) 如果 children 里面都是静态节点直接将整个 children 数组提升:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    const _hoisted_1 = /*#__PURE__*/_createElementVNode(\\"div\\", { key: \\"foo\\" }, null, -1 /* HOISTED */)
    + const _hoisted_2 = [
    +  _hoisted_1
    + ]
    
    return function render(_ctx, _cache) {
    with (_ctx) {
      const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
    
    -    return (_openBlock(), _createElementBlock(\\"div\\", null, [
    -      _hoisted_1
    -    ]))
    +    return (_openBlock(), _createElementBlock(\\"div\\", null, _hoisted_2))
    }
    }"
    
  • hoist dynamic props lists (02339b6) 动态属性名列表提升:

    1
    2
    
    -      _createElementVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, [\\"id\\"])
    +      _createElementVNode(\\"div\\", { id: foo }, null, 8 /* PROPS */, _hoisted_1)
    
  • reactivity: avoid triggering re-render if computed value did not change (ebaac9a) trigger computed value 之前先检查下值有没改变。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    if (this._dirty) {
    this._dirty = false
    const newValue = this.effect.run()!
    if (this._value !== newValue) {
      this._value = newValue
      triggerRefValue(this)
    }
    } else {
    triggerRefValue(this)
    }
    
  • reactivity: improve reactive effect memory usage (#4001) (87f69fd), closes #2345

    改动点:

    1. ReactiveEffect 改用 class 来实现(stop, run 都在这个 class 里面实现)

       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      
      export class ReactiveEffect<T = any> {
      active = true
      deps: Dep[] = []
      
      // can be attached after creation
      computed?: boolean
      allowRecurse?: boolean
      onStop?: () => void
      // dev only
      onTrack?: (event: DebuggerEvent) => void
      // dev only
      onTrigger?: (event: DebuggerEvent) => void
      
      constructor(
      public fn: () => T,
      public scheduler: EffectScheduler | null = null,
      scope?: EffectScope | null
      ) {
      recordEffectScope(this, scope)
      }
      
      run() {
      if (!this.active) {
       return this.fn()
      }
      if (!effectStack.includes(this)) {
       try {
         effectStack.push((activeEffect = this))
         enableTracking()
      
         trackOpBit = 1 << ++effectTrackDepth
      
         if (effectTrackDepth <= maxMarkerBits) {
           initDepMarkers(this)
         } else {
           cleanupEffect(this)
         }
         return this.fn()
       } finally {
         if (effectTrackDepth <= maxMarkerBits) {
           finalizeDepMarkers(this)
         }
      
         trackOpBit = 1 << --effectTrackDepth
      
         resetTracking()
         effectStack.pop()
         const n = effectStack.length
         activeEffect = n > 0 ? effectStack[n - 1] : undefined
       }
      }
      }
      
      stop() {
      if (this.active) {
       cleanupEffect(this)
       if (this.onStop) {
         this.onStop()
       }
       this.active = false
      }
      }
      }
      
    2. effect 通过 new ReactiveEffect() 创建, 收集的依赖通过 _effect.run() 执行。

  • reactivity: ref-specific track/trigger and miscellaneous optimizations (#3995) (6431040)

  • reactivity: use bitwise dep markers to optimize re-tracking (#4017) (6cf2377)

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    const url = process.env.VNEXT_PKG_RC +'/../reactivity/dist/reactivity.cjs.js'
    const value = require(url.replace('stb-', ''))
    const { reactive, effect, targetMap, toRaw } = value
    
    
    console.log('> should handle deep effect recursion using cleanup fallback');
    const results = reactive([0])
    const effects = []
    for (let i = 1; i < 40;i++) {
    ;(index => {
      const fx = effect(() => {
        results[index] = results[index - 1] * 2
      })
      effects.push({ fx, index })
    })(i)
    }
    
    // targetMap.forEach((key, value) => console.log({ key, value }))
    // console.log(toRaw(results).join(','), targetMap.get(toRaw(results)), 'xx');
    console.log(('results[39] = ' + results[39]));
    const deps = targetMap.get(toRaw(results))
    for (let i = 0; i < 40; i++) {
    const dep = deps.get('' + i)
    // dep && console.log(i + 1 + ": " + "n(newTracked): " + dep.n +', w(wasTracked): ' + dep.w);
    }
    results[0] = 1
    console.log(('results[39] = 2^39, ' + (results[39] === Math.pow(2, 39))));
    
    return 0
    
    > should handle deep effect recursion using cleanup fallback
    results[39] = 0
    results[39] = 2^39, true
    0
    
  • improve VNode creation performance with compiler hints (#3334) (ceff899)

    区分 element 和 component 创建过程,新增两个针对性的函数,分别用来创建 element(createElementVNode) 和 component(createComponentVNode),减少部分检查的 工作,总的来说优化创建 element 和 component 的过程。

    compiler-core:codegen 阶段 element 由 _createVNode 改成 _createElementVNode, _createBlock 改成 _createElementBlock

    增加的 helpers: CREATE_VNODE -> CREATE_ELEMENT_VNODE

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    
    export const CREATE_ELEMENT_BLOCK = Symbol(__DEV__ ? `createElementBlock` : ``)
    export const CREATE_ELEMENT_VNODE = Symbol(__DEV__ ? `createElementVNode` : ``)
    export const NORMALIZE_CLASS = Symbol(__DEV__ ? `normalizeClass` : ``)
    export const NORMALIZE_STYLE = Symbol(__DEV__ ? `normalizeStyle` : ``)
    export const NORMALIZE_PROPS = Symbol(__DEV__ ? `normalizeProps` : ``)
    export const GUARD_REACTIVE_PROPS = Symbol(__DEV__ ? `guardReactiveProps` : ``)
    
    // compiler-core/src/utils.ts
    export function getVNodeHelper(ssr: boolean, isComponent: boolean) {
    return ssr || isComponent ? CREATE_VNODE : CREATE_ELEMENT_VNODE
    }
    
    export function getVNodeBlockHelper(ssr: boolean, isComponent: boolean) {
    return ssr || isComponent ? CREATE_BLOCK : CREATE_ELEMENT_BLOCK
    }
    
    // runtime-core/src/vnode.ts
    export function guardReactiveProps(props: (Data & VNodeProps) | null) {
    if (!props) return null
    return isProxy(props) || InternalObjectKey in props
      ? extend({}, props)
      : props
    }
    
    // shared/src/normalizeProp.ts
    export function normalizeProps(props: Record<string, any> | null) {
    if (!props) return null
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass)
    }
    if (style) {
      props.style = normalizeStyle(style)
    }
    return props
    }
    

    测试:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    
    const url = process.env.VNEXT_PKG_RC +'/../compiler-core/dist/compiler-core.cjs.js'
    const value = require(url.replace('stb-', ''))
    const { generate, createSimpleExpression, locStub,
          createVNodeCall, createObjectExpression,
          createObjectProperty,
          createCompoundExpression,
          createArrayExpression
        } = value
    
    function createRoot(options) {
    return {
      type: 0/* ROOT */,
      children: [],
      helpers: [],
      components: [],
      directives: [],
      imports: [],
      hoists: [],
      cached: 0,
      temps: 0,
      codegenNode: createSimpleExpression(`null`, false),
      loc: locStub,
      ...options
    }
    }
    
    function genCode(node) {
    return generate(
      createRoot({
        codegenNode: node
      })
    ).code.match(/with \(_ctx\) \{\s+([^]+)\s+\}\s+\}$/)[1]
    }
    
    const mockChildren = createCompoundExpression(['children'])
    const mockDirs = createArrayExpression([
    createArrayExpression([`foo`, createSimpleExpression(`bar`, false)])
    ])
    
    const mockProps = createObjectExpression([
    createObjectProperty(`foo`, createSimpleExpression(`bar`, true))
    ])
    
    const test = (title, ...args) => console.log('> ' + title + '\n', "'" + genCode( createVNodeCall(...args) ) + "'")
    
    test('tag only', null, '"div"')
    test('with props', null, '"div"', mockProps)
    test('with children, no props', null, '"div"', undefined, mockChildren)
    test('with children + props', null, '"div"', mockProps, mockChildren)
    test('as block', null, '"dv"', mockProps, mockChildren, undefined, undefined, undefined, true)
    return 0
    
    > tag only
     'return _createElementVNode("div")
     '
    > with props
     'return _createElementVNode("div", { foo: "bar" })
     '
    > with children, no props
     'return _createElementVNode("div", null, children)
     '
    > with children + props
     'return _createElementVNode("div", { foo: "bar" }, children)
     '
    > as block
     'return (_openBlock(), _createElementBlock("dv", { foo: "bar" }, children))
     '
    0
    

Breaking Changes [1/1]

  • Output of SFC using <style scoped> generated by 3.2+ will be incompatible w/ runtime < 3.2.

总结

3.2 更新重点摘要:

  1. v-memo 组件更新条件设置。

  2. $ref() SFC setup 中的新语法糖。

  3. MutationObserver 解决 cssVar + transition + v-if 无效问题。

  4. ReactiveEffect 重构成了 class 实现,effect 不再是函数,而是其实例对象。