創建KVue類,保存options和data,需要實現響應式、數據代理、編譯模板的功能
class KVue {
constructor(options) {
this.$options = options
this.$data = options.data
// 響應式處理
observe(this.$data)
// 數據代理
Proxy(this, '$data')
// 編譯模板
new Compile(this, options.el)
}
}
- 數據代理
// 數據代理函數,將$data中的數據代理到vm上,這樣我們就能使用vm.xx來獲取數據
function Proxy (vm, prop) {
// 遍歷vm[prop]中的數據,使用object.defineProperty()將每一個數據綁定到vm上
// 使用vm.xx時,如果是獲取值,則會觸發get,設置值觸發set
Object.keys(vm[prop]).forEach(key => {
Object.defineProperty(vm, key, {
get () {
return vm[prop][key]
},
set (value) {
vm[prop][key] = value
}
})
})
}
- 數據響應式處理,和響應式基礎的代碼是一樣的,只是這裏我們創建了一個Observer類來進行數據劫持和響應化
// 1、拿出數組的原型
// 2、克隆數組的原型,不然所有數組都變了
// 3、修改七個方法
// 4、覆蓋需要響應式處理的數組的原型
const arrayProto = Array.prototype
const newProto = Object.create(arrayProto)
const methods = ['push', 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice']
methods.forEach(method => {
newProto[method] = function () {
// 執行這個方法原有的功能
arrayProto[method].apply(this, arguments)
// 這裏添加一個更新通知,並重新對arr做響應式處理
observe(this)
console.log(method + '執行了!')
}
})
// 數據劫持構造函數,對所有屬性做響應式處理
class Observer {
constructor(value) {
this.value = value
this.walk(value)
}
walk (val) {
// 判斷是否數組
if (Array.isArray(val)) {
val.__proto__ = newProto
for (let i = 0; i < val.length; i++) {
defineReactive(val[i])
}
} else {
Object.keys(val).forEach(key => {
defineReactive(val, key, val[key])
})
}
}
}
// 監控數據,如果是對象或數組需要做響應式處理
function observe (obj) {
if (!obj || typeof obj !== 'object') {
return
}
obj.__ob__ = new Observer(obj)
}
// 響應式處理數據
function defineReactive (obj, key, val) {
// 遞歸處理val
observe(val)
Object.defineProperties(obj, key, {
get () {
return val
},
set (newVal) {
if (newVal !== val) {
observe(newVal)
val = newVal
}
}
})
}
- 編譯模板
編譯模板中主要完成兩個功能:解析指令和插值表達式、每個指令對應的方法
class Compile {
constructor(vm, el) {
this.$vm = vm
this.$el = document.querySelector(el)
this.compile(this.$el)
}
compile(el) {
// 遍歷當前所有的子節點,判斷它是元素節點還是文本節點,nodeType等於1是html元素,等於3是文本
const childNodes = el.childNodes
Array.from(childNodes).forEach(node => {
if (node.nodeType === 1) {
// 元素節點,獲取他的attr,並判斷是否是指令
this.compileElement(node)
} else if (node.nodeType === 3) {
// 文本節點,判斷是否插值表達式{{aaa}}並解析
this.compileText(node)
}
// 子節點還有子節點,則要遞歸處理
if (node.childNodes) {
this.compile(node)
}
})
}
// 編譯html元素,或取其中的指令
compileElement(node) {
// 獲取node的所有attr進行遍歷,得到我們需要的指令並進行對應操作
const nodeAttr = node.attributes
Array.from(nodeAttr).forEach(attr => {
// 獲取屬性的名稱和值,根據名稱判斷是否是指令,這裏支持‘k-’開頭的指令
let attrName = attr.name
let attrValue = attr.value
if (attrName.indexOf('k-') === 0) {
// 如果是指令,執行對應的方法
const dir = attrName.substring(2)
this[dir] && this[dir](node, attrValue)
}
})
}
// 編譯文本,判斷是否插值表達式,並觸發更新函數
compileText(node) {
// /\{\{(.*)\}\}/.test(node.textContent)匹配{{}}並把大括號裏面的文本賦值給RegExp.$1
if (/\{\{(.*)\}\}/.test(node.textContent)) {
node.textContent = this.$vm[RegExp.$1]
this.update(node, RegExp.$1, 'text')
}
}
// 更新方法,接收節點、值、指令名稱三個參數
update(node, exp, dir) {
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
// 觸發Watcher去更新視圖
}
// k-html對應的方法
html(node, exp) {
node.textContent = this.$vm[exp]
this.update(node, exp, 'html')
}
htmlUpdater(node, val) {
node.innerHTML = val
}
// k-text對應的方法
text(node, exp) {
node.textContent = this.$vm[exp]
this.update(node, exp, 'text')
}
textUpdater(node, val) {
node.textContent = val
}
}
- 依賴收集
所謂依賴,就是視圖中所使用的data中的某個key。依賴收集就就是給每一個依賴創建一個Watcher來維護他,相同key的Watcher使用一個Dep來管理,當數據變化時,通過這個個Dep統一通知更新。
// 監聽數據的變化,然後更新視圖。接收key和它對應的更新函數
class Watcher {
constructor(vm, key, fn) {
this.$vm = vm
this.$key = key
this.$updateFn = fn
// 這裏將dep的target設置爲當前的這個watcher,然後通過觸發這個key的getter,將自己push到key對應的dep中去,實現一個key對應一個dep,一個dep對應多個Watcher
Dep.target = this
this.$vm[this.$key]
Dep.target = null
}
// 觸發key的對應更新函數
update() {
this.$updateFn.call(this.$vm, this.$vm[this.$key])
}
}
// 依賴,管理Watcher
class Dep {
constructor() {
this.watchers = []
}
addDep(watcher) {
this.watchers.push(watcher)
}
// 當key變化的時候,通知這個key對應的所有Watcher更新視圖
notify() {
this.watchers.forEach(watcher => {
watcher.update()
})
}
然後我們需要修改一下defineReactive方法,每響應化一個變量,都創建一個dep,這個變量的getter和setter都能訪問到它。
function (obj, key, val) {
// 遞歸處理val
observe(val)
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target)
return val
},
set(newVal) {
if (newVal !== val) {
observe(newVal)
val = newVal
// 通知watcher更新視圖
dep.notify()
}
}
})
}
compile中的update方法也需要創建Watcher,去更新視圖。
// 更新方法,接收節點、值、指令名稱三個參數
update(node, exp, dir) {
const fn = this[dir + 'Updater']
fn && fn(node, this.$vm[exp])
// 觸發Watcher去更新視圖
new Watcher(this.$vm, exp, function(val) {
fn && fn(node, val)
})
}