[Vue官方教程筆記]- 尤雨溪手寫mini-vue

上週發了 【Vue3官方教程】🎄萬字筆記 | 同步導學視頻 1050贊javascript

🔥這周我看了看了尤大神親手寫的mini版Vue3,筆記以下請你們指正。html

【原版視頻】前端

image-20201230111207968

⚡️關注公衆號【前端大班車】 回覆 【mini-vue】索取完整代碼

1、總體工做流程

Kapture 2020-12-10 at 16.13.53.gif

  1. 編譯器將視圖模板編譯爲渲染函數
  2. 數據響應模塊將數據對象初始化爲響應式數據對象
  3. 視圖渲染
    1. RenderPhase : 渲染模塊使用渲染函數根據初始化數據生成虛擬Dom
    2. MountPhase : 利用虛擬Dom建立視圖頁面Html
    3. PatchPhase:數據模型一旦變化渲染函數將再次被調用生成新的虛擬Dom,而後作Dom Diff更新視圖Html

2、三大模塊的分工

image.png

  • 數據響應式模塊
  • 編譯器
  • 渲染函數

1. 數據響應式模塊

提供建立一切數據變化都是能夠被監聽的響應式對象的方法。 Kapture 2020-12-10 at 11.47.59.gifvue

2. 編譯模塊

image.png 將html模板編譯爲渲染函數java

這個編譯過程能夠在一下兩個時刻執行node

  • 瀏覽器運行時 (runtime)
  • Vue項目打包編譯時 (compile time)

3. 渲染函數

渲染函數經過如下三個週期將視圖渲染到頁面上 image.pngweb

  • Render Phase
  • Mount Phase
  • Patch Phase

3、MVVM原型(Mock版)

MVVM原理

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

1. 接口定義

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

初始化須要肯定性能優化

  • 視圖模板
  • 數據模型
  • 模型行爲 - 好比咱們但願click的時候數據模型的message會會倒序排列。
const App = {
  // 視圖
  template: ` <input v-model="message"/> <button @click='click'>{{message}}</button> `,
  setup() {
    // 數據劫持
    const state = new Proxy(
      {
        message: "Hello Vue 3!!",
      },
      {
        set(target, key, value, receiver) {
          const ret = Reflect.set(target, key, value, receiver);
          // 觸發函數響應
          effective();
          return ret;
        },
      }
    );

    const click = () => {
      state.message = state.message.split("").reverse().join("");
    };
    return { state, click };
  },
};
const { createApp } = Vue;
createApp(App).mount("#app");
複製代碼

2. 程序骨架

程序執行過程大概如圖:

render-proxy

const Vue = {
  createApp(config) {
    // 編譯過程
    const compile = (template) => (content, dom) => {
      
    };

    // 生成渲染函數
    const render = compile(config.template);

    return {
      mount: function (container) {
        const dom = document.querySelector(container);
        
				// 實現setup函數
        const setupResult = config.setup();
				
        // 數據響應更新視圖
        effective = () => render(setupResult, dom);
        render(setupResult, dom);
      },
    };
  },
};
複製代碼

3. 編譯渲染函數

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
}
複製代碼

4、數據響應實現

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

固然咱們須要的響應就是從新更新視圖也就是從新運行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)
複製代碼

1. 視圖變化的監聽

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

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

2. 完整的代碼

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

複製代碼

5、 視圖渲染過程

Dom => virtual DOM => render functions

1. 什麼是Dom 、Document Object Model

image.png

HTML在瀏覽器中會映射爲一些列節點,方便咱們去調用。

image.png

2. 什麼是虛擬Dom

Dom中節點衆多,直接查詢和更新Dom性能較差。

A way of representing the actual DOM with JavaScript Objects. 用JS對象從新表示實際的Dom

image.png

3. 什麼是渲染函數

在Vue中咱們經過將視圖模板(template)編譯爲渲染函數(render function)再轉化爲虛擬Dom image.png

4. 經過DomDiff高效更新視圖

image.png

5. 總結

舉個栗子🌰 虛擬Dom和Dom就像大樓和大樓設計圖之間的關係。 image.png 假設你要在29層添加一個廚房 ❌ 拆除整個29層,從新建設 ✅先繪製設計圖,找出新舊結構不一樣而後建設

6、實現渲染函數

在Vue中咱們經過將視圖模板(template)編譯爲渲染函數(render function)再轉化爲虛擬Dom image.png

渲染流程一般會分爲三各部分:

vue-next-template-explorer.netlify.app/

  • RenderPhase : 渲染模塊使用渲染函數根據初始化數據生成虛擬Dom
  • MountPhase : 利用虛擬Dom建立視圖頁面Html
  • PatchPhase:數據模型一旦變化渲染函數將再次被調用生成新的虛擬Dom,而後作Dom Diff更新視圖Html
mount: function (container) {
    const dom = document.querySelector(container);
    const setupResult = config.setup();
    const render = config.render(setupResult);

    let isMounted = false;
    let prevSubTree;
    watchEffect(() => {
      if (!isMounted) {
        dom.innerHTML = "";
        // mount
        isMounted = true;
        const subTree = config.render(setupResult);
        prevSubTree = subTree;
        mountElement(subTree, dom);
      } else {
        // update
        const subTree = config.render(setupResult);
        diff(prevSubTree, subTree);
        prevSubTree = subTree;
      }
    });
  },
複製代碼

1.Render Phase

渲染模塊使用渲染函數根據初始化數據生成虛擬Dom

