帶你快速手寫一個簡易版vue瞭解vue響應式

起始

在使用vue的時候,數據雙向綁定,插值語法...等等,一系列讓人眼花繚亂的操做.做爲一個使用者搞懂輪子是怎麼造,並非爲了本身造一個輪子,而是爲了更好的使用輪子.javascript

雖然如今vue3已經開始使用,可是不少公司如今使用的應該仍是vue2.X.html

使用

在咱們使用的時候,通常都是在main文件進行初始化,通常都是這樣前端

new Vue({
  router,
  store,
  .......
  render: h => h(App)
}).$mount('#app')
複製代碼

在這裏對於我一個初入前端的切圖仔確定不會實現咱們在項目中那麼複雜,對於vue咱們還有另外一種用法,在html文件中引入vue.js,譬如這樣vue

new Vue({
    el: '#app',
    data: {
      number: 1,
    },
    methods: {
      add() {
        this.number++
      },
      changeInput() {
        console.log('changeInput');
      }
    }
  })
複製代碼

就像這樣,今天實現的就是以這樣使用的,在vue的真正使用中會有三種render,template和el,優先級也是如此排列.java

分析

首先須要分析咱們實現的是怎樣的一個類node

  • 首先它會接受一個對象,有el,data,methods.....
  • data中的數據須要響應式處理
  • 解析模板處理,也就是咱們在div中寫入的{{number}}之類
  • 響應式更新,這也是最重要的,在改變數據的時候通知視圖進行更新
  • 處理事件和指令,好比v-model,@click之類

開始操刀實現

首先建立vue這個類數組

class Vue{
  constructor(options) {
    // 將傳入的保存
    this.$options = options;
    // 將傳入data,保存在$data中
    this.$data = options.data
  }
}
複製代碼

可是在咱們的使用data的值時咱們歷來沒有使用過$data,咱們都是this.xxx就能夠拿到這個值,因而可知咱們首先須要將this.$data中的數據所有代理到這個類上讓咱們可使用this.XXX可使用這個也很簡單,使用defineProperty就能夠實現,下邊來實現這個方法.markdown

function proxy(vm) {
  //這裏的vm須要咱們拿到vue自己的實例
  Object.keys(vm.$data).forEach(key => {
    Object.defineProperty(vm, key, {
      get: () => vm.$data[key],
      set: (v) => vm.$data[key] = v
    })
  })
}
複製代碼

這個方法很簡單,不用作過多贅述,下邊在vue類中使用讓咱們能夠經過實例能夠拿到data中的值app

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
+   proxy(this)
  }
複製代碼

這樣咱們就可使用this.xx拿到,由於咱們尚未實現method因此咱們經過new Vue拿到的實例來實驗dom

const app = new Vue({
    el: '#app',
    data: {
      counter: 1,
    },
  })
  console.log(app.number);
複製代碼

咱們在控制檯能夠看到成功輸出了numbe的值

image.png 那麼對於data的代理就成功了.

模板解析

對於模板解析,須要處理寫入的插值表達式(寫入的自定義指令和事件,後邊會解決)

首先先將模板中的插值解析時頁面打開不會滿屏的大括號.

class Compile {
  //咱們須要將el和vue自己傳入來進行模板解析,el須要用來拿到元素,vue自己則須要其中的data,methds...
  constructor(el, vm) {
    this.$vm = vm
    //拿到咱們解析的元素
    this.$el = document.querySelector(el)

    if(this.$el) {
      // 編寫一個函數來解析模板
      this.compile(this.$el)
    }
  }
  compile(el) {
    // 遍歷el的子節點 判斷他們的類型作相應的處理
    const childNodes = el.childNodes
    if(!childNodes) return;
    childNodes.forEach(node => {
      if(node.nodeType === 1) {
        // 元素 處理指令和事件(後續來處理)
      } else if(this.isInter(node)) {
        // 文本
        this.compileText(node)
      }
      // 在有子元素的狀況下須要遞歸
      if(node.childNodes) {
        this.compile(node)
      }
    })
  }
  
  // 編譯文本
  compileText(node) {
      node.textContent = this.$vm[RegExp.$1]
  }

  // 是否插值表達式
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
}
複製代碼

而後在vue中使用它.

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
    proxy(this)
