相信只要去面試 Vue
,都會被用到 vue
的雙向數據綁定,你若是隻說個 mvvm
就是視圖模型模型視圖,只要數據改變視圖也會同步更新,那可能達不到面試官想要的那個層次。甚至能夠說這一點就讓面試官以爲你知識瞭解的還不夠,只是粗略地明白雙向綁定這個概念。javascript
本博客旨在經過一個簡化版的代碼來對 mvvm
理解更加深入,如若存在問題,歡迎評論提出,謝謝您!html
最後,但願你給一個點贊或 star
:star:,謝謝您的支持!前端
實現源碼傳送門vue
同時,也會收錄在小獅子前端筆記倉庫裏 ✿✿ヽ(°▽°)ノ✿java
小獅子前端の學習整理筆記 Front-end-learning-to-organize-notesnode
實現效果: git
目前幾種主流的 mvc(vm)
框架都實現了單向數據綁定,即用數據操做視圖,數據更新,視圖同步更新。而雙向數據綁定無非就是在單向綁定的基礎上給可輸入元素(如 input
、textarea
等)添加了 change(input)
事件,來動態修改 model
和 view
,這樣就能用視圖來操做數據了,即視圖更新,數據同步更新。github
實現數據綁定的作法大體有以下幾種:面試
發佈者-訂閱者模式(backbone.js) 髒值檢查(angular.js)將舊值和新值進行比對,若是有變化的話,就會更新視圖,最簡單的方式就是經過
setInterval()
定時輪詢檢測數據變更。 數據劫持(vue.js)c#
發佈者-訂閱者模式:通常經過 sub
,pub
的方式實現數據和視圖的綁定監聽,更新數據方式一般作法是 vm.set('property', value)
但上述方式對比如今來講知足不了咱們須要了,咱們更但願經過 vm.property = value
這種方式更新數據,同時自動更新視圖,因而有了下面兩種方式:
髒值檢測: angular.js
是經過髒值檢測的方式比對數據是否變動,來決定是否更新視圖,最簡單的方式就是經過 setInterval()
定時輪詢檢測數據變更。固然,它只在指定的事件觸發時才進入髒值檢測,大體以下:
DOM
事件,譬如用戶輸入文本,點擊按鈕等。(ng-click
)XHR
響應事件($http
)Location
變動事件($location
)Timer
事件($timeout
, $interval
)數據劫持: vue.js
則是採用數據劫持結合發佈者-訂閱者模式的方式,經過 object.defineProperty()
來劫持各個屬性的 setter
、getter
,在數據變更時發佈消息給訂閱者,觸發相應的監聽回調。
mvvm
的雙向綁定要實現 mvvm
的雙向綁定,就必需要實現如下幾點:
Compile
,對每一個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數Observer
,可以對數據對象的全部屬性進行監聽,若有變更可拿到最新值並通知訂閱者Watcher
,做爲鏈接Observer
和Compile
的橋樑,可以訂閱並收到每一個屬性變更的通知,執行指令綁定的相應回調函數,從而更新視圖mvvm
入口函數,整合以上三者整合流程圖以下圖所示:
compile
主要作的事情是解析模板指令,將模板中的變量替換成數據,而後初始化渲染頁面視圖,並將每一個指令對應的節點綁定更新函數,添加監聽數據的訂閱者,一旦數據有變更,收到通知,更新視圖,以下圖所示:
由於遍歷解析的過程有屢次操做 dom
節點,爲提升性能和效率,會先將 vue
實例根節點的 el
轉換成文檔碎片fragment
進行解析編譯操做,解析完成,再將fragment
添加回原來的真實dom
節點中。
html
頁面引入咱們從新寫的 myVue.js
<script src="./myVue.js"></script>
複製代碼
myVue
類建立一個 myVue
類,構造函數以下所示,將頁面的掛載 el
、數據 data
、操做集 options
進行保存。
class myVue{
constructor(options){
this.$el = options.el
this.$data = options.data
this.$options = options
if(this.$el){
// 1.實現數據觀察者(省略...)
// 2.實現指令解析器
new Compile(this.$el,this)
}
// console.log(this)
}
}
複製代碼
Compile
類具體實現步驟:
query
dom
節點,目的是減小頁面的迴流和重繪class Compile{
constructor(el,vm){
// 判斷是否爲元素節點,若是不是就query
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 一、獲取文檔碎片對象,放入內存中,會減小頁面的迴流和重繪
const fragment = this.node2Fragment(this.el)
// 二、編譯模板
this.compile(fragment)
// 三、追加子元素到根元素
this.el.appendChild(fragment)
}
複製代碼
判斷是否爲元素節點,直接判斷nodeType是否爲1便可
isElementNode(node){
return node.nodeType === 1
}
複製代碼
經過 document.createDocumentFragment()
建立文檔碎片對象,經過 el.firstChild
是否還存在來判斷,而後將 dom
節點添加到文檔碎片對象中,最後 return
node2Fragment(el){
// 建立文檔碎片對象
const fragment = document.createDocumentFragment()
let firstChild
while(firstChild = el.firstChild){
fragment.appendChild(firstChild)
}
return fragment
}
複製代碼
解析模板時,會獲取獲得全部的子節點,此時分兩種狀況,即元素
節點和文本
節點。若是當前節點還存在子節點,則須要經過遞歸操做來遍歷其子節點。
compile(fragment){
// 一、獲取全部子節點
const childNodes = fragment.childNodes;
[...childNodes].forEach(child=>{
// console.log(child)
// 若是是元素節點,則編譯元素節點
if(this.isElementNode(child)){
// console.log('元素節點',child)
this.compileElement(child)
}else{
// 其它爲文本節點,編譯文本節點
// console.log('文本節點',child)
this.compileText(child)
}
if(child.childNodes && child.childNodes.length){
this.compile(child)
}
})
}
複製代碼
節點 node
上有一個 attributes
屬性,來獲取當前節點的全部屬性,經過是否以 v-
開頭來判斷當前屬性名稱是否爲一個指令。若是是一個指令的話,還需進行分類編譯,用數據來驅動視圖。更新數據完畢後,再經過 removeAttribute
事件來刪除指令上標籤的屬性。
若是是非指令的話,例如事件 @click="sayHi"
,僅需經過指令 v-on
來實現便可。
對於不一樣的指令,咱們最好進行一下封裝,這裏就巧妙運用了 策略模式 。
compileElement(node){
const attributes = node.attributes;
[...attributes].forEach(attr=>{
// console.log(attr)
const {name,value} = attr;
// console.log(name,value)
// 判斷當前name值是否爲一個指令,經過是否以 'v-' 開頭來判斷
if(this.isDirective(name)){
// console.log(name.split('-'))
const [,directive] = name.split('-') // text html model on:click
// console.log(directive)
const [dirName,eventName] = directive.split(':') // text html model on
// 更新數據 數據驅動視圖
complieUtil[dirName](node,value,this.vm,eventName)
// 刪除指令上標籤上的屬性
node.removeAttribute('v-' + directive)
}else if(this.isEventName(name)){ // @click="sayHi"
let [,eventName] = name.split('@')
complieUtil['on'](node,value,this.vm,eventName)
}
})
}
複製代碼
判斷當前 attrName
是否爲一個指令,僅需判斷是否以 v-
開頭
isDirective(attrName){
return attrName.startsWith('v-')
}
複製代碼
判斷當前 attrName
是否爲一個事件,就看是否以'@'
開頭的事件綁定
isEventName(attrName){
return attrName.startsWith('@')
}
複製代碼
指令處理集合
const complieUtil = {
getVal(expr,vm){
return expr.split('.').reduce((data,currentVal)=>{
// console.log(currentVal)
return data[currentVal]
},vm.$data)
},
text(node,expr,vm){
let value;
// 元素節點
if(expr.indexOf('{{') !== -1){
value = expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
return this.getVal(args[1],vm);
})
}else{ // 文本節點
value = this.getVal(expr,vm)
}
this.updater.textUpdater(node,value)
},
html(node,expr,vm){
const value = this.getVal(expr,vm)
this.updater.htmlUpdater(node,value)
},
model(node,expr,vm){
const value = this.getVal(expr,vm)
this.updater.modelUpdater(node,value)
},
on(node,expr,vm,eventName){
let fn = vm.$options.methods && vm.$options.methods[expr]
// 一、讓fn經過bind函數指向原來的vm 二、默認冒泡
node.addEventListener(eventName,fn.bind(vm),false)
},
bind(node,expr,vm,attrName){
},
// 更新的函數
updater:{
textUpdater(node,value){
node.textContent = value
},
htmlUpdater(node,value){
node.innerHTML = value
},
modelUpdater(node,value){
node.value = value
}
}
}
複製代碼
利用 Obeject.defineProperty()
來監聽屬性變更,那麼將須要 observe
的數據對象進行遞歸遍歷,包括子屬性對象的屬性,都加上 setter
和 getter
。這樣的話,給這個對象的某個值賦值,就會觸發 setter
,那麼就能監聽到了數據變化。具體代碼以下:
class Observer{
constructor(data){
this.observe(data)
}
observe(data){
if(data && typeof data === 'object'){
// console.log(Object.keys(data))
// 進行數據劫持
Object.keys(data).forEach(key=>{
this.defineReactive(data,key,data[key])
})
}
}
defineReactive(obj,key,value){
// 遞歸遍歷
this.observe(value)
Object.defineProperty(obj,key,{
enumerable: true,
configurable: false,
get(){
// 訂閱數據變化時,往Dep中添加觀察者,進行依賴收集
return value
},
// 經過箭頭函數改變this指向到class Observer
set:(newVal)=>{
this.observe(newVal)
if(newVal !== value){
value = newVal
}
}
})
}
}
複製代碼
data
示例以下:
data: {
person:{
name: 'Chocolate',
age: 20,
hobby: '寫代碼'
},
msg: '超逸の技術博客',
htmlStr: '<h3>歡迎一塊兒學習~</h3>'
},
複製代碼
Watcher
訂閱者做爲
Observer
和
Compile
之間通訊的橋樑,主要作的事情是:
dep
)裏面添加本身update()
方法dep.notify()
通知時,能調用自身的 update()
方法,並觸發 Compile
中綁定的回調。Watcher
訂閱者實例化 Watcher
的時候,調用 getOldVal()
方法,來獲取舊值。經過 Dep.target = watcherInstance(this)
標記訂閱者是當前 watcher
實例(即指向本身)。
class Watcher{
constructor(vm,expr,cb){
this.vm = vm
this.expr = expr
this.cb = cb
// 先將舊值進行保存
this.oldVal = this.getOldVal()
}
getOldVal(){
// 將當前訂閱者指向本身
Dep.target = this
// 獲取舊值
const oldVal = complieUtil.getVal(this.expr,this.vm)
// 添加完畢,重置
Dep.target = null
return oldVal
}
// 比較新值與舊值,若是有變化就更新視圖
update(){
const newVal = complieUtil.getVal(this.expr,this.vm)
// 若是新舊值不相等,則將新值callback
if(newVal !== this.oldVal){
this.cb(newVal)
}
}
}
複製代碼
強行觸發屬性定義的 get
方法,get
方法執行的時候,就會在屬性的訂閱器 dep
添加當前watcher
實例,從而在屬性值有變化的時候,watcherInstance(this)
就能收到更新通知。
// 上文省略...
defineReactive(obj,key,value){
// 遞歸遍歷
this.observe(value)
const dep = new Dep()
Object.defineProperty(obj,key,{
enumerable: true,
configurable: false,
get(){
// 訂閱數據屬性時,往Dep中添加觀察者,進行依賴收集
Dep.target && dep.addSub(Dep.target)
return value
},
// 經過箭頭函數改變this指向到class Observer
set:(newVal)=>{
this.observe(newVal)
if(newVal !== value){
value = newVal
// 若是新舊值不一樣,則告訴Dep通知變化
dep.notify()
}
}
})
}
複製代碼
dep
主要作兩件事情:
class Dep{
constructor(){
this.subs = []
}
// 收集觀察者
addSub(watcher){
this.subs.push(watcher)
}
// 通知觀察者去更新
notify(){
console.log('觀察者',this.subs);
this.subs.forEach(watcher => watcher.update())
}
}
複製代碼
Compile.js
文件作完上述事情後,此時,當咱們修改某個數據時,數據已經發生了變化,可是視圖沒有更新。那咱們在何時來添加綁定 watcher
呢?請繼續看下圖
也就是說,當咱們訂閱數據變化時,來綁定更新函數,從而讓 watcher
去更新視圖。此時咱們修改咱們本來的 Compile.js
文件以下:
// 指令處理集合
const complieUtil = {
getVal(expr,vm){
return expr.split('.').reduce((data,currentVal)=>{
// console.log(currentVal)
return data[currentVal]
},vm.$data)
},
// 獲取新值 對{{a}}--{{b}} 這種格式進行處理
getContentVal(expr,vm){
return expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
// console.log(args[1]);
return this.getVal(args[1],vm);
})
},
text(node,expr,vm){
let value;
if(expr.indexOf('{{') !== -1){
value = expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
// 綁定watcher從而更新視圖
new Watcher(vm,args[1],()=>{
this.updater.textUpdater(node,this.getContentVal(expr,vm))
// console.log(expr);
})
return this.getVal(args[1],vm);
})
}else{ // 也多是v-text='obj.name' v-text='msg'
value = this.getVal(expr,vm)
}
this.updater.textUpdater(node,value)
},
html(node,expr,vm){
const value = this.getVal(expr,vm)
new Watcher(vm,expr,(newVal)=>{
this.updater.htmlUpdater(node,newVal)
})
this.updater.htmlUpdater(node,value)
},
model(node,expr,vm){
const value = this.getVal(expr,vm)
// 訂閱數據變化時 綁定更新函數 更新視圖的變化
// 數據==>視圖
new Watcher(vm,expr,(newVal)=>{
this.updater.modelUpdater(node,newVal)
})
this.updater.modelUpdater(node,value)
},
on(node,expr,vm,eventName){
let fn = vm.$options.methods && vm.$options.methods[expr]
// 一、讓fn經過bind函數指向原來的vm 二、默認冒泡
node.addEventListener(eventName,fn.bind(vm),false)
},
bind(node,expr,vm,attrName){
let attrVal = this.getVal(expr,vm)
this.updater.attrUpdater(node,attrName,attrVal)
},
// 更新的函數
updater:{
textUpdater(node,value){
node.textContent = value
},
htmlUpdater(node,value){
node.innerHTML = value
},
modelUpdater(node,value){
node.value = value
},
attrUpdater(node, attrName, attrVal){
node.setAttribute(attrName,attrVal)
}
}
}
class Compile{
constructor(el,vm){
// 判斷是否爲元素節點,若是不是就query
this.el = this.isElementNode(el) ? el : document.querySelector(el)
this.vm = vm
// 一、獲取文檔碎片對象,放入內存中,會減小頁面的迴流和重繪
const fragment = this.node2Fragment(this.el)
// 二、編譯模板
this.compile(fragment)
// 三、追加子元素到根元素
this.el.appendChild(fragment)
}
// 判斷是否爲元素節點,直接判斷nodeType是否爲1便可
isElementNode(node){
return node.nodeType === 1
}
node2Fragment(el){
// 建立文檔碎片對象
const fragment = document.createDocumentFragment()
let firstChild
while(firstChild = el.firstChild){
fragment.appendChild(firstChild)
}
return fragment
}
compile(fragment){
// 一、獲取全部子節點
const childNodes = fragment.childNodes;
[...childNodes].forEach(child=>{
// console.log(child)
// 若是是元素節點,則編譯元素節點
if(this.isElementNode(child)){
// console.log('元素節點',child)
this.compileElement(child)
}else{
// 其它爲文本節點,編譯文本節點
// console.log('文本節點',child)
this.compileText(child)
}
if(child.childNodes && child.childNodes.length){
this.compile(child)
}
})
}
// 編譯元素節點
compileElement(node){
const attributes = node.attributes;
[...attributes].forEach(attr=>{
// console.log(attr)
const {name,value} = attr;
// console.log(name,value)
// 判斷當前name值是否爲一個指令,經過是否以 'v-' 開頭來判斷
if(this.isDirective(name)){
// console.log(name.split('-'))
const [,directive] = name.split('-') // text html model on:click
// console.log(directive)
const [dirName,eventName] = directive.split(':') // text html model on
// 更新數據 數據驅動視圖
complieUtil[dirName](node,value,this.vm,eventName)
// 刪除指令上標籤上的屬性
node.removeAttribute('v-' + directive)
}else if(this.isEventName(name)){ // @click="sayHi"
let [,eventName] = name.split('@')
complieUtil['on'](node,value,this.vm,eventName)
}
})
}
// 編譯文本節點
compileText(node){
// {{}} v-text
// console.log(node.textContent)
const content = node.textContent
if(/\{\{(.+?)\}\}/.test(content)){
// console.log(content)
complieUtil['text'](node,content,this.vm)
}
}
isDirective(attrName){
return attrName.startsWith('v-')
}
// 判斷當前attrName是否爲一個事件,以'@'開頭的事件綁定
isEventName(attrName){
return attrName.startsWith('@')
}
}
複製代碼
此時,咱們就能經過數據變化來驅動視圖了,例如更改咱們的年齡 age
從原來的 20
設置爲 22
,以下圖所示,發現數據更改, watcher
去更新了視圖。
有了以前的代碼與流程圖結合,我想對於Vue
源碼分析應該更加了解了,那麼咱們再次來梳理一下咱們學習的知識點。依舊是結合下面流程圖:
Compile
來
解析指令,找到
{{xxx}}
、指令、事件、綁定等等,而後再
初始化視圖。但此時還有一件事情沒作,就是當數據發生變化的時候,在更新數據以前,咱們還要
訂閱數據變化,綁定更新函數,此時就須要加入訂閱者
Watcher
了。當訂閱者觀察到數據變化時,就會觸發
Updater
來更新視圖。
固然,建立 Watcher
的前提時要進行數據劫持來監聽全部屬性,因此建立了 Observer.js
文件。在 get
方法中,須要給 Dep
通知變化,此時就須要將 Dep
的依賴收集關聯起來,而且添加訂閱者 Watcher
(這個 Watcher
在 Complie
訂閱數據變化,綁定更新函數時就已經建立了的)。此時 Dep
訂閱器裏就有不少個 Watcher
了,有多少個屬性就對應有多少個 Watcher
。
那麼,咱們舉一個簡單例子來走一下上述流程圖:
假設本來 data
數據中有一個 a:1
,此時咱們進行更新爲 a:10
,因爲早已經對咱們的數據進行了數據劫持而且監聽了全部屬性,此時就會觸發 set
方法,在 set
方法裏就會通知 Dep
訂閱器發生了變化,而後就會通知相關 Watcher
觸發 update
函數來更新視圖。而這些訂閱者 Watcher
在 Complie
訂閱數據變化,綁定更新函數時就已經建立了。
上述,咱們基本完成了數據驅動視圖,如今咱們來完成一下經過視圖的變化來更新數據,真正實現雙向數據綁定的效果。
在咱們 complieUtil
指令處理集合中的 model
模塊,給咱們當前節點綁定一個 input
事件便可。咱們能夠經過 e.target.value
來獲取當前 input
輸入框的值。而後比對一下舊值和新值是否相同,若是不一樣的話,就得須要更新,調用 setVal
方法(具體見下文代碼)。
model(node,expr,vm){
let value = this.getVal(expr,vm)
// 訂閱數據變化時 綁定更新函數 更新視圖的變化
// 數據==>視圖
new Watcher(vm,expr,(newVal)=>{
this.updater.modelUpdater(node,newVal)
})
// 視圖==》數據
node.addEventListener('input',(e)=>{
var newValue = e.target.value
if(value == newValue) return
// 設置值
this.setVal(expr,vm,newValue)
value = newValue
})
this.updater.modelUpdater(node,value)
},
複製代碼
setVal
和 getVal
二者沒有多大區別,只是 set
時多了一個 inputVal
。它們都是找到最底層 key
值,而後更新 value
值。
getVal(expr,vm){
return expr.split('.').reduce((data,currentVal)=>{
// console.log(currentVal)
return data[currentVal]
},vm.$data)
},
setVal(expr,vm,inputVal){
return expr.split('.').reduce((data,currentVal)=>{
data[currentVal] = inputVal
},vm.$data)
},
複製代碼
更新 bug
:在上文,對於 v-text
指令處,咱們遺漏了綁定 Watcher
步驟,如今進行補充。
text(node,expr,vm){
let value;
if(expr.indexOf('{{') !== -1){
value = expr.replace(/\{\{(.+?)\}\}/g, (...args)=>{
// 綁定watcher從而更新視圖
new Watcher(vm,args[1],()=>{
this.updater.textUpdater(node,this.getContentVal(expr,vm))
// console.log(expr);
})
return this.getVal(args[1],vm);
})
}else{ // 也多是v-text='obj.name' v-text='msg'
value = this.getVal(expr,vm)
// 綁定watcher從而更新視圖
new Watcher(vm,expr,(newVal)=>{
this.updater.textUpdater(node,newVal)
// console.log(expr);
})
}
this.updater.textUpdater(node,value)
},
複製代碼
最終,當咱們更改 input
輸入框中的值時,發現其餘節點也跟着修改,這表明咱們的數據進行了修改,相關訂閱者觸發了 update
方法,雙向綁定功能實現!
咱們在使用 vue
的時候,一般能夠直接 vm.msg
來獲取數據,這是由於 vue
源碼內部作了一層代理.也就是說把數據獲取操做 vm
上的取值操做 都代理到 vm.$data
上。
class myVue{
constructor(options){
this.$el = options.el
this.$data = options.data
this.$options = options
if(this.$el){
// 1.實現數據觀察者
new Observer(this.$data)
// 2.實現指令解析器
new Compile(this.$el,this)
// 3.實現proxy代理
this.proxyData(this.$data)
}
// console.log(this)
}
proxyData(data){
for(const key in data){
Object.defineProperty(this,key,{
get(){
return data[key]
},
set(newVal){
data[key] = newVal
}
})
}
}
}
複製代碼
咱們簡單測試一下,例如咱們給 button
綁定一個 sayHi()
事件,經過設置 proxy
作了一層代理後,咱們不須要像後面那樣經過 this.$data.person.name
來更改咱們的數據,而直接能夠經過 this.person.name
來獲取咱們的數據。
methods: {
sayHi() {
this.person.name = '超逸'
//this.$data.person.name = 'Chaoyi'
console.log(this)
}
}
複製代碼
請闡述一下你對 MVVM
響應式的理解
vue.js
則是採用數據劫持結合發佈者-訂閱者模式的方式,經過 Object.defineProperty()
來劫持各個屬性的getter
,setter
,在數據變更時發佈消息給訂閱者,觸發相應的監聽回調。
MVVM
做爲數據綁定的入口,整合Observer
、Compile
和 Watcher
三者,經過Observer
來監聽本身的model
數據變化,經過Compile
來解析編譯模板指令,最終利用Watcher
搭起Observer
和Compile
之間的通訊橋樑,達到數據變化 -> 視圖更新;視圖交互變化(input
) -> 數據model
變動的雙向綁定效果。
最開始,咱們實現了 Compile
來解析指令,找到 {{xxx}}
、指令、事件、綁定等等,而後再初始化視圖。但此時還有一件事情沒作,就是當數據發生變化的時候,在更新數據以前,咱們還要訂閱數據變化,綁定更新函數,此時就須要加入訂閱者Watcher
了。當訂閱者觀察到數據變化時,就會觸發Updater
來更新視圖。
固然,建立 Watcher
的前提時要進行數據劫持來監聽全部屬性,因此建立了 Observer.js
文件。在 get
方法中,須要給 Dep
通知變化,此時就須要將 Dep
的依賴收集關聯起來,而且添加訂閱者 Watcher
(這個 Watcher
在 Complie
訂閱數據變化,綁定更新函數時就已經建立了的)。此時 Dep
訂閱器裏就有不少個 Watcher
了,有多少個屬性就對應有多少個 Watcher
。
那麼,咱們舉一個簡單例子來走一下上述流程圖:
假設本來 data
數據中有一個 a:1
,此時咱們進行更新爲 a:10
,因爲早已經對咱們的數據進行了數據劫持而且監聽了全部屬性,此時就會觸發 set
方法,在 set
方法裏就會通知 Dep
訂閱器發生了變化,而後就會通知相關 Watcher
觸發 update
函數來更新視圖。而這些訂閱者 Watcher
在 Complie
訂閱數據變化,綁定更新函數時就已經建立了。
總算是把這篇長文寫完了,字數也是達到將近 1w8。經過學習 Vue MVVM源碼
,對於 Vue
雙向數據綁定這一塊理解也更加深入了。固然,本文書寫的代碼還算是比較簡單,也參考了大佬的博客與代碼,同時,也存在不足而且小部分功能沒有實現,相較於源碼來講仍是有不少可優化和可重構的地方,那麼也歡迎小夥伴們來 PR
。一塊兒來動手實現 mvvm
。
本篇博客參考文獻 笑馬哥:Vue的MVVM實現原理 github:mvvm 視頻學習:Vue源碼解析