論如何複用一個組件的邏輯

前言

本文簡要地探討了React和Vue兩個主流視圖庫的邏輯組合與複用模式歷史: 從最初的Mixins到HOC, 再到Render Props,最後是最新推出的Hooks。css

*注:本文中JS腳本文件均爲全局引入,所以您會看到:const { createElement: h } = React;之類對象解構寫法,而非ES Modules導入的寫法。另外,請注意閱讀註釋裏的內容!html

全文共22560字,閱讀完成大約須要45分鐘。vue

Mixins

面向對象中的mixin

mixins是傳統面向對象編程中十分流行的一種邏輯複用模式,其本質就是屬性/方法的拷貝,好比下面這個例子:node

const eventMixin = {
  on(type, handler) {
    this.eventMap[type] = handler;
  },
  emit(type) {
    const evt = this.eventMap[type];
    if (typeof evt === 'function') {
      evt();
    }
  },
};

class Event {
  constructor() {
    this.eventMap = {};
  }
}

// 將mixin中的屬性方法拷貝到Event原型對象上
Object.assign(Event.prototype, eventMixin);

const evt = new Event();
evt.on('click', () => { console.warn('a'); });
// 1秒後觸發click事件
setTimeout(() => {
  evt.emit('click');
}, 1000);

Vue中的mixin

在Vue中mixin能夠包含全部組件實例能夠傳入的選項,如data, computed, 以及mounted等生命週期鉤子函數。其同名衝突合併策略爲: 值爲對象的選項以組件數據優先, 同名生命週期鉤子函數都會被調用,且mixin中的生命週期鉤子函數在組件以前被調用react

const mixin = {
  data() {
    return { message: 'a' };
  },
  computed: {
    msg() { return `msg-${this.message}`; }
  },
  mounted() {
    // 你以爲這兩個屬性值的打印結果會是什麼?
    console.warn(this.message, this.msg);
  },
};

new Vue({
  // 爲何要加非空的el選項呢? 由於根實例沒有el選項的話,是不會觸發mounted生命週期鉤子函數的, 你能夠試試把它置爲空值, 或者把mounted改爲created試試
  el: '#app',
  mixins: [mixin],
  data() {
    return { message: 'b' };
  },
  computed: {
    msg() { return `msg_${this.message}`; }
  },
  mounted() {
    // data中的message屬性已被merge, 因此打印的是b; msg屬性也是同樣,打印的是msg_b
    console.warn(this.message, this.msg);
  },
});

從mixin的同名衝突合併策略也不難看出,在組件中添加mixin, 組件是須要作一些特殊處理的, 添加衆多mixins不免會有性能損耗。編程

React中的mixin

在React中mixin已經隨着createClass方法在16版本被移除了, 不過咱們也能夠找個15的版原本看看:api

// 若是把註釋去掉是會報錯的,React對值爲對象的選項不會自動進行合併,而是提醒開發者不要聲明同名屬性
const mixin = {
  // getInitialState() {
  //   return { message: 'a' }; 
  // },
  componentWillMount() {
    console.warn(this.state.message);
    this.setData();
  },
  // setData() {
  //   this.setState({ message: 'c' });
  // },
};

const { createElement: h } = React;
const App = React.createClass({
  mixins: [mixin],
  getInitialState() {
    return { message: 'b' }; 
  },
  componentWillMount() {
    // 對於生命週期鉤子函數合併策略Vue和React是同樣的: 同名生命週期鉤子函數都會被調用,且mixin中的生命週期鉤子函數在組件以前被調用。
    console.warn(this.state.message);
    this.setData();
  },
  setData() {
    this.setState({ message: 'd' });
  },
  render() { return null; },
});

ReactDOM.render(h(App), document.getElementById('app'));

Mixins的缺陷

  • 首先Mixins引入了隱式的依賴關係, 尤爲是引入了多個mixin甚至是嵌套mixin的時候,組件中屬性/方法來源很是不清晰。
  • 其次Mixins可能會致使命名空間衝突, 全部引入的mixin都位於同一個命名空間,前一個mixin引入的屬性/方法會被後一個mixin的同名屬性/方法覆蓋,這對引用了第三方包的項目尤爲不友好
  • 嵌套Mixins相互依賴相互耦合,會致使滾雪球式的複雜性,不利於代碼維護

