vue響應(yīng)式和依賴收集

看了vue源碼后實(shí)現(xiàn)的一個(gè)很簡(jiǎn)單很簡(jiǎn)單的vue??

目的主要是串一下new Vue()之后到組件掛載的流程,及數(shù)據(jù)更新時(shí)視圖的更新流程。

源碼主要流程如下:

  1. new Vue()
  2. this._init()
    1. initLifecycle(vm)
    2. initEvents(vm)
    3. initRender(vm)
    4. callHook(vm, 'beforeCreate')
    5. initInjections(vm)
    6. initState(vm)
      1. initProps(vm)
      2. initMethods(vm)
        1. vm.XXX = this.methods[XXX]
      3. initData(vm)
        1. observe(value) // 開(kāi)啟響應(yīng)式
      4. initComputed(vm)
      5. initWatch(vm)
        1. vm.$watch(expOrFn, cb, option)
    7. initProvide(vm)
    8. callHook(vm, 'created')
    9. vm.options.el ? vm.mount(vm.options.el) : vm.mount(el)
  3. vm.$mount(el)
    1. mountComponent(vm)
      1. callHook(vm, 'beforeMount')
      2. new Watcher()
      3. callHook(vm, 'mounted')

具體包括對(duì)象和數(shù)組的響應(yīng)式原理、發(fā)布訂閱、觀察者、單例模式、依賴收集、模版編譯

// index.js
import { initState, initMethods } from './init.js'
import initLifecycle from './initLifecycle.js'
import mounted from './mounted.js'
import Compiler from './compiler.js'

class Vue {
  constructor(options) {
    this.vm = this
    this.$options = options
    this.init(this)
  }

  // 初始化操作
  init(vm) {
    initState(vm)
    initMethods(vm)
    initLifecycle(vm)
    mounted(vm)
  }

  $mount(el) {
    Compiler.getInstance(this, el)
  }
}

export default Vue
// init.js
import { def, observe, proxy } from './utils.js'

function initData(vm) {
  let data = vm.$options.data

  // 保存_data 主要是因?yàn)閐ata可能是函數(shù)
  data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {}

  Object.keys(data).forEach(key => {
    proxy(vm, '_data', key)
  })

  observe(data)
}

function initProps() {}
function initComputed() {}
function initWatch() {}

export function initState(vm) {
  initData(vm)
  initProps(vm)
  initComputed(vm)
  initWatch(vm)
}

export function initMethods(vm) {
  const methods = vm.$options.methods

  if (methods) {
    for (const key in methods) {
      if (methods.hasOwnProperty(key)) {
        def(vm, key, methods[key])
      }
    }
  }
}
// initLifecycle.js
export default function initLifecycle(vm) {
  const created = vm.$options.created

  if (created) {
    created.call(vm)
  }
}
// mounted.js
import Compiler from './compiler.js'

// 掛載
export default function mounted(vm) {
  const el = vm.$options.el

  // 是否提供 el 選項(xiàng),如果沒(méi)有則調(diào)用實(shí)例的 $mount() 方法
  if (el) {
    Compiler.getInstance(vm, el)
  }
}
// utils.js
import Observer from './observer.js'

export function def(target, key, value, enumerable = false) {
  Object.defineProperty(target, key, {
    value,
    enumerable,
    writable: true,
    configurable: true
  })
}

// 將屬性代理到實(shí)例上 以便可以通過(guò) this.XXX 方式訪問(wèn)
export function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    get() {
      return target[sourceKey][key]
    },
    set(newValue) {
      target[sourceKey][key] = newValue
    }
  })
}

export function observe(data) {
  if (typeof data !== 'object' || data === null) return

  return new Observer(data)
}
// array.js
const arrayPrototype = Array.prototype
const arrayMethods = Object.create(arrayPrototype)
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

// 重寫(xiě)數(shù)組方法
methodsToPatch.forEach(method => {
  // 緩存原方法
  const originalMethod = arrayPrototype[method]

  arrayMethods[method] = function (...args) {
    const result = originalMethod.apply(this, args)
    const ob = this.__ob__
    let inserted

    switch (method) {
      // 首尾追加元素
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
      default:
        break
    }

    if (inserted) {
      // this 為修改后的數(shù)組
      // 在一開(kāi)始初始化數(shù)組時(shí)已經(jīng)給它添加了屬性 __ob__ 指向 Observer 的實(shí)例
      ob.observeArray(this)
    }

    return result
  }
})

