Posts 双向绑定与单向绑定
Post
Cancel

双向绑定与单向绑定

说到前端框架,就总会谈论到什么「双向绑定」和「单向绑定」这些概念了。

MVC

页面上的代码总是可以分成三类:

  • 专门操作远程数据的代码(fetchDb 和 saveDb 等等)
  • 专门呈现页面元素的代码(innerHTML 等等)
  • 其他控制逻辑的代码(点击某按钮之后做啥的代码)

思想

  • M 专门负责数据
  • V 专门负责表现
  • C 负责其他逻辑

使用 MVC 模式改造意大利面条代码:

  • 把意大利面条变成三块有结构有组织的对象:model、view 和 controller
  • model 只负责存储数据、请求数据、更新数据
  • view 只负责渲染 HTML(可接受一个 data 来定制数据)
  • controller 负责调度 model 和 view

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
axios.interceptors.response.use(function (response) {
  console.log(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;
});

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="decreaseByOne">减1</button>
      <button id="square">平方</button>
      <button id="cube">立方</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: "#decreaseByOne",
      fnName: "minus",
    },
    {
      type: "click",
      selector: "#square",
      fnName: "square",
    },
    {
      type: "click",
      selector: "#cube",
      fnName: "cube",
    },
  ],
  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);
  },
  minus() {
    let newData = {
      number: this.model.data.number - 1,
    };
    this.updateModel(newData);
  },
  square() {
    let newData = {
      number: Math.pow(this.model.data.number, 2),
    };
    this.updateModel(newData);
  },
  cube() {
    let newData = {
      number: Math.pow(this.model.data.number, 3),
    };
    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 用类实现

你每次写一个 model 都要写很类似的代码 你每次写一个 view 都要写很类似的代码 你每次写一个 controller 都要写很类似的代码

为什么不利用模板代码(俗称面向对象)把重复的代码写到一个类呢(JS 里面就是把「共有属性」放到原型里)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
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;
});

class Model {
  constructor(options) {
    this.data = options.data || {};
    this.resource = options.resource;
    this.baseUrl = options.baseUrl || "/";
  }
  fetch(id) {
    return axios
      .get(this.baseUrl + this.resource + "s/" + id)
      .then(({ data }) => {
        this.data = data;
      });
  }
  create(data) {
    return axios
      .post(this.baseUrl + this.resource + "s", data)
      .then(({ data }) => {
        this.data = data;
      });
  }
  destroy() {
    let id = this.data.id;
    return axios.delete(this.baseUrl + this.resource + "s/" + id).then(() => {
      this.data = {};
    });
  }
  update(newData) {
    let id = this.data.id;
    return axios
      .put(this.baseUrl + this.resource + "s/" + id, newData)
      .then(({ data }) => {
        this.data = data;
      });
  }
}

var model = new Model({
  resource: "book",
  data: {
    id: null,
    number: 0,
    name: null,
  },
});

class View {
  constructor({ el, template }) {
    this.el = el;
    this.$el = $(el);
    this.template = template;
  }
  render(data) {
    let html = this.template;
    for (let key in data) {
      let value = data[key];
      html = html.replace(`__${key}__`, value);
    }
    this.$el.html(html);
  }
}

var view = new View({
  el: "#app",
  template: `
    <div>
      书名:《__name__》,
      数量:__number__
    </div>
    <div class="actions">
      <button id="increaseByOne">加1</button>
      <button id="decreaseByOne">减1</button>
      <button id="square">平方</button>
      <button id="cube">立方</button>
      <button id="reset">归零</button>
    </div>
  `,
});

