手寫簡單Vue

理解Vue的設(shè)計(jì)思想

image.png

MVVM框架的三要素:數(shù)據(jù)響應(yīng)式俐东、模板引擎及其渲染

數(shù)據(jù)響應(yīng)式:監(jiān)聽數(shù)據(jù)變化并在視圖中更新
Object.defineProperty()
Proxy

模版引擎:提供描述視圖的模版語法

插值:{{}}
指令:v-bind锌雀,v-on县爬,v-model寂纪,v-for,v-if
渲染:如何將模板轉(zhuǎn)換為html
模板 => vdom => dom

數(shù)據(jù)響應(yīng)式原理

數(shù)據(jù)變更能夠響應(yīng)在視圖中兄朋,就是數(shù)據(jù)響應(yīng)式徐块。vue2中利用 Object.defineProperty() 實(shí)現(xiàn)變更檢測。

簡單實(shí)現(xiàn)

 
const obj = {}
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log(`get ${key}:${val}`);
      return val
    },
    set(newVal) {
      if (newVal !== val) {
        console.log(`set ${key}:${newVal}`);
        val = newVal
} }
}) }
defineReactive(obj, 'foo', 'foo')
obj.foo
obj.foo = 'foooooooooooo'

結(jié)合視圖

 
<!DOCTYPE html>

 
<html lang="en">
<head></head>
<body>
  <div id="app"></div>
  <script>
const obj = {}
    function defineReactive(obj, key, val) {
      Object.defineProperty(obj, key, {
        get() {
          console.log(`get ${key}:${val}`);
          return val
        },
        set(newVal) {
          if (newVal !== val) {
            val = newVal
            update()
} }
}) }
    defineReactive(obj, 'foo', '')
    obj.foo = new Date().toLocaleTimeString()
    function update() {
      app.innerText = obj.foo
}
    setInterval(() => {
      obj.foo = new Date().toLocaleTimeString()
    }, 1000);
  </script>
</body>
</html>

遍歷需要響應(yīng)化的對象

 
// 對象響應(yīng)化:遍歷每個(gè)key仔涩,定義getter忍坷、setter function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}
const obj = {foo:'foo',bar:'bar',baz:{a:1}}
observe(obj)
obj.foo
obj.foo = 'foooooooooooo' obj.bar
obj.bar = 'barrrrrrrrrrr' obj.baz.a = 10 // 嵌套對象no ok

