【Vue原始碼學習】響應式原理探祕

前端南玖發表於2022-01-24

最近準備開啟Vue的原始碼學習,並且每一個Vue的重要知識點都會記錄下來。我們知道Vue的核心理念是資料驅動檢視,所有操作都只需要在資料層做處理,不必關心檢視層的操作。這裡先來學習Vue的響應式原理,Vue2.0的響應式原理是基於Object.defineProperty來實現的。Vue通過對傳入的資料物件屬性的getter/setter方法來監聽資料的變化,通過getter進行依賴收集,setter方法通知觀察者,在資料變更時更新檢視。

1.使用rollup搭建開發環境

安裝rollup環境

npm i @babel/preset-env @babel/core rollup rollup-plugin-babel rollup-plugin-serve cross-env -D

配置rollup

// rollup.config.js
import babel from "rollup-plugin-babel"
import serve from "rollup-plugin-serve"


export default {
    input: './src/index.js',  // 打包入口
    output: {
        file: 'dist/umd/vue.js', //出口路徑
        name: 'Vue' , // 指定打包後全域性變數的名字
        format: 'umd' , // 統一模組規範
        sourcemap: true, // es6->es5 開啟原始碼除錯,可以找到原始碼報錯位置
    },
    plugins:[ //使用的外掛
        babel({
            exclude:'node_modules/**' //排除檔案
        }),
        process.env.ENV==='development'?serve({
            open:true,
            openPage:'/public/index.html', //預設啟動html的路徑
            port:3000,
            contentBase: ''
        }):null
    ]
}

專案搭建

這裡搭建了一個Vue專案,主要程式碼都放在src下面

40B5B58D-2758-4C50-821E-91DA89794F23.png

2.響應式原理探祕

1.Object.defineProperty

想要了解Vue2的響應式原理,我們得先來簡單瞭解一下Object.defineProperty

Object.defineProperty()的作用就是直接在一個物件上定義一個新屬性,或者修改一個已經存在的屬性,預設情況下,使用 Object.defineProperty() 新增的屬性值是不可修改(immutable)的。

Object.defineProperty(obj,prop,desc)
  • obj:需要定義屬性的物件
  • prop:當前需要定義的物件屬性
  • desc:屬性描述符

該方法最低相容到IE8,這也就是Vue最低相容到IE8的原因。

2.Vue初始化過程

我們先來分析一下Vue的初始化都做了哪些事情,我們在使用Vue的時候一般都會這樣寫:

const vm = new Vue({
  el:'#app',
  data(){
    return {
      name: '南玖'
    }
  }
})

我們知道Vue只能通過new關鍵字初始化,所以Vue應該是一個建構函式,然後會呼叫this._init方法進行初始化過程,OK,我們自己可以來實現一下

import {initMixin} from "./init"
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  } // 開發環境下不通過new進行呼叫會告警
  /*呼叫_init初始化,這個方法是掛在Vue原型上的*/
  this._init(options)
  // options就是new Vue是傳入的引數,包括:el,data,computed,watch,methods...
}

initMixin(Vue) // 給Vue原型上新增_init方法
export default Vue

我們接著來寫這個init.js,這裡主要是給Vue原型上掛上方法:_init,$mount,_render,$nextTick

import {initState} from "./state"

//initMixin就做了一件事,就是給Vue原型掛上_init方法
export function initMixin(Vue){
    // 初始化流程
    Vue.prototype._init = function (options){
        // console.log(options)
        const vm = this // vue中使用this.$options
        vm.$options = options

        // /*初始化props、methods、data、computed與watch*/
        initState(vm) 
      // 這裡先看initState,後面還會有很多初始化事件:初始化生命週期、初始化事件、初始化render等等
    }
}

初始化data,這裡我們知道Vue支援傳入的data可以是一個物件也可以是一個方法,所以我們需要判斷一下傳入的data的資料型別,是物件就直接傳給observe,是方法就先執行再將返回值傳給observe

function initData(vm) {
    console.log('初始化資料',vm.$options.data)
    // 資料初始化
    let data = vm.$options.data;
    data = vm._data =  typeof data === 'function' ? data.call(this) : data
    // 物件劫持,使用者改變了資料 ==》 重新整理頁面
    // MVVM模式 資料驅動檢視

    // Object.definePropety() 給屬性增加get和set方法
    observe(data)  //響應式原理
}

3.響應式原理

將資料變成可觀察的,我們都知道Vue2是通過Object.defineProperty來實現的。ok,這裡我們就進入了這次的重點原理講解:我們知道Object.defineProperty這個方法,只能劫持物件不能劫持陣列,所以這裡我們判斷一下資料型別,陣列需要單獨處理,重寫陣列原型上的方法,在陣列變更時在通知到訂閱者

