阅读 1084

1W字长文+多图,带你了解vue2.x的双向数据绑定源码实现

作者:小土豆
博客园:www.cnblogs.com/HouJiao/
掘金:juejin.cn/user/243617…

前言

一起学习vue源码的第一篇来讲讲vue2.x双向数据绑定的源码实现。

vue中的双向数据绑定主要是通过变化侦测这种方式去实现的,这篇文章主要总结的是Object的变化侦测

vue双向数据绑定中Object和Array的实现原理是不一样的

我们在面试的时候,如果面试者的技术栈包含vue框架,那么面试官会有很大的几率甩出“你了解vue中双向数据绑定的原理吗”这个问题。我也听过一些回答,大家一般都能说出一个词叫发布-订阅。那在深入去问的时候,或者说你能不能给我实现一个简单的双向数据绑定,基本就回答不上来了。

说到这里我已经抛出三个名词了:双向数据绑定变化侦测发布-订阅

前面说过双向数据绑定就是通过变化侦测这种方式去实现的。那这里的发布-订阅我理解是软件的设计思想,它比变化侦测更深一层,已经到了代码的设计模式这一层了。

所以我们可以说双向数据绑定就是通过变化侦测这种方式去实现的,也可以说双向数据绑定是通过发布-订阅这种模式去实现的。我个人觉得两者说法都没有问题,只是描述方式不一样。

那不管是叫变化侦测还是发布-订阅,有一些实际生活中的例子可以便于我们理解它们。

后面的很多描述都会混用这两个名词,不用纠结叫法,了解说的是同一个东西即可

比如我们经常玩的微博:

有一个用户kk很喜欢某个博主MM,然后就在微博上关注了博主MM。

之后每一次博主MM在微博上发表一些吃吃喝喝的动态,微博客户端都会主动将动态推送给用户kk。 

在过了一段时间,博主MM爆出一个不好的新闻,用户kk便将博主MM的微博取关了。
复制代码

在这个实际场景中,我们可以称博主MM是一个发布者用户kk是一个订阅者微博客户端就是一个管理者的角色,它时刻侦测这博主MM的动态,在博主MM更新动态时主动将动态推送给订阅者。

基于我们前面这些,想来大家应该能理解发布订阅/变化侦测大致的设计思想和需要关注的几个点了:

1.如何侦测数据的变化(或者说如何侦测到发布者的发布的内容)

2.如何收集保存订阅者(也就是微博客户端这个管理者的角色如何实现)。  

3.订阅者如何实现。
复制代码

接着就我们总结的这几个点逐个去解读vue的源码实现。

如何侦测数据的变化

看过javascript高级程序设计的应该都知道Object类提供了一个方法defineProperty,在该方法中定义getset就可以实现数据的侦测。

对Object.defineProperty不了解的可以移步这里

下面就Object的defineProperty方法做一个示例演示。

var obj = {};

var name;
Object.defineProperty(obj, 'name', {
      enumerable : true,
      configurable : true,
      get: function(){
          console.log("get方法被调用");
          return name;
      },
      set: function(newName){
          console.log("set方法被调用");
          name = newName;
      }
})


// 修改name属性时会触发set方法
obj.name = 'newTodou';

// 访问name属性时会触发get方法
var objName = obj.name;
复制代码

我们将这段代码引入一个html中,执行后控制台的打印结果如下:

可以看到,当我们修改obj.name属性值时,调用了name属性的set方法,打印了"set方法被调用";当我们访问obj.name属性值时,调用name属性的get方法,打印了"get方法被调用"

那么这就是我们说的如何侦测数据的变化这个问题的答案,是不是很简单呢。

访问数据属性值时会触发定义在属性上的get方法;修改数据属性值时触发定义在属性上的set方法。

这句话很关键,希望可以牢记,后面很多内容都跟这个相关。
复制代码

实际上到这里我们已经可以实现一个简单的双向数据绑定:input输入框内容改变,实现输入框下方span文本内容改变

