[Серия Vue] Интерпретация от модели публикации-подписки к реализации принципа отзывчивости vue (включая vue3.0)

Vue.js
[Серия Vue] Интерпретация от модели публикации-подписки к реализации принципа отзывчивости vue (включая vue3.0)

Резюме истории:

](nuggets.capable/post/684490…)

  • [[vue series] Когда element-ui вводится по запросу и сталкивается с ленивой загрузкой маршрутизации vue-router

](nuggets.capable/post/684490…)

предисловие

Дело в том, что некоторые друзья из технической группы хотят, чтобы автор рассказал о публикации и подписке, режиме наблюдателя иvue3изменился наproxyПотом какие изменения публиковать-подписываться. Хотя я был очень занят в последнее время, поскольку автор согласился, я не могу нарушить свое обещание.

Итак, в этой статье автор начнет с базовой реализации модели публикации-подписки, чтобыVueсерединаEventBusРеализация модели публикации-подписки в , а затем вvue2.x响应式原理Он используется в режиме публикации-подписки и вручную реализует ответ vue. Наконец, давайте кратко поговоримvue3.0中响应式原理Сходства и различия между моделью публикации-подписки и моделью vue2.x.

Расскажите о модели публикации-подписки

Шаблон наблюдателя, также известный как发布-订阅模式, является одним из наших наиболее часто используемых шаблонов проектирования (@small partner: я понимаю, что шаблон публикации-подписки и шаблон наблюдателя — один из них). Он определяет зависимость между объектами "один ко многим". При изменении состояния объекта все объекты или функции, которые зависят от него, будут уведомлены и обновлены. Шаблон Observer обеспечивает модель подписки, в которой объекты подписываются на события и получают уведомления, когда они происходят.Этот шаблон является краеугольным камнем программирования, управляемого событиями, и облегчает хороший объектно-ориентированный дизайн. При разработке Javascript мы обычно заменяем традиционную модель публикации-подписки практической моделью.

Существует множество способов реализации каждого из шаблонов проектирования. Например, реальная модель публикации-подписки включает в себя:订牛奶,发短信,自定义事件,所有UI界面的事件监听.

Вручную реализовать простую версию публикации и подписки

  • 1. В первую очередь необходимо указать, кто будет выступать издателем
  • 2. У издателя есть кэш-очередь для хранения функций обратного вызова для уведомления подписчиков.
  • 3. При публикации сообщения издатель облегчает очередь кеша и запускает функцию обратного вызова внутри
{
    const map = {}

    const listen = (key, fn) => {
        if(!map[key]) {
            map[key] = []
        }
        map[key].push(fn)
    }

    const trigger = (key, data) => {
        map[key].forEach(item => item(data));
    }

    // 测试用例
    listen('event1', () => { console.log('this is listen 1')})
    listen('event2', () => { console.log('this is listen 2')})
    
    trigger('event1') // this is listen 1
    trigger('event2') // this is listen 2
}

распечатать результат

this is listen 1
this is listen 2

Вручную реализовать глобальную публикацию-подписку

Модель «публикация-подписка» может быть реализована с помощью глобального объекта «Событие». Подписчику не нужно знать, от какого издателя поступает информация, а издатель не знает, в какие подписки возвращается сообщение. событие действует как «посредник» для подключения подписчика к Connect с издателем.

{
    /**
     * 发布-订阅的通用实现
     */
    var Event = (function() {
        const map = {}

        // 缓存队列
        const listen = (key, fn) => {
            if(!map[key]) {
                map[key] = []
            }

            map[key].push(fn)
        }

        // 发布消息
        const trigger = (...rest) => {
            const key = rest[0]
            const args = rest.slice(1)
            const fns = map[key]

            if(!Array.isArray(fns) || fns.length === 0) {
                return false
            }

            fns.forEach(item => item(...args))
        }

        // 取消订阅事件
        const remove = (key, fn) => {
            const fns = map[key]

            if(!fns) return false
            if (!fn) fns && (fns.length = 0)

            fns.forEach((item, idx) => {
                if(fn === item) {
                    fns.splice(idx, 1)
                }
            })
            map[key] = fns
        }

        return {
            listen,
            trigger,
            remove
        }
    })();

    // 测试用例
    Event.listen(
        'event1',
        (...args) => { console.log('this is listen 1: ', args) }
    )

    const event1Cb = () => { console.log('this is listen 1.1') }
    Event.listen(
        'event1',
        event1Cb
    )

    Event.listen(
        'event2',
        () => { console.log('this is listen 2') }
    )

    Event.trigger('event1', '这是登录的用户信息', '这是用户的权限')
    Event.trigger('event2')

    Event.remove('event1', event1Cb)
    Event.trigger('event1')
}

