vue數據綁定原理


一、定義

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的值
 */

 

三、結論


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM