Vue源码(十)插槽原理

3,503 阅读5分钟

前言

通过这篇文章可以了解如下内容

  • v-slot 和 slot 属性的区别
  • 具名插槽和作用域插槽的区别
  • $forceUpdate原理

Vue中实现了具名插槽和作用域插槽两种,而具名插槽在父组件中可以通过slot="header"属性或v-slot:header指定插槽内容;先从具名插槽(slot 属性)看起。

具名插槽(slot 属性)的创建过程

父组件

先看下父组件demo

<div>
  <child>
    <h1 slot="header">{{title}}</h1>
    <p>{{message}}</p>
    <p slot="footer">{{desc}}</p>
  </child>
</div>

编译后

_c(
  "div",
  [
    _c("child", [
      _c( // header 的插槽内容
        "h1",
        {
          attrs: {  // 插槽名称
            slot: "header",
          },
          slot: "header", // 插槽名称
        },
        [_v(_s(title))]
      ),
      _v(" "),
      _c("p", [_v(_s(message))]), // 默认插槽
      _v(" "),
      _c( // footer 的插槽内容
        "p",
        {
          attrs: { // 插槽名称
            slot: "footer",
          },
          slot: "footer",  // 插槽名称
        },
        [_v(_s(desc))]
      ),
    ]),
  ],
  1
);

编译后的组件代码会有子节点,子节点上会挂载一个slot属性,值为插槽名称;并且attrs属性中也会多一个slot属性。而默认插槽没有添加任何属性

回顾下整个挂载流程,首先执行父组件的_render方法创建VNode,创建VNode过程中,给响应式属性收集依赖;遇到组件时,为组件创建组件VNode,如果组件有子节点,为子节点创建VNode,并将 子节点VNode添加到componentOptions.children,这些子节点其实就是插槽内容。

然后执行 patch 过程创建DOM元素,当遇到组件VNode时,调用组件VNode的init钩子函数创建组件实例。在组件实例初始化过程中会执行initRender方法,这个方法有如下逻辑

export function initRender (vm: Component) {
  const parentVnode = vm.$vnode = options._parentVnode
  const renderContext = parentVnode && parentVnode.context
  // options._renderChildren 就是组件VNode 的 componentOptions.children
  // 在 _init 中会合并 options,如果是组件实例,则将 componentOptions.children 赋值给 options._renderChildren
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
}

向当前Vue实例上挂载两个属性$slots$scopedSlots

vm.$slots的值是resolveSlots方法的返回值,resolveSlots方法的参数是插槽内容(VNode 数组)和父级Vue实例

export function resolveSlots (
  children: ?Array<VNode>,
  context: ?Component // 指向父级 Vue 实例
){
  if (!children || !children.length) {
    return {}
  }
  const slots = {}
  for (let i = 0, l = children.length; i < l; i++) {
    const child = children[i]
    const data = child.data
    if (data && data.attrs && data.attrs.slot) {
      delete data.attrs.slot
    }
    // 因为 slot 的 vnode 是在父组件实例的作用域中生成的,所以 child.context 指向父组件
    if ((child.context === context || child.fnContext === context) &&
      data && data.slot != null
    ) {
      const name = data.slot
      const slot = (slots[name] || (slots[name] = []))
      if (child.tag === 'template') {
        slot.push.apply(slot, child.children || [])
      } else {
        slot.push(child)
      }
    } else {
      (slots.default || (slots.default = [])).push(child)
    }
  }
  for (const name in slots) {
    if (slots[name].every(isWhitespace)) {
      delete slots[name]
    }
  }
  return slots
}

遍历VNode数组,如果有data.attrs.slot则将此属性删除;然后判断VNode中存储的Vue实例是不是和父级的Vue实例相同,因为插槽的VNode是在父组件实例中创建的,所以这条是成立的,然后判断有没有slot属性:

  • 如果有,说明是具名插槽;如果当前VNode的标签名是template,则将 当前VNode的所有子节点 添加到slots[name]中;反之将 当前VNode 添加到slots[name]
  • 如果不是,说明是默认插槽;将当前VNode添加到slots.default

最后,遍历slots,将注释VNode或者是空字符串的文本VNode去掉;并返回 slotsvm.$slots的属性值如下