export default arrayMethods
// defineReactive.js
import Dep from './dep.js'
import { observe } from './utils.js'

// 響應(yīng)式
export default function defineReactive(target, key, value) {
  observe(value)

  const dep = new Dep()

  Object.defineProperty(target, key, {
    get() {
      Dep.target && dep.addSub(Dep.target)

      return value
    },
    set(newValue) {
      if (value !== newValue) {
        value = newValue
        observe(newValue)

        // 通知依賴 更新視圖
        dep.notify()
      }
    },
    enumerable: true,
    configurable: true
  })
}
// dep.js

// 訂閱發(fā)布
export default class Dep {
  constructor() {
    this.subs = []
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
  notify() {
    this.subs.forEach(watcher => {
      watcher.update()
    })
  }
}
// watcher.js
import Dep from './dep.js'

// 觀察者
export default class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm
    this.exp = exp
    this.cb = cb

    Dep.target = this

    // 取值
    this.get()

    Dep.target = null // 避免重復(fù)添加Watcher
  }

  get() {
    this.value = this.vm[this.exp]
    this.cb(this.value)
  }

  update() {
    const value = this.vm[this.exp]

    this.cb.call(this.vm, value, this.value)
    this.value = value
  }
}
// observer.js
import arrayMethods from './array.js'
import defineReactive from './defineReactive.js'
import { observe, def } from './utils.js'

export default class Observer {
  constructor(data) {
    this.value = data
    // 便于在其他處操作data時(shí),可以找到該data對(duì)應(yīng)的Observer實(shí)例
    def(data, '__ob__', this) // 必須定義成不可枚舉的,否則會(huì)陷入死循環(huán)。仔細(xì)品一下為啥??

    if (Array.isArray(data)) {
      // 數(shù)組
      data.__proto__ = arrayMethods

      this.observeArray(data)
    } else {
      // 對(duì)象
      this.walk(data)
    }
  }

  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key])
    })
  }

  observeArray(array) {
    for (let i = 0; i < array.length; i++) {
      observe(array[i])
    }
  }
}
// compiler.js

import Watcher from './watcher.js'

// 插值正則
const REG = /\{\{(.*)\}\}/

class Compiler {
  constructor(vm, el) {
      this.$el = document.querySelector(el)
      this.$vm = vm

    if (this.$el) {
      // 將節(jié)點(diǎn)轉(zhuǎn)成文檔片段
      this.nodeToFragment(this.$el)

      // 編譯
      this.compile(this.$fragment)

      // 將編譯后的片段插入html
      this.$el.appendChild(this.$fragment)
    }
  }

  // 采用單例模式 避免同時(shí)存在傳入 el 選項(xiàng) 和 調(diào)用了 $mount(el)
  static getInstance(vm, el) {
    if (!this.instance) {
      this.instance = new Compiler(vm, el)
    }
    return this.instance
  }

  nodeToFragment(el) {
    const fragment = document.createDocumentFragment()
    let child

    while ((child = el.firstChild)) {
      fragment.appendChild(child)
    }
    this.$fragment = fragment
  }

  compile(fragment) {
    const childNodes = fragment.childNodes

    Array.from(childNodes).forEach(node => {
      if (this.isElement(node)) {
        // console.log('編譯元素', node.nodeName)

        const attrs = node.attributes

        Array.from(attrs).forEach(attr => {
          const attrName = attr.name
          const attrValue = attr.value

          if (this.isDirective(attrName)) {
            // 指令
            const dirName = attrName.slice(2) + 'Dir'

            console.log(dirName)

            this[dirName] && this[dirName](node, this.$vm, attrValue)
          }

          // 事件
          const eventName = this.isEvent(attrName)

          if (eventName) {
            this.eventHandler(node, eventName, attrValue)
          }
        })
      } else if (this.isInterpolation(node)) {
        // console.log('編譯插值表達(dá)式', node.nodeValue)
        this.update(node, this.$vm, RegExp.$1, 'text')
      }

      if (node.childNodes?.length) {
        this.compile(node)
      }
    })
  }

