感受如今的業務開發,若是不是很特殊的需求,基本都能在對應的組件庫內找到組件使用,這樣編寫代碼就成了調用組件,可是卻隱藏了組件內的思想,所以弱化了編程能力,因此我想寫這麼個分析系列來鞭策本身深刻分析組件的原理,提升代碼閱讀理解能力,我以爲必定要記下點什麼來,若是隻是看不動筆感受很快就忘了,所以準備持續寫這麼個分析
css
官網傳送門點此, 主要目錄以下圖html
package
目錄下,
src
中是一些工具函數(某些組件都會使用這些函數)和國際化相關的代碼,進入
package
目錄裏,則是全部組件的源碼
theme-chalk
文件夾裏,整個項目結構仍是很清晰
<el-row>
源碼分析首先進入打開官網查看Layout
相關部分的說明,發現主要的組件就2個: el-row
,el-col
,這2個分別表明行的容器和裏面列的容器,相似於bootstrap
的col
和row
,首先咱們查看el-row
的實現,進入package
裏面的row
文件夾,裏面是一個src
文件夾和index.js
文件vue
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後註冊組件,官網示例代碼以下,也能夠不註冊成全局組件git
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
中一探究竟,首先代碼的總體結構以下,直接導出一個對象,裏面是組件的各類配置項github
export default {
...
}
複製代碼
整個組件的代碼量很少,下面是給出了詳細註釋express
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)
element-ui
下面解釋下最後返回的渲染函數render
,這個函數有3個參數,第一個參數是html的tag名稱(最終在網頁中顯示的標籤名),第二個參數是一個包含模板相關屬性的數據對象,裏面有至關多模板相關的屬性,以下bootstrap
{
// 和`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>
源碼分析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部分大量使用模板字符串而不是字符串拼接,達到簡化代碼的目的,這個值得學習