動機
感覺現在的業務開發,如果不是很特殊的需求,基本都能在對應的元件庫內找到元件使用,這樣編寫程式碼就成了呼叫元件,但是卻隱藏了元件內的思想,因此弱化了程式設計能力,所以我想寫這麼個分析系列來鞭策自己深入分析元件的原理,提高程式碼閱讀理解能力,我覺得一定要記下點什麼來,如果只是看不動筆感覺很快就忘了,因此準備持續寫這麼個分析
Element原始碼結構
官網傳送門點此, 主要目錄如下圖
其中元件的原始碼放在package
目錄下,src
中是一些工具函式(某些元件都會使用這些函式)和國際化相關的程式碼,進入package
目錄裡,則是所有元件的原始碼
注意這些資料夾裡只包含js或者vue,而所有元件的樣式檔案在最下面的theme-chalk
資料夾裡,整個專案結構還是很清晰
Layout(佈局)原始碼分析
<el-row>
原始碼分析
首先進入開啟官網檢視Layout
相關部分的說明,發現主要的元件就2個: el-row
,el-col
,這2個分別代表行的容器和裡面列的容器,類似於bootstrap
的col
和row
,首先我們檢視el-row
的實現,進入package
裡面的row
資料夾,裡面是一個src
資料夾和index.js
檔案
index.js
,這裡最後一句匯出Row
供我們import
,而中間的install
方法則是把這個元件當成一個Vue的外掛來使用,通過Vue.use()
來使用該元件,install方法傳遞一個Vue的構造器,Element的所有元件都是一個物件{...},裡面有個render
函式來建立元件的html結構,render
方法的好處很大,使得建立html模板的程式碼更加簡潔高效,而不是冗長的各種div標籤堆疊,更類似於一種配置形式來建立html. 最後通過export default
匯出,而不是常用的單檔案元件形式,因此必須提供install方法
import Row from './src/row';
/* istanbul ignore next */
Row.install = function(Vue) {
//全域性註冊該元件(常用的元件最好全域性註冊)
Vue.component(Row.name, Row);
};
export default Row;
複製程式碼
這裡其實有2種方法使用元件,一是當做外掛,而是直接import後註冊元件,官網示例程式碼如下,也可以不註冊成全域性元件
import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';
Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或寫為
* Vue.use(Button)
* Vue.use(Select)
*/
new Vue({
el: '#app',
render: h => h(App)
});
複製程式碼
下面進入src/row.js
中一探究竟,首先程式碼的整體結構如下,直接匯出一個物件,裡面是元件的各種配置項
export default {
...
}
複製程式碼
整個元件的程式碼量不多,下面是給出了詳細註釋
export default {
//元件名稱,注意是駝峰命名法,這使得實際使用元件時短橫線連線法<el-row>和駝峰法<ElRow>都可以使用
name: 'ElRow',
//自定義屬性(該屬性不是component必需屬性),重要,用於後面<el-col>不斷向父級查詢該元件
componentName: 'ElRow',
//元件的props
props: {
//元件渲染成html的實際標籤,預設是div
tag: {
type: String,
default: 'div'
},
//該元件的裡面的<el-col>元件的間隔
gutter: Number,
/* 元件是否是flex佈局,將 type 屬性賦值為 'flex',可以啟用 flex 佈局,
* 並可通過 justify 屬性來指定 start, center, end, space-between, space-around
* 其中的值來定義子元素的排版方式。
*/
type: String,
//flex佈局的justify屬性
justify: {
type: String,
default: 'start'
},
//flex佈局的align屬性
align: {
type: String,
default: 'top'
}
},
computed: {
//row的左右margin,用於抵消col的padding,後面詳細解釋,注意是計算屬性,這裡通過gutter計算出實際margin
style() {
const ret = {};
if (this.gutter) {
ret.marginLeft = `-${this.gutter / 2}px`;
ret.marginRight = ret.marginLeft;
}
return ret;
}
},
render(h) {
//渲染函式,後面詳細解釋
return h(this.tag, {
class: [
'el-row',
this.justify !== 'start' ? `is-justify-${this.justify}` : '',
this.align !== 'top' ? `is-align-${this.align}` : '',
{ 'el-row--flex': this.type === 'flex' }
],
style: this.style
}, this.$slots.default);
}
};
複製程式碼
下面說一下計算屬性裡面的sytle()
,這裡面通過gutter
屬性計算出了本元件的左右margin,且為負數,這裡有點費解,下面上圖解釋,首先gutter
的作用是讓row裡面的col產生出間隔來,但是注意容器的最左和最右側是沒有間隔的
<el-row>
的寬度範圍,裡面是<el-col>
元件,下一節介紹, 這個元件的寬度其實按<el-row>
百分比來計算,而且box-sizing
是border-box
,注意gutter
屬性是定義在父級的<el-row>
上,子級的col通過$parent
可以拿到該屬性,然後給<el-col>
分配padding-left
和padding-right
,因此每個col都有左右padding,上圖中每個col佔寬25%,gutter的寬度就是col的padding的2倍,但是注意最左側和最右側是沒有padding的,那麼問題來了,怎麼消去最左和最右的padding? 這裡就是<el-row>
負的margin起的作用,如果不設定上面的計算屬性的style,那麼左右2側就會有col的padding,因此這裡負的margin抵消了col的padding,且該值為 -gutter/2+'px'
注意如果初看上面的圖,一般的想法是col之間用margin來間隔,其實是不行的,而用padding來間隔就很簡單,width按百分比來分配就行(box-sizing要設定為border-box)
下面解釋下最後返回的渲染函式render
,這個函式有3個引數,第一個引數是html的tag名稱(最終在網頁中顯示的標籤名),第二個引數是一個包含模板相關屬性的資料物件,裡面有相當多模板相關的屬性,如下
{
// 和`v-bind:class`一樣的 API
// 接收一個字串、物件或字串和物件組成的陣列
'class': {
foo: true,
bar: false
},
// 和`v-bind:style`一樣的 API
// 接收一個字串、物件或物件組成的陣列
style: {
color: 'red',
fontSize: '14px'
},
// 正常的 HTML 特性
attrs: {
id: 'foo'
},
// 元件 props
props: {
myProp: 'bar'
},
// DOM 屬性
domProps: {
innerHTML: 'baz'
},
// 事件監聽器基於 `on`
// 所以不再支援如 `v-on:keyup.enter` 修飾器
// 需要手動匹配 keyCode。
on: {
click: this.clickHandler
},
// 僅對於元件,用於監聽原生事件,而不是元件內部使用
// `vm.$emit` 觸發的事件。
nativeOn: {
click: this.nativeClickHandler
},
// 自定義指令。注意,你無法對 `binding` 中的 `oldValue`
// 賦值,因為 Vue 已經自動為你進行了同步。
directives: [
{
name: 'my-custom-directive',
value: '2',
expression: '1 + 1',
arg: 'foo',
modifiers: {
bar: true
}
}
],
// 作用域插槽格式
// { name: props => VNode | Array<VNode> }
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果元件是其他元件的子元件,需為插槽指定名稱
slot: 'name-of-slot',
// 其他特殊頂層屬性
key: 'myKey',
ref: 'myRef'
}
複製程式碼
尤其注意第三個引數,它代表子節點,是一個String
或者Array
,當是String
時代表文字節點的內容,此時這就是個文字節點,如果是Array
,裡面就是子節點,陣列中每個值都是一個render的引數函式
[
//文字節點
'先寫一些文字',
createElement('h1', '一則頭條'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
複製程式碼
再看上面render函式的第三個引數是this.$slots.default
,這裡的意思就是獲取該元件下面不是具名插槽的內容,default 屬性包括了所有沒有被包含在具名插槽中的節點,對於如下程式碼,該render函式就會把<el-row>
以及<h1>test<h1>
作為其子節點一起渲染出來
<el-row>
<h1>test<h1>
<slot name='t'>t1</slot>
</el-row>
複製程式碼
最後解釋下樣式相關程式碼,row.scss
的路徑是packages/theme-chalk/src/row.scss
,程式碼是scss型別,render裡的class如下
class:[
'el-row',
this.justify !== 'start' ? `is-justify-${this.justify}` : '',
this.align !== 'top' ? `is-align-${this.align}` : '',
{ 'el-row--flex': this.type === 'flex' }
],
複製程式碼
這裡的el-row
類其實沒有定義,可以自己在寫程式碼時補充,官網就是這麼用的,後面幾個都是控制flex佈局的,由此可見<el-row>
預設佔滿父容器寬度且高度auto自適應
<el-col>
原始碼分析
col的使用也很簡單,如下,有span
,offset
,pull
,push
等屬性
<el-col :span="6" :offset="6"><div class="grid-content bg-purple"></div></el-col>
複製程式碼
進入package/col
檢視,col的程式碼稍長,主要多出來的邏輯是控制自適應(@media screen)
export default {
//元件名稱
name: 'ElCol',
props: {
//元件佔父容器的列數,總共24列,如果設定為0則渲染出來display為none
span: {
type: Number,
default: 24
},
//最終渲染出的標籤名,預設div
tag: {
type: String,
default: 'div'
},
//通過制定 col 元件的 offset 屬性可以指定分欄向右偏移的欄數
offset: Number,
//柵格向右移動格數
pull: Number,
//柵格向左移動格數
push: Number,
//響應式相關
xs: [Number, Object],
sm: [Number, Object],
md: [Number, Object],
lg: [Number, Object],
xl: [Number, Object]
},
computed: {
//獲取el-row的gutter值
gutter() {
let parent = this.$parent;
//不斷通過獲取父元素直到找到el-row元素位置,注意這裡的技巧,componentName實際
//是el-row元件設定的一個自定義屬性,用來判斷是否是el-row元件
while (parent && parent.$options.componentName !== 'ElRow') {
parent = parent.$parent;
}
return parent ? parent.gutter : 0;
}
},
render(h) {
let classList = [];
let style = {};
//通過gutter計算自己的左右2個padding,達到分隔col的目的
if (this.gutter) {
style.paddingLeft = this.gutter / 2 + 'px';
style.paddingRight = style.paddingLeft;
}
//處理佈局相關,後面詳細介紹
['span', 'offset', 'pull', 'push'].forEach(prop => {
if (this[prop] || this[prop] === 0) {
classList.push(
prop !== 'span'
? `el-col-${prop}-${this[prop]}`
: `el-col-${this[prop]}`
);
}
});
//處理螢幕響應式相關
['xs', 'sm', 'md', 'lg', 'xl'].forEach(size => {
if (typeof this[size] === 'number') {
classList.push(`el-col-${size}-${this[size]}`);
} else if (typeof this[size] === 'object') {
let props = this[size];
Object.keys(props).forEach(prop => {
classList.push(
prop !== 'span'
? `el-col-${size}-${prop}-${props[prop]}`
: `el-col-${size}-${props[prop]}`
);
});
}
});
return h(this.tag, {
class: ['el-col', classList],
style
}, this.$slots.default);
}
};
複製程式碼
下面解釋下['span', 'offset', 'pull', 'push']
這幾個的作用,span很好理解,佔父容器的列數,對應scss程式碼如下
[class*="el-col-"] {
float: left;
box-sizing: border-box;
}
.el-col-0 {
display: none;
}
@for $i from 0 through 24 {
.el-col-#{$i} {
width: (1 / 24 * $i * 100) * 1%;
}
.el-col-offset-#{$i} {
margin-left: (1 / 24 * $i * 100) * 1%;
}
.el-col-pull-#{$i} {
position: relative;
right: (1 / 24 * $i * 100) * 1%;
}
.el-col-push-#{$i} {
position: relative;
left: (1 / 24 * $i * 100) * 1%;
}
}
複製程式碼
注意上面的[attribute*=value] 選擇器,它選擇了所有類名以el-col-
開頭的類,加上float和border-box,水平佈局float肯定不可少,再看for迴圈,這裡scss的威力就發揮了,如果只用css,那程式碼量要乘以24,el-col-數字
型別的類的寬度就是百分比,下面的offset
實際上是margin-left
,這可能會導致一行排列不下所有的col,會導致換行出現,而el-col-pull
則不同,僅僅只是相對原來的位置移動,不會造成擠下去換行的情況,而會造成不同col互相覆蓋
注意上面的js部分大量使用模板字串而不是字串拼接,達到簡化程式碼的目的,這個值得學習