class Controller {
  constructor({ view, model, events, init, ...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: "#decreaseByOne",
      fn: "minus",
    },
    {
      type: "click",
      el: "#square",
      fn: "square",
    },
    {
      type: "click",
      el: "#cube",
      fn: "cube",
    },
    {
      type: "click",
      el: "#reset",
      fn: "reset",
    },
  ],
  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);
  },
  minus() {
    // 注意这里有 bug
    this.model.data.number = this.model.data.number - 1;
    this.updateModel(this.model.data);
  },
  square() {
    let newData = {
      number: Math.pow(this.model.data.number, 2),
    };
    this.updateModel(newData);
  },
  cube() {
    let newData = {
      number: Math.pow(this.model.data.number, 3),
    };
    this.updateModel(newData);
  },
  reset() {
    this.updateModel({
      number: 0,
    });
  },
  updateModel(newData) {
    this.model.update(newData).then(() => {
      this.view.render(this.model.data);
    });
  },
});

MVC 添加事件机制

每次用 model 获取数据之后,还要「手动」调用 this.view.render(this.model.data),你看代码中有四处手动调用了 updateModel。

使用事件机制可以很好的解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
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;
});

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.map((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;
      }
    }
  }
}

class Model extends EventHub {
  constructor(options) {
    super();
    this.data = options.data || {};
    this.resource = options.resource;
    this.baseUrl = options.baseUrl || "/";
  }
  fetch(id) {
    return axios
      .get(this.baseUrl + this.resource + "s/" + id)
      .then(({ data }) => {
        this.data = data;
        this.emit("changed");
      });
  }
  create(data) {
    return axios
      .post(this.baseUrl + this.resource + "s", data)
      .then(({ data }) => {
        this.data = data;
        this.emit("changed");
      });
  }
  destroy() {
    let id = this.data.id;
    return axios.delete(this.baseUrl + this.resource + "s/" + id).then(() => {
      this.data = {};
      this.emit("changed");
    });
  }
  update(newData) {
    let id = this.data.id;
    return axios
      .put(this.baseUrl + this.resource + "s/" + id, newData)
      .then(({ data }) => {
        this.data = data;
        this.emit("changed");
      });
  }
}

var model = new Model({
  resource: "book",
  data: {
    id: null,
    number: 0,
    name: null,
  },
});

class View {
  constructor({ el, template }) {
    this.el = el;
    this.$el = $(el);
    this.template = template;
  }
  render(data) {
    let html = this.template;
    for (let key in data) {
      let value = data[key];
      html = html.replace(`__${key}__`, value);
    }
    this.$el.html(html);
  }
}

var view = new View({
  el: "#app",
  template: `
    <div>
      书名:《__name__》,
      数量:__number__
    </div>
    <div class="actions">
      <button id="increaseByOne">加1</button>
      <button id="decreaseByOne">减1</button>
      <button id="square">平方</button>
      <button id="cube">立方</button>
      <button id="reset">归零</button>
    </div>
  `,
});

class Controller {
  constructor({ view, model, events, init, ...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: "#decreaseByOne",
      fn: "minus",
    },
    {
      type: "click",
      el: "#square",
      fn: "square",
    },
    {
      type: "click",
      el: "#cube",
      fn: "cube",
    },
    {
      type: "click",
      el: "#reset",
      fn: "reset",
    },
  ],
  init(options) {
    this.model.on("changed", () => {
      this.view.render(this.model.data);
    });
    this.model.fetch(1).then(() => {
      this.view.render(this.model.data);
    });
  },
  add() {
    let newData = {
      number: this.model.data.number + 1,
    };
    this.updateModel(newData);
  },
  minus() {
    // 注意这里有 bug
    this.model.data.number = this.model.data.number - 1;
    this.updateModel(this.model.data);
  },
  square() {
    let newData = {
      number: Math.pow(this.model.data.number, 2),
    };
    this.updateModel(newData);
  },
  cube() {
    let newData = {
      number: Math.pow(this.model.data.number, 3),
    };
    this.updateModel(newData);
  },
  reset() {
    this.updateModel({
      number: 0,
    });
  },
  updateModel(newData) {
    this.model.update(newData);
  },
});

MVC 转向数据绑定

每次 render 都会更新 #app 的 innerHTML,这可能会丢失用户的写在页面某个 input 里面的数据。

数据绑定的初步思想

