• vue 之 双向绑定原理


    一、实现双向绑定

      

         详细版:

      

      前端MVVM实现双向数据绑定的做法大致有如下三种:

    1.发布者-订阅者模式(backbone.js)

    思路:使用自定义的data属性在HTML代码中指明绑定。所有绑定起来的JavaScript对象以及DOM元素都将“订阅”一个发布者对象。任何时候如果JavaScript对象或者一个HTML输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。

     vueJS 的思路流程:发布者dep发出通知 => 主题对象subs收到通知并推送给订阅者 => 订阅者watcher执行相应操作

    2.脏值检查(angular.js)

    思路:angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,angular只有在指定的事件触发时进入脏值检测,大致如下:

    • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )

    • XHR响应事件 ( $http )

    • 浏览器Location变更事件 ( $location )

    • Timer事件( $timeout , $interval )

    • 执行 $digest() 或 $apply()

    3.数据劫持(Vue.js)

    思路: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的settergetter,在数据变动时发布消息给订阅者,触发相应的监听回调。

    Object.defineProperty
    作用定义:直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
    Object.defineProperty(obj, prop, descriptor)
    参数 obj => 要在其上定义属性的对象;
       prop => 要定义或修改的属性的名称;
       descriptor => 将被定义或修改的属性描述符。

      属性描述符 => 数据描述符和存取描述符,两者取一
        数据描述符: 具有值的属性
        存取描述符: 由getter-setter函数对描述的属性

        具有的属性:

        

                注:configurable 可配置性相当于属性的总开关,只有为true时才能设置,而且不可逆

                  enumerable  是否可枚举,为false时for..in以及Object.keys()将不能枚举出该属性

                  writable 是否可写,为false时将不能够修改属性的值

          get 一个给属性提供 getter 的方法

          set 一个给属性提供 setter 的方法

    返回值  被传递给函数的对象obj。

    示例

            var obj = {};
            Object.defineProperty(obj, 'hello', {
                get: function() {
                    console.log('get val:'+ val);
                    return val;
               },
              set: function(newVal) {
                    val = newVal;
                    console.log('set val:'+ val);
                }
            });
    
            obj.hello;  // 触发 getter =>get val:undefined 
            obj.hello='111'; // 触发 setter =>set val:111
            obj.hello;  // 触发 getter =>get val:111    
    View Code

     vue 实现双向绑定

    实现mvvm的双向绑定的步骤:
    1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
    2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
    3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
    4、mvvm入口函数,整合以上三者

    流程图

    流程解析:

    从图中可以看出,当执行 new Vue() 时,Vue 就进入了初始化阶段,一方面Vue 会遍历 data 选项中的属性,并用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能;另一方面,Vue 的指令编译器Compile 对元素节点的指令进行解析,初始化视图,并订阅Watcher 来更新视图, 此时Wather 会将自己添加到消息订阅器中(Dep),初始化完毕。当数据发生变化时,Observer 中的 setter 方法被触发,setter 会立即调用Dep.notify(),Dep 开始遍历所有的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新。因为VUE使用Object.defineProperty方法来做数据绑定,而这个方法又无法通过兼容性处理,所以Vue 不支持 IE8 以及更低版本浏览器。另外,查看vue原代码,发现在vue初始化实例时, 有一个proxy代理方法,它的作用就是遍历data中的属性,把它代理到vm的实例上,这也就是我们可以这样调用属性:vm.a等于vm.data.a。

    Observer

    利用Obeject.defineProperty()来监听属性变动,将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 settergetter

    当给这个对象的某个值赋值,就会触发setter,进而监听到数据变化

    监听到变化之后通知订阅者,需要实现一个消息订阅器Dep,通过维护一个数组subs,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法

    流程图:

    完整代码

     1 function Observer(data) {
     2     this.data = data;
     3     this.walk(data);
     4 }
     5 
     6 Observer.prototype = {
     7     walk: function(data) {
     8         var me = this;
     9         Object.keys(data).forEach(function(key) {
    10             me.convert(key, data[key]);
    11         });
    12     },
    13     convert: function(key, val) {
    14         this.defineReactive(this.data, key, val);
    15     },
    16 
    17     defineReactive: function(data, key, val) {
    18         var dep = new Dep();
    19         var childObj = observe(val);
    20 
    21         Object.defineProperty(data, key, {
    22             enumerable: true, // 可枚举
    23             configurable: false, // 不能再define
    24             get: function() {
    25                 // 添加订阅者watcher到主题对象Dep
    26                 if (Dep.target) {
    27                     dep.depend();
    28                 }
    29                 return val;
    30             },
    31             set: function(newVal) {
    32                 if (newVal === val) {
    33                     return;
    34                 }
    35                 val = newVal;
    36                 // 新的值是object的话,进行监听
    37                 childObj = observe(newVal);
    38                 // 通知订阅者
    39                 dep.notify();
    40             }
    41         });
    42     }
    43 };
    44 
    45 function observe(value, vm) {
    46     if (!value || typeof value !== 'object') {
    47         return;
    48     }
    49 
    50     return new Observer(value);
    51 };
    52 
    53 
    54 var uid = 0;
    55 
    56 function Dep() {
    57     this.id = uid++;
    58     this.subs = [];
    59 }
    60 
    61 Dep.prototype = {
    62     addSub: function(sub) {
    63         this.subs.push(sub);
    64     },
    65 
    66     depend: function() {
    67         Dep.target.addDep(this);
    68     },
    69 
    70     removeSub: function(sub) {
    71         var index = this.subs.indexOf(sub);
    72         if (index != -1) {
    73             this.subs.splice(index, 1);
    74         }
    75     },
    76 
    77     notify: function() {
    78         this.subs.forEach(function(sub) {
    79             sub.update();
    80         });
    81     }
    82 };
    83 
    84 Dep.target = null;
    View Code

      

    Watcher 

    Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
    1、在自身实例化时往属性订阅器(dep)里面添加自己
    2、定义一个update()方法
    3、在Observe中,待属性变动触发dep.notice()发出通知,调用watcher实例自身的update()方法,并触发Compile中绑定的回调

    完整代码:

     1 function Watcher(vm, expOrFn, cb) {
     2     this.cb = cb; // 回调函数
     3     this.vm = vm; // this 调用对象
     4     this.expOrFn = expOrFn; // watch的对象的key
     5     this.depIds = {};
     6     console.log(typeof expOrFn, expOrFn);
     7     if (typeof expOrFn === 'function') { // function
     8         this.getter = expOrFn;
     9     } else { // express
    10         // this.getter 等于 this.parseGetter 的return返回的匿名函数
    11         this.getter = this.parseGetter(expOrFn);
    12     }
    13     // 调用get方法,从而触发getter
    14     // this.get() ==> this.getter.call(this.vm, this.vm) ==> this.parseGetter(expOrFn)
    15     // this.value = parseGetter中return匿名函数的返回值
    16     this.value = this.get();
    17 }
    18 
    19 Watcher.prototype = {
    20     update: function() {
    21         this.run(); // 属性值变化收到通知,每次data属性值变化触发dep.notify()
    22     },
    23     run: function() {
    24         var value = this.get(); // 取到最新值
    25         var oldVal = this.value;
    26         if (value !== oldVal) { // 新值与旧值比较
    27             this.value = value;
    28             this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
    29         }
    30     },
    31     addDep: function(dep) {
    32         // 1. 每次调用run()的时候会触发相应属性的getter
    33         // getter里面会触发dep.depend(),继而触发这里的addDep
    34         // 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已
    35         // 则不需要将当前watcher添加到该属性的dep里
    36         // 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里
    37         // 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性
    38         // 则需要将当前watcher(child.name)加入到新的 child.name 的dep里
    39         // 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
    40         // 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
    41         // 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
    42         // 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
    43         // 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
    44         // 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
    45         // 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher
    46         if (!this.depIds.hasOwnProperty(dep.id)) {
    47             dep.addSub(this);
    48             this.depIds[dep.id] = dep;
    49         }
    50     },
    51     get: function() {
    52         Dep.target = this; // 将当前订阅者指向自己
    53         console.log(Dep.target);
    54         var value = this.getter.call(this.vm, this.vm); // 触发getter,添加自己到属性订阅器中
    55         Dep.target = null; // 添加完毕,重置
    56         console.log(Dep.target);
    57         return value;
    58     },
    59 
    60     parseGetter: function(exp) {
    61         if (/[^w.$]/.test(exp)) return;
    62 
    63         var exps = exp.split('.');
    64         //  this.getter.call(this.vm, this.vm)的第二个this.vm 传入 obj
    65         return function(obj) {
    66             for (var i = 0, len = exps.length; i < len; i++) {
    67                 if (!obj) return;
    68                 obj = obj[exps[i]];
    69             }
    70             return obj;
    71         }
    72     }
    73 };
    View Code

    实例化Watcher的时候,调用get()方法,通过Dep.target = watcher实例 标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcher实例就能收到更新通知

    Compile

     Compile 指令实现,解析指令,模版渲染,更新视图,并将每个指令对应的节点绑定更新函数new Updater(),添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

    因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中;监听数据、绑定更新函数的处理是在compileUtil.bind()这个方法中,通过new Watcher()添加回调来接收数据变化的通知

    流程图:

    完整代码:

      1 function Compile(el, vm) {
      2     this.$vm = vm;
      3     this.$el = this.isElementNode(el) ? el : document.querySelector(el);
      4 
      5     if (this.$el) {
      6         this.$fragment = this.node2Fragment(this.$el);
      7         this.init();
      8         this.$el.appendChild(this.$fragment);
      9     }
     10 }
     11 
     12 Compile.prototype = {
     13     // dom节点 转化为 Fragment文档碎片
     14     node2Fragment: function(el) {
     15         var fragment = document.createDocumentFragment(),
     16             child;
     17 
     18         // 将原生节点拷贝到fragment
     19         while (child = el.firstChild) {
     20             fragment.appendChild(child);
     21         }
     22 
     23         return fragment;
     24     },
     25 
     26     init: function() {
     27         this.compileElement(this.$fragment);
     28     },
     29 
     30     compileElement: function(el) {
     31         var childNodes = el.childNodes,
     32             me = this;
     33 
     34         [].slice.call(childNodes).forEach(function(node) {
     35             var text = node.textContent; // 文本内容
     36             var reg = /{{(.*)}}/; // 匹配{{}}花括号
     37 
     38             if (me.isElementNode(node)) { //节点类型为元素
     39                 me.compile(node);
     40             } else if (me.isTextNode(node) && reg.test(text)) { //节点类型为text
     41                 me.compileText(node, RegExp.$1);
     42             }
     43 
     44             if (node.childNodes && node.childNodes.length) {
     45                 me.compileElement(node);
     46             }
     47         });
     48     },
     49 
     50     // 编译 解析 元素节点
     51     compile: function(node) {
     52         var nodeAttrs = node.attributes,
     53             me = this;
     54 
     55         [].slice.call(nodeAttrs).forEach(function(attr) {
     56             var attrName = attr.name;
     57             if (me.isDirective(attrName)) {
     58                 var exp = attr.value; // 属性值
     59                 var dir = attrName.substring(2); // v-on: 
     60                 if (me.isEventDirective(dir)) { // 事件指令 on
     61                     compileUtil.eventHandler(node, me.$vm, exp, dir);
     62                 } else { // 普通指令
     63                     compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
     64                 }
     65 
     66                 node.removeAttribute(attrName);
     67             }
     68         });
     69     },
     70     // 编译 解析 文本节点
     71     compileText: function(node, exp) {
     72         compileUtil.text(node, this.$vm, exp);
     73     },
     74 
     75     isDirective: function(attr) {
     76         return attr.indexOf('v-') == 0;
     77     },
     78 
     79     isEventDirective: function(dir) {
     80         return dir.indexOf('on') === 0;
     81     },
     82 
     83     isElementNode: function(node) {
     84         return node.nodeType == 1;
     85     },
     86 
     87     isTextNode: function(node) {
     88         return node.nodeType == 3;
     89     }
     90 };
     91 
     92 // 指令处理集合
     93 var compileUtil = {
     94     text: function(node, vm, exp) {
     95         this.bind(node, vm, exp, 'text');
     96     },
     97 
     98     html: function(node, vm, exp) {
     99         this.bind(node, vm, exp, 'html');
    100     },
    101 
    102     model: function(node, vm, exp) {
    103         this.bind(node, vm, exp, 'model');
    104 
    105         var me = this,
    106             val = this._getVMVal(vm, exp);
    107         node.addEventListener('input', function(e) {
    108             var newValue = e.target.value;
    109             if (val === newValue) {
    110                 return;
    111             }
    112 
    113             me._setVMVal(vm, exp, newValue);
    114             val = newValue;
    115         });
    116     },
    117 
    118     class: function(node, vm, exp) {
    119         this.bind(node, vm, exp, 'class');
    120     },
    121 
    122     bind: function(node, vm, exp, dir) {
    123         var updaterFn = updater[dir + 'Updater'];
    124         // 初始化 渲染视图
    125         updaterFn && updaterFn(node, this._getVMVal(vm, exp));
    126         // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
    127         new Watcher(vm, exp, function(value, oldValue) {
    128             // 监测到数据变化,更新视图
    129             updaterFn && updaterFn(node, value, oldValue);
    130         });
    131     },
    132 
    133     // 事件处理
    134     eventHandler: function(node, vm, exp, dir) {
    135         var eventType = dir.split(':')[1],
    136             fn = vm.$options.methods && vm.$options.methods[exp];
    137 
    138         if (eventType && fn) {
    139             node.addEventListener(eventType, fn.bind(vm), false);
    140         }
    141     },
    142 
    143     _getVMVal: function(vm, exp) {
    144         var val = vm;
    145         exp = exp.split('.');
    146         exp.forEach(function(k) {
    147             val = val[k];
    148         });
    149         return val;
    150     },
    151 
    152     _setVMVal: function(vm, exp, value) {
    153         var val = vm;
    154         exp = exp.split('.');
    155         exp.forEach(function(k, i) {
    156             // 非最后一个key,更新val的值
    157             if (i < exp.length - 1) {
    158                 val = val[k];
    159             } else {
    160                 val[k] = value;
    161             }
    162         });
    163     }
    164 };
    165 
    166 // 更新函数集合
    167 var updater = {
    168     textUpdater: function(node, value) {
    169         node.textContent = typeof value == 'undefined' ? '' : value;
    170     },
    171 
    172     htmlUpdater: function(node, value) {
    173         node.innerHTML = typeof value == 'undefined' ? '' : value;
    174     },
    175 
    176     classUpdater: function(node, value, oldValue) {
    177         var className = node.className;
    178         className = className.replace(oldValue, '').replace(/s$/, '');
    179 
    180         var space = className && String(value) ? ' ' : '';
    181 
    182         node.className = className + space + value;
    183     },
    184 
    185     modelUpdater: function(node, value, oldValue) {
    186         node.value = typeof value == 'undefined' ? '' : value;
    187     }
    188 };
    View Code

    MVVM

     MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,Compile解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

    完整代码:

     1 function MVVM(options) {
     2     this.$options = options || {};
     3     var data = this._data = this.$options.data;
     4     var me = this;
     5 
     6     // 数据代理
     7     // 实现 vm.xxx -> vm._data.xxx
     8     Object.keys(data).forEach(function(key) {
     9         me._proxyData(key);
    10     });
    11 
    12     // 初始化computed
    13     this._initComputed();
    14 
    15     // 调用observe,监测数据是否变化
    16     observe(data, this);
    17 
    18     // 编译解析指令模板
    19     this.$compile = new Compile(options.el || document.body, this)
    20 }
    21 
    22 MVVM.prototype = {
    23     $watch: function(key, cb, options) {
    24         new Watcher(this, key, cb);
    25     },
    26 
    27     _proxyData: function(key, setter, getter) {
    28         var me = this;
    29         setter = setter ||
    30             Object.defineProperty(me, key, {
    31                 configurable: false,
    32                 enumerable: true,
    33                 get: function proxyGetter() {
    34                     return me._data[key];
    35                 },
    36                 set: function proxySetter(newVal) {
    37                     me._data[key] = newVal;
    38                 }
    39             });
    40     },
    41 
    42     _initComputed: function() {
    43         var me = this;
    44         var computed = this.$options.computed;
    45         if (typeof computed === 'object') {
    46             Object.keys(computed).forEach(function(key) {
    47                 Object.defineProperty(me, key, {
    48                     get: typeof computed[key] === 'function' ?
    49                         computed[key] : computed[key].get,
    50                     set: function() {}
    51                 });
    52             });
    53         }
    54     }
    55 };
    View Code

    数据双向绑定的简单实现 实例

      1 <!DOCTYPE html>
      2   <head></head>
      3   <body>
      4   <div id="app">
      5     <input type="text" id="a" v-model="text">
      6     {{text}}
      7   </div>
      8   <script type="text/javascript">
      9   function Compile(node, vm) {
     10       if(node) {
     11         this.$frag = this.nodeToFragment(node, vm);
     12         console.log('vm===>', vm);
     13         console.log('$frag=>>>',this.$frag) // #document-fragment
     14         return this.$frag;
     15       }
     16     }
     17     Compile.prototype = {
     18       nodeToFragment: function(node, vm) {
     19         var self = this;
     20         var frag = document.createDocumentFragment();
     21         var child;
     22 
     23         
     24         console.log('node=>>>', node) // #app 节点
     25 
     26 
     27         while(child = node.firstChild) {
     28           console.log('child=>>>', child)
     29           self.compileElement(child, vm);
     30           frag.append(child); // 将所有子节点添加到fragment中
     31         }
     32         return frag;
     33       },
     34       compileElement: function(node, vm) {
     35         var reg = /{{(.*)}}/;
     36         console.log('reg===>', reg);
     37         console.log('node.nodeType==>>', node.nodeType);
     38 
     39 
     40         //节点类型为元素
     41         if(node.nodeType === 1) {
     42           var attr = node.attributes;
     43           // 解析属性
     44           for(var i = 0; i < attr.length; i++ ) {
     45             if(attr[i].nodeName == 'v-model') {
     46               var name = attr[i].nodeValue; // 获取v-model绑定的data中的属性名 [text]
     47               console.log('name===>',name);
     48               node.addEventListener('input', function(e) {
     49                 // 给相应的data属性赋值,进而触发该属性的set方法 
     50                 vm[name]= e.target.value;
     51               });
     52               node.value = vm[name]; // 将data的值赋给该node
     53               new Watcher(vm, node, name, 'value');
     54             }
     55           };
     56         }
     57         //节点类型为text
     58         if(node.nodeType === 3) {
     59            console.log('node.nodeValue==>>', node.nodeValue);
     60            console.log(reg.test(node.nodeValue))
     61           if(reg.test(node.nodeValue)) {
     62             var name = RegExp.$1; // 获取匹配到的字符串
     63             name = name.trim();
     64             node.nodeValue = vm[name]; // 将data的值赋给该node
     65             new Watcher(vm, node, name, 'nodeValue');
     66             console.log(vm, node, name)
     67           }
     68         }
     69       },
     70     }
     71     function Dep() {
     72       this.subs = [];
     73     }
     74     Dep.prototype = {
     75       addSub: function(sub) {
     76         this.subs.push(sub);
     77       },
     78       notify: function() {
     79         this.subs.forEach(function(sub) {
     80           sub.update();
     81         })
     82       }
     83     }
     84     // node => dom真实节点
     85     // name => data 属性key
     86     // 
     87     function Watcher(vm, node, name, type) {
     88       Dep.target = this;
     89       this.name = name;
     90       this.node = node;
     91       this.vm = vm;
     92       this.type = type;
     93       this.update();
     94       Dep.target = null;
     95     }
     96 
     97     Watcher.prototype = {
     98       update: function() {
     99         this.get();
    100         // console.log('node, type=>', this.node, this.type);
    101         this.node[this.type] = this.value; // 订阅者执行相应操作
    102       },
    103       // 获取data的属性值
    104       get: function() {
    105         this.value = this.vm[this.name]; //触发相应属性的get
    106         // console.log('value, name=>',this.value, this.name)
    107       }
    108     }
    109     function defineReactive (obj, key, val) {
    110       var dep = new Dep();
    111       Object.defineProperty(obj, key, {
    112         get: function() {
    113           console.log(Dep.target)
    114           // <input type="text" id="a" v-model="text"> => Watcher {name: "text", node: input#a, vm: Vue, type: "value"}
    115           // {{text}} => Watcher {name: "text", node: text, vm: Vue, type: "nodeValue"}
    116            //添加订阅者watcher到主题对象Dep
    117           if(Dep.target) {
    118             // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
    119             dep.addSub(Dep.target);
    120           }
    121           return val;
    122         },
    123         set: function (newVal) {
    124           console.log(newVal === val, val, newVal);
    125           if(newVal === val) return;
    126           val = newVal;
    127           console.log(val);
    128           // 作为发布者发出通知
    129           dep.notify();
    130         }
    131       })
    132     }
    133     function observe(obj, vm) {
    134       Object.keys(obj).forEach(function(key) {
    135         defineReactive(vm, key, obj[key]);
    136       })
    137     }
    138 
    139    function Vue(options) {
    140       this.data = options.data;
    141       var data = this.data;
    142       observe(data, this);
    143       var id = options.el;
    144       // console.log(this)
    145       var dom =new Compile(document.getElementById(id),this);
    146 
    147       // 编译完成后,将dom返回到app中
    148       document.getElementById(id).appendChild(dom);
    149     }
    150     var vm = new Vue({
    151       el: 'app',
    152       data: {
    153         text: 'hello world'
    154       }
    155     });
    156     console.log(vm)
    157   </script>
    158   </body>
    159 </html>
    View Code

    完整代码:https://github.com/136shine/MVVM_ada

    参考:https://www.cnblogs.com/libin-1/p/6893712.html

       https://segmentfault.com/a/1190000006599500

             http://baijiahao.baidu.com/s?id=1596277899370862119&wfr=spider&for=pc

  • 相关阅读:
    根分区/tmp满了,卸载home添加给根分区
    Docker容器技术教程
    使用vscode访问和编辑远程服务器文件
    使用 VS Code 远程连接Linux服务器告别xshell
    Docker安装参考文档记录
    yolov5在Centos系统上部署的环境搭建
    YOLOV5四种网络结构的比对
    k8s部署kube-state-metrics组件
    Kubernetes集群部署Prometheus和Grafana
    Prometheus介绍
  • 原文地址:https://www.cnblogs.com/136asdxxl/p/8448951.html
Copyright © 2020-2023  润新知