創建時間:2020-09-11
主要理解、實現如下方法:
-
Observe
:監聽器 監聽屬性變化並通知訂閱者 -
Watch
:訂閱者 收到屬性變化,更新視圖 -
Compile
:解析器 解析指令,初始化模板,綁定訂閱者,綁定事件 -
Dep
:存放對應的所有 watcher 實例
主要執行流程
右鍵點擊圖片,在新標簽頁打開,可查看更清晰圖片
將watcher裝入對應的dep實例的訂閱列表中的過程
相關html代碼,用於被解析綁定數據
這里的代碼即是compile中要解析的模板,解析其中的 v-model
{{}}
v-on
等命令
<div id="app">
<p> 姓名:<input type="text" v-model="name"></p>
<p>學號:<input type="text" v-model="number"></p>
<p><span>學號:</span> <span>{{ number }}</span></p>
<p><span>computed實現:</span> <span>{{ getStudent }}</span></p>
<p>
<button v-on:click="setData">事件綁定</button>
</p>
</div>
observer代碼
為data數據添加 get、set 以便添加 watcher,和創建 Dep 實例,通知更新視圖
const defineProp = function(obj, key, value) {
observe(value)
/*
* 預先為每一個不同屬性創建一個獨有的dep
*/
const dep = new Dep()
Object.defineProperty(obj, key, {
get: function() {
/*
* 根據不同的屬性創建且只在創建Watcher時調用
*/
if(Dep.target) {
dep.targetAddDep()
}
return value
},
set: function(newVal) {
if(newVal !== value) {
/*
* 這里的賦值操作,是以便於get 方法中返回value,因為都是賦值后馬上就會調用get方法
*/
value = newVal
/*
* 通知監聽的屬性的所有訂閱者
*/
dep.notify()
}
}
})
}
const observe = function(obj) {
if(!obj || typeof obj !== 'object') return
Object.keys(obj).forEach(function(key) {
defineProp(obj, key, obj[key])
})
}
Dep代碼
主要是將 watcher 放入 對應的 dep 訂閱列表
let UUID = 0
function Dep() {
this.id = UUID++
// 存放當前屬性的所有的監聽watcher
this.subs = []
}
Dep.prototype.addSub = function(watcher) {
this.subs.push(watcher)
}
// 目的是將當前 dep 實例 傳入 watcher
Dep.prototype.targetAddDep = function() {
// 這里 this 是實例化后的 dep
Dep.target.addDep(this)
}
Dep.prototype.notify = function() {
// 觸發當前屬性的所有 watcher
this.subs.forEach(_watcher => {
_watcher.update()
})
}
Dep.target = null
Watcher 代碼
數據更新后,更新視圖
function Watcher(vm, prop, callback) {
this.vm = vm
this.prop = prop
this.callback = callback
this.depId = {}
this.value = this.pushWatcher()
}
Watcher.prototype = {
update: function() {
/* 更新值的變化 */
const value = this.vm[this.prop]
const oldValue = this.value
if (value !== oldValue) {
this.value = value
this.callback(value)
}
},
// 目的是接收 dep 實例,用於將當前watcher實例放入 subs
addDep: function(dep) {
if(!this.depId.hasOwnProperty(dep.id)) {
dep.addSub(this)
this.depId[dep.id] = dep.id
} else {
console.log('already exist');
}
},
pushWatcher: function() {
// 存貯訂閱器
Dep.target = this
// 觸發對象的get監聽,將上面賦值給 target 的this 加入到subs
var value = this.vm[this.prop]
// 加入完后就刪除
Dep.target = null
return value
}
}
Compile 代碼
解析html模板,創建代碼片段,綁定數據事件
function Compile(vm) {
this._vm = vm
this._el = vm._el
this.methods = vm._methods
this.fragment = null
this.init()
}
Compile.prototype = {
init: function() {
this.fragment = this.createFragment()
this.compileNode(this.fragment)
// 當代碼片段中的內容編譯完了之后,插入DOM中
this._el.appendChild(this.fragment)
},
// 根據真實的DOM節點,創建文檔片段
createFragment: function() {
const fragment = document.createDocumentFragment()
let child = this._el.firstChild
while(child) {
// 將節點加入到文檔片段中后,該節點會被從原來的位置刪除,相當於移動節點位置
fragment.appendChild(child)
child = this._el.firstChild
}
return fragment
},
compileNode: function(fragment) {
let childNodes = fragment.childNodes;
[...childNodes].forEach(node =>{
if(this.isElementNode(node)) {
this.compileElementNode(node)
}
let reg = /\{\{(.*)\}\}/
// 獲取節點下的所有文本內容
let text = node.textContent
// 判斷是否已是純文本節點,且文本內容是否有{{}}
if(this.isTextNode(node) && reg.test(text)) {
let prop = reg.exec(text)[1].trim()
this.compileText(node, prop)
}
if(node.childNodes && node.childNodes.length) {
// 遞歸編譯子節點
this.compileNode(node)
}
})
},
compileElementNode: function(element) {
// 獲取屬性,只有元素節點有如下方法
let nodeAttrs = element.attributes;
[...nodeAttrs].forEach(attr => {
let name = attr.name
if(this.isDirective(name)) {
/*
* v-model 放在可接受input事件的標簽上
*/
let prop = attr.value
if (name === 'v-model') {
/*
* 獲取到的value 即為需要綁定的data
*/
this.compileModel(element, prop)
} else if(this.isEvent(name)) {
/*
* 綁定事件
*/
this.bindEvent(element, name, prop)
}
}
})
},
compileModel: function(element, prop) {
let val = this._vm[prop]
this.updateElementValue(element, val)
new Watcher(this._vm, prop, value => {
this.updateElementValue(element, value)
})
element.addEventListener('input', event => {
let newValue = event.target.value
if (val === newValue) return
this._vm[prop] = newValue
})
},
compileText: function(textNode, prop) {
let text = ''
if(/\./.test(prop)) {
var props = prop.split('.')
text = this._vm[props[0]][props[1]]
} else {
text = this._vm[prop]
}
this.updateText(textNode, text)
console.log(text);
new Watcher(this._vm, prop, (value) => {
this.updateText(textNode, value)
})
},
bindEvent: function(element, name, prop) {
var eventType = name.split(':')[1]
var fn = this._vm._methods[prop]
element.addEventListener(eventType, fn.bind(this._vm))
},
/*
* 判斷屬性是否為指令
*/
isDirective: function (text) {
return /v-/.test(text)
},
isEvent: function(text) {
return /v-on/.test(text)
},
isElementNode: function(node) {
// 元素節點返回1 文本節點(元素或屬性中的文字)3 屬性節點2(被廢棄)
return node.nodeType === 1
},
isTextNode: function(node) {
return node.nodeType === 3
},
updateElementValue: function(element, value) {
element.value = value || ''
},
updateText: function(textNode, value) {
textNode.textContent = value || ''
}
}
vue 簡要構造函數
主要實現了數據的雙向綁定,自定義事件,computed
function FakeVue(options) {
this._data = options.data
this._methods = options.methods
this._computed= options.computed
this._el = document.querySelector(options.el)
// 將 _data 中的屬性代理到外層的vm上,這里只代理了_data的第一層屬性
Object.keys(this._data).forEach(key => {
this._proxyData(key)
})
this._initComputed()
this._init()
}
FakeVue.prototype._init = function() {
// 開始遞歸監聽對象的所有屬性,直到屬性值為值類型
observe(this._data)
new Compile(this)
}
FakeVue.prototype._proxyData = function(key) {
Object.defineProperty(this, key, {
get: function() {
return this._data[key]
},
set: function (value) {
this._data[key] = value
}
})
}
FakeVue.prototype._initComputed = function() {
// 簡單的實現: 將值掛載到跟上即可
const computed = this._computed
Object.keys(computed).forEach((v) => {
Object.defineProperty(this, v, {
get: computed[v],
set: function (value) {}
})
})
}
創建vue實例
try{
let vm = new FakeVue({
el: '#app',
data: {
name: 'warren',
number: '10011',
score: {
math: 90
}
},
computed: {
getStudent: function() {
return `${this.name}:學號是 ${this.number}`
}
},
methods:{
// 通過在compile中給元素綁定事件實現
setData: function() {
alert('name:'+this.name);
}
}
});
} catch(error) {
console.error(error)
}
結語
這是從作者的理解角度,闡述的一個簡單的vue實現原理示例,希望對正在探索的你有所幫助
在這個示例中,主要的復雜點在於對 html 模板的解析,數據的雙向綁定。
建議跟着代碼的執行順序了解整個過程,關鍵點的代碼都有必要的注釋,若發現問題請指正。
最后附上 vue 源碼地址,主要關注其中的 core
和 compiler
文件;
歡迎交流 Github