用户只要输入了什么,就记录在 JS 的 data 里。(Angular)

虚拟 DOM 的初步思想

不要粗暴的操作 innerHTML,而是只更新需要更新的部位。(React)

Vue 的双向绑定

Vue 的双向绑定(也是 Angular 的双向绑定)有这些功能:

  • 只要 JS 改变了 view.number 或 view.name 或 view.n (注意 Vue 把 data 里面的 number、name 和 n 放到了 view 上面,没有 view.data 这个东西), HTML 就会局部更新。
  • 只要用户在 input 里输入了值,JS 里的 view.n 就会更新。

这就像双向绑定:JS 数据与页面元素互相绑定。同时 Vue 也实现了局部更新

代替 View

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
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;
});

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.map((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;
      }
    }
  }
}

class Model extends EventHub {
  constructor(options) {
    super();
    this.data = options.data || {};
    this.resource = options.resource;
    this.baseUrl = options.baseUrl || "/";
  }
  fetch(id) {
    return axios
      .get(this.baseUrl + this.resource + "s/" + id)
      .then(({ data }) => {
        this.data = data;
        this.emit("changed");
      });
  }
  create(data) {
    return axios
      .post(this.baseUrl + this.resource + "s", data)
      .then(({ data }) => {
        this.data = data;
        this.emit("changed");
      });
  }
  destroy() {
    let id = this.data.id;
    return axios.delete(this.baseUrl + this.resource + "s/" + id).then(() => {
      this.data = {};
      this.emit("changed");
    });
  }
  update(newData) {
    let id = this.data.id;
    return axios
      .put(this.baseUrl + this.resource + "s/" + id, newData)
      .then(({ data }) => {
        this.data = data;
        this.emit("changed");
      });
  }
}

var model = new Model({
  resource: "book",
  data: {
    id: null,
    number: 0,
    name: null,
  },
});

var view = new Vue({
  el: "#app",
  data: {
    name: "未命名",
    number: 0,
    n: 1,
  },
  template: `
    <div>
      <div>
        书名:《》,
        数量:
      </div>
      <div><input v-model="n"></div>
      <div class="actions">
        <button id="increaseByOne">加N</button>
        <button id="decreaseByOne">减N</button>
        <button id="square">平方</button>
        <button id="cube">立方</button>
        <button id="reset">归零</button>
      </div>
    </div>
  `,
});

class Controller {
  constructor({ view, model, events, init, ...rest }) {
    this.view = view;
    this.model = model;
    this.events = events;
    Object.assign(this, rest);
    this.bindEvents();
    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: "#decreaseByOne",
      fn: "minus",
    },
    {
      type: "click",
      el: "#square",
      fn: "square",
    },
    {
      type: "click",
      el: "#cube",
      fn: "cube",
    },
    {
      type: "click",
      el: "#reset",
      fn: "reset",
    },
  ],
  init(options) {
    this.model.on("changed", () => {
      console.log("c");
      this.view.name = this.model.data.name;
      this.view.number = this.model.data.number;
    });
    this.model.fetch(1);
  },
  add() {
    let newData = {
      number: this.model.data.number + (this.view.n - 0),
    };
    this.updateModel(newData);
  },
  minus() {
    // 注意这里有 bug
    this.model.data.number = this.model.data.number - this.view.n;
    this.updateModel(this.model.data);
  },
  square() {
    let newData = {
      number: Math.pow(this.model.data.number, 2),
    };
    this.updateModel(newData);
  },
  cube() {
    let newData = {
      number: Math.pow(this.model.data.number, 3),
    };
    this.updateModel(newData);
  },
  reset() {
    this.updateModel({
      number: 0,
    });
  },
  updateModel(newData) {
    this.model.update(newData);
  },
});

代替 Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
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;
});

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.map((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;
      }
    }
  }
}

