# 響應原理

response concept

這個 Render Function 會在記憶體生成 Virtual DOM 來模擬網頁上實際的 DOM 節點, 然後透過 patch 來比對 Virtual DOM 前後的差異,最後才將更新後的節點重新渲染至畫面上。

要注意的是,DOM 的更新動作在 Vue.js 裡是 非同步 執行的,當 setter 偵測到狀態被更新時, 就會啟動一個排隊的隊伍 (Queue),並且對同一個事件循環 (Event Loop) 內發生的所有變更進行緩衝,

這樣做的好處,是若同一個 watch 在短時間內被多次觸發,它只會被送進等待隊伍一次,可以省去多餘重複的計算次數, 直到下一個事件循環 (Vue 官方稱 tick) 才會刷新重整在等待隊伍內的任務,更新並且同步 Vue 實體內的 DOM。

# 資料驅動

/**
 * 判斷目標變數是否為物件
 * @param {*} target 目標變數
 */
const isObject = function(target) {
  return target && typeof target === 'object';
};

/**
 * 資料和事件觀察者中的依賴橋梁
 */
const Dependency = function() {
  // 一個屬性會配置一個依賴
  // 當屬性質改變時,透過依賴,去通知事件觀察者做動作
  this.renders = new Set();
};

/**
 * 註冊屬性的事件觀察者
 */
Dependency.prototype.add = function() {
  console.log('add');
  // 為每一個屬性的註冊觀察者
  if (Dependency.render) this.renders.add(Dependency.render);
};

/**
 * 通知屬性事件觀察者
 */
Dependency.prototype.notify = function() {
  console.log('notify');
  console.log(this.renders);
  this.renders.forEach((render) => {
    // 執行 Render 的 prototype function:call
    // 特意使用 call 作為函式名,讓 Render 和 Watcher 原型都能統一呼叫
    render.call();
  });
};

Dependency.render = null;

/**
 * 事件的觀察者
 * @param {Function} callback 回呼函式
 */
const Render = function(callback) {
  this.callback = callback;
  // 預先做一次
  this.call();
};

/**
 * 事件觀察者執行欲做函式
 */
Render.prototype.call = function() {
  console.log('render');
  // 將事件觀察者暫存至依賴的觀察者
  Dependency.render = this;
  this.callback();
};

/**
 * 將屬性增加 getter 和 setter
 * 使用閉包概念,將 value 變數保存,不論 getter 或 setter 都會去引用它
 * @param {String} key 屬性
 * @param {Object} target 目標變數
 */
const setDefinedProperty = function(key, target) {
  let value = target[key];
  const dep = new Dependency();
  Object.defineProperty(target, key, {
    get() {
      console.log('get:' + value);
      dep.add();
      return value;
    },
    set(val) {
      console.log('set:' + val);
      value = val;
      dep.notify();
    },
  });
  if (isObject(value)) {
    observe(value);
  }
};

/**
 * 將指定物件作轉為 object observe
 * @param {*} target 目標變數
 */
const observe = function(target) {
  if (isObject(target)) {
    Object.keys(target).forEach((key) => {
      setDefinedProperty(key, target);
    });
  }
  return target;
};

const person = {
  first: 'John',
  last: 'Doe',
  age: 18,
};
const renderPersonData = observe(person);

// 新增事件觀察者
new Render(() => {
  document.querySelector(
    'h1'
  ).innerText = `My name is ${renderPersonData.first} ${renderPersonData.last}, and I'm ${renderPersonData.age} years old`;
});

new Render(() => {
  document.querySelector(
    'h4'
  ).innerText = `The count is ${renderPersonData.count}`;
});
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
<h1></h1>
<button onclick="renderPersonData.first = 'test'">change name</button>
<button onclick="renderPersonData.age = '20'">change age</button>
<h4></h4>
<button onclick="renderPersonData.count--">-</button>
<button onclick="renderPersonData.count++">+</button>
1
2
3
4
5
6

# 資料計算

/**
 * 判斷目標變數是否為陣列
 * @param {*} target 目標變數
 */
const isFunction = function(target) {
  return target && typeof target === 'function';
};

/**
 * 將目標變數轉為計算變數
 * @param {*} target 目標變數
 */
const computed = function(target) {
  if (isObject(target)) {
    Object.keys(target).forEach((key) => {
      const value = target[key];
      if (isFunction(value)) {
        Object.defineProperty(target, key, {
          get() {
            console.log('get computed value:', value());
            return value();
          },
        });
      } else if (isObject(value)) {
        const { get = undefined, set = undefined } = value;
        if (get) {
          Object.defineProperty(target, key, {
            get() {
              console.log('get computed value:', value);
              return get();
            },
            set(val) {
              console.log('set computed value:', val);
              set(val);
            },
          });
        }
      }
    });
  }
  return target;
};

const computedPersonData = {
  name() {
    return renderPersonData.first + ' ' + renderPersonData.last;
  },
  counter: {
    get() {
      return renderPersonData.count * 2;
    },
    set(val) {
      renderPersonData.count = val;
    },
  },
};

const computedData = computed(computedPersonData);

// 新增事件觀察者
new Render(() => {
  document.querySelector(
    'h1'
  ).innerText = `My name is ${computedData.name}, and I'm ${renderPersonData.age} years old`;
});

new Render(() => {
  document.querySelector(
    'h4'
  ).innerText = `The count is ${computedPersonData.counter}`;
});
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

# 資料觀測

/**
 * 觀察者的原型
 * @param {Object} data 想觀察的資料變數
 * @param {String} key 目標變數的屬性名
 * @param {Function} handler 欲執行的函式
 * @param {Boolean} immediate 是否立刻執行
 */
const Watcher = function(data, key, handler, immediate) {
  this.data = data;
  this.key = key;
  this.handler = handler;
  this.immediate = immediate;
  this.value = undefined;
  // 第一次設定時,就做執行
  this.update();
};

/**
 * 判斷觀察者是否需立刻做欲執行的函式
 */
Watcher.prototype.update = function() {
  console.log('watch call');
  // 讓 update() 或 call() 中取值時,放入 Dependency 的 renders
  Dependency.render = this;
  if (this.immediate) this.call();
  else this.value = this.data[this.key];
};

/**
 * 執行欲做的函式
 * 特意使用和 Render 原型一樣的呼叫函示名稱
 */
Watcher.prototype.call = function() {
  console.log('watch update');
  const oldValue = this.value;
  const newValue = this.data[this.key];
  this.value = newValue;
  this.handler(newValue, oldValue);
};

/**
 * 將目標變數轉為觀察者變數
 * @param {Object} target 目標變數
 * @param {Object} data 想觀察的資料變數
 */
const watch = function(target, data) {
  if (isObject(target)) {
    Object.keys(target).forEach((key) => {
      if (data.hasOwnProperty(key)) {
        let handler = function() {};
        let immediate = false;
        // 物件模式
        if (isObject(target[key])) {
          handler = target[key].handler;
          immediate = target[key].immediate || false;
        }
        // 函式模式
        else if (isFunction(target[key])) {
          handler = target[key];
        }
        new Watcher(data, key, handler, immediate);
      }
    });
  }
  return target;
};

const watchPersonData = {
  age(newVal, oldVal) {
    console.log(`newVal:${newVal},oldVal:${oldVal}`);
  },
  count: {
    handler(newVal, oldVal) {
      console.log(`newVal:${newVal},oldVal:${oldVal}`);
    },
    immediate: true,
  },
};

const watchData = watch(watchPersonData, renderPersonData);
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
Last Updated: 2/25/2021, 7:56:51 AM