2

vue 数据双向绑定实现

之前每件事都差不多,直到现在才发现差很多。

现在才发现理清一件事的原委是多么快乐的一件事,我们共同勉励。

纸上得来终觉浅,绝知此事要躬行

懒得扯淡,直接正题

PS: 文章略长。

本文分3个部分来介绍:

  1. model -> view
  2. 编译器
  3. view -> model

clipboard.png

model -> view

其基于 订阅者-发布者模式,简单的讲就是订阅者订阅数据,一旦订阅的数据变更过后,更新绑定的view视图。

这里有明确的分工,分别是监听器、发布器和订阅器,这3者相互协作,各司其职。

  • 监听器:负责 创建 发布器;发布器 添加 订阅器发布器 通知 订阅器
  • 发布器:负责 添加 订阅器;通知 订阅器
  • 订阅器:负责 更新视图

目标效果

过2s数据更改,更新到视图

监听器

顾名思义,监听器,监听器,监听的就是数据的变化

创建 发布器;发布器 添加 订阅器发布器 通知 订阅器

需要解决订阅者的添加和发布器通知订阅器的时机

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

luckyyulin
217 声望9 粉丝

有一种鸟没有脚,他的一生只能在天上飞来飞去,飞累了就在风里睡觉,一辈子只能落地一次,那就是他死的时候