一、定義
vue的數據雙向綁定是基於Object.defineProperty方法,通過定義data屬性的get和set函數來監聽數據對象的變化,一旦變化,vue利用發布訂閱模式,通知訂閱者執行回調函數,更新dom。
二、實現
vue關於數據綁定的生命周期是: 利用options的data屬性初始化vue實力data---》遞歸的為data中的屬性值添加observer--》編譯html模板--》為每一個{{***}}添加一個watcher;
var app = new Vue({
data:{
message: 'hello world',
age: 1,
name: {
firstname: 'mike',
lastname: 'tom'
}
}
});
1.初始化data屬性
this.$data = options.data || {};
這個步驟比較簡單將data屬性掛在到vue實例上即可。
2.遞歸的為data中的屬性值添加observer,並且添加對應的回調函數(initbinding)
function Observer(value, type) {
this.value = value;
this.id = ++uid;
Object.defineProperty(value, '$observer', {
value: this,
enumerable: false,
writable: true,
configurable: true
});
this.walk(value); // dfs為每個屬性添加ob
}
Observer.prototype.walk = function (obj) {
let val;
for (let key in obj) {
if (!obj.hasOwnProperty(key)) return;
val = obj[key];
// 遞歸this.convert(key, val);
}
};
Observer.prototype.convert = function (key, val) {
let ob = this;
Object.defineProperty(this.value, key, {
enumerable: true,
configurable: true,
get: function () {
if (Observer.emitGet) {
ob.notify('get', key);
}
return val;
},
set: function (newVal) {
if (newVal === val) return;
val = newVal;
ob.notify('set', key, newVal);//這里是關鍵
}
});
};
上面代碼中,set函數中的notify是關鍵,當用戶代碼修改了data中的某一個屬性值比如app.$data.age = 2;,那么ob.notify就會通知observer來執行上面對應的回掉函數。
綁定回掉函數
exports._updateBindingAt = function (event, path) {
let pathAry = path.split('.');
let r = this._rootBinding;
for (let i = 0, l = pathAry.length; i < l; i++) {
let key = pathAry[i];
r = r[key];
if (!r) return;
}
let subs = r._subs;
subs.forEach((watcher) => {
watcher.cb(); // 這里執行watcher的回掉函數
});
};
/**
* 執行本實例所有子實例發生了數據變動的watcher
* @private
*/
exports._updateChildrenBindingAt = function () {
if (!this.$children.length) return;
this.$children.forEach((child) => {
if (child.$options.isComponent) return;
child._updateBindingAt(...arguments);
});
};
/**
* 就是在這里定於數據對象的變化的
* @private
*/
exports._initBindings = function () {
this._rootBinding = new Binding();
this.observer.on('set', this._updateBindingAt.bind(this))
};
3.編譯模板
這個是數據綁定的關鍵步驟,具體可以分為一下2個步驟。
A)解析htmlElement節點,這里要dfs所有的dom和上面對應的指令(v-if,v-modal)之類的
B)解析文本節點,把文本節點中的{{***}}解析出來,通過創建textNode的方法來解析為真正的HTML文件
在解析的過程中,會對指令和模板添加Directive對象和Watcher對象,當data對象的屬性值發生變化的時候,調用watcher的update方法,update方法中保存的是Directive對象更新dom方法,把在當directive對應的textNode的nodeValue變成新的data中的值。比如執行app.$data.age = 1;
首先編譯模板
exports._compile = function () {
this._compileNode(this.$el);
};
/**
* 渲染節點
* @param node {Element}
* @private
*/
exports._compileElement = function (node) {
if (node.hasChildNodes()) {
Array.from(node.childNodes).forEach(this._compileNode, this);
}
};
/**
* 渲染文本節點
* @param node {Element}
* @private
*/
exports._compileTextNode = function (node) {
let tokens = textParser.parse(node.nodeValue); // [{value:'姓名'}, {value: 'name‘,tag: true}]
if (!tokens) return;
tokens.forEach((token) => {
if (token.tag) {
// 指令節點
let value = token.value;
let el = document.createTextNode('');
_.before(el, node);
this._bindDirective('text', value, el);
} else {
// 普通文本節點
let el = document.createTextNode(token.value);
_.before(el, node);
}
});
_.remove(node);
};
exports._compileNode = function (node) {
switch (node.nodeType) {
// text
case 1:
this._compileElement(node);
break;
// node
case 3 :
this._compileTextNode(node);
break;
default:
return;
}
};
上面代碼中在編譯textNode的時候會執行bindDirctive方法,該方法的作用就是綁定指令,{{***}}其實也是一條指令,只不過是一個特殊的text指令,他會在本ob對象的directives屬性上push一個Directive對象。Directive對象本身在構造的時候,在構造函數中會實例化Watcher對象,並且執行directive的update方法(該方法就是把當前directive對應的dom更新),那么編譯完成后就是對應的html文件了。
/**
* 生成指令
* @param name {string} 'text' 代表是文本節點
* @param value {string} 例如: user.name 是表示式
* @param node {Element} 指令對應的el
* @private
*/
exports._bindDirective = function (name, value, node) {
let descriptors = dirParser.parse(value);
let dirs = this._directives;
descriptors.forEach((descriptor) => {
dirs.push(
new Directive(name, node, this, descriptor)
);
});
};
function Directive(name, el, vm, descriptor) {
this.name = name;
this.el = el; // 對應的dom節點
this.vm = vm;
this.expression = descriptor.expression;
this.arg = descriptor.arg;this._bind();
}
/**
* @private
*/
Directive.prototype._bind = function () {
if (!this.expression) return;
this.bind && this.bind();
// 非組件指令走這邊
this._watcher = new Watcher(
// 這里上下文非常關鍵
// 如果是普通的非組件指令, 上下文是vm本身
// 但是如果是prop指令, 那么上下文應該是該組件的父實例
(this.name === 'prop' ? this.vm.$parent : this.vm),
this.expression,
this._update, // 回調函數,目前是唯一的,就是更新DOM
this // 上下文
);
this.update(this._watcher.value);
};
exports.bind = function () {
};
/**
* 這個就是textNode對應的更新函數啦
*/
exports.update = function (value) {
this.el['nodeValue'] = value;
console.log("更新了", value);
};
但是,用戶代碼修改了data怎么辦,下面是watcher的相關代碼,watcher來幫你解決這個問題。
/**
* Watcher構造函數
* 有什么用呢這個東西?兩個用途
* 1. 當指令對應的數據發生改變的時候, 執行更新DOM的update函數
* 2. 當$watch API對應的數據發生改變的時候, 執行你自己定義的回調函數
* @param vm
* @param expression {String} 表達式, 例如: "user.name"
* @param cb {Function} 當對應的數據更新的時候執行的回調函數
* @param ctx {Object} 回調函數執行上下文
* @constructor
*/
function Watcher(vm, expression, cb, ctx) {
this.id = ++uid;
this.vm = vm;
this.expression = expression;
this.cb = cb;
this.ctx = ctx || vm;
this.deps = Object.create(null);//deps是指那些嵌套的對象屬性,比如name.frist 那么該watcher實例的deps就有2個屬性name和name.first屬性
this.initDeps(expression);
}
/**
* @param path {String} 指令表達式對應的路徑, 例如: "user.name"
*/
Watcher.prototype.initDeps = function (path) {
this.addDep(path);
this.value = this.get();
};
/**
根據給出的路徑, 去獲取Binding對象。
* 如果該Binding對象不存在,則創建它。
* 然后把當前的watcher對象添加到binding對象上,binding對象的結構和data對象是一致的,根節點但是rootBinding,所以根據path可以找到對應的binding對象
* @param path {string} 指令表達式對應的路徑, 例如"user.name"
*/
Watcher.prototype.addDep = function (path) {
let vm = this.vm;
let deps = this.deps;
if (deps[path]) return;
deps[path] = true;
let binding = vm._getBindingAt(path) || vm._createBindingAt(path);
binding._addSub(this);
};
初始化所有的綁定關系之后,就是wather的update了
/** * 當數據發生更新的時候, 就是觸發notify * 然后冒泡到頂層的時候, 就是觸發updateBindingAt * 對應的binding包含的watcher的update方法就會被觸發。 * 就是執行watcher的cb回調。watch在 * 兩種情況, 如果是$watch調用的話,那么是你自己定義的回調函數,開始的時候initBinding已經添加了回調函數 * 如果是directive,那么就是directive的_update方法 * 其實就是各自對應的更新方法。比如對應文本節點來說, 就是更新nodeValue的值 */
三、結論


