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

          2021年,讓我們手寫(xiě)一個(gè)mini版本的vue2.x和vue3.x框架

          共 24577字,需瀏覽 50分鐘

           ·

          2021-07-10 13:02

          作者:夕水

          來(lái)源:SegmentFault 思否社區(qū)

          mini版本的vue.js2.X版本框架

          模板代碼

          首先我們看一下我們要實(shí)現(xiàn)的模板代碼:

          <div id="app">
              <h3>{{ msg }}</h3>
              <p>{{ count }}</p>
              <h1>v-text</h1>
              <p v-text="msg"></p>
              <input type="text" v-model="count">
              <button type="button" v-on:click="increase">add+</button>
              <button type="button" v-on:click="changeMessage">change message!</button>
              <button type="button" v-on:click="recoverMessage">recoverMessage!</button>
          </div>

          邏輯代碼

          然后就是我們要編寫(xiě)的javascript代碼。

          const app = new miniVue({
              el:"#app",
              data:{
                  msg:"hello,mini vue.js",
                  count:666
              },
              methods:{
                  increase(){
                      this.count++;
                  },
                  changeMessage(){
                      this.msg = "hello,eveningwater!";
                  },
                  recoverMessage(){
                      console.log(this)
                      this.msg = "hello,mini vue.js";
                  }
              }
          });

          運(yùn)行效果

          我們來(lái)看一下實(shí)際運(yùn)行效果如下所示:

          思考一下,我們要實(shí)現(xiàn)如上的功能應(yīng)該怎么做呢?你也可以單獨(dú)打開(kāi)以上示例:
          點(diǎn)擊此處。

          源碼實(shí)現(xiàn)-2.x

          miniVue類(lèi)

          首先,不管三七二十一,既然是實(shí)例化一個(gè)mini-vue,那么我們先定義一個(gè)類(lèi),并且它的參數(shù)一定是一個(gè)屬性配置對(duì)象。如下:
           class miniVue {
               constructor(options = {}){
                   //后續(xù)要做的事情
               }
           }
          現(xiàn)在,讓我們先初始化一些屬性,比如data,methods,options等等。
          //在miniVue構(gòu)造函數(shù)的內(nèi)部
          //保存根元素,能簡(jiǎn)便就盡量簡(jiǎn)便,不考慮數(shù)組情況
          this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
          this.$methods = options.methods;
          this.$data = options.data;
          this.$options = options;
          初始化完了之后,我們?cè)賮?lái)思考一個(gè)問(wèn)題,我們是不是可以通過(guò)在vue內(nèi)部使用this訪問(wèn)到vue定義的數(shù)據(jù)對(duì)象呢?那么我們應(yīng)該如何實(shí)現(xiàn)這一個(gè)功能呢?這個(gè)功能有一個(gè)專(zhuān)業(yè)的名詞,叫做代理(proxy)。

          代理數(shù)據(jù)

          因此我們來(lái)實(shí)現(xiàn)一下這個(gè)功能,很明顯在這個(gè)miniVue類(lèi)的內(nèi)部定義一個(gè)proxy方法。如下:
          //this.$data.xxx -> this.xxx;
          //proxy代理實(shí)例上的data對(duì)象
          proxy(data){
              //后續(xù)代碼
          }
          接下來(lái),我們需要知道一個(gè)api,即Object.defineProperty,通過(guò)這個(gè)方法來(lái)完成這個(gè)代理方法。如下:
          //proxy方法內(nèi)部
          // 因?yàn)槲覀兪谴砻恳粋€(gè)屬性,所以我們需要將所有屬性拿到
          Object.keys(data).forEach(key => {
              Object.defineProperty(this,key,{
                  enumerable:true,
                  configurable:true,
                  get:() => {
                      return data[key];
                  },
                  set:(newValue){
                      //這里我們需要判斷一下如果值沒(méi)有做改變就不用賦值,需要排除NaN的情況
                      if(newValue === data[key] || _isNaN(newValue,data[key]))return;
                      data[key] = newValue;
                  }
              })
          })
          接下來(lái),我們來(lái)看一下這個(gè)_isNaN工具方法的實(shí)現(xiàn),如下:
          function _isNaN(a,b){
              return Number.isNaN(a) && Number.isNaN(b);
          }
          定義好了之后,我們只需要在miniVue類(lèi)的構(gòu)造函數(shù)中調(diào)用一次即可。如下:
          // 構(gòu)造函數(shù)內(nèi)部
          this.proxy(this.$data);
          代理就這樣完成了,讓我們繼續(xù)下一步。

          數(shù)據(jù)響應(yīng)式觀察者observer類(lèi)

          我們需要對(duì)數(shù)據(jù)的每一個(gè)屬性都定義一個(gè)響應(yīng)式對(duì)象,用來(lái)監(jiān)聽(tīng)數(shù)據(jù)的改變,所以我們需要一個(gè)類(lèi)來(lái)管理它,我們就給它取個(gè)名字叫Observer。如下:
          class Observer {
              constructor(data){
                  //后續(xù)實(shí)現(xiàn)
              }
          }
          我們需要給每一個(gè)數(shù)據(jù)都添加響應(yīng)式對(duì)象,并且轉(zhuǎn)換成getter和setter函數(shù),這里我們又用到了Object.defineProperty方法,我們需要在getter函數(shù)中收集依賴(lài),在setter函數(shù)中發(fā)送通知,用來(lái)通知依賴(lài)進(jìn)行更新。我們用一個(gè)方法來(lái)專(zhuān)門(mén)去執(zhí)行定義響應(yīng)式對(duì)象的方法,叫walk,如下:
          //再次申明,不考慮數(shù)組,只考慮對(duì)象
          walk(data){
              if(typeof data !== 'object' || !data)return;
              // 數(shù)據(jù)的每一個(gè)屬性都調(diào)用定義響應(yīng)式對(duì)象的方法
              Object.keys(data).forEach(key => this.defineReactive(data,key,data[key]));
          }
          接下來(lái)我們來(lái)看defineReactive方法的實(shí)現(xiàn),同樣也是使用Object.defineProperty方法來(lái)定義響應(yīng)式對(duì)象,如下所示:
          defineReactive(data,key,value){
              // 獲取當(dāng)前this,以避免后續(xù)用vm的時(shí)候,this指向不對(duì)
              const vm = this;
              // 遞歸調(diào)用walk方法,因?yàn)閷?duì)象里面還有可能是對(duì)象
              this.walk(value);
              //實(shí)例化收集依賴(lài)的類(lèi)
              let dep = new Dep();
              Object.defineProperty(data,key,{
                  enumerable:true,
                  configurable:true,
                  get(){
                      // 收集依賴(lài),依賴(lài)存在Dep類(lèi)上
                      Dep.target && Dep.add(Dep.target);
                      return value;
                  },
                  set(newValue){
                      // 這里也判斷一下
                      if(newValue === value || __isNaN(value,newValue))return;
                      // 否則改變值
                      value = newValue;
                      // newValue也有可能是對(duì)象,所以遞歸
                      vm.walk(newValue);
                      // 通知Dep類(lèi)
                      dep.notify();
                  }
              })
          }
          Observer類(lèi)完成了之后,我們需要在miniVue類(lèi)的構(gòu)造函數(shù)中實(shí)例化一下它,如下:
          //在miniVue構(gòu)造函數(shù)內(nèi)部
          new Observer(this.$data);
          好的,讓我們繼續(xù)下一步。

          依賴(lài)類(lèi)

          defineReactive方法內(nèi)部用到了Dep類(lèi),接下來(lái),我們來(lái)定義這個(gè)類(lèi)。如下:
          class Dep {
              constructor(){
                  //后續(xù)代碼
              }
          }
          接下來(lái),我們來(lái)思考一下,依賴(lài)類(lèi)里面,我們需要做什么,首先根據(jù)defineReactive中,我們很明顯就知道會(huì)有add方法和notify方法,并且我們需要一種數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)依賴(lài),vue源碼用的是隊(duì)列,而在這里為了簡(jiǎn)單化,我們使用ES6的set數(shù)據(jù)結(jié)構(gòu)。如下:
          //構(gòu)造函數(shù)內(nèi)部
          this.deps = new Set();
          接下來(lái),就需要實(shí)現(xiàn)add方法和notify方法,事實(shí)上這里還會(huì)有刪除依賴(lài)的方法,但是這里為了最簡(jiǎn)便,我們只需要一個(gè)addnotify方法即可。如下:
          add(dep){
              //判斷dep是否存在并且是否存在update方法,然后添加到存儲(chǔ)的依賴(lài)數(shù)據(jù)結(jié)構(gòu)中
              if(dep && dep.update)this.deps.add(dep);
          }
          notify(){
              // 發(fā)布通知無(wú)非是遍歷一道dep,然后調(diào)用每一個(gè)dep的update方法,使得每一個(gè)依賴(lài)都會(huì)進(jìn)行更新
              this.deps.forEach(dep => dep.update())
          }
          Dep類(lèi)算是完了,接下來(lái)我們就需要另一個(gè)類(lèi)。

          Watcher類(lèi)

          那就是為了管理每一個(gè)組件實(shí)例的類(lèi),確保每個(gè)組件實(shí)例可以由這個(gè)類(lèi)來(lái)發(fā)送視圖更新以及狀態(tài)流轉(zhuǎn)的操作。這個(gè)類(lèi),我們把它叫做Watcher
          class Watcher {
              //3個(gè)參數(shù),當(dāng)前組件實(shí)例vm,state也就是數(shù)據(jù)以及一個(gè)回調(diào)函數(shù),或者叫處理器
              constructor(vm,key,cb){
                  //后續(xù)代碼
              }
          }
          再次思考一下,我們的Watcher類(lèi)需要做哪些事情呢?我們先來(lái)思考一下Watcher的用法,我們是不是會(huì)像如下這樣來(lái)寫(xiě):
          //3個(gè)參數(shù),當(dāng)前組件實(shí)例vm,state也就是數(shù)據(jù)以及一個(gè)回調(diào)函數(shù),或者叫處理器
          new Watcher(vm,key,cb);
          ok,知道了使用方式之后,我們就可以在構(gòu)造函數(shù)內(nèi)部初始化一些東西了。如下:
          //構(gòu)造函數(shù)內(nèi)部
          this.vm = vm;
          this.key = key;
          this.cb = cb;
          //依賴(lài)類(lèi)
          Dep.target = this;
          // 我們用一個(gè)變量來(lái)存儲(chǔ)舊值,也就是未變更之前的值
          this.__old = vm[key];
          Dep.target = null;
          然后Watcher類(lèi)就多了一個(gè)update方法,接下來(lái)讓我們來(lái)看一下這個(gè)方法的實(shí)現(xiàn)吧。如下:
          update(){
              //獲取新的值
              let newValue = this.vm[this.key];
              //與舊值做比較,如果沒(méi)有改變就無(wú)需執(zhí)行下一步
              if(newValue === this.__old || __isNaN(newValue,this.__old))return;
              //把新的值回調(diào)出去
              this.cb(newValue);
              //執(zhí)行完之后,需要更新一下舊值的存儲(chǔ)
              this.__old = newValue;
          }

          編譯類(lèi)compiler類(lèi)

          初始化

          到了這一步,我們就算是完全脫離vue源碼了,因?yàn)関ue源碼的編譯十分復(fù)雜,涉及到diff算法以及虛擬節(jié)點(diǎn)vNode,而我們這里致力于將其最簡(jiǎn)化,所以單獨(dú)寫(xiě)一個(gè)Compiler類(lèi)來(lái)編譯。如下:
          class Compiler {
              constructor(vm){
                  //后續(xù)代碼
              }
          }
          注意:這里的編譯是我們自己根據(jù)流程來(lái)實(shí)現(xiàn)的,與vue源碼并沒(méi)有任何關(guān)聯(lián),vue也有compiler,但是與我們實(shí)現(xiàn)的完全不同。
          定義好了之后,我們?cè)趍iniVue類(lèi)的構(gòu)造函數(shù)中實(shí)例化一下這個(gè)編譯類(lèi)即可。如下:
          //在miniVue構(gòu)造函數(shù)內(nèi)部
          new Compiler(this);
          好的,我們也看到了使用方式,所以接下來(lái)我們來(lái)完善這個(gè)編譯類(lèi)的構(gòu)造函數(shù)內(nèi)部的一些初始化操作。如下:
          //編譯類(lèi)構(gòu)造函數(shù)內(nèi)部
          //根元素
          this.el = vm.$el;
          //事件方法
          this.methods = vm.$methods;
          //當(dāng)前組件實(shí)例
          this.vm = vm;
          //調(diào)用編譯函數(shù)開(kāi)始編譯
          this.compile(vm.$el);

          compile方法

          初始化操作算是完成了,接下來(lái)我們來(lái)看compile方法的內(nèi)部。思考一下,在這個(gè)方法的內(nèi)部,我們是不是需要拿到所有的節(jié)點(diǎn),然后對(duì)比是文本還是元素節(jié)點(diǎn)去分別進(jìn)行編譯呢?如下:
          compile(el){
              //拿到所有子節(jié)點(diǎn)(包含文本節(jié)點(diǎn))
              let childNodes = el.childNodes;
              //轉(zhuǎn)成數(shù)組
              Array.from(childNodes).forEach(node => {
                  //判斷是文本節(jié)點(diǎn)還是元素節(jié)點(diǎn)分別執(zhí)行不同的編譯方法
                  if(this.isTextNode(node)){
                      this.compileText(node);
                  }else if(this.isElementNode(node)){
                      this.compileElement(node);
                  }
                  //遞歸判斷node下是否還含有子節(jié)點(diǎn),如果有的話(huà)繼續(xù)編譯
                  if(node.childNodes && node.childNodes.length)this.compile(node);
              })
          }
          這里,我們需要2個(gè)輔助方法,判斷是文本節(jié)點(diǎn)還是元素節(jié)點(diǎn),其實(shí)我們可以使用節(jié)點(diǎn)的nodeType屬性來(lái)進(jìn)行判斷,由于文本節(jié)點(diǎn)的nodeType值為3,而元素節(jié)點(diǎn)的nodeType值為1。所以這2個(gè)輔助方法我們就可以實(shí)現(xiàn)如下:
          isTextNode(node){
              return node.nodeType === 3;
          }
          isElementNode(node){
              return node.nodeType === 3;
          }

          編譯文本節(jié)點(diǎn)

          接下來(lái),我們下來(lái)看compileText編譯文本節(jié)點(diǎn)的方法。如下:
          //{{ count }}數(shù)據(jù)結(jié)構(gòu)是類(lèi)似如此的
          compileText(node){
              //后續(xù)代碼
          }
          接下來(lái),讓我們思考一下,我們編譯文本節(jié)點(diǎn),無(wú)非就是把文本節(jié)點(diǎn)中的{{ count }}映射成為0,而文本節(jié)點(diǎn)不就是node.textContent屬性嗎?所以此時(shí)我們可以想到根據(jù)正則來(lái)匹配{{}}中的count值,然后對(duì)應(yīng)替換成數(shù)據(jù)中的count值,然后我們?cè)僬{(diào)用一次Watcher類(lèi),如果更新了,就再次更改這個(gè)node.textContent的值。如下:
          compileText(node){
              //定義正則,匹配{{}}中的count
              let reg = /\{\{(.+?)\}\}/g;
              let value = node.textContent;
              //判斷是否含有{{}}
              if(reg.test(value)){
                  //拿到{{}}中的count,由于我們是匹配一個(gè)捕獲組,所以我們可以根據(jù)RegExp類(lèi)的$1屬性來(lái)獲取這個(gè)count
                  let key = RegExp.$1.trim();
                  node.textContent = value.replace(reg,this.vm[key]);
                  //如果更新了值,還要做更改
                  new Watcher(this.vm,key,newValue => {
                      node.textContent = newValue;
                  })
              }
          }
          編譯文本節(jié)點(diǎn)到此為止了,接下來(lái)我們來(lái)看編譯元素節(jié)點(diǎn)的方法。

          編譯元素節(jié)點(diǎn)

          指令

          首先,讓我們想一下,我們編譯元素節(jié)點(diǎn)無(wú)非是想要根據(jù)元素節(jié)點(diǎn)上的指令來(lái)分別執(zhí)行不同的操作,所以我們編譯元素節(jié)點(diǎn)就只需要判斷是否含有相關(guān)指令即可,這里我們只考慮了v-text,v-model,v-on:click這三個(gè)指令。讓我們來(lái)看看compileElement方法吧。
          compileElement(node){
              //指令不就是一堆屬性嗎,所以我們只需要獲取屬性即可
              const attrs = node.attributes;
              if(attrs.length){
                  Array.from(attrs).forEach(attr => {
                      //這里由于我們拿到的attributes可能包含不是指令的屬性,所以我們需要先做一次判斷
                      if(this.isDirective(attr)){
                          //根據(jù)v-來(lái)截取一下后綴屬性名,例如v-on:click,subStr(5)即可截取到click,v-text與v-model則subStr(2)截取到text和model即可
                          let attrName = attr.indexOf(':') > -1 ? attr.subStr(5) : attr.subStr(2);
                          let key = attr.value;
                          //單獨(dú)定義一個(gè)update方法來(lái)區(qū)分這些
                          this.update(node,attrName,key,this.vm[key]);
                      }
                  })
              }
          }
          這里又涉及到了一個(gè)isDirective輔助方法,我們可以使用startsWith方法,判斷是否含有v-值即可認(rèn)定這個(gè)屬性就是一個(gè)指令。如下:
          isDirective(dir){
              return dir.startsWith('v-');
          }
          接下來(lái),我們來(lái)看最后的update方法。如下:
          update(node,attrName,key,value){
              //后續(xù)代碼
          }
          最后,讓我們來(lái)思考一下,我們update里面需要做什么。很顯然,我們是不是需要判斷是哪種指令來(lái)執(zhí)行不同的操作?如下:
          //update函數(shù)內(nèi)部
          if(attrName === 'text'){
              //執(zhí)行v-text的操作
          }else if(attrName === 'model'){
              //執(zhí)行v-model的操作
          }else if(attrName === 'click'){
              //執(zhí)行v-on:click的操作
          }

          v-text指令

          好的,我們知道,根據(jù)前面的編譯文本元素節(jié)點(diǎn)的方法,我們就知道這個(gè)指令的用法同前面編譯文本元素節(jié)點(diǎn)。所以這個(gè)判斷里面就好寫(xiě)了,如下:
          //attrName === 'text'內(nèi)部
          node.textContent = value;
          new Watcher(this.vm,key,newValue => {
              node.textContent = newValue;
          })

          v-model指令

          v-model指令實(shí)現(xiàn)的是雙向綁定,我們都知道雙向綁定是更改輸入框的value值,并且通過(guò)監(jiān)聽(tīng)input事件來(lái)實(shí)現(xiàn)。所以這個(gè)判斷,我們也很好寫(xiě)了,如下:
          //attrName === 'model'內(nèi)部
          node.value = value;
          new Watcher(this.vm,key,newValue => {
              node.value = newValue;
          });
          node.addEventListener('input',(e) => {
              this.vm[key] = node.value;
          })

          v-on:click指令

          v-on:click指令就是將事件綁定到methods內(nèi)定義的函數(shù),為了確保this指向當(dāng)前組件實(shí)例,我們需要通過(guò)bind方法改變一下this指向。如下:
          //attrName === 'click'內(nèi)部
          node.addEventListener(attrName,this.methods[key].bind(this.vm));
          到此為止,我們一個(gè)mini版本的vue2.x就算是實(shí)現(xiàn)了。繼續(xù)下一節(jié),學(xué)習(xí)vue3.x版本的mini實(shí)現(xiàn)吧。

          mini版本的vue.js3.x框架

          模板代碼

          首先我們看一下我們要實(shí)現(xiàn)的模板代碼:
          <div id="app"></div>

          邏輯代碼

          然后就是我們要編寫(xiě)的javascript代碼。
          const App = {
              $data:null,
              setup(){
                  let count = ref(0);
                  let time = reactive({ second:0 });
                  let com = computed(() => `${ count.value + time.second }`);
                  setInterval(() => {
                      time.second++;
                  },1000);
                  setInterval(() => {
                      count.value++;
                  },2000);
                  return {
                      count,
                      time,
                      com
                  }
              },
              render(){
                  return `
                      <h1>How reactive?</h1>
                      <p>this is reactive work:${ this.$data.time.second }</p>
                      <p>this is ref work:${ this.$data.count.value }</p>
                      <p>this is computed work:${ this.$data.com.value  }</p>
                  `
              }
          }
          mount(App,document.querySelector("#app"));

          運(yùn)行效果

          我們來(lái)看一下實(shí)際運(yùn)行效果如下所示:

          思考一下,我們要實(shí)現(xiàn)如上的功能應(yīng)該怎么做呢?

          源碼實(shí)現(xiàn)-3.x

          與vue2.x做比較

          事實(shí)上,vue3.x的實(shí)現(xiàn)思想與vue2.x差不多,只不過(guò)vue3.x的實(shí)現(xiàn)方式有些不同,在vue3.x,把收集依賴(lài)的方法稱(chēng)作是副作用effect。vue3.x更像是函數(shù)式編程了,每一個(gè)功能都是一個(gè)函數(shù),比如定義響應(yīng)式對(duì)象,那就是reactive方法,再比如computed,同樣的也是computed方法...廢話(huà)不多說(shuō),讓我們來(lái)看一下吧!

          reactive方法

          首先,我們來(lái)看一下vue3.x的響應(yīng)式方法,在這里,我們?nèi)匀恢豢紤]處理對(duì)象。如下:
          function reactive(data){
              if(!isObject(data))return;
              //后續(xù)代碼
          }
          接下來(lái)我們需要使用到es6的proxyAPI,我們需要熟悉這個(gè)API的用法,如果不熟悉,請(qǐng)點(diǎn)擊此處查看。
          我們還是在getter中收集依賴(lài),setter中觸發(fā)依賴(lài),收集依賴(lài)與觸發(fā)依賴(lài),我們都分別定義為2個(gè)方法,即track和trigger方法。如下:
          function reactive(data){
              if(!isObject(data))return;
              return new Proxy(data,{
                  get(target,key,receiver){
                      //反射api
                      const ret = Reflect.get(target,key,receiver);
                      //收集依賴(lài)
                      track(target,key);
                      return isObject(ret) ? reactive(ret) : ret;
                  },
                  set(target,key,val,receiver){
                      Reflect.set(target,key,val,receiver);
                      //觸發(fā)依賴(lài)方法
                      trigger(target,key);
                      return true;
                  },
                  deleteProperty(target,key,receiver){
                      const ret = Reflect.deleteProperty(target,key,receiver);
                      trigger(target,key);
                      return ret;
                  }
              })
          }

          track方法

          track方法就是用來(lái)收集依賴(lài)的。我們用es6的weakMap數(shù)據(jù)結(jié)構(gòu)來(lái)存儲(chǔ)依賴(lài),然后為了簡(jiǎn)便化用一個(gè)全局變量來(lái)表示依賴(lài)。如下:
          //全局變量表示依賴(lài)
          let activeEffect;
          //存儲(chǔ)依賴(lài)的數(shù)據(jù)結(jié)構(gòu)
          let targetMap = new WeakMap();
          //每一個(gè)依賴(lài)又是一個(gè)map結(jié)構(gòu),每一個(gè)map存儲(chǔ)一個(gè)副作用函數(shù)即effect函數(shù)
          function track(target,key){
              //拿到依賴(lài)
              let depsMap = targetMap.get(target);
              // 如果依賴(lài)不存在則初始化
              if(!depsMap)targetMap.set(target,(depsMap = new Map()));
              //拿到具體的依賴(lài),是一個(gè)set結(jié)構(gòu)
              let dep = depsMap.get(key);
              if(!dep)depsMap.set(key,(dep = new Set()));
              //如果沒(méi)有依賴(lài),則存儲(chǔ)再set數(shù)據(jù)結(jié)構(gòu)中
              if(!dep.has(activeEffect))dep.add(activeEffect)
          }
          收集依賴(lài)就這么簡(jiǎn)單,需要注意的是,這里涉及到了es6的三種數(shù)據(jù)結(jié)構(gòu)即WeakMap,Map,Set。下一步我們就來(lái)看如何觸發(fā)依賴(lài)。

          trigger方法

          trigger方法很明顯就是拿出所有依賴(lài),每一個(gè)依賴(lài)就是一個(gè)副作用函數(shù),所以直接調(diào)用即可。
          function trigger(target,key){
              const depsMap = targetMap.get(target);
              //存儲(chǔ)依賴(lài)的數(shù)據(jù)結(jié)構(gòu)都拿不到,則代表沒(méi)有依賴(lài),直接返回
              if(!depsMap)return;
              depsMap.get(key).forEach(effect => effect && effect());
          }
          接下來(lái),我們來(lái)實(shí)現(xiàn)一下這個(gè)副作用函數(shù),也即effect。

          effect方法

          副作用函數(shù)的作用也很簡(jiǎn)單,就是執(zhí)行每一個(gè)回調(diào)函數(shù)。所以該方法有2個(gè)參數(shù),第一個(gè)是回調(diào)函數(shù),第二個(gè)則是一個(gè)配置對(duì)象。如下:
          function effect(handler,options = {}){
              const __effect = function(...args){
                  activeEffect = __effect;
                  return handler(...args);
              }
              //配置對(duì)象有一個(gè)lazy屬性,用于computed計(jì)算屬性的實(shí)現(xiàn),因?yàn)橛?jì)算屬性是懶加載的,也就是延遲執(zhí)行
              //也就是說(shuō)如果不是一個(gè)計(jì)算屬性的回調(diào)函數(shù),則立即執(zhí)行副作用函數(shù)
              if(!options.lazy){
                  __effect();
              }
              return __effect;
          }
          副作用函數(shù)就是如此簡(jiǎn)單的實(shí)現(xiàn)了,接下來(lái)我們來(lái)看一下computed的實(shí)現(xiàn)。

          computed的實(shí)現(xiàn)

          既然談到了計(jì)算屬性,所以我們就定義了一個(gè)computed函數(shù)。我們來(lái)看一下:
          function computed(handler){
              // 只考慮函數(shù)的情況
              // 延遲計(jì)算 const c = computed(() => `${ count.value}!`)
              let _computed;
              //可以看到computed就是一個(gè)添加了lazy為true的配置對(duì)象的副作用函數(shù)
              const run = effect(handler,{ lazy:true });
              _computed = {
                  //get 訪問(wèn)器
                  get value(){
                      return run();
                  }
              }
              return _computed;
          }
          到此為止,vue3.x的響應(yīng)式算是基本實(shí)現(xiàn)了,接下來(lái)要實(shí)現(xiàn)vue3.x的mount以及compile。還有一點(diǎn),我們以上只是處理了引用類(lèi)型的響應(yīng)式,但實(shí)際上vue3.x還提供了一個(gè)ref方法用來(lái)處理基本類(lèi)型的響應(yīng)式。因此,我們?nèi)匀豢梢詫?shí)現(xiàn)基本類(lèi)型的響應(yīng)式。

          ref方法

          那么,我們應(yīng)該如何來(lái)實(shí)現(xiàn)基本類(lèi)型的響應(yīng)式呢?試想一下,為什么vue3.x中定義基本類(lèi)型,如果修改值,需要修改xxx.value來(lái)完成。如下:
          const count = ref(0);
          //修改
          count.value = 1;
          從以上代碼,我們不難得出基本類(lèi)型的封裝原理,實(shí)際上就是將基本類(lèi)型包裝成一個(gè)對(duì)象。因此,我們很快可以寫(xiě)出如下代碼:
          function ref(target){
              let value = target;
              const obj = {
                  get value(){
                      //收集依賴(lài)
                      track(obj,'value');
                      return value;
                  },
                  set value(newValue){
                      if(value === newValue)return;
                      value = newValue;
                      //觸發(fā)依賴(lài)
                      trigger(obj,'value');
                  }
              }
              return obj;
          }
          這就是基本類(lèi)型的響應(yīng)式實(shí)現(xiàn)原理,接下來(lái)我們來(lái)看一下mount方法的實(shí)現(xiàn)。

          mount方法

          mount方法實(shí)現(xiàn)掛載,而我們的副作用函數(shù)就是在這里執(zhí)行。它有2個(gè)參數(shù),第一個(gè)參數(shù)即一個(gè)vue組件,第二個(gè)參數(shù)則是掛載的DOM根元素。所以,我們可以很快寫(xiě)出以下代碼:
          function mount(instance,el){
              effect(function(){
                  instance.$data && update(instance,el);
              });
              //setup返回的數(shù)據(jù)就是實(shí)例上的數(shù)據(jù)
              instance.$data = instance.setup();
              //這里的update實(shí)際上就是編譯函數(shù)
              update(instance,el);
          }
          這樣就是實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的掛載,接下來(lái)我們來(lái)看一下編譯函數(shù)的實(shí)現(xiàn)。

          update編譯函數(shù)

          這里為了簡(jiǎn)便化,我們實(shí)現(xiàn)的編譯函數(shù)就比較簡(jiǎn)單,直接就將定義在組件上的render函數(shù)給賦值給根元素的innerHTML。如下:
          //這是最簡(jiǎn)單的編譯函數(shù)
          function update(instance,el){
              el.innerHTML = instance.render();
          }
          如此一來(lái),一個(gè)簡(jiǎn)單的mini-vue3.x就這樣實(shí)現(xiàn)了,怎么樣,不到100行代碼就搞定了,還是比較簡(jiǎn)單的。


          點(diǎn)擊左下角閱讀原文,到 SegmentFault 思否社區(qū) 和文章作者展開(kāi)更多互動(dòng)和交流,掃描下方”二維碼“或在“公眾號(hào)后臺(tái)回復(fù)“ 入群 ”即可加入我們的技術(shù)交流群,收獲更多的技術(shù)文章~

          - END -


          瀏覽 74
          點(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>
                  a毛片网站 | 内射极品尤物 | 啪啪啪AV网 | 尻屄视频在线免费看 | 毛片高清无码 |