国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

vue實現簡單的MVVM框架

2019-11-19 13:20:34
字體:
來源:轉載
供稿:網友

不知不覺接觸前端的時間已經過去半年了,越來越發覺對知識的學習不應該只停留在會用的層面,這在我學jQuery的一段時間后便有這樣的體會。

雖然jQuery只是一個JS的代碼庫,只要會一些JS的基本操作學習一兩天就能很快掌握jQuery的基本語法并熟練使用,但是如果不了解jQUery庫背后的實現原理,相信只要你一段時間不再使用jQuery的話就會把jQuery忘得一干二凈,這也許就是知其然不知其所以然的后果。

最近在學vue的時候又再一次經歷了這樣的困惑,雖然能夠比較熟練的掌握vue的基本使用,也能夠對MV*模式、數據劫持、雙向數據綁定、數據代理侃上兩句。但是要是稍微深入一點就有點吃力了。所以這幾天痛下決心研究大量技術文章(起初嘗試看早期源碼,無奈vue與jQuery不是一個層級的,相比于jQuery,vue是真正意義上的前端框架。只能無奈棄坑轉而看技術博客),對vue也算有了一個管中窺豹的認識。最后嘗試實踐一下自己學到的知識,基于數據代理、數據劫持、模板解析、雙向綁定實現了一個小型的vue框架。

溫馨提示:文章是按照每個模塊的實現依賴關系來進行分析的,但是在閱讀的時候可以按照vue的執行順序來分析,這樣對初學者更加的友好。推薦的閱讀順序為:實現VMVM、數據代理、實現Observe、實現Complie、實現Watcher。

源碼:https://github.com/yuliangbin/MVVM

功能演示如下所示:

數據代理

以下面這個模板為例,要替換的根元素“#mvvm-app”內只有一個文本節點#text,#text的內容為{{name}}。我們就以下面這個模板詳細了解一下VUE框架的大體實現流程。

<body> <div id="mvvm-app">  {{name}} </div> <script src="./js/observer.js"></script> <script src="./js/watcher.js"></script> <script src="./js/compile.js"></script> <script src="./js/mvvm.js"></script> <script>  let vm = new MVVM({   el: "#mvvm-app",   data: {    name: "hello world"   },    }) </script></body>

數據代理

1、什么是數據代理

在vue里面,我們將數據寫在data對象中。但是我們在訪問data里的數據時,既可以通過vm.data.name訪問,也可以通過vm.name訪問。這就是數據代理:在一個對象中,可以動態的訪問和設置另一個對象的屬性。

2、實現原理

我們知道靜態綁定(如vm.name = vm.data.name)可以一次性的將結果賦給變量,而使用Object.defineProperty()方法來綁定則可以通過set和get函數實現賦值的中間過程,從而實現數據的動態綁定。具體實現如下:

let obj = {};let obj1 = { name: 'xiaoyu', age: 18,}//實現origin對象代理target對象function proxyData(origin,target){ Object.keys(target).forEach(function(key){  Object.defineProperty(origin,key,{//定義origin對象的key屬性   enumerable: false,   configurable: true,   get: function getter(){    return target[key];//origin[key] = target[key];   },   set: function setter(newValue){    target[key] = newValue;   }  }) })}

vue中的數據代理也是通過這種方式來實現的。

