手摸手教你實(shí)現(xiàn)一個(gè)簡單vue(1)響應(yīng)式原理

寫在前面

1202年了独郎,再實(shí)現(xiàn)一款mvvm框架有咩用肮?其實(shí)...確實(shí)....沒咩用虹曙。你寫的再好也難卷的過已經(jīng)相當(dāng)成熟的vue,react等等框架迫横,但是對個(gè)人而言番舆,自己實(shí)現(xiàn)一個(gè)簡單版vue一定會加深你對源碼的原理的理解,這么看來還是有點(diǎn)意義的矾踱。

最終版效果:

實(shí)現(xiàn)功能:

  1. 聲明式渲染 {{message}}
  2. 條件渲染 v-if
  3. 列表渲染 v-for
  4. 事件處理 v-on
  5. 組件渲染 <component></component>
    框架特點(diǎn):
  6. 使用typescript+webpack構(gòu)建
  7. 框架直接操作真實(shí)dom而沒有用到vdom
  8. 只實(shí)現(xiàn)了vue的部分功能恨狈,因?yàn)槭聼o巨細(xì)全部實(shí)現(xiàn)的話有點(diǎn)搬磚。
  9. 不像vue2.x的缺陷,moush-vue內(nèi)數(shù)組可通過下標(biāo)索引完成視圖更新

關(guān)于沒有vdom:
這也是vue1.x和2.x的區(qū)別介返,但沒有vdom也完全不影響你理解vue原理拴事,因?yàn)槔斫饬?.x的原理沃斤,2.x無非就是在原來的基礎(chǔ)上增加了vdom和相關(guān)的diff算法而已圣蝎。

演示:

template:

  <div id="app">
    <div class="ageContent">
      <p>
        {{name}}的年齡是{{age}}
      </p>
      <ul>
        <li v-for="item in arr">
          {{item}}
        </li>
      </ul>
      <button v-on:click="addFunc">addFunc</button>
      <coma v-if="isShow"></coma>
    </div>

typescript:

const app = new moushVue({
    el: "#app",
    data: function () {
      return {
        age: 1,
        name: "小明",
        isShow: true,
        arr:[1,2,3,4,5,6,7,8,9,10,11],
      };
    },
    methods:{
       addFunc:function(){
         this.arr[0]++
       },
       switchIsShow:function(){
         this.isShow=!this.isShow
       }
    },
    components: {
      coma: {
        template: `<h1 class="com" v-bind:test="appName">局部組件{{appName}}自身屬性:{{appAttr}}</h1>`,
        data: function () {
          return {
            appName: "moush",
            appAttr:"attr",
          };
        },
      },
    },
  });

[圖片上傳失敗...(image-6ac2ac-1638493300126)]

點(diǎn)擊addFunc按鈕 列表中·1會++,瀏覽器視圖也會相應(yīng)更新

項(xiàng)目的地址:
https://github.com/moushicheng/moush-vue
騙個(gè)star不過分吧

主流程

 class moushVue implements VM{
  $options: any;
  $data: any;
  $el: HTMLElement;
  $parentVm: VM;
  $childrenVm: VM[];
  $methods:any
  $oldNode:any;
  constructor(options: OPTIONS) {
    this.$options = options;
    this.init();
    this.mount();
    this.observe();

  }
  protected init() {
    new init(this);
  }
  protected mount() {
    this.$options.beforeMount.call(this);
    this.$el =
      typeof this.$options.el == "string"
        ? document.querySelector(this.$options.el)
        : this.$options.el;
    this.$options.mounted.call(this);
  }
  protected observe() {
    new Observer('$data',this.$data,this); //使data內(nèi)部數(shù)據(jù)可觀測
    new Complier(this); //分析el內(nèi)部節(jié)點(diǎn)并生成相應(yīng)watcher
  }
}

我們先不用在意函數(shù)細(xì)節(jié)做了什么衡瓶,籠統(tǒng)認(rèn)知一下徘公,且看constructor內(nèi)部分別干了三件事:
this.init();初始化一些數(shù)據(jù)

this.mount(); 將用戶傳進(jìn)來的el掛載
this.observe(); 將數(shù)據(jù)變的可觀測并執(zhí)行編譯
前兩者比較簡單,我們的核心就是搞懂observe中

  new Observer('$data',this.$data,this); //使data內(nèi)部數(shù)據(jù)可觀測
  new Complier(this); //分析el內(nèi)部節(jié)點(diǎn)并生成相應(yīng)watcher