vm.$slots = {
  header: [VNode],
  footer: [VNode],
  default: [VNode]
}

resolveSlots方法就是生成并返回一个对象slots,属性名为插槽名称,属性值为VNode数组

子组件

当创建完子组件实例后,进入子组件的挂载过程。先看下demo和子组件编译后的代码

<div class="container">
  <header><slot name="header"></slot></header>
  <main><slot>默认内容</slot></main>
  <footer><slot name="footer"></slot></footer>
</div>

编译后的代码

_c("div", { staticClass: "container" }, [
  _c("header", [_t("header")], 2),
  _v(" "),
  _c("main", [_t("default", [_v("默认内容")])], 2),
  _v(" "),
  _c("footer", [_t("footer")], 2),
]);

编译后的代码中,<slot>标签被编译成了 _t函数,第一个参数是插槽名称,第二个参数是创建后备内容VNode的函数

子组件在执行render函数创建 VNode 时,会执行_t函数,_t 函数对应的就是 renderSlot 方法,它的定义在 src/core/instance/render-heplpers/render-slot.js 中:

export function renderSlot (
  name: string,
  fallback: ?Array<VNode>,
  props: ?Object,
  bindObject: ?Object
): ?Array<VNode> {
  const scopedSlotFn = this.$scopedSlots[name]
  let nodes
  if (scopedSlotFn) { // 作用域插槽
    } else {
    nodes = this.$slots[name] || fallback
  }

  const target = props && props.slot
  if (target) {
    return this.$createElement('template', { slot: target }, nodes)
  } else {
    return nodes
  }
}

对于具名插槽,renderSlot 方法会根据传入的插槽名称,返回vm.$slots中对应插槽名称的VNode,如果没有找到插槽VNode,则调用fallback去创建后备内容的VNode,此时是在子组件实例中,但是插槽VNode的创建是在父组件实例中创建的

到此创建过程就完成了,插槽内容也放到了对应位置。

具名插槽(slot 属性)的更新过程

当父组件修改响应式属性时,通知父组件的Render Watcher更新。调用父组件的render方法创建VNode,在这个过程中,还会创建组件VNode和插槽VNode,将插槽VNode放入componentOptions.children中。接着进入patch过程,对于组件的更新,会调用updateChildComponent函数更新传入子组件的属性

export function updateChildComponent (
  vm: Component, // 子组件实例
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode, // 组件 vnode
  renderChildren: ?Array<VNode> // 最新的插槽VNode数组
) {
  const needsForceUpdate = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  )
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
}

对于有插槽内容的具名插槽来说,vm.$options._renderChildren有值,所以needsForceUpdatetrue,调用resolveSlotsparentVnode中获取最新的vm.$slots,并调用vm.$forceUpdate()去更新组件视图。

Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}

Vue.prototype.$forceUpdate就是调用Render Watcherupdate方法去更新视图

具名插槽(slot 属性)小结

创建过程

使用slot属性表示插槽内容的具名插槽,在父组件编译和渲染阶段会直接生成 vnodes并将插槽VNode放到组件VNode的componentOptions.children中,在创建子组件实例时,将插槽VNode挂载到vm.$slots上;当创建子组件VNode的时候,根据插槽名称从vm.$slots获取对应VNode,如果没有则创建后备VNode。

更新过程

当父组件更新响应式属性时,触发父组件Render Watcher更新。生成插槽VNode;重新设置vm.$slots,并调用vm.$forceUpdate()触发子组件更新视图

作用域插槽创建过程

父组件

先看下 demo 和编译后的代码

<div>
  <child>
    <template v-slot:hello="props">
      <p>hello from parent {{props.text + props.msg}}</p>
    </template>
  </child>
</div>

编译后的父组件代码中,添加一个scopedSlots属性,属性值是_u函数

with (this) {
    return _c(
        'div',
        [
            _c('child', {
                scopedSlots: _u([ // 这里
                    {
                        key: 'hello', // 插槽名
                        fn: function (props) { // 创建插槽内容的VNode
                            return [
                                _c('p', [
                                    _v(
                                        'hello from parent ' +
                                            _s(props.text + props.msg) // 从传入的 props 中拿值
                                    ),
                                ]),
                            ]
                        },
                    },
                ]),
            }),
        ],
        1
    )
}

