作者:小土豆biubiubiu

博客园:www.cnblogs.com/HouJiao/

掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d

简书:https://www.jianshu.com/u/cb1c3884e6d5

微信公众号:土豆妈的碎碎念(扫码关注,一起吸猫,一起听故事,一起学习前端技术)

码字不易,点赞鼓励哟~

一.前言

  一起学习vue源码的第一篇,本来想起名为双向数据绑定原理,但是想来还是引用书中[深入浅出vue.js]比较专业的描述作为题目。

  (主要是因为双向数据绑定中Object和Array的实现原理是不一样的,所以还是拆分的细一些比较好)

  总归来说,双向数据绑定就是通过变化侦测这种方式去实现的,我们这篇文章主要总结的是Object的变化侦测。

  我们在面试的时候,如果面试者的技术栈包含vue框架,那么面试官会有很大的几率甩出“你了解vue中双向数据绑定的原理吗”这个问题。

  我也听过一些回答,大家一般都能说出一个词叫“发布-订阅”。

  那在深入去问的时候,或者说你能不能给我实现一个简单的双向数据绑定,基本就回答不上来了。

  

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

  前面说过双向数据绑定就是通过变化侦测这种方式去实现的。

  那这里的发布-订阅我理解是软件的设计思想,它比变化侦测更深一层,已经到了代码的设计模式这一层了。

  所以我们可以说双向数据绑定就是通过变化侦测这种方式去实现的,也可以说双向数据绑定是通过发布-订阅这种模式去实现的。

  我个人觉得两者说法都没有问题,只是描述方式不一样。

 

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

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

  比如我们经常玩的微博:

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

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

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

  在这个实际场景中,我们可以称博主MM是一个发布者

  用户kk是一个订阅者

  微博客户端就是一个管理者的角色,它时刻侦测这博主MM的动态,在博主MM更新动态是主动将动态推送给订阅者。

  

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

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

    2.如何收集保存订阅者。

    3.订阅者如何实现。

  

  接着我们就我们总结的点逐个击破。

二.如何侦测数据的变化

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

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

  

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

  1. var obj = {};
  2.  
  3. var name;
  4. Object.defineProperty(obj, 'name', {
  5. enumerable : true,
  6. configurable : true,
  7. get: function(){
  8. console.log("get方法被调用");
  9. return name;
  10. },
  11. set: function(newName){
  12. console.log("set方法被调用");
  13. name = newName;
  14. }
  15. })
  16.  
  17. // 修改name属性时会触发set方法
  18. obj.name = 'newTodou';
  19.  
  20. // 访问name属性时会触发get方法
  21. 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方法中。

  

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

  1. <input type="text" id="name"/>
  2. <br/>
  3. <span id="text"></span>
  4. <script type="text/javascript">
  5. var nameEle = document.getElementById("name");
  6. var textEle = document.getElementById('text');
  7.  
  8. var obj = {};
  9. Object.defineProperty(obj, 'name', {
  10. enumerable: true,
  11. configurable: true,
  12. get: function(){
  13. return textEle.value;
  14. },
  15. set: function(newName){
  16. textEle.innerText = newName;
  17.  
  18. }
  19. })
  20. nameEle.onkeyup = function () {
  21. obj.name = event.target.value;
  22. }
  23. </script>

  

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

  data: {

    id: 12091,

    context: {

      index:1,

      result: 0

    }

  }

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

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

  1. /*
  2. * obj数据实际上就是vue中的data数据
  3. */
  4. function Observer(obj){
  5. this.obj = obj;
  6. this.walk(obj);
  7.  
  8. }
  9. Observer.prototype.walk = function(obj) {
  10. // 获取obj对象中所有的属性
  11. var keysArr = Object.keys(obj);
  12. keysArr.forEach(element =>{
  13. defineReactive(obj, element, obj[element]);
  14. })
  15. }
  16. // 参照源码,将该方法为独立一个方法
  17. function defineReactive(obj, key, val) {
  18. // 如果obj是包含多层数据属性的对象,就需要递归每一个子属性
  19. if(typeof val === 'object'){
  20. new Observer(val);
  21. }
  22.  
  23. Object.defineProperty(obj, key,{
  24. enumerable: true,
  25. configurable: true,
  26. get: function(){
  27. return val;
  28. },
  29. set: function(newVal) {
  30. val = newVal;
  31. }
  32. })
  33. }

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

