svelte组件间通信

2,850 阅读2分钟

不管是vuereactangular还是svelte,开发时都会把一个页面拆分成粒度更小的组件,方便复用和维护。

所以组件间通信就变得异常重要,下面我们比较下vuesvelte的组件的通信方式。

通信方式vuesvelte
props支持支持
eventbus(发布-订阅)支持支持
父向子孙传递provide/injectsetContext/getContext
中心化状态管理vuexstore
关联关系组件通信支持不支持

下面我们就一个个介绍下svelte组件间的通信方式

props

在我们new一个svelte组件的时候,会把props传入

const app = new App({
	target: document.body,
	props: {
		// assuming App.svelte contains something like
		// `export let answer`:
		answer: 42
	}
});

当我们在组件标签上定义属性值时,也会被编译成props传入

下面我们在看一下,svelte怎么接收props传入的值

虽然语法上用export标记接收props,但是编译之后的结果就是一个普通的变量

eventBus(发布-订阅)

先看一个组件标签上定义的自定义事件,svelte在编译子组件时,会调用$on方法把标签上定义的自定义事件添加到子组件的事件管理中心eventBus

组件上为什么会有这个$on方法的呢,svelte编译组件的时候会继承SvelteComponent

class Inner extends SvelteComponent {
    constructor(options) {
        super(options);
        init(this, options, instance, create_fragment, safe_not_equal, {});
    }
}

SvelteComponent类中声明了$on方法

class SvelteComponent {
    ...
    $on(type, callback) {
        const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
        callbacks.push(callback);
        return () => {
            const index = callbacks.indexOf(callback);
            if (index !== -1)
                callbacks.splice(index, 1);
        };
    }
    ...
}

在我们调用$on方法的时候,也就是会在该组件的callbacks上订阅传入的自定义事件。

然后我们怎么触发这个自定义事件呢?

import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
dispatch('message', {
    text: 'Hello!'
});

首先调用createEventDispatcher生成一个可以触发当前组件的自定义事件的dispatch方法。

function createEventDispatcher() {
	//获取当前组件
    const component = get_current_component();
    //返回的dispatch方法
    return (type, detail) => {
    	//获取存储自定义事件的callbacks
        const callbacks = component.$$.callbacks[type];
        if (callbacks) {
            // TODO are there situations where events could be dispatched
            // in a server (non-DOM) environment?
            const event = custom_event(type, detail);
            //遍历调用自定义事件
            callbacks.slice().forEach(fn => {
            	//绑定component示例,传入构造的自定义事件event,自定义事件的参数挂载在event.detail上
                fn.call(component, event);
            });
        }
    };
}

和vue eventbus不同的是我们createEventDispatcher返回的dispatch触发的是当前组件的订阅的自定义事件,所以无法做全局事件发布-订阅总线。

setContext/getContext

先看下setContext方法

function setContext(key, context) {
    get_current_component().$$.context.set(key, context);
}

向当前组件context(Map)set要传递给子孙组件的值

再看下子孙组件怎么获取值

context: new Map(parent_component ? parent_component.$$.context : [])

因为父子组件关系在组件模板定义时就确定,并且在创建子组件时获取父组件context赋值给当前组件context,从而实现父组件向子孙组件的传值。

中心化状态管理store

sveltestore通过writable方法储存一个值

import { writable } from 'svelte/store';
export const count = writable(0);

先看一下writable方法

function writable(value, start = noop) {
	//取消订阅后的stop函数,subscribers数组为空触发
    let stop;
    //subscribe订阅的事件数组,数据更新时触发
    const subscribers = [];
    //设置新值
    function set(new_value) {
        if (safe_not_equal(value, new_value)) {
            value = new_value;
            if (stop) { // store is ready
                const run_queue = !subscriber_queue.length;
                for (let i = 0; i < subscribers.length; i += 1) {
                    const s = subscribers[i];
                    s[1]();
                    //subscriber_queue队列中push subscribe订阅的事件和新值
                    subscriber_queue.push(s, value);
                }
                //run_queue控制set更新数据时触发订阅的事件中如果继续set更新数据重复触发阻止死循环
                if (run_queue) { 
                    for (let i = 0; i < subscriber_queue.length; i += 2) {
                        subscriber_queue[i][0](subscriber_queue[i + 1]);
                    }
                    subscriber_queue.length = 0;
                }
            }
        }
    }
    //把update函数返回结果作为set新值
    function update(fn) {
        set(fn(value));
    }
    //订阅数据变化的事件,并返回取消订阅方法
    function subscribe(run, invalidate = noop) {
        const subscriber = [run, invalidate];
        subscribers.push(subscriber);
        if (subscribers.length === 1) {
        	//初次订阅的事件在执行前调用传入的start函数,并返回取消订阅后的stop函数
            stop = start(set) || noop;
        }
        //执行订阅事件
        run(value);
        //返回取消订阅函数
        return () => {
            const index = subscribers.indexOf(subscriber);
            if (index !== -1) {
                subscribers.splice(index, 1);
            }
            if (subscribers.length === 0) {
                stop();
                stop = null;
            }
        };
    }
    return { set, update, subscribe };
}

writable方法返回一个管理数据的对象,含有setupdatesubscribe方法

  • set:更新数据
  • update:调用update函数返回结果作为set新值
  • subscribe:订阅事件,数据更新时触发 这些方法可以让我们很好的管理数据和监听数据变化的状态,但是我们怎么实现这个响应式数据呢

使用的方式很简单$count,主要还是在于svelte强大的编译能力

let $count,
$$unsubscribe_count = noop,
$$subscribe_count = () => ($$unsubscribe_count(), $$unsubscribe_count = subscribe(count, $$value => $$invalidate(1, $count = $$value)), count);
$$self.$$.on_destroy.push(() => $$unsubscribe_count());
$$subscribe_count();

对该数据订阅一个更新dom节点的方法,这样数据每次更新时,会触发subscribe,然后调用invalidate函数触发节点更新。(invalidate函数触发节点更新的介绍可以参考svelte组件渲染过程) 同时把返回的unsubscribe函数pushon_destroy数组,组件销毁时取消订阅。

关联关系组件通信

在vue中我们可以通过vm.$parentvm.$children查找到任意我们需要通信的组件进行交互,但是svelte组件间的创建并没有建立父子级关系,而且更新节点的invalidate方法是对外封闭的,所以无法像vue那样自由的找到任意的组件的交互。

总结

svelte组件间通信方式

  • props:通过向子组件传参的形式通信
  • eventbus:事件管理中心,事件中心添加自定义的事件(订阅),在需要的时机手动触发(发布)
  • store:中心化状态管理机,svelte通过的编译的方法让store管理的数据修改时可以动态的触发节点的更新
  • setContext/getContext:组件创建阶段,父组件一层层往子组件添加context

谢谢大家的观看,有问题忘指正。