好了,以上就是本文關於mixin的全部內容,若是你有些累了不妨先休息一下, 後面還有不少內容:)數組

HOC

高階函數

咱們先來了解下高階函數, 看下維基百科的概念:閉包

在數學和計算機科學中,高階函數是至少知足下列一個條件的函數: 接受一個或多個函數做爲輸入, 輸出一個函數

在不少函數式編程語言中能找到的map函數是高階函數的一個例子。它接受一個函數f做爲參數,並返回接受一個列表並應用f到它的每一個元素的一個函數。在函數式編程中,返回另外一個函數的高階函數被稱爲Curry化的函數。app

舉個例子(請忽略我沒有進行類型檢查):

function sum(...args) {
  return args.reduce((a, c) => a + c);
}

const withAbs = fn => (...args) => fn.apply(null, args.map(Math.abs));
// 全用箭頭函數的寫法可能會對不熟悉的人帶來理解上的負擔,不過這種寫法仍是很常見的,其實就至關於下面的寫法
// function withAbs(fn) {
//   return (...args) => {
//     return fn.apply(null, args.map(Math.abs));
//   };
// }

const sumAbs = withAbs(sum);
console.warn(sumAbs(1, 2, 3));
console.warn(sumAbs(1, -2));

React中的HOC

根據上面的概念,高階組件就是一個接受組件函數,輸出一個組件函數的Curry化的函數, HOC最爲經典的例子即是爲組件包裹一層加載狀態, 例如:

對於一些加載比較慢的資源,組件最初展現標準的Loading效果,但在必定時間(好比2秒)後,變爲「資源較大,正在積極加載,請稍候」這樣的友好提示,資源加載完畢後再展現具體內容。
const { createElement: h, Component: C } = React;

// HOC的輸入能夠這樣簡單的表示
function Display({ loading, delayed, data }) {
  if (delayed) {
    return h('div', null, '資源較大,正在積極加載,請稍候');
  }
  if (loading) {
    return h('div', null, '正在加載');
  }

  return h('div', null, data);
}
// 高階組件就是一個接受組件函數,輸出一個組件函數的Curry化的函數
const A = withDelay()(Display);
const B = withDelay()(Display);

class App extends C {
  constructor(props) {
    super(props);
    this.state = {
      aLoading: true,
      bLoading: true,
      aData: null,
      bData: null,
    };
    this.handleFetchA = this.handleFetchA.bind(this);
    this.handleFetchB = this.handleFetchB.bind(this);
  }
  
  componentDidMount() {
    this.handleFetchA();
    this.handleFetchB();
  }

  handleFetchA() {
    this.setState({ aLoading: true });
    // 資源1秒加載完成,不會觸發加載提示文字切換
    setTimeout(() => {
      this.setState({ aLoading: false, aData: 'a' });
    }, 1000);
  }

  handleFetchB() {
    this.setState({ bLoading: true });
    // 資源須要7秒加載完成,請求開始5秒後加載提示文字切換
    setTimeout(() => {
      this.setState({ bLoading: false, bData: 'b' });
    }, 7000);
  }
  
  render() {
    const {
      aLoading, bLoading, aData, bData,
    } = this.state;
    
    return h('article', null, [
      h(A, { loading: aLoading, data: aData }),
      h(B, { loading: bLoading, data: bData }),
      // 從新加載後,加載提示文字的邏輯不能改變
      h('button', { onClick: this.handleFetchB, disabled: bLoading }, 'click me'),
    ]);
  }
}

// 默認5秒後切換加載提示文字
function withDelay(delay = 5000) {
  // 那麼這個高階函數要怎麼實現呢? 讀者能夠本身先寫一寫
}

ReactDOM.render(h(App), document.getElementById('app'));

寫出來大體是這樣的:

function withDelay(delay = 5000) {
  return (ComponentIn) => {
    class ComponentOut extends C {
      constructor(props) {
        super(props);
        this.state = {
          timeoutId: null,
          delayed: false,
        };
        this.setDelayTimeout = this.setDelayTimeout.bind(this);
      }

      componentDidMount() {
        this.setDelayTimeout();
      }

      componentDidUpdate(prevProps) {
        // 加載完成/從新加載時,清理舊的定時器,設置新的定時器
        if (this.props.loading !== prevProps.loading) {
          clearTimeout(this.state.timeoutId);
          this.setDelayTimeout();
        }
      }

      componentWillUnmount() {
        clearTimeout(this.state.timeoutId);
      }

      setDelayTimeout() {
        // 加載完成後/從新加載須要重置delayed
        if (this.state.delayed) {
          this.setState({ delayed: false });
        }
        // 處於加載狀態才設置定時器
        if (this.props.loading) {
          const timeoutId = setTimeout(() => {
            this.setState({ delayed: true });
          }, delay);
          this.setState({ timeoutId });
        }
      }
      
      render() {
        const { delayed } = this.state;
        // 透傳props
        return h(ComponentIn, { ...this.props, delayed });
      }
    }
    
    return ComponentOut;
  };
}

Vue中的HOC

Vue中實現HOC思路也是同樣的,不過Vue中的輸入/輸出的組件不是一個函數或是類, 而是一個包含template/render選項的JavaScript對象:

const A = {
  template: '<div>a</div>',
};
const B = {
  render(h) {
    return h('div', null, 'b');
  },
};

new Vue({
  el: '#app',
  render(h) {
    // 渲染函數的第一個傳參不爲字符串類型時,須要是包含template/render選項的JavaScript對象
    return h('article', null, [h(A), h(B)]);
  },
  // 用模板的寫法的話,須要在實例裏註冊組件
  // components: { A, B },
  // template: `
  //   <article>
  //     <A />
  //     <B />
  //   </artcile>
  // `,
});

所以在Vue中HOC的輸入須要這樣表示:

const Display = {
  // 爲了行文的簡潔,這裏就不加類型檢測和默認值設置了
  props: ['loading', 'data', 'delayed'],
  render(h) {
    if (this.delayed) {
      return h('div', null, '資源過大,正在努力加載');
    }
    if (this.loading) {
      return h('div', null, '正在加載');
    }

    return h('div', null, this.data);
  },
};
// 使用的方式幾乎徹底同樣
const A = withDelay()(Display);
const B = withDelay()(Display);

new Vue({
  el: '#app',
  data() {
    return {
      aLoading: true,
      bLoading: true,
      aData: null,
      bData: null,
    };
  },
  mounted() {
    this.handleFetchA();
    this.handleFetchB();
  },
  methods: {
    handleFetchA() {
      this.aLoading = true;
      // 資源1秒加載完成,不會觸發加載提示文字切換
      setTimeout(() => {
        this.aLoading = false;
        this.aData = 'a';
      }, 1000);
    },

    handleFetchB() {
      this.bLoading = true;
      // 資源須要7秒加載完成,請求開始5秒後加載提示文字切換
      setTimeout(() => {
        this.bLoading = false;
        this.bData = 'b';
      }, 7000);
    },
  },
  render(h) {
    return h('article', null, [
      h(A, { props: { loading: this.aLoading, data: this.aData } }),
      h(B, { props: { loading: this.bLoading, data: this.bData } }),
      // 從新加載後,加載提示文字的邏輯不能改變
      h('button', {
        attrs: {
          disabled: this.bLoading,
        },
        on: {
          click: this.handleFetchB,
        },
      }, 'click me'),
    ]);
  },
});

withDelay函數也不難寫出:

function withDelay(delay = 5000) {
  return (ComponentIn) => {
    return {
      // 若是ComponentIn和ComponentOut的props徹底一致的話能夠用`props: ComponentIn.props`的寫法
      props: ['loading', 'data'],
      data() {
        return {
          delayed: false,
          timeoutId: null,
        };
      },
      watch: {
        // 用watch代替componentDidUpdate
        loading(val, oldVal) {
          // 加載完成/從新加載時,清理舊的定時器,設置新的定時器
          if (oldVal !== undefined) {
            clearTimeout(this.timeoutId);
            this.setDelayTimeout();
          }
        },
      },
      mounted() {
        this.setDelayTimeout();
      },
      beforeDestroy() {
        clearTimeout(this.timeoutId);
      },
      methods: {
        setDelayTimeout() {
          // 加載完成後/從新加載須要重置delayed
          if (this.delayed) {
            this.delayed = false;
          }
          // 處於加載狀態才設置定時器
          if (this.loading) {
            this.timeoutId = setTimeout(() => {
              this.delayed = true;
            }, delay);
          }
        },
      },
      render(h) {
        const { delayed } = this;
        // 透傳props
        return h(ComponentIn, {
          props: { ...this.$props, delayed },
        });
      },
    };
  };
}

