深入理解Vue響應(yīng)式原理及其實(shí)現(xiàn)方式
Vue的響應(yīng)式
用過(guò)Vue這個(gè)框架的人應(yīng)該都知道,數(shù)據(jù)驅(qū)動(dòng)是Vue框架的核心,數(shù)據(jù)雙向綁定是它的一大特色,根據(jù)官方的解釋?zhuān)覀兛梢员容^清晰地去知道響應(yīng)式的簡(jiǎn)單原理。
Vue2的響應(yīng)式原理
當(dāng)你把一個(gè)普通的 JavaScript 對(duì)象傳入 Vue 實(shí)例作為 data 選項(xiàng),Vue 將遍歷此對(duì)象所有的 property,并使用 Object.defineProperty 把這些 property 全部轉(zhuǎn)為 getter/setter。Object.defineProperty 是 ES5 中一個(gè)無(wú)法 shim 的特性,這也就是 Vue 不支持 IE8 以及更低版本瀏覽器的原因。
這些 getter/setter 對(duì)用戶(hù)來(lái)說(shuō)是不可見(jiàn)的,但是在內(nèi)部它們讓 Vue 能夠追蹤依賴(lài),在 property 被訪(fǎng)問(wèn)和修改時(shí)通知變更。這里需要注意的是不同瀏覽器在控制臺(tái)打印數(shù)據(jù)對(duì)象時(shí)對(duì) getter/setter 的格式化并不同,所以建議安裝 vue-devtools 來(lái)獲取對(duì)檢查數(shù)據(jù)更加友好的用戶(hù)界面。
每個(gè)組件實(shí)例都對(duì)應(yīng)一個(gè) watcher 實(shí)例,它會(huì)在組件渲染的過(guò)程中把“接觸”過(guò)的數(shù)據(jù) property 記錄為依賴(lài)。之后當(dāng)依賴(lài)項(xiàng)的 setter 觸發(fā)時(shí),會(huì)通知 watcher,從而使它關(guān)聯(lián)的組件重新渲染。
Vue3的響應(yīng)式原理
實(shí)現(xiàn)原理:
通過(guò)Proxy(代理): 攔截對(duì)象中任意屬性的變化,包括:屬性值的讀寫(xiě),屬性的增加,屬性的刪除等。
通過(guò)Reffect(反射): 對(duì)源對(duì)象的屬性進(jìn)行操作
new Proxy(data,{ //攔截讀取屬性值 get(target, prop){ return Reflect.get(target, prop) }, //攔截設(shè)置屬性值或添加新屬性 set(target, prop, value){ return Reflect.set(target, prop, value) }, //攔截刪除屬性 deleteProperty(target, prop){ return Reflect.deleteProperty(target, prop) } })
Vue2和Vue3的響應(yīng)式原理其實(shí)有異曲同工之妙,但是Vue3的proxy封裝性和獨(dú)立性相對(duì)更強(qiáng)更靈活一些,但是我們看到的這些只是最簡(jiǎn)單的,也是最基礎(chǔ)的一個(gè)響應(yīng)式原理,如果要更深入地去了解Vue是如何利用這一原理去實(shí)現(xiàn)框架中的各種雙向綁定和數(shù)據(jù)渲染操作,我們可以對(duì)它的源碼進(jìn)行分析。
深入理解響應(yīng)式
1.數(shù)據(jù)初始化
new Vue({ el: "#app", router, store, render: (h) => h(App), });
這段代碼,大家一定非常熟悉。這就是 Vue 實(shí)例化的過(guò)程 從 new 操作符,咱們可以看出 Vue 其實(shí)就是一個(gè)構(gòu)造函數(shù),沒(méi)啥特別的,傳入的參數(shù)就是一個(gè)對(duì)象,我們叫做 options(選項(xiàng))。
// src/index.js import { initMixin } from "./init.js"; // Vue就是一個(gè)構(gòu)造函數(shù) 通過(guò)new關(guān)鍵字進(jìn)行實(shí)例化 function Vue(options) { // 這里開(kāi)始進(jìn)行Vue初始化工作 this._init(options); } // _init方法是掛載在Vue原型的方法 通過(guò)引入文件的方式進(jìn)行原型掛載需要傳入Vue // 此做法有利于代碼分割 initMixin(Vue); export default Vue;
因?yàn)樵?Vue 初始化可能會(huì)處理很多事情,比如數(shù)據(jù)處理,事件處理,生命周期處理等等,所以劃分不同文件引入利于代碼分割。
// src/init.js import { initState } from "./state"; export function initMixin(Vue) { Vue.prototype._init = function (options) { const vm = this; // 這里的this代表調(diào)用_init方法的對(duì)象(實(shí)例對(duì)象) // this.$options就是用戶(hù)new Vue的時(shí)候傳入的屬性 vm.$options = options; // 初始化狀態(tài) initState(vm); }; }
initMixin 把_init 方法掛載在 Vue 原型 供 Vue 實(shí)例調(diào)用。
// src/state.js import { observe } from "./observer/index.js"; export function initState(vm) { // 獲取傳入的數(shù)據(jù)對(duì)象 const opts = vm.$options; if (opts.props) { initProps(vm); } if (opts.methods) { initMethod(vm); } if (opts.data) { // 初始化data initData(vm); } if (opts.computed) { initComputed(vm); } if (opts.watch) { initWatch(vm); } } // 初始化data數(shù)據(jù) function initData(vm) { let data = vm.$options.data; // 實(shí)例的_data屬性就是傳入的data // vue組件data推薦使用函數(shù) 防止數(shù)據(jù)在組件之間共享 data = vm._data = typeof data === "function" ? data.call(vm) : data || {}; // 把data數(shù)據(jù)代理到vm 也就是Vue實(shí)例上面 我們可以使用this.a來(lái)訪(fǎng)問(wèn)this._data.a for (let key in data) { proxy(vm, `_data`, key); } // 對(duì)數(shù)據(jù)進(jìn)行觀(guān)測(cè) --響應(yīng)式數(shù)據(jù)核心 observe(data); } // 數(shù)據(jù)代理 function proxy(object, sourceKey, key) { Object.defineProperty(object, key, { get() { return object[sourceKey][key]; }, set(newValue) { object[sourceKey][key] = newValue; }, }); }
①通過(guò)這段代碼,就可以得到一個(gè)平時(shí)開(kāi)發(fā)Vue項(xiàng)目的時(shí)候?qū)τ谖覀冇泻艽髱椭男畔?,即關(guān)于數(shù)據(jù)初始化的順序依次是 prop>methods>data>computed>watch。關(guān)于我們能否在data里面去調(diào)用prop的值的問(wèn)題,如果知道數(shù)據(jù)渲染的順序,就迎刃而解了。
②另外通過(guò)這段源碼,我們還可以獲得一個(gè)信息,data是用了函數(shù)function封裝,而不是對(duì)象Object,就是為了避免數(shù)據(jù)在組件間共享,這樣我們每個(gè)組件才能有獨(dú)立的變量作用域。
2.對(duì)象的數(shù)據(jù)劫持
對(duì)象數(shù)據(jù)的劫持,其實(shí)很好理解,代碼中通過(guò)遞歸的方式,把對(duì)象中的每個(gè)參數(shù)都添加了對(duì)應(yīng)的監(jiān)聽(tīng)器,所以當(dāng)對(duì)象數(shù)據(jù)發(fā)生變化的時(shí)候自然就會(huì)觸發(fā)監(jiān)聽(tīng)器。
這里我們可以得到一個(gè)信息,對(duì)象只有在初始化階段的時(shí)候進(jìn)行了監(jiān)聽(tīng)標(biāo)記,當(dāng)我們后續(xù)為對(duì)象新增參數(shù)的時(shí)候,必須通過(guò)Vue提供的內(nèi)置函數(shù) s e t 和 set和 set和delete才能對(duì)對(duì)象參數(shù)進(jìn)行動(dòng)態(tài)操作,不然直接通過(guò)Object.xxx去新增參數(shù),這個(gè)時(shí)候是不具備雙向綁定的效果的。
// src/obserber/index.js class Observer { // 觀(guān)測(cè)值 constructor(value) { this.walk(value); } walk(data) { // 對(duì)象上的所有屬性依次進(jìn)行觀(guān)測(cè) let keys = Object.keys(data); for (let i = 0; i < keys.length; i++) { let key = keys[i]; let value = data[key]; defineReactive(data, key, value); } } } // Object.defineProperty數(shù)據(jù)劫持核心 兼容性在ie9以及以上 function defineReactive(data, key, value) { observe(value); // 遞歸關(guān)鍵 // --如果value還是一個(gè)對(duì)象會(huì)繼續(xù)走一遍odefineReactive 層層遍歷一直到value不是對(duì)象才停止 // 思考?如果Vue數(shù)據(jù)嵌套層級(jí)過(guò)深 >>性能會(huì)受影響 Object.defineProperty(data, key, { get() { console.log("獲取值"); return value; }, set(newValue) { if (newValue === value) return; console.log("設(shè)置值"); value = newValue; }, }); } export function observe(value) { // 如果傳過(guò)來(lái)的是對(duì)象或者數(shù)組 進(jìn)行屬性劫持 if ( Object.prototype.toString.call(value) === "[object Object]" || Array.isArray(value) ) { return new Observer(value); } }
數(shù)組的監(jiān)聽(tīng)
// src/obserber/index.js import { arrayMethods } from "./array"; class Observer { constructor(value) { if (Array.isArray(value)) { // 這里對(duì)數(shù)組做了額外判斷 // 通過(guò)重寫(xiě)數(shù)組原型方法來(lái)對(duì)數(shù)組的七種方法進(jìn)行攔截 value.__proto__ = arrayMethods; // 如果數(shù)組里面還包含數(shù)組 需要遞歸判斷 this.observeArray(value); } else { this.walk(value); } } observeArray(items) { for (let i = 0; i < items.length; i++) { observe(items[i]); } } }
數(shù)組的監(jiān)聽(tīng),是對(duì)數(shù)組的每個(gè)元素進(jìn)行判斷,如果數(shù)組中還包含數(shù)組則需要遞歸進(jìn)行監(jiān)聽(tīng),如果非數(shù)組元素則直接對(duì)數(shù)組進(jìn)行監(jiān)聽(tīng)設(shè)置的操作。
因?yàn)閷?duì)數(shù)組下標(biāo)的攔截太浪費(fèi)性能 對(duì) Observer 構(gòu)造函數(shù)傳入的數(shù)據(jù)參數(shù)增加了數(shù)組的判斷。
// src/obserber/index.js class Observer { // 觀(guān)測(cè)值 constructor(value) { Object.defineProperty(value, "__ob__", { // 值指代的就是Observer的實(shí)例 value: this, // 不可枚舉 enumerable: false, writable: true, configurable: true, }); } }
最后為了方便我們對(duì)數(shù)組的操作,Vue對(duì)數(shù)組的一些常用方法進(jìn)行了重寫(xiě),當(dāng)我們調(diào)用這些方法,Vue底層會(huì)為我們自動(dòng)添加對(duì)應(yīng)的監(jiān)聽(tīng)器,不用讓我們?cè)俅稳?duì)元素進(jìn)行數(shù)據(jù)渲染和綁定。
// src/obserber/array.js // 先保留數(shù)組原型 const arrayProto = Array.prototype; // 然后將arrayMethods繼承自數(shù)組原型 // 這里是面向切片編程思想(AOP)--不破壞封裝的前提下,動(dòng)態(tài)的擴(kuò)展功能 export const arrayMethods = Object.create(arrayProto); let methodsToPatch = [ "push", "pop", "shift", "unshift", "splice", "reverse", "sort", ]; methodsToPatch.forEach((method) => { arrayMethods[method] = function (...args) { // 這里保留原型方法的執(zhí)行結(jié)果 const result = arrayProto[method].apply(this, args); // 這句話(huà)是關(guān)鍵 // this代表的就是數(shù)據(jù)本身 比如數(shù)據(jù)是{a:[1,2,3]} 那么我們使用a.push(4) this就是a ob就是a.__ob__ 這個(gè)屬性就是上段代碼增加的 代表的是該數(shù)據(jù)已經(jīng)被響應(yīng)式觀(guān)察過(guò)了指向Observer實(shí)例 const ob = this.__ob__; // 這里的標(biāo)志就是代表數(shù)組有新增操作 let inserted; switch (method) { case "push": case "unshift": inserted = args; break; case "splice": inserted = args.slice(2); default: break; } // 如果有新增的元素 inserted是一個(gè)數(shù)組 調(diào)用Observer實(shí)例的observeArray對(duì)數(shù)組每一項(xiàng)進(jìn)行觀(guān)測(cè) if (inserted) ob.observeArray(inserted); // 之后咱們還可以在這里檢測(cè)到數(shù)組改變了之后從而觸發(fā)視圖更新的操作--后續(xù)源碼會(huì)揭曉 return result; }; });
到此這篇關(guān)于深入理解Vue響應(yīng)式原理及其實(shí)現(xiàn)方式的文章就介紹到這了,更多相關(guān)Vue響應(yīng)式原理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue2老項(xiàng)目vite升級(jí)改造過(guò)程記錄
目前vite主要默認(rèn)是支持給vue3使用的,并且如果使用官方的cli創(chuàng)建的項(xiàng)目一樣會(huì)默認(rèn)使用vue3去構(gòu)建項(xiàng)目,此時(shí)對(duì)于一些vue2的老項(xiàng)目就顯得不友好了,下面這篇文章主要給大家介紹了關(guān)于vue2老項(xiàng)目vite升級(jí)改造的相關(guān)資料,需要的朋友可以參考下2022-12-12electron實(shí)現(xiàn)打印功能支持靜默打印、無(wú)感打印
使用electron開(kāi)發(fā)應(yīng)用遇到了打印小票的功能,實(shí)現(xiàn)途中還是幾經(jīng)波折,下面這篇文章主要給大家介紹了關(guān)于electron實(shí)現(xiàn)打印功能支持靜默打印、無(wú)感打印的相關(guān)資料,需要的朋友可以參考下2023-12-12vue3組合式api實(shí)現(xiàn)v-lazy圖片懶加載的方法實(shí)例
vue作為前端主流的3大框架之一,目前在國(guó)內(nèi)有著非常廣泛的應(yīng)用,下面這篇文章主要給大家介紹了關(guān)于vue3組合式api實(shí)現(xiàn)v-lazy圖片懶加載的相關(guān)資料,需要的朋友可以參考下2022-09-09vue中使用v-if,v-else來(lái)設(shè)置css樣式的步驟
我們?cè)谑褂胿ue項(xiàng)目開(kāi)發(fā)時(shí),v-if是使用的非常多的,在這里我們談?wù)勅绾问褂胿-i來(lái)綁定修改css樣式,使用的主要是雙向數(shù)據(jù)綁定,即通過(guò)改變他的狀態(tài)來(lái)改變他的樣式,這篇文章主要介紹了vue中如何使用v-if,v-else來(lái)設(shè)置css樣式,需要的朋友可以參考下2023-03-03Vue實(shí)戰(zhàn)記錄之登陸頁(yè)面的實(shí)現(xiàn)
最近學(xué)習(xí)過(guò)程中常碰到讓用戶(hù)登錄注冊(cè)這種,這篇文章主要給大家介紹了關(guān)于Vue實(shí)戰(zhàn)記錄之登陸頁(yè)面實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2021-06-06