寫在前面
1202年了独郎,再實(shí)現(xiàn)一款mvvm框架有咩用肮?其實(shí)...確實(shí)....沒咩用虹曙。你寫的再好也難卷的過已經(jīng)相當(dāng)成熟的vue,react等等框架迫横,但是對個(gè)人而言番舆,自己實(shí)現(xiàn)一個(gè)簡單版vue一定會加深你對源碼的原理的理解,這么看來還是有點(diǎn)意義的矾踱。
最終版效果:
實(shí)現(xiàn)功能:
- 聲明式渲染 {{message}}
- 條件渲染 v-if
- 列表渲染 v-for
- 事件處理 v-on
- 組件渲染 <component></component>
框架特點(diǎn): - 使用typescript+webpack構(gòu)建
- 框架直接操作真實(shí)dom而沒有用到vdom
- 只實(shí)現(xiàn)了vue的部分功能恨狈,因?yàn)槭聼o巨細(xì)全部實(shí)現(xiàn)的話有點(diǎn)搬磚。
- 不像vue2.x的缺陷,moush-vue內(nèi)數(shù)組可通過下標(biāo)索引完成視圖更新
關(guān)于沒有vdom:
這也是vue1.x和2.x的區(qū)別介返,但沒有vdom也完全不影響你理解vue原理拴事,因?yàn)槔斫饬?.x的原理沃斤,2.x無非就是在原來的基礎(chǔ)上增加了vdom和相關(guān)的diff算法而已圣蝎。
演示:
template:
<div id="app">
<div class="ageContent">
<p>
{{name}}的年齡是{{age}}
</p>
<ul>
<li v-for="item in arr">
{{item}}
</li>
</ul>
<button v-on:click="addFunc">addFunc</button>
<coma v-if="isShow"></coma>
</div>
typescript:
const app = new moushVue({
el: "#app",
data: function () {
return {
age: 1,
name: "小明",
isShow: true,
arr:[1,2,3,4,5,6,7,8,9,10,11],
};
},
methods:{
addFunc:function(){
this.arr[0]++
},
switchIsShow:function(){
this.isShow=!this.isShow
}
},
components: {
coma: {
template: `<h1 class="com" v-bind:test="appName">局部組件{{appName}}自身屬性:{{appAttr}}</h1>`,
data: function () {
return {
appName: "moush",
appAttr:"attr",
};
},
},
},
});
[圖片上傳失敗...(image-6ac2ac-1638493300126)]
點(diǎn)擊addFunc按鈕 列表中·1會++,瀏覽器視圖也會相應(yīng)更新
項(xiàng)目的地址:
https://github.com/moushicheng/moush-vue
騙個(gè)star不過分吧
主流程
class moushVue implements VM{
$options: any;
$data: any;
$el: HTMLElement;
$parentVm: VM;
$childrenVm: VM[];
$methods:any
$oldNode:any;
constructor(options: OPTIONS) {
this.$options = options;
this.init();
this.mount();
this.observe();
}
protected init() {
new init(this);
}
protected mount() {
this.$options.beforeMount.call(this);
this.$el =
typeof this.$options.el == "string"
? document.querySelector(this.$options.el)
: this.$options.el;
this.$options.mounted.call(this);
}
protected observe() {
new Observer('$data',this.$data,this); //使data內(nèi)部數(shù)據(jù)可觀測
new Complier(this); //分析el內(nèi)部節(jié)點(diǎn)并生成相應(yīng)watcher
}
}
我們先不用在意函數(shù)細(xì)節(jié)做了什么衡瓶,籠統(tǒng)認(rèn)知一下徘公,且看constructor內(nèi)部分別干了三件事:
this.init();初始化一些數(shù)據(jù)
this.mount(); 將用戶傳進(jìn)來的el掛載
this.observe(); 將數(shù)據(jù)變的可觀測并執(zhí)行編譯
前兩者比較簡單,我們的核心就是搞懂observe中
new Observer('$data',this.$data,this); //使data內(nèi)部數(shù)據(jù)可觀測
new Complier(this); //分析el內(nèi)部節(jié)點(diǎn)并生成相應(yīng)watcher
到底干了什么
響應(yīng)式原理
要監(jiān)控一個(gè)對象(這里把Array和Object都稱為對象)內(nèi)部的數(shù)據(jù)變化哮针,我們就不得不用到一些手段,vue2.x用到了Object.defineProperty這個(gè)api來實(shí)現(xiàn)監(jiān)控?cái)?shù)據(jù)关面,但我們的框架并不是這樣,因?yàn)檫@樣做無法監(jiān)控
arr[0]=1;
數(shù)組下標(biāo)索引帶來的變化十厢。
在我們的框架中等太,用到了es6的Proxy代理對象來監(jiān)控對象變化
下面你可以嘗試一下把這段代碼復(fù)制到瀏覽器控制臺,然后進(jìn)行一些簡單的調(diào)試體驗(yàn)一下Proxy
const obj={a:1}
const proxy = new Proxy(obj, {
get(obj, property) {
console.log('@get:'+property)
return obj[property];
},
set(obj, property, value) {
console.log('@set:'+property+value)
obj[property] = value;
return true;
},
});
//調(diào)試
proxy.a=2 //@set:a2
proxy.a //@get:a
那么要對vue中data選項(xiàng)進(jìn)行數(shù)據(jù)監(jiān)控怎么做呢蛮放?那當(dāng)然如出一轍用到Proxy缩抡,在數(shù)據(jù)更新的時(shí)候,在Proxy get中順便通知依賴更新就行了
那么什么又是依賴呢包颁?這個(gè)不太好解釋瞻想,籠統(tǒng)的講就是跟數(shù)據(jù)直接相關(guān)的HTML模板
如:
<div>
{{message}}
</div>
new moushVue({
...
data:{
message:"Hello,world"
}
...
})
清晰一點(diǎn)講就是我們內(nèi)部構(gòu)建的Watcher對象,通知依賴更新娩嚼,就是通知Watcher執(zhí)行它的update方法蘑险,update方法會直接操作dom更新視圖,對不同的模板會有不同的操作(不同的回調(diào)cb)岳悟,這和我們主流程中Complier編譯有關(guān)佃迄,它會根據(jù)HTML模板創(chuàng)建不同類型的cb來為數(shù)據(jù)更新進(jìn)行服務(wù):
class Watcher {
vm:VM
cb:Function;
getter:any;
value:any;
constructor (vm,initVal,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
if(isType(expOrFn,'String'))this.getter = parsePath(expOrFn)
else if(isType(expOrFn,'Function'))this.getter=expOrFn
this.value = this.get() //收集依賴
this.value=initVal
}
get () {
window.target = this;
let value = this.getter(this.vm.$data)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
// this.value = this.get() //更新時(shí)不要觸發(fā)getter否則會收集依賴
this.value = this.getter(this.vm.$data)
this.cb.call(this.vm, this.value, oldValue)
}
}
關(guān)于get,依賴收集贵少,我們很快就會提到呵俏。
好,那么讓我們回到數(shù)據(jù)監(jiān)控
Observer
observer就是一個(gè)偵測器春瞬,它會深度遞歸將選項(xiàng)data內(nèi)部的所有數(shù)據(jù)都進(jìn)行監(jiān)控柴信。
class Observer{
$value: any;
$parent: any;
$key:string
dep: any;
constructor(key,value, parent) {
this.$key=key;
this.$value = value;
this.$parent = parent;
this.dep = new Dep();
def(value, "__ob__", this); //相當(dāng)于this.__ob__=value
this.walk(value);
this.detect(value, parent);
}
private walk(obj: Object | Array<any>) {
for (const [key, val] of Object.entries(obj)) {
if (typeof val == "object") {
//同時(shí)判斷數(shù)組和對象
new ObserverNext(key,val, obj);
}
}
}
private detect(val: any, parent: any) {
const dep = this.dep
const key=this.$key
const proxy = new Proxy(val, {
get(obj, property) {
if (!obj.hasOwnProperty(property)) {
return;
}
dep.depend(property);
return obj[property];
},
set(obj, property, value) {
obj[property] = value;
dep.notify(property);
if(parent.__ob__)parent.__ob__.dep.notify(key)
return true;
},
});
parent[this.findKey(parent, val)] = proxy;
}
//通過對象和對象內(nèi)的某個(gè)值發(fā)現(xiàn)指向這個(gè)值的鍵(key)
//比如obj.a=1,findKey(obj,1) =>返回a
private findKey(obj, value, compare = (a, b) => a === b) {
return Object.keys(obj).find((k) => compare(obj[k], value));
}
}
每個(gè)對象的數(shù)據(jù)監(jiān)控都需要有一個(gè)dep,dep是什么?因?yàn)橐粋€(gè)數(shù)據(jù)可能會對應(yīng)多個(gè)依賴宽气,所以必須要把數(shù)據(jù)對應(yīng)的所有依賴都做一個(gè)統(tǒng)一管理随常,這個(gè)統(tǒng)一管理就由dep來實(shí)現(xiàn)潜沦。
observer的主流程,初始化绪氛,創(chuàng)建dep唆鸡,然后walk(遞歸分析對象內(nèi)部是否有嵌套對象,有的話就將嵌套對象也進(jìn)行監(jiān)控枣察。
walk(obj: Object | Array<any>) {
for (const [key, val] of Object.entries(obj)) {
if (typeof val == "object") {
//同時(shí)判斷數(shù)組和對象
new ObserverNext(key,val, obj);
}
}
}
然后是偵測detect
private detect(val: any, parent: any) {
const dep = this.dep
const key=this.$key
const proxy = new Proxy(val, {
get(obj, property) {
if (!obj.hasOwnProperty(property)) {
return;
}
dep.depend(property);
return obj[property];
},
set(obj, property, value) {
obj[property] = value;
dep.notify(property);
if(parent.__ob__)parent.__ob__.dep.notify(key)
return true;
},
});
parent[this.findKey(parent, val)] = proxy;
}
先把val(傳進(jìn)來的對象)進(jìn)行代理(proxy)争占,然后在get中收集依賴dep.depend
在set中通知依賴更新dep.notify,更新的時(shí)候同時(shí)會做一層穿透通知父對象也進(jìn)行更新序目。
最后
parent[this.findKey(parent, val)] = proxy;
在將父對象中引用我們新偵測對象改為代理器臂痕。這樣便能將整個(gè)對象都變成響應(yīng)式
總結(jié)一下,Observer的作用就是深度遞歸分析對象內(nèi)部的所有數(shù)據(jù),并進(jìn)行偵測猿涨,在內(nèi)部數(shù)據(jù)更新時(shí)通知代理器使dep更新握童,在獲取內(nèi)部數(shù)據(jù)的時(shí)候就會通知代理器使dep進(jìn)行依賴收集。
dep
接下來讓我們分析一下dep叛赚,因?yàn)樵创a很簡單澡绩,所以直接上源碼
class depNext {
subs: Map<string, Array<Watcher>>;
constructor() {
this.subs = new Map();
}
addSub(prop, target) {
const sub = this.subs.get(prop);
if (!sub) {
this.subs.set(prop, [target]);
return;
}
sub.push(target);
}
// 添加一個(gè)依賴
depend(prop) {
if (window.target) {
this.addSub(prop, window.target); //不要奇怪window.target后續(xù)會講
}
}
// 通知所有依賴更新
notify(prop) {
const watchers = this.subs.get(prop);
if(!watchers)return;
for (let i = 0, l = watchers.length; i < l; i++) {
watchers[i].update();
}
}
}
注意到subs是一個(gè)Map對象,它會映射對象內(nèi)所有的數(shù)據(jù)俺附,每個(gè)映射的數(shù)據(jù)都對應(yīng)一個(gè)依賴數(shù)組肥卡。這就是上文說的一個(gè)數(shù)據(jù)可能對應(yīng)多個(gè)依賴
依賴收集
依賴收集簡單的來說就是,在獲取數(shù)據(jù)的時(shí)候收集依賴
再次貼一下Observer中的源碼
const proxy = new Proxy(val, {
get(obj, property) {
if (!obj.hasOwnProperty(property)) {
return;
}
dep.depend(property);
return obj[property];
},
set(...){...}
});
數(shù)據(jù)獲取的時(shí)候事镣,看到了嗎步鉴,dep.depend收集了依賴,然后在依賴收集器dep中蛮浑,將對應(yīng)的依賴添加到依賴數(shù)組中
dep中是這么收集的
this.addSub(prop, window.target);
window.target實(shí)際上就是watcher的實(shí)例唠叛,在創(chuàng)建watcher的時(shí)候,watcher會把自己賦予到全局window.target中沮稚,然后去獲取一下數(shù)據(jù)艺沼,數(shù)據(jù)代理器(Proxy)就會dep.depend,收集這個(gè)watcher了蕴掏。
watcher中依賴收集
get () {
window.target = this;
let value = this.getter(this.vm.$data)
window.target = undefined;
return value
}
this.getter在構(gòu)造器中被parsePath所創(chuàng)建,parsePath會把一個(gè)形如'data.a.b.c'的字符串路徑所表示的值障般,從真實(shí)的data對象中取出來,這樣就完成了依賴收集
/**
* Parse simple path.
* 把一個(gè)形如'data.a.b.c'的字符串路徑所表示的值盛杰,從真實(shí)的data對象中取出來
* 例如:
* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
export function parsePath(path) {
const bailRE = /[^\w.$]/;
const segments = path.split(".");
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
if (bailRE.test(segments[i])) {
//this.arr[0] this[arr[0]]
const match = segments[i].match(/(\w+)\[(.+)\]/);
obj = obj[match[1]];
obj = obj[match[2]];
continue;
}
obj = obj[segments[i]];
}
return obj;
};
}
總結(jié)
Observer深度遞歸分析選項(xiàng)data內(nèi)部的數(shù)據(jù)挽荡,使其具有響應(yīng)性。
observer實(shí)例內(nèi)部的dep負(fù)責(zé)統(tǒng)一管理依賴即供,
在獲取數(shù)據(jù)時(shí)定拟,dep會收集依賴,在數(shù)據(jù)更新時(shí)逗嫡,dep會通知依賴更新
而依賴就是watcher,它負(fù)責(zé)具體的更新視圖青自,通過調(diào)用其上的cb(回調(diào)函數(shù))株依。
watcher更新視圖所調(diào)用的cb都在Complier編譯構(gòu)建時(shí)決定,這一點(diǎn)會在后續(xù)的文章中講解延窜。
想具體了解上述過程可以點(diǎn)擊恋腕,直接看項(xiàng)目源碼
https://github.com/moushicheng/moush-vue
歸檔
# 手摸手教你實(shí)現(xiàn)一個(gè)簡單vue(1)響應(yīng)式原理
# 手摸手教你實(shí)現(xiàn)一個(gè)簡單vue(2)上手編寫observer