當AOP和react愉快玩耍的時候,個人代碼量更少了

前言

AOP(面向切面編程)針對業務中的一些關鍵點/關鍵時刻所作的事情(即切面)進行抽離,抽離的是代碼執行的過程當中的某個關鍵步驟。簡單來講,AOP關注的是什麼時間點下的什麼行爲/定義。前端

快速瞭解AOP和OOP區別

OOP(面向對象編程)對於前端er應該都很熟悉了,咱們下面舉個例子來對比一下AOP和OOPreact

OOP

假設咱們有一個「車🚗」的類:面試

class Car {
  constructor({ name, door, material, accelaration }) {
    Object.assign(this, {
      name,
      door,
      material,
      accelaration
    })
  }

  // 起步
  start() {
    console.log('start!')
  }

  // 行駛中
  running() {
    console.log(`${this.name} is running!`)
  }


  // 開門
  open() {
    console.log(`open the ${this.door}`)
  }

  // 加速
  accelerate() {
    console.log(`accelerate with ${this.accelaration}`)
  }
}
複製代碼

而後有一個Lamborghini的類,繼承於Car類編程

class Lamborghini extends Car {
  // Lamborghini路過的時候,擁有很高的回頭率,而且會被拍照
  running() {
    console.log(`${this.name} is running!`)
    console.log('girls: "Ahh! Lamborghini is comming!"')
    console.log('boys: "Look! Lamborghini is comming, let us take a photo"')
  }

  // Lamborghini開門的時候,你們都想看看車主到底是什麼樣的
  open() {
    console.log(`open the ${this.door}`)
    console.log("who drive this?")
  }

  // Lamborghini加速的時候,巨大的聲浪吸引了你們的回頭
  accelerate() {
    console.log(`accelerate with ${this.accelaration}`)
    console.log('~~~~~~~~~~~')
    console.log("who's comming?")
  }
}

const o = new Lamborghini({ name: 'Aventador', door: 'scissors door',  material: 'carbon', accelaration: '3s 0-100'  });
o.start();
o.running();
o.accelerate();
o.open();
複製代碼

另外有一個救護車類api

class ambulance extends Car {
  // 救護車路過的時候,你們會讓開
  running() {
    console.log(`${this.name} is running!`)
    console.log('bi~bu~, bi~bu~')
    console.log('ambulance is comming, please go aside')
  }
  // 救護車開門的時候,醫生會下來拯救傷員
  open() {
    console.log(`open the ${this.door}`)
    console.log("Are you ok?")
  }
  // 救護車加速的時候,沒什麼特別的
}
const c = new ambulance({ name: 'ambulance1', door: 'normal door',  material: 'normal', accelaration: 'normal'  });
c.start();
c.running();
c.accelerate();
c.open();
複製代碼

咱們能夠看見,OOP是經過繼承來複用一些和父類共有的屬性,若是有差別的話,那就在該子類的prototype上再定義差別之處。OOP是一種垂直上的代碼複用數組

AOP

AOP是面向切面、切點的編程,咱們須要找到切面、切點,並把有差別的特性注入到切點先後,實現水平上的代碼複用。babel

若是把上面的兩個子類改爲AOP實現,怎麼作呢?首先咱們能夠發現,每個子類不一樣的之處,只是父類的方法的一個修改。好比open方法是:app

// Lamborghini類open的時候
    console.log(`open the ${this.door}`)
    console.log("who drive this?")

// ambulance類open的時候
    console.log(`open the ${this.door}`)
    console.log("Are you ok?")
複製代碼

都有先open the ${this.door},那麼基於AOP的話,切點就是open the ${this.door},咱們要在open the door後插入差別性的行爲:異步

function injectLamborghini(target) {
  const { open } = target.prototype
  target.prototype.open = function() {
    open.call(this) // 公共特性open,也是切點
    console.log("who drive this?") // 這就是差別性的行爲
  }
  return target
}
複製代碼

