Vue3.0[Beta]要點(diǎn)與源碼解讀

這是我第21篇簡(jiǎn)書。

主播你懂vue嗎?講的啥啊丐箩,一句也聽不懂...

咳咳撬槽,進(jìn)入正題此改。


源碼知識(shí)圖譜

Vue3新增了

  • Performance:性能更強(qiáng),比Vue 2.0快了接近2倍侄柔。
  • Tree shaking support:可以將無用模塊“剪輯”共啃,僅打包需要的,按需編譯代碼暂题。
  • Composition API:組合式API移剪,類似hooks,composition API 可以實(shí)現(xiàn)更靈活且無副作用的復(fù)用代碼敢靡,mixin將不再作為推薦使用挂滓。
  • Fragment, Teleport, Suspense:“碎片”,Teleport即Protal傳送門,“懸念”
  • Better TypeScript support:更優(yōu)秀的Ts支持
  • Custom Renderer API:暴露了自定義渲染API

這里不得不提到vue3重寫了響應(yīng)式原理

在 Vue 2中赶站, Vue 通過 Object.defineProperty 轉(zhuǎn)化對(duì)象屬性getters/setters 的方法來實(shí)現(xiàn)響應(yīng)式幔虏,對(duì)于數(shù)組來說額外對(duì)常用的數(shù)組方法進(jìn)行來攔截才能截獲到數(shù)組元素的變動(dòng),但這確實(shí)也造成了一些問題贝椿,比如無法感知直接通過索引來更新數(shù)組的場(chǎng)景想括。
reactive:
在 Vue 3 中,用 ES6 的 Proxy重寫了響應(yīng)式的實(shí)現(xiàn)烙博,并將其功能 API 直接暴露給開發(fā)者瑟蜈,換言之,開發(fā)者甚至可以將 Vue 的響應(yīng)式作為一個(gè)獨(dú)立的庫來使用渣窜。

Vue3核心

一铺根、Composition API

官方文檔:https://composition-api.vuejs.org/zh/
隨著功能的增長(zhǎng),復(fù)雜組件的代碼變得越來越難以維護(hù)乔宿。 尤其發(fā)生你去新接手別人的代碼時(shí)位迂。 根本原因是 Vue 2 通過option API組織代碼,但是在大部分情況下详瑞,通過邏輯考慮來組織代碼更有意義掂林。
在Vue2下相關(guān)業(yè)務(wù)的代碼需要遵循option的配置寫到特定的區(qū)域,導(dǎo)致后續(xù)維護(hù)非常的復(fù)雜坝橡,同時(shí)代碼可復(fù)用性不高泻帮,而Vue3的Composition API就是為了解決這個(gè)問題而生的。而且可與現(xiàn)有的 Options API一起使用计寇。

1锣杂、組合式的6個(gè)主要API:
  • reactive(Composition API的核心)
    接收一個(gè)普通對(duì)象然后返回該普通對(duì)象的響應(yīng)式代理。等同于 Vue2 的 Vue.observable()饲常。
    響應(yīng)式轉(zhuǎn)換是“深層的”:會(huì)影響對(duì)象內(nèi)部所有嵌套的屬性蹲堂。基于 ES6的 Proxy 實(shí)現(xiàn)贝淤,返回的代理對(duì)象不等于原始對(duì)象柒竞。建議僅使用代理對(duì)象而避免依賴原始對(duì)象。
  • ref
    接受一個(gè)參數(shù)值并返回一個(gè)響應(yīng)式且可改變的 ref 對(duì)象播聪。ref 對(duì)象擁有一個(gè)指向內(nèi)部值的單一屬性 .value朽基。如果傳入 ref 的是一個(gè)對(duì)象,將調(diào)用 reactive 方法進(jìn)行深層響應(yīng)轉(zhuǎn)換
  • computed
    傳入一個(gè) getter 函數(shù)离陶,返回一個(gè)默認(rèn)不可手動(dòng)修改的 ref 對(duì)象稼虎。
    或者傳入一個(gè)擁有 get 和 set 函數(shù)的對(duì)象,創(chuàng)建一個(gè)可手動(dòng)修改的計(jì)算狀態(tài)
  • readonly
    傳入一個(gè)對(duì)象(響應(yīng)式或普通)或 ref招刨,返回一個(gè)原始對(duì)象的只讀代理霎俩。一個(gè)只讀的代理是“深層的”,對(duì)象內(nèi)部任何嵌套的屬性也都是只讀的。