// 把data中資料使用Object.defineProperty重新定義 es5
// Object.defineProperty 不能相容IE8及以下,所以vue2無法相容IE8版本
import {isObject,def} from "../util/index"
import {arrayMethods} from "./array.js"  // 陣列方法
export function observe (data) {
    // console.log(data,'observe')
    let isObj = isObject(data)
    if(!isObj) return 
    return new Observer(data) // 觀測資料
}

 class Observer {
     constructor(v){
        // 如果資料層次過多,需要遞迴去解析物件中的屬性,依次增加set和get方法
        def(v,'__ob__',this)
        if(Array.isArray(v)) {
            // 如果是陣列的話並不會對索引進行監測,因為會導致效能問題
            // 前端開發中很少去操作索引 push shift unshift
            v.__proto__ = arrayMethods
            // 如果陣列裡放的是物件,再進行監測
            this.observerArray(v)
        }else{
          //物件則呼叫walk進行劫持
            this.walk(v)
        }
        
     }
     observerArray(value) {
         for(let i=0; i<value.length;i++) {
             observe(value[i])
         }
     }
   /* 遍歷每一個物件並且為它們繫結getter與setter。該方法只有在資料型別為物件時才能被呼叫  */
     walk(data) {
         let keys = Object.keys(data); //獲取物件key
         keys.forEach(key => {
            defineReactive(data,key,data[key]) // 定義響應式物件
         })
     }
 }

 function  defineReactive(data,key,value){
     observe(value) // 遞迴實現深度監測,注意效能
     Object.defineProperty(data,key,{
         get(){
             // 依賴收集,下期探討
             //獲取值
            return value
         },
         set(newV) {
             //設定值
            if(newV === value) return
            observe(newV) //繼續劫持newV,使用者有可能設定的新值還是一個物件
            value = newV
           /*dep物件通知所有的觀察者,下期探討*/
      			//dep.notify()
            console.log('值變化了',value)
         }
     })
 }

4.陣列方法重寫


// 重寫陣列的7個方法: push,pop,shift,unshift,reverse,sort,splice會導致陣列本身改變

let oldArrayMethods = Array.prototype
// value.__proto__ = arrayMethods 
// arrayMethods.__proto__ = oldArrayMethods
export let arrayMethods = Object.create(oldArrayMethods)

const methods = [
    'push','pop','shift','unshift','reverse','sort','splice'
]

methods.forEach(method=>{
    arrayMethods[method] = function(...args) {
        console.log('使用者呼叫了:'+method,args)
        const res = oldArrayMethods[method].apply(this, args) // 呼叫原生陣列方法
        // 新增的元素可能還是一個物件

        let inserted = args //當前插入的元素
        //陣列新插入的元素需要重新進行observe才能響應式
        let ob = this.__ob__
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break;
            case 'splice':
                inserted = args.slice(2)
                break;
            default:
                break;
        }
        if(inserted) {
            ob.observerArray(inserted)  //將新增屬性繼續
        }

        console.log('陣列更新了:'+ JSON.stringify(inserted))
        //通知所有註冊的觀察者進行響應式處理,這裡下期再來探討
        // ob.dep.notify() 
        return res
    }
})

OK,寫到這裡我們可以來測試一下我們的Vue了

let vm = new Vue({
  el:'#app',
  data(){
    return{
      a:1,
      b:{name:'nanjiu'},
      c:[{name:'front end'}]
    }
  },
  computed:{}
})
vm._data.a = 2
vm._data.c.push({name:'sss'})

這裡控制檯應該會列印出如下內容:

陣列重寫.png

這樣Vue的資料響應式,我們就算實現了,但這裡看著有點彆扭,我們希望操作Vue的data裡的資料可以直接通過this來獲取,而不是通過this._data來獲取,這個很簡單,我們只需要再做一層代理就可以實現了。

5.代理

export function proxy (target,sourceKey,key) {
    // target: 想要代理到的目標物件,sourceKey:想要代理的物件
  	const _that = this
    Object.defineProperty(target, key, {
        enumerable: true,
        configurable: true,
        get: function(){
            return _that[sourceKey][key]
        },
        set: function(v){
            _that[sourceKey][key] = v
        }
    })
}

然後再initData裡面呼叫該方法

function initData(vm) {
    console.log('初始化資料',vm.$options.data)
    // 資料初始化
    let data = vm.$options.data;
    data = vm._data =  typeof data === 'function' ? data.call(this) : data
    // 物件劫持,使用者改變了資料 ==》 重新整理頁面
    // MVVM模式 資料驅動檢視
     Object.keys(data).forEach(i => {
        proxy.call(vm,vm,'_data',i)
    })
    // Object.definePropety() 給屬性增加get和set方法
    observe(data)  //響應式原理
}

然後我們就可以愉快的使用this直接去訪問data裡面的資料了~

3.總結

OK,Vue的響應式原理我們就算全都實現了一遍,Vue2的響應式原理主要是通過Object.defineProperty來實現的,但這個方法有缺陷,不能劫持陣列,所以對資料需要單獨處理,在Vue3中,底層把響應式處理改成了通過proxy來實現,這個方法對陣列劫持也同樣適用。這裡我們只探討了Vue是如何進行響應式處理,至於它如何收集依賴,以及如何通知檢視更新我們下期再來一起學習吧~

覺得文章不錯,可以點個贊呀_ 另外歡迎關注留言交流~

相關文章