對於很多初級的前端工程師對mixins的概念並不是很瞭解,也沒有在React中嘗試使用過Mixins,這邊文章基本會按照Mixins的作用、用途、原理等多個方面介紹React中Mixins的使用。
首先解釋一下什麼是Mixins,在一些大型專案中經常會存在多個元件需要使用相同的功能的情況,如果在每個元件中都重複性的加入相同的程式碼,那麼程式碼的維護性將會變的非常差,Mixins的出現就是為了解決這個問題。可以將通用共享的方法包裝成Mixins方法,然後注入各個元件實現,我們首先給出一個Mixins簡單的例子:
const mixin = function(obj, mixins) {
const newObj = obj;
newObj.prototype = Object.create(obj.prototype);
for (let prop in mixins) {
if (mixins.hasOwnProperty(prop)) {
newObj.prototype[prop] = mixins[prop];
}
}
return newObj;
}
const manMixins = {
speak: function (){
console.log("I'm "+this.name);
}
};
const Man = function() {
this.name = 'wang';
};
const manCanSpeak = mixin(Man,manMixins);
const man = new manCanSpeak();
man.speak(); //'I'm wang'複製程式碼
上述程式碼就實現了一個簡單的mixin
函式,其實質就是將mixins
中的方法遍歷賦值給newObj.prototype
,從而實現mixin
返回的函式建立的物件都有mixins
中的方法。在我們大致明白了mixin
作用後,讓我們來看看如何在React使用mixin
。
React.createClass
假設我們所有的React元件的props
中都含有一個預設的displayName
,在使用React.createClass
時,我們必須給每個元件中都加入
getDefaultProps: function () {
return {displayName: "component"};
}複製程式碼
當然我們,我們通過實現一個mixin
函式,就可以實現這個功能,並且在createClass
方法使用mixin
非常簡單:
var mixinDefaultProps = {
getDefaultProps: function(){
return {displayName: 'component'}
}
}
var ExampleComponent = React.createClass({
mixins: [mixinDefaultProps],
render: function(){
return <div>{this.props.displayName}</div>
}
});複製程式碼
這樣我們就實現了一個最簡單的mixin
函式,通過給每一個元件配置mixin
,我們就實現了不同元件之間共享相同的方法。需要注意的是:
mixin中有相同的函式
- 元件中含有多個
mixin
,不同的mixin
中含有相同名字的非生命週期函式,React會丟擲異常(不是後面的函式覆蓋前面的函式)。 - 元件中含有多個
mixin
,不同的mixin
中含有相同名字的生命週期函式,不會丟擲異常,mixin
中的相同的生命週期函式(除render
方法)會按照createClass
中傳入的mixins
陣列順序依次呼叫,全部呼叫結束後再呼叫元件內部的相同的宣告周期函式。
mixin中設定props或state
- 元件中含有多個
mixin
,不同的mixin
中的預設props
或初始state
中不存在相同的key值時,則預設props
和初始state
都會被合併。 - 元件中含有多個
mixin
,不同的mixin
中預設props
或初始state
中存在相同的key值時,React會丟擲異常。
JSX
目前幾乎很少有人會使用React.createClass
的方式使用React,JSX + ES6成了標配,但是JavaScript在ES6之前是原生不支援的mixin
的,ES7引入了decorator,首先介紹一下decorator到底是什麼?
Decorator
ES7的Decorator語法類似於Python中的Decorator,在ES7中也僅僅只是一個語法糖,@decorator主要有兩種,一種是面向於類(class)的@decorator,另一種是面向於方法(function)的@decorator。並且@decorator實質是利用了ES5中的Object.defineProperty
。
Object.defineProperty
關於Object.defineProperty
不是很瞭解的同學其實非常推薦看一下《JavaScript高階程式設計》的第六章第一節,大概總結一下:在ES5中物件的屬性其實分為兩種: 資料屬性和訪問器屬性
資料屬性
資料屬性有四個特性:
configurable
: 屬性是否可刪除、重新定義enumerable
: 屬性是否可列舉writable
: 屬性值是否可修改value
: 屬性值訪問器屬性
configurable
: 屬性是否可刪除、重新定義enumerable
: 屬性是否可列舉get
: 讀取屬性呼叫set
: 設定屬性呼叫
Object.defineProperty(obj, prop, descriptor)
的三個引數是定義屬性的物件、屬性名和描述符,描述符本身也是Object,其中的屬性就是資料屬性或者訪問器屬性規定的引數,舉個栗子:
var person = {};
Object.defineProperty(person,'name',{
configurable: true,
enumerable: true,
writable: true,
value: 'wang'
});
console.log(person.name);//wang複製程式碼
瞭解了Object.defineProperty
,我們分別看下面向於類(class)的@decorator和麵向於方法(function)的@decorator。
面向方法的@decorator
class語法其實僅僅只是ES6的一個語法糖而已,class其實質是function。並且class中的內部方法會通過Object.defineProperty
定義到function.prototype,例如:
class Person {
speak () {
console.log('I am Person!')
}
}複製程式碼
會被Babel轉成:
function Person(){}
Object.defineProperty(Person.prototype,'speak',{
value: function () { 'I am Person!' },
enumerable: false,
configurable: true,
writable: true
})複製程式碼
Decorator函式接受的引數與Object.defineProperty
類似,與對類(class)的方法使用@decorator,接受到的方法分別是類的prototype,內部方法名和描述符,@decorator會在呼叫Object.defineProperty
前劫持,先呼叫Decorator函式,將返回的descriptor定義到類的prototype上。
例如:
function readonly(target, key, descriptor) {
//可以通過修改descriptor引數實現各種功能
descriptor.writable = false
return descriptor
}
class Person {
@readonly
speak () {
return 'I am Person!'
}
}
const person = new Person();
person.speak = ()=>{
console.log('I am human')
}複製程式碼
面向類的@decorator
當我們對一個class使用@decorator時,接受到的引數target是類本身。例如:
function name (target) {
target.name = 'wang'
}
@name
class Person {}
console.log(Dog.name)
//'wang'複製程式碼
講完了@decorator,現在讓我們回到JSX中,react-mixin和 core-decorators兩個庫都提供了mixin函式可用。大致讓我們看一下core-decorators庫中mixin的大致程式碼:
function handleClass(target, mixins) {
if (!mixins.length) {
throw new SyntaxError(`@mixin() class ${target.name} requires at least one mixin as an argument`);
}
for (let i = 0, l = mixins.length; i < l; i++) {
const descs = getOwnPropertyDescriptors(mixins[i]);
const keys = getOwnKeys(descs);
for (let j = 0, k = keys.length; j < k; j++) {
const key = keys[j];
if (!(hasProperty(key, target.prototype))) {
defineProperty(target.prototype, key, descs[key]);
}
}
}
}
export default function mixin(...mixins) {
if (typeof mixins[0] === 'function') {
return handleClass(mixins[0], []);
} else {
return target => {
return handleClass(target, mixins);
};
}
}複製程式碼
@mixin使用如下:
import { mixin } from 'core-decorators';
const SingerMixin = {
sing(sound) {
alert(sound);
}
};
const FlyMixin = {
fly() {},
land() {}
};
@mixin(SingerMixin, FlyMixin)
class Bird {
singMatingCall() {
this.sing('tweet tweet');
}
}
var bird = new Bird();
bird.singMatingCall();複製程式碼
我們可以看到mixin
函式相當於採用Currying的方式接受mixins
陣列,返回
return target => {
return handleClass(target, mixins);
};複製程式碼
而handleClass
函式的大致作用就是採用defineProperty
將mixins陣列中的函式定義在target.prototype上,這樣就實現了mixin的要求。
Mixin在React中的作用
講了這麼多Mixin的東西,那麼Mixin在React中有什麼作用呢?Mixin的作用無非就是在多個元件中共享相同的方法,實現複用,React中的Mixin也是相同的。比如你的元件中可能有共同的工具方法,為了避免在每個元件中都有相同的定義,你就可以採用Mixin。下面依舊舉一個現實的例子。
React的效能優化一個非常常見的方法就是減少元件不必要的render
,一般我們可以在生命週期shouldComponentUpdate(nextProps, nextState)
中進行判斷,通過判斷nextProps
和nextState
與this.pros
和this.state
是否完全相同(淺比較),如果相同則返回false,表示不重新渲染,如果不相同,則返回true,使得元件重新渲染(當然你也可以不使用mixin,而使用React.PureComponent也可以達到相同的效果)。並且現在有非常多的現成的庫提供如上的功能,例如react-addons-pure-render-mixin
中提供了PureRenderMixin方法,首先我們可以在專案下執行:
npm install --save react-addons-pure-render-mixin;複製程式碼
然後在程式碼中可以如下使用
import PureRenderMixin from 'react-addons-pure-render-mixin';
import {decorate as mixin} from 'react-mixin'
@mixin(PureRenderMixin)
class FooComponent extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div className={this.props.className}>foo</div>;
}
}複製程式碼
當然你也可以這樣寫:
var PureRenderMixin = require('react-addons-pure-render-mixin');
React.createClass({
mixins: [PureRenderMixin],
render: function() {
return <div className={this.props.className}>foo</div>;
}
});複製程式碼
甚至這樣寫:
import PureRenderMixin from 'react-addons-pure-render-mixin';
class FooComponent extends React.Component {
constructor(props) {
super(props);
this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
}
render() {
return <div className={this.props.className}>foo</div>;
}
}複製程式碼
因為@decorator是ES7的用法,所以必須使用Babel才能使用,所以我們需要在.babelrc
檔案中設定:
{
"presets": ["es2015", "stage-1"],
"plugins": [
"babel-plugin-transform-decorators-legacy"
]
}複製程式碼
並安裝外掛:
npm i babel-cli babel-preset-es2015 babel-preset-stage-1 babel-plugin-transform-decorators複製程式碼
之後我們就可以盡情體驗ES7的decorator了!