<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      vue原理簡介

      寫vue也有一段時間了,對vue的底層原理雖然有一些了解,這里總結一下。

      vue.js中有兩個核心功能:響應式數(shù)據(jù)綁定,組件系統(tǒng)。主流的mvc框架都實現(xiàn)了單向數(shù)據(jù)綁定,而雙向綁定無非是在單向綁定基礎上給可輸入元素添加了change事件,從而動態(tài)地修改model和view。

      1. MVC,MVP,MVVM

      1.1 MVC

      MVC模式將軟件分為下面三個部分

      1.視圖(View):用戶界面
      2.控制器(Controller):業(yè)務邏輯
      3.模型(Model):數(shù)據(jù)保存

      MVC各個部分之間通信的方式如下:

      1.視圖傳送指令到控制器
      2.控制器完成業(yè)務邏輯后要求模型改變狀態(tài)
      3.模型將新的數(shù)據(jù)發(fā)送到視圖,用戶得到反饋

      示意圖如下:

      以上所有通信都是單向的。接受用戶指令的時候,MVC有兩種方式,一種是通過視圖接受指令,然后傳遞給控制器。另一種是用戶直接給控制器發(fā)送指令。

      實際使用中可能更加靈活,下面是以Backbone.js為例說明。

      1.用戶可以向視圖(View)發(fā)送指令(DOM事件),再由View直接要求Model改變狀態(tài)。
      2.用戶也可以向Controller發(fā)送指令(改變URL觸發(fā)hashChange事件),再由Controller發(fā)送給View。
      3.Controller很薄只起到路由作用而View非常厚業(yè)務邏輯都放在View。所以Backbone索性取消了Controller,只保留了Router(路由器)

      MVC模式體現(xiàn)了“關注點分離”這一設計原則,將一個人機交互應用涉及到的功能分為三部分,Model對應應用狀態(tài)和業(yè)務功能的封裝,可以將它理解為同時包含數(shù)據(jù)和行為的領域模型,Model接受Controller的請求并完成相應的業(yè)務處理,在應用狀態(tài)改變的時候可以向View發(fā)出通知。View實現(xiàn)可視化界面的呈現(xiàn)和用戶的交互操作,VIew層可以直接調(diào)用Model查詢狀態(tài),Model也可以在自己狀態(tài)發(fā)生變化的時候主動通知VIew。Controller是Model和View之間的連接器,用于控制應用程序的流程。View捕獲用戶交互操作后直接發(fā)送給Controller,完成相應的UI邏輯,如果涉及業(yè)務功能調(diào)用Controller會調(diào)用Model,修改Model狀態(tài)。Controller也可以主動控制原View或者創(chuàng)建新的View對用戶交互予以響應。

      1.2 MVP

      MVP模式將Controller改名為Presenter,同時改變了通信方向,如下圖:

      1.各部分之間的通信都是雙向的。
      2.視圖(View)和模型(Model)不發(fā)生聯(lián)系,都是通過表現(xiàn)Presenter)傳遞
      3.View非常薄,不部署任何業(yè)務邏輯,稱為被動視圖(Passive View),即沒有任何主動性,而Presenter非常厚,所有邏輯都這里

      MVP適用于事件驅(qū)動的應用架構中,如asp.net web form,windows forms應用。

      1.3 MVVM

      MVVM模式將Presenter層替換為ViewModel,其他與MVP模式基本一致,示意圖如下:

      它和MVP的區(qū)別是,采用雙向綁定視圖層(View)的變動,自動反映在ViewModel,反之亦然。Angular和Vue,React采用這種方式。

      MVVM的提出源于WPF,主要是用于分離應用界面層和業(yè)務邏輯層,WPF,Siverlight都基于數(shù)據(jù)驅(qū)動開發(fā)。

      MVVM模式中,一個ViewModel和一個View匹配,完全和View綁定,所有View中的修改變化,都會更新到ViewModel中,同時VewModel的任何變化都會同步到View上顯示。之所以自動同步是ViewModel中的屬性都實現(xiàn)了observable這樣的接口,也就是說當使用屬性的set方法,會同時觸發(fā)屬性修改的事件,使綁定的UI自動刷新。

      2. 訪問器屬性

      訪問器屬性是一種特殊的屬性,不能再對象中直接定義訪問器屬性,必須通過defineProperty()方法定義訪問器屬

      Object.defineProperty()方法直接在對象上定義一個新屬性,或修改一個對象現(xiàn)有的屬性,并返回這個對象。該方法允許精確添加或者修改對象的屬性。通過賦值操作(例如object.name = xxx)添加的普通屬性是可枚舉的,可枚舉(for ... in或Object.keys方法),這些屬性的值可以被修改或刪除。默認情況下,使用Object.defineProperty()添加的屬性值是不可修改的。 方法的原型如下:

      Object.defineProperty(obj, prop, descriptor)
      obj:要在其上定義屬性的對象
      prop: 要定義或者修改的屬性名字
      descriptor: 將被定義或修改的屬性描述符

      對象里目前存在的屬性描述符可以歸納為兩類:數(shù)據(jù)描述符存取描述符。數(shù)據(jù)描述符是一個具有值的屬性,configurable為true時,這個值可是可寫的,否則不可寫。存取描述符是由getter,setter函數(shù)描述的屬性。描述符必須是這兩種類型(數(shù)據(jù)描述符讀取描述符)之一,不可能同時是這兩者。

      數(shù)據(jù)描述符和存取描述符必須有下面可選鍵值:

      1. configurable:當且僅當改屬性的configurable為true的時候,該屬性描述符才能被修改,同時該屬性也能從對應的對象上被刪除。默認為false。
      2. enumerable:當且僅當改屬性的enmerable為true的時候,改屬性才能出現(xiàn)在對象的枚舉屬性中,默認為false。

      數(shù)據(jù)描述符同時具有以下可選鍵值:

      1. value:該屬性對應的值。可以是任何JavaScript有效值,數(shù)值,對象,函數(shù)等,默認為undefined。
      2. writable:當且僅當改屬性的writable為true時,value才能被賦值運算符改變,就是用“=”賦值。默認為false。

      存取描述符同時具有以下可選鍵值:

      1. get:一個給屬性提供getter的方法,如果沒有getter則為undefined。訪問這個屬性的時候,該方法會被執(zhí)行,沒有參數(shù),但是會傳入this對象(由于繼承關系,這里的this并不一定是定義該屬性的對象)。默認為undefined。
      2. set:一個給屬性提供setter的方法,如果沒有setter則為undefined。當屬性值被修改的時候,觸發(fā)這個setter方法。這個方法接受唯一參數(shù),即改屬性新的參數(shù)值。默認為undefined。

      如果一個描述符沒有value,writable,get,set任意一個關鍵字,那么它將被認為是一個數(shù)據(jù)描述符。如果一個描述符同時有(value或writable)和(get或set)關鍵字,會產(chǎn)生一個異常。

      這些選項不一定是自身屬性,如果是繼承來的也要考慮。為了確認保留這些默認值是自己定義的,可能要在這之前凍結Object.property,明確指定所有的選項,或者通過Object.create(null)將__proto__屬性指向null,要不然使用起來就有些混亂。下面使用Object.create(null)方法來給對象obj定義一個“干凈”的屬性。

          // 使用__prop__定義
         var obj = {}
         var descriptor = Object.create(null);
         // 默認沒有enumberable,configurable,writable
         descriptor.value = 'static';
         Object.defineProperty(obj, 'key', descriptor);
         console.log(obj); 

      代碼輸出結果如下:

      1. 定義一個空對象obj
      2. 定義一個屬性描述符descriptor,使用Object.Create方法繼承null對象,這樣沒有繼承屬性
      3. 設置數(shù)據(jù)描述符value,值為‘static’
      4. 使用Object.defineProperty方法給obj對象定義key屬性,使用descriptor描述符,描述符中只有一個數(shù)據(jù)描述符value,其他的都是默認值

      上面的語句和下面的效果是一樣的,就是使用Object.defineProperty方法給obj對象設置一個key屬性,屬性的屬性描述符都是默認值:

          // 顯示定義
          var obj = {}
          Object.defineProperty(obj, 'key', {
              enumerable: false,
              configurable: false,
              writable: false,
              value: "statics"
          });
          console.log(obj); 

      輸出如下:

       

      還可以循環(huán)使用同一對象最為對象描述符使用,代碼如下:

          // 循環(huán)使用統(tǒng)一對象
          function withValue (value) {
              var d = withValue.d || (
                  withValue.d = {
                      enumerable: false,
                      writable: false,
                      configurable: false,
                      value: null
                  }
              );
              d.value = value;
              return d;
          }
      
          var obj = {}
          Object.defineProperty(obj, 'key', withValue('static'));
          console.log(obj); 

      輸出結果如下:

       

      如果對象中不存在指定的屬性,Object.defineProperty()就創(chuàng)建這個屬性。當描述符中省略某些字段時,這些字段將使用它們的默認值。擁有布爾值的字段的默認值都是false,value,get,set字段默認值是undefined。一個沒有get,set,value,writable定義的屬性被稱為“通用的”,并被鍵入為一個數(shù)據(jù)描述符。

          // 在對象中添加一個屬性與數(shù)據(jù)描述符的實例, 對象o擁有了屬性a,值為37
          var obj = {};
          Object.defineProperty(obj, "a", {
              value: 37,
              writable: false,
              enumerable: false,
              configurable: true
          });
          console.log(obj); 

      輸出結果如下:

       

          // 在對象中添加一個屬性與數(shù)據(jù)描述符的實例, 對象o擁有了屬性a,值為37
          var obj = {};
          Object.defineProperty(obj, "a", {
              value: 37,
              writable: false,
              enumerable: false,
              configurable: true
          });
          console.log(obj);

       輸出結果如下:

          // 在對象中添加一個屬性與存取描述符,對象o擁有了屬性b,值為38
          var bValue;
          Object.defineProperty(obj, 'b', {
          get: function () {
             return bValue;
          },
          set: function (newValue) {
             bValue = newValue;
          },
          enumerable: true,
          configurable: true
          });
          // o.b的值現(xiàn)在總是與bValue相同,除非重新定義o.b
          bValue = 200;
          console.log(obj.b); 

       輸出結果如下: 

          // 數(shù)據(jù)描述符和存取描述符不能混合使用,否則會報錯
          var obj = {};
          Object.defineProperty(obj, 'confict', {
              value: '0x9f91102',
              get: function () {
                  return 0xdeadbeef;
              }
          });
          // 報錯:Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object> 

      2.1 修改屬性

      如果屬性已經(jīng)存在,Object.defineProperty()方法將嘗試根據(jù)描述符中的值以及對象當前的配置來修改這個屬性。如果舊對象描述符configurable為false,則屬性被認為是“不可配置的”,并且沒有屬性可以改變(除了單向改變writable為false)。當屬性不可配置時,不能再數(shù)據(jù)和訪問器屬性類型之間切換。當時圖改變不可配置屬性(除了writable屬性之外)的值時會拋出TypeError,除非當前值和心智相同。

      2.2 Writable屬性

      當writable屬性設置為false時,改屬性稱為“不可寫”。它不能被重新分配。

          // 創(chuàng)建一個新對象
          var o = {};
          Object.defineProperty(o, 'a', {
              value: 37,
              configurable: false,
              writable: false
          });
          console.log(o.a); // 輸出37
          o.a = 38;
          console.log(o.a); // writable屬性為false,o.a的值仍然是37,如果是嚴格模式,這里會拋錯:"a" is read-only

      2.3 Enumerable特性

      enumerable定義了對象的屬性是否可以在for...in循環(huán)和Object.keys()中枚舉。

          var o = {};
          Object.defineProperty(o, 'a', {value: 1, enumerable: true});
          Object.defineProperty(o, 'b', {value: 2, enumerable: false});
          // 沒有設置 enumberable屬性默認值是false
          Object.defineProperty(o, 'c', {value: 3});
          // 如果使用直接賦值的方式創(chuàng)建對象屬性,則這個屬性的enumerable為true
          o.d = 4;
          console.log(Object.keys(o)); // 輸出["a", "d"]
          console.log(o.propertyIsEnumerable('a')); // 輸出true
          console.log(o.propertyIsEnumerable('b')); // 輸出false
          console.log(o.propertyIsEnumerable('c')); // 輸出false
          console.log(o.propertyIsEnumerable('d')); // 輸出true 

      2.4 Configurable特性

      configurable特性表示對象的屬性是否可以被刪除,以及除writable特性外的其他特性是否可以被修改

          var o = {};
          Object.defineProperty(o, 'a', {
              get: function () {
                  return 1;
              },
              configurable: false
          });
          console.log(o.a); // 輸出1
      
          Object.defineProperty(o, 'a', {configurable: true}); // Uncaught TypeError: Cannot redefine property: a
          Object.defineProperty(o, "a", {enumerable: true}); // Uncaught TypeError: Cannot redefine property: a
          Object.defineProperty(o, "a", {
              set: function () {
              }
          }); // Uncaught TypeError: Cannot redefine property: a
          Object.defineProperty(o, "a", {
              get: function () {
              }
          }); // Uncaught TypeError: Cannot redefine property: a
          delete o.a;
          console.log(o.a); // 對染delete語句沒有報錯,但是沒有真正刪除a屬性,輸出1

      2.5 添加多個屬性和默認值

      使用點運算符和Object.defineProperty()為對象的屬性賦值,數(shù)據(jù)描述符的屬性默認值是不同的。

          var o = {};
          o.a = 1;
          // 上面使用點語法定義屬性,等同于下面代碼,注意writable,configurable,enumerable的默認屬性為false,但是這里使用點語法是true
          Object.defineProperty(o, "a", {
              value: 1,
              writable: true,
              configurable: true,
              enumerable: true
          });
      
          Object.defineProperty(o, "a", {value: 1});
          // 上面使用Object.defineProperty()定義屬性,等同于下面代碼
          Object.defineProperty(o, "a", {
              value: 1,
              writable: false,
              configurable: false,
              enumerable: false
          });

      2.6 一般的getter和setter

      下面的例子展示如何實現(xiàn)一個自存檔對象,當設置temperture屬性時,archive數(shù)組就會獲取日志

         function Archiver () {
             var temperature = null;
             var archiver = [];
             Object.defineProperty(this, 'temperature', {
                 get: function () {
                     console.log('get!');
                     return temperature;
                 },
                 set: function (value) {
                     temperature = value;
                     archiver.push({val: temperature});
                 }
             });
             this.getArchive = function () {
                 return archiver;
             }
         }
      
         var arc = new Archiver();
         console.log(arc.temperature); // 輸出get,但是arc.temperature是null
         arc.temperature = 11; // 觸發(fā)archiver.push({val: temperature})
         arc.temperature = 13; // 觸發(fā)archiver.push({val: temperature})
         console.log(arc.getArchive()); // 輸出[{val: 11}, {val: 13}]

      1. 定義一個方法類Archive,在內(nèi)部有私有變量temperature,archiver,
      2. 在方法內(nèi)使用Object.defineProperty()方法定義屬性temperature,定義存取描述符get,放回私有變量temperature,定義存取描述符set,用傳遞的參數(shù)給私有變量temperature賦值
      3. 定義特權方法getArchive,返回私有變量temperature
      4. 使用new操作符定義類Archive實例arc
      5. 輸出實例arc的temperature屬性,調(diào)用get方法,返回私有變量temperature的值null
      6. 給實例arc的temperatur屬性賦值,調(diào)用set方法,傳遞參數(shù)11,觸發(fā)archiver.push({val: 11})
      7. 給實例arc的temperatur屬性賦值,調(diào)用set方法,傳遞參數(shù)13,觸發(fā)archiver.push({val: 13})
      8. 輸出實例arc的temperature屬性,調(diào)用get方法,返回私有變量temperature的值[{val: 11}, {val: 13}]

         var pattern = {
             get: function () {
                 return 'I alway return this string,whatever you have assigned';
             },
             set: function () {
                 console.log('給屬性myname賦值')
                 this.myname = 'this is my name string';
             }
         }
         function TestDefineSetAndGet () {
             Object.defineProperty(this, 'myproperty', pattern);
         }
         var instance = new TestDefineSetAndGet();
         instance.myproperty = 'test'; // 輸出 “給屬性myname賦值”
         console.log(instance.myproperty); // 輸出 “I alway return this string,whatever you have assigned”
         console.log(instance.myname); // 輸出 “this is my name string”

      1. 定義屬性描述符pattern,屬性描述符上有存取描述符get,返回字符串“I alway return this string,whatever you have assigned”,存取描述符set,先輸出“給屬性myname賦值”,給當前對象的myname屬性賦值“this is my name string”
      2. 定義類方法TestDefineSetAndGet,方法內(nèi)部使用Object.defineProperty()給當前對象定義一個屬性“ myproperty”,使用屬性描述符pattern
      3. 使用new操作符定義類TestDefineSetAndGet的實例instance
      4. 給實例的屬性myproperty賦值“test”,因為使用Object.defineProperty給對象定義屬性的時候沒有指定writable,這里賦值無效。在get函數(shù)里返回的是固定值。在set函數(shù)里輸出“給屬性myname賦值”
      5. 輸出實例的屬性myproperty,訪問get函數(shù),返回“I alway return this string,whatever you have assigned”
      6. 輸出實例的屬性myname,因為訪問過set函數(shù),在setg函數(shù)中給當前對象賦過值,所以myname的值為“this is my name string”

      2.7 繼承屬性

      如果訪問者的屬性是被繼承的,它的get和set方法會在子對象的屬性被訪問或者修改時調(diào)用。如果這些方法用一個變量保存,會被所有對象共享。

          function myClass () {
          }
          var value;
          Object.defineProperty(myClass.prototype, 'x', {
              get () {
                  return value
              },
              set (x) {
                  value = x;
              }
          });
          var a = new myClass();
          var b = new myClass();
          a.x = 1;
          console.log(a.x); // 1
          console.log(b.x); // 1 

      在類myClass的原型對象上定義了x屬性,這個屬性會被類myClass的所有實例共享。通過將值保存在另一個屬性中固定,在get,set中,this指向某個被訪問和修改屬性的對象。

      代碼如下: 

              var obj = {}
              Object.defineProperty(obj, 'hello', {
                  get: function () {
                      console.log('get方法被調(diào)用')
                  },
                  set: function (v) {
                      console.log("set方法被調(diào)用了,參數(shù)是" + v)
                  }
              })
              obj.hello; // get方法被調(diào)用
              obj.hello = 'abc'; // set方法被調(diào)用了,參數(shù)是abc 

       可以像普通屬性一樣讀取,設置訪問器屬性,訪問器屬性比較特殊,讀取或設置訪問器屬性的值其實是調(diào)用內(nèi)部get,set方法來操作屬性。為屬性賦值,就是調(diào)用set方法并使用參數(shù)給屬性賦值。get,set方法內(nèi)部的this指針指向obj,這意味著get和set方法可以操作對象內(nèi)部的值。另外,訪問器屬性會覆蓋同名的普通屬性,因為訪問器屬性優(yōu)先訪問,同名的屬性會被忽略。 

          function myClass () {
          }
          Object.defineProperty(myClass.prototype, 'x', {
              get () {
                  return this.stored_x;
              },
              set (x) {
                  this.stored_x = x;
              }
          });
          var a = new myClass();
          var b = new myClass();
          a.x = 1;
          console.log(a.x); // 1
          console.log(b.x); // undefined

       不像訪問者屬性,值屬性始終在對象自身上設置,而不是一個原型。如果一個不可寫的屬性被繼承,它仍然可以防止修改對象的屬性。

          function myClass () {
          }
          myClass.prototype.x = 1;
          Object.defineProperty(myClass.prototype, 'y', {
              writable: false,
              value: 1
          });
          var a = new myClass();
          a.x = 2;
          console.log(a.x); // 2
          console.log(myClass.prototype.x); // 1
          a.y = 2;
          console.log(a.y); // 1
          console.log(myClass.prototype.y); // 1 

      1. 定義方法類myClass
      2. 在方法原型對象上通過點語法定義屬性x,值為1,它是可寫的,可配置的,可枚舉的
      3.  通過Object.defineProperty()方法在方法原型上定義屬性y,它是可寫的,不可配置的,不可枚舉的
      4. 定義一個myClass類的實例
      5. 訪問實例的屬性x,賦值為2,對象本身沒有這個屬性,在它原型對象上有這個屬性,這個屬性是可寫的,賦值為2
      6. 輸出實例x的屬性為2
      7. 輸出方法類myClass的原型對象上的屬性x是1
      8. 訪問實例的屬性y,它是通過Object.defineProperty()方法定義的,是不可寫的,賦值為2,它的值仍然是1
      9. 出事方法類myClass的原型對象上的屬性y,它仍然是1

      介紹完訪問器屬性之后我們來看看vue是如何實現(xiàn)雙向綁定的。

      3. vue.js雙向綁定

      3.1. 極簡雙向綁定

      vue.js最重要的概念是數(shù)據(jù)雙向綁定,也是MVVM主要特點。

      html代碼:

          <input type="text" id="a">
          <span id="b"></span>

       JavaScript代碼:

              var obj = {};
              Object.defineProperty(obj, 'hello', {
                  set: function (newVal) {
                    document.getElementById('a').value = newVal;
                    document.getElementById('b').innerHTML = newVal;
                  }
              })
              document.addEventListener('keyup', function (e) {
                obj.hello = e.target.value;
              });

       效果:

      這個效果就是在文本框中輸入的值會顯示在旁邊的<span>標簽里。這個例子就是雙向綁定的實現(xiàn),但是僅僅為了說明原理,這個和我們平時用的vue.js還有差距,下面是我們常見的vue.js寫法

      html代碼:

          <input type="text" v-model="text">
          {{ text }} 

      JavaScript代碼:

        var vm = new Vue({
          el: 'app',
          data: {
            text: 'hello world'
          }
        }) 

      為了實現(xiàn)這樣的容易理解的代碼vue.js背后做了很多工作,我們一一分解。

      1. 輸入框以及文本節(jié)點與data中的數(shù)據(jù)綁定顯示
      2. 輸入框變化的時候,data中的數(shù)據(jù)同步變化。即MVVM中 view => viewmodel的變化
      3. data中的數(shù)據(jù)變化時,文本節(jié)點顯示的內(nèi)容同步變化。即MVVM中viewmode => view的變化

      3.2 數(shù)據(jù)初始化綁定

      介紹數(shù)據(jù)初始化綁定之前先說一下DocumentFragment。DocumentFragment(文檔片段)可以看做是節(jié)點容器,它可以包含多個子節(jié)點,可以把它插入到DOM中,只有它的子節(jié)點會插入目標節(jié)點,所以可以把它看做是一組節(jié)點容器。使用DocumentFragment處理節(jié)點速度和性能優(yōu)于直接操作DOM。Vue進行編譯的時候就是將掛載目標的所有子節(jié)點劫持到DocumentFragment中,經(jīng)過處理后再將DocumentFragment整體返回到掛載目標。實例代碼如下:

              var dom = nodeToFragment(document.getElementById("app"));
              console.log(dom);
              function nodeToFragment (node, vm) {
                  var flag = document.createDocumentFragment();
                  var child;
                  while (child = node.firstChild) {
                      flag.appendChild(child); // 劫持node的所有節(jié)點
                  }
                  return flag;
              }
              document.getElementById("app").appendChild(dom); 

      有了文檔片段之后再看看初始化綁定。

      html代碼:

      <div id="app">
          <input type="text" v-model="text">
          {{text}}
      </div> 

      JavaScript代碼:

          function compile (node, vm) {
              var reg = /\{\{(.*)\}\}/;
              // 節(jié)點類型為元素,使用node.nodeType屬性
              if (node.nodeType === 1) {
                  var attr = node.attributes;
                  // 解析屬性
                  for (var i = 0; i < attr.length; i++) {
                      if (attr[i].nodeName === 'v-model') {
                          var name = attr[i].nodeValue; // 獲取v-model綁定的屬性名
                          node.value = vm.data[name]; // 將data的值賦給該node
                          node.removeAttribute('v-model');
                      }
                  }
              }
              // 節(jié)點類型為text,使用node.nodeType屬性
              if (node.nodeType === 3) {
                  if (reg.test(node.nodeValue)) {
                      var name = RegExp.$1; // 獲取匹配到的字符串
                      name = name.trim()
                      node.nodeValue = vm.data[name]; // 將該data的值付給該node
                  }
              }
          }
      
          function nodeToFragment (node, vm) {
              var flag = document.createDocumentFragment();
              var child;
              while (child = node.firstChild) {
                  compile(child, vm);
                  // 將子節(jié)點劫持到文檔片段中
                  flag.appendChild(child);
              }
              return flag;
          }
      
          // 構造函數(shù)
          function Vue (options) {
              this.data = options.data;
              var id = options.el;
              var dom = nodeToFragment(document.getElementById(id), this);
              // 編譯完成后把dom返回到app中
              document.getElementById(id).appendChild(dom);
          }
      
          var vm = new Vue({
              el: 'app',
              data: {
                  text: 'hello world'
              }
          }); 

      最終效果:

      我們看到hello word已經(jīng)綁定到input標簽和節(jié)點中了

      先看compile方法,這個方法主要負責給node節(jié)點賦值
      1. compile方法接收兩個參數(shù),第一個是DOM節(jié)點,第二個vm是當前對象
      2. 判斷dom節(jié)點類型,如果是1,表示元素(這里判斷不太嚴謹,只是為了說明原理),在node節(jié)點的所有屬性中查找nodeName為“v-model”的屬性,找到屬性值,這里是“text”。用當前對象中名字為“text”的屬性值給節(jié)點賦值,最后刪除這個屬性,就是刪除節(jié)點的v-model屬性。
      3. 判斷dom節(jié)點類型,如果是3,表示是節(jié)點內(nèi)容,用正則表達式判斷是“{{text}}”這樣的字符串,用當前對象中名字為“text”的屬性值給節(jié)點賦值,直接覆蓋掉“{{text}}”
      4.這里是簡單的例子,實際情況是dom結構要比這個復雜的多,可能會遞歸的尋找節(jié)點,判斷節(jié)點類型,操作賦值。

      nodeToFragment方法負責創(chuàng)建文檔片段,并將compile處理過的子節(jié)點劫持到這個文檔片段中
      1. 創(chuàng)建一個文檔片段
      2. 循環(huán)查找傳入的node節(jié)點,調(diào)用compile方法給節(jié)點賦值
      3. 將賦值后的節(jié)點劫持到文檔片段中

      Vue構造函數(shù)
      1. 用傳入?yún)?shù)的data屬性給當前對象的data屬性賦值
      2. 用傳入?yún)?shù)的id標記查找掛載節(jié)點,調(diào)用nodeToFragment方法獲取劫持后的文檔片段,這個過程稱為編譯
      3. 編譯完成后,將文檔片段插入到指定的當前節(jié)點中

      實例化vue
      1. 實例化一個vue對象,el屬性為掛載節(jié)點的id,data屬性為要綁定的屬性及屬性值

      3.3 響應式數(shù)據(jù)綁定

       初始化綁定只是實現(xiàn)了第一步,然后我們要實現(xiàn)的是在文本框中輸入內(nèi)容的時候,vue實例中的屬性值也跟著變化。思路是在文本框中輸入數(shù)據(jù)的時候,觸發(fā)文本框的input事件(也可以是keyup,change),在相應的事件處理程序中,獲取輸入內(nèi)容賦值給當前vue實例vm的text屬性。這里利用上面介紹的Object.defeinProperty()方法來給vue實例中data中的屬性重新定義為訪問器屬性,就是在定義這個屬性的時候添加get,set這兩個存取描述符,這樣給vm.text賦值的時候就會觸發(fā)set方法。然后在set方法中更新vue實例屬性的值。看下面的html,js代碼:

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>響應式數(shù)據(jù)綁定</title>
      </head>
      <body>
      <div id="app">
          <input type="text" v-model="text"/>
          {{ text }}
      </div>
      <script>
          /**
           * 使用defineProperty將data中的text設置為vm的訪問器屬性
           * @param obj 對象
           * @param 屬性名
           * @param 屬性值
           * */
          function defineReactive (obj, key, val) {
              Object.defineProperty(obj, key, {
                  get: function () {
                      return val
                  },
                  set: function (newVal) {
                      if (newVal === val) {
                          return
                      }
                      val = newVal
                      // 輸出日志
                      console.log(`set方法觸發(fā)屬性值變化${val}`)
                  }
              })
          }
          /**
           * 給vue實例定義訪問器屬性
           * @param obj vue實例中的數(shù)據(jù)
           * @param vm vue對象
           * */
          function observe (obj, vm) {
              Object.keys(obj).forEach(function (key) {
                  defineReactive(vm, key, obj[key]);
              })
          }
          /**
           * 編譯過程,給子節(jié)點初始化綁定vue實例中的屬性值
           * @param node 子節(jié)點
           * @param vm vue實例
           * */
          function compile (node, vm) {
              let reg = /\{\{(.*)\}\}/
              // 節(jié)點類型為元素
              if (node.nodeType === 1) {
                  let attr = node.attributes
                  // 解析屬性
                  for (let i = 0; i < attr.length; i++) {
                      if (attr[i].nodeName === 'v-model') {
                          // 獲取v-model綁定的屬性名,v-model一般是可輸入的dom,可修改的dom
                          let name = attr[i].nodeValue
                          // 添加監(jiān)聽事件
                          node.addEventListener('input', function (e) {
                              // 給相應的data屬性賦值,進而觸發(fā)該屬性的set方法
                              vm[name] = e.target.value;
                          });
                          // 將data的值賦給該node
                          node.value = vm.data[name];
                          node.removeAttribute('v-model')
                      }
                  }
              }
              // 節(jié)點類型為text,這里只是顯示數(shù)據(jù)的dom
              if (node.nodeType === 3) {
                  if (reg.test(node.nodeValue)) {
                      // 使用震澤表達式獲取匹配到的字符串
                      let name = RegExp.$1
                      name = name.trim()
                      // 將data的值賦給該node.nodeValue
                      node.nodeValue = vm.data[name]
                  }
              }
          }
          /**
           * DocumentFragment文檔片段,可以看作節(jié)點容器,它可以包含多個子節(jié)點,當將它插入到dom中時只有子節(jié)點插入到目標節(jié)點中。
           * 使用documentfragment處理節(jié)點速度和性能要高于直接操作dom。vue編譯的時候,就是將掛載目標的所有子節(jié)點劫持到documentfragment
           * 中,經(jīng)過處理后再將documentfragment整體返回到掛載目標中。
           * @param node 節(jié)點
           * @param vm vue實例
           * */
          function nodeToFragment (node, vm) {
              var flag = document.createDocumentFragment();
              var child;
              while (child = node.firstChild) {
                  compile(child, vm);
                  flag.appendChild(child);
              }
              return flag;
          }
          /*vue類*/
          function Vue (options) {
              this.data = options.data
              let data = this.data
              // 給vue實例的data定義訪問器屬性,覆蓋原來的同名屬性
              observe(data, this)
              let id = options.el
              let dom = nodeToFragment(document.getElementById(id), this)
              // 編譯,劫持完成后將dom返回到app中
              document.getElementById(id).appendChild(dom)
          }
      
          /*定義一個vue實例*/
          let vm = new Vue({
              el: 'app',
              // 這里的data屬性不是訪問器屬性
              data: {
                  text: 'hello world!'
              }
          })
      </script>
      </body>
      </html> 

      修改文本框中的內(nèi)容,vue實例中的屬性值也跟著變化,如下截圖:

      下面不再逐句分析,只說重點的。

      1. 在defineReactive方法中,vue實例中的data的屬性重新定義為訪問器屬性,并在set方法中將新的值更新到這個屬性
      2. 在observe方法中,遍歷vue實例中data的屬性,逐一調(diào)用defineReactive方法,把他們定義為訪問器屬性
      3. 在compile方法中,如果是input這樣的標簽,給它添加事件(也可以是keyup,change),監(jiān)聽input值變化,并給vue實例中相應的訪問器屬性賦值
      4. 在Vue類方法中,調(diào)用observer方法,傳入當前實例對象和對象的data屬性,將data屬性中的子元素重新定義為當前對象的訪問器屬性

      set方法被觸發(fā)之后,vue實例的text屬性跟著變化,但是<span>的內(nèi)容并沒有變化,下面的內(nèi)容將會介紹“訂閱/發(fā)布模式”來解決這個問題。

      3.4 雙向綁定的實現(xiàn)

      在實現(xiàn)雙向綁定之前要先學習一下“訂閱/發(fā)布模式”。訂閱發(fā)布模式(又稱為觀察者模式)定義一種一對多的關系,讓多個觀察者同時監(jiān)聽一個主題對象,主題對象狀態(tài)發(fā)生改變的時候觀察者都會得到通知

      發(fā)布者發(fā)出通知 => 主題對象收到通知并推送給訂閱者 => 訂閱者執(zhí)行相應的操作

      看下面的代碼:

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>訂閱/發(fā)布模式</title>
      </head>
      <body>
      <script>
          /**
           * 定義一個發(fā)布者publisher
           * */
          var pub = {
              publish: function () {
                  dep.notify();
              }
          }
          /**
           * 三個訂閱者
           * */
          var sub1 = {
              update: function () {
                  console.log(1);
              }
          };
          var sub2 = {
              update: function () {
                  console.log(2);
              }
          };
          var sub3 = {
              update: function () {
                  console.log(3);
              }
          }
          /**
           * 一個主題對象
           * */
          function Dep () {
              this.subs = [sub1, sub2, sub3];
          }
          Dep.prototype.notify = function () {
              this.subs.forEach(function (sub) {
                  sub.update();
              })
          }
          // 發(fā)布者發(fā)布消息,主題對象執(zhí)行notifiy方法,觸發(fā)所有訂閱者響應,執(zhí)行update
          var dep = new Dep();
          pub.publish();
      </script>
      </body>
      </html>

       運行結果如下截圖:

      1. 定義發(fā)布者對象pub,對象中定義publish方法,方法調(diào)用主題對象實例dep的notify()方法
      2. 定義三個訂閱者對象,對象中定義update方法,三個對象的update方法分別輸出1,2,3
      3. 定義一個主題方法類,主題對象中定義數(shù)組屬性subs,包含三個訂閱者對象
      4. 在主題方法類的原型對象上定義通知方法notify,方法中循環(huán)調(diào)用三個訂閱者對象的update()方法
      5. 實例化主題方法類得到實例dep
      6. 調(diào)用發(fā)布者對象的通知方法notifiy(),分別輸出1,2,3

      每當創(chuàng)建一個Vue實例的時候,主要做了兩件事情,第一是監(jiān)聽數(shù)據(jù):observe(data),第二個是編譯HTML:nodeToFragment(id)。
      在監(jiān)聽數(shù)據(jù)過程中,為data的每一個屬性生成主題對象dep
      在編譯HTML的過程中,為每個與數(shù)據(jù)綁定相關的節(jié)點生成一個訂閱者watcherwatcher會將自己添加到相應屬性的dep中
      前面已經(jīng)實現(xiàn)了:修改輸入框內(nèi)容 => 在事件回調(diào)函數(shù)中修改屬性值 => 觸發(fā)屬性set方法。
      接下來我們要實現(xiàn)的是:發(fā)出通知dep.notify() => 觸發(fā)訂閱者的updata方法 => 更新視圖,實現(xiàn)這個目標的關鍵是如何將watcher添加到關聯(lián)屬性的dep中去。 

      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>雙向綁定的實現(xiàn)</title>
      </head>
      <body>
      <div id="app">
          <input type="text" v-model="text">
          {{ text }}
      </div>
      <script>
          /**
           * 使用defineProperty將data中的text設置為vm的訪問器屬性
           * @param obj 對象
           * @param key 屬性名
           * @param val 屬性值
           */
          function defineReactive(obj, key, val) {
              // 發(fā)布者對象
              var dep = new Dep();
              Object.defineProperty(obj, key, {
                  get: function () {
                      // 依賴收集,如果主題對象類的靜態(tài)屬性target有值, 此時Watcher方法被調(diào)用,給主題對象添加訂閱者
                      if (Dep.target) dep.addSub(Dep.target);
                      return val;
                  },
                  set: function (newVal) {
                      if (newVal === val) return
                      val = newVal;
                      // 屬性被修改時通知變更,主題對象作為發(fā)布者收到通知推送給訂閱者,訂閱者收到消息回調(diào)
                      dep.notify();
                  }
              })
          }
      
          /**
           * 給vue實例定義訪問器屬性,將Vue中的data對象中的屬性轉化成getter,setter
           * @param obj vue實例中的數(shù)據(jù)
           * @param vm vue對象
           */
          function observe(obj, vm) {
              Object.keys(obj).forEach(function (key) {
                  defineReactive(vm, key, obj[key])
              })
          }
      
          /**
           * DocumentFragment文檔片段
           * @param node 節(jié)點
           * @param vm vue實例
           * */
          function nodeToFragment(node, vm) {
              var flag = document.createDocumentFragment();
              var child;
              while (child = node.firstChild) {
                  // 節(jié)點編譯,生成Watcher
                  compile(child, vm);
                  flag.appendChild(child);
              }
              return flag;
          }
      
          /**
           * 給子節(jié)點初始化綁定vue實例中的屬性值,并為節(jié)點生成Watcher
           * @param node 子節(jié)點
           * @param vm vue實例
           */
          function compile(node, vm) {
              var reg = /\{\{(.*)\}\}/;
              // 節(jié)點類型為元素,可輸入的dom
              if (node.nodeType === 1) {
                  var attr = node.attributes;
                  // 解析屬性
                  for (var i = 0; i < attr.length; i++) {
                      if (attr[i].nodeName === 'v-model') {
                          // 獲取v-model綁定的屬性名
                          var name = attr[i].nodeValue;
                          node.addEventListener('input', function (e) {
                              // 給相應的data屬性賦值,觸發(fā)set方法
                              vm[name] = e.target.value
                          });
                          // 將data的值賦給該node
                          node.value = vm[name];
                          node.removeAttribute('v-model');
                      }
                  }
                  new Watcher(vm, node, name, 'input')
              }
              if (node.nodeType === 3) {
                  if (reg.test(node.nodeValue)) {
                      var name = RegExp.$1; // 獲取匹配到的字符串
                      name = name.trim();
                      // 將data的值賦給該node,訂閱,同上
                      new Watcher(vm, node, name, 'text');
                  }
              }
          }
      
          /**
           * 編譯 HTML 過程中,為每個與 data 關聯(lián)的節(jié)點生成一個 Watcher,收集依賴的時候會addSub到subs集合中,修改data數(shù)據(jù)的時候觸發(fā)dep對象的
              * notify通知所有Wathcer對象去修改對應視圖
           * @param vm
           * @param node
           * @param name
           * @param nodeType
           * @constructor
           */
          function Watcher(vm, node, name, nodeType) {
              // 將當前對象賦值給全局變量Dep.target
              Dep.target = this;
              this.name = name;
              this.node = node;
              this.vm = vm;
              this.nodeType = nodeType;
              // 更新
              this.update();
              // 設置為空,避免重復添加訂閱者
              Dep.target = null;
          }
          Watcher.prototype = {
              // 更新
              update: function () {
                  /**調(diào)用get,這里Dep.target不為空,getter中會將當前屬性添加到訂閱者集合中,update函數(shù)執(zhí)行完之后就不行了*/
                  this.get();
                  if (this.nodeType === 'text') {
                      this.node.nodeValue = this.value;
                  }
                  if (this.nodeType === 'input') {
                      this.node.value = this.value;
                  }
              },
              get: function () {
                  // this.vm[this.name] 觸發(fā)getter
                  this.value = this.vm[this.name];
              }
          }
      
          /**
           * 定義一個發(fā)布者
           * @constructor
           */
          function Dep() {
              // 訂閱者集合
              this.subs = [];
          }
      
          /**
           * 發(fā)布者,添加訂閱者和通知變化
           * @type {{addSub: Dep.addSub, notify: Dep.notify}}
           */
          Dep.prototype = {
              // 添加訂閱者
              addSub: function (sub) {
                  this.subs.push(sub);
              },
              // 輪詢訂閱者,通知變化,觸發(fā)更新
              notify: function () {
                  this.subs.forEach(function (sub) {
                      sub.update();
                  });
              }
          };
      
          /**
           * 定義Vue類
           * @param options Vue參數(shù)選項
           * @constructor
           */
          function Vue(options) {
              this.data = options.data;
              var data = this.data;
              observe(data, this);
              var id = options.el;
              // 編譯,收集依賴
              var dom = nodeToFragment(document.getElementById(id), this);
              // 編譯完成后,將dom返回到app中
              document.getElementById(id).appendChild(dom);
          }
      
          // 定義Vue實例
          var vm = new Vue({
              el: 'app',
              data: {
                  text: 'hello world'
              }
          })
      </script>
      </body>
      </html>

       

      最終效果如下截圖:

      這里不再逐句分析,只把重點說明一下
      1. 定義主題對象Dep,對象中有addSubnotify兩個方法,前者負責向當前對象中添加訂閱者,后者輪詢訂閱者,調(diào)用訂閱者的更新方法update()
      2. 定義觀察者對象方法Watcher,在方法中先將自己賦給一個全局變量Dep.target,其實是給主題類Dep定義了一個靜態(tài)屬性target,可以直接使用Dep.target訪問這個靜態(tài)屬性。然后給類定義共有屬性name(vue實例中的訪問器屬性名“text”),node(html標簽,如<input>,{{text}}),vm(當前vue實例),nodeType(html標簽類型),其次執(zhí)行update方法,進而執(zhí)行了原型對象上的get方法,get方法中的this.vm[this.name]讀取了vm中的訪問器屬性,從而觸發(fā)了訪問器屬性的get方法,get方法中將wathcer添加到對應訪問器屬性的dep中,同時將屬性值賦給臨時變量value。再者,獲取屬性的值(保存在臨時變量value中),然后更新視圖。最后將Dep.target設為空。因為它是全局變量,也是watcher與dep關聯(lián)的唯一橋梁,任何時刻都必須保證Dep.target只有一個值。
      3. 在編譯方法compile中,劫持子節(jié)點的時候,在節(jié)點上定義一個觀察者對象Watcher
      4. defineReactive方法中,定義訪問器屬性的時候,在存取描述符get中,如果主題對象類的靜態(tài)屬性target有值, 此時Watcher方法被調(diào)用,給主題對象添加訂閱者。

      data中的數(shù)據(jù)重新定義為訪問器屬性,get中將當前數(shù)據(jù)對應的節(jié)點添加到主題對象中,set方法中通知數(shù)據(jù)對應的節(jié)點更新。編譯過程將data數(shù)據(jù)生成數(shù)據(jù)節(jié)點,并生成一個觀察者來觀察節(jié)點變化。

      4. 總結

      介紹了這么多,最后vue的原理總結如下。vue功能遠不止這些,要深入了解,需要研究源代碼,待后續(xù)博客。

      1. 給Vue定義data選項;
      2. 使用Object.defineProperty將data選項中的屬性轉化成gettersetter屬性;
      3. getter將觀察者添加到主題對象中,收集依賴;
      4. setter中通知變更,給dom元素賦值;
      5. 編譯,將el節(jié)點及子節(jié)點編譯到render函數(shù);
      6. 編譯,觸發(fā)getter,將當前節(jié)點對應的data屬性添加到主題中;
      7. 編譯,監(jiān)聽input節(jié)點的change事件,修改當前節(jié)點對應的data屬性值,觸發(fā)setter,通知變更,更新dom節(jié)點值,反饋給用戶;

      本文介紹了vue.js的簡單實現(xiàn)以及相關的知識,包含MVC,MVP,MVVM的原理,對象的訪問器屬性,html的文檔片段(DocumentFragment),觀察者模式。vue.js的實現(xiàn)主要介紹數(shù)據(jù)編譯(compile),通過文檔片段實現(xiàn)數(shù)據(jù)劫持掛載,通過觀察者模式(訂閱發(fā)布模式)的實現(xiàn)數(shù)據(jù)雙向綁定等內(nèi)容。

      參考:

      http://www.rzrgm.cn/icebutterfly/p/7977033.html
      http://www.ruanyifeng.com/blog/2015/02/mvcmvp_mvvm.html
      https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
      http://www.rzrgm.cn/kidney/p/6052935.html?utm_source=gold_browser_extension#!comments

      posted @ 2018-11-07 14:56  nd  閱讀(28074)  評論(1)    收藏  舉報
      主站蜘蛛池模板: 亚洲高清免费在线观看| 午夜欧美精品久久久久久久| 国产午夜精品理论大片| 久久se精品一区精品二区国产| 无码视频伊人| 亚洲中文字幕人妻系列| 欧美激情 亚洲 在线| 日本中文字幕久久网站| 亚洲 一区二区 在线| 精品国产成人国产在线视| 亚洲欧美中文日韩V在线观看 | 国产精品色内内在线播放| 虎林市| 亚洲国产成人无码电影| 无套后入极品美女少妇| 被c到高潮疯狂喷水国产| 精品av综合导航| 日本熟妇XXXX潮喷视频| 国产精品国产精品国产专区不卡| 亚洲精品一区二区三区综合| 久久精品国产亚洲av麻豆软件| 千阳县| 国产在线国偷精品产拍| 四虎成人精品无码| 无码专区视频精品老司机 | 亚洲无av中文字幕在线| 精品免费看国产一区二区| 99久久精品美女高潮喷水| 日韩高清不卡一区二区三区| 成人啪精品视频网站午夜| 中文字幕国产日韩精品| 亚洲岛国av一区二区| 国产欧美日韩精品丝袜高跟鞋| 久久99国产精品尤物| 江北区| 精品亚洲国产成人av在线| 亚洲AV永久无码精品秋霞电影影院| 狠狠综合久久综合88亚洲| 久热天堂在线视频精品伊人| 精品无码成人片一区二区98| 国产熟女激情一区二区三区 |