父组件创建VNode时,会执行scopedSlots属性内的_u方法,_u方法对应的就是resolveScopedSlots方法,定义在src/core/instance/render-helpers/resolve-scoped-slots.js

export function resolveScopedSlots (
  fns: ScopedSlotsData,
  res?: Object,
  hasDynamicKeys?: boolean,
  contentHashKey?: number
): { [key: string]: Function, $stable: boolean } {
  // 如果没有传入 res,则创建一个对象;
  // 对象内有一个 $stable 属性,如果不是动态属性名、插槽上没有 v-for、没有 v-if 则为 true
  res = res || { $stable: !hasDynamicKeys }
  for (let i = 0; i < fns.length; i++) {
    const slot = fns[i]
    if (Array.isArray(slot)) {
      resolveScopedSlots(slot, res, hasDynamicKeys)
    } else if (slot) {
      if (slot.proxy) { // 使用 v-slot:header (2.6新增的具名插槽)时,proxy 为 true
        slot.fn.proxy = true
      }
      res[slot.key] = slot.fn
    }
  }
  if (contentHashKey) {
    (res: any).$key = contentHashKey
  }
  return res
}

其中,fns 是一个数组,每一个数组元素都有一个 key 和一个 fnkey 对应的是插槽的名称,fn 对应一个函数。整个逻辑就是遍历这个 fns 数组,生成一个对象,对象的 key 就是插槽名称,value 就是渲染函数。这个渲染函数的作用就是生成VNode;

子组件

先看 demo 和 编译后的代码

<div class="child">
  <slot text="123" name="hello" :msg="msg"></slot>
</div>

编译后的子组件

_c(
  "div",
  { staticClass: "child" },
  [_t("hello", null, { text: "123", msg: msg })], // 这里
  2
);

生成的代码中<slot>标签也被转换成了_t函数,相对于具名插槽,作用域插槽的_t函数多了一个参数,是一个由子组件中的响应式属性组成的对象

当创建子组件实例时,会调用initRender方法,这个方法内会创建vm.$scopedSlots = emptyObject;然后执行子组件的render函数创建VNode,在_render函数中有这样一段逻辑

const { render, _parentVnode } = vm.$options
if (_parentVnode) {
  vm.$scopedSlots = normalizeScopedSlots(
    _parentVnode.data.scopedSlots,
    vm.$slots,
    vm.$scopedSlots
  )
}

如果组件VNode不为空,说明当前正在创建子组件的渲染VNode,执行normalizeScopedSlots方法,传入组件VNode的scopedSlots属性、vm.$slots(这里是空对象)、vm.$scopedSlots(也是空对象),并将返回值赋值给vm.$scopedSlots。上面说过,在创建父组件的渲染VNode时,会调用_u方法,返回一个对象赋值给_parentVnode.data.scopedSlots,属性名是插槽名称,属性值是创建插槽内容VNode的渲染函数。

export function normalizeScopedSlots (
  slots: { [key: string]: Function } | void,
  normalSlots: { [key: string]: Array<VNode> },
  prevSlots?: { [key: string]: Function } | void
): any {
  let res
  const hasNormalSlots = Object.keys(normalSlots).length > 0
  // $stable 在 _u 中定义
  const isStable = slots ? !!slots.$stable : !hasNormalSlots
  const key = slots && slots.$key
  if (!slots) {
    res = {}
  } else if (slots._normalized) {} else if () {
  } else {
    // 从这里开始
    // 创建过程
    res = {}
    // 遍历传入的slots,对每个属性值调用 normalizeScopedSlot 方法
    for (const key in slots) {
      if (slots[key] && key[0] !== '$') {
        res[key] = normalizeScopedSlot(normalSlots, key, slots[key])
      }
    }
  }
  // ...
  
  // 缓存
  if (slots && Object.isExtensible(slots)) {
    (slots: any)._normalized = res
  }
  // 将 $stable、$key、$hasNormal 添加到 res 中,并不可枚举
  def(res, '$stable', isStable)
  def(res, '$key', key)
  def(res, '$hasNormal', hasNormalSlots) // vm.$slots 有属性则为 true,反之为 false
  return res
}

