近幾年,前端框架層出不窮,在技術瞬息萬變的時代裏,關注JS語言自己,探究一些框架底層實現原理也許會讓咱們走得更深更遠。下面是本身看vue源碼的一些理解和實踐,主要是對vue雙向綁定原理和觀察者模式作了一些實踐,以v-model爲例。javascript
整個過程分爲如下幾步:html
主要涉及到幾個對象:前端
對訂閱者的操做能夠抽象成一個Dep的原型。這裏的對象屬性subscriber是訂閱者的數組,target是每次要加入subscriber中的目標訂閱者,每次都只能加一個target到subscriber中。vue
function Dep(cb) {
this.subscriber = []
this.target = null
}
Dep.prototype = {
addSub(sub) {
// 加入到訂閱者數組中
this.subscriber.push(sub)
},
notify() {
//
this.subscriber.forEach((item) => {
item.update()
})
}
}
複製代碼
而後,咱們再來抽象一個watcher對象。exp是咱們在data中定義的變量,cb是訂閱者要執行的回調函數。get獲取的是變量的值,update對應的當變量值發生更新時,執行訂閱者的cb函數。初始化一個watcher的時候,調用get可把該watcher加入到訂閱者的數組裏。java
function Watcher(vm, exp, cb) {
this.vm = vm
this.exp = exp
this.cb = cb
// 添加到訂閱者列表中
this.value = this.get()
}
Watcher.prototype = {
get() {
Dep.target = this
let value = this.vm.$data[this.exp]
Dep.target = null
return value
},
update() {
let oldVal = this.value
let newVal = this.vm.$data[this.exp]
if (newVal !== oldVal ) {
console.log('更新', this.exp)
this.value = newVal
this.cb.call(this.vm, newVal)
}
}
}
複製代碼
最後,咱們先模擬一個vue的原型。每個data中的變量對應一個Dep對象,用於收集這個變量所對應的全部訂閱者。這裏,使用了原生JS中的Object.defineProperty方法,當get的時候把相關訂閱者加入到dep對象中,當set的時候通知訂閱者執行相關回調。node
function Vue(options) {
let vm = this
vm._init(options)
//開啓觀察者模式觀察數據變化
vm._observe(options.data)
// 編譯dom
vm._compile()
}
Vue.prototype._init = function(options) {
var vm = this
vm.$el = options.el
vm.$data = options.data
}
Vue.prototype._observe = function(data) {
Object.entries(data).forEach(([key, value]) => {
var dep = new Dep()
let property = Object.getOwnPropertyDescriptor(data, key)
if (property && !property.configurable) {
return
}
let getter = property && property.get
let setter = property && property.set
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
let val = getter ? getter.call(data) : value
// get時候添加訂閱者
if (Dep.target) {
dep.addSub(Dep.target)
}
return val
},
set(newValue) {
let val = getter ? getter.call(data) : value
// 髒檢查,排除NaN !== NaN
if (newValue === val || (newValue !== newValue)) {
return
}
if (setter) {
setter.call(data, newValue)
} else {
value = newValue
}
// 通知訂閱者
dep.notify()
}
})
})
}
Vue.prototype._compile = function() {
let rootNode = document.querySelector(this.$el)
let compile = (rootNode) => {
if (rootNode.childNodes) {
Array.prototype.forEach.call(rootNode.childNodes, (node) => {
if (node.attributes && node.attributes.hasOwnProperty('v-model')) {
compileVmodel(this, node)
}
if (/{{.*}}/.test(node.innerHTML)) {
compileBrace(this, node)
}
if (node.childNodes) {
compile(node)
}
})
}
}
compile(rootNode)
}
function compileVmodel(vm, node) {
// 檢測到有v-model屬性,則添加對應watcher
let exp = node.getAttribute('v-model')
new Watcher(vm, exp, (val) => {
node.setAttribute('v-model', val)
})
// 監聽input事件
node.addEventListener('input', () => {
if (node.value !== vm.$data[exp]) {
vm.$data[exp] = node.value
}
}, false)
}
複製代碼
這裏用到了兩個函數compileVmodel和compileBrace,只是做爲模擬,實際vue解析的過程當中用到了AST。數組
function compileVmodel(vm, node) {
// 檢測到有v-model屬性,則添加對應watcher
let exp = node.getAttribute('v-model')
new Watcher(vm, exp, (val) => {
node.setAttribute('v-model', val)
})
// 監聽input事件
node.addEventListener('input', () => {
if (node.value !== vm.$data[exp]) {
vm.$data[exp] = node.value
}
}, false)
}
function compileBrace(vm, node) {
// 解析{{}}中值
let exp = node.innerHTML.match(/{{(.*)}}/)[1]
console.log('compileBrace', exp)
new Watcher(vm, exp, (val) => {
console.log('新的值:', node)
// let innerHTML = node.innerHTML.replace(/{{.*}}/g, val)
node.textContent = val
// console.log(innerHTML)
})
}
複製代碼
以上就完成了對vue雙向綁定原理的簡單建模,如今寫段代碼來實踐驗證下bash
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>vue雙向綁定原理實踐</title>
</head>
<body>
<div id="app">
<input type="testVmodel" name="testVmodel" v-model="inputValue">
<p>{{inputValue}}</p>
</div>
<script type="text/javascript">
// 這裏是引入上面的原型
window.onload = (function(window) {
let app = new Vue({
el: '#app',
data: {
inputValue: '初始化inputValue值'
}
})
// 開啓觀察者模式監測 inputValue變化
new Watcher(app, 'inputValue', function(value) {
let inputEl = document.querySelector('input')
inputEl.value = value
})
app.$data.inputValue = '測試更新'
setTimeout(() => {
app.$data.inputValue = '測試更新2'
}, 1000)
})(window)
</script>
</body>
</html>
複製代碼
以上只是本身簡單模擬了下vue的雙向綁定原理渲染過程,便於你們去理解。實際的vue源碼中比這個要詳細和複雜得多啦,尤爲是template模板解析和渲染這塊,用了AST的思想,有空能夠再研究下這個。前端框架