const original = reactive({ count: 0 })

const copy = readonly(original)

watchEffect(() => {
  // 依賴追蹤
  console.log(copy.count)
})

// original 上的修改會(huì)觸發(fā) copy 上的偵聽
original.count++

// 無法修改 copy 并會(huì)被警告
copy.count++ // warning!
  • wathEffect
    立即執(zhí)行傳入的一個(gè)函數(shù)打却,并響應(yīng)式追蹤其依賴杉适,并在其依賴變更時(shí)重新運(yùn)行該函數(shù)。
    當(dāng) watchEffect 在組件的 setup() 函數(shù)或生命周期鉤子被調(diào)用時(shí)柳击, 偵聽器會(huì)被鏈接到該組件的生命周期猿推,并在組件卸載時(shí)自動(dòng)停止。
const count = ref(0)

watchEffect(() => console.log(count.value))
// -> 打印出 0

setTimeout(() => {
  count.value++
  // -> 打印出 1
}, 100)
  • watch
    和vue2的watch一樣
2捌肴、生命周期鉤子函數(shù)
Vue3的生命周期鉤子函數(shù)變化
(1)setup函數(shù)

setup 函數(shù)是一個(gè)新的組件選項(xiàng)蹬叭。作為在組件內(nèi)使用 Composition API 的入口點(diǎn)。

  • 調(diào)用時(shí)機(jī):
    創(chuàng)建組件實(shí)例状知,然后初始化 props 秽五,緊接著就調(diào)用setup 函數(shù)。從生命周期鉤子的視角來看饥悴,它會(huì)在 beforeCreate 鉤子之前被調(diào)用筝蚕。
  • 參數(shù)
const MyComponent = {
  setup(props, context) {
    context.attrs
    context.slots
    context.emit
  },
}

props 作為其第一個(gè)參數(shù) (注:props 對(duì)象是響應(yīng)式的,watchEffect 或 watch 會(huì)觀察和響應(yīng) props 的更新铺坞。不要解構(gòu) props 對(duì)象,那樣會(huì)使其失去響應(yīng)性)洲胖。

第二個(gè)參數(shù)提供了一個(gè)上下文對(duì)象context济榨,attrsslots 都是內(nèi)部組件實(shí)例上對(duì)應(yīng)項(xiàng)的代理,可以確保在更新后仍然是最新值绿映,所以可以解構(gòu)擒滑,無需擔(dān)心后面訪問到過期的值。

  • 特別注意this的用法
    this 在 setup() 中不可用叉弦!
    由于 setup() 在解析 vue2 選項(xiàng)前被調(diào)用丐一,setup() 中的 this 將與 vue2選項(xiàng)中的 this 完全不同。同時(shí)在 setup() 和 vue2 選項(xiàng)中使用 this 時(shí)將造成混亂淹冰。
setup() {
  function onClick() {
    this // 這里 `this` 與你期望的不一樣库车!
  }
}

二、簡(jiǎn)易代碼展示Vue3核心:

<script src="../dist/vue.global.js"></script>
<script>
  Vue2的options api 很難做tree-shaking
  // export default {
  //   data() {
  //     return {}
  //   }樱拴,
  //   methods: {

  //   },
  //   computed: {
        
  //   }
  // }
 

  // createApp代替new Vue()
  const {createApp, reactive, watchEffect, computed } = Vue
  // 按需引入柠衍,tree-shaking生效
  // 就算你引入了computed ,如果沒有用到它晶乔,打包時(shí)就會(huì)把這段代碼刪掉珍坊。
  const App = {
    // template => render function(返回vdom) 
    // compile-dom和compile-core做的
    template: `
      <button @click="onclick">
          {{state.count}} -- {{state.double}}
      </button>
    `,
    setup() {
      // 響應(yīng)式,用Proxy取代object.defineProperty
      const state = reactive({
        count: 0,
        double: computed(()=> state.count*2)
      })
      watchEffect(()=> {
        console.log('數(shù)據(jù)變了哦:', state.count)
      })
      function onclick() {
        state.count += 1
      }
      return {
        state,
        onclick
      }
    }

  }
  createApp(App).mount('#app')