三.如何收集保存订阅者

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

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

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

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

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

  

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

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

  1. function Dep(){
  2. this.subs = [];
  3. }
  4.  
  5. Dep.prototype.addSub = function(sub){
  6. this.subs.push(sub);
  7. }
  8. // 添加依赖
  9. Dep.prototype.depend = function() {
  10. // 这里可以先不用关注depObject是什么
  11. // 就先暂时理解它是一个订阅者/依赖对象
  12. this.addSub(depObject);
  13. }
  14.  
  15. // 移除依赖
  16. Dep.prototype.removeSub = function(sub) {
  17. // 源码中是通过抽出来一个remove方法来实现移除的
  18. if(this.subs.length > 0){
  19. var index = this.subs.indexOf(sub);
  20. if(index > -1){
  21. // 注意splice的用法
  22. this.subs.splice(index, 1);
  23. }
  24. }
  25. }
  26.  
  27. // 通知数据更新
  28. Dep.prototype.notify = function() {
  29. for(var i = 0; i < this.subs.length; i++ ){
  30. // 这里相当于依次调用subs中每个元素的update方法
  31. // update方法内部实现可以先不用关注,了解其目的就是为了更新数据
  32. this.subs[i].update()
  33. }
  34. }

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

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

  那对应到代码中,可以视作访问了对象的属性,那我们就可以在访问对象属性的时候添加依赖。

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

  在对应到代码中,可以视作修改了对象的属性,那我们就可以在修改对象属性的时候通知数据更新。

  

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

  这个操作实际上就相当于是模板中的div便签读取并且依赖了vue中的data.text数据,那我们就可以将这个div作为一个依赖对象收集起来。

  之后当text数据发生变化后,我们就需要通知这个div标签更新它内部的数据。

 

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

    在get中添加依赖,在set中通知数据更新。

   

  关于添加依赖通知数据更新这两个操作均是Dep这个类的功能,接口分别为:Dep.depend和Dep.notify。

  那现在我们就将Observer这个类进行完善:get中添加依赖,在set中通知数据更新。

  1. /*
  2. * obj数据实际上就是vue中的data数据
  3. */
  4. function Observer(obj){
  5. this.obj = obj;
  6. if(Array.isArray(this.obj)){
  7. //如果是数组,则会调用数组的侦测方法
  8. }else{
  9. this.walk(obj);
  10. }
  11. }
  12. Observer.prototype.walk = function(obj) {
  13. // 获取obj对象中所有的属性
  14. var keysArr = Object.keys(obj);
  15. keysArr.forEach(element =>{
  16. defineReactive(obj, element, obj[element]);
  17. })
  18. }
  19. // 参照源码,将该方法为独立一个方法
  20. function defineReactive(obj, key, val) {
  21. // 如果obj是包含多层数据属性的对象,就需要递归每一个子属性
  22. if(typeof val === 'object'){
  23. new Observer(val);
  24. }
  25. var dep = new Dep();
  26. Object.defineProperty(obj, key,{
  27. enumerable: true,
  28. configurable: true,
  29. get: function(){
  30. // 在get中添加依赖
  31. dep.depend();
  32. return val;
  33. },
  34. set: function(newVal) {
  35. val = newVal;
  36. // 在set中通知数据更新
  37. dep.notify();
  38.  
  39. }
  40. })
  41. }

