VUE原理(二)发布订阅者模式实现

390 阅读4分钟

前言

回顾上文,实现的v-text、v-model存在一定的不足,数据更新时无法做到精准更新。进而优化的思路就是采用发布订阅者模式进行优化。

优化具体思路

1.数据变化之后实际上node.innerText = data[nodevalue]就可以了

2.要完成以上代码我们需要关注俩个主体。 node:当前要操作哪个dom元素 nodeValue:我们要去data中查找的对象属性名是谁'name' 'age'

3.正常逻辑下函数的执行之后内部所有的变量都会被销毁。为了保证我们来个关键信息node和 nodeValue属性名不被销毁我们需要引入闭包机制强制让他们常驻内存

解惑:之所以不能被销毁,是因为应用跑起来之后,我们一直在操作数据一直在操作数据对应的更新

()=>{
    node.innerText = data[ nodeValue]
}

这样做可以借助函数引用外层函数的node变量和nodeValue变量从而使它们一直在内存不被销毁所以我们才可以一直对他们进行操作

4.由于我们响应式数据绑定到的dom节点可能有多个,所以node节点可能存在多个。一旦响应式属性(name)发生变化与name属性相关的所有的dom节点都需要进行一轮更新,所以属性和更新函数之间是一个一对多的关系

nodeValue: [
    ()=>{node1.innerText = data[ nodeValue]} // p标签1
    ()=>{node2.innerText = data[ nodeValue]} // p标签2
]

image.png

发布订阅模式

定义

发布订阅模式,也叫自定义事件。

.他定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得将得到通知。

引子

给按钮绑定多个点击事件。

const btn = document.getElementById('btn')
btn.onclick = function(){
    console.log('123')
}
btn.onclick = function(){
    console.log('456')
}

这样执行出来是456

那么有没有一个方法能给元素绑定多个事件呢,有,可以使用addEventListener()

btn.addEventListener('click', () => {
    console.log('123')
})
btn.addEventListener('click', () => {
    console.log('456')
})

这样结果是123 456,这种一对多的特性就算发布订阅者模式的优化。

简单实现

实现一个自定义的addEventListener()方法

浏览器的实现过程分析:

1.浏览器实现了一个方法叫做addEventListener

2.这个方法接受俩个参数,参数1代表事件类型,参数2代表回调函数

3.为了实现一对多架构 {click:['回调函数1',"回调函数2",....]}

4.当鼠标点击的时候,通过事件类型click去数据结构中找到存放了所有相关回调函数的数组然后遍历,都执行一遍,从而实现一对多

let map = {} // 存储
// 收集
function collect(eventName, fn){
    if(!map[enentName]){
        map[eventName] = []
    }
    map[eventName].push(fn)
}
// 触发
function tragger(eventName){
    map[eventName].forEach(fn => fn())
}

优化一下

let my = {
    map = {},
    collect(eventName, fn){
        if(!this.map[enentName]){
            this.map[eventName] = []
        }
    this.map[eventName].push(fn)
    },
    tragger(eventName){
        this.map[eventName].forEach(fn => fn())
    }
}

// 使用
my.collect('click', ()=>{console.log('123')})
my.collect('click', ()=>{console.log('456')})
my.tragger('click')

这样就完成了一个简单的发布订阅者模式,

回到v-model的优化中

let my = {
    map = {},
    collect(eventName, fn){
        if(!this.map[enentName]){
            this.map[eventName] = []
        }
    this.map[eventName].push(fn)
    },
    tragger(eventName){
        this.map[eventName].forEach(fn => fn())
    }
}
let data ={
    name: 'zs'
}

Object.keys(data).forEach((key) => {
    turn(data, key, data[key])
})

function turn (data, name, value){
    Object.defineProperty(data, name, {
        get(){
            return value
        },
        set(newValue){
            if(newValue === value){
                return
            }
            value = newValue
            my.tragger(name)
        }
    })
}
let app = document.getElementById('app')
app.childNodes.forEach((node) => {
    if(node.nodeType === 1){  // 是固定的
        const attr = node.attributes // {v-text,class}
        Array.from(attr).forEach((attr) => {
            const nodeType = nodeName
            const nodeValue = nodeValue
            if(nodeType === 'v-text'){
                // node.innerText = data[nodeValue]
                // 优化为
                my.collect(nodeValue,()=>{node.innerText = data[nodeValue]})
            }
            if(nodeType === 'v-model'){
                // 优化为
                my.collect(nodeValue,()=>{node.innerText = data[nodeValue]})
                node.addEventListener('input',(e) => {
                    let newValue = e.target.value
                    data[nodeType] = newValue
                })
            }
        })
    }
})



总结

实现步骤

  1. 首先要制定好谁充当发布者(比如售楼处)
  2. 然后给发布者添加一个缓存列表,用于存放回调函数一遍通知订阅者(售楼处的花名册)
  3. 最后发布消息的时候,发布者会遍历这个缓存列表,依次触发里面存放的订阅者回调函数(遍历花名册,挨个发短信)

优点

时间上的解耦、对象之间解耦

缺点

创建订阅者本身要消耗一定的时间和内存,而且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。当复杂的时候不容易维护。