zoukankan      html  css  js  c++  java
  • VUE高阶组件解析

    一、高阶组件介绍

      vue 高阶组件的认识,在React中组件是以复用代码实现的,而Vue中是以mixins 实现,并且官方文档中也缺少一些高阶组件的概念,因为在vue中实现高阶组很困难,并不像React简单,其实vue中mixins也同样可以代替,在读了一部分源码之后,对vue有了更深的认识。

      所谓高阶组件其实就是高阶函数啦,ReactVue 都证明了一件事儿:一个函数就是一个组件。所以组件是函数这个命题成立了。所谓高阶组件其实就是一个高阶函数, 即返回一个组件函数的函数,Vue中怎么实现呢? 注意高阶组件有如下特点

    1. 高阶组件(HOC)应该是无副作用的纯函数,且不应该修改原组件,即原组件不能有变动
    2. 高阶组件(HOC)不关心你传递的数据(props)是什么,并且新生成组件不关心数据来源
    3. 高阶组件(HOC)接收到的 props 应该透传给被包装组件即直接将原组件prop传给包装组件
    4. 高阶组件完全可以添加、删除、修改 props

    二、高阶组件举例

      Base.vue

    <template>
      <div>
        <p @click="Click">props: {{test}}</p>
      </div>
    </template>
    <script>
    export default {
      name: 'Base',
      props: {
        test: Number
      },
      methods: {
        Click () {
          this.$emit('Base-click')
        }
      }
    }
    </script>

      Vue 组件主要就是三点:props、event 以及 slots。对于 Base组件而言,它接收一个数字类型的 props 即 test,并触发一个自定义事件,事件的名称是:Base-click,没有 slots。我们会这样使用该组件:

    <Base @Base-click="xxxx" :test="100" /></Base>

      现在我们需要 base-component 组件每次挂载完成的时候都打印一句话:haha,同时这也许是很多组件的需求,所以按照 mixins 的方式,我们可以这样做,首先定义个 mixins

    export default consoleMixin {
      mounted () {
        console.log('haha')
      }
    }

      然后在 Base 组件中将 consoleMixin 混入:

    <template>
      <div>
        <p @click="Click">props: {{test}}</p>
      </div>
    </template>
    <script>
    export default {
      name: 'Base',
      props: {
        test: Number
      },
      mixins: [ consoleMixin ],
      methods: {
        Click () {
          this.$emit('Base-click')
        }
      }
    }
    </script>

      这样使用 Base 组件的时候,每次挂载完成之后都会打印一句 haha,不过现在我们要使用高阶组件的方式实现同样的功能,回忆高阶组件的定义:接收一个组件作为参数,返回一个新的组件,那么此时我们需要思考的是,在 Vue 中组件是什么?Vue 中组件是函数,不过那是最终结果,比如我们在单文件组件中的组件定义其实就是一个普通的选项对象,如下:

    export default {
      name: 'Base',
      props: {...},
      mixins: [...]
      methods: {...}
    }

      这难道不是一个纯对象嘛

    import Base from './Base.vue'
    console.log(Base)

      这里的Base是什么呢?对,就是一个JSON对象(如图),而当以把他加入到一个组件的components,Vue最终会以该参数即option来构造实例的构造函数,所以Vue中组件就是个函数,但是在引入之前仍只是一个options对象,所以这样就很好明白了 Vue中组件开始只是一个对象,即高阶组件就是:一个函数接受一个纯对象,并且返回一个新纯对象

    export default function Console (BaseComponent) {
      return {
        template: '<wrapped v-on="$listeners" v-bind="$attrs"/>',
        components: {
          wrapped: BaseComponent
        },
        mounted () {
          console.log('haha')
        }
      }
    }

      这里 Console就是一个高阶组件,它接受一个参数 BaseComponent即传入的组件,返回一个新组件,将BaseComponent作为新组件的子组件并且在mounted里设置钩子函数 打印haha,我们可以完成mixins同样做到的事,我们并没有修改子组件Base,这里的 $listeners $attrs 其实是在透传props 和事件 那这样真的就完美解决问题了吗?不是的,首先 template 选项只有在完整版的 Vue 中可以使用,在运行时版本中是不能使用的,所以最起码我们应该使用渲染函数(render)替代模板(template)

    // Console.js
        export default function Console (BaseComponent) {
          return {
            mounted () {
              console.log('haha')
            },
            render (h) {
              return h(BaseComponent, {
                on: this.$listeners,
                attrs: this.$attrs,
              })
            }
          }
        }

      我们将模板改写成了渲染函数,看上去没什么问题,实际还是有问题,上面的代码中 BaseComponent 组件依然收不到 props,为什么呢,我们不是已经在 h 函数的第二个参数中将 attrs 传递过去了吗,怎么还收不到?当然收不到,attrs 指的是那些没有被声明为 props 的属性,所以在渲染函数中还需要添加 props 参数

        export default function Console (BaseComponent) {
          return {
            mounted () {
              console.log('haha')
            },
            render (h) {
              return h(BaseComponent, {
                on: this.$listeners,
                attrs: this.$attrs,
                props: this.$props
              })
            }
          }
        }

      那这样呢?其实还是不行,props始终是空对象,这里的props是高阶组件的对象,但是高阶组件并没有声明props所以如此故要再声明一个props

        export default function Console (BaseComponent) {
          return {
            mounted () {
              console.log('haha')
            },
            props: BaseComponent.props,
            render (h) {
              return h(BaseComponent, {
                on: this.$listeners,
                attrs: this.$attrs,
                props: this.$props
              })
            }
          }
        }

      那么一个差不多的高阶组件就完成了,但是还没完,我们只实现了:透传props、透传事件,就剩下slot了,我们修改 Base 组件为其添加一个具名插槽和默认插槽。

    // Base.vue
    <template>
      <div>
        <span @click="handleClick">props: {{test}}</span>
        <slot name="slot1"/> <!-- 具名插槽 --></slot>
        <p>===========</p>
        <slot><slot/> <!-- 默认插槽 -->
      </div>
    </template>
     
    <script>
    export default {
      ...
    }
    </script>
        <template>
          <div>
            <Base>
              <h2 slot="slot1">BaseComponent slot</h2>
              <p>default slot</p>
            </Base>
            <wrapBase>
              <h2 slot="slot1">EnhancedComponent slot</h2>
              <p>default slot</p>
            </wrapBase>
          </div>
        </template>
         
        <script>
          import Base from './Base.vue'
          import Console from './Console.js'
         
          const wrapBase = Console(Base)
         
          export default {
            components: {
              Base,
              wrapBase
            }
          }
        </script>

      这里的执行结果就是 wrapBase里的slot都没有了 所以就要改一下高阶组建了

    function Console (BaseComponent) {
      return {
        mounted () {
          console.log('haha')
        },
        props: BaseComponent.props,
        render (h) {
     
          // 将 this.$slots 格式化为数组,因为 h 函数第三个参数是子节点,是一个数组
          const slots = Object.keys(this.$slots)
            .reduce((arr, key) => arr.concat(this.$slots[key]), [])
     
          return h(BaseComponent, {
            on: this.$listeners,
            attrs: this.$attrs,
            props: this.$props
          }, slots) // 将 slots 作为 h 函数的第三个参数
        }
      }
    }

      这时 slot内容确实渲染出来了 但是顺序不太对,高阶组件的全部渲染到了末尾。其实 Vue在处理具名插槽会考虑作用域的因素,首先 Vue 会把模板(template)编译成渲染函数(render),比如如下模板:

    <div>
      <p slot="slot1">Base slot</p>
    </div>

      会被编译成如下渲染函数:

    var render = function() {
      var _vm = this
      var _h = _vm.$createElement
      var _c = _vm._self._c || _h
      return _c("div", [
        _c("div", {
          attrs: { slot: "slot1" },
          slot: "slot1"
        }, [
          _vm._v("Base slot")
        ])
      ])
    }

      观察上面的渲染函数,我们发现普通的 DOM 是通过 _c 函数创建对应的 VNode 的。现在我们修改模板,模板中除了有普通 DOM 之外,还有组件,如下:

    <div>
      <Base>
        <p slot="slot1">Base slot</p>
        <p>default slot</p>
      </Base>
    </div>

      其render函数

        var render = function() {
          var _vm = this
          var _h = _vm.$createElement
          var _c = _vm._self._c || _h
          return _c(
            "div",
            [
              _c("Base", [
                _c("p", { attrs: { slot: "slot1" }, slot: "slot1" }, [
                  _vm._v("Base slot")
                ]),
                _vm._v(" "),
                _c("p", [_vm._v("default slot")])
              ])
            ],
          )
        }

      我们发现无论是普通DOM还是组件,都是通过 _c 函数创建其对应的 VNode 的,其实 _c 在 Vue 内部就是 createElement 函数。

      createElement 函数会自动检测第一个参数是不是普通DOM标签。如果不是普通DOM标签那么 createElement 会将其视为组件,并且创建组件实例(注意组件实例是这个时候才创建的)但是创建组件实例的过程中就面临一个问题:组件需要知道父级模板中是否传递了 slot 以及传递了多少,传递的是具名的还是不具名的等等。那么子组件如何才能得知这些信息呢?很简单,假如组件的模板如下

    <div>
      <Base>
        <p slot="slot1">Base slot</p>
        <p>default slot</p>
      </Base>
    </div>

      父组件的模板最终会生成父组件对应的 VNode,所以以上模板对应的 VNode 全部由父组件所有,那么在创建子组件实例的时候能否通过获取父组件的 VNode 进而拿到 slot 的内容呢?即通过父组件将下面这段模板对应的 VNode 拿到

    <Base>
        <p slot="slot1">Base slot</p>
        <p>default slot</p>
    </Base>

      如果能够通过父级拿到这段模板对应的 VNode,那么子组件就知道要渲染哪些 slot 了,其实 Vue 内部就是这么干的,实际上你可以通过访问子组件的 this.$vnode 来获取这段模板对应的 VNode。

      this.$vnode 并没有写进 Vue 的官方文档

      子组件拿到了需要渲染的 slot 之后进入到了关键的一步,这一步就是导致高阶组件中透传 slot 给 Base组件 却无法正确渲染的原因。children的VNode中的context引用父组件实例,其本身的context也会引用本身实例 其实是一个东西

    console.log(this.$vnode.context === this.$vnode.componentOptions.children[0].context) //ture

      而 Vue 内部做了一件很重要的事儿,即上面那个表达式必须成立,才能够正确处理具名 slot,否则即使 slot 具名也不会被考虑,而是被作为默认插槽。这就是高阶组件中不能正确渲染 slot 的原因

      即 高阶组件中 本来是父组件和子组件之间插入了一个组件(高阶组件),而子组件的 this.$vnode其实是高阶组件的实例,但是我们将slot透传给子组件,slot里 VNode 的context实际引用的还是父组件 所以

    console.log(this.$vnode.context === this.$vnode.componentOptions.children[0].context) // false

      最终导致具名插槽被作为默认插槽,从而渲染不正确。

      解决办法也很简单,只需要手动设置一下 slot 中 VNode 的 context 值为高阶组件实例即可

    function Console (Base) {
      return {
        mounted () {
          console.log('haha')
        },
        props: Base.props,
        render (h) {
          const slots = Object.keys(this.$slots)
            .reduce((arr, key) => arr.concat(this.$slots[key]), [])
            // 手动更正 context
            .map(vnode => {
              vnode.context = this._self //绑定到高阶组件上
              return vnode
            })
     
          return h(WrappedComponent, {
            on: this.$listeners,
            props: this.$props,
            attrs: this.$attrs
          }, slots)
        }
      }
    }

      说明白就是强制把slot的归属权给高阶组件,而不是 父组件。通过当前实例 _self 属性访问当实例本身,而不是直接使用 this,因为 this 是一个代理对象。

  • 相关阅读:
    2018-06-15for与数组/for-in与数组/一维二维多维数组
    2018-06-14控制语句for+switch+while循环
    2018-06-13JS分支结构+转义符
    2018-06-12JavaScript基础知识1
    2018-06-11笔记
    2018-06-08CSS常用样式+浮动+定位+盒子模型
    2018-06-06Div+CSS基础理论
    2018-06-05表单结构
    2018-06-04表格结构+表格嵌套
    2018-06-02笔记
  • 原文地址:https://www.cnblogs.com/goloving/p/13678332.html
Copyright © 2011-2022 走看看