class Model extends EventHub {
  constructor(options) {
    super();
    this.data = options.data || {};
    this.resource = options.resource;
    this.baseUrl = options.baseUrl || "/";
  }
  fetch(id) {
    return axios
      .get(this.baseUrl + this.resource + "s/" + id)
      .then(({ data }) => {
        this.data = data;
        this.emit("changed");
      });
  }
  create(data) {
    return axios
      .post(this.baseUrl + this.resource + "s", data)
      .then(({ data }) => {
        this.data = data;
        this.emit("changed");
      });
  }
  destroy() {
    let id = this.data.id;
    return axios.delete(this.baseUrl + this.resource + "s/" + id).then(() => {
      this.data = {};
      this.emit("changed");
    });
  }
  update(newData) {
    let id = this.data.id;
    return axios
      .put(this.baseUrl + this.resource + "s/" + id, newData)
      .then(({ data }) => {
        this.data = data;
        this.emit("changed");
      });
  }
}

var model = new Model({
  resource: "book",
  data: {
    id: null,
    number: 0,
    name: null,
  },
});

var view = new Vue({
  el: "#app",
  data: {
    name: "未命名",
    number: 0,
    n: 1,
  },
  template: `
    <div>
      <div>
        书名:《》,
        数量:
      </div>
      <div><input v-model="n"></div>
      <div class="actions">
        <button v-on:click="add">加N</button>
        <button v-on:click="minus">减N</button>
        <button v-on:click="square">平方</button>
        <button v-on:click="cube">立方</button>
        <button v-on:click="reset">归零</button>
      </div>
    </div>
  `,
  created() {},
  methods: {
    add() {
      let newData = {
        number: this.number + (this.n - 0),
      };
      this.updateModel(newData);
    },
    minus() {
      let newData = {
        number: this.number - (this.n - 0),
      };
      this.updateModel(newData);
    },
    square() {
      let newData = {
        number: Math.pow(this.number, 2),
      };
      this.updateModel(newData);
    },
    cube() {
      let newData = {
        number: Math.pow(this.number, 3),
      };
      this.updateModel(newData);
    },
    reset() {
      this.updateModel({
        number: 0,
      });
    },
    updateModel(newData) {
      model.update(newData);
    },
  },
});

model.on("changed", () => {
  console.log("c");
  view.name = model.data.name;
  view.number = model.data.number;
});
model.fetch(1);

代替 Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
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);
  }
  console.log("response");
  console.log(response);
  return response;
});

class Model {
  constructor(options) {
    this.resource = options.resource;
    this.baseUrl = options.baseUrl || "/";
  }
  fetch(id) {
    return axios.get(this.baseUrl + this.resource + "s/" + id);
  }
  create(data) {
    return axios.post(this.baseUrl + this.resource + "s", data);
  }
  destroy(id) {
    return axios.delete(this.baseUrl + this.resource + "s/" + id);
  }
  update(id, newData) {
    return axios.put(this.baseUrl + this.resource + "s/" + id, newData);
  }
}

var model = new Model({
  resource: "book",
});

var view = new Vue({
  el: "#app",
  data: {
    book: {
      id: null,
      name: "未命名",
      number: 0,
    },
    n: 1,
  },
  template: `
    <div>
      <div>
        书名:《》,
        数量:
      </div>
      <div><input v-model="n"></div>
      <div class="actions">
        <button v-on:click="add">加N</button>
        <button v-on:click="minus">减N</button>
        <button v-on:click="square">平方</button>
        <button v-on:click="cube">立方</button>
        <button v-on:click="reset">归零</button>
      </div>
    </div>
  `,
  created() {
    model.fetch(1).then(({ data }) => {
      view.book = data;
    });
  },
  methods: {
    add() {
      let newData = {
        number: this.book.number + (this.n - 0),
      };
      this.updateModel(newData);
    },
    minus() {
      let newData = {
        number: this.book.number - (this.n - 0),
      };
      this.updateModel(newData);
    },
    square() {
      let newData = {
        number: Math.pow(this.book.number, 2),
      };
      this.updateModel(newData);
    },
    cube() {
      let newData = {
        number: Math.pow(this.book.number, 3),
      };
      this.updateModel(newData);
    },
    reset() {
      this.updateModel({
        number: 0,
      });
    },
    updateModel(newData) {
      model.update(this.book.id, newData).then(({ data }) => {
        console.log(data);
        this.book = data;
      });
    },
  },
});

