目录
  • 问题三、是否可以不使用vue.extend

场景介绍

最近做h5遇到了一个场景:每个页面需要展示一个带有标题的头部。一个实现思路是使用全局组件。假设我们创建一个名为theheader.vue的全局组件,伪代码如下:

<template>
    <h2>{{ title }}</h2>
</template>

<script>
export default {
props: {
    title: {
        type: string,
        default: ''
    }
}
}
</script>

创建好全局组件后,在每个页面组件中引用该组件并传入props中即可。例如我们在页面a中引用该组件,页面a对应的组件是a.vue

<template>
    <div>
        <theheader :title="title" />
    </div>
</template>
<script>
    export default {
        data() {
            title: ''
        },
        created(){
            this.title = '我的主页'
        }
    }
</script>

使用起来非常简单,不过有一点美中不足:如果头部组件需要传入的props很多,那么在页面组件中维护对应的props就会比较繁琐。针对这种情况,有一个更好的思路来实现这个场景,就是使用vue插件。

同样是在a.vue组件调用头部组件,使用vue插件的调用方式会更加简洁:

<template>
    <div />
</template>
<script>
    export default {
        created(){
            this.$setheader('我的主页')
        }
    }
</script>

我们看到,使用vue插件来实现,不需要在a.vue中显式地放入theheader组件,也不需要在a.vue的data函数中放入对应的props,只需要调用一个函数即可。那么,这个插件是怎么实现的呢?

插件实现

它的实现具体实现步骤如下:

  1. 创建一个sfc(single file component),这里就是theheader组件
  2. 创建一个plugin.js文件,引入sfc,通过vue.extend方法扩展获取一个新的vue构造函数并实例化。
  3. 实例化并通过函数调用更新vue组件实例。

按照上面的步骤,我们来创建一个plugin.js文件:

import theheader from './theheader.vue'
import vue from 'vue'

const headerplugin = {
    install(vue) {
        const vueinstance = new (vue.extend(theheader))().$mount()
        vue.prototype.$setheader = function(title) {
            vueinstance.title = title
            document.body.prepend(vueinstance.$el)
            
        }
    }
}
vue.use(headerplugin)

我们随后在main.js中引入plugin.js,就完成了插件实现的全部逻辑过程。不过,尽管这个插件已经实现了,但是有不少问题。

问题一、重复的头部组件

如果我们在单页面组件中使用,只要使用router.push方法之后,我们就会发现一个神奇的问题:在新的页面出现了两个头部组件。如果我们再跳几次,头部组件的数量也会随之增加。这是因为,我们在每个页面都调用了这个方法,因此每个页面都在文档中放入了对应dom。

考虑到这点,我们需要对上面的组件进行优化,我们把实例化的过程放到插件外面:

import theheader from './theheader.vue'
import vue from 'vue'

const vueinstance = new (vue.extend(theheader))().$mount()
const headerplugin = {
    install(vue) {
        vue.prototype.$setheader = function(title) {
            vueinstance.title = title
            document.body.prepend(vueinstance.$el)
            
        }
    }
}
vue.use(headerplugin)

这样处理,虽然还是会重复在文档中插入dom。不过,由于是同一个vue实例,对应的dom没有发生改变,所以插入的dom始终只有一个。这样,我们就解决了展示多个头部组件的问题。为了不重复执行插入dom的操作,我们还可以做一个优化:

import theheader from './theheader.vue'
import vue from 'vue'

const vueinstance = new (vue.extend(theheader))().$mount()
const hasprepend = false
const headerplugin = {
    install(vue) {
        vue.prototype.$setheader = function(title) {
            vueinstance.title = title
            if (!hasprepend) {
                document.body.prepend(vueinstance.$el)
                hasprepend = true
            }
            
        }
    }
}
vue.use(headerplugin)

增加一个变量来控制是否已经插入了dom,如果已经插入了,就不再执行插入的操作。优化以后,这个插件的实现就差不多了。不过,个人在实现过程中有几个问题,这里也一并记录一下。

问题二、另一种实现思路

在实现过程中突发奇想,是不是可以直接修改theheader组件的data函数来实现这个组件呢?看下面的代码:

import theheader from './theheader.vue'
import vue from 'vue'

