一、定義
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的值 */