Vue2源码【8-4】slot和slotScope

对于同一个组件实例,其 vnode 其实包括其“占位符节点”和“真实节点”,它们是父子节点关系。基于这点知识,再去理解slot等机制并不难。

然后再解释了一下为什么slot和slotScope之流为什么不是响应式的。已经再介绍一下vue3正式登场的v-slot。

slot的原理 #这是2.6之前(之后又略微调整了)

总结:slot插槽(即传给子组件的props.children)的vnode的生成,已在父组件创建和渲染阶段完成。

可以打印中间过程的代码生成结果,来 验证 这个看法:

父组件及其slot代码示例:/* 父组件略 */<h1 slot="header">anything</h1>/* 父组件略 */

// 父组件,经编译阶段最终生成的代码:
// _s可不管 _v:createTextVNode
// with(this) { return
_c('div', [_c('app-layout', [_c('h1', {
  attrs: {
    "slot": "header"
  },
  slot: "header"
}, [_v(_s(title))]), _c('p', [_v(_s(msg))]), _c('p', {
  attrs: {
    "slot": "footer"
  },
  slot: "footer"
}, [_v(_s(desc))])])], 1)

子组件代码示例:<slot name="header">默认内容</slot>

// 子组件,经编译阶段最终生成的代码:
// 【重要】 _t是renderSlot
// with(this) { return
_c('div', {
  staticClass: "container"
}, [ _c('header', [_t("header")], 2),
    _c('main', [_t("default", [_v("默认内容")])], 2),
    _c('footer', [_t("footer")], 2)])

slotScope的原理 #这是2.6之前(之后又略微调整了)

总结:把 scopeSlot插槽 作为一个函数保留下来。scopeSlot插槽(即传给子组件占位符的props.children)的vnode的生成,要延迟到子组件的创建阶段

也是打印中间过程生成代码:

父组件:

// with(this) { return
_c('div',
  [_c('child', {
    scopedSlots: _u([{
      key: "default",
      fn: function(props) {
        return [
          _c('p', [_v("Hello from parent")]),
          _c('p', [_v(_s(props.text + props.msg))])
        ]}}])})],1)

子组件,例如:<slot name="header" text="hello" :msg="msg">默认内容</slot>

// 【重要】 _t是renderSlot。
// 这里代码简单了很多:
_c('div',
    {staticClass:"child"},
    [_t("default",null,
      {text:"Hello ",msg:msg}
    )],

插槽的数据更新(v2.6之前): 通常在响应式系统“组件更新”的流程中 #非响应式

vm.$slots:请注意插槽不是响应性的。

首先,先前依靠的是父组件渲染watcher的依赖收集和派发更新。

比如对于slot场景下的父组件:

  • 所依赖的data发生变化
  • (渲染watcher的)派发更新,也就是调用 updateComponent 回调函数
  • __patch__在浏览器环境中就是patch。这里因为是“新旧节点是相似节点”,判断走的是patchVnode
  • patchVnode
    • i.prePatch ==> updateChildComponent 更新子组件,也就是更新一下子组件占位符vnode所传入的slot部分,然后经判断触发子组件forceUpdate!!!也就是调用子组件“渲染watcher.update()”。且更新的子组件的vm.$slots
    • (后面逻辑已不相关)

原理:

// updateChildComponent 中的代码:
  // Any static slot children from the parent may have changed during parent's update. Dynamic scoped slots may also have changed.
  //  In such cases, a forced update is necessary to ensure correctness.
  var needsForceUpdate = !!(
    // renderChildren指的就是 子组件的slot
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
  );

我们知道,一般来说都是子组件(即渲染vnode)的vnode先生成好了。但,在这些情况,无论slot(以已有slot vnode的形式)或者是scopeSlot(以函数的形式),若有更新则会产生flag,在占位符节点的updateChildren阶段(通过计算needsForceUpdate)发现了flag,就会执行vm.$forceUpdate(更新的子组件的vnode)。

官网:vm.$forceUpdate() :迫使 Vue 实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。

slot和其他响应式更新子组件的方式对比 #所以为什么说插槽不是响应式

首先一个背景是,react是自顶向下的进行递归更新的,而vue是组件的局部订阅,也就是一般来说,vue父组件渲染watcher相关的更新不会触发子组件的更新。

而props是响应式的,也就是子组件可以订阅了props的更新。slot这里明显和props的这种方式不一样。

(响应式外)特殊情况的处理:也是手动 vm.$forceUpdate

比如这个slot用v-if控制,这样针对子渲染watcher的响应式就不管用了。这个时候,子组件patch阶段updateChildComponent会判定hasDynamicScopedSlot为true,去执行vm.$forceUpdate()。

v-slot v2.6之后

在编译阶段的变化:调整语法做了点优化,加上语法糖用起来会比较爽一点。

在运行时阶段的变化: 重要

  • slotslot-scope 在组件内部被统一整合成了 函数
  • 他们的渲染作用域都是 子组件 (forceUpdate将要作用于的组件实例更加准确)
  • 并且都能通过 this.$scopedSlots去访问

数据更新时,v-slot的逻辑: forceUpdate将要作用于的组件实例更加准确

子组件数据更新的情况,没啥好说的。

  • 父组件数据更新的情况:
    • 因为之前首次构建的时候,执行 _trenderSlot 函数时,全局的组件渲染上下文是 子组件,那么依赖收集自然也就是收集到 子组件的依赖了。所以在 msgInParent 更新后,其实是直接去触发子组件的重新渲染的
    • 对比 2.5 的版本,这是一个优化:缩短了响应式的逻辑链条。

总结v-slot的特点

  • 记一下插槽名也是可以动态的 这个事实
  • 解构插槽 Prop
    • 作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里
文章目录
  1. slot的原理 #这是2.6之前(之后又略微调整了)
  2. slotScope的原理 #这是2.6之前(之后又略微调整了)
  3. 插槽的数据更新(v2.6之前): 通常在响应式系统“组件更新”的流程中 #非响应式
    1. slot和其他响应式更新子组件的方式对比 #所以为什么说插槽不是响应式
  • (响应式外)特殊情况的处理:也是手动 vm.$forceUpdate
  • v-slot v2.6之后
    1. 数据更新时,v-slot的逻辑: forceUpdate将要作用于的组件实例更加准确
  • 总结v-slot的特点