数据劫持
function observe(data){
if(typeof data !== 'object') return
new Observer(data)
}
//Observer 是用于给属性对象加上监听属性的
class Observer{
constructor(value){
this.value = value
this.walk()
}
walk(){
Object.keys(this.value).forEach((key) => defineReactive(this.value,key))
}
}
//我们需要一个全局变量来保存这个值
function defineReactive(data,key,value = data[key]){
//如果value是对象,递归调用observe来监听对象
//如果value不是对象,observe函数会直接返回
observe(value)
Object.defineProperty(data,key,{
get(){
console.log(`你视图访问一个getter属性 -> ${value} `);
return value
},
set(newValue){
if(newValue === value) return
console.log(`你视图访问一个setter属性 -> ${newValue} `);
value = newValue
observe(newValue) //设置的值也要被监听
}
})
}
const obj = {
a: 1,
b: {
c: 2
}
}
observe(obj);
代码执行顺序
执行observe(obj) 入口函数
├── new Observer(obj),并执行this.walk()遍历obj的属性,执行defineReactive()
├── defineReactive(obj, a)
├── 执行observe(obj.a) 发现obj.a不是对象,直接返回
├── 执行defineReactive(obj, a) 的剩余代码
├── defineReactive(obj, b)
├── 执行observe(obj.b) 发现obj.b是对象
├── 执行 new Observer(obj.b),遍历obj.b的属性,执行defineReactive()
├── 执行defineReactive(obj.b, c)
├── 执行observe(obj.b.c) 发现obj.b.c不是对象,直接返回
├── 执行defineReactive(obj.b, c)的剩余代码
├── 执行defineReactive(obj, b)的剩余代码
代码执行结束
js发布订阅模式
发布-订阅模式 有3个模块,发布者,订阅者,调度中心
例子:这里处理中心相当于报刊办事大厅。发布者相当与某个杂志负责人,他来中心这注册一个的杂志,而订阅者相当于用户,我在中心订阅了这分杂志。每当发布者发布了一期杂志,办事大厅就会通知订阅者来拿新杂志
使用类封装,不用考虑this指向,便于理解
// 发布-订阅类 小案例
export default class Event{
constructor(){
}
// 定义一个事件容器,用来装事件数组(因为订阅者可以是多个)
handlers = {}
// 参数是 事件名 和 事件方法
addEventListener(type,handler){
// 首先判断handlers内有没有type事件容器,没有则创建一个新数组容器
if(!(type in this.handlers)){
this.handlers[type] = []
}
// 将事件存入
this.handlers[type].push(handler)
}
// 触发事件两个参数 (事件名, 参数)
dispatchEvent(type,...params){
console.log("params参数",params);
// 若没有注册该事件则抛出错误
if(!(type in this.handlers)){
throw new Error('未注册该事件')
}
// 便利触发
this.handlers[type].forEach(handler => {
handler(...params)
})
}
//事件移除参数(事件名,删除的事件,若无第二个参数则删除该事件的订阅和发布)
removeEventlistener(type,handler){
// 无效事件抛出
if(!(type in this.handlers)){
throw new Error("无效事件")
}
if(!handler){
// 直接移除事件
delete this.handlers[type]
}else{
const idx = this.handlers[type].findIndex(ele => ele == handler)
// 抛出异常事件
if(idx === undefined){
throw new Error('无该绑定事件');
}
// 移除事件
this.handlers[type].splice(idx,1)
this.handlers[type].length || delete this.handlers[type]
}
}
}
-
知识弱点
-
//收 let [a,...rest] = [1,2,3,4,5] console.log(a,rest); //1 [ 2, 3, 4, 5 ]
-
所以在函数中也是一样的,形参使用(…rest) 语法,打印rest 是 收成一个数组,如果想要扩展还是要…rest
-
不使用类实现
- 创建一个对象
- 在该对象上创建一个缓存列表(调度中心)
- on 方法用来把函数 fn 都加到缓存列表中(订阅者注册事件到调度中心),
这里的缓存列表是一个数组,因为会有多个订阅者
- emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)
- off 方法可以根据 event 值取消订阅(取消订阅)
- once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)
// 公众号对象
let eventEmitter = {};
// 缓存列表,存放 event 及 fn
eventEmitter.list = {};
//订阅
eventEmitter.on = function (event,fn){
let _this = this;
//如果没有对应的event值,既是说没有被订阅过, 就给event创建缓存列表
// 如有对象中相应的event值,把fn添加到对应event 的缓存列表里
(_this.list[event] || (_this.list[event] = [])).push(fn);
return _this;
}
//发布
eventEmitter.emit = function (){
// _this 保存的是eventEmitter 里边的emit对象 所以下边调用 里边的方法需要 把_this 传入进去
let _this = this;
//第一个参数 是对应的 event 值,直接用数组的shift方法去除 (arguments里边保存了 调用函数的 参数)
let event = [].shift.call(arguments),fns = [..._this.list[event]];
//如果缓存列表里没有 fn 就返回 fasle
if(!fns || fns.length === 0){
return false;
}
// 遍历 event 值对应的缓存列表,依次执行fn
fns.forEach(fn => {
fn.apply(_this,arguments);
});
return _this;
}
- 知识盲点
- arguments里边保存了 调用函数的 参数,所以
[].shift.call(arguments)
,取到的是arguments 里第一个
,call(arguments)意思是把this指向arguments - 为什么fn.apply(_this,arguments)要传入_this?
- 因为外层是一个箭头函数,this的指向不明确,所以外层要保存 一层_this,
- _this里边呢,保存的是
eventEmitter
这个对象,然后进行调用
- 为什么需要把_this返回出去?
- 不知道
- arguments里边保存了 调用函数的 参数,所以
添加off 和 once 方法
//取消订阅
eventEmitter.off = function(event,fn){
let _this = this;
let fns = _this.list[event]; //存放对应 标识 的 订阅列表
// 如果缓存列表中没有相应的 fn ,返回false
if(!fns) return false;
if(!fn){
//如果没有传fn的话,就会将event值对应缓存列表中的fn都清空
fns && (fns.length = 0)
}else{
// 若有fn,遍历对标,看看传入的fn与哪个函数相同,如果相同 直接在缓存列表中删除即可
let cb = fns.findIndex(item => item === fn);
fns.splice(cb,1);
}
return _this
}
- 知识弱点
- 两个相同的函数比较 布尔值 为 true ,因为引用地址是一样的
// 监听一次
eventEmitter.once = function(event,fn){
// 先绑定,调用后删除
let _this = this;
function on(){
_this.off(event,on);
fn.apply(_this,arguments)
}
on.fn = fn;
_this.on(event,on); //这里调用的 是外边的 on方法
return _this;
}
-
知识盲区
-
这个once方法 不是说 订阅 就触发,而是 绑定到调度中心,使用一次后就执行off 方法进行一个取消订阅
-
为什么 要 on.fn = fn 这样写? 不这样写 好像也能正常进行调用且 ,调度中心里边也会删除
- 不知道
-
调用
function user1 (content) {
console.log('用户1订阅了:', content);
};
function user2 (content) {
console.log('用户2订阅了:', content);
};
function user3(content){
console.log('用户3订阅了:', content);
}
// 订阅
eventEmitter.on('article', user1);
eventEmitter.on('article', user2);
//监听一次
eventEmitter.once("article2",user3)
// 取消订阅
eventEmitter.off("article",user1)
// 发布
eventEmitter.emit('article', 'Javascript 发布-订阅模式');
eventEmitter.emit('article2', 'Javascript 发布-观察模式');
eventEmitter.emit('article2', 'Javascript 发布-观察模式');
个人总结
- 数据劫持的作用呢?就是给所有的对象属性添加上getter 和 setter属性,这样所有的对象就是响应式的了
- 怎么添加呢?就是通过一个Observe类,里边自定义一个方法walk(),通过Object.keys()遍历
顶级对象
的key
,调用defineReactive()方法添加,如果遍历key对应值 为对象,再递归调用
- 怎么添加呢?就是通过一个Observe类,里边自定义一个方法walk(),通过Object.keys()遍历
- 了解发布订阅的作用?便于理解vue中的 e m i t , emit, emit,on, o n c e , once, once,off,总的来说发布订阅就是 把需要在全局使用的函数,订阅到 一个全局对象(调用中心)里边去,当我们需要在那个 阶段使用 这个函数的时候,只需要发布就能执行。该模式有利也有弊,好处就是对象之间的解耦,缺点就是创建订阅者 要消耗一定的时间和内存(暂时没有体验到)