四.如何实现订阅者

  还是前面微博的例子,其中用户KK被视为一个订阅者,vue源码中将定义为Watcher

  那订阅者需要做什么事情呢?

  先回顾一下我们实现的订阅器Dep

  第一个功能就是添加订阅者。

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

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

  那现在我们就知道depObject实际上就是一个Watcher实例

  那如何触发depend方法添加订阅者呢?

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

  

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

  

  下面将代码贴出来。

  1. /*
  2. * vm: vue实例对象
  3. * exp: 属性名
  4. */
  5. function Watcher(vm, exp){
  6. this.vm = vm;
  7. this.exp = exp;
  8.  
  9. // 初始化的时候触发数据属性的get方法,即可以将订阅者添加到订阅器中
  10. this.value = this.get();
  11. }
  12.  
  13. // 触发数据属性的get方法: 访问数据属性即可实现
  14. Watcher.prototype.get = function() {
  15. // 访问数据属性逻辑
  16. var value = this.vm.data[this.exp];
  17. return value;
  18. }

  

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

    数据属性的访问肯定是需要传递数据和对应的属性名才能实现。

    然后我们想一下vue中的data属性是可以使用vue的实例对象"."操作符进行访问的。

    所以vue在这里设计的时候没有直接将数据传入,而是传递一个vue实例,使用vue实例.data['属性名']对属性进行访问,从而去触发属性的get方法。

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

  到这里,由订阅器Dep的depend方法顺藤摸瓜出来的Watcher的第一个功能就完成了,即:

    Watcher初始化的时候触发数据属性的get方法,将订阅者添加到订阅器中。

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

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

  这段代码最重要的一行:this.subs[i].update(),这行代码实际上触发的是订阅者Watcher实例的update方法。

  (因为subs中的每一个元素就是一个订阅者实例)

  所以我们的Watcher的第二个功能就是需要实现一个真正包含更新数据逻辑的update函数

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

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

  当text数据发生变化后,真正更新数据的逻辑就是: div.innerText = newText;

  那Watcher中的update方法我们应该大致了解了。

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

  

  1. function Watcher(vm, exp, cb){
  2. this.vm = vm;
  3. this.exp = exp;
  4. this.cb = cb;
  5. // 初始化的时候触发数据属性的get方法,即可以将订阅者添加到订阅器中
  6. this.value = this.get();
  7. }
  8.  
  9. // 触发数据属性的get方法: 访问数据属性即可实现
  10. Watcher.prototype.get = function() {
  11. // 访问数据属性逻辑
  12. var value = this.vm.data[this.exp];
  13. return value;
  14. }
  15. Watcher.prototype.update = function() {
  16. // 当update被触发时,此时获取到的数据属性值是已经被修改过后的新值
  17. var newValue = this.vm.data[this.exp];
  18.  
  19. // 触发传递给Watcher的更新数据的函数
  20. this.cb.call(this.vm, newValue);
  21.  
  22. }

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

  我们在get方法中访问了数据的属性,并将数据为修改前的初值保存到了this.value中。

  所以update方法的优化就是在执行update后续代码之前,先对this.value和newValue做一个比较,即对旧值和新值作比较。

  只有在新值和旧值不相等的情况下,才会触发cb函数。

  1. Watcher.prototype.update = function() {
  2. // 当update被触发时,此时获取到的数据属性值是已经被修改过后的新值
  3. var newValue = this.vm.data[this.exp];
  4. var oldValue = this.value;
  5.  
  6. if(oldValue !== newValue){
  7. // 触发传递给Watcher的更新数据的函数
  8. this.cb.call(this.vm, newValue);
  9. }
  10. }

