你踩过的坑会帮助你日后走得更快
动态数据绑定(二)
- <a href="http://ife.baidu.com/course/detail/id/20">题目</a>
- <a href="https://github.com/CaiYiLiang/2017ife-Baidu/blob/master/vue-DynamicDataBinding%2302.js">任务源码</a>
- 考察知识点:
递归Recursion
发布-订阅模式
任务二主要涉及两大块:访问引用类型的属性,利用发布-订阅模式实现事件监听。
先讲讲如何解决“比较深”的属性
看到任务二,发现了在<a href="http://www.jianshu.com/p/3fb7c2a6b047">任务一</a>写的代码有一点问题,就是无法对“比较深”的对象的进行有效访问:
在<a href="https://github.com/CaiYiLiang/2017ife-Baidu/blob/master/vue-DynamicDataBinding%2301.js">任务一</a>的代码中,为了使实例通过person1.data.
进行属性访问,新建了原型对象属性Observer.prototype.data = {}
,但是当传入参数对象是一个“比较深”的对象(属性值也是对象),就无法建立如例子中的person1.data.address.add1
属性访问,因此<a href="https://github.com/CaiYiLiang/2017ife-Baidu/blob/master/vue-DynamicDataBinding%2301.js">任务一</a>的代码需要重构。踩坑,会沉没一点时间成本,但也是一个自我发现和进步的机会。你踩过的坑会帮助你日后走得更快。
在<a href="https://github.com/CaiYiLiang/2017ife-Baidu/blob/master/vue-DynamicDataBinding%2302.js">任务二</a>中,关键的是在function函数中定义this.data = obj;
令每个实例对象的data
与传入的对象obj
指向相同的内存空间(不要忘了,data
与obj
都是引用类型,它们的值都是指向同一个地址的"指针")。
当遍历到较深的属性时,利用递归new Observer(obj[key])
实现较深的属性的getter/setter定义。同时,在对象实例设置新的值是一个对象的时候,也是利用递归new Observer(newValue)
对新增的"较深"属性定义getter/setter响应。
不多说,上代码,只是<a href="https://github.com/CaiYiLiang/2017ife-Baidu/blob/master/vue-DynamicDataBinding%2301.js">任务一</a>的修正和拓展。
function Observer(obj){
this.data = obj;
this.walk(obj);
}
Observer.prototype.walk = function(obj) {
Object.keys(obj).forEach(key => {
let val = obj[key]
console.log(key)
if(typeof obj[key] === "object"){
new Observer(obj[key])
}
Object.defineProperty(this.data,key,{
enumerable: true,
configurable: true,
get:function(){
console.log("You are visiting the attribute: "+ key +" - " + val)
return val },
set:function(newValue) {
if(typeof newValue === 'object'){
new Observer(newValue)
}
console.log("You are updating the attribute: "+ key +" - "+ newValue)
val = newValue
}
})
})
}
#######~~(╯﹏╰)b一不小心又是自己挖的坑
在写Object.defineProperty
的getter/setter
的时候,遇到了一个Uncaught RangeError: Maximum call stack size exceeded
,其中代码是这样的
Object.defineProperty(this.data,key,{
get:function(){
console.log("You are visiting the attribute: "+ key +" - " + obj[key])
return obj[key] },
....
}
在getter
里访问obj[key]就相当于陷入死循环无限调用get()
方法,直到超过最大的栈数。
解决了任务二中的问题一和问题二,
现在了解发布-订阅模式
观察者模式又叫做发布-订阅模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察着对象。
看完定义有点懵逼,我把发布-订阅模式定义为以下三点:
- 发布-订阅模式由两个角色组成:(事件)发布者和订阅者。
- 订阅者向(事件)发布者进行注册(订阅),在事件发布时被通知。
- 发布者在事件发生时向(之前在他这里进行注册/订阅的)订阅者发送通知。
这个情景是不是似曾相识?其实存在于我们生活的方方面面。
例如订阅周刊
小明(订阅者)对前端周刊(主题/发布者)有兴趣,于是交钱订阅每期的前端周刊(订阅/注册事件),并计划把每期收到的前端周刊(事件发布)带回学校阅读(订阅者在事件发生后所进行的动作)。
小红(订阅者)也对前端周刊(主题/发布者)有兴趣,于是也交钱订阅每期的前端周刊(订阅/注册事件),但小红只是想收藏书籍,所以就把每期收到的前端周刊(事件发布)放到家里书柜(订阅者在事件发生后所进行的动作)。
例如Github上的watch按钮
小明(订阅者)在github上建立了一个开源项目,他想在有人start了他的项目/有人向他提出issue/有人pull request的时候(主题/发布者)及时收到邮件通知,于是他点击了watch
按钮(订阅/注册事件)进行通邮件知设置,以便他及时处理issue/PR(订阅者在事件发生后所进行的动作)。
代码如下:
// 主题发布者
function Publisher() {
this.subscribers = {};
// 用于存储某事件对应的订阅者列表....
}
// 添加一个发布功能(方法),在事件发生时通知订阅者名单里的每一个人
Publisher.prototype.publish = function(eventType) {
if(eventType in this.subscribers){
this.subscribers[eventType].forEach(function(subscriberCb){
subscriberCb();
})
}
}
// 观察者/订阅者
function Observer() {
}
// 订阅者有订阅/注册能力
Observer.prototype.subscribe = (publisher,eventType,cb) =>{
if(!publisher.subscribers.hasOwnProperty(eventType)){
publisher.subscribers[eventType] = []
}
publisher.subscribers[eventType].push(cb)
}
测试
// 实例化
// 例1.
var techbook_Publisher = new Publisher()
var xiaoming = new Observer()
var xiaohong = new Observer()
//读者进行订阅/注册
xiaoming.subscribe(techbook_Publisher,"web-book",function(){
console.log("小明要把期刊带回学校阅读")
})
xiaohong.subscribe(techbook_Publisher,"web-book",function(){
console.log("小红要把期刊在家里收藏")
})
//出版社出版期刊,事件发生
techbook_Publisher.publish("web-book")
再看看例2
// 例2.
var github = new Publisher()
var xiaoming = new Observer()
//用户对该仓库的动态进行订阅/注册
xiaoming.subscribe(github,"watch-this-repo",function(){console.log("小明的github-repo在有人提issue/PR时收到邮件通知")})
github.publish("watch-this-repo")
当然,订阅者还会拥有取消订阅功能,以及在事件发生后不同订阅者可以有不同反应(或者有参数传入)。
因此,可以总结出发布-订阅模式的流程:
- 订阅者订阅某个主题(或者关注某个发布者)
- 某个主题(某个发布者)将该订阅者记录进待通知名单
- 在某个情景下主题发布,通知在待通知名单上的各个读者,各个读者进行各自的后续操作。
回归到<a href="https://github.com/CaiYiLiang/2017ife-Baidu/blob/master/vue-DynamicDataBinding%2302.js">任务二</a>,
实现订阅者的订阅功能Observer.prototype.$watch
以及某时刻事件触发(发布)功能Observer.prototype.$change
,并在对象实例属性值变化的时候调用Observer.prototype.$change
。
Observer.prototype.oberseredList = {} //subscriber list,shared property with oberser&publisher
let oberseredList = Observer.prototype.oberseredList
Observer.prototype.$watch = (oberseredKey,cb) =>{ //subscriber register
if(!oberseredList.hasOwnProperty(oberseredKey)){
oberseredList[oberseredKey] = []
}
oberseredList[oberseredKey].push(cb)
}
Observer.prototype.$change = function(oberseredKey,newValue){ //Event Trigger
let params = Array.prototype.slice.call(arguments,1)
if(oberseredList.hasOwnProperty(oberseredKey)) {
oberseredList[oberseredKey].forEach(cb => {
cb.apply(null,params)
})
}
}
还有关键在set
里调用Observer.prototype.$change
。
set:function(newValue) {
......
Observer.prototype.$change.call(this,key,newValue) //Event Trigger
....
}
/*Test Case*/
var person1 = new Observer({name:"xiaoming", age:20,
address:{add1:"China",add2:"UK"} });
person1.data.age
person1.$watch('age', function(age) {
console.log(`我的年纪变了,现在已经是:${age}岁了`)
});
person1.data.age = 55
Reference
系列目录
<a href="http://www.jianshu.com/p/3fb7c2a6b047">ife.baidu笔记 | 动态数据绑定(一)</a>
原创文章
简书:<a href="http://www.jianshu.com/u/c0600377679d">HelloCherry</a>
Github: <a href="https://github.com/CaiYiLiang">CaiYiLiang</a>
Girhub / vue-demos:
- <a href="https://github.com/CaiYiLiang/simply-calculator-vuejs" >利用vue.js实现简易计算器</a>
- <a href="https://github.com/CaiYiLiang/vue-demos/tree/master/wikipediaViewer-vuejs">实现简单的单页面应用(vue2.0,vue-router,vue-cliand ajax(jsonp))</a>
- <a href="https://github.com/CaiYiLiang/vue-demos/tree/master/shoppingcart-vuejs" >利用vue.js,vuex,vue-router和 Element UI实现购物车场景</a>
很高兴写的<a href="https://github.com/CaiYiLiang/vue-demos">vue demos</a>被收录到 <a href="https://github.com/vuejs/awesome-vue">awesome-vue</a>中,简直就是一朵小红花😋
如果觉得有一点点帮助,一个❤❤就是鼓励(。⌒∇⌒)