1) 视图(View):用户界面
2) 控制器(Controller):业务逻辑
3) 模型(Model):数据保存
1)视图传送指令到控制器
2)控制器完成业务逻辑后要求模型改变状态
3)模型将新的数据发送到视图,用户得到反馈
1) 用户可以向视图(View)发送指令(DOM事件),再由View直接要求Model改变状态;
2) 用户也可以向Controller发送指令(改变URL触发hashChange事件,再由Controller发送给View
3) Controller很薄,只起到路由作用,而View非常厚,业务逻辑都放在View,所以Backbone索性取消了Controller,只保留了Router(路由器)
1)各部分之间的通信都是双向的;
2)视图(View)和模型(Model)不发生联系,都是通过表现(Presenter)传递的
3)View非常薄,不部署任何业务逻辑,称之为被动视图(Passive View)即没有任何主动性,而Presenter非常厚,所有逻辑都在这里
1) 它和MVP的区别是,采用双向绑定,视图层(View)的变动,自动反映在ViewModel,反之亦然,Angular和Vue,React采用这种方式
2) MVVM的提出源于WPF,主要用于分离应用界面层和业务逻辑层,WPF,Siverlight都基于数据驱动开发
3) MVVM模式中,一个ViewModel和一个View匹配,完全和View绑定,所有View中的修改变化,都会更新到ViewModel中,同时ViewModel的任何变化都会同步到View上显示;之所以自动同步是ViewModel的属性都实现了observable这样的接口,也就是说当使用属性的set方法,会同时出发属性修改的事件,使绑定的UI自动刷新;
1) 建立虚拟DOM Tree,通过document.createDocumentFragment(),遍历指定根节点内部节点,根据{{prop}},v-model等规则进行compile(主要负责给node节点赋值);
2) 通过Object.defineProperty()进行数据变化拦截
3) 截取到的数据变化,通过发布者-订阅者模式,触发Watcher,从而改变虚拟DOM中的具体数据;
订阅发布模式(又称为观察者模式)定义一种一对多的关系,让多个观察者同时监听一个主题对象,主题对象状态发生改变的时候通知所有的观察者。
发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应的操作
Dep 主要负责依赖的收集
Watcher 主要负责Dep和Compiler之间的联系
Compiler 可以理解为virtual dom(虚拟DOM) + patch 也就是负责视图层的渲染
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象
语法:Object.defineProperty(obj,prop,descriptor)
obj:要在其上定义属性的对象
prop:要定义或者修改的属性名字
descriptor:将定义或修改的属性描述符
var obj = {};
Object.defineProperty(obj,'hello',{ //这里整个都是属性描述符
get:function(){
//我们在这里拦截到了数据
console.log("get方法被调用");
},
set:function(newValue){
//改变数据的值,拦截下来额
console.log("set方法被调用");
}
});
obj.hello//输出为“get方法被调用”,输出了值。
obj.hello = 'new Hello';//输出为set方法被调用,修改了新值
let obj = {};
Object.defineProperty(obj, 'name', {
set: function(newValue){
console.log('触发setter');
document.querySelector('.text-box').innerHTML = newValue;
document.querySelector('.inp-text').value = newValue;
},
get: function(){
console.log('触发getter');
}
});
document.querySelector('.inp-text').addEventListener('keyup', function(e){
obj.name = e.target.value;
}, false);
{{ msg }}
{{ msg }}
var container = document.getElementById('container');
//这里我们把vue实例中的data提取出来,更加直观
var data = {
msg: 'Hello world!',
inpText: 'Input text'
};
var fragment = virtualDom(container, data);
container.appendChild(fragment);
//虚拟dom创建方法,将目标盒子内所有子节点添加到其内部,注意这里只有子节点
function virtualDom(node, data){
let frag = document.createDocumentFragment();
let child;
// 遍历dom节点
while(child = node.firstChild){
compile(child, data);
frag.appendChild(child);
}
return frag;
}
//编译规则,子节点通过compile进行编译,a:如果节点为元素,其nodeType = 1;b:如果节点为文本,其nodeType = 3
function compile(node, data){
let reg = /\{\{(.*)\}\}/g;
if(node.nodeType === 1){ // 标签
let attr = node.attributes;
for(let i = 0, len = attr.length; i < len; i++){
// console.log(attr[i].nodeName, attr[i].nodeValue);
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
node.value = data[name]; //给node节点赋值data
}
}
if(node.hasChildNodes()){
node.childNodes.forEach((item) => {
compile(item, data); // 递归,如果第二步子节点仍有子节点,通过hasChildNodes()来确认,如果有递归调用Compile方法
});
}
}
if(node.nodeType === 3){ // 文本节点
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
node.nodeValue = data[name];
}
}
}
function defineReact(obj, key, value){
Object.defineProperty(obj, key, {
set: function(newValue){
console.log(`触发setter`);
value = newValue;
console.log(value);
},
get: function(){
console.log(`触发getter`);
return value;
}
});
}
function observe(obj, vm){
Object.keys(obj).forEach((key) => {
defineReact(vm, key, obj[key]); //定义访问器属性
})
}
function Vue(options){
this.data = options.data;
let id = options.el;
observe(this.data, this); // 将每个data属相绑定到Vue的实例上this
}
var vm = new Vue({
el: 'container',
data: {
msg: 'Hello world!',
inpText: 'Input text'
}
});
console.log(vm.msg); // Hello world!
console.log(vm.inpText); // Input text
function Vue(options){
this.data = options.data;
let id = options.el;
observe(this.data, this); // 将每个data属相绑定到Vue的实例上this
//------------------------添加以下代码
let container = document.getElementById(id);
let fragment = virtualDom(container, this); // 这里通过vm对象初始化
container.appendChild(fragment);
}
function compile(node, data){
let reg = /\{\{(.*)\}\}/g;
if(node.nodeType === 1){ // 标签
let attr = node.attributes;
for(let i = 0, len = attr.length; i < len; i++){
// console.log(attr[i].nodeName, attr[i].nodeValue);
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
node.value = data[name];
// ------------------------添加监听事件
node.addEventListener('keyup', function(e){
data[name] = e.target.value;
}, false);
// -----------------------------------
}
}
if(node.hasChildNodes()){
node.childNodes.forEach((item) => {
compile(item, data);
});
}
}
if(node.nodeType === 3){ // 文本节点
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
node.nodeValue = data[name];
}
}
}
*****订阅者:三个订阅者都有update方法
var subscribe_1 = {
update: function(){
console.log('This is subscribe_1');
}
};
var subscribe_2 = {
update: function(){
console.log('This is subscribe_2');
}
};
var subscribe_3 = {
update: function(){
console.log('This is subscribe_3');
}
};
*****发布者:发布者通过notify方法对订阅者广播,订阅者通过update来接受信息
function Publisher(){
this.subs = [subscribe_1, subscribe_2, subscribe_3]; // 添加订阅者
}
Publisher.prototype = {
constructor: Publisher,
notify: function(){
this.subs.forEach(function(sub){
sub.update();
})
}
};
*****实例化publisher:
var publisher = new Publisher();
publisher.notify();
*****创建中间件来处理发布者-订阅者模式:
var publisher = new Publisher();
var middleware = {
publish: function(){
publisher.notify();
}
};
middleware.publish();
*****发布者:
function Publisher(){
this.subs = []; // 订阅者容器
}
Publisher.prototype = {
constructor: Publisher,
add: function(sub){
this.subs.push(sub); // 添加订阅者
},
notify: function(){
this.subs.forEach(function(sub){
sub.update(); // 发布订阅
});
}
};
function Subscriber(node, vm, name){
this.node = node;
this.vm = vm;
this.name = name;
}
Subscriber.prototype = {
constructor: Subscriber,
update: function(){
let vm = this.vm;
let node = this.node;
let name = this.name;
switch(this.node.nodeType){
case 1:
node.value = vm[name]; //赋值功能移到了订阅者这里
break;
case 3:
node.nodeValue = vm[name]; //赋值功能移到了订阅者这里
break;
default:
break;
}
}
};
*****我们要把订阅者添加到compile进行虚拟dom的初始化,替换掉原来的赋值:
function compile(node, data){
let reg = /\{\{(.*)\}\}/g;
if(node.nodeType === 1){ // 标签
let attr = node.attributes;
for(let i = 0, len = attr.length; i < len; i++){
// console.log(attr[i].nodeName, attr[i].nodeValue);
if(attr[i].nodeName === 'v-model'){
let name = attr[i].nodeValue;
// --------------------这里被替换掉
// node.value = data[name];
new Subscriber(node, data, name);
// ------------------------添加监听事件
node.addEventListener('keyup', function(e){
data[name] = e.target.value;
}, false);
}
}
if(node.hasChildNodes()){
node.childNodes.forEach((item) => {
compile(item, data);
});
}
}
if(node.nodeType === 3){ // 文本节点
if(reg.test(node.nodeValue)){
let name = RegExp.$1;
name = name.trim();
// ---------------------这里被替换掉
// node.nodeValue = data[name];
new Subscriber(node, data, name);
}
}
}
function Subscriber(node, vm, name){
this.node = node;
this.vm = vm;
this.name = name;
this.update();
}
Subscriber.prototype = {
constructor: Subscriber,
update: function(){
let vm = this.vm;
let node = this.node;
let name = this.name;
switch(this.node.nodeType){
case 1:
node.value = vm[name];
break;
case 3:
node.nodeValue = vm[name];
break;
default:
break;
}
}
};
*****发布者添加到 defineRect函数,来观察数据的变化:
function defineReact(data, key, value){
let publisher = new Publisher();
Object.defineProperty(data, key, {
set: function(newValue){
console.log(`触发setter`);
value = newValue;
console.log(value);
publisher.notify(); // 发布订阅
},
get: function(){
console.log(`触发getter`);
if(Publisher.global){ //这里为什么来添加判断条件,主要是让publisher.add只执行一次,初始化虚拟dom编译的时候来执行
publisher.add(Publisher.global); // 添加订阅者
}
return value;
}
});
}
function Subscriber(node, vm, name){
Publisher.global = this;
this.node = node;
this.vm = vm;
this.name = name;
this.update();
Publisher.global = null;
}
Subscriber.prototype = {
constructor: Subscriber,
update: function(){
let vm = this.vm;
let node = this.node;
let name = this.name;
switch(this.node.nodeType){
case 1:
node.value = vm[name];
break;
case 3:
node.nodeValue = vm[name];
break;
default:
break;
}
}
};
cnpm install vuex –save
import Vue from 'vue'
import App from './App'
//1.引入vuex
import Vuex from 'vuex'
import Apple from './components/Apple'
import Banana from './components/Banana'
Vue.config.productionTip = false
//2.注册
Vue.use(Vuex);
//3.实例化store
let store=new Vuex.Store({
state:{
totalPrice:0
},
mutations:{
increment(state,price){
state.totalPrice+=price
},
decrement(state,price){
state.totalPrice-=price
}
},
actions:{
increase (context,price){
context.commit('increment',price)
},
decrease (context,price){
context.commit('decrement',price)
}
},
getters:{
discount(state){
return state.totalPrice *0.8;
}
}
})
new Vue({
el: '#app',
//4.把store放在全局的实例化对象里,可以在全局的所有地方用
store,
components: { App},
template: ' '
})
*****参数介绍:
1) state
vuex使用单一状态树,那么就可以用一个对象包含全部的应用层级状态,所以state就作为数据源
2) mutations
更改Vuex的store中的状态的唯一方法就是提交mutations,Vuex中的mutations非常类似于事件:每个mutation都有一个字符串的 事件类型(type)和一个回调函数(handler),这个回调函数就是我们实际进状态更改的地方,并且它会接受state作为第一个参数。不能直接调用一个mutation handler,这个选项更像是事件注册:当触发一个type为 increment的mutation时,就调用handler。要唤醒一个mutation handler,需要调用store.commit方法触发相应的type,可以向store.commit传入额外的参数,这个参数就叫做mutation的载荷。在更多的情况下,载荷应该是一个对象,这样可以包含更多的字段;
mutations必须是同步函数,那么我们如何来异步的更新state呢?答案就是actions
3) actions
actions类似于mutations,不同的是:
actions提交的是mutations,而不是直接变更状态,这也就形成了actions--mutations--state的过程;
actions可以包含任意异步操作;
action 函数接受一个与store实例具有相同方法和属性的context对象,因此你可以调用context.commit提交一个mutation,或者通过context.state和context.getter来获取state和getter,但是如何触发呢?答案是:store.dispatch
4) getters
有时候我们需要从store中的state中派生出一些状态,getter会暴露为store.getter对象在组件中使用。
5) modules
除了上边用到的4个参数,store还有另一个参数:modules;
vuex允许把store进行一个功能拆分,分割成不同的模块(module),每个模块都拥有自己的store,mutation,action,getters
总价{{totalPrice}}
折后价:{{discountPrice}}
import { mapState } from 'vuex'
computed: {
...mapState(['totalPrice'])
...
}
*****Banana.vue:
{{msg}} 单价{{price}}
methods:{
addOne(){
this.increment(this.price)
},
minusOne(){
this.decrement(this.price)
},
...mapMutations(['increment', 'decrement'])
}
{{msg}}单价:{{price}}