从 vue-rx 源码分析 vue 自定义插件

vue-rx 源码只有不到 350 行,但是实现了自定义的指令、hook、options。
本文旨在研究 vue-rx 源码总结如何自定义指令、hook 及 options。

vue-rx 概览

hook \ options:

  1. subscriptions: 接受函数或对象,提供多个 Observable
  2. domStreams: 数组,自动创建对应 Subject,配合 v-stream
  3. observableMethods: 数组或对象,自动创建对应方法的 Subject,配合 v-on

directives:

  1. v-stream: 将事件输入进对应 Subject 或者 { subject: Subject, data: any, options: { once: boolean, passive: boolean, capture: boolean } }
<button v-stream:click="plus$">+</button>
new Vue({
  domStreams: ["plus$"],
  subscriptions() {
    return {
      count: this.plus$.pipe(
        map(() => 1),
        startWith(0),
        scan((total, change) => total + change)
      ),
    };
  },
});

API Methods:

1.$watchAsObservable(expOrFn, [options]): 一个类似于 $watch 的函数,返回值为 Observable

vm.$watchAsObservable("a").subscribe(
  ({ newValue, oldValue }) => console.log("stream value", newValue, oldValue),
  (err) => console.error(err),
  () => console.log("complete")
);

2.$eventToObservable(event): 类似 .$on

const vm = new Vue({
  created() {
    this.$eventToObservable("customEvent").subscribe((event) =>
      console.log(event.name, event.msg)
    );

    // vm.$once vue-rx version
    this.$eventToObservable("customEvent").pipe(take(1));

    // Another way to auto unsub:
    let beforeDestroy$ = this.$eventToObservable("hook:beforeDestroy").pipe(
      take(1)
    );

    interval(500).pipe(takeUntil(beforeDestroy$));
  },
});

3.$subscribeTo(observable, next, error, complete): 实际上就是 subscribe,使用这个 vue-rx 帮你自动停止订阅

mounted () {
  this.$subscribeTo(interval(1000), function (count) {
    console.log(count)
  })
}

4.$fromDOMEvent(selector, event): Rx.Observable.fromEvent,由于 subscriptions 是在页面 DOM 实际渲染之前,所以 Rx.Observable.fromEvent 是不能使用的,通过$fromDOMEvent就能使用

subscriptions () {
  return {
    inputValue: this.$fromDOMEvent('input', 'keyup').pipe(
      pluck('target', 'value')
    )
  }
}

5.$createObservableMethod(methodName): 创建名为 methodName 的热 Observable

hook \ options

hook 使用 mixin,在 created、beforeDestroyed 中进行处理,通过 vm.$options 获取到 hook 或者 options 内容

var rxMixin = {
  created: function created() {
    var vm = this;
    var domStreams = vm.$options.domStreams;
    ...

    var observableMethods = vm.$options.observableMethods;
    ...

    var obs = vm.$options.subscriptions;
    if (typeof obs === 'function') {
      obs = obs.call(vm);
    }
    ...
  },

  beforeDestroy: function beforeDestroy() {
    if (this._subscription) {
      this._subscription.unsubscribe();
    }
  },
};

由于是通过 mixin 的方式来获取 hook 或者处理 options,所以这时是无法使用 watch option 去监听 subscriptions。但是可以使用 $watch

在我们自己定义插件,涉及到 hook 的时候也要注意,可以再做一个 your-watch-options 来注册监听的选项,然后再在你的数据初始化之后使用 vm.$watch 进行处理。

directives

自定义指令最常用的场景应该是注册子组件抛出的事件,这里主要关注自定义指令如何获取子组件抛出的事件。vue-rx 中通过$eventToObservable 去将事件转为 Observable。

handle.subscription = vnode.componentInstance
  .$eventToObservable(event)
  .subscribe(function (e) {
    modifiersExists.forEach(function (mod) {
      return modifiersFuncs[mod](e);
    });
    next({
      event: e,
      data: handle.data,
    });
  });