一樣的方法,咱們將其餘差別的特性注入到繼承父類的一個子類裏面,就是一個新的子類了:ide

function injectLamborghini(target) {
  const { open, running, accelerate } = target.prototype
  target.prototype.open = function() {
    open.call(this) // 切點
    console.log("who drive this?")
  }
  target.prototype.running = function() {
    running.call(this) // 切點
    console.log('girls: "Ahh! Lamborghini is comming!"')
    console.log('boys: "Look! Lamborghini is comming, let us take a photo"')
  }
  target.prototype.accelerate = function() {
    accelerate.call(this) // 切點
    console.log('~~~~~~~~~~~')
    console.log("who's comming?")
  }
  return target
}
const injectLamborghiniSubClass = injectLamborghini(class extends Car{})
const o = new injectLamborghiniSubClass({ name: 'Aventador', door: 'scissors door',  material: 'carbon', accelaration: '3s 0-100'  })
o.start();
o.running();
o.accelerate();
o.open();

// injectLamborghiniSubClass可使用裝飾器語法:
// 須要babel,能夠去本身的項目裏面試一下
@injectLamborghini
class Lamborghini extends Car{}
複製代碼

至於ambulance類如何改爲AOP風格來實現,相信你們應該內心有數了

在react中的運用

規避對卸載的組件setState

一個異步請求,當請求返回的時候,拿到數據立刻setState並把loading組件換掉,很常規的操做。可是,當那個須要setState的組件被卸載的時候(切換路由、卸載上一個狀態組件)去setState就會警告:

若是要解決這個問題,咱們須要修改掛載、卸載、請求時的代碼

// 掛載
componentDidMount() {
  this._isMounted = true;
}
// 卸載
componentWillUnmount() {
   this._isMounted = false;
}
// 後面請求的時候
request(url)
.then(res => {
  if (this._isMounted) {
    this.setState(...)
  }
})
複製代碼

可使用HOC來實現,也能夠基於裝飾器來實現AOP風格的代碼注入。使用裝飾器最終的表現就是,若是須要這個「不要對卸載的組件setState」功能的組件,加上一個裝飾器便可:

function safe(target) {
  const {
    componentDidMount,
    componentWillUnmount,
    setState,
  } = target.prototype;
  target.prototype.componentDidMount = function() {
    componentDidMount.call(this); // 掛載的切點
    this._isMounted = true;
  }

  target.prototype.componentWillUnmount = function() {
    componentWillUnmount.call(this);// 卸載的切點
    this._isMounted = false;
  }

  target.prototype.setState = function(...args) {
    if (this._isMounted) { // 讓setstate只能在掛載後的元素進行
      setState.call(this, ...args); // setstate的切點
    }
  } 
}

// 使用的時候,只須要加一個safe的裝飾器
@safe
export default class Test extends Component {
 // ...
}
複製代碼

在函數組件中使用

函數組件內部狀態由hook維護,各類相似class組件的行爲均可以使用hook來模擬。並且之後整個項目全是函數組件是一個趨勢,沒有class如何使用AOP呢?

其實,hook已經天生自帶一絲的AOP的風格了,把一些邏輯寫好封裝到一個自定義hook裏面,須要使用的時候,往函數組件裏面插入該hook便可。

若是要在函數組件裏面基於AOP來複用代碼,首先,咱們要明確指出切點是哪裏。其次,咱們要對切點先後注入其餘代碼。最簡單的實現,就是使用發佈-訂閱模式往切點注入新的邏輯

// 自定義一個hook
function useAOP(opts = {}) {
  const store = useRef({
    ...opts,
    $$trigger(key, ...args) {
      if (store[key]) {
        store[key].apply(null, args);
      }
    }
  }).current;
  return store.$$trigger;
}

// 函數組件
function Test(props) {
  const trigger = useAOP({
    mount() {
      console.log("did mount");
    },
    click() {
      console.log('click')
    }
  });
  useEffect(() => {
   // 切點是組件掛載
    trigger("mount");
  }, [trigger]); // trigger確定是每次都同樣的,只會執行一次這個effect
// 切點是點擊的時候
  return <div onClick={() => trigger('click')}>1</div>;
}
複製代碼

上面的實現,能夠支持依賴組件內部狀態的狀況。若是不須要依賴組件內部狀態,那麼咱們能夠直接在外面包一個函數,注入trigger到props裏面:

function createAOP(opts = {}) {
  const store = {
    ...opts,
    $$trigger(key, ...args) {
      if (store[key]) {
        store[key].apply(null, args);
      }
    }
  };
  return function(cpn) {
    return function(...args) {
      const props = args.shift(); // 給props注入trigger
      // 注意,不能直接賦值哦,只能傳一個新的進去
      return cpn.apply(null, [
        { ...props, $$trigger: store.$$trigger },
        ...args
      ]);
    };
  };
}

// 函數組件Test
function Test(props) {
  const { $$trigger: trigger } = props;
  useEffect(() => {
   // 切點是組件掛載
    trigger("mount");
  }, [trigger]); // trigger確定是每次都同樣的,只會執行一次這個effect
// 切點是點擊的時候
  return <div onClick={() => trigger('click')}>1</div>;
}

// 用的時候就用這個了
export default createAOP({
  mount() {
    console.log("did mount");
  },
  click() {
    console.log("click");
  }
})(Test)
複製代碼

應用場景舉例

若是有兩個頁面,頁面結構徹底不同,可是有幾個接口以及數據處理邏輯是徹底同樣的(增刪改)

// 有兩個頁面,操做的時候,請求的接口方法同樣

class A extends Component {
  state = {
    list: [{ info: "info1" }, { info: "info2" }]
  };
  add = () => {}
  del = (index) => {}
  edit = (index) => {}
  render() {
    // 刪除和修改的時候傳index進去處理某項數據
    return (
      <main> <button onClick={this.add}>新增</button> <ul> {this.state.list.map(({ info }, index) => ( <li> <a onClick={this.del.bind(this, index)}>刪除</a> <a onClick={this.edit.bind(this, index)}>修改</a> <h2>{info}</h2> </li> ))} </ul> </main>
    );
  }
}

class B extends Component {
  state = {
    list: [{ info: "不同的信息" }, { info: "不同的ui" }]
  };
  add = () => {}
  del = (index) => {}
  edit = (index) => {}
  render() {
    // 新增就新增,刪除和修改的時候傳index進去處理某項數據
    return (
      <section> {this.state.list.map(({ info }, index) => ( <p> <span onClick={this.del.bind(this, index)}>del</span> <a onClick={this.edit.bind(this, index)}>edit</a> <footer>{info}</footer> </p> ))} <a onClick={this.add}>+</a> </section>
    );
  }
}

複製代碼

通常狀況下,咱們多是把新增、修改、刪除單獨抽離出來,而後兩個組件裏面import進來,在class裏面新增這些方法,和state關聯起來(請求、請求成功、返回數據、setstate、作一些其餘的掛在this下的操做),這樣子咱們仍是作了一些相似且重複的事情。若是使用裝飾器爲這三個操做切點注入一些操做,那麼最後咱們只須要新增一行裝飾器代碼

// 僞代碼
function injectOperation(target) {
  target.prototype.add = function(...args) {
    // do something for this.state
    request('/api/add', {
      params: {
        // ...
      }
    }).then(r => { // this已經綁的了,對state作一些事情 })
  }
  target.prototype.edit = function() {} // 相似的
  target.prototype.del = function() {}
  return target;
}

// example,組件內部再也不須要寫add、edit、del函數
@injectOperation
class A extends Component {}
複製代碼

關注公衆號《不同的前端》,以不同的視角學習前端,快速成長,一塊兒把玩最新的技術、探索各類黑科技

相關文章
相關標籤/搜索