我们先梳理一下这整个的实现思路:监听输入框的内容,将输入框的内容同步到span的innerText属性。

监听输入框内容的变化可以通过keyup事件,在事件内部获取到input框中的内容,即获取到变化的数据,我们把这个数据保存到一个obj对象的name属性中。

将输入框的内容同步到span的innerText属性这个操作相当于将变化的数据同步更新到视图中,更新的逻辑很简单:spanEle.innerText = obj.name

我们需要考虑的是在哪里触发这个更新操作。

在监听输入框内容变化的逻辑中我们说过会将变化的数据保存到obj.name中。那这个操作实际上就是为对象的属性赋值,会触发定义在属性上的set方法。那么将输入框的内容同步到span的innerText属性这个操作,很自然的就落到了name属性的set方法中。

到这里,相信大家已经很轻松能写出代码了。

<input type="text" id="name"/>
<br/>
<span id="text"></span>
<script type="text/javascript">
    var nameEle = document.getElementById("name");
    var textEle = document.getElementById('text');

    var obj = {};
    Object.defineProperty(obj, 'name', {
        enumerable: true,
        configurable: true,
        get: function(){
            return textEle.value;
        },
        set: function(newName){
            textEle.innerText = newName;

        }
    })
    nameEle.onkeyup = function () {
        obj.name = event.target.value;
    }
</script>
复制代码

整个逻辑代码都比较简单,但是基本已经可以应付面试官的问题:你能不能给我实现一个简单的双向数据绑定这个问题了。

接着还没完,我们知道一个对象里面一般都会有多个属性,vue data中一般也会存在多个或者多层的属性和数据,比如:

data: {
    id: 12091,
    context: {
        index:1,
        result:0
        
    }
}
复制代码

所以我们得让对象中的所有属性都变得可侦测:递归遍历对象的所有属性,为每个属性都定义get和set方法。

vue源码是封装了一个Observer类来实现这个功能。

/*
*   obj数据实际上就是vue中的data数据
*/
function Observer(obj){
    this.obj = obj;
    this.walk(obj);
   
}
Observer.prototype.walk = function(obj) {
    // 获取obj对象中所有的属性
    var keysArr = Object.keys(obj);
    keysArr.forEach(element =>{
        defineReactive(obj, element, obj[element]);
    })
}
// 参照源码,将该方法为独立一个方法
function defineReactive(obj, key, val) {
    // 如果obj是包含多层数据属性的对象,就需要递归每一个子属性
    if(typeof val === 'object'){
        new Observer(val);
    }

    Object.defineProperty(obj, key,{
        enumerable: true,
        configurable: true,
        get: function(){
            return val;
        },
        set: function(newVal) {
            val = newVal;
        }
    })        
}
复制代码

到这里,数据侦测这一步就完成了。

如何收集保存订阅者

收集保存订阅者说的简单点就是一个数据存储的问题,所以也不用太纠结,就将订阅者保持到数组中。

前面我们说过微博的那个例子:

用户kk关注博主MM,对应的就是往数组中添加一个订阅者/元素。

用户kk取关博主MM,可以理解为从数组中移除一个订阅者/元素。

博主MM发布动态,微博客户端主动动态给用户kk,这可以理解为通知数据更新操作。
复制代码

那上面描述的一整个内容就是收集保存订阅者需要关注的东西,深入浅出vue.js这本书中把它叫做如何收集依赖

那么现在就我们说的内容,实现一个类Dep,后面把它称为订阅器,用于管理订阅者/管理依赖

function Dep(){
    this.subs = [];
}

Dep.prototype.addSub = function(sub){
    this.subs.push(sub);
}
// 添加依赖
Dep.prototype.depend = function() {
    // 这里可以先不用关注depObject是什么
    // 就先暂时理解它是一个订阅者/依赖对象
    this.addSub(depObject);
}

 // 移除依赖
 Dep.prototype.removeSub = function(sub) {
    // 源码中是通过抽出来一个remove方法来实现移除的
    if(this.subs.length > 0){
        var index = this.subs.indexOf(sub);
        if(index > -1){
            // 注意splice的用法
            this.subs.splice(index, 1);
        }
    }
}