+   new Compile(options.el, this)
  }
}
複製代碼

這樣頁面中的插值就能夠被替換爲data中的數據了,可是咱們在外邊經過vue實例修改的時候數據是沒有變化的.

實現數據響應式

在實現數據響應式這裏,借用源碼的思想,使用Observer來作響應式,watcher和dep來通知更新,在vue中一個組件只有一個watcher,而咱們的粒度就沒辦法相提並論了,畢竟是一個只實現部分的簡單版vue. 首先編寫Observer

Observer編寫

// 遍歷obj作響應式
function observer(obj) {
  if(typeof obj !== 'object' || obj === null) {
    return
  }
  // 遍歷obj的全部key作響應式
  new Observer(obj);
}
// 遍歷obj的全部key作響應式
class Observer {
  constructor(value) {
    this.value = value
    if(Array.isArray(this.value)) {
      // TODO 對於數組的操做不作處理 無非是對於數組的七個方法進行重寫覆蓋
    } else {
      this.walk(value)
    }
  }
  // 對象響應式
  walk(obj) {
    Object.keys(obj).forEach(key => {
      //這個東西看到不少人會以爲很眼熟,固然也只是名字眼熟
      defineReactive(obj, key, obj[key])
    })
  }
}
複製代碼

defineReactive這裏是用於數據進行響應式處理函數,在進行這個函數的編寫以前,還須要先進行watcher的編寫

watcher編寫

// 監聽器:負責依賴更新
const watchers = []; //先不使用dep 先使用一個數組來收集watcher進行更新
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn
    watchers.push(this)
  }

  // 將來被Dep調用
  update() {
    // 執行實際的更新操做
    //由於咱們須要在watcher更新時拿到最新的值,因此須要咱們在這裏做爲參數傳遞給咱們在收集到的函數
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}
複製代碼

有了watcher以後,須要修改咱們以前寫的Compile類來進行watcher的收集,於此同時,修改編譯時的修改方法同時爲v-model和v-test...作前置基礎

Compile進化

class Compile {
  //咱們須要將el和vue自己傳入來進行模板解析,el須要用來拿到元素,vue自己則須要其中的data,methds...
  constructor(el, vm) {
    this.$vm = vm
    //拿到咱們解析的元素
    this.$el = document.querySelector(el)

    if(this.$el) {
      // 編寫一個函數來解析模板
      this.compile(this.$el)
    }
  }
  compile(el) {
    // 遍歷el的子節點 判斷他們的類型作相應的處理
    const childNodes = el.childNodes
    if(!childNodes) return;
    childNodes.forEach(node => {
      if(node.nodeType === 1) {
        // 元素 處理指令和事件(後續來處理)
      } else if(this.isInter(node)) {
        // 文本
        this.compileText(node)
      }
      // 在有子元素的狀況下須要遞歸
      if(node.childNodes) {
        this.compile(node)
      }
    })
  }
  
  //新添加函數
  //node 爲修改的元素 exp爲獲取到大括號內的值的key dir爲這邊自定義的要執行的操做
  update(node, exp, dir) {
    // 初始化
    const fn = this[dir + 'Update']
    fn && fn(node, this.$vm[exp])
    // 更新 在這裏建立watcher 並將更新的函數傳進去 這裏的val就是watcher觸發更新函數時傳入的最新值
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val)
    })
  }
  //新添加函數
  textUpdate(node, val) {
     node.textContent = val
   }
  // 編譯文本
  compileText(node) {
  -   //node.textContent = this.$vm[RegExp.$1]
  +   this.update(node, RegExp.$1, 'text')
  }

  // 是否插值表達式
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
}
複製代碼

defineReactive編寫

下邊來編寫defineReactive來達到視圖真正的更新

/** * 在key發生變化時能夠感知作出操做 * @param {*} obj 對象 * @param {*} key 須要攔截的key * @param {*} val 初始值 */
 function defineReactive(obj, key, val) {
  // 遞歸
  observer(val);
  Object.defineProperty(obj, key, {
    get() {
      return val
    },
    set(newVal) {
      console.log('set', newVal);
      if (newVal != val) {
        observer(newVal)
        val = newVal
        //這裏爲粗糙實現 後續會加入dep會更加精緻一點
        watchers.forEach(w => w.update())
      }
    }
  })
}
複製代碼

在vue類中使用

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
+   observer(this.$data)
    proxy(this)
    new Compile(options.el, this)
  }
}
複製代碼

接下來在修改咱們使用的頁面

const app = new Vue({
    el: '#app',
    data: {
      number:10,
    }
  })
  console.log(app.number);
  setTimeout(() => {
    app.counter = 100
  }, 1000)
複製代碼

就能夠在頁面中看到在一秒鐘以後視圖朝着咱們預想的結果發生變化,這樣子就達到了我想要的結果,可是這樣存在一個問題,就是當咱們在data中定義了不少屬性的時候,在頁面中使用. 當咱們改變其中一個的時候,咱們全部的watcher都會執行一遍,頁面全部用到data的地方都會發生更新,這樣的問題我是確定不想要的,這樣就須要dep來管理,達到咱們修改其中一個值,那只有修改的地方會發生變化,這樣就會更好一點. 接下來先編寫dep這個類,這個類的的功能很簡單,一個dep管理data中的一條數據,收集和這條數據有關係的watcher,在發生變化時通知更新

Dep編寫

class Dep{
  constructor() {
    this.deps = []
  }
  addDep(dep) {
    this.deps.push(dep)
  }
  notify() {
    this.deps.forEach(dep => dep.update())
  }
}
複製代碼

在dep爲前提時須要修改watcher這個類,同時也再也不須要watchers這個數組來管理.

Watcher進化

// 監聽器:負責依賴更新
- //const watchers = []; //先不使用dep 先使用一個數組來收集watcher進行更新
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn
-    watchers.push(this)
    // 觸發依賴收集
+   Dep.target = this
    //在這裏純屬由於須要觸發get來進行收集,下邊重寫defineReactive是會用到
+   this.vm[this.key]
+   Dep.target = null
  }

  // 將來被Dep調用
  update() {
    // 執行實際的更新操做
    //由於咱們須要在watcher更新時拿到最新的值,因此須要咱們在這裏做爲參數傳遞給咱們在收集到的函數
    this.updateFn.call(this.vm, this.vm[this.key])
  }
}
複製代碼

編寫好Watcher後來從新方法,來使用到dep來管理更新,而不是將全部watcher都進行觸發更新

defineReactive進化

/** * 在key發生變化時能夠感知作出操做 * @param {*} obj 對象 * @param {*} key 須要攔截的key * @param {*} val 初始值 */
 function defineReactive(obj, key, val) {
  // 遞歸
  observer(val);
   // 建立Dep實例
  // data中的數據每一項都會進入到此,建立一個Dep
+  const dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
       // 依賴收集
      // 只有在調用時存在target纔會被Dep收集更新(在初始化時設置靜態屬性target爲watcher,被收集)
      //咱們在watcher進入時獲取過一次當前使用到的watcher的值這裏就會進入get,而且當時wacher將本身設置爲了Dep的target,在這裏使用到進行收集方便更新,可是在觸發以後將dep.target設置爲了null,使咱們在平時的讀取時不作這個操做
+     Dep.target && dep.addDep(Dep.target)
+     return val
    },
    set(newVal) {
      console.log('set', newVal);
      if (newVal != val) {
        observer(newVal)
        val = newVal
        //這裏爲粗糙實現 後續會加入dep會更加精緻一點
-       watchers.forEach(w => w.update())
        // 被修改時通知全部屬於本身的watcher更新
        // 一個watcher對應一處依賴,一個Dep對應一個data中的數據 一個dep的更新能夠指向多個watcher
+       dep.notify()
      }
    }
  })
}
複製代碼

接下來修改使用的html文件來查看效果

const app = new Vue({
    el: '#app',
    data: {
      counter: 1,
      number:10,
    },
  })
  console.log(app.number);
  setTimeout(() => {
    app.counter = 100
  }, 2000)
複製代碼

這樣子咱們在沒有使用到這個數據的地方就不會產生dom更新.在使用number的地方並無產生變化

image.png 接下來編寫指令和事件的梳理 主要修改Compile類在模板解析時處理,處理以前咱們首先修改Vue類,將methods保存

