面试偶尔问的 Vue2 中 slot 和 slot-scope 的原理

return {

msg: “World”,

};

},

mounted() {

// 一秒后更新

setTimeout(() => {

this.msg = “Changed”;

}, 1000);

},

});

那么 template 就会被编译为这样的函数:

with (this) {

return _c(“div”, [_t(“bar”), _t(“foo”, null, null, { msg })], 2);

}

已经有那么些端倪了,接下来就研究一下 _t 函数的实现,就可以接近真相了。

_t 也就是 renderSlot的别名,简化后的实现是这样的:

export function renderSlot (

name: string,

fallback: ?Array,

props: ?Object,

bindObject: ?Object

): ?Array {

// 通过 name 拿到函数

const scopedSlotFn = this.$scopedSlots[name]

let nodes

if (scopedSlotFn) { // scoped slot

props = props || {}

// 执行函数返回 vnode

nodes = scopedSlotFn(props) || fallback

}

return nodes

}

其实很简单,

如果是 普通插槽,就直接调用函数生成 vnode,如果是 作用域插槽

就直接带着 props 也就是 { msg } 去调用函数生成 vnode。2.6 版本后统一为函数的插槽降低了很多心智负担。

更新

在上面的 test 组件中, 1s 后我们通过 this.msg = "Changed"; 触发响应式更新,此时编译后的 render 函数:

with (this) {

return _c(“div”, [_t(“bar”), _t(“foo”, null, null, { msg })], 2);

}

重新执行,此时的 msg 已经是更新后的 Changed 了,自然也就实现了更新。

一种特殊情况是,在父组件的作用于里也使用了响应式的属性并更新,比如这样:

Hello

{{prop.msg}}

其实,是因为执行 _t 函数时,全局的组件渲染上下文是 子组件,那么依赖收集自然也就是收集到 子组件的依赖了。所以在 msgInParent 更新后,其实是直接去触发子组件的重新渲染的,对比 2.5 的版本,这是一个优化。

那么还有一些额外的情况,比如说 template 上有 v-ifv-for 这种情况,举个例子来说:

Hello

function render() {

with(this) {

return _c(‘test’, {

scopedSlots: _u([(show) ? {

key: “bar”,

fn: function () {

return [_c(‘span’, [_v(“Hello”)])]

},

proxy: true

} : null], null, true)

})

}

}

注意这里的 _u 内部直接是一个三元表达式,读取 _u 是发生在父组件的 _render 中,那么此时子组件是收集不到这个 show 的依赖的,所以说 show 的更新只会触发父组件的更新,那这种情况下子组件是怎么重新执行 $scopedSlot 函数并重渲染的呢?

我们已经有了一定的前置知识:Vue的更新粒度[2],知道 Vue 的组件不是递归更新的,但是 slotScopes 的函数执行是发生在子组件内的,父组件在更新的时候一定是有某种方式去通知子组件也进行更新。

其实这个过程就发生在父组件的重渲染的 patchVnode中,到了 test 组件的 patch 过程,进入了 updateChildComponent 这个函数后,会去检查它的 slot 是否是稳定的,显然 v-if 控制的 slot 是非常不稳定的。

const newScopedSlots = parentVnode.data.scopedSlots

const oldScopedSlots = vm.$scopedSlots

const hasDynamicScopedSlot = !!(

(newScopedSlots && !newScopedSlots.$stable) ||

(oldScopedSlots !== emptyObject && !oldScopedSlots.$stable) ||

(newScopedSlots && vm. s c o p e d S l o t s . scopedSlots. scopedSlots.key !== newScopedSlots.$key)

)

// 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.

const needsForceUpdate = !!hasDynamicScopedSlot

if (needsForceUpdate) {

// 这里的 vm 对应 test 也就是子组件的实例,相当于触发了子组件强制渲染。

vm.$forceUpdate()

}

这里有一些优化措施,并不是说只要有 slotScope 就会去触发子组件强制更新。

有如下三种情况会强制触发子组件更新:

  1. scopedSlots 上的 $stable 属性为 false

一路追寻这个逻辑,最终发现这个 $stable_u 也就是 resolveScopedSlots 函数的第三个参数决定的,由于这个 _u 是由编译器生成 render 函数时生成的的,那么就到 codegen 的逻辑中去看:

let needsForceUpdate = el.for || Object.keys(slots).some(key => {

const slot = slots[key]

return (

slot.slotTargetDynamic ||

slot.if ||

slot.for ||

containsSlotChild(slot) // is passing down slot from parent which may be dynamic

)

})

简单来说,就是用到了一些动态语法的情况下,就会通知子组件对这段 scopedSlots 进行强制更新。

  1. 也是 $stable 属性相关,旧的 scopedSlots 不稳定

这个很好理解,旧的scopedSlots需要强制更新,那么渲染后一定要强制更新。

  1. 旧的 $key 不等于新的 $key

这个逻辑比较有意思,一路追回去看 $key 的生成,可以看到是 _u 的第四个参数 contentHashKey,这个contentHashKey 是在 codegen 的时候利用 hash 算法对生成代码的字符串进行计算得到的,也就是说,这串函数的生成的 字符串 改变了,就需要强制更新子组件。

function hash(str) {

let hash = 5381

let i = str.length

while(i) {

hash = (hash * 33) ^ str.charCodeAt(–i)

}

return hash >>> 0

}

总结

Vue 2.6 版本后对 slotslot-scope  做了一次统一的整合,让它们全部都变为函数的形式,所有的插槽都可以在 this.$slotScopes 上直接访问,这让我们在开发高级组件的时候变得更加方便。

在优化上,Vue 2.6 也尽可能的让 slot 的更新不触发父组件的渲染,通过一系列巧妙的判断和算法去尽可能避免不必要的渲染。(在 2.5 的版本中,由于生成 slot 的作用域是在父组件中,所以明明是子组件的插槽 slot 的更新是会带着父组件一起更新的)

之前听尤大的演讲,Vue3 会更多的利用模板的静态特性做更多的预编译优化,在文中生成代码的过程中我们已经感受到了他为此付出努力,非常期待 Vue3 带来的更加强悍的性能。

参考资料


[1]
Vue 2.6 发布了: https://zhuanlan.zhihu.com/p/56260917
[2]
Vue的更新粒度: https://juejin.im/post/5e854a32518825736c5b807f#heading-3

其实前端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

这里再分享一个复习的路线:(以下体系的复习资料是我从各路大佬收集整理好的)

《前端开发四大模块核心知识笔记》

最后,说个题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值