render(content) {
  return h("div", null, [
    h("div", null, String(content.state.message)),
    h(
      "button",
      {
        onClick: content.click,
      },
      "click"
    ),
  ]);
},
複製代碼

2. Mount Phase

利用虛擬Dom建立視圖頁面Html

function mountElement(vnode, container) {
  // 渲染成真實的 dom 節點
  const el = (vnode.el = createElement(vnode.type));

  // 處理 props
  if (vnode.props) {
    for (const key in vnode.props) {
      const val = vnode.props[key];
      patchProp(vnode.el, key, null, val);
    }
  }

  // 要處理 children
  if (Array.isArray(vnode.children)) {
    vnode.children.forEach((v) => {
      mountElement(v, el);
    });
  } else {
    insert(createText(vnode.children), el);
  }

  // 插入到視圖內
  insert(el, container);
}

複製代碼

3. Patch Phase(Dom diff)

數據模型一旦變化渲染函數將再次被調用生成新的虛擬Dom,而後作Dom Diff更新視圖Html

function patchProp(el, key, prevValue, nextValue) {
  // onClick
  // 1. 若是前面2個值是 on 的話
  // 2. 就認爲它是一個事件
  // 3. on 後面的就是對應的事件名
  if (key.startsWith("on")) {
    const eventName = key.slice(2).toLocaleLowerCase();
    el.addEventListener(eventName, nextValue);
  } else {
    if (nextValue === null) {
      el.removeAttribute(key, nextValue);
    } else {
      el.setAttribute(key, nextValue);
    }
  }
}
複製代碼

經過DomDiff - 高效更新視圖

image.png

image-20201230104838657

function diff(v1, v2) {
  // 1. 若是 tag 都不同的話,直接替換
  // 2. 若是 tag 同樣的話
  // 1. 要檢測 props 哪些有變化
  // 2. 要檢測 children -》 特別複雜的
  const { props: oldProps, children: oldChildren = [] } = v1;
  const { props: newProps, children: newChildren = [] } = v2;
  if (v1.tag !== v2.tag) {
    v1.replaceWith(createElement(v2.tag));
  } else {
    const el = (v2.el = v1.el);
    // 對比 props
    // 1. 新的節點不等於老節點的值 -> 直接賦值
    // 2. 把老節點裏面新節點不存在的 key 都刪除掉
    if (newProps) {
      Object.keys(newProps).forEach((key) => {
        if (newProps[key] !== oldProps[key]) {
          patchProp(el, key, oldProps[key], newProps[key]);
        }
      });

      // 遍歷老節點 -》 新節點裏面沒有的話,那麼都刪除掉
      Object.keys(oldProps).forEach((key) => {
        if (!newProps[key]) {
          patchProp(el, key, oldProps[key], null);
        }
      });
    }
    // 對比 children

    // newChildren -> string
    // oldChildren -> string oldChildren -> array

    // newChildren -> array
    // oldChildren -> string oldChildren -> array
    if (typeof newChildren === "string") {
      if (typeof oldChildren === "string") {
        if (newChildren !== oldChildren) {
          setText(el, newChildren);
        }
      } else if (Array.isArray(oldChildren)) {
        // 把以前的元素都替換掉
        v1.el.textContent = newChildren;
      }
    } else if (Array.isArray(newChildren)) {
      if (typeof oldChildren === "string") {
        // 清空以前的數據
        n1.el.innerHTML = "";
        // 把全部的 children mount 出來
        newChildren.forEach((vnode) => {
          mountElement(vnode, el);
        });
      } else if (Array.isArray(oldChildren)) {
        // a, b, c, d, e -> new
        // a1,b1,c1,d1 -> old
        // 若是 new 的多的話,那麼建立一個新的

        // a, b, c -> new
        // a1,b1,c1,d1 -> old
        // 若是 old 的多的話,那麼把多的都刪除掉
        const length = Math.min(newChildren.length, oldChildren.length);
        for (let i = 0; i < length; i++) {
          const oldVnode = oldChildren[i];
          const newVnode = newChildren[i];
          // 能夠十分複雜
          diff(oldVnode, newVnode);
        }

        if (oldChildren.length > length) {
          // 說明老的節點多
          // 都刪除掉
          for (let i = length; i < oldChildren.length; i++) {
            remove(oldChildren[i], el);
          }
        } else if (newChildren.length > length) {
          // 說明 new 的節點多
          // 那麼須要建立對應的節點
          for (let i = length; i < newChildren.length; i++) {
            mountElement(newChildren[i], el);
          }
        }
      }
    }
  }
}
複製代碼

7、編譯器原理

這個地方尤大神並無實現 後續然叔會給你們提供一個超簡潔的版本 這個章節咱們主要看看compile這個功能。

compiler

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

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

簡單的說就是

  • 輸入:視圖模板
  • 輸出:渲染函數

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

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感覺一下

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

image-20200713150630150

編譯函數解析

1. 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}}'
        }
    ],
})
複製代碼

2. Transform轉換處理

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

好比:vFor、vOn

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

  • compile-core 核心編譯邏輯

    • AST-Parser

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

      image-20200713183256931

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

    • v-html

    • v-model

    • v-clock

      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'
                },
            }]
        }
    ],
})
複製代碼

3. 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
}

複製代碼

🔥關注公衆號【前端大班車】 回覆 【mini-vue】索取完整代碼

關注全棧然叔

近期文章(感謝掘友的鼓勵與支持🌹🌹🌹)

歡迎拍磚,一塊兒探討更優雅的實現

相關文章
相關標籤/搜索