当前位置: 七九推 > IT编程>软件设计>设计模式 > 理解vue数据驱动

理解vue数据驱动

2022年06月23日 设计模式 我要评论
vue是双向数据绑定的框架,数据驱动是他的灵魂,他的实现原理众所周知是object.defineproperty方法实现的get、set重写,但是这样说太牵强外门了。本文将宏观介绍他的实现分析obje

vue是双向数据绑定的框架,数据驱动是他的灵魂,他的实现原理众所周知是object.defineproperty方法实现的get、set重写,但是这样说太牵强外门了。本文将宏观介绍他的实现

使用vue

举个非常简单的栗子

# html
<div id="#app">
  {{msg}}
</div>

# script
<script>
new vue({
  el: '#app',
  data: {
    msg: 'hello'
  },
  mounted() {
    settimeout(() => {
      this.msg = 'hi'
    }, 1000);
  }
})
</script>

上面代码, new vue进行创建vue对象, el属性是挂载的dom选择器,这里选择id为app的dom,data对象保存这所有数据响应的属性,当其中的某一属性值改变,就触发view渲染,从而实现了“数据->视图”的动态响应;

示例中msg初始值为hello,因此页面渲染时为hello,一秒之后,msg变为了hi,触发了view渲染,我们看到hello变为了li。那么接下来就从这简单的栗子来讲解vue的数据驱动把。

分析object.defineproperty

我们说vue是怎么实现双向数据绑定的?是object.defineproperty实现了,那么我们就直接聚焦object.defineproperty

以下是代码

function definereactive (
  obj,
  key,
  val,
  customsetter,
  shallow
) {
  // 创建派发器
  var dep = new dep();

  var property = object.getownpropertydescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  var childob = !shallow && observe(val);
  object.defineproperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactivegetter () {
      var value = getter ? getter.call(obj) : val;
      // 收集依赖对象
      if (dep.target) {
        dep.depend();
        if (childob) {
          childob.dep.depend();
          if (array.isarray(value)) {
            dependarray(value);
          }
        }
      }
      return value
    },
    set: function reactivesetter (newval) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newval === value || (newval !== newval && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if ("development" !== 'production' && customsetter) {
        customsetter();
      }
      if (setter) {
        setter.call(obj, newval);
      } else {
        val = newval;
      }
      childob = !shallow && observe(newval);
      dep.notify();
    }
  });
}

vue在给每一个data的属性执行definereactive函数,来达到数据绑定的目的。从代码中可以看到几点:

  1. 每一个数据绑定,都会new一个dep(暂且叫他派发器),派发器的功能是什么?依赖收集以事件分发;
  2. 在属性get中,除了获取当前属性的值,还做了dep.depend()操作;
  3. dep.depend的目的是什么?看dep部分代码,很简单,其实就是依赖收集,将dep.target需要收集的依赖进行添加到自己的派发器里
  4. 在属性set时,就是给属性改变值时,除了改变值意外,还执行了dep.notify()操作;
  5. dep.notify的目的又是什么?看代码,依旧很简单,将自己派发器的所有依赖触发update函数;

这一部分很容易了解,在data的属性get时,触发了派发器的依赖收集(dep.depend),在data的属性set时,触发了派发器的事件通知(dep.notify);

结合已知知识,vue的数据绑定是上面这个函数带来的副作用,因此可以得出结论:

  1. 当我们改变某个属性值时,派发器dep通知了view层去更新
  2. dep.target是派发器dep收集的依赖,并在属性值改变时触发了update函数,view层的更新与dep.target有必然的联系。换句话说:数据->视图的数据驱动就等于dep.target.update()

简单的源码解析

上一节已经确定,当更改属性值时,是dep.target.update更新了view,因此带着这个目的,此小节做一个简单的源码解析

一切从头开始

function vue (options) {
  this._init(options);
}

vue.prototype._init = function (options) {
  var vm = this;
  callhook(vm, 'beforecreate');
  initstate(vm);
  callhook(vm, 'created');

  if (vm.$options.el) {
    vm.$mount(vm.$options.el);
  }
};

function initstate (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.data) {
    initdata(vm);
  } else {
    observe(vm._data = {}, true /* asrootdata */);
  }
}

function initdata (vm) {
  var data = vm.$options.data;
  observe(data, true /* asrootdata */);
}

function observe (value, asrootdata) {
  if (!isobject(value) || value instanceof vnode) {
    return
  }
  var ob = new observer(value);;
  return ob
}

从头开始,一步一步进入,发现最终我们new vue传进来的data进入了new observer中;

数据驱动部分-观察者

var observer = function observer (value) {
  this.value = value;
  this.dep = new dep();
  this.vmcount = 0;
  def(value, '__ob__', this);
  if (array.isarray(value)) {
    var augment = hasproto
      ? protoaugment
      : copyaugment;
    augment(value, arraymethods, arraykeys);
    this.observearray(value);
  } else {
    this.walk(value);
  }
};
observer.prototype.walk = function walk (obj) {
  var keys = object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    definereactive(obj, keys[i]);
  }
};