Я писал опубликовать и подписаться несколько раз, но это все еще очень просто, мне все еще нужно разбирать это здесь? Каждый может это понять. 😁

eventBus в vue

Передача данных в Vue очень распространена при обмене данными между родительскими и дочерними компонентами и родственными компонентами. Связь между родительским и дочерним компонентами очень проста, родительский компонентpropsОн передается дочернему компоненту, а дочерний компонент сообщает об этом родительскому компоненту через событие $emit. Связь между компонентами на нескольких уровнях может бытьprovideа такжеinject, но проблема в том, что данные не обновляются в режиме реального времени. Подобно сцене, которая отвечает за отображение нескольких всплывающих окон на странице, и одновременно отображается только одно всплывающее окно, используйтеEventbus- Шина событий Vue больше подходит, конечно, для сценариев, где разные представления обмениваются данными, обновляют данные, а бизнес более сложен, рекомендуется использоватьVuexдля управления обменом данными между компонентами. На этот раз поговорим оEventBusПринципы использования и реализации связи.

EventBusШина событий уникальна не только для Vue, например, в Android, бэкенде и т. д. эта концепция широко используется. В конце концов, это общий шаблон проектирования и решение для публикации и подписки. В Vue используйтеEventBusПоскольку центр событий используется всеми компонентами, вы можете зарегистрироваться в центре, чтобы отправлять или получать события.

Как использовать EventBus

Создайте шину событий без DOM, создав экземпляр Vue. Экземпляры Vue также предоставляют в своем интерфейсе событий$emit,$on,  $offа также$onceметод.

Обратите внимание, что система событий Vue отличается от системы событий браузера.EventTargetAPI. Хотя они работают одинаково,$emit,$on, а также $offнетdispatchEvent,addEventListener а также removeEventListenerпсевдоним .

const vm = new Vue();

vm.$on('test', function (msg) {
  console.log(msg)
})
vm.$emit('test', 'hi')
// => "hi

Слушайте пользовательские события в текущем экземпляре. События могут бытьvm.$emitСработав, функция обратного вызова получит все дополнительные параметры, переданные в функцию триггера события. У вас могут возникнуть вопросы,EventBusКак он реализуется и каков его принцип?

Реализовать простой EventBus вручную

Простой EventBus должен соответствовать реализации: отправка событий, прослушивание событий, уничтожение событий, однократное прослушивание событий и т. д.

  • пройти через $emit(eventName, eventHandler)инициировать событие
  • пройти через $on(eventName, eventHandler)слушать событие
  • пройти через $once(eventName, eventHandler)Слушайте одно событие за раз
  • пройти через $off(eventName, eventHandler)перестать слушать событие

Простая реализация выглядит следующим образом:

{
    class EventBus {
        constructor() {
            this.listeners = {}
        }
        /**
         * 缓存事件监听
         * @param {String} type 事件类型
         * @param {Function} cb 回调函数
         */
        on(type, cb) {
            if (!this.listeners[type]) {
                this.listeners[type] = []
            }
            this.listeners[type].push(cb)
        }

        /**
         * 
         * @param {String} type 事件类型
         * @param  {...params} args 参数列表,传回给callback
         */
        emit(type, ...args) {
            if (this.listeners[type] && this.listeners[type].length > 0) {
                const types = this.listeners[type]
                types.forEach(cb => cb(...args));
            }
        }

        /**
         * 移除事件监听
         * 传两个参 移除该事件类型的 回调函数
         * 传一个类型 移除该类型下的所有回调函数列表
         * @param {*} type 
         * @param {*} cb 
         */
        off(type, cb) {
            if (this.listeners[type]) {
                const curIndex = this.listeners[type].findIndex(it => it === cb)
                if (curIndex >= 0) {
                    this.listeners[type].splice(curIndex, 1)
                }
                // 只传type时,移除该事件的所有监听者
                if (this.listeners[type].length === 0) {
                    delete this.listeners[type]
                }
            }
        }
    }

    // 实例化事件总线
    const eb = new EventBus()

    // 注册一个下班事件监听
    eb.on('下班', (params) => {
        console.log('下班啦,撤了!')
    })
    // 发布`下班`事件
    eb.emit('下班')

    // 注册一个回家事件监听
    eb.on('回家', (eat, sleep) => {
        console.log(`下班回家${eat}、${sleep}。`)
    })
    // 发布`回家`事件
    eb.emit('回家', '吃饭', '睡觉')

    // 移除事件监听 测试
    const toBeOffFn = () => {
        console.log('这是一个可以被移除的事件。')
    }
    eb.on('offFn', toBeOffFn)
    eb.emit('offFn')
    eb.off('offFn', toBeOffFn)
    eb.emit('offFn')
    eb.off('offFn')

    console.log(eb)
}