// 通知数据更新
Dep.prototype.notify = function() {
    for(var i = 0; i < this.subs.length; i++ ){
        // 这里相当于依次调用subs中每个元素的update方法
        // update方法内部实现可以先不用关注,了解其目的就是为了更新数据
        this.subs[i].update()
    }
}
复制代码

依赖收集和管理实现了之后,我们需要考虑两个问题:什么时候添加依赖什么时候通知更新数据

在微博的例子中,用户kk关注博主MM,对应的就是往数组中添加一个订阅者/元素。那对应到代码中,可以视作访问了对象的属性,那我们就可以在访问对象属性的时候添加依赖。

博主MM发布动态,微博客户端主动动态给用户kk,这可以理解为通知数据更新操作。在对应到代码中,可以视作修改了对象的属性,那我们就可以在修改对象属性的时候通知数据更新。

这段话可能不是很好理解(有点强卖强卖的赶脚),所以我们可以去联想平时我们在vue中的操作:使用双花括号{{text}}在模板的div标签内插入数据

这个操作实际上就相当于是模板中的div标签读取并且依赖了vue中的data.text数据,那我们就可以将这个div作为一个依赖对象收集起来。之后当text数据发生变化后,我们就需要通知这个div标签更新它内部的数据。

说了这么多,我们刚刚的提的什么时候添加依赖,什么时候通知更新数据这个问题就已经有答案了:在get中添加依赖,在set中通知数据更新

关于添加依赖通知数据更新这两个操作均是Dep这个类的功能,接口分别为:Dep.dependDep.notify。那现在我们就将Observer这个类进行完善:get中添加依赖,在set中通知数据更新。

/*
*   obj数据实际上就是vue中的data数据
*/
function Observer(obj){
    this.obj = obj;
    if(Array.isArray(this.obj)){
        //如果是数组,则会调用数组的侦测方法
    }else{
        this.walk(obj);
    }
}
Observer.prototype.walk = function(obj) {
    // 获取obj对象中所有的属性
    var keysArr = Object.keys(obj);
    keysArr.forEach(element =>{
        defineReactive(obj, element, obj[element]);
    })
}
// 参照源码,将该方法为独立一个方法
function defineReactive(obj, key, val) {
    // 如果obj是包含多层数据属性的对象,就需要递归每一个子属性
    if(typeof val === 'object'){
        new Observer(val);
    }
    var dep = new Dep();    
    Object.defineProperty(obj, key,{
        enumerable: true,
        configurable: true,
        get: function(){
            // 在get中添加依赖
            dep.depend();
            return val;
        },
        set: function(newVal) {
            val = newVal;
            // 在set中通知数据更新
            dep.notify();

        }
    })        
}
复制代码

如何实现订阅者

还是前面微博的例子,其中用户KK被视为一个订阅者,vue源码中将定义为Watcher。那订阅者需要做什么事情呢?

先回顾一下我们实现的订阅器Dep。第一个功能就是添加订阅者

depend() {
        // 这里可以先不用关注depObject是什么
        // 就先暂时理解它是一个订阅者/依赖对象
        this.addSub(depObject);
}
复制代码

可以看到这段代码中当时的注释是可以先不用关注depObject是什么,暂时理解它是一个订阅者/依赖对象

那现在我们就知道depObject实际上就是一个Watcher实例。那如何触发depend方法添加订阅者呢?

在前面编写侦测数据变化代码时,触发depend方法添加依赖的逻辑在属性的get方法中。

那vue源码的设计是在Watcher初始化的时候触发数据属性的get方法,即可以将订阅者添加到订阅器中。

下面将代码贴出来。

