從0到1實現VUE
vue的原理相信不少同學都已經看過,源碼分析也有許多大牛分析過。不過不知道大家有沒有這種的感覺,原理看起來很明白了,但是源碼一看就懵。最近在學習尤大在frontend masters的課。(Advanced Vue.js Features from the Ground Up)突然覺得有點豁然開朗。那為什麼不能從Ground Up開始實現一個vue.js呢。所以決定按照尤大課上的思路寫一寫。作者說過vue是漸進式的框架,那麼我想也用漸進式的思路去寫寫。每一步都去解決一點問題。
leason 1 響應式的實現
我們將響應式的實現分為3步,最終實現一個能夠雙向數據綁定的Vue
- 轉化:這裡我們將實現一個convert函數能夠監聽響應式的數據變化
- 依賴的處理:對數據的依賴進行存儲,收集和觸發
- 編譯:將響應式的數據與視圖進行綁定
本文代碼下載
1、轉化:convert函數實現
目標:實現b的值能夠跟隨a值自動變化?
let data = {
a1 : 1,
a2 : 2
}
b = data.a1*10;
data.a1 = 2;
console.log(b) //20 b不需要重新賦值就可以自動跟隨data.a1變化
分析:
- convert應該監聽到a給b賦值,並且能夠a變化後,同時更改b的值
- 可以利用Object.defineProperty()中的set和get去實現(vue.2x)
- 可以利用es6中的Proxy中的set和get去實現(vue.3x)
代碼:
function convert(obj){
let arr = Object.keys(obj); // 將對象中的key值取出
arr.forEach((key)=>{ // 每個屬性用 Object.defineProperty轉化
let inertValue = obj[key]; // 保存初始值
Object.defineProperty(obj,key,{
get(){
return inertValue; // 訪問時給出key的值
},
set(newValue){
inertValue = newValue; // 賦值時將新值賦給inertValue
b = data.a1*10; // 執行相關響應式操作
}
})
})
}
convert(data)
- convert函數中重點是對賦值操作進行監聽,當每次取值時,自動執行需要響應的操作
- 不難看出convert函數雖然實現了我們的需求,但顯然是不夠通用
問題:首先我們還需要把所有依賴data.a1的操作手動寫到set函數中去,其次在給data中其他屬性(如data.a2)賦值時也會觸發set中的操作
2、依賴的處理
class Dep{
constructor(){
this.subscribers = []
}
depend(){
this.subscribers.push(saveFunction);
}
notify(){
this.subscribers.forEach(fn=>fn())
}
}
let saveFunction;
function autorun(fn){
saveFunction = fn;
fn();
saveFunction = null;
}
- 我們定義了一個Dev的類,它需有存放,收集,和觸發依賴的屬性和方法
- 為了能夠保存操作,我們需要將所有操作保存在函數中
- 所以我們定義了一個autorun函數,它參數fn用來接收來包裹依賴的操作
- fn是autorun的一個參數,我們在this.subscribers.push中無法訪問,用一個全局變數saveFunction去傳遞
function convert(obj){
Object.keys(data).forEach((key)=>{
let dep = new Dep(); //為每個key創建一個dep實例
let inertValue = obj[key];
Object.defineProperty(obj,key,{
get(){
dep.depend() //當取值時,收集依賴
return inertValue;
},
set(newValue){
inertValue = newValue;
dep.notify() //當設置值時,觸發依賴
}
})
})
}
let data = {
a1 : 1,
a2 : 2
}
convert(data)
autorun(()=>{b = data.a1*10;}) //將所有響應式的操作通過autorun去執行
data.a1 = 2;
console.log(b) //20, b值可以跟隨a值去自動響應
- 在對data數據轉化時,我們為每個key創建一個dep實例
- 當get時,收集依賴,當set時,觸發依賴
- 將所有響應式的操作通過autorun去執行
問題:當我們在set中觸發依賴的函數執行時,就會重新出發get收集依賴?
depend(){
if(!saveFunction){
this.subscribers.push(saveFunction);
}
}
- 我們需要在dep中去判斷一下saveFunction是否為null
- saveFunction是一個全局變數,所以以後我們可以將它定義為Dep的屬性——Dep.target
問題:現在我們已經很好的完成了對數據的響應式處理,如果操作的是視圖我們該如何綁定呢?
3、編譯:數據與視圖進行綁定
目標:實現一個數據綁定
<div id = "app">
<div>{{message}}</div>
<input v-modle = "message" >
</div>
let vm = new Vue({
el: #app,
data: {
message: Hello Vue!
}
})
分析:
- 首先我們data應該被轉化成為響應式的數據
- 然後在我們需要將模版中的數據,事件進行編譯
- 在編譯的過程中,我們進行收集依賴
function Vue (options){
Observe(options.data,this) //將data數據進行轉化
let dom = document.getElementById(options.el); //獲模版的dom元素
let newDom = compile(dom,this); //對dom元素進行編譯
dom.appendChild(newDom); //添加到文本中
}
- 定義一個Vue的構造函數
- 我們將上文中的convert函數名字修改為Observe,對數據進行響應式轉化
- 在上文中,我們通過data.a去訪問,在vue中我們希望把data中的屬性綁定在vue實例中,所以傳入this
- 然後我們應該將模版中的dom對象與數據進行綁定,得到新的newDom
- 最後添加到dom中
- 所以接下來我們看看我們如何去進行編譯的,在編譯過程中如何收集的依賴?
function compile(node,vm){
let frag = document.createDocumentFragment();
let child = node.firstElementChild;
while(child){
compileElement(child,vm)
child = child.nextElementSibling;
}
return frag;
}
- 首先我們創建一個節點片段,然後對每個子節點進行處理
- 這裡我們只考慮一層子節點,因為今天主要寫的是響應式,對dom樹的遍歷會在虛擬dom中實現
- 然後我們需要對子節點分別去處理通過compileElement(child,vm)函數
- 下面我們就來實現這個compileElement函數
function compileElement(node,vm){
let attr = node.attributes;
for(let i = 0; i<attr.length ; i++){
let name = attr[i].nodeValue;
console.log(name)
switch (attr[i].nodeName) {
case "v-model":
node.addEventListener("input",(e)=>{
vm[name] = e.target.value;
console.log(e.target.value)
})
autorun(()=>{ node.value = vm[name]})
break;
}
}
let reg = /{{(.*)}}/;
if(reg.test(node.innerHTML)){
var name = RegExp.$1;
name = name.trim();
autorun(()=>{ node.innerHTML = vm[name]})
}
}
- 兩次傳進來的node應該分別是 div和 input
- so我只實現了對節點中{{message}}和"v-model"的判斷~哈哈,偷偷懶啦
- 根據v-modle的所有我們應該給node綁定一個input事件,但輸入時改變vm上綁定的值
- 我們將所有賦值的操作在autorun中執行,這樣就能收集到依賴
- 當vm上的值改變時,就能重新執行autorun函數中的操作
- autorun函數中的操作,都是dom操作,對dom進行修改
問題:我們發現autorun函數執行都是對dom的屬性值的修改,所以我們可以進行進一步的抽象
class Watcher{
constructor(node,type,vm,name){
Dep.target = this;
this.node = node;
this.name = name;
this.type = type;
this.vm = vm;
this.update();
Dep.target = null;
}
update(){
this.node[this.type] = this.vm[this.name];
}
}
new Watcher(node,"value" ,vm ,name )
//autorun(()=>{ node.value = vm[name]})
new Watcher(node,"innerHTML" ,vm ,name )
//autorun(()=>{ node.value = vm[name]})
- 我們將原來autorun函數的功能封裝成一個類
- 然後將參數傳進行進行保存,然後執行update方法時對依賴進行執行
現在我們已經實現了Vue的響應式,是不是更加深刻理解vue的MVVM模式是如何實現的啦。
以上就是Vue響應式的核心原理,編譯過程與vue1x版本類似,因為vue.2x引用了虛擬Dom過程會比較複雜。會在下一篇專門去講。
問題:這裡大家思考一個問題,在這一版本中,Vue直接用watcher操作的Dom, 如果我們綁定的數據是一個數組,當只更改數組中的一小部分,此時Dom重新渲染的效率會不會很低。我們該如何改進,哈哈哈,當然是使用虛擬dom啦~下一節會詳細講解,下面是下一節的原理圖。
推薦閱讀: