主要是經過數據劫持和發佈訂閱一塊兒實現的html
Observe監聽器
劫持數據, 感知數據變化, 發出通知給訂閱者, 在get中將訂閱者
添加到訂閱器
中Dep消息訂閱器
存儲訂閱者, 通知訂閱者調用更新函數訂閱者Wather
取出模型值,更新視圖解析器Compile
解析指令, 更新模板數據, 初始化視圖, 實例化一個訂閱者, 將更新函數綁定到訂閱者上, 能夠在接收通知二次更新視圖, 對於v-model
還須要監聽input
事件,實現視圖到模型的數據流動 <div id="app">
<form>
<input type="text" v-model="username">
</form>
<p v-bind="username"></p>
</div>
複製代碼
#app
input
, 使用v-model
指令綁定數據username
v-bind
綁定數username
簡單的模擬Vue類node
將實例化時的選項options
, 數據options.data
進行保存 此外,經過options.el
獲取dom元素,存儲到$el
上bash
class MyVue {
constructor(options) {
this.$options = options
this.$el = document.querySelector(this.$options.el)
this.$data = options.data
}
}
複製代碼
實例化一個MyVue,傳遞選項進去,選項中指定綁定的元素el
和數據對象data
app
const myVm = new MyVue({
el: '#app',
data: {
username: 'LastStarDust'
}
})
複製代碼
劫持數據是爲了修改數據的時候能夠感知, 發出通知, 執行更新視圖操做dom
class MyVue {
constructor(options) {
// ...
// 監視數據的屬性
this.observable(this.$data)
}
// 遞歸遍歷數據對象的全部屬性, 進行數據屬性的劫持 { username: 'LastStarDust' }
observable(obj) {
// obj爲空或者不是對象, 不作任何操做
const isEmpty = !obj || typeof obj !== 'object'
if(isEmpty) {
return
}
// ['username']
const keys = Object.keys(obj)
keys.forEach(key => {
// 若是屬性值是對象,遞歸調用
let val = obj[key]
if(typeof val === 'object') {
this.observable(val)
}
// this.defineReactive(this.$data, 'username', 'LastStarDust')
this.defineReactive(obj, key, val)
})
return obj
}
// 數據劫持,修改屬性的get和set方法
defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`取出${key}屬性值: 值爲${val}`)
return val
},
set(newVal) {
// 沒有發生變化, 不作更新
if(newVal === val) {
return
}
console.log(`更新屬性${key}的值爲: ${newVal}`)
val = newVal
}
})
}
}
複製代碼
存儲訂閱者, 收到通知時,取出訂閱者,調用訂閱者的update方法函數
// 定義消息訂閱器
class Dep {
// 靜態屬性 Dep.target,這是一個全局惟一 的Watcher,由於在同一時間只能有一個全局的 Watcher
static target = null
constructor() {
// 存儲訂閱者
this.subs = []
}
// 添加訂閱者
add(sub) {
this.subs.push(sub)
}
// 通知
notify() {
this.subs.forEach(sub => {
// 調用訂閱者的update方法
sub.update()
})
}
}
複製代碼
爲每個屬性添加訂閱者ui
defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 會在初始化時, 觸發屬性get()方法,來到這裏Dep.target有值,將其做爲訂閱者存儲起來,在觸發屬性的set()方法時,調用notify方法
if(Dep.target) {
dep.add(Dep.target)
}
console.log(`取出${key}屬性值: 值爲${val}`)
return val
},
set(newVal) {
// 沒有發生變化, 不作更新
if(newVal === val) {
return
}
console.log(`更新屬性${key}的值爲: ${newVal}`)
val = newVal
dep.notify()
}
})
}
複製代碼
從模型中
取出數據並更新視圖this
// 定義訂閱者類
class Wather {
constructor(vm, exp, cb) {
this.vm = vm // vm實例
this.exp = exp // 指令對應的字符串值, 如v-model="username", exp至關於"username"
this.cb = cb // 回到函數 更新視圖時調用
this.value = this.get() // 將本身添加到消息訂閱器Dep中
}
get() {
// 將當前訂閱者做爲全局惟一的Wather,添加到Dep.target上
Dep.target = this
// 獲取數據,觸發屬性的getter方法
const value = this.vm.$data[this.exp]
// 在執行添加到消息訂閱Dep後, 重置Dep.target
Dep.target = null
return value
}
// 執行更新
update() {
this.run()
}
run() {
// 從Model模型中取出屬性值
const newVal = this.vm.$data[this.exp]
const oldVal = this.value
if(newVal === oldVal) {
return false
}
// 執行回調函數, 將vm實例,新值,舊值傳遞過去
this.cb.call(this.vm, newVal, oldVal)
}
}
複製代碼
v-model
指令, 監聽'input'事件,實現視圖更新是,去更新模型的數據 // 定義解析器
// 解析指令,替換模板數據,初始視圖
// 模板的指令綁定更新函數, 數據更新時, 更新視圖
class Compile {
constructor(el, vm) {
this.el = el
this.vm = vm
this.init(this.el)
}
init(el) {
this.compileEle(el)
}
compileEle(ele) {
const nodes = ele.children
// 遍歷節點進行解析
for(const node of nodes) {
// 若是有子節點,遞歸調用
if(node.children && node.children.length !== 0) {
this.compileEle(node)
}
// 指令時v-model而且是標籤是輸入標籤
const hasVmodel = node.hasAttribute('v-model')
const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1
if(hasVmodel && isInputTag) {
const exp = node.getAttribute('v-model')
const val = this.vm.$data[exp]
const attr = 'value'
// 初次模型值推到視圖層,初始化視圖
this.modelToView(node, val, attr)
// 實例化一個訂閱者, 將更新函數綁定到訂閱者上, 將來數據更新,能夠更新視圖
new Wather(this.vm, exp, (newVal)=> {
this.modelToView(node, newVal, attr)
})
// 監聽視圖的改變
node.addEventListener('input', (e) => {
this.viewToModel(exp, e.target.value)
})
}
// 指令時v-bind
if(node.hasAttribute('v-bind')) {
const exp = node.getAttribute('v-bind')
const val = this.vm.$data[exp]
const attr = 'innerHTML'
// 初次模型值推到視圖層,初始化視圖
this.modelToView(node, val, attr)
// 實例化一個訂閱者, 將更新函數綁定到訂閱者上, 將來數據更新,能夠更新視圖
new Wather(this.vm, exp, (newVal)=> {
this.modelToView(node, newVal, attr)
})
}
}
}
// 將模型值更新到視圖
modelToView(node, val, attr) {
node[attr] = val
}
// 將視圖值更新到模型上
viewToModel(exp, val) {
this.vm.$data[exp] = val
}
}
複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<form>
<input type="text" v-model="username">
</form>
<div>
<span v-bind="username"></span>
</div>
<p v-bind="username"></p>
</div>
<script>
class MyVue {
constructor(options) {
this.$options = options
this.$el = document.querySelector(this.$options.el)
this.$data = options.data
// 監視數據的屬性
this.observable(this.$data)
// 編譯節點
new Compile(this.$el, this)
}
// 遞歸遍歷數據對象的全部屬性, 進行數據屬性的劫持 { username: 'LastStarDust' }
observable(obj) {
// obj爲空或者不是對象, 不作任何操做
const isEmpty = !obj || typeof obj !== 'object'
if(isEmpty) {
return
}
// ['username']
const keys = Object.keys(obj)
keys.forEach(key => {
// 若是屬性值是對象,遞歸調用
let val = obj[key]
if(typeof val === 'object') {
this.observable(val)
}
// this.defineReactive(this.$data, 'username', 'LastStarDust')
this.defineReactive(obj, key, val)
})
return obj
}
// 數據劫持,修改屬性的get和set方法
defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 會在初始化時, 觸發屬性get()方法,來到這裏Dep.target有值,將其做爲訂閱者存儲起來,在觸發屬性的set()方法時,調用notify方法
if(Dep.target) {
dep.add(Dep.target)
}
console.log(`取出${key}屬性值: 值爲${val}`)
return val
},
set(newVal) {
// 沒有發生變化, 不作更新
if(newVal === val) {
return
}
console.log(`更新屬性${key}的值爲: ${newVal}`)
val = newVal
dep.notify()
}
})
}
}
// 定義消息訂閱器
class Dep {
// 靜態屬性 Dep.target,這是一個全局惟一 的Watcher,由於在同一時間只能有一個全局的 Watcher
static target = null
constructor() {
// 存儲訂閱者
this.subs = []
}
// 添加訂閱者
add(sub) {
this.subs.push(sub)
}
// 通知
notify() {
this.subs.forEach(sub => {
// 調用訂閱者的update方法
sub.update()
})
}
}
// 定義訂閱者類
class Wather {
constructor(vm, exp, cb) {
this.vm = vm // vm實例
this.exp = exp // 指令對應的字符串值, 如v-model="username", exp至關於"username"
this.cb = cb // 回到函數 更新視圖時調用
this.value = this.get() // 將本身添加到消息訂閱器Dep中
}
get() {
// 將當前訂閱者做爲全局惟一的Wather,添加到Dep.target上
Dep.target = this
// 獲取數據,觸發屬性的getter方法
const value = this.vm.$data[this.exp]
// 在執行添加到消息訂閱Dep後, 重置Dep.target
Dep.target = null
return value
}
// 執行更新
update() {
this.run()
}
run() {
// 從Model模型中取出屬性值
const newVal = this.vm.$data[this.exp]
const oldVal = this.value
if(newVal === oldVal) {
return false
}
// 執行回調函數, 將vm實例,新值,舊值傳遞過去
this.cb.call(this.vm, newVal, oldVal)
}
}
// 定義解析器
// 解析指令,替換模板數據,初始視圖
// 模板的指令綁定更新函數, 數據更新時, 更新視圖
class Compile {
constructor(el, vm) {
this.el = el
this.vm = vm
this.init(this.el)
}
init(el) {
this.compileEle(el)
}
compileEle(ele) {
const nodes = ele.children
for(const node of nodes) {
if(node.children && node.children.length !== 0) {
// 遞歸調用, 編譯子節點
this.compileEle(node)
}
// 指令時v-model而且是標籤是輸入標籤
const hasVmodel = node.hasAttribute('v-model')
const isInputTag = ['INPUT', 'TEXTAREA'].indexOf(node.tagName) !== -1
if(hasVmodel && isInputTag) {
const exp = node.getAttribute('v-model')
const val = this.vm.$data[exp]
const attr = 'value'
// 初次模型值推到視圖層,初始化視圖
this.modelToView(node, val, attr)
// 實例化一個訂閱者, 將更新函數綁定到訂閱者上, 將來數據更新,能夠更新視圖
new Wather(this.vm, exp, (newVal)=> {
this.modelToView(node, newVal, attr)
})
// 監聽視圖的改變
node.addEventListener('input', (e) => {
this.viewToModel(exp, e.target.value)
})
}
if(node.hasAttribute('v-bind')) {
const exp = node.getAttribute('v-bind')
const val = this.vm.$data[exp]
const attr = 'innerHTML'
// 初次模型值推到視圖層,初始化視圖
this.modelToView(node, val, attr)
// 實例化一個訂閱者, 將更新函數綁定到訂閱者上, 將來數據更新,能夠更新視圖
new Wather(this.vm, exp, (newVal)=> {
this.modelToView(node, newVal, attr)
})
}
}
}
// 將模型值更新到視圖
modelToView(node, val, attr) {
node[attr] = val
}
// 將視圖值更新到模型上
viewToModel(exp, val) {
this.vm.$data[exp] = val
}
}
const myVm = new MyVue({
el: '#app',
data: {
username: 'LastStarDust'
}
})
// console.log(Dep.target)
</script>
</body>
</html>
複製代碼