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

话不多说来张图

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