Vue.extend API(源碼級詳解)

2021-02-19 前端學習棧
前言

Vue.extend是 Vue 裡的一個全局 API,它提供了一種靈活的掛載組件的方式,這個 API 在日常開發中很少使用,畢竟只在碰到某些特殊的需求時它才能派上用場,但是我們也要學習它,學習這個 API 可以讓我們對 Vue 更加了解,更加熟悉 Vue 的組件初始化和掛載流程,除此之外,也經常會有面試官問到這個東西。下面我們就來從源碼到應用徹徹底底的看一看這個 API。

Vue.extend 定義

引用一個官方的定義:

使用基礎 Vue 構造器,創建一個「子類」。參數是一個包含組件選項的對象。

data 選項是特例,需要注意 - 在 Vue.extend() 中它必須是函數

<div id="mount-point"></div>
複製代碼

// 創建構造器
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')
複製代碼

結果如下

<p>Walter White aka Heisenberg</p>
複製代碼

之前沒用過這個 API 的小夥伴看到這個定義肯定會一頭霧水,但是沒關係,相信你看完這篇文章後以定會理解的。

這個 API 可以實現很靈活的功能,比如 ElementUI 裡的$message,我們使用this.$message('hello')的時候,其實就是通過這種方式創建一個組件實例,然後再將這個組件掛載到了 body 上,本篇文章也會分析如何實現這個組件,下面我們先來看下Vue.extend的源碼,從根源上來了解它。

源碼分析

你可以在源碼目錄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)
}
複製代碼

那麼上文的例子中的new Profile()執行的就是這個方法了,因為繼承了 Vue 的原型,這裡的_init就是 Vue 原型上的_init方法,你可以在源碼目錄下src/core/instance/init.js中找到它:

Vue.prototype._init = function(options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++

let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}

vm._isVue = true
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vmnext
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}

if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
複製代碼

這個函數裡有很多邏輯,它主要做的事情就是初始化組件的事件,狀態等,大多不是我們本次分析的重點,你目前只需要關心裏面的這一段代碼:

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

執行new Profile()的時候沒有傳任何參數,所以這裡的 options 是 undefined,會走到 else 分值,然後resolveConstructorOptions(vm.constructor)其實就是拿到Sub.options這個東西,你可以在上文的Vue.extend源碼中找到它,然後將Sub.options和new Profile()傳入的options合併,再賦值給實例的$options,所以如果new Profile()的時候傳入了一個 options,這個 options 將會合併到vm.$options上,然後在這個_init函數的最後判斷了下vm.$options.el是否存在,存在的話就執行vm.$mount將組件掛載到 el 上,因為我們沒有傳 options,所以這裡的 el 肯定是不存在的,所以你才會看到例子中的new Profile().$mount('#mount-point')手動執行了$mount方法,其實經過這些分析你就會發現,我們直接執行new Profile({ el: '#mount-point' })也是可以的,除了 el 也可以傳其他參數,接著往下看就知道了。

$mount 方法會執行「掛載」,其實內部的整個過程是很複雜的,會執行 render、update、patch 等等,由於這些不是本次文章的重點,你只需要知道她會將組件的 dom 掛載到對應的 dom 節點上就行了,如$mount('#mount-point')會把組件 dom 掛載到#mount-point這個元素上。

如何使用

經過上面的分析,你應該大致了解了Vue.extend的原理以及初始化過程,以及簡單的使用,其實這個初始化和平時的new Vue()是一樣的,畢竟兩個執行的同一個方法。但是在實際的使用中,我們可能還需要給組件傳 props,slots 以及綁定事件,下面我們來看下如何做到這些事情。

使用 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')
複製代碼

你可能會不明白為什麼要穿propsData,沒關係,接下來就來搞懂它,畢竟文章的目的就是徹底分析。

在上文的_init函數中,在合併完$options後,還執行了一個函數initState(vm),它的作用就是初始化組件狀態(props,computed,data):

export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe((vm._data = {}), true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
複製代碼

別的不看,只看這個:

if (opts.props) initProps(vm, opts.props)
複製代碼

function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = (vm._props = {})
// ...省略其他邏輯
}
複製代碼

這裡的 propsData 就是數據源,他會從vm.$options.propsData上取,所我們傳入的propsData經過mergeOptions後合併到vm.$options,再到這裡進行 props 的初始化。