function MVVM(options) { this.$options = options || {}; var data = this._data = this.$options.data; var _this = this;//當前實例vm // 數據代理 // 實現 vm._data.xxx -> vm.xxx  Object.keys(data).forEach(function(key) {  _this._proxyData(key); }); observe(data, this); this.$compile = new Compile(options.el || document.body, this);}MVVM.prototype = {_proxyData: function(key) { var _this = this; if (typeof key == 'object' && !(key instanceof Array)){//這里只實現了對對象的監聽,沒有實現數組的  this._proxyData(key); } Object.defineProperty(_this, key, {  configurable: false,  enumerable: true,  get: function proxyGetter() {   return _this._data[key];  },  set: function proxySetter(newVal) {   _this._data[key] = newVal;  } });},};

實現Observe

1、雙向數據綁定

數據變動  --->  視圖更新

視圖更新  --->  數據變動

要想實現當數據變動時視圖更新,首先要做的就是如何知道數據變動了,可以通過Object.defineProperty()函數監聽data對象里的數據,當數據變動了就會觸發set()方法。所以我們需要實現一個數據監聽器Observe,來對數據對象中的所有屬性進行監聽,當某一屬性數據發生變化時,拿到最新的數據通知綁定了該屬性的訂閱器,訂閱器再執行相應的數據更新回調函數,從而實現視圖的刷新。

當設置this.name = 'hello vue'時,就會執行set函數,通知訂閱器里的訂閱者執行相應的回調函數,實現數據變動,對應視圖更新。

function observe(data){ if (typeof data != 'object') {  return ; } return new Observe(data);}function Observe(data){ this.data = data; this.walk(data);}Observe.prototype = { walk: function(data){  let _this = this;  for (key in data) {   if (data.hasOwnProperty(key)){    let value = data[key];    if (typeof value == 'object'){     observe(value);    }    _this.defineReactive(data,key,data[key]);   }  } }, defineReactive: function(data,key,value){  Object.defineProperty(data,key,{   enumerable: true,//可枚舉   configurable: false,//不能再define   get: function(){    console.log('你訪問了' + key);return value;   },   set: function(newValue){    console.log('你設置了' + key);    if (newValue == value) return;    value = newValue;    observe(newValue);//監聽新設置的值   }  }) }}

2、實現一個訂閱器

要想通知訂閱者,首先得要有一個訂閱器(統一管理所有的訂閱者)。為了方便管理,我們會為每一個data對象的屬性都添加一個訂閱器(new Dep)。

訂閱器里存著的是訂閱者Watcher(后面會講到),由于訂閱者可能會有多個,我們需要建立一個數組來維護。一旦數據變化,就會觸發訂閱器的notify()方法,訂閱者就會調用自身的update方法實現視圖更新。

function Dep(){ this.subs = [];}Dep.prototype = { addSub: function(sub){this.subs.push(sub); }, notify: function(){  this.subs.forEach(function(sub) {   sub.update();  }) }}

每次響應屬性的set()函數調用的時候,都會觸發訂閱器,所以代碼補充完整。

Observe.prototype = { //省略的代碼未作更改 defineReactive: function(data,key,value){  let dep = new Dep();//創建一個訂閱器,會被閉包在key屬性的get/set函數內,因此每個屬性對應唯一一個訂閱器dep實例  Object.defineProperty(data,key,{   enumerable: true,//可枚舉   configurable: false,//不能再define   get: function(){    console.log('你訪問了' + key);    return value;   },   set: function(newValue){    console.log('你設置了' + key);    if (newValue == value) return;    value = newValue;    observe(newValue);//監聽新設置的值    dep.notify();//通知所有的訂閱者   }  }) }}

實現Complie

compile主要做的事情是解析模板指令,將模板中的data屬性替換成data屬性對應的值(比如將{{name}}替換成data.name值),然后初始化渲染頁面視圖,并且為每個data屬性添加一個監聽數據的訂閱者(new Watcher),一旦數據有變動,收到通知,更新視圖。

遍歷解析需要替換的根元素el下的HTML標簽必然會涉及到多次的DOM節點操作,因此不可避免的會引發頁面的重排或重繪,為了提高性能和效率,我們把根元素el下的所有節點轉換為文檔碎片fragment進行解析編譯操作,解析完成,再將fragment添加回原來的真實dom節點中。

注:文檔碎片本身也是一個節點,但是當將該節點append進頁面時,該節點標簽作為根節點不會顯示html文檔中,其里面的子節點則可以完全顯示。

Compile解析模板,將模板內的子元素#text添加進文檔碎片節點fragment。

function Compile(el,vm){ this.$vm = vm;//vm為當前實例 this.$el = document.querySelector(el);//獲得要解析的根元素  if (this.$el){  this.$fragment = this.nodeToFragment(this.$el);  this.init();  this.$el.appendChild(this.$fragment); } }Compile.prototype = { nodeToFragment: function(el){  let fragment = document.createDocumentFragment();  let child;  while (child = el.firstChild){   fragment.appendChild(child);//append相當于剪切的功能  }  return fragment;   },};

compileElement方法將遍歷所有節點及其子節點,進行掃描解析編譯,調用對應的指令渲染函數進行數據渲染,并調用對應的指令更新函數進行綁定,詳看代碼及注釋說明:

因為我們的模板只含有一個文本節點#text,因此compileElement方法執行后會進入_this.compileText(node,reg.exec(node.textContent)[1]);//#text,'name'

Compile.prototype = { nodeToFragment: function(el){  let fragment = document.createDocumentFragment();  let child;  while (child = el.firstChild){   fragment.appendChild(child);//append相當于剪切的功能  }  return fragment;   },  init: function(){  this.compileElement(this.$fragment); },  compileElement: function(node){  let childNodes = node.childNodes;  const _this = this;  let reg = //{/{(.*)/}/}/g;  [].slice.call(childNodes).forEach(function(node){      if (_this.isElementNode(node)){//如果為元素節點,則進行相應操作    _this.compile(node);   } else if (_this.isTextNode(node) && reg.test(node.textContent)){    //如果為文本節點,并且包含data屬性(如{{name}}),則進行相應操作    _this.compileText(node,reg.exec(node.textContent)[1]);//#text,'name'   }      if (node.childNodes && node.childNodes.length){    //如果節點內還有子節點,則遞歸繼續解析節點    _this.compileElement(node);       }  }) }, compileText: function(node,exp){//#text,'name'   compileUtil.text(node,this.$vm,exp);//#text,vm,'name' },};

CompileText()函數實現初始化渲染頁面視圖(將data.name的值通過#text.textContent = data.name顯示在頁面上),并且為每個DOM節點添加一個監聽數據的訂閱者(這里是為#text節點新增一個Wather)。

let updater = { textUpdater: function(node,value){   node.textContent = typeof value == 'undefined' ? '' : value; },} let compileUtil = { text: function(node,vm,exp){//#text,vm,'name'  this.bind(node,vm,exp,'text'); },  bind: function(node,vm,exp,dir){//#text,vm,'name','text'  let updaterFn = updater[dir + 'Updater'];  updaterFn && updaterFn(node,this._getVMVal(vm,exp));  new Watcher(vm,exp,function(value){   updaterFn && updaterFn(node,value)  });  console.log('加進去了'); }};

現在我們完成了一個能實現文本節點解析的Compile()函數,接下來我們實現一個Watcher()函數。

實現Watcher

我們前面講過,Observe()函數實現data對象的屬性劫持,并在屬性值改變時觸發訂閱器的notify()通知訂閱者Watcher,訂閱者就會調用自身的update方法實現視圖更新。

Compile()函數負責解析模板,初始化頁面,并且為每個data屬性新增一個監聽數據的訂閱者(new Watcher)。

Watcher訂閱者作為Observer和Compile之間通信的橋梁,所以我們可以大致知道Watcher的作用是什么。

主要做的事情是:

在自身實例化時往訂閱器(dep)里面添加自己。

自身必須有一個update()方法 。

待屬性變動dep.notice()通知時,能調用自身的update()方法,并觸發Compile中綁定的回調。

先給出全部代碼,再分析具體的功能。

//Watcherfunction Watcher(vm, exp, cb) { this.vm = vm; this.cb = cb; this.exp = exp; this.value = this.get();//初始化時將自己添加進訂閱器};Watcher.prototype = { update: function(){  this.run(); }, run: function(){  const value = this.vm[this.exp];  //console.log('me:'+value);  if (value != this.value){   this.value = value;   this.cb.call(this.vm,value);  } }, get: function() {   Dep.target = this; // 緩存自己  var value = this.vm[this.exp] // 訪問自己,執行defineProperty里的get函數     Dep.target = null; // 釋放自己  return value; }}//這里列出Observe和Dep,方便理解Observe.prototype = { defineReactive: function(data,key,value){  let dep = new Dep();  Object.defineProperty(data,key,{   enumerable: true,//可枚舉   configurable: false,//不能再define   get: function(){    console.log('你訪問了' + key);    //說明這是實例化Watcher時引起的,則添加進訂閱器    if (Dep.target){     //console.log('訪問了Dep.target');     dep.addSub(Dep.target);    }    return value;   },  }) }}Dep.prototype = { addSub: function(sub){this.subs.push(sub); },}

我們知道在Observe()函數執行時,我們為每個屬性都添加了一個訂閱器dep,而這個dep被閉包在屬性的get/set函數內。所以,我們可以在實例化Watcher時調用this.get()函數訪問data.name屬性,這會觸發defineProperty()函數內的get函數,get方法執行的時候,就會在屬性的訂閱器dep添加當前watcher實例,從而在屬性值有變化的時候,watcher實例就能收到更新通知。

那么Watcher()函數中的get()函數內Dep.taeger = this又有什么特殊的含義呢?我們希望的是在實例化Watcher時將相應的Watcher實例添加一次進dep訂閱器即可,而不希望在以后每次訪問data.name屬性時都加入一次dep訂閱器。所以我們在實例化執行this.get()函數時用Dep.target = this來標識當前Watcher實例,當添加進dep訂閱器后設置Dep.target=null。

實現VMVM

MVVM作為數據綁定的入口,整合Observer、Compile和Watcher三者,通過Observer來監聽自己的model數據變化,通過Compile來解析編譯模板指令,最終利用Watcher搭起Observer和Compile之間的通信橋梁,達到數據變化 -> 視圖更新;視圖交互變化(input) -> 數據model變更的雙向綁定效果。

function MVVM(options) { this.$options = options || {}; var data = this._data = this.$options.data; var _this = this; // 數據代理 // 實現 vm._data.xxx -> vm.xxx  Object.keys(data).forEach(function(key) {  _this._proxyData(key); }); observe(data, this); this.$compile = new Compile(options.el || document.body, this);}

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 新平| 桦川县| 永济市| 剑河县| 津南区| 界首市| 荆州市| 克山县| 永年县| 灵武市| 昭通市| 尤溪县| 扎鲁特旗| 车致| 陆河县| 玛沁县| 呼伦贝尔市| 温泉县| 嘉祥县| 桐庐县| 潍坊市| 佳木斯市| 远安县| 静海县| 共和县| 高要市| 高密市| 丰顺县| 航空| 新宾| 唐河县| 渑池县| 长顺县| 洪雅县| 安国市| 米易县| 通城县| 格尔木市| 大悟县| 叶城县| 九寨沟县|