剖析vue內部運行機制

掘金小冊-剖析Vue.js內部運行機制
Vue.js技術揭祕
Vue源碼分析
Vue面試題
尤雨溪講解vue

Vue的實質

Vue實際上是一個方法類,在原型上擴展了很多方法
源碼目錄:src/core/instance/index.js
在這裏插入圖片描述

Vue生成Dom的流程

源碼目錄:src/core/instance/init.js
在這裏插入圖片描述在這裏插入圖片描述初始化數據,將屬性變爲響應式數據
源碼目錄:src/core/instance/state.js
在這裏插入圖片描述
new Vue()創建一個類的實例,調用Vue方法類中的_init初始化方法(生命週期、事件、數據、render、數據等),最後,檢測到如果有 el 屬性,調用 vm.$mount 方法掛載 vm,將模板渲染成最終的dom。

掛載的過程

$mount
--------》
compileToFunctions(將template內容轉化爲render方法)
--------》
mountComponent
vm._render 調用 createElement 方法返回vnode(虛擬dom)
實例化渲染Watcher,在回調函數中調用 updateComponent
通過 vm._update (調用vm.__patch__方法,通過patch函數利用emptyNodeAt 方法把 oldVnode 轉換成 VNode 對象,然後調用 createElm方法(通過虛擬節點創建真實的 DOM 並插入到它的父節點中),創建子元素,添加到虛擬隊列中,最後調用insert 方法把 DOM 插入到父節點中)更新 DOM
--------》
生成DOM
圖解:
在這裏插入圖片描述

組件patch的整體過程
createComponent–>子組件初始化–>子組件render–>子組件patch
activeInstance爲當前激活的vm實例;vm.$vnode爲組件的佔位vnode;vm._vnode爲組件的渲染vnode,也就是根vnode
嵌套組件的插入順序爲先子後父

組件轉化爲vnode(通過createComponent實現)
有3個關鍵邏輯:
構造子類構造函數
安裝組裝鉤子函數(installComponentHooks)
實例化vnode

在這裏插入圖片描述

響應式原理

核心
Object.defineProperty(obj,prop,descriptor)
直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回這個對象。
obj是要在其上定義屬性的對象;prop是要定義或修改屬性的名稱;descriptor是將被定義或修改屬性的描述符。
其中最重要的是descriptor,它有很多可選鍵值,get 是一個給屬性提供的 getter 方法,當我們訪問了該屬性的時候會觸發 getter 方法;set 是一個給屬性提供的 setter 方法,當我們對該屬性做修改的時候會觸發 setter 方法。一旦對象擁有了 getter 和 setter,我們可以簡單地把這個對象稱爲響應式對象
具體過程

  1. initState初始化數據(props,methods,data,computed,watcher)
  2. proxy把props和data上的屬性代理到vm實例上
  3. observe給非VNode對象類型添加Observe類,通過defineReactive方法給對象的屬性添加getter和setter,用於依賴收集和派發更新
    在這裏插入圖片描述在這裏插入圖片描述

依賴收集
源碼目錄:src/core/observer/dep.js

/* @flow */

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []//存儲與當前dep有關的watcher
  }
  //添加一個watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  //移除
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)// 將當前的 dep 與 當前渲染 watcher 關聯起來
    }
  }

  /** 
   * 每一個屬性 都會包含一個 dep 實例
   * 這個 dep 實例會記錄下 參與計算或渲染的 watcher
   */
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    //依次觸發 this.subs 中的 watcher 的 update 方法,起到更新的作用
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
//當前watcher
Dep.target = null
//定義一個容器,用於存放watcher
const targetStack = []
//將當前操作的 watcher 存儲到 全局容器中, 參數 target 就是當前 watcher
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
//移除watcher
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
/**
 * 在 watcher 調用 get 方法的時候, 調用 pushTarget( this )
 * 在 watcher 的 get 方法結束的時候, 調用 popTarget()
 */

Dep類的作用實際上就是建立數據與watcher的橋樑,Dep.target靜態屬性,確保這個watcher爲全局唯一。
依賴收集就是訂閱數據變化的watcher的收集;
依賴收集的目的是爲了當這些響應式數據發生變化,觸發它們的setter的時候,能知道應該通知哪些訂閱者去做相應的處理。

