記一次vue原理學習記錄,涉及瞭如何進行數據劫持、數據代理、觀察者模式(發佈訂閱)、數據雙向綁定功能等知識點,也簡單實現了vue中一些經常使用功能,代碼中作了比較詳細的註釋,能夠直接複製執行查看結果javascript
index.htmlhtml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MVVM原理</title>
</head>
<body>
<div id="app">
{{school.name}}{{age}}
<div>{{age}}</div>
<input type="text" v-model="school.name">
<div>{{school.name}}</div>
<div>compute: {{getNewName}}</div>
<ul>
<li>li_1</li>
<li>li_2</li>
</ul>
<button v-on:click="changeName">methods</button>
<div v-html="htmlName"></div>
</div>
<!-- <script src="./node_modules/vue/dist/vue.js"></script> -->
<script src="mvvm.js"></script>
<script> let vm = new Vue({ // el: document.querySelector('#app'), el: '#app', data: { school: { name: '學校名' }, age: 20, htmlName: '<h1>inner html</h1>' }, methods: { changeName () { this.school.name = '我是methods方法修改的' } }, computed: { getNewName () { return this.school.name + 'new' } } }) </script>
</body>
</html>
複製代碼
mvvm.jsvue
// 基礎類 負責調度
class Vue {
constructor (options) {
this.$el = options.el
this.$data = options.data
let computed = options.computed
let methods = options.methods
// 編譯模版
if (this.$el) {
// 數據劫持 把數據所有轉化爲Object.defineProperty來定義
new Observer(this.$data)
// computed存在依賴關係,執行computed方法會取值觸發get
for (let key in computed) {
Object.defineProperty(this.$data, key, {
get: () => {
return computed[key].call(this)
}
})
}
for (let key in methods) {
Object.defineProperty(this, key, {
get () {
return methods[key]
}
})
}
// 數據代理 vm上的數據操做代理到vm.$data上
this.proxyVm(this.$data)
// 編譯模版
new Compiler(this.$el, this)
}
}
// 將vm操做代理到vm.$data上取值
proxyVm (data) {
for (let key in data) {
Object.defineProperty(this, key, {
get () {
return data[key] // 進行轉化操做
},
set (newVal) {
data[key] = newVal
}
})
}
}
}
// 對數據進行劫持
class Observer {
constructor (data) {
this.observer(data)
}
observer (data) {
// 若是是對象就進行觀察
if (data && typeof data === 'object') {
for (let key in data) {
this.defineReactive(data, key, data[key])
}
}
}
defineReactive (obj, key, value) {
// 給每一個數據都添加一個具備發佈訂閱的功能
let dep = new Dep()
Object.defineProperty(obj, key, {
get () {
// 當建立watcher時候會取到值,會觸發get,而且將watcher放到全局
Dep.target && dep.addSub(Dep.target)
return value
},
set: (newVal) => {
if (value !== newVal) {
this.observer(newVal) // 對新賦值的進行劫持
value = newVal
dep.notify() // 觸發發佈方法,通知watcher修改了數據
}
}
})
this.observer(value) // 進行遞歸劫持數據
}
}
// 觀察者模式 (發佈訂閱模式)觀察者 被觀察者
class Watcher { // 觀察者
constructor (vm, expr, cb) {
this.vm = vm
this.expr = expr
this.cb = cb
this.oldVal = this.getValue() // 先存放一個老值
}
getValue () {
Dep.target = this // 先將本身放到Dep.target
let value = CompilerUtils.getValue(this.vm, this.expr)
Dep.target = null // 觸發get後清除掉
return value
}
update () { // 更新操做據發生變化了會執行觀察者的update方法
let newVal = CompilerUtils.getValue(this.vm, this.expr)
if (newVal !== this.oldVal) {
this.cb(newVal)
}
}
}
// 發佈訂閱
class Dep {
constructor () {
this.subs = [] // 存放全部的watcher
}
// 訂閱
addSub (watcher) {
this.subs.push(watcher)
}
// 發佈
notify () { // 執行全部watcher的update方法
this.subs.forEach(watcher => watcher.update())
}
}
// 編譯類
class Compiler {
constructor (el, vm) { // 傳入 '#app' || document.querySelector('#app')
this.vm = vm
// 判斷el是否爲元素節點
this.el = this.isElementNode(el) ? el : document.querySelector(el)
// console.log(this.el)
// 將元素使用文檔碎片存到內存中
this.fragment = this.el2fragment(this.el)
// console.log('fragment:', this.fragment)
// 編譯模版 將fragment中的變量進行替換
this.compile(this.fragment)
// 從新將fragment塞回el
this.el.appendChild(this.fragment)
}
isElementNode (node) { // 判斷是否元素節點
return node.nodeType === 1
}
// 把節點存到內存中
el2fragment (node) {
// 建立一個文檔碎片
let fragment = document.createDocumentFragment()
let firstChild
while (firstChild = node.firstChild) {
// console.log(firstChild)
// fragment.appendChild 具備移動性,會刪除相應的節點
fragment.appendChild(firstChild)
}
return fragment
}
compile (fragment) { // 用來編譯內存中的dom節點
let childNodes = [...fragment.childNodes]// 類數組
childNodes.forEach(child => {
if (this.isElementNode(child)) {
this.compileElement(child)
// 遞歸編譯子節點
this.compile(child)
} else {
// console.log('text: ', child)
this.complieText(child)
}
})
}
// 編譯節點
compileElement (node) {
let attrs = [...node.attributes]
// console.log('attrs:', [...attrs])
attrs.forEach(attr => {
let { name, value:expr } = attr
// 判斷是否節點
if (this.isDirecttive(name)) {
let [, directive] = name.split('-')
let arr
if (directive.split(':').length > 0) {
arr = directive.split(':')
} else {
arr = [directive]
}
let [directiveName, eventName] = arr
CompilerUtils[directiveName](node, expr, this.vm, eventName)
}
})
}
// 編譯文本節點
complieText (node) {
let content = node.textContent
if (/\{\{(.+?)\}\}/.test(content)) {
// console.log(content)
CompilerUtils['text'](node, content, this.vm)
}
}
// 判斷是否指令
isDirecttive (attrName) {
return attrName.startsWith('v-')
}
}
// 編譯工具方法
CompilerUtils = {
// 根據表達式取值
getValue (vm, expr) {
return expr.split('.').reduce((data, curKey) => {
return data[curKey]
}, vm.$data)
},
setValue (vm, expr, value) {
expr.split('.').reduce((data, curKey, index, arr) => {
if (index === arr.length -1) { // 給表達式最後一個賦值
return data[curKey] = value
}
return data[curKey]
}, vm.$data)
},
updater: { // 更新值的方法
modelUpdater (node, value) {
node.value = value
},
htmlUpdater (node, value) {
node.innerHTML = value
},
textUpdater (node, value) { // 更新文本節點
node.textContent = value
}
},
model (node, expr, vm) { // node 節點 expr 表達式 vm 當前實例
// 給輸入框賦值value
let value = this.getValue(vm, expr)
let fn = this.updater['modelUpdater']
// 放入觀察者
new Watcher(vm, expr, (newVal) => {
fn(node, newVal) // 將新值給輸入框賦值
})
fn(node, value)
// 視圖驅動數據 給v-model綁定input事件
node.addEventListener('input', evt => {
let val = evt.target.value
this.setValue(vm, expr, val)
})
},
on (node, expr, vm, eventName) {
node.addEventListener(eventName, (evt) => {
// console.log(vm, expr)
vm[expr].call(vm, evt) // 執行相應方法methods
})
},
getContentVal (vm, expr) {
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
return this.getValue(vm, args[1])
})
return content
},
text (node, expr, vm) { // 處理文本節點
let fn = this.updater.textUpdater
let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
// 爲文本插入觀察者 給{{}}的都加上觀察者
new Watcher(vm, args[1], () => {
fn(node, this.getContentVal(vm, expr)) // 返回一個全字符串
})
return this.getValue(vm, args[1])
})
fn(node, content) // 更新值
},
html (node, expr, vm) {
let fn = this.updater['htmlUpdater']
new Watcher(vm, expr, (newVal) => {
fn(node, newVal) // 將新值給v-html賦值
})
fn(node, this.getValue(vm, expr))
}
}
複製代碼