解決嵌套對象問題

 
function defineReactive(obj, key, val) {
    observe(val)
    Object.defineProperty(obj, key, {
    //...

解決賦的值是對象的情況

 obj.baz = {a:1}
obj.baz.a = 10 // no ok
 
set(newVal) {
    if (newVal !== val) {
observe(newVal) // 新值是對象的情況 notifyUpdate()

如果添加/刪除了新屬性無法檢測

obj.dong = 'dong' obj.dong // 并沒有g(shù)et信息
 
function set(obj, key, val) {
  defineReactive(obj, key, val)
}

Vue中的數(shù)據(jù)響應(yīng)化

目標(biāo)代碼

vue.html


<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
  <p>{{counter}}</p>
</div>
<script src="node_modules/vue/dist/vue.js"></script>
<script>
  const app = new Vue({
    el:'#app',
    data: {
counter: 1 },
  })
  setInterval(() => {
    app.counter++
  }, 1000);
</script>
</body>
</html>

原理分析

  1. newVue()首先執(zhí)行初始化,對data執(zhí)行響應(yīng)化處理熔脂,這個(gè)過程發(fā)生在Observer中
  1. 同時(shí)對模板執(zhí)行編譯佩研,找到其中動態(tài)綁定的數(shù)據(jù),從data中獲取并初始化視圖霞揉,這個(gè)過程發(fā)生在 Compile中
  2. 同時(shí)定義一個(gè)更新函數(shù)和Watcher旬薯,將來對應(yīng)數(shù)據(jù)變化時(shí)Watcher會調(diào)用更新函數(shù)
  3. 由于data的某個(gè)key在一個(gè)視圖中可能出現(xiàn)多次,所以每個(gè)key都需要一個(gè)管家Dep來管理多個(gè)
    Watcher
  4. 將來data中數(shù)據(jù)一旦發(fā)生變化适秩,會首先找到對應(yīng)的Dep绊序,通知所有Watcher執(zhí)行更新函數(shù)


    image.png

    涉及類型介紹
    KVue:框架構(gòu)造函數(shù)
    Observer:執(zhí)行數(shù)據(jù)響應(yīng)化(分辨數(shù)據(jù)是對象還是數(shù)組)
    Compile:編譯模板,初始化視圖秽荞,收集依賴(更新函數(shù)骤公、watcher創(chuàng)建)
    Watcher:執(zhí)行更新函數(shù)(更新dom)
    Dep:管理多個(gè)Watcher,批量更新

KVue

框架構(gòu)造函數(shù):執(zhí)行初始化 執(zhí)行初始化扬跋,對data執(zhí)行響應(yīng)化處理阶捆,vue.js

 
function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
return
}
  new Observer(obj)
}

function defineReactive(obj, key, val) {}
class KVue {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    observe(this.$data)
  }
}
class Observer {
  constructor(value) {
    this.value = value
    this.walk(value);
  }
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
} }

為$$data做代理,代理$method

class KVue {
constructor(options) {
// 。钦听。洒试。
  proxy(this, '$data')
}
}
function proxy(vm) {
Object.keys(vm.$methods).forEach(key => {

  Object.defineProperty(vm, key, {
    get() {
      return  vm.$methods[key]
    },
  })

Object.keys(vm.$data).forEach(key => {
  Object.defineProperty(vm, key, {
    get() {
      return vm.$data[key];
    },
    set(newVal) {
      vm.$data[key] = newVal;
} });
}) }

編譯 - Compile

編譯模板中vue模板特殊語法,初始化視圖朴上、更新視圖


image.png

初始化視圖

根據(jù)節(jié)點(diǎn)類型編譯垒棋,compile.js

  class Compile {
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);
    if (this.$el) {
      this.compile(this.$el);
} }
  compile(el) {
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach(node => {
if (this.isElement(node)) { console.log("編譯元素" + node.nodeName);
} else if (this.isInterpolation(node)) { console.log("編譯插值文本" + node.textContent);
      }
      if (node.childNodes && node.childNodes.length > 0) {
          this.compile(node);
      }
}); }
  isElement(node) {
    return node.nodeType == 1;
 
}
  isInterpolation(node) {
    return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
} }

編譯插值,compile.js

 
compile(el) {
    // ...
} else if (this.isInerpolation(node)) {
// console.log("編譯插值文本" + node.textContent); this.compileText(node);
} });
}
compileText(node) {
    console.log(RegExp.$1);
    node.textContent = this.$vm[RegExp.$1];
}

編譯元素

compile(el) {
    //...
if (this.isElement(node)) {
// console.log("編譯元素" + node.nodeName); this.compileElement(node)
} }
compileElement(node) {
    let nodeAttrs = node.attributes;
    Array.from(nodeAttrs).forEach(attr => {
        let attrName = attr.name;
        let exp = attr.value;
        if (this.isDirective(attrName)) {
            let dir = attrName.substring(2);
            this[dir] && this[dir](node, exp);
        }
}); }

 
isDirective(attr) {
    return attr.indexOf("k-") == 0;
}
text(node, exp) {
    node.textContent = this.$vm[exp];
}

k-html

html(node, exp) {
  node.innerHTML = this.$vm[exp]
}

依賴收集

視圖中會用到data中某key痪宰,這稱為依賴叼架。同一個(gè)key可能出現(xiàn)多次畔裕,每次都需要收集出來用一個(gè)
Watcher來維護(hù)它們,此過程稱為依賴收集碉碉。 多個(gè)Watcher需要一個(gè)Dep來管理柴钻,需要更新時(shí)由Dep統(tǒng)一通知。 看下面案例垢粮,理出思路:


new Vue({
   template:
       `<div>
           <p>{{name1}}</p>
           <p>{{name2}}</p>
           <p>{{name1}}</p>
        <div>`,
   data: {
       name1: 'name1',
       name2: 'name2'
   }
});
image.png