嵌套的HOC

這裏就用React的寫法來舉例:

const { createElement: h, Component: C } = React;

const withA = (ComponentIn) => {
  class ComponentOut extends C {
    renderA() {
      return h('p', { key: 'a' }, 'a');
    }
    render() {
      const { renderA } = this;
      return h(ComponentIn, { ...this.props, renderA });
    }
  }

  return ComponentOut;
};

const withB = (ComponentIn) => {
  class ComponentOut extends C {
    renderB() {
      return h('p', { key: 'b' }, 'b');
    }
    // 在HOC存在同名函數
    renderA() {
      return h('p', { key: 'c' }, 'c');
    }
    render() {
      const { renderB, renderA } = this;
      return h(ComponentIn, { ...this.props, renderB, renderA });
    }
  }

  return ComponentOut;
};

class App extends C {
  render() {
    const { renderA, renderB } = this.props;
    return h('article', null, [
      typeof renderA === 'function' && renderA(),
      'app',
      typeof renderB === 'function' && renderB(),
    ]);
  }
}

// 你以爲renderA返回的是什麼? withA(withB(App))呢?
const container = withB(withA(App));

ReactDOM.render(h(container), document.getElementById('app'));

因此不難看出,對於HOC而言,props也是存在命名衝突問題的。一樣的引入了多個HOC甚至是嵌套HOC的時候,組件中prop的屬性/方法來源很是不清晰

HOC的優點與缺陷

先說缺陷:

  • 首先和Mixins同樣,HOC的props也會引入隱式的依賴關係, 引入了多個HOC甚至是嵌套HOC的時候,組件中prop的屬性/方法來源很是不清晰
  • 其次HOC的props可能會致使命名空間衝突, prop的同名屬性/方法會被以後執行的HOC覆蓋。
  • HOC須要額外的組件實例嵌套來封裝邏輯,會致使無謂的性能開銷

再說優點:

  • HOC是沒有反作用的純函數,嵌套HOC不會相互依賴相互耦合
  • 輸出組件不和輸入組件共享狀態,也不能使用自身的setState直接修改輸出組件的狀態,保證了狀態修改來源單一。

你可能想知道HOC並無解決太多Mixins帶來的問題,爲何不繼續使用Mixins呢?

一個很是重要的緣由是: 基於類/函數語法定義的組件,須要實例化後才能將mixins中的屬性/方法拷貝到組件中,開發者能夠在構造函數中自行拷貝,可是類庫要提供這樣一個mixins選項比較困難。