let el = null
const headerplugin = {
    install(vue) {
        vue.prototype.$setheader = function(title) {
            theheader.data = function() {
                title
            }
            const vueinstance = new (vue.extend(theheader))().$mount()
            el = vueinstance.$el
            if (el) {
                document.body.removechild(el)
                document.body.prepend(el)
            }
            
        }
    }
}
vue.use(headerplugin)

看上去也没什么问题。不过实践后发现,调用$setheader方法,只有第一次传入的值会生效。例如第一次传入的是’我的主页’,第二次传入的是’个人信息’,那么头部组件将始终展示我的主页,而不会展示个人信息。原因是什么呢?

深入vue源码后发现,在第一次调用new vue以后,header多了一个ctor属性,这个属性缓存了header组件对应的构造函数。后续调用new vue(theheader)时,使用的构造函数始终都是第一次缓存的,因此title的值也不会发生变化。vue源码对应的代码如下:

vue.extend = function (extendoptions) {
    extendoptions = extendoptions || {};
    var super = this;
    var superid = super.cid;
    var cachedctors = extendoptions._ctor || (extendoptions._ctor = {}); 
    if (cachedctors[superid]) { // 如果有缓存,直接返回缓存的构造函数
      return cachedctors[superid]
    }

    var name = extendoptions.name || super.options.name;
    if (process.env.node_env !== 'production' && name) {
      validatecomponentname(name);
    }

    var sub = function vuecomponent (options) {
      this._init(options);
    };
    sub.prototype = object.create(super.prototype);
    sub.prototype.constructor = sub;
    sub.cid = cid++;
    sub.options = mergeoptions(
      super.options,
      extendoptions
    );
    sub['super'] = super;

    // for props and computed properties, we define the proxy getters on
    // the vue instances at extension time, on the extended prototype. this
    // avoids object.defineproperty calls for each instance created.
    if (sub.options.props) {
      initprops$1(sub);
    }
    if (sub.options.computed) {
      initcomputed$1(sub);
    }

    // allow further extension/mixin/plugin usage
    sub.extend = super.extend;
    sub.mixin = super.mixin;
    sub.use = super.use;

    // create asset registers, so extended classes
    // can have their private assets too.
    asset_types.foreach(function (type) {
      sub[type] = super[type];
    });
    // enable recursive self-lookup
    if (name) {
      sub.options.components[name] = sub;
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if super's options have
    // been updated.
    sub.superoptions = super.options;
    sub.extendoptions = extendoptions;
    sub.sealedoptions = extend({}, sub.options);

    // cache constructor
    cachedctors[superid] = sub; // 这里就是缓存ctor构造函数的地方
    return sub
  }

找到了原因,我们会发现这种方式也是可以的,我们只需要在plugin.js中加一行代码

import theheader from './theheader.vue'
import vue from 'vue'

let el = null
const headerplugin = {
    install(vue) {
        vue.prototype.$setheader = function(title) {
            theheader.data = function() {
                title
            }
            theheader.ctor = {}
            const vueinstance = new vue(theheader).$mount()
            el = vueinstance.$el
            if (el) {
                document.body.removechild(el)
                document.body.prepend(el)
            }
            
        }
    }
}
vue.use(headerplugin)

每次执行$setheader方法时,我们都将缓存的构造函数去掉即可。

问题三、是否可以不使用vue.extend

实测其实不使用vue.extend,直接使用vue也是可行的,相关代码如下:

import theheader from './theheader.vue'
import vue from 'vue'

const vueinstance = new vue(theheader).$mount()
const hasprepend = false
const headerplugin = {
    install(vue) {
        vue.prototype.$setheader = function(title) {
            vueinstance.title = title
            if (!hasprepend) {
                document.body.prepend(vueinstance.$el)
                hasprepend = true
            }
            
        }
    }
}
vue.use(headerplugin)

直接使用vue来创建实例相较extend创建实例来说,不会在header.vue中缓存ctor属性,相较来说是一个更好的办法。但是之前有看过vant实现toast组件,基本上是使用vue.extend方法而没有直接使用vue,这是为什么呢?

总结

到此这篇关于vue插件实现过程中遇到问题的文章就介绍到这了,更多相关vue插件实现问题内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!