{
tag: 'DIV',
attrs:{
id:'app'
},
children: [
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] }
]
},
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] },
{ tag: 'A', children: [] }
]
}
]
}
把上面虚拟Dom转化成下方真实Dom
function createDOM(vnode) {
if (typeof vnode === 'string') {
// 用于创建文本节点,即将一个字符串转换为一个 DOM 元素
return document.createTextNode(vnode);
}
// 解构赋值
const {
tag,
attrs = {},
children = []
} = vnode;
const el = document.createElement(tag);
// 将其作为元素的属性名
for (let attr in attrs) {
el.setAttribute(attr, attrs[attr]);
}
for (let child of children) {
el.appendChild(createDOM(child));
}
return el;
}
全局事件总线,严格来说不能说是观察者模式,而是发布-订阅模式。它在我们日常的业务开发中应用非常广。
在Vue中使用Event Bus来实现组件间的通讯
Event Bus/Event Emitter
作为全局事件总线,它起到的是一个沟通桥梁的作用。我们可以把它理解为一个事件中心,我们所有事件的订阅/发布都不能由订阅方和发布方“私下沟通”,必须要委托这个事件中心帮我们实现。
在Vue中,有时候 A 组件和 B 组件中间隔了很远,看似没什么关系,但我们希望它们之间能够通信。这种情况下除了求助于 Vuex
之外,我们还可以通过 Event Bus
来实现我们的需求。
创建一个 Event Bus
(本质上也是 Vue 实例)并导出:
const EventBus = new Vue()
export default EventBus
在主文件里引入EventBus,并挂载到全局:
import bus from 'EventBus的文件路径'
Vue.prototype.bus = bus
订阅事件:
// 这里func指someEvent这个事件的监听函数
this.bus.$on('someEvent', func)
发布(触发)事件:
// 这里params指someEvent这个事件被触发时回调函数接收的入参
this.bus.$emit('someEvent', params)
大家会发现,整个调用过程中,没有出现具体的发布者和订阅者(比如上面的PrdPublisher
和DeveloperObserver
),全程只有bus
这个东西一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的发布/订阅操作,必须经由事件中心,禁止一切“私下交易”!
class EventEmitter {
constructor() {
// handlers是一个map,用于存储事件与回调之间的对应关系
this.handlers = {}
}
// on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
on(eventName, cb) {
// 先检查一下目标事件名有没有对应的监听函数队列
if (!this.handlers[eventName]) {
// 如果没有,那么首先初始化一个监听函数队列
this.handlers[eventName] = []
}
// 把回调函数推入目标事件的监听函数队列里去
this.handlers[eventName].push(cb)
}
// emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
emit(eventName, ...args) {
// 检查目标事件是否有监听函数队列
if (this.handlers[eventName]) {
this.handlers[eventName].forEach((callback) => {
callback(...args)
})
}
}
// 移除某个事件回调队列里的指定回调函数
off(eventName, cb) {
const callbacks = this.handlers[eventName];
const index = callbacks.indexOf(cb);
if (index !== -1) {
callbacks.splice(index, 1)
}
}
// 为事件注册单次监听器
once(eventName, cb) {
// 对回调函数进行包装,使其执行完毕自动被移除
const wrapper = (...args) => {
cb.apply(...args);
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}
defineProperty 版本
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 数据劫持
Object.defineProperty(data, 'text', {
// 数据变化 --> 修改视图
set(newVal) {
input.value = newVal;
span.innerHTML = newVal;
}
})
// 视图更改 --> 数据变化
input.addEventListener('keyup', function (e) {
data.text = e.target.value;
});
proxy 版本
// 数据
const data = {
text: 'default'
};
const input = document.getElementById('input');
const span = document.getElementById('span');
// 数据劫持
const handler = {
set(target, key, value) {
target[key] = value;
// 数据变化 --> 修改视图
input.value = value;
span.innerHTML = value;
return value;
}
}
const proxy = new Proxy(data, handler);
// 视图更改 --> 数据变化
input.addEventListener('keyup', function (e) {
proxy.text = e.target.value;
});
Vue
,这个类接收的是一个options
,那么其中可能有需要挂载的根元素的id
,也就是el
属性;然后应该还有一个data
属性,表示需要双向绑定的数据。Dep
类,这个类产生的实例对象中会定义一个subs
数组用来存放所依赖这个属性的依赖,已经添加依赖的方法addSub
,还有一个update
方法用来遍历更新它subs
中的所有依赖,同时Dep类有一个静态属性target
它用来表示当前的观察者,当后续进行依赖收集的时候可以将它添加到dep.subs
中。observe
方法,这个方法接收的是传进来的data
,也就是options.data
,里面会遍历data
中的每一个属性,并使用Object.defineProperty()
来重写它的get
和set
,那么这里面呢可以使用new Dep()
实例化一个dep
对象,在get
的时候调用其addSub
方法添加当前的观察者Dep.target
完成依赖收集,并且在set
的时候调用dep.update
方法来通知每一个依赖它的观察者进行更新。compile
方法来将HTML模版和数据结合起来。在这个方法中首先传入的是一个node
节点,然后遍历它的所有子级,判断是否有firstElmentChild
,有的话则进行递归调用compile方法,没有firstElementChild
的话且该child.innderHTML
用正则匹配满足有/\{\{(.*)\}\}/
项的话则表示有需要双向绑定的数据,那么就将用正则new Reg('\\{\\{\\s*' + key + '\\s*\\}\\}', 'gm')
替换掉是其为msg
变量。Dep.target
指向当前的这个child
,且调用一下this.opt.data[key]
,也就是为了触发这个数据的get
来对当前的child
进行依赖收集,这样下次数据变化的时候就能通知child
进行视图更新了,不过在最后要记得将Dep.target
指为null
哦(其实在Vue
中是有一个targetStack
栈用来存放target
的指向的)。document
的DOMContentLoaded
然后在回调函数中实例化这个Vue
对象就可以了
姓名
{{name}}
年龄
{{age}}
document.addEventListener("DOMContentLoaded", () => {
let opt = {
el: "#app",
data: {
name: "等待修改...",
age: 20
}
};
let vm = new Vue(opt);
setTimeout(() => {
opt.data.name = "jing";
}, 2000);
}, false)
class Vue {
constructor(opt) {
this.opt = opt;
this.observer(opt.data);
let root = document.querySelector(opt.el);
this.compile(root);
}
observer(data) {
// 遍历数据对象
Object.keys(data).forEach((key) => {
// 创建一个 Dep 对象实例
let obv = new Dep();
// 为每一个属性添加一个下划线开头的备份属性
data["_" + key] = data[key];
// 通过 Object.defineProperty() 方法为数据对象的每一个属性设置 getter 和 setter
Object.defineProperty(data, key, {
// getter 方法,用于获取属性值
get() {
// 在 Dep.target 存在的情况下,向当前 Dep 对象添加订阅者(即存储一个对该订阅者的引用)
Dep.target && obv.addSubNode(Dep.target);
// 返回备份属性的值
return data["_" + key];
},
set(newVal) {
// 通过当前 Dep 对象向所有订阅该对象的订阅者发送通知
obv.update(newVal);
// 更新备份属性的值
data["_" + key] = newVal;
}
})
})
}
compile(node) {
// 通过 Array.prototype.forEach.call 将 NodeList 转换为数组并循环处理
[].forEach.call(node.childNodes, (child) => {
// 如果该节点没有子节点,且内部包含形如 {{xxx}} 的模板字符串,则执行以下逻辑
if (!child.firstElementChild && /\{\{(.*)\}\}/.test(child.innerHTML)) {
// 获取模板字符串中的变量名
let key = RegExp.$1.trim();
// 将模板字符串中的变量名替换为变量的实际值
child.innerHTML = child.innerHTML.replace(
new RegExp("\\{\\{\\s*" + key + "\\s*\\}\\}", "gm"),
this.opt.data[key]
)
// 将当前节点设置为 Dep.target(订阅器的静态属性)
Dep.target = child;
// 获取变量的实际值,这会触发该变量的 getter,从而将当前节点添加为其依赖
this.opt.data[key];
// 将 Dep.target 重置为 null
Dep.target = null;
} // 如果该节点有子节点,则递归调用 compile 函数处理子节点
else if (child.firstElementChild) {
this.compile(child);
}
})
}
}
class Dep {
constructor() {
this.subNode = [];
}
// 添加一个新的节点到subNode数组中
addSubNode(node) {
this.subNode.push(node);
}
// 遍历subNode数组,更新节点的内容为newVal
update(newVal) {
this.subNode.forEach((node) => {
node.innerHTML = newVal;
});
}
}
function update() {
console.log('数据变化~~~ mock update view')
}
let obj = [1, 2, 3];
// 变异方法 push shift unshfit reverse sort splice pop
// Object.defineProperty
let oldProto = Array.prototype;
let proto = Object.create(oldProto); // 克隆了一分
['push', 'shift'].forEach(item => {
proto[item] = function () {
update();
oldProto[item].apply(this, arguments);
}
})
function observer(value) { // proxy reflect
if (Array.isArray(value)) {
return value.__proto__ = proto;
// 重写 这个数组里的push shift unshfit reverse sort splice pop
}
if (typeof value !== 'object') {
return value;
}
for (let key in value) {
defineReactive(value, key, value[key]);
}
}
function defineReactive(obj, key, value) {
observer(value); // 如果是对象 继续增加getter和setter
Object.defineProperty(obj, key, {
get() {
return value;
},
set(newValue) {
if (newValue !== value) {
observer(newValue);
value = newValue;
update();
}
}
})
}
observer(obj);
// AOP
// obj.name = {n:200}; // 数据变了 需要更新视图 深度监控
// obj.name.n = 100;
obj.push(123);
obj.push(456);
console.log(obj);
首先定义了一个
update
函数,用于在数据变化时更新视图。接着创建了一个数组对象obj
,并定义了一个observer
函数,该函数判断传入的值是不是一个对象,如果是对象,就遍历对象的所有属性,给每个属性添加 getter 和 setter,从而实现数据劫持。如果是一个数组,就对数组的原型对象进行克隆,并重写了数组对象的push
和shift
方法,以便在数据变化时能够自动更新视图。
defineReactive
函数用于定义一个对象属性的 getter 和 setter,其中get
方法返回该属性的值,set
方法在该属性被赋新值时更新该属性的值并调用update
函数更新视图。在set
方法中,如果新的值与旧的值不同,则先调用observer
函数,如果新的值也是一个对象,那么会给它的属性添加 getter 和 setter,实现递归的数据劫持。最后调用
observer
函数,对obj
进行数据劫持,并给obj
调用push
方法添加 AOP,以便在push
方法被调用时自动更新视图。最终输出了
obj
的值,即[1,2,3,123,456]
。
简单实现:
封装成一个class:
const box = document.getElementsByClassName('box')[0];
class HashRouter {
constructor(hashStr, cb) {
this.hashStr = hashStr
this.cb = cb
this.watchHash()
this.watch = this.watchHash.bind(this)
window.addEventListener('hashchange', this.watch)
}
watchHash() {
let hash = window.location.hash.slice(1)
this.hashStr = hash
this.cb(hash)
}
}
new HashRouter('red', (color) => {
box.style.background = color
})
function createStore(reducer) {
let currentState
let listeners = []
function getState() {
return currentState
}
function dispatch(action) {
currentState = reducer(currentState, action)
listeners.map(listener => {
listener()
})
return action
}
function subscribe(cb) {
listeners.push(cb)
return () => {}
}
dispatch({
type: 'ZZZZZZZZZZ'
})
return {
getState,
dispatch,
subscribe
}
}
// 应用实例如下:
function reducer(state = 0, action) {
switch (action.type) {
case 'ADD':
return state + 1
case 'MINUS':
return state - 1
default:
return state
}
}
const store = createStore(reducer)
console.log(store);
store.subscribe(() => {
console.log('change');
})
console.log(store.getState());
console.log(store.dispatch({
type: 'ADD'
}));
console.log(store.getState());