從0到1實現VUE

vue的原理相信很多同窗都已經看過,源碼分析也有許多大牛分析過。不過不知道你們有沒有這種的感受,原理看起來很明白了,可是源碼一看就懵。最近在學習尤大在frontend masters的課。(Advanced Vue.js Features from the Ground Up)忽然以爲有點豁然開朗。那爲何不能從Ground Up開始實現一個vue.js呢。因此決定按照尤大課上的思路寫一寫。做者說過vue是漸進式的框架,那麼我想也用漸進式的思路去寫寫。每一步都去解決一點問題。vue

leason 1 響應式的實現

咱們將響應式的實現分爲3步,最終實現一個可以雙向數據綁定的Vuenode

  • 轉化:這裏咱們將實現一個convert函數可以監聽響應式的數據變化
  • 依賴的處理:對數據的依賴進行存儲,收集和觸發
  • 編譯:將響應式的數據與視圖進行綁定

本文代碼下載git

一、轉化:convert函數實現es6

目標:實現b的值可以跟隨a值自動變化?github

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)

代碼:bash

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中的操做app

二、依賴的處理框架

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收集依賴?frontend

depend(){
    if(!saveFunction){
        this.subscribers.push(saveFunction);
    }
  }

複製代碼
  • 咱們須要在dep中去判斷一下saveFunction是否爲null
  • saveFunction是一個全局變量,因此之後咱們能夠將它定義爲Dep的屬性——Dep.target

問題:如今咱們已經很好的完成了對數據的響應式處理,若是操做的是視圖咱們該如何綁定呢?

三、編譯:數據與視圖進行綁定

目標:實現一個數據綁定

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

相關文章
相關標籤/搜索