好了,以上就是本文關於HOC的所有內容。本文沒有介紹使用HOC的注意事項/compose函數之類的知識點,不熟悉的讀者能夠閱讀React的官方文檔, (逃

Render Props

React中的Render Props

其實你在上文的嵌套的HOC一節中已經看到過Render Props的用法了,其本質就是把渲染函數傳遞給子組件:

const { createElement: h, Component: C } = React;

class Child extends C {
  render() {
    const { render } = this.props;
    return h('article', null, [
      h('header', null, 'header'),
      typeof render === 'function' && render(),
      h('footer', null, 'footer'),
    ]);
  }
}

class App extends C {
  constructor(props) {
    super(props);
    this.state = { loading: false };
  }
  
  componentDidMount() {
    this.setState({ loading: true });
    setTimeout(() => {
      this.setState({ loading: false });
    }, 1000);
  }
  renderA() { return h('p', null, 'a'); }
  renderB() { return h('p', null, 'b'); }

  render() {
    const render = this.state.loading ? this.renderA : this.renderB;
    // 固然你也能夠不叫render,只要把這個渲染函數準確地傳給子組件就能夠了
    return h(Child, { render });
  }
}

ReactDOM.render(h(App), document.getElementById('app'));

Vue中的slot

在Vue中Render Props對應的概念是插槽(slots)或是籠統地稱爲Renderless Components。

const child = {
  template: `
    <article>
      <header>header</header>
      <slot></slot>
      <footer>footer</footer>
    </article>
  `,
  // 模板的寫法很好理解, 渲染函數的寫法是這樣:
  // render(h) {
  //   return h('article', null, [
  //     h('header', null, 'header'),
  //     // 由於沒有用到具名slot, 因此這裏就直接用default取到全部的Vnode
  //     this.$slots.default,
  //     h('footer', null, 'footer'),
  //   ]);
  // },
};

new Vue({
  el: '#app',
  components: { child },
  data() {
    return { loading: false };
  },
  mounted() {
    this.loading = true;
    setTimeout(() => {
      this.loading = false;
    }, 1000);
  },
  template: `
    <child>
      <p v-if="loading">a</p>
      <p v-else>b</p>
    </child>
  `,
});

不難看出在Vue中,咱們不須要顯式地去傳遞渲染函數,庫會經過$slots自動傳遞。

限於篇幅,Vue2.6版本以前的寫法: slotslot-scope這裏就不介紹了,讀者能夠閱讀Vue的官方文檔, 這裏介紹下v-slot的寫法:

const child = {
  data() {
    return {
      obj: { name: 'obj' },
    };
  },
  // slot上綁定的屬性能夠傳遞給父組件,經過`v-slot:[name]="slotProps"`接收,固然slotProps能夠命名爲其餘名稱, 也能夠寫成下文中的對象解構的寫法
  template: `
    <article>
      <header>header</header>
      
      <slot name="content"></slot>
      <slot :obj="obj"></slot>
      
      <footer>footer</footer>
    </article>
  `,
};
    
new Vue({
  el: '#app',
  components: { child },
  data() {
    return { loading: false };
  },
  mounted() {
    this.loading = true;
    setTimeout(() => {
      this.loading = false;
    }, 1000);
  },
  // #content是v-slot:content的簡寫
  template: `
    <child>
      <template #content>
        <p v-if="loading">a</p>
        <p v-else>b</p>  
      </template>

      <template #default="{ obj }">
        {{ obj.name }}
      </template>
    </child>
  `,
});

須要注意的是跟slot不一樣,v-slot只能添加在<template>上,而非任意標籤。

Render Props的優點和缺陷

就跟這個模式的名稱同樣,Render Props只是組件prop的一種用法,爲了邏輯複用,須要將狀態/視圖的操做都封裝到prop的這個渲染函數中,所以和HOC同樣也會形成性能上的損耗。可是因爲prop的屬性只有一個,不會致使HOC prop名稱衝突的問題。

好了,以上就是本文關於Render Props的所有內容, 最後咱們將介紹目前最優秀的組件邏輯組合與複用模式Hooks。

Hooks

React中的Hooks

Hooks在React中在16.8版本正式引入,咱們先看下操做狀態的鉤子useState:

const { createElement: h, useState } = React;

function App() {
  // 沒有super(props), 沒有this.onClick = this.onClick.bind(this)
  const [count, setCount] = useState(0);

  function onClick() {
    // 沒有this.state.count, 沒有this.setState
    setCount(count + 1);
  }

  return h('article', null, [
    h('p', null, count),
    h('button', { onClick }, 'click me'),
  ]);
}

ReactDOM.render(h(App), document.getElementById('app'));

函數中沒有生命週期函數鉤子,所以React Hooks提供了一個操做反作用的鉤子useEffect, 鉤子中的callback會在渲染完成後被調用

const { createElement: h, useState, useEffect } = React;

function App() {
  const [message, setMessage] = useState('a');
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    // 未指定`useEffect`的第二個參數,每次渲染完成後都會調用callback, 所以點擊按鈕會一直打印use effect
    console.warn('use effect', count);
    setTimeout(() => {
      setMessage('b');
    }, 1000);

    // useEffect中返回的函數會在渲染完成後,下一個effect開始前被調用
    return () => {
      console.warn('clean up', count);
    };
  });

  useEffect(() => {
    // 告訴 React 你的 effect 不依賴於 props 或 state 中的任何值,因此它永遠都不須要重複執行, 至關於componentDidMount
  }, []);
  // 空數組能夠替換成state不會改變的變量組成的數組
  // const [fake] = useState(0);
  // useEffect(() => {}, [fake]);
  
  useEffect(() => {
    return () => {
      // 至關於componentWillUnmount
    };
  }, []);

  console.warn('render', count);

  return h('article', null, [
    h('p', null, count),
    h('button', { onClick }, 'click me'),
    h('p', null, message),
  ]);
}

ReactDOM.render(h(App), document.getElementById('app'));