/*
*   vm: vue实例对象
*   exp: 属性名
*/
function Watcher(vm, exp){
    this.vm = vm;
    this.exp = exp;

    // 初始化的时候触发数据属性的get方法,即可以将订阅者添加到订阅器中
    this.value = this.get();
}

// 触发数据属性的get方法: 访问数据属性即可实现
Watcher.prototype.get = function() {
    // 访问数据属性逻辑
    var value =  this.vm.data[this.exp];
    return value;
}
复制代码

这里对get方法的逻辑简单的解读一下:

数据属性的访问肯定是需要传递数据和对应的属性名才能实现。
然后我们想一下vue中的data属性是可以使用vue的实例对象加"."操作符进行访问的。
所以vue在这里设计的时候没有直接将数据传入,而是传递一个vue实例,使用vue实例.data['属性名']对属性进行访问,从而去触发属性的get方法。
复制代码

注意:vue还将访问到的数据属性值保存到了Watcher中value变量中。

到这里,由订阅器Depdepend方法顺藤摸瓜出来的Watcher的第一个功能就完成了,即:Watcher初始化的时候触发数据属性的get方法,将订阅者添加到订阅器中。

我们在接着摸瓜,看一下订阅器Dep的第二个功能:通知数据更新。

// 通知数据更新
notify() {
        for(let i = 0; i < this.subs.length; i++ ){
            // 这里相当于依次调用subs中每个元素的update方法
            // update方法内部实现可以先不用关注,了解其目的就是为了更新数据
            this.subs[i].update()
        }
}
复制代码

这段代码最重要的一行:this.subs[i].update(),这行代码实际上触发的是订阅者Watcher实例的update方法。(因为subs中的每一个元素就是一个订阅者实例)所以我们的Watcher的第二个功能就是需要实现一个真正包含更新数据逻辑的update函数。

那什么叫真正更新数据的逻辑呢?

还是vue的双花括号示例:使用双花括号{{text}}在模板的div标签内插入数据。

当text数据发生变化后,真正更新数据的逻辑就是: div.innerText = newText。那Watcher中的update方法我们应该大致了解了。

在说回vue的设计,它将真正更新数据的逻辑封装成一个函数,Watcher实例初始化的时候传递给Watcher的构造函数,然后在update方法中进行调用。

代码实现如下。

/*
*   vm: vue实例对象
*   exp: 对象的属性
*   cb: 真正包含数据更新逻辑的函数
*/
function Watcher(vm, exp, cb){
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    // 初始化的时候触发数据属性的get方法,即可以将订阅者添加到订阅器中
    this.value = this.get();
}

// 触发数据属性的get方法: 访问数据属性即可实现
Watcher.prototype.get = function() {
    // 访问数据属性逻辑
    var value =  this.vm.data[this.exp];
    return value;
}
Watcher.prototype.update = function() {
    // 当update被触发时,此时获取到的数据属性值是已经被修改过后的新值
    var newValue = this.vm.data[this.exp];

    // 触发传递给Watcher的更新数据的函数
    this.cb.call(this.vm, newValue);
    
}
复制代码

那简单的update代码就实现了,不过vue在这里有做小小的优化。

我们在get方法中访问了数据的属性,并将数据为修改前的初值保存到了this.value中。所以update方法的优化就是在执行update后续代码之前,先对this.valuenewValue做一个比较,即对旧值新值作比较。只有在新值旧值不相等的情况下,才会触发cb函数。

Watcher.prototype.update = function() {
    // 当update被触发时,此时获取到的数据属性值是已经被修改过后的新值
    var newValue = this.vm.data[this.exp];
    var oldValue = this.value;

    if(oldValue !== newValue){
        // 触发传递给Watcher的更新数据的函数
        this.cb.call(this.vm, newValue);
    }
}
复制代码

巧妙保存订阅者(Watcher)实例

到这里呢,我们遗漏了一个逻辑,先看看前面实现的订阅器Depdepend方法。

depend() {
        // 这里可以先不用关注depObject是什么
        // 就先暂时理解它是一个订阅者/依赖对象
        this.addSub(depObject);
}
复制代码

关于这个depObject我们说过它是一个订阅者,即Watcher的一个实例,那如何获取Watcher这个实例呢?

我们回头再看看这个depend方法的触发流程:

即创建Watcher实例,调用Watcher实例的get方法,从而触发数据属性上定义的get方法,最终触发dep.depend方法。

所以按照这个流程,在触发数据属性上定义的get方法之前,就必须将Watcher实例准备好。我们知道在初始化Watcher时,Watcher内部的this的指向就是Watcher实例。所以vue设计的时候,在Watcherget方法中把Watcher实例保存到了Deptarget属性上。这样Watcher实例化完成后,全局访问Dep.target就能获取到Watcher实例。

所以现在将Watcher类的get方法进行补充。

// 触发数据属性的get方法: 访问数据属性即可实现
Watcher.prototype.get = function() {
    // 把Watcher实例保存到了Dep的target属性上
    Dep.target = this;
    // 访问数据属性逻辑
    var value =  this.vm.data[this.exp];
    // 将实例清空释放
    Dep.target = null;
    return value;
}
复制代码

对于get方法中清空释放Dep.target的代码,是有一定原因的,请先继续往下看。

接着我们需要将Dep中的depend方法进行补全。

// 添加依赖
Dep.prototype.depend = function() {
    // addSub添加的是一个订阅者/依赖对象
    // Watcher实例就是订阅者,在Watcher实例初始化的时候,已经将自己保存到了Dep.target中
    if(Dep.target){
        this.addSub(Dep.target);
    } 
}
复制代码

现在我在说一下清空释放Dep.target的代码。

假如我们没有Dep.target = null这行代码,depend方法中也没有if(Dep.target)的判断。那第一个订阅者添加完成后是正常的,当数据发生变化后,代码执行逻辑:

触发数据属性上定义的set方法

执行dep.notify  

执行Watcher实例的update方法

....
复制代码

后面的就不说了,我们看一下这个过程中执行Watcher实例的update方法这一步。

Watcher.prototype.update = function() {
    // 当update被触发时,此时获取到的数据属性值是已经被修改过后的新值
    var newValue = this.vm.data[this.exp];
    var oldValue = this.value;

    if(oldValue !== newValue){
        // 触发传递给Watcher的更新数据的函数
        this.cb.call(this.vm, newValue);
    }
}
复制代码

可以看到,update方法中因为在执行真正更新数据的函数cb之前需要获取到新值。所以再次访问了数据属性,那可想而知,访问数据属性就会调用属性的get方法。

又因为dep.depend的执行没有任何条件判断,导致当前Watcher被植入订阅器两次。这显然是不正常的。因此,Dep.target = nullif(Dep.target)的判断是非常必须的步骤。

完整代码

现在我们将ObserverDepWatcher的完整代码贴出来。

Observer实现

/*
*   obj数据实际上就是vue中的data数据
*/
function Observer(obj){
    this.obj = obj;
    if(Array.isArray(this.obj)){
        //如果是数组,则会调用数组的侦测方法
    }else{
        this.walk(obj);
    }
}
Observer.prototype.walk = function(obj) {
    // 获取obj对象中所有的属性
    var keysArr = Object.keys(obj);
    keysArr.forEach(element =>{
        defineReactive(obj, element, obj[element]);
    })
}
// 参照源码,将该方法为独立一个方法
function defineReactive(obj, key, val) {
    // 如果obj是包含多层数据属性的对象,就需要递归每一个子属性
    if(typeof val === 'object'){
        new Observer(val);
    }
    var dep = new Dep();    
    Object.defineProperty(obj, key,{
        enumerable: true,
        configurable: true,
        get: function(){
            // 在get中添加依赖
            dep.depend();
            return val;
        },
        set: function(newVal) {
            val = newVal;
            // 在set中通知数据更新
            dep.notify();

        }
    })        
}
复制代码

