vue 數據雙向綁定實現

vue 數據雙向綁定實現

以前每件事都差很少,直到如今才發現差不少。

如今才發現理清一件事的原委是多麼快樂的一件事,咱們共同勉勵。javascript

紙上得來終覺淺,絕知此事要躬行html

懶得扯淡,直接正題vue

PS: 文章略長。java

本文分3個部分來介紹:node

  1. model -> view
  2. 編譯器
  3. view -> model

clipboard.png

model -> view

其基於 訂閱者-發佈者模式,簡單的講就是訂閱者訂閱數據,一旦訂閱的數據變動事後,更新綁定的view視圖。git

這裏有明確的分工,分別是監聽器、發佈器和訂閱器,這3者相互協做,各司其職。github

  • 監聽器:負責 建立 發佈器;發佈器 添加 訂閱器發佈器 通知 訂閱器
  • 發佈器:負責 添加 訂閱器;通知 訂閱器
  • 訂閱器:負責 更新視圖

目標效果

過2s數據更改,更新到視圖bash

監聽器

顧名思義,監聽器,監聽器,監聽的就是數據的變化app

建立 發佈器;發佈器 添加 訂閱器發佈器 通知 訂閱器

須要解決訂閱者的添加和發佈器通知訂閱器的時機dom

Object.difineProperty爲咱們提供了方便,其語法以下:

var obj = {};

Object.defineProperty(obj, 'a', {
    enumerable: true,
    configurable: true,
    value: 'a'
});

console.log(obj.a); // 輸出a

definePropery 除了能夠定義數值之外,還能夠定義 get 和 set 訪問器,以下:

var obj = {};
var value = 'a';

Object.defineProperty(obj, 'a', {
    enumerable: true,
    configurable: true,
    get: function () {
        console.log('獲取 key 爲 "a" 的值');
        return value;
    },
    set: function (val) {
        console.log('修改 key 爲 "a" 的值');
        value = val;
    }    
});

console.log(obj.a);
obj.a = 'b';
console.log(obj.a);

運行結果以下所示:

clipboard.png

數據的變化無非就是讀和寫,由此,咱們能夠得出 訂閱器的添加和發佈器通知訂閱器的時機,就是屬性值的獲取和重置。

具體代碼

function observe(data) {
    if (!data || typeof data !== 'object') {
        return ;
    }

    Object.keys(data).forEach((val, key) => {
        defineReactive(data, val, data[val]);
    })
}

function defineReactive(data, key, val) {
    observe(val);
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            console.log(`獲取參數${key},值爲${val}`);
            return val;
        },
        set: function (newValue) {
            console.log(`修改參數${key},變爲${val}`);
            val = newValue;
        }
    });
}

var obj = {
    type: 'object',
    data: {
        a: 'a'
    }
}

observe(obj);

發佈器

添加 訂閱器;通知 訂閱器
function Dep () {
    this.subs = [];
}

Dep.prototype.addSub = function(sub){
    this.subs.push(sub);
};

Dep.prototype.notify = function(){
    this.subs.forEach(function(sub, index) {
        sub.update();
    });
};

發佈器代碼寫好了,咱們再從新修改一下監聽器代碼。主要修改點爲:添加訂閱器和發佈器通知訂閱器