class Vue{
  constructor(options) {
    this.$options = options;
    this.$data = options.data
+   this.$methods = options.methods
    observer(this.$data)
    proxy(this)
    new Compile(options.el, this)
  }
}
複製代碼

Compile最終成型

接下來修改Compile類在模板解析時處理指令和事件

class Compile {
  //咱們須要將el和vue自己傳入來進行模板解析,el須要用來拿到元素,vue自己則須要其中的data,methds...
  constructor(el, vm) {
    this.$vm = vm
    //拿到咱們解析的元素
    this.$el = document.querySelector(el)

    if(this.$el) {
      // 編寫一個函數來解析模板
      this.compile(this.$el)
    }
  }
  compile(el) {
    // 遍歷el的子節點 判斷他們的類型作相應的處理
    const childNodes = el.childNodes
    if(!childNodes) return;
    childNodes.forEach(node => {
      if(node.nodeType === 1) {
        // 元素 處理指令和事件
+       const attrs = node.attributes
+       Array.from(attrs).forEach(attr => {
+         // v-xxx="abc"
+         const attrName = attr.name
+         const exp = attr.value
+         //當屬性值以v-開頭便認爲這是一個指令 這裏只處理v-text v-model v-html
+         if(attrName.startsWith('v-')) {
+           const dir = attrName.substring(2)
+           this[dir] && this[dir](node, exp)
+         //事件的處理也很是簡單
+         } else if(attrName.startsWith('@')) {
+           const dir = attrName.substring(1)
+           this.eventFun(node, exp, dir)
+         }
+       })
      } else if(this.isInter(node)) {
        // 文本
        this.compileText(node)
      }
      // 在有子元素的狀況下須要遞歸
      if(node.childNodes) {
        this.compile(node)
      }
    })
  }
+ //新添加函數 處理事件
  eventFun(node, exp, dir) {
    node.addEventListener(dir, this.$vm.$methods[exp].bind(this.$vm))
  }
  //node 爲修改的元素 exp爲獲取到大括號內的值的key dir爲這邊自定義的要執行的操做
  update(node, exp, dir) {
    // 初始化
    const fn = this[dir + 'Update']
    fn && fn(node, this.$vm[exp])
    // 更新 在這裏建立watcher 並將更新的函數傳進去 這裏的val就是watcher觸發更新函數時傳入的最新值
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val)
    })
  }
  textUpdate(node, val) {
     node.textContent = val
   }
+ //新添加函數 處理v-text
  text(node, exp) {
    this.update(node, exp, 'text')
  }
+ //新添加函數 處理v-html
  html(node, exp) {
    this.update(node, exp, 'html')
  }
+ //新添加函數
  htmlUpdate(node, val) {
    node.innerHTML = val
  }
  // 編譯文本
  compileText(node) {
  -   //node.textContent = this.$vm[RegExp.$1]
  +   this.update(node, RegExp.$1, 'text')
  }
+ //新添加函數 處理v-model
  model(node, exp) {
    // console.log(node, exp);
    this.update(node, exp, 'model')
    node.addEventListener('input', (e) => {
      // console.log(e.target.value);
      // console.log(this);
      this.$vm[exp] = e.target.value
    })
  }
  // 是否插值表達式
  isInter(node) {
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)
  }
}
複製代碼

接下來修改html文件查看效果

<body>
  <div id="app"> <p @click="add">{{counter}}</p> <p>{{counter}}</p> <p>{{counter}}</p> <p>{{number}}</p> <p v-text="counter"></p> <p v-html="desc"></p> <p><input type="text" c-model="desc"></p> <p><input type="text" value="changeInput" @input="changeInput"></p> </div> </body> <script> const app = new Vue({ el: '#app', data: { counter: 1, number:10, desc: `<h1 style="color:red">hello CVue</h1>` }, methods: { add() { console.log('add',this); this.counter++ }, changeInput() { console.log('changeInput'); } } }) </script> 複製代碼

到這裏就編寫結束了,頁面也達到了預期的效果.

image.png

體驗網址: codesandbox.io/s/dazzling-…

做爲本身學習的筆記,代碼中也有不少疏忽,借鑑別人的代碼來實現,不過學到了就是本身的.

最後

點個贊再走吧!

相關文章
相關標籤/搜索