创建过程的就是生成一个对象,对象key是插槽名称,valuenormalizeScopedSlot函数的返回值

function normalizeScopedSlot(normalSlots, key, fn) {
  const normalized = function () {}
  if (fn.proxy) {}
  return normalized
}

normalizeScopedSlot方法创建并返回了一个normalized函数;对于作用域插槽来说,fn.proxyfalse

回到normalizeScopedSlots方法中,将生成的对象缓存到slots._normalized,然后将 $stable$key$hasNormal 添加到 res 中,返回res。也就是说vm.$scopedSlots是一个对象,属性名是插槽名称,属性值是normalized函数。vm.$scopedSlots中还有$stable$key$hasNormal 这三个属性。

vm.$scopedSlots赋值完成后,接下来执行子组件的render函数,在这期间会执行_t函数,也就是renderSlot函数

export function renderSlot (
  name: string,
  fallback: ?Array<VNode>,
  props: ?Object,
  bindObject: ?Object
): ?Array<VNode> {
  const scopedSlotFn = this.$scopedSlots[name]
  let nodes
  if (scopedSlotFn) { // scoped slot
    props = props || {}
    // ...
    
    nodes = scopedSlotFn(props) || fallback
  } else {
    nodes = this.$slots[name] || fallback
  }

  const target = props && props.slot
  if (target) {
    return this.$createElement('template', { slot: target }, nodes)
  } else {
    return nodes
  }
}

首先根据传入的插槽名称从$scopedSlots中获取对应normalized函数,并调用normalized函数,将props传入,这个props就是子组件通过插槽传递给父组件使用的属性

const normalized = function () {
  // 调用插槽的渲染函数,创建插槽VNode
  let res = arguments.length ? fn.apply(null, arguments) : fn({})
  res = res && typeof res === 'object' && !Array.isArray(res)
    ? [res] // single vnode
  : normalizeChildren(res)
  return res && (
    res.length === 0 ||
    (res.length === 1 && res[0].isComment) // #9658
  ) ? undefined
  : res
}

normalized会执行插槽的渲染函数,并传入props去创建VNode、对使用到的属性做依赖收集。由此作用域插槽创建VNode过程就结束了。其实可以发现,作用域插槽的VNode的创建是在子组件中创建的,所以创建插槽VNode过程中,收集到的依赖是组件的Render Watcher

作用域插槽更新过程

子组件更新,父组件不更新

当子组件修改响应式属性时,通知子组件Watcher更新,创建子组件的渲染VNode;在创建期间会调用normalizeScopedSlots根据vm.$scopedSlots获取key为插槽名、value为插槽的渲染函数的对象,对于更新过程,这里做了一个优化(看注释);

// initRender
const { render, _parentVnode } = vm.$options
if (_parentVnode) {
  vm.$scopedSlots = normalizeScopedSlots(
    _parentVnode.data.scopedSlots,
    vm.$slots,
    vm.$scopedSlots
  )
}
export function normalizeScopedSlots (
  slots: { [key: string]: Function } | void,
  normalSlots: { [key: string]: Array<VNode> },
  prevSlots?: { [key: string]: Function } | void
): any {
  let res
  const hasNormalSlots = Object.keys(normalSlots).length > 0
  const isStable = slots ? !!slots.$stable : !hasNormalSlots
  const key = slots && slots.$key
  if (!slots) {
    res = {}
  } else if (slots._normalized) {
    // fast path 1: 只有子组件更新,父组件不更新,返回上次创建的对象
    // 后面会说怎么判断的,也可以直接全局搜 `fast path 1`
    return slots._normalized
  } else if (
    isStable &&
    prevSlots &&
    prevSlots !== emptyObject &&
    key === prevSlots.$key &&
    !hasNormalSlots &&
    !prevSlots.$hasNormal
  ) {
    // fast path 2: 父组件更新,但是作用域插槽没有变化,返回上次创建的对象
    return prevSlots
  } else {

  }
  if (slots && Object.isExtensible(slots)) {
    (slots: any)._normalized = res
  }
  return res
}

父组件更新

当父组件修改某响应式属性时,通知父组件Render Watcher更新。在父组件创建VNode阶段调用_u函数重新获取scopedSlots属性;在patch过程中,会调用updateChildComponent方法