observer构造函数中,最终执行了definereactive为每一个属性进行定义,并且是递归调用,以树型遍历我们传入的data对象的所有节点属性,每一个节点都会被包装为一个观察者,当数据get时,进行依赖收集,当数据set时,事件分发。

看到这里,感觉好像少了点什么,好像data到这里就结束了,但是并没有看懂为什么数据改变更新视图的,那么继续往下看

vue挂载到dom

回看的_init方法,在这个方法中,最后调用了vm.$mount(vm.$options.el),这是把vm挂载到真实dom,并渲染view的地方,因此接着看下去。

vue.prototype.$mount = function (
  el,
  hydrating
) {
  return mountcomponent(this, el, hydrating)
};
// 渲染dom的真实函数
function mountcomponent (
  vm,
  el,
  hydrating
) {
  vm.$el = el;
  callhook(vm, 'beforemount');

  var updatecomponent;
  updatecomponent = function () {
    vm._update(vm._render(), hydrating);
  };
  // new 一个watcher,开启了数据驱动之旅
  new watcher(vm, updatecomponent, noop, {
    before: function before () {
      if (vm._ismounted) {
        callhook(vm, 'beforeupdate');
      }
    }
  }, true /* isrenderwatcher */);
  hydrating = false;
  if (vm.$vnode == null) {
    vm._ismounted = true;
    callhook(vm, 'mounted');
  }
  return vm
}

上面部分看到的是,vue将vue对象挂载到真实dom的经历,最终执行了new watcher,并且回调为vm._update(vm._render(), hydrating)。顾名思义,这里是执行了vue的更新view的操作(本文暂且不讲更新view,在其他文章已经讲过。本文专注数据驱动部分)。

问:为什么说new watcher开启了数据驱动之旅呢?watcher又是什么功能?

简述watcher

如果说object.defineproperty是vue数据驱动的灵魂,那么watcher则是他的骨骼。

// 超级简单的watcher
var watcher = function watcher (
  vm,
  exporfn,
  cb,
  options,
  isrenderwatcher
) {
  this.cb = cb;
  this.deps = [];
  this.newdeps = [];
  // 计算属性走if
  if (this.computed) {
    this.value = undefined;
    this.dep = new dep();
  } else {
    this.value = this.get();
  }
};

watcher.prototype.get = function get () {
  pushtarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleerror(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    poptarget();
    this.cleanupdeps();
  }
  return value
};

简化后watcher在new时,最终会调用自己的get方法,get方法中第一个语句pushtarget(this)是开启数据驱动的第一把钥匙,看下文

function pushtarget (_target) {
  if (dep.target) { targetstack.push(dep.target); }
  dep.target = _target;
}

pushtarget将传入的watcher对象赋值给了dep.target,还记得在讲object.defineproperty时提到了,dep.target.update是更新view的触发点,在这里终于找到了!

下面看dep.targe.update

watcher.prototype.update = function update () {
  var this$1 = this;

  /* istanbul ignore else */
  if (this.computed) {
    if (this.dep.subs.length === 0) {
      this.dirty = true;
    } else {
      this.getandinvoke(function () {
        this$1.dep.notify();
      });
    }
  } else if (this.sync) {
    this.run();
  } else {
  // update执行了这里
    queuewatcher(this);
  }
};

我们看到update方法最后执行了queuewatcher,继续看下去发现,这其实是一个更新队列,vue对同一个微任务的所有update进行了收集更新,最终执行了watcher.run,run方法又执行了getandinvoke方法,getandinvoke又执行了this.get方法。

到来一大圈,终于找到:在改变属性值时,触发了dep.target所对应的watcher的 this.get方法,this.get方法其实就是传入进来的回调函数。回想前面介绍的,vue在挂载到真实dom时,new watcher传入的回调是updatecomponent。串联起来得到了结论:

  1. 我们在get属性时,dep派发器收集到了watcher当作依赖
  2. 当我们set属性时,dep派发器事件分发,使所有收集到的依赖执行this.get,这时候view会更新。

到这里,有没有明白为什么所有属性的派发器都会收集updatecomponent的watcher,从而在自己set时通知更新?如果没明白,那就看下一节分析

从宏观角度看问题

  1. 当我们new vue时,首先会将传入的data将被vue包装为观察者,所有get和set行为都会捕捉到并执行响应的操作
  2. 接下来vue会将vue对象挂载到真实dom(其实指的虚拟的渲染),这个时候new 一个watcher, 在new watcher时,会执行一次this.get初始化一次值,对标updatecomponent函数,这个时候会触发vue渲染过程
  3. 在vue渲染过程中,所有的数据都需要进行get行为才能得到值并给真实dom赋值,因此这时触发了所有data属性的get,并且此时dep.target是updatecomponent的watcher,因此所有的data属性派发器都收集到了此watcher,在set时,派发器notify进行事件分发,收集到的依赖watcher都得到了通知进行update,所有又会执行updatecompoent进行更新view

通过案例进行分析

