MVC与单向绑定和双向绑定


MVC

MVC 的出现

举栗子
如果没有 MVC 代码是怎样的呢?
没错就是意大利面条

axios.interceptors.response.use(function(response) {
  let {
    config: { url, method, data }
  } = response;
  data = JSON.parse(data || "{}");
  let row = {
    id: 1,
    name: "JavaScript 高级程序设计",
    number: 2
  };
  if (url === "/books/1" && method === "get") {
    response.data = row;
  } else if (url === "/books/1" && method === "put") {
    response.data = Object.assign(row, data);
  }
  return response;
});
// <!-- 专门负责操作远程数据的代码 -->

function fetchDb() {
  return axios.get("/books/1");
}

function saveDb(newData) {
  return axios.put("/books/1", newData);
}

// <!-- 专门呈现页面元素的代码 -->
var template = `

<div>
  书名:《__name__》,
  数量:<span id="number">__number__</span>
</div>
<div class="actions">
  <button id="increaseByOne">加1</button>
  <button id="reset">归零</button>
</div>
`;
fetchDb().then(response => {
  let result = response.data;
  $("#app").html(
    template
      .replace("__number__", result.number)
      .replace("__name__", result.name)
  );
  // <!-- 其他逻辑代码 -->
  //加1
  $("#increaseByOne").on("click", e => {
    let oldResult = parseInt($("#number").text(), 10);
    let newResult = oldResult + 1;
    saveDb({ number: newResult }).then(function({ data }) {
      console.log(data);
      $("#number").text(data.number);
    });
  });

  //归零
  $("#reset").on("click", e => {
    let newResult = 0;
    saveDb({ number: newResult }).then(({ data }) => {
      $("#number").text(data.number);
    });
  });
});

聪明的前端程序员发现这些代码总是可以分为三类(类似于后端的分类)

专门操作远程数据的代码(fetchDb 和 saveDb 等等)
专门呈现页面元素的代码(innerHTML 等等)
其他控制逻辑的代码(点击某按钮之后做啥的代码)
慢慢的被大家完善完善,最终形成了现在的 MVC 思想

M 专门负责数据
V 专门负责表现
C 负责其他逻辑
ok,看懂上面的代码,我们就可以尝试把这些代码改写成 MVC 的模式

改写成 MVC

分成三个对象 view model controller
model 只负责存储数据,请求数据,更新数据
view 只负责渲染 HTML
controller 负责调度 mdoel 和 view
看代码

let model = {
  data: {
    number: 0,
    name: ""
  },
  fetch(id) {
    return axios.get(`/books/${id}`).then(response => {
      this.data = response.data;
    });
  },
  update(newData) {
    let id = this.data.id;
    return axios.put(`/books/${id}`, newData).then(({ data }) => {
      this.data = data;
    });
  }
};

let view = {
  el: "#app",
  template: `
<div>
  书名:《__name__》,
  数量:__number__
</div>
<div class="actions">
  <button id="increaseByOne">加1</button>
  <button id="reset">归零</button>
</div>
  `,
  render(data) {
    let html = this.template
      .replace("__name__", data.name)
      .replace("__number__", data.number);
    console.log(data);
    $(this.el).html(html);
  }
};

var controller = {
  init({ view, model }) {
    this.view = view;
    this.model = model;
    this.view.render(this.model.data);
    this.bindEvents();
    console.log(1);
    this.fetchModel();
    console.log(2);
  },
  events: [
    { type: "click", selector: "#increaseByOne", fnName: "add" },
    { type: "click", selector: "#reset", fnName: "reset" }
  ],
  bindEvents() {
    this.events.map(event => {
      $(this.view.el).on(
        event.type,
        event.selector,
        this[event.fnName].bind(this)
      );
    });
  },
  add() {
    let newData = { number: this.model.data.number + 1 };
    this.updateModel(newData);
  },

  fetchModel() {
    this.model.fetch(1).then(() => {
      this.view.render(this.model.data);
    });
  },
  updateModel(newData) {
    this.model.update(newData).then(() => {
      this.view.render(this.model.data);
    });
  }
};
controller.init({ view, model });

嘿嘿嘿初步的 MVC 就实现了,这真是个好套路,没错都是套路
BUT
如果你有好多页面,那么每个页面就都有各自的 MVC,有重复就能优化
所以,我们用面向对象的方式来改进一下,把共有的属性放到原型里
(也就是把重复的代码放到一个类里)

面向对象

class Model  {
  constructor({resource,data}){
    this.resource = resource
    this.data = data
  },
  fetch(id) {
    return axios.get(`/${resource}s/${id}`).then((response)=>{
      this.data = response.data
    })
  },
  update(newData) {
    let id = this.data.id
    return axios.put(`/${resource}s/${id}`, newData).then(({data})=>{
      this.data = data
    })
  }
}
// <!-- 这样就可以new很多不同的model,他们的共有属性都在Model类里 -->
let model = new Model({
  resource:'book',
  data:{
    number:0,
    name:''
  }
})