Dep实现

function Dep(){
    this.subs = [];
}

Dep.prototype.addSub = function(sub){
    this.subs.push(sub);
}
// 添加依赖
Dep.prototype.depend = function() {
    // addSub添加的是一个订阅者/依赖对象
    // Watcher实例就是订阅者,在Watcher实例初始化的时候,已经将自己保存到了Dep.target中
    if(Dep.target){
        this.addSub(Dep.target);
    } 
}

 // 移除依赖
 Dep.prototype.removeSub = function(sub) {
    // 源码中是通过抽出来一个remove方法来实现移除的
    if(this.subs.length > 0){
        var index = this.subs.indexOf(sub);
        if(index > -1){
            // 注意splice的用法
            this.subs.splice(index, 1);
        }
    }
}

// 通知数据更新
Dep.prototype.notify = function() {
    for(var i = 0; i < this.subs.length; i++ ){
        // 这里相当于依次调用subs中每个元素的update方法
        // update方法内部实现可以先不用关注,了解其目的就是为了更新数据
        this.subs[i].update()
    }
}
复制代码

Watcher实现

/*
*   vm: vue实例对象
*   exp: 对象的属性
*   cb: 真正包含数据更新逻辑的函数
*/
function Watcher(vm, exp, cb){
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    // 初始化的时候触发数据属性的get方法,即可以将订阅者添加到订阅器中
    this.value = this.get();
}

// 触发数据属性的get方法: 访问数据属性即可实现
Watcher.prototype.get = function() {
    // 把Watcher实例保存到了Dep的target属性上
    Dep.target = this;
    // 访问数据属性逻辑
    var value =  this.vm.data[this.exp];
    // 将实例清空释放
    Dep.target = null;
    return value;
}
Watcher.prototype.update = function() {
    // 当update被触发时,此时获取到的数据属性值是已经被修改过后的新值
    var newValue = this.vm.data[this.exp];
    var oldValue = this.value;

    if(oldValue !== newValue){
        // 触发传递给Watcher的更新数据的函数
        this.cb.call(this.vm, newValue);
    }
}
复制代码

实践

关键核心的代码已经梳理完成,接下来就是使用了。

因为这个过程中没有模板编译的实现,因此有些代码需要写死。

回想vue中双向数据绑定的用法,我们先写一段简单的代码。

<html>
    <head>
        <meta charset="utf-8" />
        <title>一起学习Vue源码-Object的变化侦测</title>
    </head>
    <body>
        <h1>一起学习Vue源码-Object的变化侦测</h1>
        <div id="box">
            {{text}}
        </div>
    </body>
    <script type="text/javascript" src="./Dep.js"></script> 
    <script type="text/javascript" src="./Observer.js"></script>    
    <script type="text/javascript" src="./Watcher.js"></script>

    <script type='text/javascript'>
        /*
        *   data: 数据
        *   el: 元素
        *   exp:对象的属性
        *   (传递这个exp固定参数也是因为没有模板编译相关的代码,所以就暂时写死一个属性)
        */
        function Vue(data, el, exp){
            this.data = data;
            this.el = el;
            // 因为没有模板相关的代码,所以{{text}}的值使用这种方式进行解析
            this.innerHTML = this.data[exp];
        }

        var data = {
            text: 'hello Vue'
        };
        var el = document.getElementById('box');
      
        var vm = new Vue(data, el);      
    </script>
</html>
复制代码

这段代码运行后,浏览器中已经可以显示{{text}}的值了。

正常显示并不是因为我们对模板和花括号进行编译,而是使用el.innerHTML = data.text;这种写死的方式实现的。

接着,第一步就是将数据变得可观测,即调用Observer传入data数据,我们将代码写到Vue构造函数中。

/*
 *   data: 数据
 *   el: 元素
 *   exp:对象的属性
 */