function defineReactive(data, key, val) {
+    var dep = new Dep();
     observe(val);
     Object.defineProperty(data, key, {
         enumerable: true,
         configurable: true,
         get: function () {
             console.log(`獲取參數${key},值爲${val}`);
+            dep.addSub(<watch>);
             return val;
         },
         set: function (newValue) {
+            dep.notify();
             console.log(`修改參數${key},變爲${val}`);
             val = newValue;
         }

訂閱器

更新視圖
function Watcher (vm, key, cb) {
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    this.value = this.get();
}

Watcher.prototype.update = function(){
    var oldValue = this.value;
    var value = this.vm.data[this.key];

    if (oldValue !== value) {
        this.value = value;
        this.cb.call(this, this.vm.data[this.key]);
    }
};

Watcher.prototype.get = function(){
    Dep.target = this;
    var value = this.vm.data[this.key];
    Dep.target = null;

    return value;
};

訂閱器代碼寫好了,咱們再從新修改一下監聽器代碼。主要修改點爲:如何添加訂閱器

enumerable: true,
         configurable: true,
         get: function () {
-            dep.addSub(<watch>);
+            if (Dep.target) {
+                dep.addSub(Dep.target);
+            }
             return val;
         },
         set: function (newValue) {

vue 實現

function Vue (data, dom, key) {
    this.data = data;
    observe(data);
    dom.innerHTML = data[key];

    var watcher = new Watcher(this, key, function (name) {
        dom.innerHTML = name;
    });
}

實際使用

<div id="root"></div>
var vm = new Vue({
            name: 'mumu'
        }, document.getElementById('root'), 'name');

        setTimeout(() => {
            vm.data.name = 'yiyi';
        }, 2000);

還有一點,一般數據的變動是直接使用vm.name,而非vm.data.name,其實也很簡單,直接使用代理,vm.name讀取和寫都代理到vm.data.name上便可。

+    var self = this;
+    Object.keys(this.data).forEach(function(property, index) {
+        self.proxyProperty(property);
+    });
+
     var watcher = new Watcher(this, key, function (name) {
         dom.innerHTML = name;
     });
-}
+}
+
+Vue.prototype.proxyProperty = function(property){
+    Object.defineProperty(this, property, {
+        configurable: true,
+        get: function () {
+            return this.data[property];
+        },
+        set: function (value) {
+            this.data[property] = value;
+        }
+    });
+};

詳細代碼參考github項目

$ git clone https://github.com/doudounannan/vue-like.git
$ cd vue-like
$ git checkout model2view

編譯器

上面的實例看起來,有點問題,咱們是寫死監聽的數據,而後修改dom上的innerHTML,實際中,確定不會這樣,須要在dom中綁定數據,而後動態監聽數據變化。

首先須要明確編譯器 有哪些工做須要作

  • 解析 dom 結構,對 text 節點中綁定數據作一次模板字符串到數據的替換
  • 數據綁定時,添加對應的 訂閱器

目標效果

視圖綁定數據,2s後數據更新,更新到視圖

實現

對dom結構的解析這裏使用 文檔片斷,其dom操做性能優於其餘。

function Compile (options, vm) {
    this.compile = this;
    this.vm = vm;
    this.domEle = document.getElementById(options.el);
    this.fragment = this.createElement(this.domEle);

    this.compileElement(this.fragment);
    this.viewRefresh();
}
Compile.prototype.createElement = function (ele) {
    var fragment = document.createDocumentFragment();
    var child = ele.firstChild;

    while (child) {
        fragment.appendChild(child);
        child = ele.firstChild;
    }

    return fragment;
}

Compile.prototype.compileElement = function (el) {
    var childNodes = el.childNodes;

    [].slice.apply(childNodes).forEach((node) => {
        var reg = /\{\{(\w+)\}\}/;
        var text = node.textContent;

        if (reg.test(text)) {
            this.compileText(node, reg.exec(text)[1]);
        }

        if (node.childNodes && node.childNodes.length > 0) {
            this.compileElement(node);
        }
    });
}

Compile.prototype.compileText = function (node, key) {
    var text = this.vm[key];
    var self = this;

    self.updateText(node, text);

    new Watcher(this.vm, key, function (newText) {
        self.updateText(node, newText);
    });
}

Compile.prototype.updateText = function (node, text) {
    node.textContent = text;
}

Compile.prototype.viewRefresh = function(){
    this.domEle.appendChild(this.fragment);
};

詳細代碼參考github項目

$ git clone https://github.com/doudounannan/vue-like.git
$ cd vue-like
$ git checkout compile

view -> model

這個比較簡單,在compile 解析時,判斷是不是元素節點,若是元素節點中包含指令v-model,從中讀取監聽的數據屬性,再從 model中讀取,除此之外還要綁定一個input事件,用於view -> model

目標效果

目標效果

詳細代碼參考github項目

$ git clone https://github.com/doudounannan/vue-like.git
$ cd vue-like
$ git checkout view2model

事件

目標效果

詳細代碼參考github項目

$ git clone https://github.com/doudounannan/vue-like.git
$ cd vue-like
$ git checkout event

生命週期

好比說建立、初始化、更新、銷燬等。

詳細代碼參考github項目

$ git clone https://github.com/doudounannan/vue-like.git
$ cd vue-like
$ git checkout lifecircle
相關文章
相關標籤/搜索