五.代码补充

  Watcher中触发数据属性get方法的执行已经补充完毕,我们在看看订阅器Dep的depend方法。

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

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

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

    

  

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

  所以按照这个流程,在触发数据属性上定义的get方法之前,就必须将Watcher实例准备好。

  我们知道在初始化Watcher时,Watcher内部的this的指向就是Watcher实例。

  所以vue设计的时候,在Watcher的get方法中把Watcher实例保存到了Dep的target属性上。

  这样Watcher实例化完成后,全局访问Dep.target就能获取到Watcher实例。

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

  1. // 触发数据属性的get方法: 访问数据属性即可实现
  2. Watcher.prototype.get = function() {
  3. // 把Watcher实例保存到了Dep的target属性上
  4. Dep.target = this;
  5. // 访问数据属性逻辑
  6. var value = this.vm.data[this.exp];
  7. // 将实例清空释放
  8. Dep.target = null;
  9. return value;
  10. }

  备注:对于get方法中清空释放Dep.target的代码,是有一定原因的。请先继续往下看,把Dep.depend的补全代码看完。

 

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

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

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

  假如我们没有Dep.target = null这行代码,depend方法中也没有if(Dep.target)的判断。

  那第一个订阅者添加完成后是正常的,当数据发生变化后,代码执行逻辑:

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

    执行dep.notify

    执行Watcher实例的update方法

    ....

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

  1. Watcher.prototype.update = function() {
  2. // 当update被触发时,此时获取到的数据属性值是已经被修改过后的新值
  3. var newValue = this.vm.data[this.exp];
  4. var oldValue = this.value;
  5.  
  6. if(oldValue !== newValue){
  7. // 触发传递给Watcher的更新数据的函数
  8. this.cb.call(this.vm, newValue);
  9. }
  10. }

  可以看到,update方法中因为在执行真正更新数据的函数cb之前需要获取到新值。

  所以再次访问了数据属性,那可想而知,访问数据属性就会调用属性的get方法。

  又因为dep.depend的执行没有任何条件判断,导致当前Watcher被植入订阅器两次。

  这显然是不正常的。因此,Dep.target = nullif(Dep.target)的判断是非常必须的步骤。

六.完整代码

  现在我们将Observer、Dep、Watcher的完整代码贴出来。

  Observer实现

  1. /*
  2. * obj数据实际上就是vue中的data数据
  3. */
  4. function Observer(obj){
  5. this.obj = obj;
  6. if(Array.isArray(this.obj)){
  7. //如果是数组,则会调用数组的侦测方法
  8. }else{
  9. this.walk(obj);
  10. }
  11. }
  12. Observer.prototype.walk = function(obj) {
  13. // 获取obj对象中所有的属性
  14. var keysArr = Object.keys(obj);
  15. keysArr.forEach(element =>{
  16. defineReactive(obj, element, obj[element]);
  17. })
  18. }
  19. // 参照源码,将该方法为独立一个方法
  20. function defineReactive(obj, key, val) {
  21. // 如果obj是包含多层数据属性的对象,就需要递归每一个子属性
  22. if(typeof val === 'object'){
  23. new Observer(val);
  24. }
  25. var dep = new Dep();
  26. Object.defineProperty(obj, key,{
  27. enumerable: true,
  28. configurable: true,
  29. get: function(){
  30. // 在get中添加依赖
  31. dep.depend();
  32. return val;
  33. },
  34. set: function(newVal) {
  35. val = newVal;
  36. // 在set中通知数据更新
  37. dep.notify();
  38.  
  39. }
  40. })
  41. }

  Dep实现

  1. function Dep(){
  2. this.subs = [];
  3. }
  4.  
  5. Dep.prototype.addSub = function(sub){
  6. this.subs.push(sub);
  7. }
  8. // 添加依赖
  9. Dep.prototype.depend = function() {
  10. // addSub添加的是一个订阅者/依赖对象
  11. // Watcher实例就是订阅者,在Watcher实例初始化的时候,已经将自己保存到了Dep.target中
  12. if(Dep.target){
  13. this.addSub(Dep.target);
  14. }
  15. }
  16.  
  17. // 移除依赖
  18. Dep.prototype.removeSub = function(sub) {
  19. // 源码中是通过抽出来一个remove方法来实现移除的
  20. if(this.subs.length > 0){
  21. var index = this.subs.indexOf(sub);
  22. if(index > -1){
  23. // 注意splice的用法
  24. this.subs.splice(index, 1);
  25. }
  26. }
  27. }
  28.  
  29. // 通知数据更新
  30. Dep.prototype.notify = function() {
  31. for(var i = 0; i < this.subs.length; i++ ){
  32. // 这里相当于依次调用subs中每个元素的update方法
  33. // update方法内部实现可以先不用关注,了解其目的就是为了更新数据
  34. this.subs[i].update()
  35. }
  36. }

  Watcher实现

  1. function Watcher(vm, exp, cb){
  2. this.vm = vm;
  3. this.exp = exp;
  4. this.cb = cb;
  5. // 初始化的时候触发数据属性的get方法,即可以将订阅者添加到订阅器中
  6. this.value = this.get();
  7. }
  8.  
  9. // 触发数据属性的get方法: 访问数据属性即可实现
  10. Watcher.prototype.get = function() {
  11. // 把Watcher实例保存到了Dep的target属性上
  12. Dep.target = this;
  13. // 访问数据属性逻辑
  14. var value = this.vm.data[this.exp];
  15. // 将实例清空释放
  16. Dep.target = null;
  17. return value;
  18. }
  19. Watcher.prototype.update = function() {
  20. // 当update被触发时,此时获取到的数据属性值是已经被修改过后的新值
  21. var newValue = this.vm.data[this.exp];
  22. var oldValue = this.value;
  23.  
  24. if(oldValue !== newValue){
  25. // 触发传递给Watcher的更新数据的函数
  26. this.cb.call(this.vm, newValue);
  27. }
  28. }

