漸進式手敲Vue3.0框架 - 2萬字以上 - 持續更新

只有刻意練習才能提升。javascript

前面關注Vue3主要是閱讀源碼也趁機摸魚了提了一些PR,竟然還有一個經過的算是給vue大業也作了點點貢獻。html

https://github.com/vuejs/vue-next/pull/1389vue

爲了更好的理解Vue3源碼我計劃使用漸進式的方法完成一個簡寫版的Vue框架。java

寫做計劃

歡迎你們持續關注、首先作一個簡單的計劃。git

這個計劃必定會變😜,要否則怎麼叫迭代呢。github

📎 Mock狀態web

🚀簡版實現數組

Step 00 01 02 03 04 05 06
響應式邏輯 - 📎 🚀 🚀 🚀 🚀 🚀
編譯函數 - 📎 📎 📎 🚀 🚀 🚀
Parser - 📎 📎 🚀 🚀 🚀
Transformer - - 📎 📎 🚀 🚀 🚀
Generator - - 📎 📎 🚀 🚀 🚀
運行時環境 - 📎 📎 🚀 🚀 🚀 🚀
渲染器 - 📎 📎 📎 🚀 🚀 🚀
核心特性 Dom Diff - - - - - 🚀 🚀
靜態提高 - - - - - - 🚀
自定義渲染器 - - - - - - 🚀
- - - - - - 🚀

完整的代碼瀏覽器

Step00 NoMVVM

想象一下若是沒有MVVM框架咱們要怎麼實現一個這樣的功能。性能優化

  • 建立一個數據模型
const data = {
 message: 'Hello Vue 3!!'  } 複製代碼
  • 建立一個視圖
<div id='app'>
 <input />  <button></button> </div> 複製代碼
  • 建立一個將模型數據更新到視圖上的渲染函數
