<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>

          有關(guān)vue源碼的簡單實現(xiàn) 實現(xiàn)一個屬于自己的min-vue

          共 9241字,需瀏覽 19分鐘

           ·

          2022-07-07 18:54

          vue-study

          自己實現(xiàn)的mini-vue倉庫:https://github.com/maolovecoding/mini-vue2-stage[1] 建議克隆代碼觀看,效果更佳。

          1. 實現(xiàn)了Vue
          2. 實現(xiàn)了響應(yīng)式數(shù)據(jù)
          3. 實現(xiàn)了模板編譯
          4. 實現(xiàn)了ast轉(zhuǎn)render
          5. render執(zhí)行生成虛擬dom
          6. 虛擬dom轉(zhuǎn)真實dom渲染頁面
          7. 響應(yīng)式數(shù)據(jù)和頁面渲染結(jié)合 數(shù)據(jù)改變可自動更新視圖
          8. 實現(xiàn)同步更新數(shù)據(jù),異步更新視圖
          9. 優(yōu)雅降級

          10.實現(xiàn)nextTick 11. 實現(xiàn)了mixin,目前只支持生命周期的合并

          代碼中注釋很多,覺得不習(xí)慣的可以一邊看一邊刪除多余的注釋。

          vue的常見源碼實現(xiàn)

          rollup環(huán)境搭建

          安裝rollup及其插件

          npm i rollup rollup-plugin-babel @babel/core @babel/preset-env rollup-plugin-node-resolve -D
          復(fù)制代碼

          編寫配置文件 rollup.config.js

          這個可以直接使用es module

          // rollup默認(rèn)可以導(dǎo)出一個對象 作為打包的配置文件
          import babel from "rollup-plugin-babel";
          import resolve from 'rollup-plugin-node-resolve'
          export default {
            // 入口
            input"./src/index.js",
            // 出口
            output: {
              // 生成的文件
              file"./dist/vue.js",
              // 全局對象 Vue 在global(瀏覽器端就是window)上掛載一個屬性 Vue
              name"Vue",
              // 打包方式 esm commonjs模塊 iife自執(zhí)行函數(shù) umd 統(tǒng)一模塊規(guī)范 -> 兼容cmd和amd
              format"umd",
              // 打包后和源代碼做關(guān)聯(lián)
              sourcemaptrue,
            },
            plugins: [
              babel({
                // 排除第三方模塊
                exclude"node_modules/**",
              }),
              // 自動找文件夾下的index文件
              resolve()
            ],
          };


          復(fù)制代碼
          babel.config.js
          // babel config
          module.exports =  {
            // 預(yù)設(shè)
            presets: ["@babel/preset-env"],
          };

          復(fù)制代碼

          編寫腳本

          "scripts": {
              "dev""rollup -cw"
            }
          復(fù)制代碼

          -c表示使用配置文件,-w表示監(jiān)控文件變化。

          element.outerHTML

          outerHTML屬性獲取描述元素(包括其后代)的序列化HTML片段。它也可以設(shè)置為用從給定字符串解析的節(jié)點替換元素。

            <div id="app">
              <h2>{{name}}</h2>
              <span>{{age}}</span>
            </div>
            <script>
              console.log(document.querySelector("#app").outerHTML)
              /*
              <div id="app">
                <h2>{{name}}</h2>
                <span>{{age}}</span>
              </div>
            */
            
          </script>
          復(fù)制代碼

          核心流程

          vue的核心流程:

          1. 創(chuàng)造響應(yīng)式數(shù)據(jù)
          2. 模板編譯 生成 ast
          3. ast 轉(zhuǎn)為render函數(shù) 后續(xù)每次數(shù)據(jù)更新 只執(zhí)行render函數(shù)(不需要再次進(jìn)行ast的轉(zhuǎn)換)
          4. render函數(shù)執(zhí)行 生成 vNode節(jié)點(會使用到響應(yīng)式數(shù)據(jù))
          5. 根據(jù)vNode 生成 真實dom 渲染頁面
          6. 數(shù)據(jù)更新 重新執(zhí)行render

          數(shù)據(jù)劫持

          Vue2中使用的是Object.definedProperty,Vue3中直接使用Proxy了

          模板編譯為ast

          vue2中使用的是正則表達(dá)式進(jìn)行匹配,然后轉(zhuǎn)換為ast樹。

          模板引擎 性能差 需要正則匹配 替換 vue1.0 沒有引入虛擬dom的改變,vue2 采用虛擬dom 數(shù)據(jù)變化后比較虛擬dom的差異 最后更新需要更新的地方, 核心就是我們需要將模板變成我們的js語法 通過js語法生成虛擬dom,語法之間的轉(zhuǎn)換 需要先變成抽象語法樹AST 再組裝為新的語法,這里就是把template語法轉(zhuǎn)為render函數(shù)。

          ast轉(zhuǎn)render

          把生成的ast語法樹,通過字符串拼接等方式轉(zhuǎn)為render函數(shù)。render函數(shù)內(nèi)部主要用到:

          1. _c函數(shù):創(chuàng)建元素虛擬dom節(jié)點
          2. _v函數(shù):創(chuàng)建文本虛擬dom節(jié)點
          3. _s函數(shù):將函數(shù)內(nèi)的變量字符串化

          render函數(shù)生成真實dom

          調(diào)用render函數(shù),會生成虛擬dom,然后把虛擬dom轉(zhuǎn)為真實DOM,掛載到頁面即可。

          回憶流程

          核心流程:

          1. 數(shù)據(jù)處理成響應(yīng)式,在 initState中處理的(針對對象來說主要是definedProperty,數(shù)組則是重寫七個方法)
          2. 模板編譯:先把模板轉(zhuǎn)成ast語法樹,再把語法樹生成render函數(shù)
          3. 調(diào)用render函數(shù),可能會進(jìn)行變量的取值操作(_s函數(shù)內(nèi)有變量),產(chǎn)生對應(yīng)的虛擬dom
          4. 虛擬dom渲染為真實dom,掛載到頁面即可

          完成了,虛擬和真實dom的渲染,也完成了響應(yīng)式數(shù)據(jù)的處理,接下來需要進(jìn)行視圖和響應(yīng)式數(shù)據(jù)的關(guān)聯(lián),在渲染頁面的時候,收集依賴數(shù)據(jù)。

          1. 使用觀察者模式實現(xiàn)依賴收集
          2. 異步更新策略
          3. mixin的實現(xiàn)原理

          模板的依賴收集

          要完成依賴的收集,很明顯的就是,我們要如何得知,此模板在此次渲染的時候,用到了那些響應(yīng)式數(shù)據(jù)。

          我們可以給模板中的屬性,增加一個收集器(dep)。這個收集器,是給每個屬性單獨增加的。頁面渲染的時候,我們把渲染邏輯封裝到watcher中。(其實就是手動更新視圖的那兩個方法app._update(app._render()))。讓dep記住這個watcher即可,在屬性變化了以后,可以找到對應(yīng)的dep中存放的watcher,然后執(zhí)行重新渲染頁面。

          這里面我們用到的方式其實就是觀察者模式。

          /**
           * watcher 進(jìn)行實際的視圖渲染
           * 每個組件都有自己的watcher,可以減少每次更新頁面的部分
           * 給每個屬性都增加一個dep,目的就是收集watcher
           * 一個視圖(組件)可能有很多屬性,多個屬性對應(yīng)一個視圖 n個dep對應(yīng)1個watcher
           * 一個屬性也可能對應(yīng)多個視圖(組件)
           * 所以 dep 和 watcher 是多對多關(guān)系
           * 
           * 每個屬性都有自己的dep,屬性就是被觀察者
           * watcher就是觀察者(屬性變化了會通知觀察者進(jìn)行視圖更新)-> 觀察者模式
           */

          class Watcher{}
          復(fù)制代碼

          先讓watcher收集dep,如果dep已經(jīng)收集過,則不會再次收集。當(dāng)dep被收集的時候,我們也會讓dep反向收集當(dāng)前的watcher。實現(xiàn)二者的雙向收集。

          然后在響應(yīng)式數(shù)據(jù)發(fā)送改變的時候,通知dep的觀察者(watcher)進(jìn)行視圖更新。

          image-20220415105750259

          視圖同步渲染

          此時,已經(jīng)完成了響應(yīng)式數(shù)據(jù)和視圖的綁定,在數(shù)據(jù)發(fā)生改變的情況下,視圖會同步更新。也就是說,我們更新了兩次響應(yīng)式數(shù)據(jù),也會更新兩次視圖。

          image-20220415110028536

          正常情況下,更新兩次視圖是沒有問題的,但是此時兩次數(shù)據(jù)的更新發(fā)生在一次同步代碼中,我們應(yīng)該讓視圖的更新是異步的,這樣在一次操作更新多個數(shù)據(jù)的情況下,也只會渲染一次視圖,提高渲染速率。

          那么我們的想法就是合并更新,在所有的更新數(shù)據(jù)做完以后,在刷新頁面。也就是批處理,事件環(huán)。

          事件環(huán)

          我們的期望就是,同步代碼執(zhí)行完畢之后,在執(zhí)行視圖的渲染(作為異步任務(wù))。把更新操作延遲。

          方法就是使用一個隊列維護(hù)需要更新的watcher,第一次更新屬性值的時候,就開啟一個定時器,清空所有的watcher。后續(xù)的數(shù)據(jù)改變的操作,都不會再次開啟定時器,只是會把需要更新的watcher再次入隊列。(當(dāng)然watcher我們會先去重)。

          但是這個清空操作是在同步代碼執(zhí)行完畢后才會執(zhí)行的。

          // watcher queue 本次需要更新的視圖隊列
          let queue = [];
          // watcher 去重  {0:true,1:true}
          let has = {};
          // 批處理 也可以說是防抖
          let pending = false;
          /**
           * 不管執(zhí)行多少次update操作,但是我們最終只執(zhí)行一輪刷新操作
           * @param {*} watcher
           */

          function queueWatcher(watcher{
            const id = watcher.id;
            // 去重
            if (!has[id]) {
              queue.push(watcher);
              has[id] = true;
              console.log(queue);
              if (!pending) {
                // 刷新隊列 多個屬性刷新 其實執(zhí)行的只是第一次 合并刷新了
                setTimeout(flushSchedulerQueue, 0);
                pending = true;
              }
            }
          }
          /**
           * 刷新調(diào)度隊列 且清理當(dāng)前的標(biāo)識 has pending 等都重置
           * 先執(zhí)行第一批的watcher,如果刷新過程中有新的watcher產(chǎn)生,再次加入隊列即可
           */

          function flushSchedulerQueue({
            const flushQueue = [...queue];
            queue = [];
            has = {};
            pending = false;
            // 刷新視圖 如果在刷新過程中 還有新的watcher 會重新放到queueWatcher中
            flushQueue.forEach((watcher) => watcher.run()); // run 就是執(zhí)行render
          }
          復(fù)制代碼

          nextTick

          原理:

          因為我們數(shù)據(jù)的更新和視圖的更新不再是同步,導(dǎo)致我們在同步獲取視圖最新的dom元素時,可能出現(xiàn)獲取的元素和視圖實際顯示的元素不一致的情況。于是出現(xiàn)了 nextTick方法

          實際上:nextTick方法內(nèi)部也是維護(hù)了一個異步回調(diào)隊列,開啟一個定時器,每次調(diào)用該方法傳入回調(diào),都是把回調(diào)函數(shù)放入隊列,并不是每次調(diào)用nextTick方法都開啟一個定時器(比較銷毀性能)。再放入第一個回調(diào)函數(shù)的時候,開啟定時器,后續(xù)的回調(diào)函數(shù)只放入隊列而不會再次開啟定時器了,。所以nextTick不是創(chuàng)建了異步任務(wù),而是將這個任務(wù)維護(hù)到了隊列而已。

          nextTick方法是同步還是異步?

          把任務(wù)(回調(diào))放到隊列是同步,實際執(zhí)行任務(wù)是異步。

          // 任務(wù)隊列
          let callbacks = [];
          // 是否等待任務(wù)刷新
          let waiting = false;
          /**
           * 刷新異步回調(diào)函數(shù)隊列
           */

          function flushCallbacks({
            const cbs = [...callbacks];
            callbacks = [];
            waiting = false;
            cbs.forEach((cb) => cb());
          }
          /**
           * 異步批處理
           * 是先執(zhí)行內(nèi)部的回調(diào) 還是用戶的? 用個隊列 排序
           * @param {Function} cb 回調(diào)函數(shù)
           */

          export function nextTick(cb{
            // 使用隊列維護(hù)nextTick中的callback方法
            callbacks.push(cb);
            if (!waiting) {
              setTimeout(flushCallbacks, 0); // 刷新
              waiting = true;
            }
          }
          復(fù)制代碼

          vue的nextTick

          實際上,vue的nextTick方法,內(nèi)部并沒有直接使用原生的某一個異步api(比如promise,setTimeout等)。而是采用優(yōu)雅降級的方法。

          1. 內(nèi)部先采用的是promise(ie不兼容)。
          2. 有一個和Promise等價的 MutationObserve[2]。也是異步微任務(wù)。(此API是H5的,只能在瀏覽器中使用)
          3. 考慮ie瀏覽器專享的 setImmediate API。性能比settimeout好一些
          4. 最后直接上setTimeout

          **采用優(yōu)雅降級的目的,**還是為了用戶可以盡快看見頁面的渲染。

          /**
           * 優(yōu)雅降級  Promise -> MutationObserve -> setImmediate -> setTimeout(需要開線程 開銷最大)
           */

          let timerFunc = null;
          if (Promise) {
            timerFunc = () => Promise.resolve().then(flushCallbacks);
          else if (MutationObserver) {
            // 創(chuàng)建并返回一個新的 MutationObserver 它會在指定的DOM發(fā)生變化時被調(diào)用(異步執(zhí)行callback)。
            const observer = new MutationObserver(flushCallbacks);
            // TODO 創(chuàng)建文本節(jié)點的API 應(yīng)該封裝 為了方便跨平臺
            const textNode = document.createTextNode(1);
            console.log("observer-----------------")
            // 監(jiān)控文本值的變化
            observer.observe(textNode, {
              characterDatatrue,
            });
            timerFunc = () => (textNode.textContent = 2);
          else if (setImmediate) {
            // IE平臺
            timerFunc = () => setImmediate(flushCallbacks);
          else {
            timerFunc = () => setTimeout(flushCallbacks, 0);
          }
          復(fù)制代碼

          對于vue3,肯定就不需要這種方式了,在不兼容ie的情況下,可以直接使用promise了。

          image-20220415150046818

          經(jīng)過一次次處理,現(xiàn)在是可以在視圖更新以后再去拿最新的dom了。

          當(dāng)然:對于更改值放在取值的下面,那么獲取到的肯定還是舊的dom值。vue也是如此的。

          image-20220415150347883

          mixin的實現(xiàn)

          Vue的mixin,可以實現(xiàn)全局混入和局部混入。

          全局混入對所有組件實例都生效。

          暫時我實現(xiàn)了生命周期的混入,對于data等其他特殊選項的合并還未處理。

          對于混入的生命周期,無論是一個還是多個相同的生命周期,最終我們都轉(zhuǎn)為使用數(shù)組包裹,每個數(shù)組元素都是混入進(jìn)來的生命周期。在創(chuàng)建組件實例的時候,把傳入的選項和全局的Vue.options選項進(jìn)行合并到實例上,實現(xiàn)混入效果。

          image-20220415220542253

          參考資料

          [1]

          https://github.com/maolovecoding/mini-vue2-stage

          [2]

          https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver

          關(guān)于本文

          來自:codermao

          https://juejin.cn/post/7105011877159108644


          最后


          歡迎關(guān)注【前端瓶子君】??ヽ(°▽°)ノ?
          回復(fù)「算法」,加入前端編程源碼算法群,每日一道面試題(工作日),第二天瓶子君都會很認(rèn)真的解答喲!
          回復(fù)「交流」,吹吹水、聊聊技術(shù)、吐吐槽!
          回復(fù)「閱讀」,每日刷刷高質(zhì)量好文!
          如果這篇文章對你有幫助,在看」是最大的支持
           》》面試官也在看的算法資料《《
          “在看和轉(zhuǎn)發(fā)”就是最大的支持


          瀏覽 35
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  日韩一区二区特一级免费在线观看 | 蜜臀日韩免费 | 欧美午夜成人性爱网站 | 高清免费无码视频 | 国产av色婷婷 |