實(shí)現(xiàn)思路

  1. defineReactive時(shí)為每一個(gè)key創(chuàng)建一個(gè)Dep實(shí)例
  2. 初始化視圖時(shí)讀取某個(gè)key贴届,例如name1,創(chuàng)建一個(gè)watcher1
  3. 由于觸發(fā)name1的getter方法蜡吧,便將watcher1添加到name1對應(yīng)的Dep中
  4. 當(dāng)name1更新毫蚓,setter觸發(fā)時(shí),便可通過對應(yīng)Dep通知其管理所有Watcher更新
image.png

創(chuàng)建Watcher昔善,kvue.js

const watchers = [];//臨時(shí)用于保存watcher測試用
// 監(jiān)聽器:負(fù)責(zé)更新視圖 class Watcher {
constructor(vm, key, updateFn) { // kvue實(shí)例
this.vm = vm;
// 依賴key


this.key = key;
// 更新函數(shù)
this.updateFn = updateFn;
// 臨時(shí)放入watchers數(shù)組
   watchers.push(this)
 }
// 更新 update() {
   this.updateFn.call(this.vm, this.vm[this.key]);
 }
}

編寫更新函數(shù)元潘、創(chuàng)建watcher

// 調(diào)用update函數(shù)執(zhí)插值文本賦值 compileText(node) {
   // console.log(RegExp.$1);
   // node.textContent = this.$vm[RegExp.$1];
   this.update(node, RegExp.$1, 'text')
}
text(node, exp) {
   this.update(node, exp, 'text')
}
html(node, exp) {
   this.update(node, exp, 'html')
}
update(node, exp, dir) {
   const fn = this[dir+'Updater']
   fn && fn(node, this.$vm[exp])
   new Watcher(this.$vm, exp, function(val){
       fn && fn(node, val)
   })
}
textUpdater(node, val) {
   node.textContent = val;
}
htmlUpdater(node, val) {
   node.innerHTML = val
}

聲明Dep

class Dep {
   constructor () {
       this.deps = []
   }
   addDep (dep) {
       this.deps.push(dep)
}
   notify() {
       this.deps.forEach(dep => dep.update());
} }

創(chuàng)建watcher時(shí)觸發(fā)getter

class Watcher {
 constructor(vm, key, updateFn) {
   Dep.target = this;
   this.vm[this.key];
   Dep.target = null;
} }

依賴收集,創(chuàng)建Dep實(shí)例

defineReactive(obj, key, val) {
 this.observe(val);
 const dep = new Dep()
 Object.defineProperty(obj, key, {
   get() {
     Dep.target && dep.addDep(Dep.target);
return val },
   set(newVal) {
     if (newVal === val) return
     dep.notify()
} })
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末君仆,一起剝皮案震驚了整個(gè)濱河市翩概,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌返咱,老刑警劉巖钥庇,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異咖摹,居然都是意外死亡评姨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門萤晴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吐句,“玉大人,你說我怎么就攤上這事店读∴率啵” “怎么了?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵屯断,是天一觀的道長文虏。 經(jīng)常有香客問我,道長裹纳,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任紧武,我火速辦了婚禮剃氧,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘阻星。我一直安慰自己朋鞍,他們只是感情好已添,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著滥酥,像睡著了一般更舞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上坎吻,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天缆蝉,我揣著相機(jī)與錄音,去河邊找鬼瘦真。 笑死刊头,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的诸尽。 我是一名探鬼主播原杂,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼您机!你這毒婦竟也來了穿肄?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤际看,失蹤者是張志新(化名)和其女友劉穎咸产,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體仿村,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锐朴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蔼囊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片焚志。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖畏鼓,靈堂內(nèi)的尸體忽然破棺而出酱酬,到底是詐尸還是另有隱情,我是刑警寧澤云矫,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布膳沽,位于F島的核電站,受9級特大地震影響让禀,放射性物質(zhì)發(fā)生泄漏挑社。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一巡揍、第九天 我趴在偏房一處隱蔽的房頂上張望痛阻。 院中可真熱鬧,春花似錦腮敌、人聲如沸阱当。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弊添。三九已至录淡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間油坝,已是汗流浹背嫉戚。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留免钻,地道東北人彼水。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像极舔,于是被迫代替她去往敵國和親凤覆。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354