  isElement({ nodeType }) {
    return nodeType === 1
  }

  // 含有插值表達(dá)式的文本節(jié)點(diǎn)
  isInterpolation({ nodeType, nodeValue }) {
    return nodeType === 3 && REG.test(nodeValue)
  }

  textUpdate(node, value) {
    node.textContent = value
  }

  // 更新函數(shù)
  update(node, vm, exp, dir) {
    const updateFn = this[`${dir}Update`]

    new Watcher(vm, exp, function (newValue) {
      updateFn && updateFn(node, newValue)
    })
  }

  isDirective(name) {
    return /^v-(.*)/.test(name)
  }

  isEvent(name) {
    return /^v-on:|@(.*)/.test(name) && RegExp.$1
  }

  textDir(node, vm, exp) {
    this.commonWatcher(node, vm, 'textContent', exp)
  }

  modelDir(node, vm, exp) {
    this.commonWatcher(node, vm, 'value', exp)

    node.addEventListener('input', e => {
      vm[exp] = e.target.value
    })
  }

  htmlDir(node, vm, exp) {
    this.commonWatcher(node, vm, 'innerHTML', exp)
  }

  commonWatcher(node, vm, prop, exp) {
    new Watcher(vm, exp, function (value) {
      node[prop] = value
    })
  }

  eventHandler(node, eventName, exp) {
    node.addEventListener(eventName, this.$vm[exp].bind(this.$vm))
  }
}

export default Compiler
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="app">
    <h1>{{title}}</h1>
    <h2>{{age}}</h2>
    <h3>
      <span>{{age}}</span>
    </h3>
    <hr>
    <input type="text" v-model="tel">
    <h3>{{tel}}</h3>
    <hr>
    <div v-text="desc"></div>
    <br>
    <div>這是靜態(tài)文本</div>
    <hr>
    <button @click="add">add</button>
    <h2>{{count}}</h2>
    <div v-html="html"></div>
  </div>
</body>
</html>

<script type="module">
  import Vue from './index.js'

  new Vue({
    el: '#app',
    data() {
      return {
        title: 'hello',
        tel: 1571121,
        desc: '這是v-text文本',
        count: 2,
        age: 12,
        html: '<p>這是html片段</p>',
        info: {
          name: 'mike',
          age: 23
        },
        skill: ['eat', 'song', {
          foo: 'bar'
        }]
      }
    },
    methods: {
      add(e) {
        this.count = this.count + 1
        this.html = '<h2>修改html片段</h2>'
        this.tel = 565744
        this.desc = '3453534'
      }
    },
    created() {
      setTimeout(() => {
        this.age = 34
      }, 2000)
    }
  }).$mount('#app')   // 如果沒(méi)有傳el選項(xiàng),則調(diào)用$mount方法實(shí)現(xiàn)掛載

  // OR
  // const vm = new Vue()
  // vm.$mount('#app')
</script>
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 入口文件 vue-2.6.11\src\platforms\web\entry-runtime-with-comp...
    愛(ài)吃饅頭不吃辣閱讀 861評(píng)論 0 0
  • [TOC] Vue 學(xué)習(xí)筆記 Vue 源碼解析 - 主線流程 Vue 源碼解析 - 模板編譯 Vue 源碼解析 -...
    Whyn閱讀 1,722評(píng)論 0 0
  • 第二次來(lái)梳理Vue源碼邏輯了。第一次因?yàn)椴皇煜?,梳理的很?xì)致才弄懂。第二次就更有大局觀一些了,這次我主要抓住流程的...
    LoveBugs_King閱讀 1,898評(píng)論 1 5
  • 物有本末,事有終始,知所先后,則近道矣 ---《大學(xué)》 在分析Vue初始化之前,我們先看看Vue源碼的目錄結(jié)構(gòu):...
    海洋之木閱讀 8,180評(píng)論 13 46
  • 今天感恩節(jié)哎,感謝一直在我身邊的親朋好友。感恩相遇!感恩不離不棄。 中午開(kāi)了第一次的黨會(huì),身份的轉(zhuǎn)變要...
    余生動(dòng)聽(tīng)閱讀 10,805評(píng)論 0 11

友情鏈接更多精彩內(nèi)容