оглянуться сейчасEventBus, не будет чувствовать себя очень просто. Принцип его реализации основан на модели публикации-подписки. Выше мы завершили шину событий. этоEventBusПреимущество заключается в том, что он уменьшает связанность и его легко понять. Недостаток также очевиден. Чем больше вы его используете, тем сложнее его поддерживать.

принцип отзывчивости vue2.x

надEventBusЭто более традиционная модель публикации-подписки, которая сначала публикует, а затем подписывает. Должна ли модель публикации-подписки сначала подписываться, а затем публиковать? Например, это требование: проксировать все свойства объекта, когда свойство используется, функция, использующая это свойство, будет сохранена в своей очереди зависимостей. Когда свойство изменяется, оно уведомляется об обновлении очереди зависимостей.

Эта сцена на самом делеvue的响应式原理,пройти черезObject.definePropertyнастраиватьsetterа такжеgetterфункция для реализации响应式так же как依赖收集,существуетgetterПри сборе зависимых функцийsetterКогда атрибут получен, все зависимости, полученные свойством, обновляются. Объекты-подписчики должны предоставлятьupdateметод для издателя, чтобы сделать обратный вызов при необходимости. Затем реализуйте простой чистый ручной vue.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>响应式vue</title>
</head>
<body>
    <div id="app"></div>
    <script>
    (function() {
        class Dep{
            constructor(){
                this.subs = []
            }

            addSub(sub) {
                if(sub && (this.subs.indexOf(sub) === -1)) {
                    this.subs.push(sub)
                }
            }

            notify() {
                this.subs.length > 0 && this.subs.forEach(sub => {
                    sub.update()
                })
            }
        }
        Dep.depTarget = null;

        // 我依赖别人,别人变了的话,调用我的update
        class Watcher{
            constructor(value, getter) {
                this.getter = getter
                this.value = this.get()
                this.val = value
            }

            get (){
                Dep.depTarget = this
                this.getter()
                Dep.depTarget = null 
                return this.val
            }

            update() {
                this.value = this.get()
            }
        }

        const typeTo = (val) => Object.prototype.toString.call(val)

        function defineReactive(obj, key, val) {
            let dep = new Dep()
            Object.defineProperty(obj, key, {
                enumerable: true,
                configurable: true,
                get(){
                    dep.addSub(Dep.depTarget)
                    return val;
                },
                set(newValue){
                    if(newValue === val) return;
                    val = newValue;
                    dep.notify()
                }
            })
        }

        function walk(obj) {
            Object.keys(obj).forEach(key => {
                if(typeTo(obj[key]) === '[object Object]'){
                    walk(obj[key])
                }
                defineReactive(obj, key, obj[key])
            })
        }

        function observe(obj){
            if(typeTo(obj) !== '[object Object]') {
                return null
            }
            walk(obj)
        }

        class Vue{
            constructor(options) {
                this.$options = options;
                this._data = options.data();
                this.render = options.render;
                this.$el = typeof options.el === 'string' 
                    ? document.querySelector(options.el) 
                    : options.el;
                observe(this._data)
                new Watcher(this._data, ()=> {
                    this.$mount()
                })
            }

            createElement(tagName, data, children){
                let element = document.createElement(tagName)
                if(Object.prototype.toString.call(children) === '[object Array]'){
                    children.forEach(child => {
                        element.appendChild(child)
                    });
                } else {
                    element.textContent = children
                }
                return element
            }

            $mount(){
                const elements = this.render(this.createElement)
                this.$el.innerHTML = ''
                this.$el.appendChild(elements)
            }
        }

        window.app = new Vue({
            el: '#app',
            data(){
                return {
                    info: {
                        message: '个人信息'
                    },
                    age: 3
                }
            },
            render(createElement) {
                return createElement(
                    'div',
                    {
                        attr: {
                            title: this._data.info.message
                        }
                    },
                    [
                        createElement('span', {}, `黑宝快${this._data.age}岁了`)
                    ]
                )
            }
        });

        setTimeout(() => {
            window.app._data.info.message = '更改文案';
            window.app._data.age = 6;
            // console.log('window.app._data.info.message: ', window.app._data.info.message)
        }, 1000)
    })();
    </script>
