摘要:方法實現將所有屬性掛載在觀察對象,將每一項做一個數據劫持就是將中每一項用定義新屬性并返回這個對象。當和發生變化時,自動會觸發視圖更新,獲取得到的也就是最新值。
MVVM及Vue實現原理
Github源碼地址:https://github.com/wyj2443573...
mvvm 雙向數據綁定1. Object.defineProperty
數據影響視圖,視圖影響數據
angular 臟值檢測 vue數據劫持+發布訂閱模式
vue 不兼容低版本 用的是Object.defineProperty
下面涉及涵蓋的知識點
因為vue底層是基于Object.defineProperty實現的,所以對于這方面不懂的自己先學習。
Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 并返回這個對象。
語法Object.defineProperty(obj, prop, descriptor)
基本用法
var o = {}; o.a = 1; // 等同于 : Object.defineProperty(o, "a", { value : 1, writable : true, configurable : true, enumerable : true }); // 另一方面, Object.defineProperty(o, "a", { value : 1 }); // 等同于 : Object.defineProperty(o, "a", { value : 1, writable : false, configurable : false, enumerable : false });
let o={} Object.defineProperty(o,"a",{ get(){ //獲取o.a的值時,會調用get方法 return "hello"; }, set(value){ //o.a="s" 賦值的時候觸發set方法 console.log(value) } }) o.a="s"http://"s" o.a //"hello"2.數據劫持Observe
vue基本格式
//html{{a}}
模仿vue的格式
vue 中有$options : 存在屬性 data、el、components 等等
_data : Vue實例參數中data對象
接下來構建基本頁面
index.html
Title {{a}}
mvvm.js
function Vue(options={}){ this.$options=options; //將所有屬性掛載在$options; //this._data let data=this._data=this.$options.data; //觀察data對象,將每一項做一個數據劫持;就是將data中每一項用Object.defineProperty定義新屬性并返回這個對象。 observe(data); } function Observe(data) { //這里寫的是主要邏輯 for(let key in data){ //把data屬性通過object.defineProperty的方式 定義屬性 let val=data[key]; Object.defineProperty(data,key,{ enumerable:true, //可以枚舉 get(){ return val; //僅僅是將以前的 a:1的方式 轉換換位defineProperty的方式 }, set(newValue){ //更改值的時候觸發 if(newValue===val){ //如果設置的值跟之前的值一樣則什么也不做 return; } val=newValue; //將新值賦給val,那么get獲取val的時候,獲取的就是newValue; } }) } } //觀察對象給對象增加ObjectDefinedProperty function observe(data){ return new Observe(data); }
此時在控制臺打印 vue,發現已經在vue._data中映射了正確的數據。
接下來操作: vue._data.a=30
如果此時data中a是一個復雜的對象類型,如下
{{a.a}}
則此時打印輸出vue._data,只有第一層添加上了defineProperty,第二層的a無法劫持
那么我們要遞歸 深層添加defineProperty 另外遞歸的時候注意添加退出條件,當value不是對象的時候退出。
代碼添加如下
function Observe(data) { //這里寫的是主要邏輯 for(let key in data){ //把data屬性通過object.defineProperty的方式 定義屬性 let val=data[key]; observe(val); //遞歸 劫持 Object.defineProperty(data,key,{ enumerable:true, //可以枚舉 get(){ return val; //僅僅是將以前的 a:1的方式 裝換位defineProperty的方式 }, set(newValue){ //更改值的時候觸發 if(newValue===val){ //如果設置的值跟之前的值一樣則什么也不做 return; } val=newValue; //將新值賦給val,那么get獲取val的時候,獲取的就是newValue; } }) } } function observe(data){ if(typeof data!="object"){ //如果非對象,則退出遍歷遞歸 return; } return new Observe(data); }
此時內層的a也同樣得到劫持
如果我們給a設置新值的時候vue._data.a={b:3} ,會發現內層b并沒有被數據劫持。那么在賦新值的時候,也應該通過defineProperty去定義。
在set中用defineProperty定義新屬性
set(newValue){ //更改值的時候觸發 if(newValue===val){ //如果設置的值跟之前的值一樣則什么也不做 return; } val=newValue; //將新值賦給val,那么get獲取val的時候,獲取的就是newValue; observe(newValue) }
分析到這里,我們已經能夠實現了深度的數據觀察
3.數據代理上面的代碼如果想要訪問a的屬性需要通過 vue._data.a 這樣的寫法獲得,這種寫法過于繁瑣。我們接下來改善一下:用vue.a的方式直接訪問到a(用vue 代理 vue._data )
-- 1.首先用this代理this._data; 讓數據中的每一項都用defineProperty代理。
function Vue(options={}){ this.$options=options; //將所有屬性掛載在$options; //this._data let data=this._data=this.$options.data; //觀察data對象,將每一項做一個數據劫持;就是將data中每一項用Object.defineProperty定義新屬性并返回這個對象。 observe(data); for(let key in data){ Object.defineProperty(this,key,{ //this 代理this._data; enumerable:true, get(){ return this._data[key]; //相當于 this.a={a:1} }, set(newVal){ } }) } }
到這一步我們已經實現了數據代理的初級版,vue.a 可以直接獲取值而非vue._data.a
-- 2.get方法很容易理解,這里較為重點的是在set中設置。首先思考:如果直接設置this.a={name:1} ,this.a 與this._data.a 它們的值同步改變嗎?
set(newVal){ this[key]=newVal; //?可以這樣做嗎?我們來實踐下 }
很顯然兩者是不能夠同步改變的。
方法實現
function Vue(options={}){ this.$options=options; //將所有屬性掛載在$options; //this._data let data=this._data=this.$options.data; //觀察data對象,將每一項做一個數據劫持;就是將data中每一項用Object.defineProperty定義新屬性并返回這個對象。 observe(data); //this 代理this._data; for(let key in data){ Object.defineProperty(this,key,{ enumerable:true, get(){ return this._data[key]; //相當于 this.a={a:1} }, set(newVal){ //如果直接更改this[key]="XXX",那么this._data[key]的值是不會被同步改變的。 // 我們可以通過給this._data[key]=value賦值,從而調取Observe方法中的set,賦予this._data[key]新值。 // get(){return this._data[key]},獲取到的值即是調取Observe方法中get方法return的值 // 也就是根源上的改變是this._data[key];這樣不管是this._data[key]還是this[key]隨便哪一個被賦予新值,兩者都是同步變化的 this._data[key]=newVal; } }) } }
下面來分析一下思路
1.我們可以在set中設置this._data[key]=newValue,如果此時vue.a={name:1}它調取是Observe方法中的set,賦予this._data[key]新值。
設置值的時候相當于走的是這一步
set(newValue){ //更改值的時候觸發 if(newValue===val){ //如果設置的值跟之前的值一樣則什么也不做 return; } val=newValue; //將新值賦給val,那么get獲取val的時候,獲取的就是newValue; observe(newValue) }
2.如果此時我們獲取vue.a 的值,即通過get方法獲取return this._data[key],得到的就是最新值
這里解釋說明一下
4.編譯模板Compile學到這里我們應該了解到:
vue特點不能新增不存在的屬性 因為不存在的屬性沒有get 和 set,它就不會監控數據的變化
深度響應: 因為每次賦予一個新對象時會給這個新對象增加數據劫持。
這一步我們要做的目的是將目標元素內{{xx}} 花括號中的xx替換成對應的值。
第一步、代碼實現如下
function Vue(options={}){ /*代碼承接上面*/ new Compile(options.el,this) //實例化Compile } function Compile(el){ //el表示替換哪個元素范圍內的模板 let replacePart=document.querySelector(el); let fragment=document.createDocumentFragment(); while(child = replacePart.firstChild){ //將app中的內容移至內存中 fragment.appendChild(child); } replace() //我們在此要做的是通過replace方法,將代碼片段中的{{a.a}}的a.a替換為data中對應的值。 replacePart.appendChild(fragment); } function replace(){ }
第二步、replace方法先找到所有要替換的地方,代碼如下:
{{A}}{{a.a}}
{{b}}
{{c}}
{hllp7zz}
function Compile(el,vm){ //el代表替換的范圍 let replacePart=document.querySelector(el); let fragment=document.createDocumentFragment(); while(child = replacePart.firstChild){ //將app中的內容移至內存中 fragment.appendChild(child); } replace(fragment) //我們在此要做的是通過replace方法,將代碼片段中的{{a.a}}的a.a替換為data中對應的值。 replacePart.appendChild(fragment); function replace(fragment){ Array.from(fragment.childNodes).forEach(function(node){ let text=node.textContent; let reg=/{{(.*)}}/; if(node.nodeType===3&& reg.test(text)){ //nodeType:3 文本節點 console.log(RegExp.$1); // A、 a.a 、b 等等 } if(node.childNodes){ replace(node) //如果當前node存在子節點,遞歸找到所有需要替換的地方 } }) } }
這一步我們能夠找到所有要替換的目標了
第三步、replace方法中 用對應值替換掉需要替換掉的地方,代碼如下:
function replace(fragment){ Array.from(fragment.childNodes).forEach(function(node){ let text=node.textContent; let reg=/{{(.*)}}/; if(node.nodeType===3&®.test(text)){ //nodeType:3 文本節點 console.log(RegExp.$1); let arr=RegExp.$1.split(".") // [A] [a,a] [b] ... let val=vm; //val:{a:{a:1}} arr.forEach(function(k){ val=val[k] //舉例 第一次循環 val=val.a val賦值后val:{a:1} ;第二次循環 val=val.a val賦值后為1 }) node.textContent=text.replace(reg,val) } if(node.childNodes){ replace(node) //如果當前node存在子節點,遞歸替換 } }) }
替換結果如下
5.發布訂閱模式不明白發布訂閱模式的朋友先去學習
參考鏈接:https://segmentfault.com/a/11...
代碼一:
//發布訂閱模式 先訂閱 再發布 // 訂閱就是往事件池里面扔函數 發布的就是事件池中的函數依次執行 //我們假設subs中每個方法中都有update屬性, function Dep(){ this.subs=[] } Dep.prototype.addSub=function(sub){ //訂閱 this.subs.push(sub) } Dep.prototype.notify=function(){ this.subs.forEach(sub=>{ sub.update(); }) } function Watcher(fn){ //watch是一個類 通過這個類創建的實例都擁有update方法 this.fn=fn } Watcher.prototype.update=function(){ this.fn(); } let watcher=new Watcher(function(){console.log(1)}); //監聽函數 let dep=new Dep(); dep.addSub(watcher); //將watcher放在數組中 dep.addSub(watcher); dep.addSub(watcher); dep.notify() // 數組關系
代碼二(然后我們將代碼二的這個發布訂閱的模板放到我們的mvvm.js最下面)
function Dep(){ this.subs=[] } Dep.prototype.addSub=function(sub){ //訂閱 this.subs.push(sub) } Dep.prototype.notify=function(){ this.subs.forEach(sub=>{ sub.update(); }) } function Watcher(fn){ this.fn=fn } Watcher.prototype.update=function(){ this.fn(); }6.連接視圖與數據
那么我們接下來的目的是:當數據變化的時候,我們需要重新刷新視圖,將頁面中的{{a.a}}雙括號中的a.a也能夠被實時的替換掉。這就用到了上面介紹的發布訂閱模式。方法:我們得先將node.textContent=text.replace(reg,val)訂閱一下,當數據發生變化的時候,執行node.textContent=text.replace(reg,val)此操作。
我們先寫要訂閱的事件
這里思考當數據變化的時候會產生新的值,我們需要用newValue替換原有的值。要想取到新的值,我們需要用到當前實例vm與正則的捕獲到的RegExp.$1,從而獲取類似this.a.a的最新值。
new Watcher(vm,RegExp.$1,function(newValue){ //訂閱的事件 函數里需要接受新的值 node.textContent=text.replace(reg,newValue); });
此時Watcher類也應該改動下,不懂沒關系,可以順著看下面的解析。
function Watcher(vm,exp,fn){ this.fn=fn; this.vm=vm; this.exp=exp; //我們要將fn添加到訂閱中 Dep.target=this; console.log(Dep.target) let val=vm; let arr=exp.split("."); arr.forEach(function(k){ val=val[k]; }) Dep.target=null; }
Dep.target為
下面我們來分析代碼
這個邏輯相對復雜,不明白的話多看幾遍
上述代碼中,我們可以看到執行了這一步
let val=vm; let arr=exp.split("."); arr.forEach(function(k){ val=val[k]; //獲取this.a.a的時候就會觸發get方法 })
這一步遍歷 arr 取val[k]的時候相當于獲取this.a,會觸發了默認的get方法。也就是觸發這里的get方法
在get方法中我們需要訂閱事件:
上述代碼中,取值的時候會調取get方法,Dep.target值是存在的,此時將Dep.target放到我們的事件池中。
當我們set的時候,觸發事件池中的事件
此時update的方法我們得更改下,賦予新值
Watcher.prototype.update=function(){ let val=this.vm; let arr=this.exp.split("."); arr.forEach(function(k){ //this.a.a val=val[k]; }) this.fn(val); //newValue }
此時我們就能得到我們想要的結果了,當數據發生改變時,視圖也會更新。
7.雙向數據綁定的實現思路: 先找到v-model這個屬性,取對應的屬性值s,然后用vue.s 替換input中value值
在Compile方法中寫邏輯
打印結果如下
此時已經能夠將對應的s值賦值給輸入框
接下來我們需要做的是,每次更改輸入框中的值的時候,對應的s也會跟著改變
操作結果如下
8.實現computed官網上有兩種computed的基礎用法
用法一
var vm = new Vue({ el: "#example", data: { message: "Hello" }, computed: { // 一個 computed 屬性的 getter 函數 reversedMessage: function () { // `this` 指向 vm 實例 return this.message.split("").reverse().join("") } } })
用法二
computed: { fullName: { // getter 函數 get: function () { return this.firstName + " " + this.lastName }, // setter 函數 set: function (newValue) { var names = newValue.split(" ") this.firstName = names[0] this.lastName = names[names.length - 1] } } }
思路: computed實現相對來說比較簡單,只是把數據掛在vm上
1.html頁面如下
s的值:{{s}}
computed的值:{{hello}}
{{A}}{{a.a}}
{{b}}
{{c}}
{tl5hbb7}
2.將hello屬性掛載在當前實例上,先初始化computed
initComputed 函數實現
function initComputed(){ let vm=this; let computed=this.$options.computed; //Object.keys [name:"tom",age:2]=>[name,age] Object.keys(computed).forEach(function(key){ Object.defineProperty(vm,key,{ //computed[key] get:typeof computed[key]==="function"? computed[key]:computed[key].get, set(){ } }) }) }
分析以上代碼,我們能得知this.hello的變化 只依賴于this.s 和 this.c 。當s和c發生變化時,自動會觸發視圖更新,獲取this.hello得到的也就是最新值。
++++++++++++到此已經完成了分享,如有相關的問題和建議還望不吝提出++++++++++++
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/103195.html
摘要:插件開發前端掘金作者原文地址譯者插件是為應用添加全局功能的一種強大而且簡單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內優雅的實現文件分片斷點續傳。 Vue.js 插件開發 - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應用添加全局功能的一種強大而且簡單的方式。插....
摘要:模塊化是隨著前端技術的發展,前端代碼爆炸式增長后,工程化所采取的必然措施。目前模塊化的思想分為和。特別指出,事件不等同于異步,回調也不等同于異步。將會討論安全的類型檢測惰性載入函數凍結對象定時器等話題。 Vue.js 前后端同構方案之準備篇——代碼優化 目前 Vue.js 的火爆不亞于當初的 React,本人對寫代碼有潔癖,代碼也是藝術。此篇是準備篇,工欲善其事,必先利其器。我們先在代...
摘要:最近,筆者就在為組里的框架去做一套基本的工具。通過這邊文章,筆者希望大家都能簡單的去實現一個屬于自己的腳手架工具。我們在下新增文件,這個文件導出一個的類。結語到此,一個簡單的就制作完成了,大家可以參考等優秀的適當的擴展自己的工具。 你有沒有遇到過在沒有vue-cli、create-react-app這樣子的腳手架的時候一個文件一個文件的去拷貝老項目的配置文件。最近,筆者就在為組里的框架...
閱讀 3638·2021-11-25 09:43
閱讀 636·2021-09-22 15:59
閱讀 1744·2021-09-06 15:00
閱讀 1769·2021-09-02 09:54
閱讀 689·2019-08-30 15:56
閱讀 1176·2019-08-29 17:14
閱讀 1839·2019-08-29 13:15
閱讀 880·2019-08-28 18:28