阅读 49

[vue解析]插槽在vue中是如何编译和执行的?

前言

slotsvue的一个核心功能,它和keep-alive、transition组件都有关系,所以我们有必要对它做一个全面的了解。

<div id="app">   <app-layout>     <template v-slot:header>头部内容</template>     <template v-slot>默认内容</template>     <template v-slot:footer>底部内容</template>   </app-layout>   <button @click="change">change title</button> </div> <script>   const AppLayout = {     template:       '<div class="container">' +       '<header><slot name="header"></slot></header>' +       '<main><slot></slot></main>' +       '<footer><slot name="footer"></slot></footer>' +       '</div>'   }   const vm = new Vue({     el: '#app',     components: {       AppLayout     }   }) </script> 复制代码

因为vue-next已经全面改成使用v-slot了,所以我们分析插槽只分析v-slot API

注意 当我们开始分析vue的指令的时候,我们需要分两步走:

  1. 编译阶段做了哪些工作,ASTrender方法是什么样子的?

  2. 初始化做了什么工作,执行的时候又做了什么?

插槽的编译

vue的编译分为三步

  1. parse:将html转换成AST

  2. opitimize: 将AST中不变的数据标识成static,优化的作用

  3. codegen: 将AST代码转成字符串,并通过new Function实例化为函数

parse

那么我们先看第一步,在parse方法里面vue调用了parseHTML方法。parseHTML方法传入两个参数,一个是html字符串, 在第二个参数里面传入了四个钩子函数start、end、chars、comment,这里要讲清楚的话比较麻烦,而我们通过查看执行栈的方式来看parse的执行过程。

首先我们知道我们现在的例子和slot有关,那么很显然vue在定义方法的时候肯定和slot有关系,我们查找到两个方法processSlotContentprocessSlotOutlet两个方法。 在第一个方法打上断点,查看执行栈

执行栈

这样就能清晰的看到代码的执行过程了。这里我们进入processSlotContent方法,开始分析