export function updateChildComponent (
  vm: Component, // 子组件实例
  propsData: ?Object,
  listeners: ?Object,
  parentVnode: MountedComponentVNode, // 组件 vnode
  renderChildren: ?Array<VNode>
) {
  const newScopedSlots = parentVnode.data.scopedSlots
  const oldScopedSlots = vm.$scopedSlots
  const hasDynamicScopedSlot = !!(
    (newScopedSlots && !newScopedSlots.$stable) ||
    (oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||
    (newScopedSlots && vm.$scopedSlots.$key !== newScopedSlots.$key)
  )

  const needsForceUpdate = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  )

  // resolve slots + force update if has children
  if (needsForceUpdate) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context)
    vm.$forceUpdate()
  }
}

updateChildComponent方法内,首先获取新老scopedSlots对象,判断needsForceUpdate是否为true,如果为true则调用vm.$forceUpdate()触发更新

needsForceUpdatetrue的条件是

  • 有子节点
  • 有插槽子节点
  • hasDynamicScopedSlottrue
    • scopedSlots对象不为空,并且有动态插槽或者插槽上有v-forv-if
    • scopedSlots对象不为空,并且有动态插槽或者插槽上有v-forv-if
    • scopedSlots对象不为空,并且新老scopedSlots对象的$key不同

也就是说对于通过slot属性指定插槽内容的具名插槽,当父组件修改响应式属性时, 触发子组件更新,不管父组件的响应式属性有没有在插槽中使用;除非没有插槽内容。

而对于作用域插槽,当父组件修改响应式属性时,只有插槽名是动态的时候,才会触发子组件更新。

作用域插槽小结

创建过程

作用域插槽在父组件编译和渲染阶段并不会直接生成 vnodes,而是在父节点 vnodedata 中保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数,创建子组件实例时,将这个属性挂载到vm.$scopedSlots中;当创建子组件的渲染VNode时,将子组件响应式属性传入并执行这个渲染函数从而创建传入的插槽VNode;创建过程中会将子组件的Render Watcher添加到响应式属性的dep.subs

更新过程

子组件更新,父组件不更新

当子组件修改响应式属性时(不管这个属性有没有应用到作用域插槽中),触发Watcher更新。重新获取vm.$scopedSlots;在创建渲染VNode过程中,执行插槽函数创建插槽VNode并传入子组件属性

父组件更新

当父组件修改某响应式属性时,通知父组件Render Watcher更新。执行render函数过程中,创建新的插槽对象,如果新老插槽对象中有动态插槽则调用vm.$forceUpdate()触发子组件更新,反之不触发

解释 fast path 1

normalizeScopedSlots中有个fast path 1,如果父组件触发了子组件更新,执行_u函数创建插槽对象_parentVnode.data.scopedSlots,由于新创建的_parentVnode.data.scopedSlots上面没有挂载_normalized属性,所以只有子组件更新,父组件不更新时,才会走这个逻辑。

v-slot形式的具名插槽创建过程

2.6之后,具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slotslot-scope 这两个属性

父组件

<div>
  <child>
    <template v-slot:hello>
      <p>hello from parent {{title}}</p>  <!-- 使用的是父组件的属性 -->
    </template>
  </child>
</div>

编译后,和作用域插槽的一个区别就是使用的属性title是从父组件中获取的,并且proxytrue

with (this) {
    return _c(
        'div',
        [
            _c('child', {
                scopedSlots: _u([
                    {
                        key: 'hello',  // 插槽名
                        fn: function () {  // 插槽VNode的渲染函数
                            return [
                                _c('p', [_v('hello from parent ' + _s(title))]), // 从 this 上拿值
                            ]
                        },
                        proxy: true, // 这里为 true
                    },
                ]),
            }),
        ],
        1
    )
}

和作用域插槽流程基本一致,先执行_u创建一个插槽对象,属性名是插槽名称,属性值是一个渲染函数,用于创建VNode。然后在子组件的_render方法中,执行normalizeScopedSlots方法

