面试重点----vue源码解析----数据绑定

Posted springxxxx

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试重点----vue源码解析----数据绑定相关的知识,希望对你有一定的参考价值。

首先需要明确两个概念——

数据绑定:一旦更新data中的某个属性数据,所有页面上直接使用或者间接使用了该属性的节点都会自动更新。

数据劫持:通过Object.defineProperty()来监视data中的所有属性(任意层次)数据的变化,一旦数据发生变化就去更新界面。

它们的关系:数据绑定是我们的目的,而数据劫持是vue中用来实现数据绑定的方式

 

划重点:数据绑定的主角有两位,一是data对象中的属性

data() {    //  注意这里一共是四个属性:name、friends、friends.name、friends.age
   return {
   name: \'bob\'
   friends: {
    name: \'Mary\'
    age: 28
   }
   }
}

二是页面上使用了该属性的节点,更具体的说,是模板中的表达式,类似于:

<p v-text="name"></p>
<h2>{{name}}</h2>
<p v-text=\'friends.name\'></p>
<p v-text=\'friends.age\'></p>

如上所示,一个属性可能会在模板中被多个表达式使用;另一方面,一个表达式也可能会用到多个属性(如上面的friends.name)——所以属性和表达式之间是多对多的关系。

 

为了管理属性和表达式之间错综复杂的关系,我们定义两个重要的构造函数:Dep(dependency的简写)和Watcher。

Vue会为data中的每个属性创建一个Dep的实例对象(记作dep),dep包含两个属性:唯一标识符id,以及subs(subsribers的简写)。subs初始时为空数组,随后会往其中添加watcher,表示当前属性在哪些表达式中被使用。

Vue还会为模板中的每个表达式创建一个Watcher的实例对象(记作watcher),watcher的属性有好几个,其中包含一个对象属性depIds,初始时为空对象,随后会往其中添加dep(键为dep.id,值为dep对象),表示当前表达式使用了哪些属性。

一个属性对应一个dep,一个表达式对应一个watcher,它们之间又相互引用,这就是响应式的基本架构,下面来看看具体是如何实现的。

 

vue文件的解析主要有三步:数据代理、数据劫持、模板解析。当数据代理完成后,Vue会遍历data中的属性,每次将一个属性的key、value,和data对象一起传给defineReactive方法。

defineReactive: function(data, key, val) {
        //  每个属性都对应一个dep对象
        var dep = new Dep();
        //  属性值若也是对象,则为其内部属性绑定监视器(类似于递归)
        var childObj = observe(val);
        //  重新给data添加当前属性,区别在于这次添加了set和get方法
        Object.defineProperty(data, key, {
            enumerable: true, // 可枚举
            configurable: false, // 不能再define
            //  get方法主要在初始化时生效
            get: function() {
                if (Dep.target) {
                    dep.depend();
                }
                //  下面这一句才是get原始的功能
                return val;
            },
            //  set方法在数据修改时生效
            set: function(newVal) {
                if (newVal === val) {
                    return;
                }
                val = newVal;
                // 新值若也是对象,则为其内部属性绑定监视器  贯彻概念中"任意层次"四个字
                childObj = observe(newVal);
                // 通知订阅者数据修改
                dep.notify();
            }
        });
    }

defineReactive方法最关键的部分就是新写的get方法和set方法,其中get方法会在对属性执行读操作、且Dep.target有值时生效。这里可以提前透露一下,Dep.target的值就是某个watcher,然而此时此刻watcher一个都还没被创建,所以当前Dep.target的值为null,语句块根本没有机会被执行。而set方法要在数据修改时才生效,一时半会也没法生效。所以等到后面触发了这两个方法我们再来对其进行解读。

数据劫持完成后,下一步是模板解析。在每条指令(如v-text="name")被解析完成后,会为其包含的表达式(如"name")创建一个watcher对象:

updaterFn && updaterFn(node, this._getVMVal(vm, exp));

new Watcher(vm, exp, function(value, oldValue) {
     updaterFn && updaterFn(node, value, oldValue);
});

其中updateFn是模板解析的核心操作:更新页面,但在这里不做讨论。我们只需要知道创建的watcher对象有三个参数:vm对象、对应的表达式和一个回调函数。显然。若对应的表达式中某个属性被更改,便会调用该回调函数来实现响应式更新页面。下面是Watcher实例化时的内部操作:

function Watcher(vm, expOrFn, cb) {
    this.cb = cb;
    this.vm = vm;
    this.expOrFn = expOrFn;
    this.depIds = {};
    //  保存表达式的初始值并保存
    this.value = this.get();
}
get: function() {
        //  给dep指定当前的watcher
        Dep.target = this;
        //  获取表达式的值  内部会调用get建立dep与watcher的关系
        var value = this.getter.call(this.vm, this.vm);
        //  清除target的指向
        Dep.target = null;
        return value;
}

watcher实例有五个属性,前三个就是把传入的参数进行了保存;第四个就是前面提到的depIds,在后面它会用来存储相关的dep对象;而第五个属性是——表达式的初始值,那就必须得获取表达式中各个属性的值,就会触发这些属性的get方法,而且此时Dep.target已经指向了当前新创建的watcher,于是让我们再次返回上面defineReactive方法中的get方法。

 

假设当前watcher对应的表达式是\'friends.name\',它将要依次和friends和friends.name两个属性建立联系,但此时它的depIds还是空对象,friends和friends.name二者的subs也都是空数组。

get: function() {  
       if (Dep.target) {
            dep.depend();
       }
       //  下面这一句才是get原始的功能
       return val;
}

读属性时,会触发dep.depend()方法

depend: function() {
        Dep.target.addDep(this);
}

dep.depend()方法中,执行了当前的watcher对象的addDep方法

addDep: function(dep) {
        //  判断dep和watcher是否已经建立了联系
        if (!this.depIds.hasOwnProperty(dep.id)) {
            //  给dep添加新的sub(watcher)      用于更新
            dep.addSub(this);
            //  给watcher添加新的dep    用于避免重复建立关系
            this.depIds[dep.id] = dep;
        }
},
addSub: function(sub) {
        this.subs.push(sub);
}

为了避免重复添加,addDep方法中先执行了一个判断,如果当前watcher和当前属性之间还未建立联系,便执行两个最关键的操作:给dep添加新的sub(watcher) 、给watcher添加新的dep,礼尚往来。

这两步完成后,当前的watcher.depIds中便加入了某个属性对应的dep,而该属性对应的dep.subs中也加入了当前的watcher。

类似的,接下来又会在当前的watcher和其它相关属性之间建立联系,结果就是watcher.depIds包含了多个属性,而这些属性的dep.subs分别加入当前的watcher。

当前watcher和所有相关属性都建立了联系后,下一个表达式又会创建一个watcher实例(记作watcher02),watcher02会和它的相关属性都建立联系。

这个感觉像是一个两层循环,外层遍历watcher,内层遍历当前watcher对应的dep,最终实现多对多的连接。

 

至此,我们就完成了数据绑定中的初始化部分。还有一部分操作,发生在数据更改后,比如:执行vm.name = harden。

此时会触发names属性中的set方法:

//  set方法在数据修改时生效
set: function(newVal) {
     if (newVal === val) {
         return;
     }
     val = newVal;
     // 新值若也是对象,则为其内部属性绑定监视器  贯彻概念中"任意层次"四个字
     childObj = observe(newVal);
     // 通知订阅者数据修改
     dep.notify();
}

核心操作是dep.notify()方法

notify: function() {
        //  遍历当前属性的订阅者(watchers), 要求它们同步更新数据
        this.subs.forEach(function(sub) {
            sub.update();
        });
}

这也很好理解,就是遍历当前属性的subs,通知与之相关的watcher同步更新数据。

update: function() {
        this.run();
    },
    run: function() {
        //  获取新值
        var value = this.get();
        //  读取旧值
        var oldVal = this.value;
        //  若新值和旧值不相等
        if (value !== oldVal) {
            //  保存新值
            this.value = value;
            //  执行更新界面的回调函数
            this.cb.call(this.vm, value, oldVal);
        }
}

update只是个过渡方法,起作用的是run方法,其功能可以用一句话表达:如果修改后的值与原来的值不等,则执行更新界面的回调函数。(还记得watcher实例化时传入的那个回调函数吗!)

至此,vue1.0的数据绑定就基本实现了,总结一下:

  在实现数据代理之后,模板解析之前进行数据劫持——为data中每个属性创建一个dep对象,并重写这些属性的get和set方法。在模板解析过程中,为每个表达式创建一个watcher对象,由于创建过程中需要读属性值,所以会触发属性的get方法,在get方法内部watcher和dep之间建立起多对多的联系;当data中某个属性被修改,会触发属性的set方法,该属性对应的dep会通知与之相关联的所有watcher对象,同步更新数据,实现响应式更新。

以上是关于面试重点----vue源码解析----数据绑定的主要内容,如果未能解决你的问题,请参考以下文章

vuex源码分析(二)——双向数据绑定

前端技能树,面试复习第 44 天—— Vue 基础 | Vue 原理解析 | 双向数据绑定原理 | nextTick 原理

vue双向绑定原理源码解析

前端vue经典面试题78道(重点详细简洁)

前端面试题:Vue面试题及Vue源码解析分享

Vue+网络协议+Webpack高频面试题