到底干了什么

響應(yīng)式原理

要監(jiān)控一個(gè)對象(這里把Array和Object都稱為對象)內(nèi)部的數(shù)據(jù)變化哮针,我們就不得不用到一些手段,vue2.x用到了Object.defineProperty這個(gè)api來實(shí)現(xiàn)監(jiān)控?cái)?shù)據(jù)关面,但我們的框架并不是這樣,因?yàn)檫@樣做無法監(jiān)控

 arr[0]=1;

數(shù)組下標(biāo)索引帶來的變化十厢。
在我們的框架中等太,用到了es6的Proxy代理對象來監(jiān)控對象變化
下面你可以嘗試一下把這段代碼復(fù)制到瀏覽器控制臺,然后進(jìn)行一些簡單的調(diào)試體驗(yàn)一下Proxy

    const obj={a:1}
    const proxy = new Proxy(obj, {
      get(obj, property) {
        console.log('@get:'+property)
        return obj[property];
      },
      set(obj, property, value) {
      console.log('@set:'+property+value)
        obj[property] = value;
        return true;
      },
    });
   //調(diào)試
   proxy.a=2 //@set:a2
   proxy.a  //@get:a

那么要對vue中data選項(xiàng)進(jìn)行數(shù)據(jù)監(jiān)控怎么做呢蛮放?那當(dāng)然如出一轍用到Proxy缩抡,在數(shù)據(jù)更新的時(shí)候,在Proxy get中順便通知依賴更新就行了
那么什么又是依賴呢包颁?這個(gè)不太好解釋瞻想,籠統(tǒng)的講就是跟數(shù)據(jù)直接相關(guān)的HTML模板
如:

<div>
{{message}}
</div>

new moushVue({
...
data:{
  message:"Hello,world"
}
...
})

清晰一點(diǎn)講就是我們內(nèi)部構(gòu)建的Watcher對象,通知依賴更新娩嚼,就是通知Watcher執(zhí)行它的update方法蘑险,update方法會直接操作dom更新視圖,對不同的模板會有不同的操作(不同的回調(diào)cb)岳悟,這和我們主流程中Complier編譯有關(guān)佃迄,它會根據(jù)HTML模板創(chuàng)建不同類型的cb來為數(shù)據(jù)更新進(jìn)行服務(wù):

class Watcher {
    vm:VM
    cb:Function;
    getter:any;
    value:any;
    
    constructor (vm,initVal,expOrFn,cb) {
      this.vm = vm;
      this.cb = cb;
      if(isType(expOrFn,'String'))this.getter = parsePath(expOrFn)
      else if(isType(expOrFn,'Function'))this.getter=expOrFn
      this.value = this.get() //收集依賴
      this.value=initVal
    }
    get () {
      window.target = this;
      let value = this.getter(this.vm.$data)
      window.target = undefined;
      return value
    }
    update () {
      const oldValue = this.value
      // this.value = this.get() //更新時(shí)不要觸發(fā)getter否則會收集依賴
      this.value = this.getter(this.vm.$data)
      this.cb.call(this.vm, this.value, oldValue)
    }
  }

關(guān)于get,依賴收集贵少,我們很快就會提到呵俏。
好,那么讓我們回到數(shù)據(jù)監(jiān)控

Observer

observer就是一個(gè)偵測器春瞬,它會深度遞歸將選項(xiàng)data內(nèi)部的所有數(shù)據(jù)都進(jìn)行監(jiān)控柴信。

class Observer{
  $value: any;
  $parent: any;
  $key:string
  dep: any;
  constructor(key,value, parent) {
    this.$key=key;
    this.$value = value;

    this.$parent = parent;

    this.dep = new Dep();

    def(value, "__ob__", this); //相當(dāng)于this.__ob__=value
    this.walk(value);
    this.detect(value, parent);
  }
  private walk(obj: Object | Array<any>) {
    for (const [key, val] of Object.entries(obj)) {
      if (typeof val == "object") {
        //同時(shí)判斷數(shù)組和對象
        new ObserverNext(key,val, obj);
      }
    }
  }
  private detect(val: any, parent: any) {
    const dep = this.dep
    const key=this.$key
    const proxy = new Proxy(val, {
      get(obj, property) {
        if (!obj.hasOwnProperty(property)) {
          return;
        }
        dep.depend(property);
        return obj[property];
      },
      set(obj, property, value) {
        obj[property] = value;

        dep.notify(property);
        if(parent.__ob__)parent.__ob__.dep.notify(key)

        return true;
      },
    });

    parent[this.findKey(parent, val)] = proxy;
  }
  //通過對象和對象內(nèi)的某個(gè)值發(fā)現(xiàn)指向這個(gè)值的鍵(key)
  //比如obj.a=1,findKey(obj,1) =>返回a
  private findKey(obj, value, compare = (a, b) => a === b) {
    return Object.keys(obj).find((k) => compare(obj[k], value));
  }
}

