根据流程图实现一个MVVM:

如上图所示,我们可以看到,整体实现分为四步:

1、实现一个Compile,对指令进行解析,初始化视图,并且订阅数据的变更,绑定好更新函数

2、实现一个Observer,对数据进行劫持,通知数据的变化

3、实现一个Watcher,将其作为以上两者的一个中介点,在接收数据变更的同时,让Dep添加当前Watcher,并及时通知视图进行update。

4、实现MVVM,整合以上三者,作为一个入口函数

流程解读:

第一步:创建MVVM、Compile类,并且利用createDocumentFragment将<div id="app"></div>下的标签放到JS文档碎片中去。

第二步:对 标签 进行编译,将带有 v- 指令的标签和{{ }}的标签解析出来

第三步:创建Observer类进行数据劫持、深度递归劫持,当data中设置值或者修改值的时候,利用Object.defineProperty对值进行监控。

第四步:创建Watch类观察者,用新值和老值进行比对,如果发生变化,就调用更新方法,进行视图更新。

第五步:将输入框v-model和视图绑定起来,输入框的值变化,同时页面中通过{{}}绑定的值也变化,实现双向数据绑定。

vue.js 则是采用数据劫持结合发布者-订阅者模式的方式

通过Object.defineProperty()来劫持各个属性的setter,getter

在数据变动时发布消息给订阅者,触发相应的监听回调。

我们先来看Object.defineProperty()这个方法:

  1. var obj = {};
  2. Object.defineProperty(obj, 'name', {
  3. get: function() {
  4. console.log('我被获取了')
  5. return val;
  6. },
  7. set: function (newVal) {
  8. console.log('我被设置了')
  9. }
  10. })
  11. obj.name = '张三';//在给obj设置name属性的时候,触发了set这个方法
  12. var val = obj.name;//在得到obj的name属性,会触发get方法

vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持

那么在设置或者获取的时候我们就可以在get或者set方法里假如其他的触发函数,达到监听数据变动的目的

通过Object.defineProperty()可以实现数据劫持,是的属性在赋值的时候触发set方法

  1. <!doctype html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Document</title>
  6. </head>
  7. <body>
  8. <div id="demo"></div>
  9. <input type="text" id="inp">
  10. <script>
  11. var obj = {};
  12. var demo = document.querySelector('#demo')
  13. var inp = document.querySelector('#inp')
  14. Object.defineProperty(obj, 'name', {
  15. get: function() {
  16. return val;
  17. },
  18. set: function (newVal) {//当该属性被赋值的时候触发
  19. inp.value = newVal;
  20. demo.innerHTML = newVal;
  21. }
  22. })
  23. inp.addEventListener('input', function(e) {
  24. // 给obj的name属性赋值,进而触发该属性的set方法
  25. obj.name = e.target.value;
  26. });
  27. obj.name = 'fei';//在给obj设置name属性的时候,触发了set这个方法
  28. </script>
  29. </body>
  30. </html>

当然要是这么粗暴,肯定不行,性能会出很多的问题

原理图:

observer用来实现对每个vue中的data中定义的属性循环用Object.defineProperty()实现数据劫持

以便利用其中的setter和getter,然后通知订阅者,订阅者会触发它的update方法,对视图进行更新。

在vue中v-model,v-name,{{}}等都可以对数据进行显示,也就是说假如一个属性都通过这三个指令了,那么每当这个属性改变的时候,相应的这个三个指令的html视图也必须改变

于是vue中就是每当有这样的可能用到双向绑定的指令,就在一个Dep中增加一个订阅者,其订阅者只是更新自己的指令对应的数据

也就是v-model=’name’和{{name}}有两个对应的订阅者,各自管理自己的地方。每当属性的set方法触发,就循环更新Dep中的订阅者。

  1. function defineReactive (obj, key, val) {
  2. var dep = new Dep();
  3. Object.defineProperty(obj, key, {
  4. get: function() {
  5. //添加订阅者watcher到主题对象Dep
  6. if(Dep.target) {
  7. // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
  8. dep.addSub(Dep.target);
  9. }
  10. return val;
  11. },
  12. set: function (newVal) {
  13. if(newVal === val) return;
  14. val = newVal;
  15. console.log(val);
  16. // 作为发布者发出通知
  17. dep.notify();//通知后dep会循环调用各自的update方法更新视图
  18. }
  19. })
  20. }
  21. function observe(obj, vm) {
  22. Object.keys(obj).forEach(function(key) {
  23. defineReactive(vm, key, obj[key]);
  24. })
  25. }
  1. function Compile(node, vm) {
  2. if(node) {
  3. this.$frag = this.nodeToFragment(node, vm);
  4. return this.$frag;
  5. }
  6. }
  7. Compile.prototype = {
  8. nodeToFragment: function(node, vm) {
  9. var self = this;
  10. var frag = document.createDocumentFragment();
  11. var child;
  12. while(child = node.firstChild) {
  13. console.log([child])
  14. self.compileElement(child, vm);
  15. frag.append(child); // 将所有子节点添加到fragment中
  16. }
  17. return frag;
  18. },
  19. compileElement: function(node, vm) {
  20. var reg = /\{\{(.*)\}\}/;
  21. //节点类型为元素(input元素这里)
  22. if(node.nodeType === 1) {
  23. var attr = node.attributes;
  24. // 解析属性
  25. for(var i = 0; i < attr.length; i++ ) {
  26. if(attr[i].nodeName == 'v-model') {//遍历属性节点找到v-model的属性
  27. var name = attr[i].nodeValue; // 获取v-model绑定的属性名
  28. node.addEventListener('input', function(e) {
  29. // 给相应的data属性赋值,进而触发该属性的set方法
  30. vm[name]= e.target.value;
  31. });
  32. new Watcher(vm, node, name, 'value');//创建新的watcher,会触发函数向对应属性的dep数组中添加订阅者,
  33. }
  34. };
  35. }
  36. //节点类型为text
  37. if(node.nodeType === 3) {
  38. if(reg.test(node.nodeValue)) {
  39. var name = RegExp.$1; // 获取匹配到的字符串
  40. name = name.trim();
  41. new Watcher(vm, node, name, 'nodeValue');
  42. }
  43. }
  44. }
  45. }
  1. function Watcher(vm, node, name, type) {
  2. Dep.target = this;
  3. this.name = name;
  4. this.node = node;
  5. this.vm = vm;
  6. this.type = type;
  7. this.update();
  8. Dep.target = null;
  9. }
  10. Watcher.prototype = {
  11. update: function() {
  12. this.get();
  13. this.node[this.type] = this.value; // 订阅者执行相应操作
  14. },
  15. // 获取data的属性值
  16. get: function() {
  17. console.log(1)
  18. this.value = this.vm[this.name]; //触发相应属性的get
  19. }
  20. }
  1. function Dep() {
  2. this.subs = [];
  3. }
  4. Dep.prototype = {
  5. addSub: function(sub) {
  6. this.subs.push(sub);
  7. },
  8. notify: function() {
  9. this.subs.forEach(function(sub) {
  10. sub.update();
  11. })
  12. }
  13. }

首先我们为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep

然后在编译的时候在该属性的数组dep中添加订阅者,v-model会添加一个订阅者,{{}}也会,v-bind也会,只要用到该属性的指令理论上都会

接着为input会添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep

订阅者数组循环调用各订阅者的update方法更新视图。

版权声明:本文为pp-yang原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/pp-yang/p/12520898.html