export function normalizeScopedSlots (
  slots: { [key: string]: Function } | void,
  normalSlots: { [key: string]: Array<VNode> },
  prevSlots?: { [key: string]: Function } | void
): any {
  let res
  const hasNormalSlots = Object.keys(normalSlots).length > 0
  const isStable = slots ? !!slots.$stable : !hasNormalSlots
  const key = slots && slots.$key
  if (!slots) {} else if (slots._normalized) {} else if () {
  } else {
    res = {}
    for (const key in slots) {
      if (slots[key] && key[0] !== '$') {
        res[key] = normalizeScopedSlot(normalSlots, key, slots[key])
      }
    }
  }
  for (const key in normalSlots) {
    if (!(key in res)) {
      res[key] = proxyNormalSlot(normalSlots, key)
    }
  }
  if (slots && Object.isExtensible(slots)) {
    (slots: any)._normalized = res
  }
  def(res, '$stable', isStable)
  def(res, '$key', key)
  def(res, '$hasNormal', hasNormalSlots)
  return res
}

创建一个res对象,属性名为插槽名称,属性值为渲染函数。属性值是通过 normalizeScopedSlot返回的

function normalizeScopedSlot(normalSlots, key, fn) {
  const normalized = function () {}
  if (fn.proxy) {
    // 如果是 v-slot:header 的方式,向 vm.$slot 中添加属性 header,属性值是 normalized
    Object.defineProperty(normalSlots, key, {
      get: normalized,
      enumerable: true,
      configurable: true
    })
  }
  return normalized
}

相比于作用域插槽,当创建完normalized函数后,会将插槽名称添加到vm.$slots中,属性值为normalized函数。

子组件

子组件和作用域插槽的demo相同

继续执行,直到执行子组件的render函数,会调用_t函数,也就是调用renderSlot函数;如果有插槽内容则执行normalized函数创建VNode;反之执行创建后备内容VNode的函数。执行过程中,如果使用到了父组件的属性,则对这个属性做依赖收集,将子组件的Render Watcher添加到此属性的dep.subs。收集的是子组件的Render Watcher

v-slot形式的具名插槽更新过程

当父组件修改的响应式属性在插槽内容中使用过时

创建插槽VNode是在子组件中创建的,所以收集的Watcher是子组件的Render Watcher,所以触发子组件的Watcher更新,在执行子组件的_render函数时,执行normalizeScopedSlots方法,因为在第一次生成vm.$scopedSlots对象时,会添加_normalized属性用于缓存插槽对象,所以会直接返回之前的缓存,并通过_t函数执行对应的插槽函数,执行期间会获取最新的父组件属性值。

父组件修改的响应式属性没有在插槽内容中使用过

父组件创建VNode期间,会重新创建子组件VNode的data.scopedSlots属性;在更新传入子组件的属性过程中,如果新老scopedSlots中有动态插槽名,则更新子组件视图,反之不更新

总结

v-slot 和 slot 属性的区别

slot:父组件在编译和渲染阶段就生成vnodes,并收集了父组件的Render Watcher;修改父组件属性值时触发父组件更新,并重新创建插槽VNode;然后调用子组件的$forceUpdate方法触发子组件更新。也就是说当修改的响应式属性,没有在插槽中使用时,也会触发子组件更新

v-slot:父组件在编译和渲染阶段并不会直接生成 vnodes,而是在父节点 vnodedata 中保留一个 scopedSlots 对象,存储着不同名称的插槽以及它们对应的渲染函数;只有在渲染子组件阶段才会执行这个渲染函数生成 vnodes,此时收集的Watcher是子组件的Render Watcher。当父组件修改响应式属性时,如果修改的属性没有在插槽中使用时,是不会触发子组件更新的;只有使用到的属性更新时,才会触发子组件的Watcher更新,重新执行这个插槽函数,获取最新的属性值

v2.6以后具名插槽和作用域插槽的区别

作用域插槽v-slot:header="props"v-slot基本相同,区别是编译后的代码,如果是作用域插槽,渲染函数中的变量是props.test的形式,也就是说访问的其实是子组件中的响应式属性

$forceUpdate

Vue.prototype.$forceUpdate = function () {
  const vm: Component = this
  if (vm._watcher) {
    vm._watcher.update()
  }
}

调用当前组件的 Render Watcher 的update方法更新视图