綁定事件

可能有時候我們還想給組件綁定事件,其實這裡應該很多小夥伴都知道怎麼做,我們可以通過vm.$on給組件綁定事件,這個也是平時經常用到的一個 api

const MessageBoxCtor = Vue.extend(MessageBox)
const messageBoxInstance = new MessageBox({
propsData: {
message: 'hello'
}
}).$mount('#target')
messageBoxInstance.$on('some-event', () => {
console.log('success')
})
複製代碼

使用插槽

為了更加靈活的定製組件,我們還可以給組件傳入插槽,比如組件可能是這樣的:

<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')就是渲染插槽時執行的函數,_t是renderSlot的縮寫,你可以在源碼目錄的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中取到對應的插槽函數,然後執行這個函數,得到虛擬節點,然後返回虛擬節點,需要注意的是,Vue 在2.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 MessageBox({
propsData: {
message: 'hello'
}
})
const h = this.$createElement
messageBoxInstance.$scopedSlots = {
footer: function() {
return [h('div', 'slot-content')]
}
}
messageBoxInstance.$mount('#target')
複製代碼

這裡需要注意的是$mount一定要在設置完$scopedSlots之後,因為$mount中會執行渲染函數,我們要保證在執行渲染函數時能獲取到$scopedSlots。

如果你想使用作用域插槽,也很簡單,和普通插槽是一樣的,只需要在函數中接收參數就可以了:

<slot name="head" :message="message"></slot>
複製代碼

messageBoxInstance.$scopedSlots = {
footer: function(slotData) {
return [h('div', slotData.message)]
}
}
複製代碼

這樣就可以成功渲染出message了。

總結

本文章針對Vue.extend這個 API,從源碼到使用徹底進行了分析,相信你看過後應該能遊刃有餘地使用了。當然只有這些理論肯定還是不夠的,我們需要在實際開發場景中找到它的應用場景,比如我們常用的命令式彈窗組件,在很多 UI 組件庫裡都能看到它,你也可以去看一下 ElementUI 的 Message 組件的源碼,看看是怎麼實現的,有了這篇文章的基礎後相信你一定能看懂。

