Vue源碼中實現依賴收集(觀察者模式),實現了三個類:
Dep:扮演觀察目標的角色,每一個數據都會有Dep類實例,它內部有個subs隊列,subs就是subscribers的意思,保存着依賴本數據的觀察者,當本數據變更時,調用dep.notify()通知觀察者Watcher:扮演觀察者的角色,進行觀察者函數的包裝處理。如render()函數,會被進行包裝成一個Watcher實例Observer:輔助的可觀測類,數組/對象通過它的轉化,可成為可觀測數據
index.html
<!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">
<div @click="getValue">
<p k-text="inputData"></p>
</div>
<p @click="getValue">阿斯加德八級考試{{ form.test }}你很快就發{{ form.test }}{{ obj.a.c }}</p>
<p>{{ inputData }}</p>
<input :value="inputData" @input="setInput" />
</div>
</body>
<!-- 瀏覽器不支持es6語法 -->
<script src="./kvue.js"></script>
<script>
new Kvue({
el: 'app',
data: {
form: {
test: '1124dsa'
},
inputData: '',
test: 111,
obj: {
a:{
c:3
}
}
},
watch: {
inputData(val) {
console.log(val)
}
},
created(){
console.log(this.test)
console.log(this.obj)
},
methods: {
getValue() {
console.log(this)
},
setInput(event) {
this.inputData = event.target.value
}
}
})
</script>
</html>
kvue.js
function getData(data, vm) {
return data.call(vm, vm)
}
// 多層對象時獲取對象的值
function parsePath (path) {
const segments = path.split('.');
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) { return }
obj = obj[segments[i]]
}
return obj
}
}
function initMethods(vm, methods) {
for(let key in methods) {
vm[key] = typeof methods[key] !== 'function' ? function(){} : methods[key].bind(vm)
}
}
function initWatch(vm, watch) {
for(let key in watch) {
new Watcher(vm, key, watch[key])
}
}
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/
class Kvue{
constructor(option) {
// 初始化data
let data = option.data
data = this.$data = typeof data === 'function'
? getData(data, this)
: data || {}
const keys = Object.keys(data)
var i = keys.length
while(i--) {
this.proxyData(keys[i])
}
this.observe(this.$data)
// 初始化watch
initWatch(this, option.watch)
// 初始化methods
initMethods(this, option.methods)
// 模板編譯-
new Compile(this,option.el)
// 生命周期
if(option.created){
option.created.call(this)
}
}
// 觀察者
observe(obj) {
// 判斷是否符合標准,有值並且是個對象,如果不是對象則不進行遍歷操作
if(!obj || Object.prototype.toString.call(obj) !== '[object Object]') return
// 遍歷obj對象的屬性,獲取屬性值后執行數據響應化處理
Object.keys(obj).forEach((key) => {
// 響應化處理
this.defineProperty(obj, key, obj[key])
})
}
defineProperty(obj,key,val) {
// 調用觀察者,如果val是對象會再執行遍歷對象屬性值的操作
this.observe(val)
// 對象的每個屬性都會執行defineProperty方法,也意味着每個屬性都有一個dep實例,
// 用addDep來收集這個屬性在使用的時候的Dep.tergat(前提是Dep.tergat有值)
// 使用數組是因為一個屬性在模板中可能有多個地方都在用
const dep = new Dep()
Object.defineProperty(obj, key, {
get(){
// 依賴收集-收集 Watcher
Dep.tergat&&dep.addDep(Dep.tergat)
return val
},
set(newVal) {
if(newVal === val) {
return
}
val = newVal
// 當數據發生變化時執行
dep.notify()
}
})
}
// 代理函數-一次賦值就可以影響this[key]和this.$data[key],也不會影響this.$data綁定的依賴
proxyData(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(newVal) {
this.$data[key] = newVal
}
})
}
}
class Dep {
constructor() {
// deps用來儲存watcher
this.deps = []
}
addDep(dep) {
this.deps.push(dep)
}
notify() {
// 更新watcher方法
this.deps.forEach((watcher) => {
watcher&&watcher.update()
})
}
}
// 模板編譯的時候會獲取watcher
class Watcher {
constructor(vm,key,cb) {
this.vm = vm
this.key = key
// 將Dep.tergat綁定上watcher
Dep.tergat = this
// 獲取this.vm[key]的時候會執行key的get方法,從而將Watcher收集到deps
parsePath(this.key)(this.vm)
// 回調方法用來更新模板內容
this.cb = cb
// 初始化更新
this.update()
}
update() {
this.cb.call(this.vm, parsePath(this.key)(this.vm))
}
}
class Compile {
// vm是指vue的this,el用來獲取html數據
constructor(vm, el) {
this.$vm = vm
this.$el = document.getElementById(el)
// 如果存在$el節點
if(this.$el) {
this.$fragment = this.nodeFragment(this.$el)
// 執行編譯
this.compile(this.$fragment)
// 將編譯后的元素添加到el
this.$el.appendChild(this.$fragment)
}
}
nodeFragment(el) {
// DocumentFragment節點不屬於文檔樹,繼承的parentNode屬性總是null。
// 它有一個很實用的特點,當請求把一個DocumentFragment節點插入文檔樹時,插入的不是DocumentFragment自身,而是它的所有子孫節點,即插入的是括號里的節點。
// 這個特性使得DocumentFragment成了占位符,暫時存放那些一次插入文檔的節點。它還有利於實現文檔的剪切、復制和粘貼操作。
// 另外,當需要添加多個dom元素時,如果先將這些元素添加到DocumentFragment中,再統一將DocumentFragment添加到頁面,會減少頁面渲染dom的次數,效率會明顯提升。
// 如果使用appendChid方法將原dom樹中的節點添加到DocumentFragment中時,會刪除原來的節點
// 創建一個虛擬的節點對象
const frag = document.createDocumentFragment()
// 將el的子元素添加到createDocumentFragment節點
let child
while (child = el.firstChild) {
// 使用appendChid方法在向frag添加子元素的同時刪除了el的子元素
frag.appendChild(child)
}
return frag
}
compile(frag) {
const nodes = frag.childNodes || []
// Array.from將nodelist轉為可以循環的數組
Array.from(nodes).forEach((node, index) => {
// html文檔中的回車空格等也是一個node節點(#text)
// console.log(frag,node,node.nodeType)
if(this.isElement(node)) {
// 如果是一個元素節點則獲取它的attributes,根據attributes來獲取指令和方法綁定等
const attributes = node.attributes
Array.from(attributes).forEach((attr) => {
const name = attr.name
const value = attr.value
if(this.isDirective(name)){
// 獲取指令名稱
const directive = name.substring(2)
// 如果存在這個指令,則執行這個指令
this[directive] && this[directive](node, this.$vm, value)
}
if(this.isEvent(name)) {
// 指定事件名。
const event = name.substring(1)
this.eventHandler(node, this.$vm, event, value)
}
})
}
if(this.isTextNode(node)) {
this.textNode(node, this.$vm)
}
if(node.childNodes){
// 遞歸
this.compile(node)
}
})
}
update(node, vm, exp, type) {
const updateFn = this[`update${type}`]
// 依賴綁定
new Watcher(vm,exp,function(value){
updateFn&&updateFn(node,value)
})
}
// nodeType 屬性返回以數字值返回指定節點的節點類型
// Node.ELEMENT_NODE 1 一個 元素 節點,例如 <p> 和 <div>。
// Node.TEXT_NODE 3 Element 或者 Attr 中實際的 文字
// Node.CDATA_SECTION_NODE 4 一個 CDATASection,例如 <!CDATA[[ … ]]>。
// Node.PROCESSING_INSTRUCTION_NODE 7 一個用於XML文檔的 ProcessingInstruction ,例如 <?xml-stylesheet ... ?> 聲明。
// Node.COMMENT_NODE 8 一個 Comment 節點。
// Node.DOCUMENT_NODE 9 一個 Document 節點。
// Node.DOCUMENT_TYPE_NODE 10 描述文檔類型的 DocumentType 節點。例如 <!DOCTYPE html> 就是用於 HTML5 的。
// Node.DOCUMENT_FRAGMENT_NODE 11 一個 DocumentFragment 節點
// 是否是元素節點
isElement(node) {
return node.nodeType === 1
}
isTextNode(node) {
return node.nodeType === 3
}
// 是否是指令,以k-開頭
isDirective(attrName) {
return attrName.startsWith('k-')
}
// 是否是方法
isEvent(attrName) {
return attrName.startsWith('@')
}
/*
* @作用: text指令函數
* @params: node 操作的節點
* @params: vm kvue的實例
* @params: exp 節點的屬性value值(以此來綁定對應的kvue的data)
*/
text(node, vm, exp) {
// 綁定更新方法
this.update(node, vm, exp, 'Text')
}
textNode(node, vm) {
const execs = defaultTagRE.exec(node.textContent)
if(execs){
const exp = execs[1].trimStart().trimEnd()
this.update(node, vm, exp, 'TextNode')
// 有多個{{}}時需要進行遞歸修改
this.textNode(node, vm)
}
}
// 文本指令更新方法
updateText(node, value) {
node.textContent = value
}
// 更新文本節點信息
updateTextNode(node, value) {
const textContent = node.textContent
if(textContent) {
node.textContent = textContent.replace(defaultTagRE, value)
}
}
// 綁定方法
eventHandler(node, vm, event, exp) {
const fn = vm[exp]
node.addEventListener(event,fn)
}
}