七.实践

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

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

  回想vue中双向数据绑定的用法。

  我们先写一段简单的代码。

  1. <html>
  2. <head>
  3. <meta charset="utf-8" />
  4. <title>一起学习Vue源码-Object的变化侦测</title>
  5. </head>
  6. <body>
  7. <h1>一起学习Vue源码-Object的变化侦测</h1>
  8. <div id="box">
  9. {{text}}
  10. </div>
  11. </body>
  12. <script type="text/javascript" src="./Dep.js"></script>
  13. <script type="text/javascript" src="./Observer.js"></script>
  14. <script type="text/javascript" src="./Watcher.js"></script>
  15.  
  16. <script type='text/javascript'>
  17. /*
  18. * data: 数据
  19. * el: 元素
  20. * exp:对象的属性
  21. * (传递这个exp固定参数也是因为没有模板编译相关的代码,所以就暂时写死一个属性)
  22. */
  23. function Vue(data, el, exp){
  24. this.data = data;
  25. this.el = el;
  26. // 因为没有模板相关的代码,所以{{text}}的值使用这种方式进行解析
  27. this.innerHTML = this.data[exp];
  28. }
  29.  
  30. var data = {
  31. text: 'hello Vue'
  32. };
  33. var el = document.getElementById('box');
  34.  
  35. var vm = new Vue(data, el);
  36. </script>
  37. </html>

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

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

  

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

  1. /*
  2. * data: 数据
  3. * el: 元素
  4. * exp:对象的属性
  5. */
  6. function Vue(data, el, exp){
  7. this.data = data;
  8. this.el = el;
  9. this.exp = exp;
  10.  
  11. // 因为没有模板相关的代码,所以{{text}}的值使用这种方式进行解析
  12. this.el.innerHTML = this.data[exp];
  13.  
  14. //初始化vue实例需要将data数据变得可观测
  15. new Observer(data);
  16. }

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

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

  1. /*
  2. * data: 数据
  3. * el: 元素
  4. * exp:对象的属性
  5. */
  6. function Vue(data, el, exp){
  7. this.data = data;
  8. this.el = el;
  9. this.exp = exp;
  10.  
  11. // 因为没有模板相关的代码,所以{{text}}的值使用这种方式进行解析
  12. this.el.innerHTML = this.data[exp];
  13.  
  14. //初始化vue实例需要将data数据变得可观测
  15. new Observer(data);
  16.  
  17. this.cb = function(newVal){
  18. this.el.innerHTML = newVal;
  19. }
  20. // 创建一个订阅者
  21. new Watcher(this, exp, this.cb);
  22. }

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

  

  这些操作完成后,最后一步就是修改data.text的数据,如果修改完成后,div的内容发生变化,就证明我们这份代码已经成功运行了。

  那修改data.text数据的逻辑我借用一个button来实现:监听button的click事件,触发时将data.text的值改为"hello new vue"。

  1. <html>
  2. <head>
  3. <meta charset="utf-8" />
  4. <title>一起学习Vue源码-Object的变化侦测</title>
  5. </head>
  6. <body>
  7. <h1>一起学习Vue源码-Object的变化侦测</h1>
  8. <div id="box">
  9. {{text}}
  10. </div>
  11. <br/>
  12. <button onclick="btnClick()">点击我改变div的内容</button>
  13. </body>
  14. <script type="text/javascript" src="./Dep.js"></script>
  15. <script type="text/javascript" src="./Observer.js"></script>
  16. <script type="text/javascript" src="./Watcher.js"></script>
  17.  
  18. <script>
  19. /*
  20. * data: 数据
  21. * el: 元素id
  22. * exp:对象的属性
  23. * (传递这个exp固定参数也是因为没有模板编译相关的代码,所以就暂时写死一个属性)
  24. * cb: 真正包含数据更新逻辑的函数
  25. */
  26. function Vue(data, el, exp){
  27. this.data = data;
  28. this.el = el;
  29. this.exp = exp;
  30. // 因为没有模板相关的代码,所以{{text}}的值使用这种方式进行解析
  31. this.el.innerHTML = this.data[exp];
  32. this.cb = function(newVal){
  33. this.el.innerHTML = newVal;
  34. }
  35. //初始化vue实例需要将data数据变得可观测
  36. new Observer(data);
  37. //创建一个订阅者
  38. new Watcher(this, exp, this.cb);
  39. }
  40. var data = {
  41. text: 'hello Vue'
  42. };
  43. var el = document.getElementById('box');
  44.  
  45. var exp = 'text';
  46.  
  47. var vm = new Vue(data, el, exp);
  48.  
  49. function btnClick(){
  50. vm.data.text = "hello new vue";
  51. }
  52. </script>
  53. </html>

  我们看一下效果。

  

  

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

  到此,这篇 "一起学习vue源码 - Object的变化侦测" 总结完成。 

  结束语:

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

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

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

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

  