除了這兩個最經常使用的鉤子,React Hooks還提供了許多內置的鉤子函數,這裏舉個useCallback的例子:

const { createElement: h, useCallback } = React;

function useBinding(initialValue) {
  const [value, setValue] = useState(initialValue);

  // 利用useCallback能夠輕鬆地實現雙向綁定的功能
  const onChange = useCallback((evt) => {
    setValue(evt.target.value);
  }, [value]);

  return [value, onChange];
}

function App() {
  const [value, onChange] = useBinding('text');

  return h('article', null, [
    h('p', null, value),
    h('input', { value, onChange }),
  ]);
}

好了,咱們知道了Hooks的基本用法。那麼上文中HOC的例子用Hooks要怎麼改寫呢?

對於一些加載比較慢的資源,組件最初展現標準的Loading效果,但在必定時間(好比2秒)後,變爲「資源較大,正在積極加載,請稍候」這樣的友好提示,資源加載完畢後再展現具體內容。

仔細觀察上文中的withDelay函數,不難發現就組件層面而言, 咱們只是給輸入組件傳遞了一個名爲delayed的prop。

那麼對於Hooks而言也是同樣, 咱們能夠保證視圖組件Display和根組件App不變, 僅僅修改withDelay這一HOC爲自定義HookuseDelay, 這個Hook只返回delayed變量。

function useDelay({ loading, delay = 5000 }) {
  // 自定義Hook, 須要返回一個delayed變量
}

function HookDisplay(props) {
  const delayed = useDelay({ loading: props.loading });
  // Display組件函數請看上文中的React中的HOC章節
  return h(Display, { ...props, delayed });
}

// 因爲例子中的兩個組件除了props其他部分都是一致的,所以共用一個組件函數(你仔細觀察HOC的例子會發現其實也是同樣的)
const A = HookDisplay;
const B = HookDisplay;
// 你還能用更簡潔的函數完成這個函數完成的事情嗎?
function useDelay({ loading, delay = 5000 }) {
  const [delayed, setDelayed] = useState(false);

  useEffect(() => {
    // 加載完成後/從新加載須要重置delayed
    if (delayed) {
      setDelayed(false);
    }
    // 處於加載狀態才設置定時器
    const timeoutId = loading ? setTimeout(() => {
      setDelayed(true);
    }, delay) : null;

    return () => {
      clearTimeout(timeoutId);
    };
  }, [loading]);

  return delayed;
}

Vue中的Composition API

Vue中Hooks被稱爲Composition API提案,目前API還不太穩定,所以下面的內容有可能還會更改.

一樣的咱們先來看下操做狀態的鉤子, Vue提供了兩個操做狀態的Hook, 分別是refreactive(在以前的RFC中分別叫作valuestate):

<main id="app">
  <p>{{ count }}</p>
  <button @click="increment">click me</button>
</main>

<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script src="https://unpkg.com/@vue/composition-api/dist/vue-composition-api.umd.js"></script>

<script>
  // vueCompositionApi.default是一個包含install屬性的對象(也就是Vue的插件)
  const { ref, default: VueCompositionApi } = vueCompositionApi;
  Vue.use(VueCompositionApi);

  new Vue({
    el: '#app',
    setup() {
      // 你會發現count就是一個響應式對象,只含有一個value屬性, 指向其內部的值。由ref聲明的變量被稱爲包裝對象(value wrapper)
      // 包裝對象在模板中使用會被自動展開,便可以直接使用`count`而不須要寫`count.value`
      const count = ref(0);
      
      function increment() {
        // 這裏須要很是微妙地加上一個`.value`, 這也是Vue決定將`value`重命名爲`ref`的緣由之一(叫value函數返回的倒是個包含value屬性的對象不太符合直覺)
        count.value += 1;
      }

      return { count, increment };
    },
  });
</script>

值得注意的是Vue的ref鉤子和React的useRef鉤子仍是有一些差異的,useRef本質上並非一個操做狀態的鉤子(或者說操做的狀態不會影響到視圖)。

const { createElement: h, useRef } = React;

function App() {
  const count = useRef(0);
  function onClick() {
    // 雖然每次渲染都會返回同一個ref對象,可是變動current屬性並不會引起組件從新渲染
    console.warn(count.current);
    count.current += 1;
  }      

  return h('article', null, [
    h('p', null, count.current),
    h('button', { onClick }, 'click me'),
  ]);
}

