2021年,讓我們手寫(xiě)一個(gè)mini版本的vue2.x和vue3.x框架
作者:夕水
來(lái)源:SegmentFault 思否社區(qū)
mini版本的vue.js2.X版本框架
模板代碼
首先我們看一下我們要實(shí)現(xiàn)的模板代碼:
<div id="app">
<h3>{{ msg }}</h3>
<p>{{ count }}</p>
<h1>v-text</h1>
<p v-text="msg"></p>
<input type="text" v-model="count">
<button type="button" v-on:click="increase">add+</button>
<button type="button" v-on:click="changeMessage">change message!</button>
<button type="button" v-on:click="recoverMessage">recoverMessage!</button>
</div>
邏輯代碼
然后就是我們要編寫(xiě)的javascript代碼。
const app = new miniVue({
el:"#app",
data:{
msg:"hello,mini vue.js",
count:666
},
methods:{
increase(){
this.count++;
},
changeMessage(){
this.msg = "hello,eveningwater!";
},
recoverMessage(){
console.log(this)
this.msg = "hello,mini vue.js";
}
}
});
運(yùn)行效果
我們來(lái)看一下實(shí)際運(yùn)行效果如下所示:

源碼實(shí)現(xiàn)-2.x
miniVue類(lèi)
mini-vue,那么我們先定義一個(gè)類(lèi),并且它的參數(shù)一定是一個(gè)屬性配置對(duì)象。如下: class miniVue {
constructor(options = {}){
//后續(xù)要做的事情
}
}
//在miniVue構(gòu)造函數(shù)的內(nèi)部
//保存根元素,能簡(jiǎn)便就盡量簡(jiǎn)便,不考慮數(shù)組情況
this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
this.$methods = options.methods;
this.$data = options.data;
this.$options = options;
代理數(shù)據(jù)
//this.$data.xxx -> this.xxx;
//proxy代理實(shí)例上的data對(duì)象
proxy(data){
//后續(xù)代碼
}
Object.defineProperty,通過(guò)這個(gè)方法來(lái)完成這個(gè)代理方法。如下://proxy方法內(nèi)部
// 因?yàn)槲覀兪谴砻恳粋€(gè)屬性,所以我們需要將所有屬性拿到
Object.keys(data).forEach(key => {
Object.defineProperty(this,key,{
enumerable:true,
configurable:true,
get:() => {
return data[key];
},
set:(newValue){
//這里我們需要判斷一下如果值沒(méi)有做改變就不用賦值,需要排除NaN的情況
if(newValue === data[key] || _isNaN(newValue,data[key]))return;
data[key] = newValue;
}
})
})
_isNaN工具方法的實(shí)現(xiàn),如下:function _isNaN(a,b){
return Number.isNaN(a) && Number.isNaN(b);
}
// 構(gòu)造函數(shù)內(nèi)部
this.proxy(this.$data);
數(shù)據(jù)響應(yīng)式觀察者observer類(lèi)
Observer。如下:class Observer {
constructor(data){
//后續(xù)實(shí)現(xiàn)
}
}
Object.defineProperty方法,我們需要在getter函數(shù)中收集依賴(lài),在setter函數(shù)中發(fā)送通知,用來(lái)通知依賴(lài)進(jìn)行更新。我們用一個(gè)方法來(lái)專(zhuān)門(mén)去執(zhí)行定義響應(yīng)式對(duì)象的方法,叫walk,如下://再次申明,不考慮數(shù)組,只考慮對(duì)象
walk(data){
if(typeof data !== 'object' || !data)return;
// 數(shù)據(jù)的每一個(gè)屬性都調(diào)用定義響應(yīng)式對(duì)象的方法
Object.keys(data).forEach(key => this.defineReactive(data,key,data[key]));
}
defineReactive方法的實(shí)現(xiàn),同樣也是使用Object.defineProperty方法來(lái)定義響應(yīng)式對(duì)象,如下所示:defineReactive(data,key,value){
// 獲取當(dāng)前this,以避免后續(xù)用vm的時(shí)候,this指向不對(duì)
const vm = this;
// 遞歸調(diào)用walk方法,因?yàn)閷?duì)象里面還有可能是對(duì)象
this.walk(value);
//實(shí)例化收集依賴(lài)的類(lèi)
let dep = new Dep();
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get(){
// 收集依賴(lài),依賴(lài)存在Dep類(lèi)上
Dep.target && Dep.add(Dep.target);
return value;
},
set(newValue){
// 這里也判斷一下
if(newValue === value || __isNaN(value,newValue))return;
// 否則改變值
value = newValue;
// newValue也有可能是對(duì)象,所以遞歸
vm.walk(newValue);
// 通知Dep類(lèi)
dep.notify();
}
})
}
Observer類(lèi)完成了之后,我們需要在miniVue類(lèi)的構(gòu)造函數(shù)中實(shí)例化一下它,如下://在miniVue構(gòu)造函數(shù)內(nèi)部
new Observer(this.$data);
依賴(lài)類(lèi)
defineReactive方法內(nèi)部用到了Dep類(lèi),接下來(lái),我們來(lái)定義這個(gè)類(lèi)。如下:class Dep {
constructor(){
//后續(xù)代碼
}
}
defineReactive中,我們很明顯就知道會(huì)有add方法和notify方法,并且我們需要一種數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)依賴(lài),vue源碼用的是隊(duì)列,而在這里為了簡(jiǎn)單化,我們使用ES6的set數(shù)據(jù)結(jié)構(gòu)。如下://構(gòu)造函數(shù)內(nèi)部
this.deps = new Set();
add方法和notify方法,事實(shí)上這里還會(huì)有刪除依賴(lài)的方法,但是這里為了最簡(jiǎn)便,我們只需要一個(gè)add和notify方法即可。如下:add(dep){
//判斷dep是否存在并且是否存在update方法,然后添加到存儲(chǔ)的依賴(lài)數(shù)據(jù)結(jié)構(gòu)中
if(dep && dep.update)this.deps.add(dep);
}
notify(){
// 發(fā)布通知無(wú)非是遍歷一道dep,然后調(diào)用每一個(gè)dep的update方法,使得每一個(gè)依賴(lài)都會(huì)進(jìn)行更新
this.deps.forEach(dep => dep.update())
}
Watcher類(lèi)
Watcher。class Watcher {
//3個(gè)參數(shù),當(dāng)前組件實(shí)例vm,state也就是數(shù)據(jù)以及一個(gè)回調(diào)函數(shù),或者叫處理器
constructor(vm,key,cb){
//后續(xù)代碼
}
}
Watcher的用法,我們是不是會(huì)像如下這樣來(lái)寫(xiě)://3個(gè)參數(shù),當(dāng)前組件實(shí)例vm,state也就是數(shù)據(jù)以及一個(gè)回調(diào)函數(shù),或者叫處理器
new Watcher(vm,key,cb);
//構(gòu)造函數(shù)內(nèi)部
this.vm = vm;
this.key = key;
this.cb = cb;
//依賴(lài)類(lèi)
Dep.target = this;
// 我們用一個(gè)變量來(lái)存儲(chǔ)舊值,也就是未變更之前的值
this.__old = vm[key];
Dep.target = null;
update(){
//獲取新的值
let newValue = this.vm[this.key];
//與舊值做比較,如果沒(méi)有改變就無(wú)需執(zhí)行下一步
if(newValue === this.__old || __isNaN(newValue,this.__old))return;
//把新的值回調(diào)出去
this.cb(newValue);
//執(zhí)行完之后,需要更新一下舊值的存儲(chǔ)
this.__old = newValue;
}
編譯類(lèi)compiler類(lèi)
初始化
class Compiler {
constructor(vm){
//后續(xù)代碼
}
}
//在miniVue構(gòu)造函數(shù)內(nèi)部
new Compiler(this);
//編譯類(lèi)構(gòu)造函數(shù)內(nèi)部
//根元素
this.el = vm.$el;
//事件方法
this.methods = vm.$methods;
//當(dāng)前組件實(shí)例
this.vm = vm;
//調(diào)用編譯函數(shù)開(kāi)始編譯
this.compile(vm.$el);
compile方法
compile(el){
//拿到所有子節(jié)點(diǎn)(包含文本節(jié)點(diǎn))
let childNodes = el.childNodes;
//轉(zhuǎn)成數(shù)組
Array.from(childNodes).forEach(node => {
//判斷是文本節(jié)點(diǎn)還是元素節(jié)點(diǎn)分別執(zhí)行不同的編譯方法
if(this.isTextNode(node)){
this.compileText(node);
}else if(this.isElementNode(node)){
this.compileElement(node);
}
//遞歸判斷node下是否還含有子節(jié)點(diǎn),如果有的話(huà)繼續(xù)編譯
if(node.childNodes && node.childNodes.length)this.compile(node);
})
}
isTextNode(node){
return node.nodeType === 3;
}
isElementNode(node){
return node.nodeType === 3;
}
編譯文本節(jié)點(diǎn)
compileText編譯文本節(jié)點(diǎn)的方法。如下://{{ count }}數(shù)據(jù)結(jié)構(gòu)是類(lèi)似如此的
compileText(node){
//后續(xù)代碼
}
{{ count }}映射成為0,而文本節(jié)點(diǎn)不就是node.textContent屬性嗎?所以此時(shí)我們可以想到根據(jù)正則來(lái)匹配{{}}中的count值,然后對(duì)應(yīng)替換成數(shù)據(jù)中的count值,然后我們?cè)僬{(diào)用一次Watcher類(lèi),如果更新了,就再次更改這個(gè)node.textContent的值。如下:compileText(node){
//定義正則,匹配{{}}中的count
let reg = /\{\{(.+?)\}\}/g;
let value = node.textContent;
//判斷是否含有{{}}
if(reg.test(value)){
//拿到{{}}中的count,由于我們是匹配一個(gè)捕獲組,所以我們可以根據(jù)RegExp類(lèi)的$1屬性來(lái)獲取這個(gè)count
let key = RegExp.$1.trim();
node.textContent = value.replace(reg,this.vm[key]);
//如果更新了值,還要做更改
new Watcher(this.vm,key,newValue => {
node.textContent = newValue;
})
}
}
編譯元素節(jié)點(diǎn)
指令
v-text,v-model,v-on:click這三個(gè)指令。讓我們來(lái)看看compileElement方法吧。compileElement(node){
//指令不就是一堆屬性嗎,所以我們只需要獲取屬性即可
const attrs = node.attributes;
if(attrs.length){
Array.from(attrs).forEach(attr => {
//這里由于我們拿到的attributes可能包含不是指令的屬性,所以我們需要先做一次判斷
if(this.isDirective(attr)){
//根據(jù)v-來(lái)截取一下后綴屬性名,例如v-on:click,subStr(5)即可截取到click,v-text與v-model則subStr(2)截取到text和model即可
let attrName = attr.indexOf(':') > -1 ? attr.subStr(5) : attr.subStr(2);
let key = attr.value;
//單獨(dú)定義一個(gè)update方法來(lái)區(qū)分這些
this.update(node,attrName,key,this.vm[key]);
}
})
}
}
isDirective輔助方法,我們可以使用startsWith方法,判斷是否含有v-值即可認(rèn)定這個(gè)屬性就是一個(gè)指令。如下:isDirective(dir){
return dir.startsWith('v-');
}
update方法。如下:update(node,attrName,key,value){
//后續(xù)代碼
}
//update函數(shù)內(nèi)部
if(attrName === 'text'){
//執(zhí)行v-text的操作
}else if(attrName === 'model'){
//執(zhí)行v-model的操作
}else if(attrName === 'click'){
//執(zhí)行v-on:click的操作
}
v-text指令
//attrName === 'text'內(nèi)部
node.textContent = value;
new Watcher(this.vm,key,newValue => {
node.textContent = newValue;
})
v-model指令
//attrName === 'model'內(nèi)部
node.value = value;
new Watcher(this.vm,key,newValue => {
node.value = newValue;
});
node.addEventListener('input',(e) => {
this.vm[key] = node.value;
})
v-on:click指令
//attrName === 'click'內(nèi)部
node.addEventListener(attrName,this.methods[key].bind(this.vm));
mini版本的vue.js3.x框架
模板代碼
<div id="app"></div>
邏輯代碼
const App = {
$data:null,
setup(){
let count = ref(0);
let time = reactive({ second:0 });
let com = computed(() => `${ count.value + time.second }`);
setInterval(() => {
time.second++;
},1000);
setInterval(() => {
count.value++;
},2000);
return {
count,
time,
com
}
},
render(){
return `
<h1>How reactive?</h1>
<p>this is reactive work:${ this.$data.time.second }</p>
<p>this is ref work:${ this.$data.count.value }</p>
<p>this is computed work:${ this.$data.com.value }</p>
`
}
}
mount(App,document.querySelector("#app"));
運(yùn)行效果

思考一下,我們要實(shí)現(xiàn)如上的功能應(yīng)該怎么做呢?
源碼實(shí)現(xiàn)-3.x
與vue2.x做比較
effect。vue3.x更像是函數(shù)式編程了,每一個(gè)功能都是一個(gè)函數(shù),比如定義響應(yīng)式對(duì)象,那就是reactive方法,再比如computed,同樣的也是computed方法...廢話(huà)不多說(shuō),讓我們來(lái)看一下吧!reactive方法
function reactive(data){
if(!isObject(data))return;
//后續(xù)代碼
}
function reactive(data){
if(!isObject(data))return;
return new Proxy(data,{
get(target,key,receiver){
//反射api
const ret = Reflect.get(target,key,receiver);
//收集依賴(lài)
track(target,key);
return isObject(ret) ? reactive(ret) : ret;
},
set(target,key,val,receiver){
Reflect.set(target,key,val,receiver);
//觸發(fā)依賴(lài)方法
trigger(target,key);
return true;
},
deleteProperty(target,key,receiver){
const ret = Reflect.deleteProperty(target,key,receiver);
trigger(target,key);
return ret;
}
})
}
track方法
//全局變量表示依賴(lài)
let activeEffect;
//存儲(chǔ)依賴(lài)的數(shù)據(jù)結(jié)構(gòu)
let targetMap = new WeakMap();
//每一個(gè)依賴(lài)又是一個(gè)map結(jié)構(gòu),每一個(gè)map存儲(chǔ)一個(gè)副作用函數(shù)即effect函數(shù)
function track(target,key){
//拿到依賴(lài)
let depsMap = targetMap.get(target);
// 如果依賴(lài)不存在則初始化
if(!depsMap)targetMap.set(target,(depsMap = new Map()));
//拿到具體的依賴(lài),是一個(gè)set結(jié)構(gòu)
let dep = depsMap.get(key);
if(!dep)depsMap.set(key,(dep = new Set()));
//如果沒(méi)有依賴(lài),則存儲(chǔ)再set數(shù)據(jù)結(jié)構(gòu)中
if(!dep.has(activeEffect))dep.add(activeEffect)
}
trigger方法
function trigger(target,key){
const depsMap = targetMap.get(target);
//存儲(chǔ)依賴(lài)的數(shù)據(jù)結(jié)構(gòu)都拿不到,則代表沒(méi)有依賴(lài),直接返回
if(!depsMap)return;
depsMap.get(key).forEach(effect => effect && effect());
}
effect方法
function effect(handler,options = {}){
const __effect = function(...args){
activeEffect = __effect;
return handler(...args);
}
//配置對(duì)象有一個(gè)lazy屬性,用于computed計(jì)算屬性的實(shí)現(xiàn),因?yàn)橛?jì)算屬性是懶加載的,也就是延遲執(zhí)行
//也就是說(shuō)如果不是一個(gè)計(jì)算屬性的回調(diào)函數(shù),則立即執(zhí)行副作用函數(shù)
if(!options.lazy){
__effect();
}
return __effect;
}
computed的實(shí)現(xiàn)
function computed(handler){
// 只考慮函數(shù)的情況
// 延遲計(jì)算 const c = computed(() => `${ count.value}!`)
let _computed;
//可以看到computed就是一個(gè)添加了lazy為true的配置對(duì)象的副作用函數(shù)
const run = effect(handler,{ lazy:true });
_computed = {
//get 訪問(wèn)器
get value(){
return run();
}
}
return _computed;
}
ref方法
const count = ref(0);
//修改
count.value = 1;
function ref(target){
let value = target;
const obj = {
get value(){
//收集依賴(lài)
track(obj,'value');
return value;
},
set value(newValue){
if(value === newValue)return;
value = newValue;
//觸發(fā)依賴(lài)
trigger(obj,'value');
}
}
return obj;
}
mount方法
function mount(instance,el){
effect(function(){
instance.$data && update(instance,el);
});
//setup返回的數(shù)據(jù)就是實(shí)例上的數(shù)據(jù)
instance.$data = instance.setup();
//這里的update實(shí)際上就是編譯函數(shù)
update(instance,el);
}
update編譯函數(shù)
innerHTML。如下://這是最簡(jiǎn)單的編譯函數(shù)
function update(instance,el){
el.innerHTML = instance.render();
}

評(píng)論
圖片
表情
