数据驱动
数据响应式:
- 数据模型仅仅是普通的javascript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率
双向绑定:
- 数据改变,视图改变;视图改变,数据也随之改变
- 我们可以使用v-model在表单元素上创建双向绑定
数据驱动是vue最独特的特性之一:
- 开发过程中仅需要关注数据本身,不需要关心数据是如何渲染到视图的
数据响应式的核心原理
Vue2.x中是基于Object.defineProperty实现的;
let data = {
msg:'hello',
count:10,
};
let vm = {};
Object.keys(data).forEach((v) => {
Object.defineProperty(vm,v,{
enumerable:true,
configurable:true,
get(){
console.log('get');
return data[v]
},
set(value){
console.log('set:' + value);
if(value === data[v]) return;
data[v] = value;
document.getElementById('app').innerHTML = value
}
})
})
vm.msg = 'hello world';
console.log(vm.msg)
Vue3.x中是基于proxy来实现的;
let data = {
msg:'hello',
count:10
};
let vm = new Proxy(data,{
get(target,key){
console.log('get:' + key);
return target[key];
},
set(target,key,value){
if(value === target[key])return;
console.log('set:'+key);
target[key] = value;
document.querySelector('#app').innerHTML = value;
}
})
vm.msg = 'hello world';
console.log(vm.msg)
发布订阅模式
发布订阅模式:
- 订阅者
- 发布者
- 信号中心 我们假定,存在着一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行,这就叫做“发布/订阅模式”(publish-subscribe pattern)
class EventEmmit{
constructor(){
this.subs = {}
}
$on(type,fn){
this.subs[type] = this.subs[type] || [];
this.subs[type].push(fn);
}
$emit(type){
if(this.subs[type]){
this.subs[type].forEach((fn) => fn())
}
}
}
let em = new EventEmmit();
em.$on('click',() => {
console.log('click1')
})
em.$on('click',() => {
console.log('click2')
})
em.$emit('click');
观察者模式
观察者(订阅者) -- Watcher
- update():当事件发生时,具体要做的事情 目标(发布者) -- Dep
- subs数组:存储所有的观察者
- addSub():添加观察者
- notify():当事件发生,调用所有观察者的update()方法 没有事件中心
class Dep{
constructor(){
this.subs = []
}
addSubs(sub){
if(sub && sub.update){
this.subs.push(sub)
}
}
notify(){
this.subs.forEach((sub) => sub.update())
}
}
class Watcher{
update(){
console.log('watcher')
}
}
let dep = new Dep();
let watch = new Watcher();
dep.addSubs(watch);
dep.notify();
总结:
- 观察者模式是由具体目标调度,比如当事件触发,Dep就会去调用观察者的方法,所有观察者模式的订阅者与发布者直接是存在依赖的;
- 发布/订阅模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在
vue响应式原理模拟
vue整体结构:
- Vue:把data中的成员注入到vue实例,并且把data中的成员转成getter/setter
- Observer:能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知Dep;
- Compiler:解析每个元素中的指令/插值表达式,并替换成相应的数据
- Dep:添加观察者(watcher),当数据变化时通知所有观察者
- Watcher:数据变化更新视图 Vue:
- 功能:
- 负责接收初始化的参数(选项)
- 负责把data中的属性注入到Vue实例,转换成getter/setter
- 负责调用observer监听data中所有属性的变化
- 负责调用compiler解析指令/插值表达式
- 结构:
- 实现
- 构造器:(1)创建vue实例;(2)保存options参数;(3)挂载传入的data数据,调用this._proxyData方法将el;(5)调用Observer对象,传入this.$data,监听数据的变化;(6)调用Compiler对象,传入当前的vue实例,解析指令和插值表达式;
- _proxyData方法:接收vue实例中的$data对象,遍历对象中的属性,将每个属性用Object.defineProperty转换成getter和setter,并挂载到当前Vue实例上;
class Vue {
constructor(options) {
// 通过属性保存选项中的数据
this.$options = options || {};
this.$data = options.data || {};
// 判断options.el是选择器还是document对象
this.$el =
typeof options.el === 'string'
? document.querySelector(options.el)
: options.el;
// 把data中的成员转换成getter/setter,注入到vue实例中
this._proxyData(this.$data);
// 调用Observer对象 监听数据得变化
new Observer(this.$data);
// 调用Compiler对象,解析指令和插值表达式
new Compiler(this)
}
_proxyData(data) {
// 遍历data中的所有属性
Object.keys(data).forEach((key) => {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get() {
return data[key];
},
set(value) {
if (value === data[key]) return;
data[key] = value;
},
});
});
}
}
Observer:
- 功能
- 负责把data选项中的属性转换成响应式数据
- data中的某个属性也是对象,把该属性转换成响应式数据
- 数据变化发送通知
- 结构
- 实现
- 构造器:接收传入的data对象,并调用walk方法
- walk方法:判断传入的参数是不是对象,如果是对象就遍历对象中的所有属性并调用defineReactive方法,将每个属性转换成getter/setter;
- defineReactive方法:接收三个参数:对象,属性,和当前属性的值;第三个参数要传value是因为如果此处直接取obj[key]就好发生对象的循环调用,造成堆栈溢出;如果传入的value是对象,则递归调用walk方法,把value对象内部的属性也转换成getter/setter;调用DEP对象,收集依赖并发送通知;在get中收集依赖;在set中,如果新的value是对象,则调用walk将新的value也转换成getter/setter,并发送通知更新视图;
class Observer {
constructor(data) {
this.walk(data);
}
walk(data) {
// 判断data是不是对象,不是对象就什么都不做
if (!data || typeof data !== 'object') return;
// 遍历data对象的所有属性
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key]);
});
}
// 第三个参数要穿value是因为如果此处直接取obj[key]就会发生对象的循环调用,造成堆栈溢出
// 此处会形成闭包,所有value的值并不会被垃圾回收机制处理掉
defineReactive(obj, key, value) {
let that = this;
// 负责收集依赖并发送通知
let dep = new Dep();
// 如果value是对象,则把value内部的属性也转换成响应式数据
this.walk(value);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 收集依赖
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) {
if (newValue === value) return;
value = newValue;
// 如果newValue是对象,则把newValue内部的属性转换成响应式数据
that.walk(newValue);
// 发送通知
dep.notify()
},
});
}
}
Compiler:
- 功能
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染
- 当数据变化后重新渲染视图
- 结构
- 实现
- 构造器:保存传入的vue实例;保存vue实例中的$el;调用compiler()方法,传入el,编译模板;
- compile方法:编译模板,处理文本节点和元素节点;通过el.childNodes获取el下面的所有子节点;遍历所有的子节点,如果是文本节点,则调用compileText方法处理文本节点,如果是元素节点,则调用compileElement方法处理元素节点;如果当前node节点下面还有子节点,则递归调用compile方法,继续处理
- isDirective方法:判断传入的attrName属性是不是指令;vue的指令都是以v-开头的,,所有判断属性是否以v-开头;
- isTextNode方法:判断传入的node节点是不是文本节点;node属性中有一个nodeType字段,nodeType=3则是文本节点;
- isElementNode方法:判断传入的node节点是不是元素节点;nodeType=1是元素节点;
- compileElement方法:接收node节点,获取node节点的所有属性,判断是否是vue指令,如果是vue指令则调用相应方法执行指令任务;
- compileText方法:接收文本节点,定义正则表达式,处理插值表达式,调用watcher对象,创建观察者,当数据改变时,更新视图;
- update方法:接收node节点,当前属性和当前属性的值,根据属性名称去调用相应的指令处理方法,调用时使用call方法,将this指向当前的compile实例;
- textUpdate方法:处理v-text指令,将当前node节点的textContent值改为value,并创建watch对象,监听数据变化更新视图;
- modelUpdate方法:处理v-model指令,将当前node节点的value值改为传入的value,并为当前节点监听input事件,实现双向数据绑定;然后创建watch监听,监听数据变化更新视图;
class Compiler {
constructor(vm) {
this.el = vm.$el;
this.vm = vm;
this.compile(this.el);
}
// 编译模板,处理文本节点和元素节点
compile(el) {
// 获取el下面的所有子节点
let childNodes = Array.from(el.childNodes);
childNodes.forEach((node) => {
// 处理文本节点
if (this.isTextNode(node)) {
this.compileText(node);
}
// 处理元素节点
if (this.isElementNode(node)) {
this.compileElement(node);
}
// 判断node节点是否有子节点,如果有子节点,要递归调用compiler
if (node.childNodes && node.childNodes.length) {
this.compile(node);
}
});
}
// 编译元素节点,处理指令
compileElement(node) {
// console.dir(node.attributes);
// 获取元素节点所以的属性
let attributes = Array.from(node.attributes);
// 遍历所有属性
attributes.forEach((attr) => {
// 判断属性是不是指令(是不是以v-开头)
if (this.isDirective(attr.name)) {
let attrName = attr.name.substr(2);
let attrValue = attr.value;
this.update(node, attrValue, attrName);
}
});
}
update(node, key, attrName) {
let updateFn = this[attrName + 'Update'];
// 调用call方法将updateFn执行时的this指向当前compiler对象
updateFn && updateFn.call(this,node, this.vm[key],key);
}
// 处理v-text指令
textUpdate(node, value,key) {
node.textContent = value;
new Watcher(this.vm,key,(newValue) => {
node.textContent = newValue;
})
}
// 处理v-model指令
modelUpdate(node, value,key) {
node.value = value;
new Watcher(this.vm,key,(newValue) => {
node.value = newValue;
})
//给node添加input事件,实现双向数据绑定
node.addEventListener('input',() => {
this.vm[key] = node.value;
})
}
// 编译文本节点,处理差值表达式
compileText(node) {
// 定义正则表达式,匹配差值表达式 {{ }}
let reg = /\{\{(.+?)\}\}/;
// 获取文本节点的内容
let value = node.textContent;
if (reg.test(value)) {
let key = RegExp.$1.trim();
node.textContent = value.replace(reg, this.vm[key]);
new Watcher(this.vm,key,(newValue) => {
node.textContent = newValue;
})
}
}
// 判断元素属性是否是指令
isDirective(attrName) {
// 判断属性是否以v-开头,vue中的指令都是以v-开头的
return attrName.startsWith('v-');
}
// 判断节点是否是文本节点
isTextNode(node) {
// node属性中的nodeType = 3是文本节点
return node.nodeType === 3;
}
// 判断节点是否是元素节点
isElementNode(node) {
// node属性中的nodeType = 1是元素节点
return node.nodeType === 1;
}
}
Dep:
- 功能
- 收集依赖,添加观察者(watcher)
- 通知所有观察者
- 结构
- 实现
- 构造器:创建subs数组,存储所有的观察者
- addSub方法:添加观察者
- notify方法:发送通知
class Dep{
constructor(){
// 存储所有的观察者
this.subs = []
}
// 添加观察者
addSub(sub){
if(sub && sub.update){
this.subs.push(sub)
}
}
// 发送通知
notify(){
this.subs.forEach((sub) => {
sub.update();
})
}
}
Watcher:
- 功能
- 当数据变化触发依赖,dep通知所有的Watcher实例更新视图
- 自身实例化的时候往Dep
- 结构
- 实现
- 构造器:结受vue实例,key和一个回调函数,并保存到当前实例中;把当前的watcher对象记录到Dep类的target属性上;触发get方法保存oldValue;添加完成之后清空Dep的target属性,防止重复添加;
- update方法:当数据发送变化时,调用回调函数更新视图;
class Watcher{
constructor(vm,key,cb){
this.vm = vm;
// data中的属性名称
this.key = key;
// 回调函数
this.cb = cb;
// 把watcher对象记录到Dep类的静态属性target
Dep.target = this;
// 触发get方法,在get方法中会调用addSub;
this.oldValue = vm[key];
// 添加完之后清空Dep的target属性
Dep.target = null;
}
// 当数据发送变化时更新视图
update(){
let newValue = this.vm[this.key];
if(newValue === this.oldValue) return;
this.cb(newValue);
}
}