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啦~下一節會詳細講解,下面是下一節的原理圖。


推薦閱讀:
相关文章