作者:小土豆biubiubiu

博客园:www.cnblogs.com/HouJiao/

掘金:https://juejin.im/user/58c61b4361ff4b005d9e894d

简书:https://www.jianshu.com/u/cb1c3884e6d5

微信公众号:土豆妈的碎碎念(扫码关注,一起吸猫,一起听故事,一起学习前端技术)

码字不易,点赞鼓励哟~

一起学习vue源码 - Object的变化侦测的更多相关文章

  1. 手牵手,从零学习Vue源码 系列二(变化侦测篇)

    系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 陆续更新中... 预计八月中旬更新完毕. 1 概述 Vue最大的特点之一就是数据驱动视 ...

  2. 学习 vue 源码 -- 响应式原理

    概述 由于刚开始学习 vue 源码,而且水平有限,有理解或表述的不对的地方,还请不吝指教. vue 主要通过 Watcher.Dep 和 Observer 三个类来实现响应式视图.另外还有一个 sch ...

  3. 手牵手,从零学习Vue源码 系列一(前言-目录篇)

    系列文章: 手牵手,从零学习Vue源码 系列一(前言-目录篇) 手牵手,从零学习Vue源码 系列二(变化侦测篇) 手牵手,从零学习Vue源码 系列三(虚拟DOM篇) 陆续更新中... 预计八月中旬更新 ...

  4. 一起学习vue源码 - Vue2.x的生命周期(初始化阶段)

    作者:小土豆biubiubiu 博客园:https://www.cnblogs.com/HouJiao/ 掘金:https://juejin.im/user/58c61b4361ff4b005d9e8 ...

  5. 学习Vue源码前的几项必要储备(二)

    7项重要储备 Flow 基本语法 发布/订阅模式 ES6+ 语法 原型链.闭包 函数柯里化 event loop 接上讲 聊到了ES6的几个重要语法,加下来到第四点继续开始. 4.原型链.闭包 原型链 ...

  6. 学习Vue源码前的几项必要储备(一)

    从接下来的一段时间里,Mg要进行阅读源码的工作.再阅读源码前,梳理一下准备工作. 7项重要储备 Flow 基本语法 发布/订阅模式 ES6+ 语法 原型链.闭包 函数柯里化 event loop 1. ...

  7. Vue源码学习(一):调试环境搭建

    最近开始学习Vue源码,第一步就是要把调试环境搭好,这个过程遇到小坑着实费了点功夫,在这里记下来 一.调试环境搭建过程 1.安装node.js,具体不展开 2.下载vue项目源码,git或svn等均可 ...

  8. 【Vuejs】350- 学习 Vue 源码的必要知识储备

    前言 我最近在写 Vue 进阶的内容.在这个过程中,有些人问我看 Vue 源码需要有哪些准备吗?所以也就有了这篇计划之外的文章. 当你想学习 Vue 源码的时候,需要有扎实的 JavaScript 基 ...

  9. Vue源码详细解析:transclude,compile,link,依赖,批处理...一网打尽,全解析!

    用了Vue很久了,最近决定系统性的看看Vue的源码,相信看源码的同学不在少数,但是看的时候却发现挺有难度,Vue虽然足够精简,但是怎么说现在也有10k行的代码量了,深入进去逐行查看的时候感觉内容庞杂并 ...

随机推荐

  1. BZOJ3566 [SHOI2014]概率充电器 (树形DP&概率DP)

    3566: [SHOI2014]概率充电器 Description 著名的电子产品品牌 SHOI 刚刚发布了引领世界潮流的下一代电子产品——概率充电器:“采用全新纳米级加工技术,实现元件与导线能否通电 ...

  2. 算法之匹配:KMP

    public static int getIndexOf(String str1, String str2) { if (str1 == null || str2 == null || str1.le ...

  3. MergeSort(归并排序)原理及C++代码实现

    归并排序利用分治策略进行排序.原理如下 分解:分解待排的n个元素的序列成个具n/2个元素的两个子序列. 解决:使用归并排序递归地排序两个子序列. 合并:合并两个已排序的子序列以产生已排序的答案. 归并 ...

  4. SQL语言:DDL,DML,DCL,DQL,TCL

    DDL(Data Definition Language)数据库定义语言 statements are used to define the database structure or schema. ...

  5. SHELL小练习

    1.SHELL编程服务器IP修改脚本 脚本实现动态IP修改: 脚本实现静态IP修改: 实现IP地址输入判断正确性: IP地址修改成功判断&回滚: 2.SHELL编程Tomcat多实例管理脚本( ...

  6. 对《java程序员上班那点事》笔者对数组占用内存质疑

    1.<java程序员上班那点事>笔者对数组占用内存的描述 2.实际测试情况: /** * 测试一维数组占用内存 */ public static void testOneArray() { ...

  7. mybatis generator 使用教程(生成带注释的实体类)

    引言: 最近的一个项目,由于数据库表巨多,导致需要创建N多个java实体.dao.mapper.xml映射文件,如果均使用纯手工编写,无疑需要耗费大量时间和精力.于是上网学习了mybatis gene ...

  8. 【待填坑】LG_2467_[SDOI2010]地精部落

    不错的dp题...思维难度和码量成反比...

  9. python&&Java&&jsp+servlet连接数据库报错收藏(sql server,mysql)

    写在最前面:sql server和mysql 是不同的东西... 我在完成java连接数据库的时候把这俩当成一个东西,结果下的mysql的jar包. 但自己的sql是server.. 然后尝试用 py ...

  10. redis保存dataset

    公司统一走redis缓存,也将之前的memcache迁移到redis碰到问题是redis的dataset缓存. memcache底层封装了dataset的序列化. 而redis引的DLL包,未支持.所 ...