function update() {
 // 更新視圖  document.querySelector('button').innerHTML = data.message  document.querySelector('input').value = data.message }  複製代碼
  • 執行首次數據更新
// 首次數據渲染
update() 複製代碼
  • 綁定按鈕點擊事件
    • 修改模型中數據: 反轉字符串
    • 修改模型後從新渲染數據
document.querySelector('button').addEventListener('click', function () {
 data.message = data.message.split('').reverse().join('')  update() }) 複製代碼
  • 對輸入項變化進行監聽
    • 數據項變化時修改模型中數據
    • 修改模型後從新渲染數據
document.querySelector('input').addEventListener('keyup', function () {
 data.message = this.value  update() }) 複製代碼

Step01 整體架構 - MVVM(Mock版)

MVVM原理
MVVM原理

MVVM框架其實就是在原先的View和Model之間增長了一個VM層完成如下工做。完成數據與視圖的監聽。咱們這一步先寫一個Mock版本。其實就是先針對固定的視圖和數據模型實現監聽。

接口定義

咱們MVVM的框架接口和Vue3如出一轍。

初始化須要肯定

  • 視圖模板
  • 數據模型
  • 模型行爲 - 好比咱們但願click的時候數據模型的message會會倒序排列。
const App = {
 // 視圖模板  template: ` <input v-model="message"/> <button @click='click'>{{message}}</button> `,  // 數據模型  data() {  return {  message: 'Hello Vue 3!!'  }  },  // 行爲函數  methods: {  click() {  this.message = this.message.split('').reverse().join('')  }  } } const {  createApp } = Vue createApp(App).mount('#app') 複製代碼

程序骨架

const Vue = {
 createApp(config) {  // 編譯過程  const compile = (template) => (observed, dom) => {  }  // 生成渲染函數  const render = compile()   // 定義響應函數  let effective  // 數據劫持  observed = new Proxy(config.data(), {  })   return {  // 初始化  mount: function (container) {  }  }  } } 複製代碼

編譯渲染函數

MVVM框架中的渲染函數是會經過視圖模板的編譯創建的。

// 編譯函數
// 輸入值爲視圖模板 const compile = (template) => {  //渲染函數  return (observed, dom) => {  // 渲染過程  } } 複製代碼

簡單的說就是對視圖模板進行解析並生成渲染函數。

大概要處理如下三件事

  • 肯定哪些值須要根據數據模型渲染

    // <button>{{message}}</button>
    // 將數據渲染到視圖 button = document.createElement('button') button.innerText = observed.message dom.appendChild(button) 複製代碼
  • 綁定模型事件

    // <button @click='click'>{{message}}</button>
    // 綁定模型事件 button.addEventListener('click', () => {  return config.methods.click.apply(observed) }) 複製代碼
  • 肯定哪些輸入項須要雙向綁定

// <input v-model="message"/>
// 建立keyup事件監聽輸入項修改 input.addEventListener('keyup', function () {  observed.message = this.value }) 複製代碼

完整的代碼

const compile = (template) => (observed, dom) => {
  // 從新渲染  let input = dom.querySelector('input')  if (!input) {  input = document.createElement('input')  input.setAttribute('value', observed.message)   input.addEventListener('keyup', function () {  observed.message = this.value  })  dom.appendChild(input)  }  let button = dom.querySelector('button')  if (!button) {  console.log('create button')  button = document.createElement('button')  button.addEventListener('click', () => {  return config.methods.click.apply(observed)  })  dom.appendChild(button)  }  button.innerText = observed.message } 複製代碼

數據監聽的實現

Vue廣泛走的就是數據劫持方式。不一樣的在於使用DefineProperty仍是Proxy。也就是一次一個屬性劫持仍是一次劫持一個對象。固然後者比前者聽着就明顯有優點。這也就是Vue3的響應式原理。

Proxy/Reflect是在ES2015規範中加入的,Proxy能夠更好的攔截對象行爲,Reflect能夠更優雅的操縱對象。 優點在於

  • 針對整個對象定製 而不是對象的某個屬性,因此也就不須要對keys進行遍歷。
  • 支持數組,這個DefineProperty不具有。這樣就省去了重載數組方法這樣的Hack過程。
  • Proxy 的第二個參數能夠有 13 種攔截方法,這比起 Object.defineProperty() 要更加豐富
  • Proxy 做爲新標準受到瀏覽器廠商的重點關注和性能優化,相比之下 Object.defineProperty() 是一個已有的老方法
  • 能夠經過遞歸方便的進行對象嵌套。

說了這麼多咱們先來一個小例子

var obj = new Proxy({}, {
 get: function (target, key, receiver) {  console.log(`getting ${key}!`);  return Reflect.get(target, key, receiver);  },  set: function (target, key, value, receiver) {  console.log(`setting ${key}!`);  return Reflect.set(target, key, value, receiver);  } }) obj.abc = 132  複製代碼

這樣寫若是你修改obj中的值,就會打印出來。

也就是說若是對象被修改就會得的被響應。

image-20200713122621925
image-20200713122621925

固然咱們須要的響應就是從新更新視圖也就是從新運行render方法。

首先製造一個抽象的數據響應函數

// 定義響應函數
let effective observed = new Proxy(config.data(), {  set(target, key, value, receiver) {  const ret = Reflect.set(target, key, value, receiver)  // 觸發函數響應  effective()  return ret  }, }) 複製代碼

在初始化的時候咱們設置響應動做爲渲染視圖

const dom = document.querySelector(container)
// 設置響應動做爲渲染視圖 effective = () => render(observed, dom) render(observed, dom) 複製代碼

視圖變化的監聽

瀏覽器視圖的變化,主要體如今對輸入項變化的監聽上,因此只須要經過綁定監聽事件就能夠了。

document.querySelector('input').addEventListener('keyup', function () {
 data.message = this.value  }) 複製代碼

完整的代碼

<html lang="en">
 <body>  <div id='app'></div>  <script>  const App = {  // 視圖  template: `  <input v-model="message"/>  <button @click='click'>{{message}}</button>  `,  data() {  return {  message: 'Hello Vue 3!!'  }  },  methods: {  click() {  this.message = this.message.split('').reverse().join('')  }  }  }   const Vue = {  createApp(config) {   // 編譯過程 const compile = (template) => (observed, dom) => {   // 從新渲染  let input = dom.querySelector('input')  if (!input) {  input = document.createElement('input')  input.setAttribute('value', observed.message)  input.addEventListener('keyup', function () {  observed.message = this.value  })  dom.appendChild(input)  }  let button = dom.querySelector('button')  if (!button) {  console.log('create button')  button = document.createElement('button')  button.addEventListener('click', () => {  return config.methods.click.apply(observed)  })  dom.appendChild(button)  }  button.innerText = observed.message }  // 生成渲染函數  const render = compile()   // 定義響應函數  let effective   // 數據劫持  observed = new Proxy(config.data(), {  set(target, key, value, receiver) {  const ret = Reflect.set(target, key, value, receiver)  // 觸發函數響應  effective()  return ret  },  })   return {  mount: function (container) {  const dom = document.querySelector(container)  effective = () => render(observed, dom)  render(observed, dom)  }  }  }  }   const {  createApp  } = Vue  createApp(App).mount('#app')  </script> </body>  </html> 複製代碼

OK今天寫到這,終於完成了第一步雖然大部分還都是固定的至少把大致結構搞定了。

Step02 編譯流程(Mock)

這個章節咱們主要看看compile這個功能。

上文已經說過編譯函數的功能

// 編譯函數
// 輸入值爲視圖模板 const compile = (template) => {  //渲染函數  return (observed, dom) => {  // 渲染過程  } } 複製代碼

簡單的說就是

  • 輸入:視圖模板

  • 輸出:渲染函數

細分起來還能夠分爲三個個小步驟

Snip20200713_17
Snip20200713_17
  • Parse 模板字符串 -> AST(Abstract Syntax Treee)抽象語法樹

  • Transform 轉換標記 譬如 v-bind v-if v-for的轉換

  • Generate AST -> 渲染函數

    // 模板字符串 -> AST(Abstract Syntax Treee)抽象語法樹
    let ast = parse(template) // 轉換處理 譬如 v-bind v-if v-for的轉換 ast = transfer(ast) // AST -> 渲染函數 return generator(ast) 複製代碼

    咱們能夠經過在線版的VueTemplateExplorer感覺一下

    https://vue-next-template-explorer.netlify.com/

image-20200713150630150
image-20200713150630150

編譯函數解析

Parse解析器

解析器的工做原理其實就是一連串的正則匹配。

好比:

標籤屬性的匹配

  • class="title"

  • class='title'

  • class=title

const attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)=("([^"]*)"|'([^']*)'|([^\s"'=<>`]+)/
 "class=abc".match(attr); // output (6) ["class=abc", "class", "abc", undefined, undefined, "abc", index: 0, input: "class=abc", groups: undefined]  "class='abc'".match(attr); // output (6) ["class='abc'", "class", "'abc'", undefined, "abc", undefined, index: 0, input: "class='abc'", groups: undefined]  複製代碼

這個等實現的時候再仔細講。能夠參考一下文章。

AST解析器實戰

那對於咱們的項目來說就能夠寫成這個樣子

// <input v-model="message"/>
// <button @click='click'>{{message}}</button> // 轉換後的AST語法樹 const parse = template => ({  children: [{  tag: 'input',  props: {  name: 'v-model',  exp: {  content: 'message'  },  },  },  {  tag: 'button',  props: {  name: '@click',  exp: {  content: 'message'  },  },  content:'{{message}}'  }  ], }) 複製代碼

Transform轉換處理

前一段知識作的是抽象語法樹,對於Vue3模板的特別轉換就是在這裏進行。

好比:vFor、vOn

在Vue三種也會細緻的分爲兩個層級進行處理

  • compile-core 核心編譯邏輯

    • AST-Parser

    • 基礎類型解析 v-for 、v-on

      image-20200713183256931
      image-20200713183256931
  • compile-dom 針對瀏覽器的編譯邏輯

    • v-html

    • v-model

    • v-clock

      image-20200713183210079
      image-20200713183210079
const transfer = ast => ({
 children: [{  tag: 'input',  props: {  name: 'model',  exp: {  content: 'message'  },  },  },  {  tag: 'button',  props: {  name: 'click',  exp: {  content: 'message'  },  },  children: [{  content: {  content: 'message'  },  }]  }  ], }) 複製代碼

Generate生成渲染器

生成器其實就是根據轉換後的AST語法樹生成渲染函數。固然針對相同的語法樹你能夠渲染成不一樣結果。好比button你但願渲染成 button仍是一個svg的方塊就看你的喜歡了。這個就叫作自定義渲染器。這裏咱們先簡單寫一個固定的Dom的渲染器佔位。到後面實現的時候我在展開處理。

const generator = ast => (observed, dom) => {
 // 從新渲染  let input = dom.querySelector('input')  if (!input) {  input = document.createElement('input')  input.setAttribute('value', observed.message)  input.addEventListener('keyup', function () {  observed.message = this.value  })  dom.appendChild(input)  }  let button = dom.querySelector('button')  if (!button) {  console.log('create button')  button = document.createElement('button')  button.addEventListener('click', () => {  return config.methods.click.apply(observed)  })  dom.appendChild(button)  }  button.innerText = observed.message }  複製代碼

喜歡的點贊👍👍👍👍👍 保持關注

我會持續更新的

本文使用 mdnice 排版

歡迎你們加入一塊兒共同窗習進步。

最新消息和優秀文章我會第一時間推送的。

視頻講解 b站視頻 www.bilibili.com/video/BV1fa…

關於發佈時間

具體時間能夠看你們能夠看看官方時間表。 官方時間表

目前在Vue3處於Beta版本,後面主要是處理穩定性問題。也就是說主要Api不會有不少改進。尤大神從直播中說雖然不少想法,可是大的變化最快也會出如今3.1上面了。因此目前的版本應該應該和正式版差別很小了。 看來Q2能發佈的可能性極大。

腦圖

>>>>>>>️腦圖連接<<<<<<<
相關文章
相關標籤/搜索