派發更新
源碼目錄:src/core/observer/dep.js
dep.notify(),通知所有的訂閱者,整個派發更新的過程
當數據發生變化的時候,觸發setter邏輯,把在依賴過程中訂閱的所有觀察者,也就是watcher,都觸發它們的update過程。這個過程又利用了隊列做了進一步優化,在nextTick後執行所有watcher的run,最後執行它們的回調函數。
整個派發更新過程:
在這裏插入圖片描述
在這裏插入圖片描述
nextTick
把要執行的任務推入到一個隊列中,在下一個tick執行
數據改變後觸發watcher的update,但是watchers的flush是在nextTick後,所以重新渲染是異步的

響應式數據中對於對象新增刪除屬性以及數組的下標訪問修改和添加數據等的變化觀測不到,因爲這些操作不能觸發setter
通過Vue.set以及數組的API可以解決這些問題,本質上它們內部手動去做了依賴更新的派發
手動調用ob.dep.notify();重新渲染
源碼目錄:src/core/observer/scheduler.js
在這裏插入圖片描述

計算屬性和偵聽屬性
計算屬性的本質是computed watcher
偵聽屬性的本質是user watcher
計算屬性適合用在模板渲染中,某個值是依賴了其他的響應式對象甚至是計算屬性計算而來
偵聽屬性適用於監測某個值的變化去完成一段複雜的業務邏輯
源碼目錄:src/core/observer/watcher.js

/* @flow */

import {
  warn,
  remove,
  isObject,
  parsePath,
  _Set as Set,
  handleError,
  noop
} from '../util/index'

import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import type { SimpleSet } from '../util/index'

let uid = 0

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string; // 關聯表達式 或 渲染方法體
  cb: Function; // 在定義 Vue 構造函數的時候, 傳入的 watch 
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean; // 計算屬性, 和 watch 來控制不要讓 Watcher 立即執行
  sync: boolean;
  dirty: boolean;
  active: boolean;
                        // 在 Vue 中使用了 二次提交的概念
                        // 每次在數據 渲染 或 計算的時候 就會訪問響應式的數據, 就會進行依賴收集
                        // 就將關聯的 Watcher 與 dep 相關聯,
                        // 在數據發生變化的時候, 根據 dep 找到關聯的 watcher, 依次調用 update
                        // 執行完成後會清空 watcher
  deps: Array<Dep>;
  depIds: SimpleSet;
  
  newDeps: Array<Dep>;
  newDepIds: SimpleSet;
  
  before: ?Function; // Watcher 觸發之前的, 類似於 生命週期


  getter: Function; // 就是 渲染函數 ( 模板或組件的渲染 ) 或 計算函數 ( watch )
  
  value: any; // 如果是渲染函數, value 無效; 如果是計算屬性, 就會有一個值, 值就存儲在 value 中

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)

    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()

    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''

    
    // parse expression for getter
    if (typeof expOrFn === 'function') { // 就是 render 函數
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }


    // 如果是 lazy 就什麼也不做, 否則就立即調用 getter 函數求值 ( expOrFn ),初始化渲染
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps() // "清空" 關聯的 dep 數據
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      
      this.newDepIds.add(id)
      this.newDeps.push(dep) // 讓 watcher 關聯到 dep

      if (!this.depIds.has(id)) {
        dep.addSub(this) // 讓 dep 關聯到 watcher
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) { // 在 二次提交中 歸檔就是 讓 舊的 deps 和 新 的 newDeps 一致
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps // 同步
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {            // 主要針對計算屬性, 一般用於求值計算
      this.dirty = true
    } else if (this.sync) {     // 同步, 主要用於 SSR, 同步就表示立即計算 
      this.run()
    } else {
      queueWatcher(this)        // 一般瀏覽器中的異步運行, 本質上就是異步執行 run
                                // 類比: setTimeout( () => this.run(), 0 )
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   * 
   * 調用 get 求值或渲染, 如果求值, 新舊值不同, 觸發 cb
   */
  run () {
    if (this.active) {
      const value = this.get() // 要麼渲染, 要麼求值
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

組件更新
組件更新過程核心是新舊vnode diff
新舊節點不同:創建新節點 --> 更新父佔位符節點 --> 刪除舊節點
新舊節點相同:獲取它們的children,根據不同情況做不同的更新邏輯。如果它們都存在子節點,會執行updateChildren邏輯

響應式原理圖
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章