原理
在創建Wvue示例的時候,將掛載在實例中的參數通過數據劫持來做一層代理。使得在訪問或者賦值時候可以進行更多的操作。通過掛載在參數中的el參數來獲取HTML,並且用compile進行分析,將具有特殊含義的,比如{{}}中的內容,@click定義的事件等進行單獨的處理。
{{}}中綁定的數據,每一個都用watch監聽。也就是在每一個new Wvue()實例中掛載的data裏面的變量,在template中每運用一次,就將會被一個watch監聽。而每一個變量的watch都將有一個Dep去統一管理。當變量變化之後,Dep會通知所有的watch去執行之前在watch中綁定的回調函數。從而實現修改data中的變量,渲染真實DOM的功能。
目標
分成三個階段,循序漸進實現{{}}、v-model、v-html、@click功能
第一階段
目錄結構
index.html
<style>
#app{
border: 1px solid red;
margin: 10px;
padding: 20px;
}
</style>
<body>
<div id="app">
<input type="text" v-modal="name">
<div class="outer">
<span>{{name}}</span>
<p><span v-html="name"></span></p>
</div>
<button @click="reset">重置</button>
</div>
</body>
<script src="./wvue.js"></script>
<script>
// 階段一
const data = {
el: '#app',
data: {
name: '米粒'
},
methods: {
reset() {
this.name = ''
}
},
}
const app = new Wvue(data)
</script>
Wvue.js
class Wvue {
constructor(option) {
this.$option = option
this.$data = option.data
this.$methods = option.methods
// 數據劫持
// 監聽數據並且做代理 使得訪問this.name即可訪問到this.$data.name
this.observer(this.$data)
// 這一步會觸發name與$data.$name的get方法 所以先回打印出get裏面的內容
console.log(this.name)
// 一定時間去修改name的內容
setTimeout(() => {
console.log('數據發生變化-----------------------------')
// 在這一步只會觸發name的set
this.name = '可愛米粒'
}, 2000)
}
observer(obj) {
if (!obj || typeof obj !== "object") {
return;
}
console.log('observer')
Object.keys(obj).forEach(key => {
this.defineProperty(obj, key, obj[key])
this.proxyObj(key)
})
}
defineProperty(obj, key, val) {
// 如果是綁定的是對象,則用迭代的方式,繼續監聽對象中的數據
this.observer(val)
// Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,
// 或者修改一個對象的現有屬性, 並返回這個對象。
Object.defineProperty(obj, key, {
get() {
console.log('defineProperty獲取')
return val
},
set(newVal) {
// 採用閉包的形式,只要Wvue沒有銷燬,則val會一直存在
console.log('defineProperty更新了', newVal)
val = newVal
}
})
}
// 做代理 使得訪問更加簡潔
proxyObj(key) {
Object.defineProperty(this, key, {
get() {
console.log('proxyObj獲取')
return this.$data[key]
},
set(newVal) {
console.log('proxyObj更新', newVal)
this.$data[key] = newVal
}
})
}
}
實際效果
用Object.defineProperty給data中的變量都設置get,set屬性。對name的賦值,就會觸發$data.$name的set屬性。根據思路,get屬性中就是收集watch放進Dep進行統一管理的地方。
另外,只要Wvue不銷燬,變量的get,set屬性就不會銷燬。
知識點:
Object.defineProperty
閉包與內存
第二階段
實現watch的創建與收集,修改Wvue.js
Wvue.js
class Wvue {
constructor(option) {
this.$option = option
this.$data = option.data
this.$methods = option.methods
this.observer(this.$data)
// ----------------新增Watcher實例,綁定回調方法,當收到通知,打印數據
new Watcher(this, 'name', () => {
console.log('watcher生效')
})
console.log(this.name)
setTimeout(() => {
console.log('數據發送變化-----------------------------')
this.name = '可愛米粒'
}, 2000)
}
observer(obj) {
if (!obj || typeof obj !== "object") {
return;
}
console.log('observer')
Object.keys(obj).forEach(key => {
this.defineProperty(obj, key, obj[key])
this.proxyObj(key)
})
}
defineProperty(obj, key, val) {
this.observer(val)
//---------------- 新增爲每一個變量都創建管理watcher的Dep實例
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
console.log('defineProperty獲取')
// 每次訪問name 都會創建一個watcher,並加入到Dep中
Dep.target !== null && dep.addDep(Dep.target)
return val
},
set(newVal) {
console.log('defineProperty更新了', newVal)
val = newVal
dep.notify()
}
})
}
proxyObj(key) {
Object.defineProperty(this, key, {
get() {
console.log('proxyObj獲取')
return this.$data[key]
},
set(newVal) {
console.log('proxyObj更新', newVal)
this.$data[key] = newVal
}
})
}
}
// -----------新增Watcher類 用於根據通知觸發綁定的回調函數
class Watcher {
constructor(vm, key ,cb) {
this.$vm = vm
this.$key = key
this.$cb = cb
// 用一個全局變量來指代當前watch
Dep.target = this
console.log('Watcher-------')
// 實際是訪問了this.name,觸發了當前變量的get,
// 當前變量的get會收集當前Dep.target指向的watcher,即當前watcher
this.$vm[this.$key]
Dep.target = null
}
update() {
// 執行
this.$cb.call(this.$vm, this.$vm[this.$key])
}
}
// -----------新增Dep類 用於收集watcher
class Dep {
constructor() {
this.dep = []
}
addDep(dep) {
console.log('addDep')
this.dep.push(dep)
}
notify() {
// 通知所有的watcher執行更新
this.dep.forEach(watcher => {
watcher.update()
})
}
}
本階段,在name的get屬性中,將name所有的watcher用Dep實例收集起來。並在set的過程中,觸發Dep中的notify方法,通知所有的watcher更新。所以我們在構造函數中,手動創建了一個watcher。在this.name="可愛米粒"的賦值操作時,就會調用watcher中的callback,打印出數據。
然而我們的watcher不可能是手動創建的,我們平時用Vue的時候,template中{{}}中的內容,就是響應式的,所以當我們改變data中的數據的時候,界面就會重新更改。所以,很明顯,每一個{{}}就需要一個watcher(在Vue1.0中,就是因爲watcher太多了,導致渲染效果差,在vue2.0之後,都改爲一個組件一個watcher)。於是在下一階段,分析html的時候, 就需要加上watcher。
第三階段
新增compile.js
class Compile {
constructor(el, vm) {
this.$vm = vm
// $el掛載的就是需要處理的DOM
this.$el = document.querySelector(el)
// 將真實的DOM元素拷貝一份作爲文檔片段,之後進行分析
const fragment = this.node2Fragment(this.$el)
// 解析文檔片段
this.compileNode(fragment)
// 將文檔片段加入到真實的DOM中去
this.$el.appendChild(fragment)
}
// https://developer.mozilla.org/zh-CN/search?q=querySelector
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node node對象
node2Fragment(el) {
// 創建空白文檔片段
const fragment = document.createDocumentFragment()
let child
// appendChild會把原來的child給移動到新的文檔中,當el.firstChild爲空時,
// while也會結束 a = undefined => 返回 undefined
while((child = el.firstChild)) {
fragment.appendChild(child);
}
return fragment
}
// 通過迭代循環來找出{{}}中的內容,v-xxx與@xxx的內容,並且單獨處理
compileNode(node) {
const nodes = node.childNodes
// 類數組的循環
Array.from(nodes).forEach(node => {
if (this.isElement(node)) {
this.compileElement(node)
} else if (this.isInterpolation(node)) {
this.compileText(node)
}
node.childNodes.length > 0 && this.compileNode(node)
});
}
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node Node.nodeType
isElement(node) {
return node.nodeType === 1;
}
// 校驗是否是文本節點 並且是大括號中的內容
isInterpolation(node) {
return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
}
compileText(node) {
const reg = /\{\{(.*?)\}\}/g
const string = node.textContent.match(reg)
// 取出大括號中的內容,並且處理
// RegExp.$1是RegExp的一個屬性,指的是與正則表達式匹配的第一個 子匹配(以括號爲標誌)字符串
// 以此類推,RegExp.$2,RegExp.$3,..RegExp.$99總共可以有99個匹配
this.text(node, RegExp.$1)
}
compileElement(node) {
const nodeAttrs = node.attributes;
Array.from(nodeAttrs).forEach(arr => {
if (arr.name.indexOf('v-') > -1) {
this[`${arr.name.substring(2)}`](node, arr.value)
}
if (arr.name.indexOf('@') > -1) {
// console.log(node, arr.value)
this.eventHandle(node, arr.name.substring(1), arr.value)
}
})
}
// 因爲是大括號裏面的內容,所以沿用之前的邏輯,都加上watcher
text(node, key) {
new Watcher(this.$vm, key, () => {
node.textContent = this.$vm[key]
})
// 第一次初始化界面, 不然如果不進行賦值操作,
// 就不會觸發watcher裏面的回調函數
node.textContent = this.$vm[key]
}
html(node, key) {
new Watcher(this.$vm, key, () => {
node.innerHTML = this.$vm[key]
})
node.innerHTML = this.$vm[key]
}
// 對@xxx事件的處理
eventHandle(node, eventName, methodName) {
node.addEventListener(eventName, () => {
this.$vm.$methods[methodName].call(this.$vm)
})
}
// v-modal的處理 不僅僅當賦值的時候回觸發watcher,並且爲input添加事件
// input中的值去修改this.$data.$xxx的值,實現雙向綁定
modal(node, key) {
console.log(node.value)
new Watcher(this.$vm, key, () => {
node.value = this.$vm[key]
})
node.value = this.$vm[key]
node.addEventListener('input', (e) => {
this.$vm[key] = e.target.value
})
}
}
Wvue.js 中Wvue的構造函數
constructor(option) {
this.$option = option
this.$data = option.data
this.$methods = option.methods
this.observer(this.$data)
// -------------- 刪除原來的手動調用watcher
// ---------------新增對HTML的解析與處理
// ---------------在這個方法中增加watche 還要將當前this指向傳入
new Compile(option.el, this)
}
index.html
<!-- 引入順序問題 -->
<script src="./compile.js"></script>
<script src="./wvue.js"></script>
因爲wvue.js中有對compile的引用,所以引入順序很關鍵。
][10]
重置之前,修改input框,會影響{{}}與v-html中綁定的值
重置之後