最近有了點空,就想着 把 vue 給搞定了,
看了一遍之後,決定自己寫一個乞丐版的 vue,上班的時候划水一個早上也算是 結束了
index.html
<div id="app">
{{age}}
<p>{{name}}</p>
<p>{{name}}</p>
<div>
<p>
<span @click="printName">{{name}}</span>
</p>
<input type="text" v-model="inp">
{{inp}}
</div>
</div>
<script src="./dcue/PoorVue.js"></script>
<script src="./dcue/analyze.js"></script>
<script src="./index.js"></script>
index.js
const props = {
el: document.querySelector('#app'),
data: {
name: 'jack',
age: 123,
inp: ''
},
methods: {
printName() {
this.name = this.name + 1
}
}
}
new PoorVue(props)
PoorVue.js
由於當時是純手打,沒有看其他的東西,所以函數的命名上、部分代碼可能會有出入
class PoorVue {
constructor(props) {
// 對當前的 數據進行保存
this.props = props;
this.$data = props.data;
// 在這裏對數據進行一個 雙向綁定的前期工作,也就是代理工作
// 大名鼎鼎的 defineProperty 就是在 這裏進行的
this.defineData(this.$data);
// 對 html 進行解析,這裏只會 提取 {{}} 和 @ 事件
new Analyze(this.props.el, this)
}
defineData(data) {
// 判斷當前的 數據是不是一個對象
if (Object.prototype.toString.call(data) === "[object Object]") {
Object.keys(data).map(key => {
// 這裏是對 data 中的數據進行一個代理
// 這樣的話,就可以直接使用 this[props] 而不是 this.$data[props]
this.proxydata(key)
// 這裏真正的 對數據進行代理,並收集 watcher 了
this.defineProperty(data, key, data[key]);
})
}
}
// 這裏是對 data 中的數據進行一個代理
// 這樣的話,就可以直接使用 this[props] 而不是 this.$data[props]
proxydata(key) {
Object.defineProperty(this, key, {
get() {
return this.$data[key]
},
set(val) {
this.$data[key] = val
}
})
}
defineProperty(obj, key, value) {
// 對數據進行一個遍歷
// 可以對 對象內部的 數據進行深層次的綁定
this.defineData(value)
// 建立一個 Dep ,也就是依賴收集工具
// 把所有 記錄着 key 這個參數 要做的操作 都放進這個數據之中
const dep = new Dep()
Object.defineProperty(obj, key, {
get() {
// 判斷 Dep.target 是否存在,如果存在,就 進行依賴收集
// 這樣也是爲了防止多次收集
Dep.target && dep.addDep()
return value
},
set(val) {
// 當 數據一致的時候,不進行任何操作
if (value === val) return
value = val;
// 進行響應,頁面開始進行變化
// 這裏的 dep 實際上已經在 閉包裏面了
// 所以也就是說,一個 key 對應 於一個 dep
dep.notify()
}
})
}
}
class Dep {
constructor() {
// 建立一個收集 依賴的數組
this.deps = []
}
addDep() {
// 收集 對應的 Watcher
this.deps.push(Dep.target);
}
notify() {
// 執行所有 的 Watcher
// 注意,這裏的 Watcher 都是對應於 同一個 key 之下的
this.deps.map(dep => dep.update())
}
}
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm;
this.cb = cb;
// 對應了 上面數據之中 的 Dep.target && dep.addDep()
Dep.target = this;
// 執行 一下 這個函數,
// 也就是說 在這個過程之中
// 先是執行了 之前就定義好的 defineProperty get 函數
// 這樣就 能夠 將 當前的 Watcher 給收集到 Dep 當中
// 同時 也可以對頁面進行第一步 的渲染
this.cb.call(this.vm);
// 將這個 置爲 null, 也就是說 當前依賴已經收集完畢
// 爲 接下來的 Watcher 騰位置的同時
Dep.target = null;
}
update() {
// 執行當前 的 收集的依賴
this.cb && this.cb.call(this.vm);
}
}
analyze.js
class Analyze {
constructor(el, vm) {
this.$el = el;
this.vm = vm;
// 開始解析 當前的 html 代碼
this.resolve(this.$el)
}
createFrgment(el) {
const frg = document.createDocumentFragment();
const childNodes = el.childNodes;
Array.from(childNodes).map(child => {
frg.appendChild(child)
})
return frg
}
resolve(el) {
// 創建 Fragment,並將 當前頁面上的 所有節點放到 這裏來
// 實際上也是爲了防止 在解析的過程中,會進行 過多的 dom 操作
// 避免 資源的浪費
const fragment = this.createFrgment(el);
// 開始解析
this.ergodic(fragment)
this.$el.appendChild(fragment)
}
ergodic(frag) {
const nodeList = frag.childNodes;
Array.from(nodeList).map(node => {
// 是節點類型的話,開始解析 當前節點的 屬性
if (node.nodeType === 1) {
const attr = node.attributes
this.dealAttribute(node, attr)
}
// 是文本節點的話,看看是不是 {{}} 插值 插進去的
if (node.nodeType === 3) {
if (this.interP(node.textContent)) {
// 進行當前的依賴管理
this.update(node, RegExp.$1, 'text')
}
}
// 如果當前是節點,並且還有子節點的話,繼續 解析
if (node.childNodes && node.childNodes.length > 0) {
this.ergodic(node)
}
})
}
dealAttribute(node) {
Array.from(node.attributes).map(attr => {
// 這裏寫的很簡單,就是 以 @ 開頭的屬性的話,就在當前節點進行綁定
// 注意,vue 是綁定在 document 上,進行了 事件委託的
if (attr.name.startsWith('@')) {
const method = attr.value
node.addEventListener(attr.name.substr(1), this.vm.props.methods[method].bind(this.vm))
}
// 這裏關於 v-model 就不做太多的校驗了,明白意思就好
if (attr.name === 'v-model') {
const value = attr.value
console.log(value)
this.update(node, value, 'model')
}
})
}
update(node, exp, type) {
// 找到 當前處理的 函數 handler
const handler = this[`${type}Handler`];
const vm = this.vm
// 創建一個 Watcher ,把 渲染的函數發過去
new Watcher(this.vm, exp, function() {
handler && handler(node, exp, vm)
})
}
// 解析 {{}}
// 注意,這樣解析之後 RegExp.$1 就是匹配的 結果
interP(text) {
return /\{\{(.*)\}\}/.test(text)
}
// 這個就很簡單了,在 update 之中被調用 不贅述
textHandler(node, exp, vm) {
node.textContent = vm.$data[exp]
}
// v-model 的渲染函數
modelHandler(node, exp, vm) {
node.value = vm[exp];
node.addEventListener('input', (e) => {
vm[exp] = e.target.value
})
}
}
所以說,在自己實現了一遍之後,倒是對於 Dep Watcher 的理解更深刻了
每一個 data 裏面的數據,包括深層次對象裏面的 數據,每一個 字段都會有一個 Dep,每一個 雙向綁定的地方,就會有一個 Watcher
然後每一個 頁面,或者 computed ,或者 watch 每一個 都會有 一個 Watcher,
例如上面,
頁面上有 兩個 {{name}}, 那麼每一個 name ,就會有一個 Watcher,然後這些 name 的 Watcher都會被放在 同一個 Dep 之中
=========
2020.3.20更新
這次在 這個 乞丐版的 vue 上增加 一個 computed 屬性
先看 對應的 文件配置,增加了 一個 fullname
<input type="text" v-model="firstname">
<input type="text" v-model="lastname">
{{fullname}}
const props = {
el: document.querySelector('#app'),
data: {
name: 'jack',
age: 123,
inp: '',
++ firstname: '',
++ lastname: ''
},
++ computed: {
++ fullname() {
++ return this.firstname + this.lastname
++ }
},
methods: {
printName() {
this.name = this.name + 1
},
reduce() {
this.name = this.name.slice(0, -1)
}
}
}
new PoorVue(props)
由於 只是 簡單地實現,以及 瞭解其原理,就不搞那麼複雜了
PoorVue.js
class PoorVue {
constructor(props) {
// 對當前的 數據進行保存
this.props = props;
this.$data = props.data;
// 在這裏對數據進行一個 雙向綁定的前期工作,也就是代理工作
// 大名鼎鼎的 defineProperty 就是在 這裏進行的
this.defineData(this.$data);
// 對 computed 屬性進行解析
this.defineComputed(this.props.computed);
// 對 html 進行解析,這裏只會 提取 {{}} 和 @ 事件
new Analyze(this.props.el, this)
}
....
....
// 可以看到,我其實什麼都沒有改,
// 就是在這裏增加了這麼一段代碼,就讓 整個函數運行起來了
defineComputed(computed) {
const _this = this;
Object.keys(computed).map(comput => {
const dep = new Dep()
Object.defineProperty(this, comput, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addDep()
return computed[comput].call(_this)
},
})
})
}
....
....
}
- 首先得了解的一點,在 vue 的源碼中, computed 的初始化時相對靠後的位置的,所以 在 computed 裏可以使用之前就定義好的 data ,props 屬性
- 這裏的 computed 屬性我只弄成了 只讀,也就是 只有 get,沒有 set
- 在 解析 裏解析到 computed 的屬性之後,就執行 上面定義好的 關於 computed 的 defineproperty
- 執行了 computed 的 get,也就是 那個函數之後,也就 立即調用了 裏面 依賴的 那兩個 data 屬性,firstname 和 lastname
- 這兩個屬性 在被 get 的時候,執行了 自己定義的 defineproperty
- 這樣的話, 在 firstname 和 lastname 裏,也就會執行 Dep.target && dep.addDep() 這段代碼
- Dep.target 在這個時候 指向的是 computed 裏的 fullname 的 watcher
- Dep.target 有值,就把 這個 watcher 分別插進了 firstname 和 lastname 的 dep 裏面
- 然後 fullname 的 watcher 執行完畢,Dep.target = null 被清空
- 這樣 當 firstname 或者 lastname 的值進行了改變的時候,就會 去 執行 computed 裏的 fullname 的 watcher,然後渲染到頁面上
效果圖
所以說,在這裏我們可以看到 這個 computed 屬性 fullname 的 watcher 不僅僅是進入了一個 Dep 中,還被放進了 fullname 和 lastname 的 Dep 之中