vue2.x的双向绑定原理及实现

192 阅读6分钟

前言

本文为个人对vue2.x响应式数据的学习理解记录,虽然在vue3.0中已经使用了Proxy方法替代了2.x中的object.defineProperty来对数据劫持,但是他们的实现原理还是差不多的,就作为一个过渡吧。

本文主要内容:

1.vue双向数据绑定的实现原理

2.简单理解并实现vue双向数据绑定

3.vue中源码中是如何实现双向数据绑定的

一、vue双向数据绑定的原理

简单概括一下:vue内部通过object.defineProperty方法对data里面的数据进行劫持,把数据的读取转换为setter/getter方法,通过订阅和发布的方式,当数据发生改变通知视图更新。

通过控制台输出一个定义在vue初始化数据上的对象是个什么东西

var vm = new Vue({
    data: {
        obj: {
            name: 'lgf'
        }
    },
    created: function () {
        console.log(this.obj);
    }
});

结果:

我们可以看到输出的对象name属性多了俩个对应的set/get方法,这个俩个方法是怎么来的?是因为在vue中通过object.defineProperty()实现对数据的劫持

object.defineProperty(obj,props,descriptor)

它可以控制一个对象属性的一些特有的操作,例如写入和读取,是否可以枚举等

参数

  • obj:要在其上定义属性的对象。

  • prop:要定义或修改的属性的名称。

  • descriptor:将被定义或修改的属性描述符。

简单实用

let val = 'lgf'
let person = {}
Object.defineProperty(person,'name',{
    get(){
        console.log('name属性被读取了...');
        return val;
    },
    set(newVal){
        console.log('name属性被修改为:'+newVal);
        val = newVal;
    }
})

果然,实用object.defineProperty方法可以对对象属性的读取和写入进行监听。这样我们就可以通过set方法来实现data更新数据改变,更新view

MVVM双向数据绑定即:数据改变更新视图,视图改变更新数据(通过响应事件来更新数据)

二、简单理解并实现vue双向数据绑定

实现过程

首先要对数据进行劫持监听,所以我们要设置一个今天去Observer,用来遍历监听所有属性。如果属性发生变化,就告诉订阅者Watcher看是否需要更新(通过新旧值的比较),因为订阅者不止一个,所以我们需要一个订阅器Dep来专门的收集订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理。最后为了更方便更新视图,我们还需要一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化为一个订阅者Watcher,并替换模板数据({{ }})或者绑定相对应的函数。此时当订阅者接收到相应的属性变化时,就会这些相对应的更新函数,从而改变视图view

所以实现双向数据绑定,需要实现以下几步

1.实现一个监听器Observer: 用来劫持监听所以属性,如有变化,通知订阅者

2.实现一个订阅器Dep:用来收集订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理。

3.实现一个订阅者Watcher: 用来接收属性变化通知,从而更新视图

4.实现一个解析器Compile: 用来遍历解析节点上的指令(v-moel),并初始化模板数据以及初始化相应的订阅者

5.关联监听器,订阅器,订阅者,解析器。

1. 实现一个监听器Observer

监听器的核心是object.defineProperty()监听对象里某个属性,为了监听所以属性我们可以通过递归的方式来给每一属性都进行监听。

function defineReactive(data, key, val) {
    // 递归遍历监听所有子属性
    observe(val);
    // 核心方法 监听读写
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            return val;
        },
        set: function(newVal) {
            val = newVal;
            console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
        }
    });
}
 
function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};

使用:

var library = {
    book1: {
        name: ''
    },
    book2: ''
};
observe(library);
library.book1.name = 'vue权威指南'; // 属性name已经被监听了,现在值为:“vue权威指南”
library.book2 = '没有此书籍';  // 属性book2已经被监听了,现在值为:“没有此书籍”

2.实现一个监听器Dep

为了更方便的收集订阅者,统一管理通知,我们需要一个Dep监听器来统一管理

function Dep () {
    this.subs = []; // 订阅者数组
}
Dep.prototype = {
    // 收集订阅者
    addSub: function(sub) {
        this.subs.push(sub);
    },
    // 通知订阅者
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();  // 订阅者实际更新方法
        });
    

整合监听器Observer和订阅器Dep

function defineReactive(data, key, val) {
    observe(val); // 递归遍历所有子属性
    var dep = new Dep(); 
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            if (是否需要添加订阅者) {
                dep.addSub(watcher); // 在这里添加一个订阅者
            }
            return val;
        },
        set: function(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
            dep.notify(); // 如果数据变化,通知所有订阅者
        }
    });
}
 
function Dep () {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

上面代码中,我们通过在读取get属性的值的时候添加订阅者,属性值变化set时通知订阅者

3.实现一个监听者Watcher

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    this.value = this.get();  // 将自己添加到订阅器的操作
}
 
Watcher.prototype = {
    // 更新方法
    update: function() {
        this.run();
    },
    // 改变属性值
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this;  // 缓存自己
        var value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
        Dep.target = null;  // 释放自己
        return value;
    }
};

为了,触发Observerget方法,将订阅者收集到Dep中,在初始化订阅者的时候,我们就通过获取属性的值,来触发Observerget方法,因为我们只要在订阅者Watcher初始化的时候才需要添加订阅者,所以我们需要通过创建缓存,添加订阅者,清空缓存的方式,同判断来保证只在初始化时添加订阅者

具体判断在defineReactive方法中

function defineReactive(data, key, val) {
    observe(val); // 递归遍历所有子属性
    var dep = new Dep(); 
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            if (Dep.target) {.  // 判断是否需要添加订阅者
                dep.addSub(Dep.target); // 在这里添加一个订阅者
            }
            return val;
        },
        set: function(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            console.log('属性' + key + '已经被监听了,现在值为:“' + newVal.toString() + '”');
            dep.notify(); // 如果数据变化,通知所有订阅者
        }
    });
}
Dep.target = null;

通过监听器 Observer 订阅器 Dep 和订阅者 Watcher 的实现,其实就已经实现了一个双向数据绑定的例子。但是只能对模板数据进行,内容替换的方法来进行。并没有解析节点

4.实现一个解析器Compile

对每个节点元素进行扫描和解析,将相关指令初始化为一个订阅者Watcher,并替换模板数据({{ }})或者绑定相对应的函数

Compile的核心代码

function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/;
        var text = node.textContent;
 
        if (self.isTextNode(node) && reg.test(text)) {  // 判断是否是符合这种形式{{}}的指令
            self.compileText(node, reg.exec(text)[1]);
        }
 
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  // 继续递归遍历子节点
        }
    });
},
function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node, initText);  // 将初始化的数据初始化到视图中
    new Watcher(this.vm, exp, function (value) {  // 生成订阅器并绑定更新函数
        self.updateText(node, value);
    });
},
function updateText (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}

5.将解析器与监听器和订阅者关联起来

为了将将解析器与监听器和订阅者关联起来,并像vue一样调用

function SelfVue (options) {
    var self = this;
    this.vm = this;
    this.data = options;
 
    Object.keys(this.data).forEach(function(key) {
        self.proxyKeys(key);
    });
 
    observe(this.data);
    new Compile(options, this.vm);
    return this;
}

基本使用

// 初始化
new SelfVue({
        el: '#app',
        data: {
            title: 'hello world',
            name: 'lgf'
        },
        methods: {
            clickMe: function () {
                this.title = 'hello world';
            }
        },
        mounted: function () {
            window.setTimeout(() => {
                this.title = '你好';
            }, 1000);
        }
    });

完整代码: github.com/canfoo/self…

参考文章

www.cnblogs.com/canfoo/p/68…

juejin.cn/post/684490…