function processSlotContent (el) {   // 2.6 v-slot syntax   if (process.env.NEW_SLOT_SYNTAX) {     if (el.tag === 'template') {       // v-slot on <template>       const slotBinding = getAndRemoveAttrByRegex(el, slotRE)       if (slotBinding) {         const { name, dynamic } = getSlotName(slotBinding)         el.slotTarget = name         el.slotTargetDynamic = dynamic         el.slotScope = slotBinding.value || emptySlotScopeToken        }     }   } } 复制代码

这里我们先记录一下,el当前的对象中attrsList的值

el: {   attrsList: [{     end: 68,     name: "v-slot:header",     start: 52,     value: ""   }] } 复制代码

然后直接跳过getAndRemoveAttrByRegex方法查看新值和返回值,attrsList被清空了,并且我们拿到了里面的值

{  end: 68,  name: "v-slot:header",  start: 52,  value: "" } 复制代码

getSlotName方法拿到name值,这个值是header。并且这里也会判断是不是动态slot和新增特性相关。 最后vue赋值了三个属性

slotScope: "_empty_" slotTarget: "\"header\"" slotTargetDynamic: false 复制代码

这里我们还要去closeElement方法看一下

function closeElement (element) {  if (!inVPre && !element.processed) {    element = processElement(element, options)  }  if (currentParent && !element.forbidden) {    if (element.elseif || element.else) {      ...    } else {      if (element.slotScope) {        const name = element.slotTarget || '"default"'        ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element      }      currentParent.children.push(element)      element.parent = currentParent    }  }  element.children = element.children.filter(c => !c.slotScope) } 复制代码

在执行完processElement之后,我们看上面的代码,currentParentstart钩子函数中赋值,拿到是当前的element的父级。

这里我们看到将当前element元素放到了currentParentscopedSlotschildren

为什么要在scopedSlots中也放一份?

为了正确维护v-if的关系,看下面这段代码,过滤slotScope中不存在的数据

element.children = element.children.filter(c => !c.slotScope) 复制代码

这样父组件内的第一个v-slot就解析完毕了。

好,我们看最终生成的AST,父组件是这样的

const ast = {   children: [     {       tag: "app-layout",       scopedSlots: {         'default': {slotTarget: '"default"', slotScope: '_empty_', parent: 'parentAST'},         'footer': {slotTarget: '"footer"', slotScope: '_empty_', parent: 'parentAST'},         'header': {slotTarget: '"header"', slotScope: '_empty_', parent: 'parentAST'},       }     }   ] } 复制代码

什么时候进入子组件?

只要看el.tag是否是app-layout就行。好,开始执行子组件解析的时候,我们进入processSlotOutlet

function processSlotOutlet (el) {   if (el.tag === 'slot') {     el.slotName = getBindingAttr(el, 'name')   } } 复制代码

这里主要是赋值了slotName。这个就很简单,我们直接看子组件的AST

const childAst = {   children: [     {       tag: 'header',       children: [{slotName:'header', tag:'slot'}]     }     ...   ] } 复制代码

codegen

拿到AST之后,我们就需要将它转换成字符串代码,在src/compiler/index.js文件中,我们可以看到 vue编译的三个步骤,那么我们现在就来看看generate(ast, options)。在该方法中,我们可以看到, 主要分两步

  1. 初始化options,拿到state

  2. 通过genElement拿到code

我们直接看genElement,对于父组件,我们看其中的genData

export function genData (el: ASTElement, state: CodegenState): string {   let data = '{'   if (el.scopedSlots) {     data += `${genScopedSlots(el, el.scopedSlots, state)},`   }   return data } 复制代码

看上面父组件的AST,我们要拿到children才会走上面的逻辑,所以直接跳到子el。进入genScopedSlots

function genScopedSlots (   el: ASTElement,   slots: { [key: string]: ASTElement },   state: CodegenState ): string {   // 优化的代码,先去掉   const generatedSlots = Object.keys(slots)     .map(key => genScopedSlot(slots[key], state))     .join(',')   return `scopedSlots:_u([${generatedSlots}]${     needsForceUpdate ? `,null,true` : ``   }${     !needsForceUpdate && needsKey ? `,null,false,${hash(generatedSlots)}` : ``   })` } 复制代码

前面是关于在v-slots中使用v-if的优化。genScopedSlot就是每一个v-slot生成的过程,里面就不进去看了,也是很简单的判断和字符串拼接,我们直接看结果。

{scopedSlots:_u([   {key:"header",fn:function(){return [_v("头部内容")]},proxy:true},   {key:"default",fn:function(){return [_v("默认内容")]},proxy:true},   {key:"footer",fn:function(){return [_v("底部内容")]},proxy:true}]) } 复制代码

父组件返回的字符串结果如图,我们看子组件。在子组件中,它的AST关键在于el.tag=slot 所以我们看的是

export function genElement (el: ASTElement, state: CodegenState): string {   if (el.parent) {     el.pre = el.pre || el.parent.pre   } else if (el.tag === 'slot') {     return genSlot(el, state)   }   return code } 复制代码

所以我们就可以拿到它的code

_c('div',{staticClass:"container"}, [_c('header',[_t("header")],2), _c('main',[_t("default")],2), _c('footer',[_t("footer")],2)] 复制代码

执行

编译阶段结束,那么我们就能拿到匿名执行函数去执行,在之前的文章我曾经说过,我们在执行匿名函数的时候,其实就是在执行render-helpers里面定义的方法,那么_u就是resolveScopedSlots方法

export function resolveScopedSlots (   fns: ScopedSlotsData, // see flow/vnode   res?: Object,   // the following are added in 2.6   hasDynamicKeys?: boolean,   contentHashKey?: number ): { [key: string]: Function, $stable: boolean } {   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) {         slot.fn.proxy = true       }       // 规整化为 插槽名称:fn       res[slot.key] = slot.fn     }   }   if (contentHashKey) {     res.$key = contentHashKey   }   return res } 复制代码

看这个方法,fns就是我们上面通过_u([...])传入的数组,并且vueslots.fn.proxy=true。最终的返回是

res: {   default: fn(),   footer:fn(),   header: fn(), } 复制代码

父组件处理完了,我们看子组件。看上面vue调用的_t方法。在render-helpers中是renderSlot方法,

export function renderSlot (   name: string,   fallbackRender: ?((() => Array<VNode>) | Array<VNode>),   props: ?Object,   bindObject: ?Object ): ?Array<VNode> {   const scopedSlotFn = this.$scopedSlots[name]   let nodes   if (scopedSlotFn) {     // scoped slot     props = props || {}     nodes =       scopedSlotFn(props) ||       (typeof fallbackRender === 'function' ? fallbackRender() : fallbackRender)   }    return nodes } 复制代码

唉,我们这里看到this.$scopedSlots里面好像存着,父组件上面解析好的slots这是为什么,哪里赋值的呢?

我们回过头来看一段代码,在Vue.prototype._render方法中

Vue.prototype._render = function (): VNode {     const vm: Component = this     // 通过规整化好后的 $options拿到 渲染函数     const { render, _parentVnode } = vm.$options     // 拿到$scopeSlots     if (_parentVnode) {       vm.$scopedSlots = normalizeScopedSlots(         _parentVnode.data.scopedSlots,         vm.$slots,         vm.$scopedSlots       )     }     return vnode   } 复制代码

在子组件中_parentVnode是肯定存在的,那么就会调用normalizeScopedSlots,它传的第一个参数就是父组件上的scopedSlots对象,那么很显然,当前的scopedSlots也就有值了。

回过头去看上面的方法,scopedSlotFn存在值,那么就被执行了,执行的是return [_v("头部内容")]。因为props不存在值,那么直接的就返回了nodes。这样插槽就会被渲染到子组件了

作用域插槽

先看例子

<div id="app">  <app-layout>    <template v-slot:header="props">头部内容 {{props.msg}}</template>    <template v-slot="props">默认内容 {{props.msg}}</template>    <template v-slot:footer="props">底部内容 {{props.msg}}</template>  </app-layout> </div> <script>  const AppLayout = {    data() {      return {        msg1: 'header',        msg2: 'default',        msg3: 'footer'      }    },    template:      '<div class="container">' +      '<header><slot name="header" :msg="msg1"></slot></header>' +      '<main><slot :msg="msg2"></slot></main>' +      '<footer><slot name="footer" :msg="msg3"></slot></footer>' +      '</div>'  }  const vm = new Vue({    el: '#app',    components: {      AppLayout    }  }) </script> 复制代码

例子很简单,这次我们不一步步分析了,直接来看父组件和子组件的最终code

scopedSlots:_u([   {key:"header",fn:function(props){return [_v("头部内容 "+_s(props.msg))]}},{key:"default",fn:function(props){return [_v("默认内容 "+_s(props.msg))]}},{key:"footer",fn:function(props){return [_v("底部内容 "+_s(props.msg))]}} ]) 复制代码

不同点在于,方法中传了参数,并且调用了_s

再看子组件

[_c('header',[_t("header",null,{"msg":msg1})],2), _c('main',[_t("default",null,{"msg":msg2})],2), _c('footer',[_t("footer",null,{"msg":msg3})],2)] 复制代码

这里我们传入了多个参数,那么很简单,我们去看resolveScopedSlotsrenderSlot方法。 前者没什么不同,还是把数组形式转换成了对象形式。看renderSlot方法。

nodes = scopedSlotFn(props) 复制代码

那么再来看子组件,首先在运行到msg1的时候,vue会触发依赖收集,这都是在子组件进行的。然后触发_t 这时候我们可以看到通过get拦截器vue拿到了值_t('footer', null, {msg: 'header'})。那么在方法中,就能正确的渲染props.msg

因为上面的依赖收集,当我们修改msg1的时候,就会触发子组件更新,这时候就会重新调用_t执行一遍

总结

插槽就是一段函数,可以看到,父组件里面的模板是父级作用域编译的,子组件通过拿到scopedSlots执行里面的方法,然后在子组件生成vnode渲染了真实dom。那么也就是官网中的话

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的


作者:idenet
链接:https://juejin.cn/post/7054095893015822349


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