相關焦點

  • Vue-Router源碼學習之index.js(vue-router類)
    前言上一篇我們聊了:Vue-Router源碼學習之install方法雖然最近需求著實不少,但是感覺自己學習勁頭還是蠻足的今天,帶來Vue-Router源碼解析系列的第二篇文章:index.js。正文vue-router類裡面都做了什麼?
  • 解讀 vue-class-component 源碼實現原理
    vue-property-decorator 提供定義好的裝飾器,輔助完成所需功能,對這個過程好奇,就研究了源碼。內部主要依靠vue-class-component 實現,所以將重點放在對 vue-class-component 的解讀上。
  • 實現最精簡的響應式系統來學習Vue的data、computed、watch源碼
    導讀記得初學Vue源碼的時候,在defineReactive、Observer、Dep、Watcher等等內部設計源碼之間跳來跳去,發現再也繞不出來了。Vue發展了很久,很多fix和feature的增加讓內部源碼越來越龐大,太多的邊界情況和優化設計掩蓋了原本精簡的代碼設計,讓新手閱讀源碼變得越來越困難,但是面試的時候,Vue的響應式原理幾乎成了Vue技術棧的公司面試中高級前端必問的點之一。
  • vue.extend,mixins和vue.component的區別
    Vue.extend Vue.extend就是傳入一下組件options,然後返回了一個Vue的子類,也可以看做是一個組件構造函數。 真的是一個Vue的子類,源碼中,使用原型集成和Copy屬性和方法形式,將其繼承自Vue。
  • 2021全新 最火Vue面試題(一)(附源碼)
    默認.vue文件中的template處理是通過vue-loader來進行處理的並不是通過運行時的編譯 - 後面我們會說到默認vue項目中引入的vue.js是不帶有compiler模塊的)。/global-api/mixin:56.
  • 學習vue源碼(3) 手寫Vue.directive、Vue.filter、Vue.component方法
    -- 註冊組件,傳入一個擴展過的構造器 -->Vue.component('my-component',Vue.extend({/*...*/}));<!-- 註冊組件,傳入一個選項對象(自動調用Vue.extend) -->Vue.component('my-component',{/*...*/});<!
  • uni-app實現簡單API攔截
    簡單使用也很簡單,依靠教程,直接創建新項目,這裡我推薦大家用命令行的方式,因為我們可以知道框架為我們提供了什麼,我們需要怎麼配置,多踩坑才能走得更遠:全局安裝vue-cli:npm install -g @vue/cli
  • 結合源碼理解vue列表渲染v-for中的key屬性
    剛開始使用vue框架的同學有的喜歡用v-for的index係數來作為key的值,這樣和用元素的唯一id作為key有什麼區別呢?這次來給大家講解。        上篇文章結合源碼給大家講解了vue2.0的生命周期和響應式原理,模板一開始要經過編譯成virtualDOM,之後vue在渲染VirtualDOM時通過get函數進行依賴收集和當數據改變時通過set函數進行派發更新。
  • Vue造輪子必備*.vue文件源碼讀取並高亮展示
    v4.5.9@vue/compiler v3.0.4GitHub:  vue-source-demo前言(需求)就是想讀取 *.vue 文件的源碼並高亮展示到頁面上,又不想用第三方的依賴(其實是找不到)。2.
  • 使用 Vue 實例——全局 API
    一、總覽Vue.extend(options) Vue.nextTick
  • Springboot Vue Login(從零開始實現Springboot+Vue登錄)
    四、View 層代碼編寫編寫三個 vue 文件:login.vue(登錄頁面)、success.vue(登錄成功頁面)、error.vue(登錄失敗頁面)1.login.vue代碼如下 (比較懶,直接從 mall
  • 詳解Vue中的computed和watch
    關於計算屬性的緩存這個知識點需要我們去閱讀Vue的源碼實現,所以我們一起來看看源碼吧。相信大家看到源碼這個詞就會有點膽戰心驚,不過不用過分擔心,文章寫到這裡的時候考慮到本篇文章的內容和側重點,所以不會詳細去解讀計算屬性的源碼,著重學習計算屬性的緩存實現,並且點到為止。
  • 我居然把 Vue3 的原理用到了 React 上?
    前言vue-next是Vue3的源碼倉庫,Vue3採用lerna做package的劃分,而響應式能力@vue/reactivity被劃分到了單獨的一個package中。如果我們想把它集成到React中,可行嗎?來試一試吧。
  • 為什麼 Vue3 的 ref 讓很多大佬操碎了心?
    /Foo.vue'// declaring a variable that compiles to a refref: count = 1const inc = () => {  count++}// access the raw ref object by prefixing with $console.log($count.value
  • Vue 3.0前的 TypeScript 最佳入門實踐
    ,其初始內容是這樣的:// shims-tsx.d.tsimportVue, { VNode} from'vue';declare global{namespace JSX {// tslint:disable no-empty-interfaceinterfaceElementextendsVNode{}
  • 總結 Vue 知識體系之高級技巧應用篇
    我們先來看一下源碼import { toArray } from '../util/index'export function initUse(Vue: GlobalAPI) { Vue.use = function(plugin: Function | Object) { const installedPlugins = this.
  • Vue Element+Node.js開發企業通用管理後臺系統
    3-1 $emit和$on用法3-2 directive指令用法3-3 Vue.component用法3-4 Vue.extend基本用法3-5 Vue.extend進階用法3-6 Vue.use用法第4章 Vue進階(下)對Vue的進階知識進行講解,包括組件通信、過濾器、***、Vue2.6重要新特性等相關內容。
  • Vue面試題(3)Vue-Router和Vuex
    具體可以查看源碼中的 withCommit 函數。這是一種很經典對於 js單線程機制 的利用。Store.prototype._withCommit = function _withCommit(fn) {  var committing = this._committing  this.
  • webpack的幾個常見loader源碼淺析,以及動手實現一個md2html-loader
    順便簡單的了解一下幾個style-loader,vue-loader,babel-loader的源碼以及工作流程。loader簡介webpack允許我們使用loader來處理文件,loader是一個導出為function的node模塊。可以將匹配到的文件進行一次轉換,同時loader可以鏈式傳遞。
  • 20 道必看的 Vue 面試題 | 原力計劃
    npm install:下載 node_modules 資源包的命令npm run dev:啟動 vue-cli 開發環境的 npm 命令npm run build:vue-cli 生成生產環境部署資源的 npm 命令11. 請說出 vue-cli 工程中每個文件夾和文件的用處。