面试重点----vue源码解析----数据绑定
Posted springxxxx
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试重点----vue源码解析----数据绑定相关的知识,希望对你有一定的参考价值。
首先需要明确两个概念——
数据绑定:一旦更新data中的某个属性数据,所有页面上直接使用或者间接使用了该属性的节点都会自动更新。
数据劫持:通过Object.defineProperty()来监视data中的所有属性(任意层次)数据的变化,一旦数据发生变化就去更新界面。
它们的关系:数据绑定是我们的目的,而数据劫持是vue中用来实现数据绑定的方式。
划重点:数据绑定的主角有两位,一是data对象中的属性:
二是页面上使用了该属性的节点,更具体的说,是模板中的表达式,类似于:
<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源码解析----数据绑定的主要内容,如果未能解决你的问题,请参考以下文章
前端技能树,面试复习第 44 天—— Vue 基础 | Vue 原理解析 | 双向数据绑定原理 | nextTick 原理