/**
 * @see {@link https://vuejs.org/v2/api/#vm-on}
 * @param {String||Array} evtName Event name
 * @return {Observable} Event stream
 */
function eventToObservable(evtName) {
  var vm = this;
  var evtNames = Array.isArray(evtName) ? evtName : [evtName];
  var obs$ = new Observable(function (observer) {
    var eventPairs = evtNames.map(function (name) {
      var callback = function (msg) {
        return observer.next({ name: name, msg: msg });
      };
      vm.$on(name, callback);
      return { name: name, callback: callback };
    });
    // 参考TeardownLogic 返回函数用于在Observable.unsubscribe的时候调用进行资源的清除
    return function () {
      // Only remove the specific callback
      eventPairs.forEach(function (pair) {
        return vm.$off(pair.name, pair.callback);
      });
    };
  });

  return obs$;
}

vnode.componentInstance 这个是指令挂载的组件,在 eventToObservable 中通过 $on 监听上面的事件。可以参考我实现的 v-debouncev-throttle 指令(github)。

下面是 v-debounce 的实现

'debounce': {
  bind: function (el, { value, arg, modifiers }, vnode) {
    const delay = modifiers.long ? 1000 : modifiers.short ? 100 : 300
    const fn = debounce(value, delay)
    if (modifiers.native) {
      el.addEventListener(arg, fn.bind(vnode.context))
    } else {
      // $on 监听挂载组件上的对应事件
      vnode.componentInstance.$on(arg, fn.bind(vnode.context))
    }
  }
},

API methods

vue-rx 最终是返回一个 VueRx 的函数。也可以返回一个对象,其中有 install 的函数,Vue.use 都会将自身传入并执行该函数。

function VueRx(Vue) {
  install(Vue);
  Vue.mixin(rxMixin);
  Vue.directive("stream", streamDirective);
  Vue.prototype.$watchAsObservable = watchAsObservable;
  Vue.prototype.$fromDOMEvent = fromDOMEvent;
  Vue.prototype.$subscribeTo = subscribeTo;
  Vue.prototype.$eventToObservable = eventToObservable;
  Vue.prototype.$createObservableMethod = createObservableMethod;
  Vue.config.optionMergeStrategies.subscriptions =
    Vue.config.optionMergeStrategies.data;
}

API methods 通过挂在 Vue.prototype,使得所有 Vue 实例都可以调用这些 API methods。我们这篇文章主要分析如何进行自定义插件,所以不会全部详细讲解这些 API。

接下来分析 watchAsObservable。

function watchAsObservable(expOrFn, options) {
  var vm = this;
  var obs$ = new Observable(function (observer) {
    var _unwatch;
    var watch = function () {
      _unwatch = vm.$watch(
        expOrFn,
        function (newValue, oldValue) {
          observer.next({ oldValue: oldValue, newValue: newValue });
        },
        options
      );
    };

    // if $watchAsObservable is called inside the subscriptions function,
    // because data hasn't been observed yet, the watcher will not work.
    // in that case, wait until created hook to watch.
    if (vm._data) {
      watch();
    } else {
      vm.$once("hook:created", watch);
    }

    // Returns function which disconnects the $watch expression
    // 这里返回一个Subscription,构造函数传入的方法会在unsubscribe的时候调用
    return new Subscription(function () {
      _unwatch && _unwatch();
    });
  });

  return obs$;
}

值得注意的是,由于提供了 subscriptions hook,而 subscriptions 是在 created 期间触发,在此时实际上 vm._data 已经存在,这里的判断似乎并没有意义。

Vue 初始化流程如下:

Vue.prototype._init = function (options) {
  ...
  initLifecycle(vm);
  initEvents(vm);
  initRender(vm);
  callHook(vm, "beforeCreate");
  initInjections(vm); // resolve injections before data/props
  initState(vm); // vm._data 在这里初始化
  initProvide(vm); // resolve provide after data/props
  callHook(vm, "created");
  ...
};
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354