每個(gè)對象的數(shù)據(jù)監(jiān)控都需要有一個(gè)dep,dep是什么?因?yàn)橐粋€(gè)數(shù)據(jù)可能會對應(yīng)多個(gè)依賴宽气,所以必須要把數(shù)據(jù)對應(yīng)的所有依賴都做一個(gè)統(tǒng)一管理随常,這個(gè)統(tǒng)一管理就由dep來實(shí)現(xiàn)潜沦。
observer的主流程,初始化绪氛,創(chuàng)建dep唆鸡,然后walk(遞歸分析對象內(nèi)部是否有嵌套對象,有的話就將嵌套對象也進(jìn)行監(jiān)控枣察。

walk(obj: Object | Array<any>) {
    for (const [key, val] of Object.entries(obj)) {
      if (typeof val == "object") {
        //同時(shí)判斷數(shù)組和對象
        new ObserverNext(key,val, obj);
      }
    }
  }

然后是偵測detect

 private detect(val: any, parent: any) {
    const dep = this.dep
    const key=this.$key
    const proxy = new Proxy(val, {
      get(obj, property) {
        if (!obj.hasOwnProperty(property)) {
          return;
        }
        dep.depend(property);
        return obj[property];
      },
      set(obj, property, value) {
        obj[property] = value;

        dep.notify(property);
        if(parent.__ob__)parent.__ob__.dep.notify(key)

        return true;
      },
    });
    parent[this.findKey(parent, val)] = proxy;
  }

先把val(傳進(jìn)來的對象)進(jìn)行代理(proxy)争占,然后在get中收集依賴dep.depend
在set中通知依賴更新dep.notify,更新的時(shí)候同時(shí)會做一層穿透通知父對象也進(jìn)行更新序目。
最后

parent[this.findKey(parent, val)] = proxy;

在將父對象中引用我們新偵測對象改為代理器臂痕。這樣便能將整個(gè)對象都變成響應(yīng)式

總結(jié)一下,Observer的作用就是深度遞歸分析對象內(nèi)部的所有數(shù)據(jù),并進(jìn)行偵測猿涨,在內(nèi)部數(shù)據(jù)更新時(shí)通知代理器使dep更新握童,在獲取內(nèi)部數(shù)據(jù)的時(shí)候就會通知代理器使dep進(jìn)行依賴收集。

dep

接下來讓我們分析一下dep叛赚,因?yàn)樵创a很簡單澡绩,所以直接上源碼

 class depNext {
  subs: Map<string, Array<Watcher>>;
  constructor() {
    this.subs = new Map();
  }

  addSub(prop, target) {
    const sub = this.subs.get(prop);

    if (!sub) {
      this.subs.set(prop, [target]);
      return;
    }
    sub.push(target);
  }
  // 添加一個(gè)依賴
  depend(prop) {
    if (window.target) {
      this.addSub(prop, window.target); //不要奇怪window.target后續(xù)會講
    }
  }
  // 通知所有依賴更新
  notify(prop) {

    const watchers = this.subs.get(prop);
    if(!watchers)return;
    for (let i = 0, l = watchers.length; i < l; i++) {
      watchers[i].update();
    }
  }
}

注意到subs是一個(gè)Map對象,它會映射對象內(nèi)所有的數(shù)據(jù)俺附,每個(gè)映射的數(shù)據(jù)都對應(yīng)一個(gè)依賴數(shù)組肥卡。這就是上文說的一個(gè)數(shù)據(jù)可能對應(yīng)多個(gè)依賴

依賴收集

依賴收集簡單的來說就是,在獲取數(shù)據(jù)的時(shí)候收集依賴
再次貼一下Observer中的源碼

const proxy = new Proxy(val, {
      get(obj, property) {
        if (!obj.hasOwnProperty(property)) {
          return;
        }
        dep.depend(property);
        return obj[property];
      },
      set(...){...}
    });

數(shù)據(jù)獲取的時(shí)候事镣,看到了嗎步鉴,dep.depend收集了依賴,然后在依賴收集器dep中蛮浑,將對應(yīng)的依賴添加到依賴數(shù)組中
dep中是這么收集的
this.addSub(prop, window.target);