class View {
  constructor({template,el}){
    this.template = template
    this.el = el
    this.$el = $(this.el)
  }
  render(data) {
    let html = this.template
    for(let key in data){
      html = html.replace(__`${key}`__,data[key])
      this.$el.html(html)
    }
  }
}
// <!-- 现在可以new view了 -->
let view = new View({
  template: `
<div>
  书名:《__name__》,
  数量:__number__
</div>
<div class="actions">
  <button id="increaseByOne">加1</button>
  <button id="reset">归零</button>
</div>
  `,
  el: '#app',
})

class Controller{
  constructor({view,model,events,...rest}){
    this.view = view
    this.model = model
    this.events = events
    Object.assign(this,rest)
    this.bindEvents()
    this.view.render(this.model.data)
    init.apply(this,arguments)
  }
  bindEvents() {
    this.events.map((e) => {
      this.view.$el.on(e.type, e.el, this[e.fn].bind(this))
    })
  }
}
var controller = new Controller({
  view: view,
  model: model,
  events: [
    { type: 'click', el: '#increaseByOne', fn: 'add' },
    { type: 'click', el: '#reset', fn: 'reset' }
  ],
  // 这里可以优化1
  init(options) {
    this.model.fetch(1)
      .then(() => {
        this.view.render(this.model.data)
      })
  },
  add() {
    let newData = {number: this.model.data.number + 1}
    this.updateModel(newData)
  },
  reset() {
    this.updateModel({number: 0})
  },
  updateModel(newData) {
    // 1
    this.model.update(newData).then(()=>{
      this.view.render(this.model.data)
    })
  }
// <!-- controller相对复杂,好好看看 -->

每次更新数据都要手动 render,这样不好…
怎么优化呢?引入发布订阅模式
通过事件机制,每次数据更新,就自动触发一个事件,
这个事件会自动渲染 data,看代码

发布订阅

class EventHub {
  constructor() {
    this.events = {};
  }
  on(eventName, fn) {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(fn);
  }
  emit(eventName, params) {
    let fnList = this.events[eventName];
    fnList.forEach(fn => {
      fn.apply(null, params);
    });
  }
  off(eventName, fn) {
    let fnList = this.events[eventName];
    for (let i = 0; i < fnList.length; i++) {
      if (fnList[i] === fn) {
        delete fnList[i];
        break;
      }
    }
  }
}

好的下面介绍一下这个事件中心

有一个 events 负责存储事件名和事件对应的函数
方法 on 就负责判断你穿进来的事件在不在 events 里
如果不在,就把这个事件对应一个空数组
然后把传进来的函数 push 到这个事件名对应的数组里
就像这样 events = {eventName:[fn1,fn2]}
那么 emit 是什么呢?就是触发一个事件,也就是调用这个事件对应的函数
首先第一个参数是事件名,它对应一个数组,这个数组里可能有多个函数
所以,我们要遍历这个数组,然后分别调用每一个数组里函数,this 用 apply 绑定成
null 就行
还可能有一个 off,和 emit 差不多,遍历数组 delete 每个函数
有了事件中心,我们就可以让 model 继承这个 EventHub(别忘了写 super())
这样 model 就具有了 on,emit 等这些属性,
然后再 controller 里绑定事件 changed,

this.model.on("changed", () => {
  this.view.render(this.model.data);
});

更新数据之后,this.emit(‘changed’)

详细代码请戳这里

基本没问题了,但是 render 函数还不太完美,
如果有个输入框,你每次 render 之后,输入框了的数据就没有了,是的没有了
那么怎么解决呢

把输入的数据记录在 js 的 data 里(数据绑定的思想),这个数据并不是存在数据库里的,
它属于 UI 层面的数据.
哪里变动改哪里,就像哪里不会点哪里一样.不要粗暴的操作 innerHTML(虚拟 DOM 的思想)
为了解决类似这样的 MVC 的 bug,各种框架应运而生
Angular 就是基于第一种数据绑定的思想
React 就是基于第二种虚拟 DOM 的思想

前端花了很长的时间普及内容和行为分离的观点.但是这些框架实在是太方便了…..
了解的 MVVM 我们就可以尝试把代码改写成 vue 的代码

vue

渐进式(从 MVC 过渡到 vue 比较自然)
双向绑定了解一下
单向数据流了解一下
看文档,文档,档

React

断崖式(学习曲线比较陡峭)
jsx 了解一下
单向绑定了解一下
redux 了解一下
虚拟 DOM 了解一下
state->render->input->触发 onchange->setState
应该大体就是这个意思

双向绑定 Vs 单向绑定

双向绑定实现方式:

赃值检查
数据劫持和发布订阅
Object.defineProperty()无法监听一开始不存在的数据
Proxy 代理(vue 之后会用 Proxy 改写)


文章作者: 沐雪
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 沐雪 !
评论
 上一篇
虚拟DOM 虚拟DOM
虚拟 DOM虚拟 DOM 实际上就是一个 对象,使用它来减少对真实 DOM 的操作次数。对比之后选择更新那些 dom,而不是全部删除再重建 实现一个简单的虚拟 DOMclass vNode { constructor() {
2018-08-18
下一篇 
前端路由 前端路由
前端路由简单的说,路由是根据不同的 url 地址展示不同的内容或组件前端路由更多用在单页应用上, 也就是 SPA, 因为单页应用, 基本上都是前后端分离的, 后端自然也就不会给前端提供路由。 主要方案在 html5 的 history
2018-08-06
  目录