function Vue(data, el, exp){
     this.data = data;
     this.el = el;
     this.exp = exp;

     // 因为没有模板相关的代码,所以{{text}}的值使用这种方式进行解析
     this.el.innerHTML = this.data[exp];

     //初始化vue实例需要将data数据变得可观测
     new Observer(data);
}

复制代码

接着,手动为datatext属性创建一个订阅者,代码依然写在vue构造函数中。

手动创建订阅者也是因为没有模板编译代码,否则创建订阅者正常的逻辑是遍历模板动态创建订阅者。

/*
 *   data: 数据
 *   el: 元素
 *   exp:对象的属性
 */
function Vue(data, el, exp){
     this.data = data;
     this.el = el;
     this.exp = exp;

     // 因为没有模板相关的代码,所以{{text}}的值使用这种方式进行解析
     this.el.innerHTML = this.data[exp];

     //初始化vue实例需要将data数据变得可观测
     new Observer(data);

     this.cb = function(newVal){
          this.el.innerHTML = newVal;
     }
     // 创建一个订阅者
     new Watcher(this, exp, this.cb);
}
复制代码

创建订阅者的时候有一个cb参数,cb就是我们前面一直说的那个真正包含更新数据逻辑的函数

这些操作完成后,最后一步就是修改data.text的数据,如果修改完成后,div的内容发生变化,就证明我们这份代码已经成功运行了。那修改data.text数据的逻辑我借用一个button来实现:监听buttonclick事件,触发时将data.text的值改为"hello new vue"

<html>
    <head>
        <meta charset="utf-8" />
        <title>一起学习Vue源码-Object的变化侦测</title>
    </head>
    <body>
        <h1>一起学习Vue源码-Object的变化侦测</h1>
        <div id="box">
            {{text}}
        </div>
        <br/>
        <button onclick="btnClick()">点击我改变div的内容</button>
    </body>
    <script type="text/javascript" src="./Dep.js"></script> 
    <script type="text/javascript" src="./Observer.js"></script>    
    <script type="text/javascript" src="./Watcher.js"></script>

    <script>
        /*
        *   data: 数据
        *   el: 元素id
        *   exp:对象的属性
        *   (传递这个exp固定参数也是因为没有模板编译相关的代码,所以就暂时写死一个属性)
        *   cb: 真正包含数据更新逻辑的函数
        */
        function Vue(data, el, exp){
            this.data = data;
            this.el = el;
            this.exp = exp;
            // 因为没有模板相关的代码,所以{{text}}的值使用这种方式进行解析
            this.el.innerHTML = this.data[exp];
            this.cb = function(newVal){
                this.el.innerHTML = newVal;
            }
            //初始化vue实例需要将data数据变得可观测
            new Observer(data);
            //创建一个订阅者
            new Watcher(this, exp, this.cb);
        }
        var data = {
            text: 'hello Vue'
        };
        var el = document.getElementById('box');
        
        var exp = 'text';

        var vm = new Vue(data, el, exp);

        function btnClick(){
            vm.data.text = "hello new vue";
        }
    </script>
</html>
复制代码

上面这份代码已经是完整的代码了,我们一起在浏览中操作一波看下结果。

可以看到,我们的代码已经成功运行。

那到这里,vue的双向数据绑定源码实现梳理完毕。

结束语:

    我的vue源码的学习途径主要会参考我自己刚入手的《深入浅出vue.js》这本书,同时会参考网上一些内容。

    我会尽量将从源码中解读出的内容,以一种更通俗易懂的方式总结出来。

    如果我的内容能给你带来帮助,可以持续关注我,或者在评论区指出不足之处。

    同时因为是源码学习,所以这个过程中我也充当一个源码搬运工的角色,不创造代码只搬运并解读源码。
    
复制代码

写在最后

如果这篇文章有帮助到你,❤️关注+点赞❤️鼓励一下作者

文章公众号首发,关注 不知名宝藏程序媛 第一时间获取最新的文章

笔芯❤️~