<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【Vue】Vue官方教程筆記——尤雨溪手寫mini-vue

          共 32647字,需瀏覽 66分鐘

           ·

          2021-05-22 23:04

          ??這周我看了看了尤大神親手寫的mini版Vue3,筆記如下請(qǐng)大家指正。

          一、整體工作流程

          Kapture 2020-12-10 at 16.13.53.gif
          1. 編譯器將視圖模板編譯為渲染函數(shù)
          2. 數(shù)據(jù)響應(yīng)模塊將數(shù)據(jù)對(duì)象初始化為響應(yīng)式數(shù)據(jù)對(duì)象
          3. 視圖渲染
            1. RenderPhase :渲染模塊使用渲染函數(shù)根據(jù)初始化數(shù)據(jù)生成虛擬Dom
            2. MountPhase  :利用虛擬Dom創(chuàng)建視圖頁面Html
            3. PatchPhase:數(shù)據(jù)模型一旦變化渲染函數(shù)將再次被調(diào)用生成新的虛擬Dom,然后做Dom Diff更新視圖Html

          二、三大模塊的分工

          image.png
          • 數(shù)據(jù)響應(yīng)式模塊
          • 編譯器
          • 渲染函數(shù)

          1. 數(shù)據(jù)響應(yīng)式模塊

          提供創(chuàng)建一切數(shù)據(jù)變化都是可以被監(jiān)聽的響應(yīng)式對(duì)象的方法。

          2. 編譯模塊

          將html模板編譯為渲染函數(shù)

          這個(gè)編譯過程可以在一下兩個(gè)時(shí)刻執(zhí)行

          • 瀏覽器運(yùn)行時(shí)  (runtime)
          • Vue項(xiàng)目打包編譯時(shí) (compile time)

          3. 渲染函數(shù)

          渲染函數(shù)通過以下三個(gè)周期將視圖渲染到頁面上

          • Render Phase
          • Mount Phase
          • Patch Phase

          三、MVVM原型(Mock版)

          MVVM原理

          MVVM框架其實(shí)就是在原先的View和Model之間增加了一個(gè)VM層完成以下工作。完成數(shù)據(jù)與視圖的監(jiān)聽。我們這一步先寫一個(gè)Mock版本。其實(shí)就是先針對(duì)固定的視圖和數(shù)據(jù)模型實(shí)現(xiàn)監(jiān)聽。

          1. 接口定義

          我們MVVM的框架接口和Vue3一模一樣。

          初始化需要確定

          • 視圖模板
          • 數(shù)據(jù)模型
          • 模型行為 - 比如我們希望click的時(shí)候數(shù)據(jù)模型的message會(huì)會(huì)倒序排列。
          const App = {
            // 視圖
            template`
          <input v-model="message"/>
          <button @click='click'>{{message}}</button>
          `
          ,
            setup() {
              // 數(shù)據(jù)劫持
              const state = new Proxy(
                {
                  message"Hello Vue 3!!",
                },
                {
                  set(target, key, value, receiver) {
                    const ret = Reflect.set(target, key, value, receiver);
                    // 觸發(fā)函數(shù)響應(yīng)
                    effective();
                    return ret;
                  },
                }
              );

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

          2. 程序骨架

          程序執(zhí)行過程大概如圖:

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

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

              return {
                mountfunction (container{
                  const dom = document.querySelector(container);
                  
              // 實(shí)現(xiàn)setup函數(shù)
                  const setupResult = config.setup();
              
                  // 數(shù)據(jù)響應(yīng)更新視圖
                  effective = () => render(setupResult, dom);
                  render(setupResult, dom);
                },
              };
            },
          };

          3.  編譯渲染函數(shù)

          MVVM框架中的渲染函數(shù)是會(huì)通過視圖模板的編譯建立的。

          // 編譯函數(shù)
          // 輸入值為視圖模板
          const compile = (template) => {
            //渲染函數(shù)
            return (observed, dom) => {
             // 渲染過程
           }
          }

          簡(jiǎn)單的說就是對(duì)視圖模板進(jìn)行解析并生成渲染函數(shù)。

          大概要處理以下三件事

          • 確定哪些值需要根據(jù)數(shù)據(jù)模型渲染

            // <button>{{message}}</button>
            // 將數(shù)據(jù)渲染到視圖
            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)
            })
          • 確定哪些輸入項(xiàng)需要雙向綁定

          // <input v-model="message"/>
          // 創(chuàng)建keyup事件監(jiān)聽輸入項(xiàng)修改
          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
          }

          四、數(shù)據(jù)響應(yīng)實(shí)現(xiàn)

          Vue普遍走的就是數(shù)據(jù)劫持方式。不同的在于使用DefineProperty還是Proxy。也就是一次一個(gè)屬性劫持還是一次劫持一個(gè)對(duì)象。當(dāng)然后者比前者聽著就明顯有優(yōu)勢(shì)。這也就是Vue3的響應(yīng)式原理。

          Proxy/Reflect是在ES2015規(guī)范中加入的,Proxy可以更好的攔截對(duì)象行為,Reflect可以更優(yōu)雅的操縱對(duì)象。優(yōu)勢(shì)在于

          • 針對(duì)整個(gè)對(duì)象定制 而不是對(duì)象的某個(gè)屬性,所以也就不需要對(duì)keys進(jìn)行遍歷。
          • 支持?jǐn)?shù)組,這個(gè)DefineProperty不具備。這樣就省去了重載數(shù)組方法這樣的Hack過程。
          • Proxy 的第二個(gè)參數(shù)可以有 13 種攔截方法,這比起 Object.defineProperty() 要更加豐富
          • Proxy 作為新標(biāo)準(zhǔn)受到瀏覽器廠商的重點(diǎn)關(guān)注和性能優(yōu)化,相比之下 Object.defineProperty() 是一個(gè)已有的老方法
          • 可以通過遞歸方便的進(jìn)行對(duì)象嵌套。

          說了這么多我們先來一個(gè)小例子

          var obj = new Proxy({}, {
              getfunction (target, key, receiver{
                  console.log(`getting ${key}!`);
                  return Reflect.get(target, key, receiver);
              },
              setfunction (target, key, value, receiver{
                  console.log(`setting ${key}!`);
                  return Reflect.set(target, key, value, receiver);
              }
          })
          obj.abc = 132

          這樣寫如果你修改obj中的值,就會(huì)打印出來。

          也就是說如果對(duì)象被修改就會(huì)得的被響應(yīng)。

          image-20200713122621925

          當(dāng)然我們需要的響應(yīng)就是重新更新視圖也就是重新運(yùn)行render方法。

          首先制造一個(gè)抽象的數(shù)據(jù)響應(yīng)函數(shù)

          // 定義響應(yīng)函數(shù)
          let effective
          observed = new Proxy(config.data(), {
            set(target, key, value, receiver) {
              const ret = Reflect.set(target, key, value, receiver)
              // 觸發(fā)函數(shù)響應(yīng)
              effective()
              return ret
            },
          })

          在初始化的時(shí)候我們?cè)O(shè)置響應(yīng)動(dòng)作為渲染視圖

          const dom = document.querySelector(container)
          // 設(shè)置響應(yīng)動(dòng)作為渲染視圖
          effective = () => render(observed, dom)
          render(observed, dom)

          1. 視圖變化的監(jiān)聽

          瀏覽器視圖的變化,主要體現(xiàn)在對(duì)輸入項(xiàng)變化的監(jiān)聽上,所以只需要通過綁定監(jiān)聽事件就可以了。

          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);
                    };
                    
                    // 生成渲染函數(shù)
                    const render = compile(config.template);

                    return {
                      mountfunction (container{
                        const dom = document.querySelector(container);
                        const setupResult = config.setup();
                        effective = () => render(setupResult, dom);
                        render(setupResult, dom);
                      },
                    };
                  },
                };
                // 定義響應(yīng)函數(shù)
                let effective;
                const App = {
                  // 視圖
                  template`
                          <input v-model="message"/>
                          <button @click='click'>{{message}}</button>
                      `
          ,
                  setup() {
                    // 數(shù)據(jù)劫持
                    const state = new Proxy(
                      {
                        message"Hello Vue 3!!",
                      },
                      {
                        set(target, key, value, receiver) {
                          const ret = Reflect.set(target, key, value, receiver);
                          // 觸發(fā)函數(shù)響應(yīng)
                          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>

          五、 視圖渲染過程

          Dom => virtual DOM => render functions

          1. 什么是Dom 、Document Object Model

          image.png

          HTML在瀏覽器中會(huì)映射為一些列節(jié)點(diǎn),方便我們?nèi)フ{(diào)用。

          image.png

          2. 什么是虛擬Dom

          Dom中節(jié)點(diǎn)眾多,直接查詢和更新Dom性能較差。

          A way of representing the actual DOM with JavaScript Objects. 用JS對(duì)象重新表示實(shí)際的Dom

          image.png

          3. 什么是渲染函數(shù)

          在Vue中我們通過將視圖模板(template)編譯為渲染函數(shù)(render function)再轉(zhuǎn)化為虛擬Dom

          4. 通過DomDiff高效更新視圖

          image.png

          5. 總結(jié)

          舉個(gè)栗子?? 虛擬Dom和Dom就像大樓和大樓設(shè)計(jì)圖之間的關(guān)系。假設(shè)你要在29層添加一個(gè)廚房 ? 拆除整個(gè)29層,重新建設(shè) ?先繪制設(shè)計(jì)圖,找出新舊結(jié)構(gòu)不同然后建設(shè)

          六、實(shí)現(xiàn)渲染函數(shù)

          在Vue中我們通過將視圖模板(template)編譯為渲染函數(shù)(render function)再轉(zhuǎn)化為虛擬Dom

          渲染流程通常會(huì)分為三各部分:

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

          • RenderPhase :渲染模塊使用渲染函數(shù)根據(jù)初始化數(shù)據(jù)生成虛擬Dom
          • MountPhase  :利用虛擬Dom創(chuàng)建視圖頁面Html
          • PatchPhase:數(shù)據(jù)模型一旦變化渲染函數(shù)將再次被調(diào)用生成新的虛擬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

          渲染模塊使用渲染函數(shù)根據(jù)初始化數(shù)據(jù)生成虛擬Dom

          render(content) {
            return h("div"null, [
              h("div"nullString(content.state.message)),
              h(
                "button",
                {
                  onClick: content.click,
                },
                "click"
              ),
            ]);
          },

          2. Mount Phase

          利用虛擬Dom創(chuàng)建視圖頁面Html

          function mountElement(vnode, container{
            // 渲染成真實(shí)的 dom 節(jié)點(diǎn)
            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);
            }

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

          3. Patch Phase(Dom diff)

          數(shù)據(jù)模型一旦變化渲染函數(shù)將再次被調(diào)用生成新的虛擬Dom,然后做Dom Diff更新視圖Html

          function patchProp(el, key, prevValue, nextValue{
            // onClick
            // 1. 如果前面2個(gè)值是 on 的話
            // 2. 就認(rèn)為它是一個(gè)事件
            // 3. on 后面的就是對(duì)應(yīng)的事件名
            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. 要檢測(cè) props 哪些有變化
            //    2. 要檢測(cè) children  -》 特別復(fù)雜的
            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);
              // 對(duì)比 props
              // 1. 新的節(jié)點(diǎn)不等于老節(jié)點(diǎn)的值 -> 直接賦值
              // 2. 把老節(jié)點(diǎn)里面新節(jié)點(diǎn)不存在的 key 都刪除掉
              if (newProps) {
                Object.keys(newProps).forEach((key) => {
                  if (newProps[key] !== oldProps[key]) {
                    patchProp(el, key, oldProps[key], newProps[key]);
                  }
                });

                // 遍歷老節(jié)點(diǎn) -》 新節(jié)點(diǎn)里面沒有的話,那么都刪除掉
                Object.keys(oldProps).forEach((key) => {
                  if (!newProps[key]) {
                    patchProp(el, key, oldProps[key], null);
                  }
                });
              }
              // 對(duì)比 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") {
                  // 清空之前的數(shù)據(jù)
                  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 的多的話,那么創(chuàng)建一個(gè)新的

                  // 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];
                    // 可以十分復(fù)雜
                    diff(oldVnode, newVnode);
                  }

                  if (oldChildren.length > length) {
                    // 說明老的節(jié)點(diǎn)多
                    // 都刪除掉
                    for (let i = length; i < oldChildren.length; i++) {
                      remove(oldChildren[i], el);
                    }
                  } else if (newChildren.length > length) {
                    // 說明 new 的節(jié)點(diǎn)多
                    // 那么需要?jiǎng)?chuàng)建對(duì)應(yīng)的節(jié)點(diǎn)
                    for (let i = length; i < newChildren.length; i++) {
                      mountElement(newChildren[i], el);
                    }
                  }
                }
              }
            }
          }

          七、編譯器原理

          這個(gè)地方尤大神并沒有實(shí)現(xiàn) 后續(xù)然叔會(huì)給大家提供一個(gè)超簡(jiǎn)潔的版本 這個(gè)章節(jié)我們主要看看compile這個(gè)功能。

          compiler

          上文已經(jīng)說過編譯函數(shù)的功能

          // 編譯函數(shù)
          // 輸入值為視圖模板
          const compile = (template) => {
            //渲染函數(shù)
            return (observed, dom) => {
             // 渲染過程
           }
          }

          簡(jiǎn)單的說就是

          • 輸入:視圖模板
          • 輸出:渲染函數(shù)

          細(xì)分起來還可以分為三個(gè)個(gè)小步驟

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

          • Transform  轉(zhuǎn)換標(biāo)記 譬如 v-bind v-if v-for的轉(zhuǎn)換

          • Generate AST -> 渲染函數(shù)

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

            我們可以通過在線版的VueTemplateExplorer感受一下

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

          image-20200713150630150

          編譯函數(shù)解析

          1. Parse解析器

          解析器的工作原理其實(shí)就是一連串的正則匹配。

          比如:

          標(biāo)簽屬性的匹配

          • 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"undefinedundefined"abc"index0input"class=abc"groupsundefined]

          "class='abc'".match(attr);
          // output
          (6) ["class='abc'""class""'abc'"undefined"abc"undefinedindex0input"class='abc'"groupsundefined]

          這個(gè)等實(shí)現(xiàn)的時(shí)候再仔細(xì)講。可以參考一下文章。

          AST解析器實(shí)戰(zhàn)

          那對(duì)于我們的項(xiàng)目來講就可以寫成這個(gè)樣子

          // <input v-model="message"/>
          // <button @click='click'>{{message}}</button>
          // 轉(zhuǎn)換后的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轉(zhuǎn)換處理

          前一段知識(shí)做的是抽象語法樹,對(duì)于Vue3模板的特別轉(zhuǎn)換就是在這里進(jìn)行。

          比如:vFor、vOn

          在Vue三種也會(huì)細(xì)致的分為兩個(gè)層級(jí)進(jìn)行處理

          • compile-core 核心編譯邏輯

            • AST-Parser

            • 基礎(chǔ)類型解析 v-for 、v-on

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

            • 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生成渲染器

          生成器其實(shí)就是根據(jù)轉(zhuǎn)換后的AST語法樹生成渲染函數(shù)。當(dāng)然針對(duì)相同的語法樹你可以渲染成不同結(jié)果。比如button你希望渲染成 button還是一個(gè)svg的方塊就看你的喜歡了。這個(gè)就叫做自定義渲染器。這里我們先簡(jiǎn)單寫一個(gè)固定的Dom的渲染器占位。到后面實(shí)現(xiàn)的時(shí)候我在展開處理。

          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
          }



          瀏覽 66
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  久久综合狼人 | 91精品国久久久久久无码一区二区三区 | 香蕉国产乱伦 | 欧美a片在线看 | 亚洲日韩一级精品片在线播放 |