ReactDOM.render(h(App), document.getElementById('app'));
<main id="app">
  <p>{{ state.count }}</p>
  <p>{{ state.double }}</p>
  
  <button @click="increment">click me</button>
</main>

<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script src="https://unpkg.com/@vue/composition-api/dist/vue-composition-api.umd.js"></script>

<script>
  const { default: VueCompositionApi, reactive, computed } = vueCompositionApi;
  Vue.use(VueCompositionApi);

  new Vue({
    el: '#app',
    setup() {
      const state = reactive({
        count: 0,
        double: computed(() => state.count * 2),
      });
      // 對於值屬性而言能夠直接用Vue.observable代替
      // 而對於計算屬性,vueCompositionApi.computed返回的是個包裝對象須要進行處理, 讀者能夠去除註釋打印state.double看看
      // const state = Vue.observable({
      //   count: 0,
      //   double: computed(() => state.count * 2),
      // });
      function increment() {
        state.count += 1;
      }

      return { state, increment };
    },
  });
</script>
React Hooks 在每次組件渲染時都會調用,經過隱式地將狀態掛載在當前的內部組件節點上,在下一次渲染時根據調用順序取出。而 Vue 的 setup() 每一個組件實例只會在初始化時調用一次 ,狀態經過引用儲存在 setup() 的閉包內。

所以Vue沒有直接提供操做反作用的鉤子,提供的依舊是生命週期函數的鉤子,除了加了on前綴和以前沒太多的差異, 以onMounted爲例:

<main id="app">
  <ul>
    <li v-for="item in list">{{ item }}</li>
  </ul>
</main>

<script src="https://cdn.bootcss.com/vue/2.6.10/vue.js"></script>
<script src="https://unpkg.com/@vue/composition-api/dist/vue-composition-api.umd.js"></script>

<script>
  const { default: VueCompositionApi, reactive, onMounted } = vueCompositionApi;
  Vue.use(VueCompositionApi);

  new Vue({
    el: '#app',
    setup() {
      const list = reactive([1, 2, 3]);
      onMounted(() => {
        setTimeout(() => {
          list.push(4);
        }, 1000);
      });

      return { list };
    },
  });
</script>

那麼上文中HOC的例子遷移到Composition API幾乎不須要修改, 保持Display組件對象和根Vue實例選項不變:

function useDelay(props, delay = 5000) {
  // 自定義Hook, 須要返回一個delayed變量
}

const HookDisplay = {
  props: ['loading', 'data'],
  setup(props) {
    const delayed = useDelay(props);
    return { delayed };
  },
  render(h) {
    // Display組件對象請看上文中的Vue中的HOC章節
    return h(Display, {
      props: {
        ...this.$props, delayed: this.delayed,
      },
    });
  },
};

const A = HookDisplay;
const B = HookDisplay;
const {
  default: VueCompositionApi, ref, watch, onMounted, onUnmounted,
} = vueCompositionApi;
Vue.use(VueCompositionApi);

function useDelay(props, delay = 5000) {
  const delayed = ref(false);
  let timeoutId = null;

  // 你能夠試試把傳參props換成loading
  // 因爲loading是基礎類型, 在傳參的時候會丟失響應式的能力(再也不是對象的getter/setter)
  watch(() => props.loading, (val, oldVal) => {
    if (oldVal !== undefined) {
      clearTimeout(timeoutId);
      setDelayTimeout();
    }
  });
  onMounted(() => {
    setDelayTimeout();
  });
  onUnmounted(() => {
    clearTimeout(timeoutId);
  });

  function setDelayTimeout() {
    if (delayed.value) {
      delayed.value = false;
    }
    if (props.loading) {
      timeoutId = setTimeout(() => {
        delayed.value = true;
      }, delay);
    }
  }

  return delayed;
}

Hooks的優點

不難看出Hooks和Render Props的思想有些許的類似,只不過Render Props返回的是組件,Hooks返回的是一些狀態(須要你本身傳遞給組件)。得益於Hooks這種細粒度的封裝能力,渲染函數再也不須要經過組件傳遞,修正了Render Props須要額外的組件實例嵌套來封裝邏輯的缺陷。

好了,以上就是本文關於邏輯組合與複用模式的所有內容。行文不免有疏漏和錯誤,還望讀者批評指正。

相關文章
相關標籤/搜索