React 的单向绑定

  • 单向-半自动的双向绑定
  • 双向-全自动的双向绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
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;
});

class Model {
  constructor(options) {
    this.resource = options.resource;
    this.baseUrl = options.baseUrl || "/";
  }
  fetch(id) {
    return axios.get(this.baseUrl + this.resource + "s/" + id);
  }
  create(data) {
    return axios.post(this.baseUrl + this.resource + "s", data);
  }
  destroy(id) {
    return axios.delete(this.baseUrl + this.resource + "s/" + id);
  }
  update(id, newData) {
    return axios.put(this.baseUrl + this.resource + "s/" + id, newData);
  }
}

var model = new Model({
  resource: "book",
});

class BookCard extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      book: { id: null, name: "", number: 0 },
      n: 1,
    };
  }
  componentDidMount() {
    model.fetch(1).then((response) => {
      this.setState({
        book: response.data,
      });
    });
  }
  render() {
    return (
      <div>
        <div>
          书名:《{this.state.book.name}》, 数量:{this.state.book.number}
        </div>
        <div>
          <input value={this.state.n} onChange={this.changeN} />
        </div>
        <div className="actions">
          <button onClick={this.add.bind(this)}>加N</button>
          <button onClick={this.minus.bind(this)}>减N</button>
          <button onClick={this.square.bind(this)}>平方</button>
          <button onClick={this.cube.bind(this)}>立方</button>
          <button onClick={this.reset.bind(this)}>归零</button>
        </div>
      </div>
    );
  }
  changeN(e) {
    console.log(e);
    this.setState({
      n: this.state.n,
    });
  }
  add() {
    const newData = {
      number: this.state.book.number + this.state.n,
    };
    model.update(this.state.book.id, newData).then((response) => {
      this.setState({ book: response.data });
    });
  }
  minus() {
    const newData = {
      number: this.state.book.number - this.state.n,
    };
    model.update(this.state.book.id, newData).then((response) => {
      this.setState({ book: response.data });
    });
  }
  square() {
    const newData = {
      number: Math.pow(this.state.book.number, 2),
    };
    model.update(this.state.book.id, newData).then((response) => {
      this.setState({ book: response.data });
    });
  }
  cube() {
    const newData = {
      number: Math.pow(this.state.book.number, 3),
    };
    model.update(this.state.book.id, newData).then((response) => {
      this.setState({ book: response.data });
    });
  }
  reset() {
    const newData = {
      number: 0,
    };
    model.update(this.state.book.id, newData).then((response) => {
      this.setState({ book: response.data });
    });
  }
}

var view = <BookCard />;

ReactDOM.render(view, document.getElementById("app"));

双向绑定的要点

Dirty Checking

AngularJS 1.x 的遭人嘲笑的方式,所有可以更改数据的地方,都重写了方法,目的是为了执行完 $http 等方法后调用一个 render 方法,你自己悄悄改了数据它是完全不知道的。

Reactive

getter、setter

可以控制属性的设置与获取,缺点:无法监听不存在的属性、或者新增的属性。

Proxy

Vue3.0 计划用 Proxy 重写,可以解决 getter、setter 的缺点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class X {
  constructor(options) {
    const data = options.data;

    return new Proxy(data, {
      get(target, name) {
        return name in target ? target[name] : undefined;
      },
      set(target, name, value) {
        if (name === "name") {
          console.log("有人修改了 name");
        }
      },
    });
  }
}

var view = new X({
  data: {
    name: "chenng",
  },
});

console.log(view.name === "chenng"); // 输出 true
view.name = "jack"; // 有人修改了 name
This post is licensed under CC BY 4.0 by the author.
Trending Tags
Contents

Trending Tags