Vue 源碼解析 06-手寫自己的 Vue
最近一段時間一直在研究 Vue 的源碼,突然間想寫一個乞丐的 Vue 實現,爲了理一下自己的思路,同時也作爲一個階段性的總結.
實現雙向數據綁定
Vue 雙向綁定看這裏
Vue2.0/1.0 雙向數據綁定簡單來說就是利用了 Object.defineProperty()和觀察者模式對 data 數據進行數據劫持.
廢話不多說,直接上代碼
Watcher 實現數據的更新操作
//Watcher,觀察者,真正執行更新操作的角色
class Watcher{
constructor(vm,key,update){
//保存傳入的選項
this.vm = vm
this.key = key
this.update = update
//將當前watcher添加到Dep中
Dep.target = this
this.vm[this.key]
Dep.target = null
}
//用來執行我們的更新函數
update(){
this.update&&this.update(this.vm[this.key])
}
}
Dep 維護更新隊列和通知更新
//Dep負責維護一個更新隊列,
class Dep{
constructor(){
this._dep = []
}
//添加更新隊列(這是一個乞丐版的)
addDep(watcher){
this._dep.push(watcher)
}
//通知隊列更新
notify(){
this._dep.forEach(item=>item())
}
}
observer 的實現
class Vue{
//構造函數接收配置項,將其保存起來
constructor(options){
this.$options = options||{}
this.$data = options.data||{}
//執行響應化處理
this.$data&&this.observe(this.$data)
//執行編譯
new Compile(options.el, this)
}
observe(obj){
//響應化的數據必須是對象
if(!obj||typeof obj !=='object')return
//遍歷
Object.keys(obj).forEach(key=>{
//執行響應化
this.defineReactive(obj,key,obj[key])
//代理數據(將data數據代理到當前vue實例上)
this.proxyData(key)
})
}
//數據劫持
defineReactive(obj,key,val){
//當前val爲對象時,遞歸處理
this.observe(val)
//每一個key都對應一個dep,用來維護更新隊列
const dep = new Dep()
Object.defineProperty(obj,key,{
get(){
Dep.target&&dep.addDep(Dep.target)
return val
},
set(newVal){
if(newVal!==val){
val = newVal
//通知更新隊列更新
dep.notify()
}
}
})
}
//代理數據
proxyData(key){
Object.defineProperty(this,key,{
get(){
return this.$data[key]
},
set(newVal){
this.$data[key]=newVal
//如果新添加的是一個對象,繼續響應化處理
this.observe(this.$data[key])
}
})
}
}
編譯的實現
編譯的實現原理很簡單,我們可以簡化爲三步:
- 獲取並遍歷 DOM 樹
- 如果是文本節點,獲取{{}}的內容並解析替換
- 如果是元素節點,訪問節點特性,截取“v-”和@開頭的並解析
現在看一下代碼的實現
class Compile{
//接收一個宿主元素,確定掛載目標,同時接收一個當前vue實例
constructor(vm,el){
this.$vm = vm
this.$el = document.querySelector(el)
//如果el存在,則執行編譯
this.$el&&this.compile(this.$el)
}
//編譯
compile(el){
//獲取所有子元素,獲取DOM樹
const childNodes = el.childNodes
//遍歷所有子元素
Array.from(childNodes).forEach(node=>{
//判斷節點類型
if(this.isElement(node)){
//如果是元素節點,執行元素節點更新
this.compileElement(node)
}else if(this.isText(node)){
//如果是文本節點,執行文本節點更新
this.compileText(node)
}
//如果子元素下面還有子元素,遞歸處理
if(node.childNodes&&node.childNodes.length>0){
this.compile(node)
}
})
}
//編譯元素節點
compileElement(node){
//獲取節點特性
const attrs = node.attributes
//遍歷處理特性
Array.from(attrs).forEach(item=>{
//獲取特性信息
const name = item.name
const val = item.value
//如果包含“v-”,我們認爲是指令
if(name.indexOf('v-')>-1){
//截取指令名稱
const dir = name.substring('2')
//如果存在指令更新函數,則進行更新
this.update(node,val,dir)
}else if(name.indexOf('@')>-1){
//@認爲是監聽事件
const dir = name.substring(1)
node.addEventListener(dir,this.$vm[val].bind(this.$vm));
}
})
}
//編譯文本節點
compileText(node){
//獲取數據內容
const exp = RegExp.$1
//執行更新
this.text(node,exp)
}
text(node,key){
this.update(node,key,'text')
}
//雙向數據綁定
model(node,key){
//v-model的原理就是一個監聽事件+“類似v-text”的語法糖
this.update(node,key,'model')
//添加input的監聽事件
node.addEventListener('input',event=>{
this.$vm[key]=event.target.value
})
}
html(node,key){
this.update(node,key,'html')
}
//update函數
update(node,key,dir){
//獲取具體的更新函數
const updater =this[dir+'Updater']
updater&&updater.call(this,node,this.$vm[key])
//添加Watcher
new Watcher(this.$vm,key,val=>{
updater&&updater.call(this,node,val)
})
}
//更新text文本
textUpdater(node,val){
node.textContent =val
}
//更新v-html
htmlUpdater(node,val){
node.innerHTML= val
//響應式的編譯新添加的子節點
this.compile(node)
}
//model更新函數
modelUpdater(node,val){
//value屬性賦值
node.value = val
}
//判斷是否爲元素節點
isElement(node){
//元素節點的nodeType類型爲1
return node.nodeType===1
}
//判斷是否爲文本節點
isText(node){
//不僅要判斷元素類型,並且文本內容包含{{}}
const res = node.nodeType===3&&/\{\{.*\}\}/.test(node.textContent)
return res
}
}
寫在最後
以上,就是一個乞丐版的 Vue,簡單的實現了 Vue 的雙向數據綁定和 DOM 編譯解析更新.這裏只是實現了一個簡單的函數更新,Vue2.0 裏面的 Watcher.run()函數是進行虛擬 DOM 的更新.
雖然是乞丐版的實現,但是感覺思路是相通的:通過 Object.defineProperty 實現數據劫持,通過 Compile 模塊實現 DOM 的更新.