美文网首页
Vue.extend 源码分析

Vue.extend 源码分析

作者: 隔壁老王z | 来源:发表于2021-09-01 21:06 被阅读0次

    Vue.extend是 Vue 构造函数的一个静态方法,它提供了一种灵活的挂载组件的方式,它在日常开发中使用不多,但是在一些特殊场景会派上用场。在 ElementUI 里,我们使用this.$message('hello')的时候,其实就是通过这种方式创建一个组件实例,然后再将这个组件挂载到了 body 上。

    // vue 官方文档中的示例
    // 创建构造器
    var Profile = Vue.extend({
      template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
      data: function () {
        return {
          firstName: 'Walter',
          lastName: 'White',
          alias: 'Heisenberg'
        }
      }
    })
    // 创建 Profile 实例,并挂载到一个元素上。
    new Profile().$mount('#mount-point')
    

    Vue.extend(...)的返回值是一个继承了Vue构造函数的函数。
    在目录src/core/global-api/extend.js可以找到它的定义:

    export function initExtend(Vue: GlobalAPI) {
      // 这个cid是一个全局唯一的递增的id
      // 缓存的时候会用到它
      Vue.cid = 0
      let cid = 1
    
      /**
       * Class inheritance
       */
      Vue.extend = function(extendOptions: Object): Function {
        // extendOptions就是我我们传入的组件options
        extendOptions = extendOptions || {}
        const Super = this
        const SuperId = Super.cid
        // 每次创建完Sub构造函数后,都会把这个函数储存在extendOptions上的_Ctor中
        // 下次如果用再同一个extendOptions创建Sub时
        // 就会直接从_Ctor返回
        const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
        if (cachedCtors[SuperId]) {
          return cachedCtors[SuperId]
        }
    
        const name = extendOptions.name || Super.options.name
        if (process.env.NODE_ENV !== 'production' && name) {
          validateComponentName(name)
        }
    
        // 创建Sub构造函数
        const Sub = function VueComponent(options) {
          this._init(options)
        }
    
        // 继承Super,如果使用Vue.extend,这里的Super就是Vue
        Sub.prototype = Object.create(Super.prototype)
        Sub.prototype.constructor = Sub
        Sub.cid = cid++
    
        // 将组件的options和Vue的options合并,得到一个完整的options
        // 可以理解为将Vue的一些全局的属性,比如全局注册的组件和mixin,分给了Sub
        Sub.options = mergeOptions(Super.options, extendOptions)
        Sub['super'] = Super
    
        // 下面两个设置了下代理,
        // 将props和computed代理到了原型上
        // 你可以不用关心这个
        if (Sub.options.props) {
          initProps(Sub)
        }
        if (Sub.options.computed) {
          initComputed(Sub)
        }
    
        // 继承Vue的global-api
        Sub.extend = Super.extend
        Sub.mixin = Super.mixin
        Sub.use = Super.use
    
        // 继承assets的api,比如注册组件,指令,过滤器
        ASSET_TYPES.forEach(function(type) {
          Sub[type] = Super[type]
        })
    
        // 在components里添加一个自己
        // 不是主要逻辑,可以先不管
        if (name) {
          Sub.options.components[name] = Sub
        }
    
        // 将这些options保存起来
        // 一会创建实例的时候会用到
        Sub.superOptions = Super.options
        Sub.extendOptions = extendOptions
        Sub.sealedOptions = extend({}, Sub.options)
    
        // 设置缓存
        // 就是上文的缓存
        cachedCtors[SuperId] = Sub
        return Sub
      }
    }
    
    function initProps(Comp) {
      const props = Comp.options.props
      for (const key in props) {
        proxy(Comp.prototype, `_props`, key)
      }
    }
    
    function initComputed(Comp) {
      const computed = Comp.options.computed
      for (const key in computed) {
        defineComputed(Comp.prototype, key, computed[key])
      }
    }
    

    其实这个Vue.extend做的事情很简单,就是继承 Vue,正如定义中说的那样,创建一个子类,最终返回的这个 Sub 是:

    const Sub = function VueComponent(options) {
      this._init(options)
    }
    

    这里的_init就是 Vue 原型上的_init方法,你可以在源码目录下src/core/instance/init.js找到它。
    这个函数里有很多逻辑,但目前只需要关心这一段代码:

    
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    

    执行new Profile()的时候没有传任何参数,所以这里的 optionsundefined,会走到 else分支,然后resolveConstructorOptions(vm.constructor)其实就是拿到Sub.options,,然后将Sub.optionsnew Profile()传入的options合并,再赋值给实例的$options,然后在这个_init函数的最后判断了下vm.$options.el是否存在,存在的话就执行vm.$mount将组件挂载到 el 上,因为我们没有传 options,所以这里的 el 肯定是不存在的,所以你才会看到例子中的new Profile().$mount('#mount-point')手动执行了$mount方法,其实经过这些分析你就会发现,我们直接执行new Profile({ el: '#mount-point' })也是可以的,除了 el 也可以传其他参数。

    如何使用:
    其实Vue.extend初始化和平时的new Vue()是一样的,毕竟两个执行的同一个方法。但是在实际的使用中,我们可能还需要给组件传 propsslots 以及绑定事件,下面我们来看下如何做到这些事情。

    • 1、传递props

    比如我们有一个 MessageBox 组件:

    <template>
      <div class="message-box">
        {{ message }}
      </div>
    </template>
    
    <script>
      export default {
        props: {
          message: {
            type: String,
            default: ''
          }
        }
      }
    </script>
    

    它需要一个 props 来显示这个message,在使用Vue.extend时,要想给组件传参数,我们需要在实例化的时候传一个 propsData, 如:

    const MessageBoxCtor = Vue.extend(MessageBox)
    new MessageBox({
      propsData: {
        message: 'hello'
      }
    }).$mount('#target')
    

    因为在上文的_init函数中,在合并完$options后,还执行了一个函数initState(vm),它的作用就是初始化组件状态props,computed,data,在initState中执行了initProps:

    function initProps(vm: Component, propsOptions: Object) {
      const propsData = vm.$options.propsData || {}
      const props = (vm._props = {})
      // ...省略其他逻辑
    }
    

    这里的 propsData 就是数据源,他会从vm.$options.propsData上取,所我们传入的propsData经过mergeOptions后合并到vm.$options,再到这里进行 props 的初始化。

    • 2、绑定事件
      可能有时候我们还想给组件绑定事件,其实这里应该很多小伙伴都知道怎么做,我们可以通过vm.$on给组件绑定事件,这个也是平时经常用到的一个 api
    const MessageBoxCtor = Vue.extend(MessageBox)
    const messageBoxInstance = new MessageBoxCtor({
      propsData: {
        message: 'hello'
      }
    }).$mount('#target')
    messageBoxInstance.$on('some-event', () => {
      console.log('success')
    })
    
    • 3、使用插槽
      为了更加灵活的定制组件,我们还可以给组件传入插槽,比如组件可能是这样的:
    <template>
      <div class="message-box">
        {{ message }}
        <slot name="footer"/>
      </div>
    </template>
    
    <script>
      export default {
        props: {
          message: {
            type: String,
            default: ''
          }
        }
      }
    </script>
    

    如何才能给组件传入插槽内容?其实这里写的 template 会被 Vue 的编译器编译成一个 render 函数,组件渲染时执行的是这个渲染函数,我们先来看下这个 template 编译后的 render 是什么:

    function render() {
      with (this) {
        return _c(
          'div',
          {
            staticClass: 'message-box'
          },
          [_v(_s(message)), _t('footer')],
          2
        )
      }
    }
    

    这里的_t('footer')就是渲染插槽时执行的函数,_trenderSlot的缩写,你可以在源码目录的src/core/instance/render-helpers/render-slot.js中找到这个函数,简化后它的逻辑是这样的:

    export function renderSlot(name, fallback, props) {
      const scopedSlotFn = this.$scopedSlots[name]
      let nodes /** Array<VNode> */
      if (scopedSlotFn) {
        // scoped slot
        props = props || {}
        nodes = scopedSlotFn(props) || fallback
      } else {
        nodes = this.$slots[name] || fallback
      }
      return nodes
    }
    

    这个函数就是从$scopedSlots中取到对应的插槽函数,然后执行这个函数,得到虚拟节点,然后返回虚拟节点,需要注意的是,Vue2.6.x版本中已经将普通插槽和作用域插槽都整合在了$scopedSlots,所有的插槽都是返回虚拟节点的函数,renderSlot里面的else分支中从$slots取插槽是兼容以前的写法的,所以说如果用的是Vue2.6.x版本的话,是不需要去关心$slots的。

    由于renderSlot执行在组件实例的作用域中,所以this.$scopedSlots这里的this是组件的实例vm,所以我们只需要在创建完组件实例后,在实例上添加$scopedSlots就可以了,再根据之前的分析,这个$scopedSlots是一个对象,其中的 key 是插槽名称,value 是一个返回虚拟节点数组的函数:

    const MessageBoxCtor = Vue.extend(MessageBox)
    const messageBoxInstance = new MessageBoxCtor({
      propsData: {
        message: 'hello'
      }
    })
    const h = this.$createElement
    messageBoxInstance.$scopedSlots = {
      footer: function() {
        return [h('div', 'slot-content')]
      }
    }
    messageBoxInstance.$mount('#target')
    

    如果想使用作用域插槽,只需要在函数中接收参数:

    <slot name="head" :message="message"></slot>
    
    messageBoxInstance.$scopedSlots = {
      footer: function(slotData) {
        return [h('div', slotData.message)]
      }
    }
    

    这样就可以成功渲染出message了。

    参考:
    1、https://blog.51cto.com/u_15077561/2594733
    2、vue.js源码

    相关文章

      网友评论

          本文标题:Vue.extend 源码分析

          本文链接:https://www.haomeiwen.com/subject/mqxfwltx.html