vue数据驱动的前提

vue数据驱动是有前提条件的,不是怎么用都可以的,前提条件就是必须在data中声明的属性才会参与数据驱动,数据->视图。看下面栗子

有如下html:

<div id="app">
    <div>{{prev}}{{next}}</div>
</div>

如下js:

new vue({
  el: "#app",
  data: {
    prev: 'hello',
  },
  created() {
  },
  mounted() {
    this.next = 'world';
  }
})

页面渲染的结果是什呢?

答:hello;

为什么this.next明明赋值,没有渲染到view中去?因为他并没有参与数据驱动的观察者,还记得前面讲到vue会把传入的data对象深度遍历包装为观察者来吧,这里next属性并没有被成为观察者,因此并不会引发view更新。

看到的未必真实的

为什么看到的未必真实的,上面的栗子我们发现,view中看到的只有hello,但是数据真的是有hello么?未必,看下面栗子。

new vue({
  el: "#app",
  data: {
    prev: 'hello',
  },
  created() {
  },
  mounted() {
    this.next = 'world';
    settimeout(() => {
      this.prev = 'hi';
      
    }, 1000);
  }
})

这个代码比上面栗子就多了3行代码,再页面渲染1秒后,改变prev的值为hi,那么页面会展现什么效果呢?

答:hi world

从这里可以看到,虽然next赋值并没有引起view更新,但是data确实成功变更了,当prev改变时,触发了update,从而将view更新,此时next有值,因此就显示在了view中。这就是很多初学者会遇到为什么明明赋值没有显示,但是点了一下其他的东西,却显示了的问题。

看到的未必真实2

还是根据第一个栗子引申一个案例如下:

new vue({
  el: "#app",
  data: {
    prev: 'hello',
  },
  created() {
    this.next = 'world';
  },
  mounted() {
    settimeout(() => {
      this.next = 'memory'
    }, 1000)
  }
})

我们在created生命周期中赋值next,在mounted生命周期延迟一秒改变next的值,那结果会这样?

答:永远显示helloworld

如果已经掌握了vue实例化过程的同学可能已经猜到了为什么

当created生命周期执行时,此时还没有做vnode转化为真实dom的操作,此时data属性已经代理到this下,因此修改this.next就修改了data对象的值,data就变为了{prev: 'hello', next: 'world'},因此在render时就将next也渲染到了页面上

另外此时已经完成了数据驱动的灵魂步骤(将data遍历包装为观察者),因此在延迟1s后改变next值,仍然跟栗子2一样不会引起view更新的。

因此,写vue出现以上改变data时view未更新,首先要检查自己的代码,而不是怀疑vue框架的问题。。

注意事项

  • 本文参考vue版本v2.5.17-beta.0
  • 本文专注数据驱动主线data,computed等支线并没有介绍,因此贴的代码都做了大量删减
  • data属性收集到的依赖dep.target并不止updatecomponent的watcher,还可能多个,比如computed属性

附加讨论

有些面试官会问在异步获取数据并改变data值时,放在created还是mounted?

我感觉没什么可答的,2个都没问题,当然对于代码优化,放在created更早的发出请求,因此放在created里更合适。

(0)
打赏 微信扫一扫 微信扫一扫

相关文章:

  • 23种设计模式概述

    23种设计模式概述

    设计模式共有23种,创建型模式5种,结构型模式7种,行为型模式11种,本章只做概念性说明。 ࿰... [阅读全文]
  • 设计模式 - 策略模式

    设计模式 - 策略模式

    前言:  先不管模式, 把他和他的名字都忘了, 来看看问题 和 设计思路. 为啥要这么做.场景:  有一家店铺, 里面有一个售货员, 售货员当然是要卖东西的啦,... [阅读全文]
  • 大话设计模式笔记(三)の六大原则

    大话设计模式笔记(三)の六大原则

    单一职责原则英文:single responsibility principle,简称srp。定义就一个类而言,应该仅有一个引起它变化的原因。描述如果一个类承担... [阅读全文]
  • 设计模式之工厂模式(三)

    上一次我们已经通过代码,简单的认识了工厂方法模式,具体的思路请移步到,进行查看。这次,让我们通过设计模式的思想,来好好认识下工厂方法模式。创建者和产品所有工厂模式都用来封装对象的创…

    2022年06月23日 软件设计
  • 创建型模式之工厂模式(2.2)

    创建型模式之工厂模式(2.2)

    什么是工厂模式?工厂模式是创建型模式的一种,工厂是用来生产的,而在java里,是用来生产对象实例的。和单例模式相似,工厂模式同样聚焦于在考虑整个软件构建的情况下... [阅读全文]
  • 设计模式--Proxy模式

    设计模式--Proxy模式

    这篇主要介绍代理模式相关内容,主要是一些基本概念普及。代理模式1、什么是代理模式?代理模式(proxy),为其他对象提供一种代理以控制对这个对象的访问。[dp]... [阅读全文]

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2022  七九推 保留所有权利. 粤ICP备17035492号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com