1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

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

        共 32647字,需瀏覽 66分鐘

         ·

        2021-05-22 23:04

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

        一、整體工作流程

        Kapture 2020-12-10 at 16.13.53.gif
        1. 編譯器將視圖模板編譯為渲染函數(shù)
        2. 數(shù)據(jù)響應(yīng)模塊將數(shù)據(jù)對象初始化為響應(yīng)式數(shù)據(jù)對象
        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)式對象的方法。

        2. 編譯模塊

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

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

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

        3. 渲染函數(shù)

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

        • Render Phase
        • Mount Phase
        • Patch Phase

        三、MVVM原型(Mock版)

        MVVM原理

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

        1. 接口定義

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

        初始化需要確定

        • 視圖模板
        • 數(shù)據(jù)模型
        • 模型行為 - 比如我們希望click的時候數(shù)據(jù)模型的message會會倒序排列。
        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ù)是會通過視圖模板的編譯建立的。

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

        簡單的說就是對視圖模板進(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。也就是一次一個屬性劫持還是一次劫持一個對象。當(dāng)然后者比前者聽著就明顯有優(yōu)勢。這也就是Vue3的響應(yīng)式原理。

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

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

        說了這么多我們先來一個小例子

        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中的值,就會打印出來。

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

        image-20200713122621925

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

        首先制造一個抽象的數(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è)置響應(yīng)動作為渲染視圖

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

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

        瀏覽器視圖的變化,主要體現(xiàn)在對輸入項(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在瀏覽器中會映射為一些列節(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對象重新表示實(shí)際的Dom

        image.png

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

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

        4. 通過DomDiff高效更新視圖

        image.png

        5. 總結(jié)

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

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

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

        渲染流程通常會分為三各部分:

        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個值是 on 的話
          // 2. 就認(rèn)為它是一個事件
          // 3. on 后面的就是對應(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. 要檢測 props 哪些有變化
          //    2. 要檢測 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);
            // 對比 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);
                }
              });
            }
            // 對比 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)建一個新的

                // 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)多
                  // 那么需要創(chuàng)建對應(yīng)的節(jié)點(diǎn)
                  for (let i = length; i < newChildren.length; i++) {
                    mountElement(newChildren[i], el);
                  }
                }
              }
            }
          }
        }

        七、編譯器原理

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

        compiler

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

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

        簡單的說就是

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

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

        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]

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

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

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

        // <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)換處理

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

        比如:vFor、vOn

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

        • compile-core 核心編譯邏輯

          • AST-Parser

          • 基礎(chǔ)類型解析 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生成渲染器

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

        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)贊
        評論
        收藏
        分享

        手機(jī)掃一掃分享

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

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产精品久久久久网站 | 成人无码手机在线观看 | 日本理伦少妇2做爰 | 三级片电影麻豆 | 欧美大爆乳性猛交 | 91亚洲精品久久久蜜桃 网站 | 美女被操91 | 成年人天堂 | 国产嗷嗷叫| 国产性生大片免费观看性 |