美文网首页vue的响应式
响应式数据原理---订阅发布模式

响应式数据原理---订阅发布模式

作者: 哎呦呦胖子斌 | 来源:发表于2020-10-10 10:18 被阅读0次
    话不多说来张图

    数据劫持Observer

            所谓数据劫持就是给对象的每一个属性增加get,set方法
    1.观察对象,给对象增加Object.defineProperty
    2.vue特点是不能新增不存在的属性,不存在的属性没有get和set
    3.深度响应,因为每次赋予一个新对象时会给这个新对象增加defineProperty

    // 创建一个Observer构造函数
    function Observe(data) {
        let dep = new Dep()
        // 既然要给对象的每一个属性增加get、set,那就先遍历一遍对象
        for(let key in data) {
            let val = data[key]
            // 递归继续向下找,实现深度的数据劫持
            observe(val)
            Object.defineProperty(data, key, {
                configurable: true,
                get() {
                    // 当获取值的时候就会自动调用get方法,于是在数据劫持observe修改一下get方法,将watcher添加到订阅事件中
                    // Dep.target && dep.addSub(Dep.target)
                    if (Dep.target) {
                        dep.depend()   // 和上面一行代码的意思是一样的
                    }
                    return val
                },
                set(newVal) {
                    // 如果设置的新值和以前的值一样,就不处理
                    if (val === newVal) {
                        return
                    }
                    val = newVal
                    // 当设置完新值后,也需要把新值再去数据劫持(不然新值的属性没有get和set方法)
                    observe(newVal)
                    // 让所有watcher的update方法执行
                    dep.notify()
                }
            })
        }
    }
    

    数据代理

            数据代理就是让我们每次取data里面的数据时,不用每次都写一长串,比如mvvm._data.album.name这种,我们可以直接写成mvvm.album.name这种显而易见的方式。

    for(let key in data) {
        Object.defineProperty(this, key, {
            configurable: true,
            get() {
                return this._data[key]
            },
            set(newVal) {
                this._data[key] = newVal
            }
        })
    }
    

    数据编译Compile

            options中的el参数,为我们指定了需要编译哪些内容,而我们需要做的仅仅是解析出通过v-model、v-text、{{}}等等标识和指令,然后获取绑定数据的值,替换掉标识的内容,并进行数据的变化监听watcher,当再有值发生变化时,可以及时通知其修改对应dom元素。

    function Compile(el, vm) {
        // 讲el挂载到实例上方便调用
        vm.$el = document.querySelector(el)
    
        // 创建一个新的空白的文档片段,在el范围里将内容都拿到,当然不能一个一个的拿,可以选择移到内存中去,然后放入文档碎片中,节省开销
        // DocumentFragment是DOM节点,它不是DOM树的一部分,通常的用例是创建文档片段,讲元素附加到文档片段,然后将文档片段附加到DOM树,在DOM树中,文档片段将其所有的子元素所代替。因为文档片段存在于内存中,并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此使用文档片段通常会带来更多好的性能。
        let fragment = document.createDocumentFragment()
        while (child = vm.$el.firstChild) {
            // 将el中的内容放入到内存中
            fragment.appendChild(child)
        }
    
        // 对el里面的内容进行替换
        function replace(frag) {
            Array.from(frag.childNodes).forEach(node => {
                let txt = node.textContent
                // 正则匹配{{}}
                let reg = /\{\{(.*?)\}\}/g
                // 如果既是文本节点又有大括号
                if (node.nodeType === 3 && reg.test(txt)) {
                    function replaceTxt() {
                        node.textContent = txt.replace(reg, (matched, placeholder) => {
                     
                            // 我们需要订阅一个事件,当数据改变的时候需要重新刷新视图,这就需要在replace替换的逻辑来进行处理
                            // 通过new Watcher 把数据订阅一下,数据一变就执行改变内容的操作
                            // 监听变化,给watcher再添加两个参数,用来取新的值给回调函数
                            new Watcher(vm, placeholder, replaceTxt)
                             return placeholder.split('.').reduce((val, key) => {
                                 return val[key]
                             }, vm)
                            // 举个例子解释一下上面的代码
                            // 'album.name'.split('.') => ['album','name'] => ['album','name'].reduce((val,key) => val[key])
                            // 这里vm还是作为初始值传给val,进行第一次调用,返回的是vm['album'],然后将返回的vm['album']这个对象传给下一次调用的val
                            // 最后变成了vm['album']['name'] => '知足'
                        })
                    }
                    replaceTxt()
                }
              
                // 如果还有子节点,继续递归replace
                if (node.childNodes && node.childNodes.length) {
                    replace(node)
                }
            })
        }
    
        replace(fragment)
        vm.$el.appendChild(fragment)
    }
    

    发布订阅Dep、Watcher

            就像买房的中介一样,用户(watcher)去买房,不可能天天去房地产开发商那边去问有没有房源,更多的是找一个中介(dep),然后把我们的需求和联系方式告诉中介(dep.depend()),中介一旦有满足需求的房源,便会打电话来通知我们dep.notify()。
            我们需要一个订阅器Dep,它需要有收集需求和联系方式的功能,也需要有打电话通知的功能。

    function Dep() {
        // 定义一个数组,用来存放函数的事件池
        this.subs = []
    }
    Dep.prototype = {
        // 收集需求和联系方式的功能
        depend() {
            if (Dep.target) {
                Dep.target.addDep(this)
            }
        },
        addSub(sub) {
            this.subs.push(sub)
        },
        // 发通知的功能
        notify() {
            // 绑定的方法,都有一个update方法
            this.subs.forEach(sub => sub.update())
        }
    }
    

            我们需要一个订阅者watcher,它包含接受通知的功能,以及建立与Dep关联的功能。

    function Watcher(vm, exp, fn) {
        // 将fn放到实例上
        this.fn = fn
        this.vm = vm
        this.exp = exp
    
        // 建立关联
        Dep.target = this
        let arr = exp.split('.')
        // 这里取值,会触发value的get方法,所以需要在get方法里将联系人的方式给中介,代码47行get方法
        let val = vm
        // 取值,获取到this.album.name,默认就会调用get方法
        arr.forEach(key => {
            val = val[key]
        })
        // 释放关联
        Dep.target = null
    }
    Watcher.prototype = {
        // 接受通知的功能,收到消息后,进行更新数据的操作
        update() {
            // notify的时候值已经更改了,再通过vm,exp来获取新的值
            let arr = this.exp.split('.')
            let val = this.vm
            arr.forEach(key => {
                // 通过get获取到新的值
                val = val[key]
            })
            // 将每次拿到的新值去替换{{}}的内容
            this.fn(val)
        },
    
        addDep(dep) {
            dep.addSub(this)
        }
    }
    

    双向数据绑定

    数据--------------->Dom
    1.通过compile解析指令和数据,为其添加watcher
    2.watcher触发对应的get方法,使其进行依赖收集,把对应的watcher进行收集
    3.当数据发送变化的时候,触发set方法,使其通知watcher进行视图更新

    Dom--------------->数据
    1.通过compile解析指令和数据
    2.监听Dom input等更新动作,当触发dom更新时,在对应回调函数中更新实例vm中的数据值

    // 如果是元素节点
    if (node.nodeType === 1) {
        // 获取dom上的所有属性,是个类数组
        let nodeAttr = node.attributes
        Array.from(nodeAttr).forEach(attr => {
            let name = attr.name      // v-model
            let exp = attr.value      // who
            if (name.includes('v-')) {
                node.value = vm[exp]   // 获取this.who的值
            }
            // 监听变化
            new Watcher(vm, exp, function (newVal) {
                node.value = newVal
            })
            node.addEventListener('input', e => {
                let newVal = e.target.value
                // 相当于给this.who 赋了一个新值,而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
                vm[exp] = newVal
            })
        })
    }
    

    以上就实现了一个MVVM模型

    完整代码
    Index.html

    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <div id="app">
            <h1>{{song}}</h1>
            <p>《{{album.name}}》是{{singer}}2005年发行的专辑</p>
            <p>主打歌为{{album.theme}}</p>
            <input v-model="who" type="text">
        </div>
    
    <script src="mvvm.js"></script>
    <script>
        let mvvm = new Mvvm({
            el: '#app',
            data: {
                song: '闲鱼',
                album: {
                    name: '知足专辑',
                    theme: '知足主打歌'
                },
                singer: '五月天',
                who: '五月天还是周杰伦'
            }
        })
    </script>
    </body>
    

    mvvm.js

    // 创建一个Mvvm构造函数,讲options赋一个初始值,防止没传,等同于options || {}
    function Mvvm(options = {}) {
        // 在vue上将所有的属性都挂载到了vm.$options 上,所以我们也同样实现,将所有属性挂载到了$options
        this.$options = options;
        // this._data这里也和vue一样
        let data = this._data = this.$options.data;
        // 一、数据劫持
        observe(data)
        // 二、数据代理
        // 数据代理就是让我们每次取data里面的数据时,不用每次都写一长串,比如mvvm._data.album.name这种,我们可以直接写成mvvm.album.name这种显而易见的方式
        for(let key in data) {
            Object.defineProperty(this, key, {
                configurable: true,
                get() {
                    return this._data[key]
                },
                set(newVal) {
                    this._data[key] = newVal
                }
            })
        }
        // 三、数据编译
        new Compile(options.el, this)
    }
    
    // 一、数据劫持(所谓数据劫持就是给对象增加get,set)
    // 为什么要做数据劫持?
    // 1.观察对象,给对象增加Object.defineProperty
    // 2.vue特点是不能新增不存在的属性,不存在的属性没有get和set
    // 3.深度响应,因为每次赋予一个新对象时会给这个新对象增加defineProperty
    function observe(data) {
        // 如果不是对象的话就直接return掉,放置递归溢出
        if(!data || typeof data !== 'object') return
        return new Observe(data)
    }
    
    // 创建一个Observer构造函数
    function Observe(data) {
        let dep = new Dep()
        // 既然要给对象的每一个属性增加get、set,那就先遍历一遍对象
        for(let key in data) {
            let val = data[key]
            // 递归继续向下找,实现深度的数据劫持
            observe(val)
            Object.defineProperty(data, key, {
                configurable: true,
                get() {
                    // 当获取值的时候就会自动调用get方法,于是在数据劫持observe修改一下get方法,将watcher添加到订阅事件中
                    // Dep.target && dep.addSub(Dep.target)
                    if (Dep.target) {
                        dep.depend()   // 和上面一行代码的意思是一样的
                    }
                    return val
                },
                set(newVal) {
                    // 如果设置的新值和以前的值一样,就不处理
                    if (val === newVal) {
                        return
                    }
                    val = newVal
                    // 当设置完新值后,也需要把新值再去数据劫持(不然新值的属性没有get和set方法)
                    observe(newVal)
                    // 让所有watcher的update方法执行
                    dep.notify()
                }
            })
        }
    }
    
    // 三、创建Compile构造函数
    // options中的el参数,为我们指定了需要编译哪些内容,而我们需要做的仅仅是解析出通过v-model、v-text、{{}}等等标识和指令,然后获取绑定数据的值,替换掉标识的内容,并进行数据的变化监听watcher,当再有值发生变化时,可以及时通知其修改对应dom元素。
    function Compile(el, vm) {
        // 讲el挂载到实例上方便调用
        vm.$el = document.querySelector(el)
    
        // 创建一个新的空白的文档片段,在el范围里将内容都拿到,当然不能一个一个的拿,可以选择移到内存中去,然后放入文档碎片中,节省开销
        // DocumentFragment是DOM节点,它不是DOM树的一部分,通常的用例是创建文档片段,讲元素附加到文档片段,然后将文档片段附加到DOM树,在DOM树中,文档片段将其所有的子元素所代替。因为文档片段存在于内存中,并不在DOM树中,所以讲子元素插入到文档片段时不会引起页面回流(对元素位置和几何上的计算)。因此使用文档片段通常会带来更多好的性能。
        let fragment = document.createDocumentFragment()
        while (child = vm.$el.firstChild) {
            // 将el中的内容放入到内存中
            fragment.appendChild(child)
        }
    
        // 对el里面的内容进行替换
        function replace(frag) {
            Array.from(frag.childNodes).forEach(node => {
                let txt = node.textContent
                // 正则匹配{{}}
                let reg = /\{\{(.*?)\}\}/g
                // 如果既是文本节点又有大括号
                if (node.nodeType === 3 && reg.test(txt)) {
                    function replaceTxt() {
                        node.textContent = txt.replace(reg, (matched, placeholder) => {
                            // 五、数据更新视图
                            // 我们需要订阅一个事件,当数据改变的时候需要重新刷新视图,这就需要在replace替换的逻辑来进行处理
                            // 通过new Watcher 把数据订阅一下,数据一变就执行改变内容的操作
                            // 监听变化,给watcher再添加两个参数,用来取新的值给回调函数
                            new Watcher(vm, placeholder, replaceTxt)
                             return placeholder.split('.').reduce((val, key) => {
                                 return val[key]
                             }, vm)
                            // 举个例子解释一下上面的代码
                            // 'album.name'.split('.') => ['album','name'] => ['album','name'].reduce((val,key) => val[key])
                            // 这里vm还是作为初始值传给val,进行第一次调用,返回的是vm['album'],然后将返回的vm['album']这个对象传给下一次调用的val
                            // 最后变成了vm['album']['name'] => '知足'
                        })
                    }
                    replaceTxt()
                }
                // 六、双向数据绑定
                // 如果是元素节点
                if (node.nodeType === 1) {
                    // 获取dom上的所有属性,是个类数组
                    let nodeAttr = node.attributes
                    Array.from(nodeAttr).forEach(attr => {
                        let name = attr.name      // v-model
                        let exp = attr.value      // who
                        if (name.includes('v-')) {
                            node.value = vm[exp]   // 获取this.who的值
                        }
                        // 监听变化
                        new Watcher(vm, exp, function (newVal) {
                            node.value = newVal
                        })
                        node.addEventListener('input', e => {
                            let newVal = e.target.value
                            // 相当于给this.who 赋了一个新值,而值的改变会调用set,set中又会调用notify,notify中调用watcher的update方法实现了更新
                            vm[exp] = newVal
                        })
                    })
                }
                // 如果还有子节点,继续递归replace
                if (node.childNodes && node.childNodes.length) {
                    replace(node)
                }
            })
        }
    
        replace(fragment)
        vm.$el.appendChild(fragment)
    }
    
    // 四、发布订阅
    //     就像买房的中介一样,用户(watcher)去买房,不可能天天去房地产开发商那边去问有没有房源,更多的是找一个中介(dep),然后把我们的需求和联系方式告诉中介(dep.depend()),中介一旦有满足需求的房源,便会打电话来通知我们dep.notify()
    
    //     我们需要一个订阅器Dep,它需要有收集需求和联系方式的功能,也需要有打电话通知的功能
    // 发布订阅主要靠的就是数组关系,订阅就是放入函数,发布就是让数组里的函数执行   如[fn1, fn2, fn3]
    // 订阅器
    function Dep() {
        // 定义一个数组,用来存放函数的事件池
        this.subs = []
    }
    Dep.prototype = {
        // 收集需求和联系方式的功能
        depend() {
            if (Dep.target) {
                Dep.target.addDep(this)
            }
        },
        addSub(sub) {
            this.subs.push(sub)
        },
        // 发通知的功能
        notify() {
            // 绑定的方法,都有一个update方法
            this.subs.forEach(sub => sub.update())
        }
    }
    
        // 我们需要一个订阅者watcher,它包含接受通知的功能,以及建立与Dep关联的功能
    // 监听函数,通过watcher这个类创建的实例,都拥有update方法
    // 订阅者
    function Watcher(vm, exp, fn) {
        // 将fn放到实例上
        this.fn = fn
        this.vm = vm
        this.exp = exp
    
        // 建立关联
        Dep.target = this
        let arr = exp.split('.')
        // 这里取值,会触发value的get方法,所以需要在get方法里将联系人的方式给中介,代码47行get方法
        let val = vm
        // 取值,获取到this.album.name,默认就会调用get方法
        arr.forEach(key => {
            val = val[key]
        })
        // 释放关联
        Dep.target = null
    }
    Watcher.prototype = {
        // 接受通知的功能,收到消息后,进行更新数据的操作
        update() {
            // notify的时候值已经更改了,再通过vm,exp来获取新的值
            let arr = this.exp.split('.')
            let val = this.vm
            arr.forEach(key => {
                // 通过get获取到新的值
                val = val[key]
            })
            // 将每次拿到的新值去替换{{}}的内容
            this.fn(val)
        },
    
        addDep(dep) {
            dep.addSub(this)
        }
    }
    
    
    // 数据--------------->Dom
    // 1.通过compile解析指令和数据,为其添加watcher
    // 2.watcher触发对应的get方法,使其进行依赖收集,把对应的watcher进行收集
    // 3.当数据发送变化的时候,触发set方法,使其通知watcher进行视图更新
    
    // Dom--------------->数据
    // 1.通过compile解析指令和数据
    // 2.监听Dom input等更新动作,当触发dom更新时,在对应回调函数中更新实例vm中的数据值
    

    相关文章

      网友评论

        本文标题:响应式数据原理---订阅发布模式

        本文链接:https://www.haomeiwen.com/subject/ptudpktx.html