</script>
手寫代碼展示vue3整個(gè)源碼流程

三正罢、reactive源碼學(xué)習(xí)與響應(yīng)式實(shí)現(xiàn)

劃重點(diǎn)U舐!!

// 作為緩存
// WeakMap:
// (1)Map對(duì)象的鍵可以是任何類型履怯,但WeakMap對(duì)象中的鍵只能是對(duì)象引用
// (2)WeakMap不能包含無引用的對(duì)象回还,否則會(huì)被自動(dòng)清除出集合(垃圾回收機(jī)制)。
// (3)WeakSet對(duì)象是不可枚舉的虑乖,無法獲取大小懦趋。
// 原始對(duì)象=> 響應(yīng)式對(duì)象
let toProxy = new WeakMap()
// 響應(yīng)式對(duì)象=> 原始對(duì)象
let toRaw = new WeakMap()

let effectStack = [] //存儲(chǔ)effect的地方
let targetMap = new WeakMap() // 特殊的對(duì)象 key是object
// obj.name
// {
//   target: deps :{ key:[ dep1,dep2] }
// }
// 以上 存儲(chǔ)依賴關(guān)系

// 目的:收集依賴
function track(target, key) {
  // 最后一個(gè) 就是最新的
  const effect = effectStack[effectStack.length - 1]
  // 最新的effect
  if (effect) {
    let depMap = targetMap.get(target)
    if (depMap === undefined) {
      depMap = new Map()
      targetMap.set(target, depMap)
    }
    let dep = depMap.get(key) // obj.count  target是obj,key是count
    if (dep == undefined) {
      dep = new Set()
      depMap.set(key, dep)
    }
    // 雙向存儲(chǔ)無處不在疹味,優(yōu)化的原則
    if (!dep.has(effect)) {
      dep.add(effect)
      effect.deps.push(dep)
    }
  }
}
// 目的:觸發(fā)更新
function trigger(target, key, info) {
  // 尋找依賴effect
  const depMap = targetMap.get(target)
  if (depMap === undefined) {
    // 沒有依賴
    return
  }
  const effects = new Set()
  const computedRunners = new Set()

  if (key) {
    let deps = depMap.get(key)
    // deps里面全部是effect
    deps.forEach((effect) => {
      // effect()
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
  effects.forEach((effect) => effect())
  computedRunners.forEach((computed) => computed())
}

function effect(fn, options = {}) {
  // 其實(shí)就是往effectStackpush了一個(gè)effect函數(shù)仅叫,執(zhí)行fn
  // @todo 處理options
  let e = createReactiveEffect(fn, options)

  if (!options.lazy) {
    e()
  }

  return e
}

function createReactiveEffect(fn, options) {
  // 構(gòu)造effect
  const effect = function effect(...args) {
    return run(effect, fn, args)
  }
  effect.deps = []
  effect.computed = options.computed
  effect.lazy = options.lazy
  return effect
}

function run(effect, fn, args) {
  if (effectStack.indexOf(effect) === -1) {
    try {
      effectStack.push(effect)
      return fn(...args) // 執(zhí)行 執(zhí)行的時(shí)候,是要獲取的
    } finally {
      effectStack.pop() // effect用完就要推出去
    }
  }
}

function computed(fn) {
  // computed就是一個(gè)特殊的effect
  const runner = effect(fn, {
    computed: true,
    lazy: true
  })
  return {
    effect: runner,
    get value() {
      return runner()
    }
  }
}
// 舉例:
// let obj = {name:'kkb'}   背后有一個(gè)proxy監(jiān)聽 響應(yīng)式
// obj.name  觸發(fā)get函數(shù)
// 響應(yīng)式代理(重點(diǎn))
const baseHandler = {
  get(target, key) {
    // target就是obj糙捺,key就是name
    // 收集依賴 track
    // @todo
    // 大部分情況可以直接return target[key]
    const res = Reflect.get(target, key) // es6新api Reflect诫咱,和proxy搭配使用
    // 查找并返回target對(duì)象的property屬性
    track(target, key)
    // 遞歸,如果有嵌套對(duì)象接著reactive
    return typeof res == 'object' ? reactive(res) : res
  },
  set(target, key, val) {
    const info = {
      oldValue: target[key],
      newValue: val
    }
    // obj.name = xx 這里 我們是需要通知更新的
    const res = Reflect.set(target, key, val)
    // 觸發(fā)更新
    // @todo
    trigger(target, key, info)
    return res
  }
}
// 響應(yīng)式
function reactive(target) {
  // 查詢緩存
  let observed = toProxy.get(target)
  if (observed) {
    return observed
  }
  if (toRaw.get(target)) {
    return target
  }
  // 響應(yīng)式核心:榈啤?茬浴!G┕场L秃簟!
  observed = new Proxy(target, baseHandler)
  // 監(jiān)聽完后铅檩,設(shè)置緩存
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  // 這兩步實(shí)現(xiàn)了雙向搜索地圖
  return observed
}

reactive流程圖:

四憎夷、compiler編譯原理與Vdom

嫌麻煩可在線對(duì)比編譯結(jié)果:
Vue2.6:https://template-explorer.vuejs.org/

Vue3:https://vue-next-template-explorer.netlify.app/

template => Vdom過程:

template 解析過程

  1. 解析成抽象語法樹 AST
  2. 根據(jù)AST,用transform模板轉(zhuǎn)化
    @click.prevent.capture
  3. codeGen生成代碼字符串string
  4. 使用new Function() (es6新建函數(shù)) 把string 轉(zhuǎn)換成可執(zhí)行的函數(shù)
  5. 這個(gè)函數(shù)執(zhí)行后返回的才是Vdom

這5部是Vdom的邏輯昧旨,無論vue還是react都是這個(gè)邏輯

流程圖



compiler編譯涉及非常多的正則拾给,這里不做詳細(xì)展示。兔沃。根灯。略過~

vue為啥需要vdom逻谦?

compile模塊 vue處理成vdom
js描述dom拼卵,這個(gè)就是vdom
有了compiler 跨端才成了可能:

json:
{
  type:'div',
  props:{id:app},
  children:[ name, 
  {type:div } ]
}
有了compiler 跨端才成了可能史汗。
<div><input></div> 這些標(biāo)簽,只有瀏覽器耗時(shí)

這個(gè)對(duì)象缰雇,或者json入偷,跨平臺(tái)的 使用不同平臺(tái)的render
結(jié)構(gòu)化的對(duì)象 很好解析 別的平臺(tái), 只需要記錄好映射關(guān)系就可以械哟。

(Vdom)虛擬dom優(yōu)點(diǎn):

虛擬dom輕量快速疏之,最小dom操作,提升性能和用戶體驗(yàn)
跨平臺(tái):將虛擬dom和嗎好想轉(zhuǎn)換為不同運(yùn)行時(shí)特殊操作實(shí)現(xiàn)跨平臺(tái)
兼容性:還可以加入兼容性代碼增強(qiáng)操作的兼容性

  • 緩存的意義:innerHTML 內(nèi)置vdom
  • 樹形結(jié)構(gòu)
  • 編譯時(shí)優(yōu)化暇咆,足夠多的標(biāo)記
return function render(_ctx, _cache) {
  with (_ctx) {
    const {
      toDisplayString: _toDisplayString,
      createVNode: _createVNode,
      openBlock: _openBlock,
      createBlock: _createBlock
    } = _Vue

    return (
      _openBlock(),
      _createBlock('div', null, [
        _createVNode(
          'p',
          {
            id: xx
          },
          _toDisplayString(name),
          9 /* TEXT, PROPS */,
          ['id']
        ),
        _createVNode('h2', null, '大家聽我扯淡'), // 靜態(tài) 永遠(yuǎn)不會(huì)變 不用做diff 不用考慮更新
        _createVNode('h2', null, '大家聽我扯淡123')
      ])
    )
  }
}
<script>
  // 創(chuàng)建虛擬DOM
  function createElement(type, props, children) {
    return {
      type,
      props,
      children
    }
  }

  function render(dom) {
    let el = document.createElement(dom.type)
    for (let key in dom.props) {
      el.setAttribute(key, dom.props[key])
    }
    dom.children.forEach(child => {
      child = (typeof child == 'object') ? render(child) : document.createTextNode(child)
      el.appendChild(child)
    })
    return el
  }

  // let vdom = <ul> </ul>
  let vdom = createElement('ul', {
    class: 'list'
  }, [
    createElement('li', {
      class: 'item'
    }, ['item1']),
    createElement('li', {
      class: 'item'
    }, ['item2']),
    createElement('li', {
      class: 'item'
    }, ['item3'])
  ])
  var el = render(vdom)
  document.body.appendChild(el)
</script>

五锋爪、runtime

1丙曙、runtime-core

與平臺(tái)無關(guān)的運(yùn)行時(shí),專門用于自定義的render其骄。其實(shí)現(xiàn)的功能有虛擬 DOM 渲染器亏镰、Vue 組件和 Vue 的各種API,我們可以利用這個(gè) runtime 實(shí)現(xiàn)針對(duì)某個(gè)具體平臺(tái)的高階 runtime拯爽,比如自定義渲染器索抓。


2、 runtime-dom

針對(duì)瀏覽器的 runtime毯炮。其功能包括處理原生 DOM API逼肯、DOM 事件和 DOM 屬性等。主要功能是適配了瀏覽器環(huán)境下節(jié)點(diǎn)和節(jié)點(diǎn)屬性的增刪改查桃煎。它暴露了兩個(gè)重要 API:rendercreateApp篮幢,并聲明了一個(gè) ComponentPublicInstance接口。

3为迈、功能概述

  • 速度顯著提升
  • 同時(shí)支持 Composition API 和 Options API三椿,以及 typings
  • 基于 Proxy 實(shí)現(xiàn)的數(shù)據(jù)變更檢測(cè)
  • 支持 Fragments
    碎片化,不再限于模板中的單個(gè)根節(jié)點(diǎn)
    render 函數(shù)也可以返回?cái)?shù)組了葫辐,類似實(shí)現(xiàn)了 React.Fragments 的功能 搜锰;Just works
  • 支持 Portals
  • 支持 Suspense w/ async setup()
  • 服務(wù)器端渲染
  • <keep-alive>

接下來看尤大大怎么更,期待正式版耿战,持續(xù)關(guān)注~~


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末纽乱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子昆箕,更是在濱河造成了極大的恐慌,老刑警劉巖租冠,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鹏倘,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡顽爹,警方通過查閱死者的電腦和手機(jī)纤泵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來镜粤,“玉大人捏题,你說我怎么就攤上這事∪饪剩” “怎么了公荧?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)同规。 經(jīng)常有香客問我循狰,道長(zhǎng)窟社,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任绪钥,我火速辦了婚禮灿里,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘程腹。我一直安慰自己匣吊,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布寸潦。 她就那樣靜靜地躺著色鸳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪甸祭。 梳的紋絲不亂的頭發(fā)上缕碎,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音池户,去河邊找鬼咏雌。 笑死,一個(gè)胖子當(dāng)著我的面吹牛校焦,可吹牛的內(nèi)容都是我干的赊抖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼寨典,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼氛雪!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起耸成,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤报亩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后井氢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弦追,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年花竞,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了劲件。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡约急,死狀恐怖零远,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情厌蔽,我是刑警寧澤牵辣,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站奴饮,受9級(jí)特大地震影響服猪,放射性物質(zhì)發(fā)生泄漏供填。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一罢猪、第九天 我趴在偏房一處隱蔽的房頂上張望近她。 院中可真熱鬧,春花似錦膳帕、人聲如沸粘捎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽攒磨。三九已至,卻和暖如春汤徽,著一層夾襖步出監(jiān)牢的瞬間娩缰,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國打工谒府, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拼坎,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓完疫,卻偏偏與公主長(zhǎng)得像泰鸡,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子壳鹤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359