</body>
</html>

Следует отметить, что каждое свойство объекта имеетDep, функция, используемая для хранения атрибута зависимости, хранящегося в замыканииsubsв очереди. когда свойство срабатываетsetвремя, тогдаupdateВсе зависимые функции.vueПринцип отзывчивости также реализуется на основе модели публикации-подписки. Увидев, что здесь есть лес, я вдруг понял.

Принцип отзывчивости Vue3.0

В отзывчивости vue2.0 есть серьезный изъян: он не может отслеживать добавление и удаление свойств, изменение индекса и длины массива. Недавно запущенный Vue3.0 примет новый API ES6 —Proxy, что обеспечивает перехват перед операцией над целевым объектом. реализовать один вручнуюvue3.0响应式Бар.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>手工vue</title>
</head>
<body>
    <div id="app"></div>
    <script>
        (function () {
            class Dep {
                constructor() {
                    this.subs = []
                }

                addSub(sub) {
                    if (sub && (this.subs.indexOf(sub) === -1)) {
                        this.subs.push(sub)
                    }
                }

                notify() {
                    this.subs.length > 0 && this.subs.forEach(sub => {
                        sub.update()
                    })
                }
            }
            Dep.depTarget = null;

            class Watcher {
                constructor(value, getter) {
                    this.getter = getter
                    this.value = this.get()
                    this.val = value
                }

                get() {
                    Dep.depTarget = this
                    this.getter()
                    Dep.depTarget = null
                    return this.val
                }

                update() {
                    this.value = this.get()
                }
            }

            const typeTo = (val) => Object.prototype.toString.call(val)

            function observe(obj) {
                let dep = new Dep()

                if (typeTo(obj) !== '[object Object]') {
                    return null
                }

                return new Proxy(obj, {
                    get(target, key, receiver) {
                        dep.addSub(Dep.depTarget)
                        return target[key];
                    },
                    set(target, key, value, receiver) {
                        let newValue = Reflect.set(target, key, value, receiver)
                        dep.notify()
                        return newValue;
                    }
                })
            }

            class Vue {
                constructor(options) {
                    this.$options = options;
                    this._data = options.data();
                    this.render = options.render;
                    this.$el = typeof options.el === 'string' ?
                        document.querySelector(options.el) :
                        options.el;
                    this.$data = observe(this._data)
                    new Watcher(this._data, () => {
                        this.$mount()
                    })
                }

                createElement(tagName, data, children) {
                    let element = document.createElement(tagName)
                    if (Object.prototype.toString.call(children) === '[object Array]') {
                        children.forEach(child => {
                            element.appendChild(child)
                        });
                    } else {
                        element.textContent = children
                    }
                    return element
                }

                $mount() {
                    const elements = this.render(this.createElement)
                    this.$el.innerHTML = ''
                    this.$el.appendChild(elements)
                }
            }

            window.app = new Vue({
                el: '#app',
                data() {
                    return {
                        info: {
                            message: '个人信息'
                        },
                        age: 3
                    }
                },
                render(createElement) {
                    return createElement(
                        'div', {
                            attr: {
                                title: this.$data.info.message
                            }
                        },
                        [
                            createElement('span', {}, `黑宝(我家猫)快${this.$data.age}岁了`)
                        ]
                    )
                }
            });

            setTimeout(() => {
                window.app.$data.info.message = 4;
                window.app.$data.age = 4;
            }, 1000)
        })();
    </script>
</body>
</html>

Видно, что в основном помимоObject.definePropertyиспользовать вместо этогоProxyДля достижения или поддержания исходной модели публикации-подписки. Это просто для демонстрации, на самом делеVue3.0Адаптивная реализация сложнее, и вы можете самостоятельно перейти к исходному коду.

наконец

На этом наш основной материал на сегодня заканчивается. От публикации-подписки к VueEventBus, затем кVue2.xШаблон публикации-подписки, используемый в реактивной реализации , иVue3.x的响应式实现Модель публикации-подписки, используемая в . Я надеюсь, что сегодняшнее введение поможет вам получить начальное представление о модели публикации-подписки и принципе адаптивности vue.

Если эта статья поможет вам понять модель публикации-подписки, подпишитесь, лайкните и прокомментируйте три раза подряд Ваша поддержка — самая большая мотивация для автора написать эту статью.

Ссылаться на