window.target實(shí)際上就是watcher的實(shí)例唠叛,在創(chuàng)建watcher的時(shí)候,watcher會把自己賦予到全局window.target中沮稚,然后去獲取一下數(shù)據(jù)艺沼,數(shù)據(jù)代理器(Proxy)就會dep.depend,收集這個(gè)watcher了蕴掏。

watcher中依賴收集
    get () {
      window.target = this;
      let value = this.getter(this.vm.$data)
      window.target = undefined;
      return value
    }

this.getter在構(gòu)造器中被parsePath所創(chuàng)建,parsePath會把一個(gè)形如'data.a.b.c'的字符串路徑所表示的值障般,從真實(shí)的data對象中取出來,這樣就完成了依賴收集

  /**
   * Parse simple path.
   * 把一個(gè)形如'data.a.b.c'的字符串路徑所表示的值盛杰,從真實(shí)的data對象中取出來
   * 例如:
   * data = {a:{b:{c:2}}}
   * parsePath('a.b.c')(data)  // 2
   */
export function parsePath(path) {
  const bailRE = /[^\w.$]/;
  const segments = path.split(".");
  return function (obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return;
      if (bailRE.test(segments[i])) {
        //this.arr[0]  this[arr[0]]
        const match = segments[i].match(/(\w+)\[(.+)\]/);
        obj = obj[match[1]];
        obj = obj[match[2]];
        continue;
      }
      obj = obj[segments[i]];
    }
    return obj;
  };
}

總結(jié)

Observer深度遞歸分析選項(xiàng)data內(nèi)部的數(shù)據(jù)挽荡,使其具有響應(yīng)性
observer實(shí)例內(nèi)部的dep負(fù)責(zé)統(tǒng)一管理依賴即供,
在獲取數(shù)據(jù)時(shí)定拟,dep會收集依賴,在數(shù)據(jù)更新時(shí)逗嫡,dep會通知依賴更新
依賴就是watcher,它負(fù)責(zé)具體的更新視圖青自,通過調(diào)用其上的cb(回調(diào)函數(shù))株依。
watcher更新視圖所調(diào)用的cb都在Complier編譯構(gòu)建時(shí)決定,這一點(diǎn)會在后續(xù)的文章中講解延窜。

想具體了解上述過程可以點(diǎn)擊恋腕,直接看項(xiàng)目源碼
https://github.com/moushicheng/moush-vue

歸檔

# 手摸手教你實(shí)現(xiàn)一個(gè)簡單vue(1)響應(yīng)式原理
# 手摸手教你實(shí)現(xiàn)一個(gè)簡單vue(2)上手編寫observer

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市逆瑞,隨后出現(xiàn)的幾起案子荠藤,更是在濱河造成了極大的恐慌,老刑警劉巖获高,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哈肖,死亡現(xiàn)場離奇詭異,居然都是意外死亡谋减,警方通過查閱死者的電腦和手機(jī)牡彻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門扫沼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來出爹,“玉大人,你說我怎么就攤上這事缎除⊙暇停” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵器罐,是天一觀的道長梢为。 經(jīng)常有香客問我,道長轰坊,這世上最難降的妖魔是什么铸董? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮肴沫,結(jié)果婚禮上粟害,老公的妹妹穿的比我還像新娘。我一直安慰自己颤芬,他們只是感情好悲幅,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著站蝠,像睡著了一般汰具。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上菱魔,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天留荔,我揣著相機(jī)與錄音,去河邊找鬼澜倦。 笑死聚蝶,一個(gè)胖子當(dāng)著我的面吹牛拔疚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播既荚,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼稚失,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了恰聘?” 一聲冷哼從身側(cè)響起句各,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎晴叨,沒想到半個(gè)月后凿宾,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡兼蕊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年初厚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片孙技。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡产禾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出牵啦,到底是詐尸還是另有隱情亚情,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布哈雏,位于F島的核電站楞件,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏裳瘪。R本人自食惡果不足惜土浸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望彭羹。 院中可真熱鬧黄伊,春花似錦、人聲如沸皆怕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽愈腾。三九已至憋活,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間虱黄,已是汗流浹背悦即。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人辜梳。 一個(gè)月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓粱甫,卻偏偏與公主長得像,于是被迫代替她去往敵國和親作瞄。 傳聞我的和親對象是個(gè)殘疾皇子茶宵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容