談Vue.js `數據綁定` 的資料從何而來

本文撰寫時的Vue.js 版本為2.1.8

Vue.js 使用HTML-based 的模板語法,允許開發者將Dom 綁定至底層的Vue instance,在讓Vue.js 再透過其本身的機制渲染網頁

而Vue.js 數據綁定的方式有兩種,一種是修改data 屬性的方法,而另一種是實作 computed function ,先來介紹一下這兩種使用方法

  1. using data property

    1
    2
    3
    4
    5
    6
    7
    let vm = new Vue({
    data: {
    return 'message': 'hello';
    }
    });
    console.log(vm.message)
    // output: hello
  2. Using computed function

    1
    2
    3
    4
    5
    6
    7
    8
    9
    let vm = new Vue({
    computed: {
    message() {
    return 'hello';
    }
    }
    });
    console.log(vm.message);
    // output: hello

上方這兩種方法都會輸出 “hello”,你也可以把hello輸出在dom 內:

1
<p>{{ message }}</p>

但是vue.js 怎麼知道要到 data 或者 computed 屬性內尋找相對應的 key 或者 function。
其實data 的key 與 function 同時只能存在一個,如果我們兩個都同時宣告的話:

1
2
3
4
5
6
7
8
9
10
new Vue({
data: {
return 'message': 'hello';
},
computed: {
message() {
return 'hello';
}
}
});

重啟網頁時會看到以下警告:

1
2
vue.js:525 [Vue warn]: existing instance property "message" will be overwritten by a computed property with the same name. 
(found in root instance)

回到原始碼內,Vue.js 的初始化階段在3407 行附近:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Vue$3 (options) {
if ("development" !== 'production' &&
!(this instanceof Vue$3)) {
warn('Vue is a constructor and should be called with the `new` keyword');
}
this._init(options);
}

initMixin(Vue$3);
stateMixin(Vue$3);
eventsMixin(Vue$3);
lifecycleMixin(Vue$3);
renderMixin(Vue$3);

上方的五個函數代表了Vue.js 的執行週期


vue.js lifecycle

而我們關心的資料初始化階段則是在 beforeCreatecreated 之:

1
2
3
4
5
6
7
8
9
10
11
12
function initMixin (Vue) {
Vue.prototype._init = function (options) {
var vm = this;
// 略
initLifecycle(vm);
initEvents(vm);
callHook(vm, 'beforeCreate');
initState(vm);
callHook(vm, 'created');
initRender(vm);
};
}

因此我們直接來看 initState 的函式,行數不多就全部貼上來了:

1
2
3
4
5
6
7
8
9
10
11
12
13
function initState (vm) {
vm._watchers = [];
var opts = vm.$options;
if (opts.props) { initProps(vm, opts.props); }
if (opts.methods) { initMethods(vm, opts.methods); }
if (opts.data) {
initData(vm);
} else {
observe(vm._data = {}, true /* asRootData */);
}
if (opts.computed) { initComputed(vm, opts.computed); }
if (opts.watch) { initWatch(vm, opts.watch); }
}

這邊可以看到檢查property 的階段為
props -> methods -> data -> computed -> watch
比照我們剛剛收到的warning,property "message" will be overwritten by a computed property ,data初始化階段比computed 還早,當然會被改寫。

InitData

接下來我們來看看 初始化data 的方式怎麼實作的吧:

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
function initData (vm) {

var data = vm.$options.data;
data = vm._data = typeof data === 'function'
? data.call(vm)
: data || {};
if (!isPlainObject(data)) {
data = {};
"development" !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
);
}
// proxy data on instance
var keys = Object.keys(data);
var props = vm.$options.props;
var i = keys.length;
while (i--) {
if (props && hasOwn(props, keys[i])) {
"development" !== 'production' && warn(
"The data property \"" + (keys[i]) + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
);
} else {
proxy(vm, keys[i]);
}
}
// observe data
observe(data, true /* asRootData */);
}

一開始先檢查data 的格式是function 或者 object,會這樣檢查的原因是因為在data 內有兩種實作方式:

  1. implement with object

    1
    2
    3
    data: {
    message: 'hello'
    }
  2. implement with function

    1
    2
    3
    4
    5
    data() {
    return {
    message: 'hello'
    }
    }

資料格式無誤後在透過while 迴圈取得key value pair,這時還需檢查一次該key 是否為所設定的props,也就是說你不能寫成這樣的格式:

1
2
3
4
5
6
new Vue({
props: ['message'],
data: {
message: 'hello'
}
})

兩次檢查都無誤後便可以透過 proxy(vm, keys[i]); 直接賦值了。

關於proxy 內的 defineProperty用法可以參考網路上的文章,這邊不多加說明。

InitComputed

與 initData 相比之下,initComputed 的處理流程簡單許多

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
function initComputed (vm, computed) {
for (var key in computed) {
/* istanbul ignore if */
if ("development" !== 'production' && key in vm) {
warn(
"existing instance property \"" + key + "\" will be " +
"overwritten by a computed property with the same name.",
vm
);
}
var userDef = computed[key];
if (typeof userDef === 'function') {
computedSharedDefinition.get = makeComputedGetter(userDef, vm);
computedSharedDefinition.set = noop;
} else {
computedSharedDefinition.get = userDef.get
? userDef.cache !== false
? makeComputedGetter(userDef.get, vm)
: bind$1(userDef.get, vm)
: noop;
computedSharedDefinition.set = userDef.set
? bind$1(userDef.set, vm)
: noop;
}
Object.defineProperty(vm, key, computedSharedDefinition);
}
}

首先檢查function 內所有的函數名稱是否與目前在data 內註冊的名稱衝突,如果衝突就取代,這也是我們上方看到會警告overwritten 的原因了。

檢查完便可以把function name 註冊到vm instance內了,Vue.js 提供兩種實作方式

  1. default getter

    1
    2
    3
    4
    5
    6
    7
    new Vue({
    computed: {
    message: function() {
    return 'hello';
    }
    }
    })
  2. setter and getter

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    new Vue({
    data: {
    showMessage: ''
    },
    computed: {
    message: {
    get: function () {
    return showMessage;
    },
    set: function(value) {
    this.showMessage = value;
    }
    }
    }
    })

Vue.js 在computed function 預設是只做getter,但也允許你另外實作setter
以上是Vue.js 透過instance 取得data 的流程,希望對大家有幫助。

參考資料:
Vue.js官網