新聞中心
【稿件】前言

創(chuàng)新互聯(lián)建站不只是一家網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司;我們對營銷、技術(shù)、服務(wù)都有自己獨特見解,公司采取“創(chuàng)意+綜合+營銷”一體化的方式為您提供更專業(yè)的服務(wù)!我們經(jīng)歷的每一步也許不一定是最完美的,但每一步都有值得深思的意義。我們珍視每一份信任,關(guān)注我們的網(wǎng)站制作、成都網(wǎng)站制作質(zhì)量和服務(wù)品質(zhì),在得到用戶滿意的同時,也能得到同行業(yè)的專業(yè)認可,能夠為行業(yè)創(chuàng)新發(fā)展助力。未來將繼續(xù)專注于技術(shù)創(chuàng)新,服務(wù)升級,滿足企業(yè)一站式營銷型網(wǎng)站需求,讓再小的品牌網(wǎng)站制作也能產(chǎn)生價值!
Vue 最獨特的特性之一,是其非侵入性的響應(yīng)式系統(tǒng)。數(shù)據(jù)模型僅僅是普通的 JavaScript 對象。而當你修改它們時,視圖會進行更新。這使得狀態(tài)管理非常簡單直接,不過理解其工作原理同樣重要,這樣你可以避開一些常見的問題。----官方文檔 本文將針對響應(yīng)式原理做一個詳細介紹,并且?guī)銓崿F(xiàn)一個基礎(chǔ)版的響應(yīng)式系統(tǒng)。本文的代碼請猛戳Github博客
什么是響應(yīng)式
我們先來看個例子:
Price :¥{{ price }} Total:¥{{ price * quantity }} Taxes: ¥{{ totalPriceWithTax }}
- var app = new Vue({
- el: '#app',
- data() {
- return {
- price: 5.0,
- quantity: 2
- };
- },
- computed: {
- totalPriceWithTax() {
- return this.price * this.quantity * 1.03;
- }
- },
- methods: {
- changePrice() {
- this.price = 10;
- }
- }
- })
上例中當price 發(fā)生變化的時候,Vue就知道自己需要做三件事情:
- 更新頁面上price的值
- 計算表達式 price*quantity 的值,更新頁面
- 調(diào)用totalPriceWithTax 函數(shù),更新頁面
發(fā)生變化后,會重新對頁面渲染,這就是Vue響應(yīng)式,那么這一切是怎么做到的呢?
想完成這個過程,我們需要:
- 偵測數(shù)據(jù)的變化
- 收集視圖依賴了哪些數(shù)據(jù)
- 數(shù)據(jù)變化時,自動“通知”需要更新的視圖部分,并進行更新
對應(yīng)專業(yè)俗語分別是:
- 數(shù)據(jù)劫持 / 數(shù)據(jù)代理
- 依賴收集
- 發(fā)布訂閱模式
如何偵測數(shù)據(jù)的變化
首先有個問題,在Javascript中,如何偵測一個對象的變化? 其實有兩種辦法可以偵測到變化:使用Object.defineProperty和ES6的Proxy,這就是進行數(shù)據(jù)劫持或數(shù)據(jù)代理。這部分代碼主要參考珠峰架構(gòu)課。
方法1.Object.defineProperty實現(xiàn)
Vue通過設(shè)定對象屬性的 setter/getter 方法來監(jiān)聽數(shù)據(jù)的變化,通過getter進行依賴收集,而每個setter方法就是一個觀察者,在數(shù)據(jù)變更的時候通知訂閱者更新視圖。
- function render () {
- console.log('模擬視圖渲染')
- }
- let data = {
- name: '浪里行舟',
- location: { x: 100, y: 100 }
- }
- observe(data)
- function observe (obj) {
- // 判斷類型
- if (!obj || typeof obj !== 'object') {
- return
- }
- Object.keys(obj).forEach(key => {
- defineReactive(obj, key, obj[key])
- })
- function defineReactive (obj, key, value) {
- // 遞歸子屬性
- observe(value)
- Object.defineProperty(obj, key, {
- enumerable: true, //可枚舉(可以遍歷)
- configurable: true, //可配置(比如可以刪除)
- get: function reactiveGetter () {
- console.log('get', value) // 監(jiān)聽
- return value
- },
- set: function reactiveSetter (newVal) {
- observe(newVal) //如果賦值是一個對象,也要遞歸子屬性
- if (newVal !== value) {
- console.log('set', newVal) // 監(jiān)聽
- render()
- value = newVal
- }
- }
- })
- }
- }
- data.location = {
- x: 1000,
- y: 1000
- } //set {x: 1000,y: 1000} 模擬視圖渲染
- data.name // get 浪里行舟
幾個注意點補充說明:
- 這種方式無法檢測到對象屬性的添加或刪除(如data.location.a=1)。
這是因為 Vue 通過Object.defineProperty來將對象的key轉(zhuǎn)換成getter/setter的形式來追蹤變化,但getter/setter只能追蹤一個數(shù)據(jù)是否被修改,無法追蹤新增屬性和刪除屬性。如果是刪除屬性,我們可以用vm.$delete實現(xiàn),那如果是新增屬性,該怎么辦呢? 1)可以使用 Vue.set(location, a, 1) 方法向嵌套對象添加響應(yīng)式屬性; 2)也可以給這個對象重新賦值,比如data.location = {...data.location,a:1}
- Object.defineProperty 不能監(jiān)聽數(shù)組的變化,需要進行數(shù)組方法的重寫
- function render() {
- console.log('模擬視圖渲染')
- }
- let obj = [1, 2, 3]
- let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']
- // 先獲取到原來的原型上的方法
- let arrayProto = Array.prototype
- // 創(chuàng)建一個自己的原型 并且重寫methods這些方法
- let proto = Object.create(arrayProto)
- methods.forEach(method => {
- proto[method] = function() {
- // AOP
- arrayProto[method].call(this, ...arguments)
- render()
- }
- })
- function observer(obj) {
- // 把所有的屬性定義成set/get的方式
- if (Array.isArray(obj)) {
- obj.__proto__ = proto
- return
- }
- if (typeof obj == 'object') {
- for (let key in obj) {
- defineReactive(obj, key, obj[key])
- }
- }
- }
- function defineReactive(data, key, value) {
- observer(value)
- Object.defineProperty(data, key, {
- get() {
- return value
- },
- set(newValue) {
- observer(newValue)
- if (newValue !== value) {
- render()
- value = newValue
- }
- }
- })
- }
- observer(obj)
- function $set(data, key, value) {
- defineReactive(data, key, value)
- }
- obj.push(123, 55)
- console.log(obj) //[1, 2, 3, 123, 55]
這種方法將數(shù)組的常用方法進行重寫,進而覆蓋掉原生的數(shù)組方法,重寫之后的數(shù)組方法需要能夠被攔截。但有些數(shù)組操作Vue時攔截不到的,當然也就沒辦法響應(yīng),比如:
- obj.length-- // 不支持數(shù)組的長度變化
- obj[0]=1 // 修改數(shù)組中***個元素,也無法偵測數(shù)組的變化
ES6提供了元編程的能力,所以有能力攔截,Vue3.0可能會用ES6中Proxy 作為實現(xiàn)數(shù)據(jù)代理的主要方式。
方法2.Proxy實現(xiàn)
Proxy 是 JavaScript 2015 的一個新特性。Proxy 的代理是針對整個對象的,而不是對象的某個屬性,因此不同于 Object.defineProperty 的必須遍歷對象每個屬性,Proxy 只需要做一層代理就可以監(jiān)聽同級結(jié)構(gòu)下的所有屬性變化,當然對于深層結(jié)構(gòu),遞歸還是需要進行的。此外**Proxy支持代理數(shù)組的變化。**
- function render() {
- console.log('模擬視圖的更新')
- }
- let obj = {
- name: '前端工匠',
- age: { age: 100 },
- arr: [1, 2, 3]
- }
- let handler = {
- get(target, key) {
- // 如果取的值是對象就在對這個對象進行數(shù)據(jù)劫持
- if (typeof target[key] == 'object' && target[key] !== null) {
- return new Proxy(target[key], handler)
- }
- return Reflect.get(target, key)
- },
- set(target, key, value) {
- if (key === 'length') return true
- render()
- return Reflect.set(target, key, value)
- }
- }
- let proxy = new Proxy(obj, handler)
- proxy.age.name = '浪里行舟' // 支持新增屬性
- console.log(proxy.age.name) // 模擬視圖的更新 浪里行舟
- proxy.arr[0] = '浪里行舟' //支持數(shù)組的內(nèi)容發(fā)生變化
- console.log(proxy.arr) // 模擬視圖的更新 ['浪里行舟', 2, 3 ]
- proxy.arr.length-- // 無效
以上代碼不僅精簡,而且還是實現(xiàn)一套代碼對對象和數(shù)組的偵測都適用。不過Proxy兼容性不太好!
我們之所以要觀察數(shù)據(jù),其目的在于當數(shù)據(jù)的屬性發(fā)生變化時,可以通知那些曾經(jīng)使用了該數(shù)據(jù)的地方。比如***例子中,模板中使用了price 數(shù)據(jù),當它發(fā)生變化時,要向使用了它的地方發(fā)送通知。那如何收集依賴呢?
收集依賴與發(fā)布訂閱模式
如何收集依賴,總結(jié)起來就一句話,在getter中收集依賴,在setter中觸發(fā)依賴 我們先來實現(xiàn)一個 Dep 類,用于解耦屬性的依賴收集和派發(fā)更新操作。
- // 通過 Dep 解耦屬性的依賴和更新操作
- class Dep {
- constructor() {
- this.subs = []
- }
- // 添加依賴
- addSub(sub) {
- this.subs.push(sub)
- }
- // 更新
- notify() {
- this.subs.forEach(sub => {
- sub.update()
- })
- }
- }
- // 全局屬性,通過該屬性配置 Watcher
- Dep.target = null
當需要依賴收集的時候調(diào)用 addSub,當需要派發(fā)更新的時候調(diào)用 notify。具體如何調(diào)用呢?
- let dp = new Dep()
- dp.addSub(() => {
- console.log('emit here')
- })
- dp.notify()
這就是一個簡單實現(xiàn)的“事件發(fā)布訂閱模式”,當然代碼只是啟發(fā)思路,真實應(yīng)用還比較“粗糙”,沒有進行事件名設(shè)置,APIs 也并不豐富,但完全能夠說明問題了。
接下來我們先來簡單的了解下 Vue 組件掛載時添加響應(yīng)式的過程。在組件掛載時,會先對所有需要的屬性調(diào)用 Object.defineProperty(),然后實例化 Watcher,傳入組件更新的回調(diào)。在實例化過程中,會對模板中的屬性進行求值,觸發(fā)依賴收集。我們可以把Watcher理解成一個中介的角色,數(shù)據(jù)發(fā)生變化時通知它,然后它再通知其他地方。
***需要對 defineReactive 函數(shù)進行改造,在自定義函數(shù)中添加依賴收集和派發(fā)更新相關(guān)的代碼。
- function render () {
- console.log('模擬視圖渲染')
- }
- let data = {
- name: '浪里行舟',
- location: { x: 100, y: 100 }
- }
- observe(data)
- let dp = new Dep()
- function observe (obj) {
- // 判斷類型
- if (!obj || typeof obj !== 'object') {
- return
- }
- Object.keys(obj).forEach(key => {
- defineReactive(obj, key, obj[key])
- })
- function defineReactive (obj, key, value) {
- // 遞歸子屬性
- observe(value)
- Object.defineProperty(obj, key, {
- enumerable: true, //可枚舉(可以遍歷)
- configurable: true, //可配置(比如可以刪除)
- get: function reactiveGetter () {
- console.log('get', value) // 監(jiān)聽
- // 將 Watcher 添加到訂閱
- if (Dep.target) {
- dp.addSub(Dep.target)
- }
- return value
- },
- set: function reactiveSetter (newVal) {
- observe(newVal) //如果賦值是一個對象,也要遞歸子屬性
- if (newVal !== value) {
- console.log('set', newVal) // 監(jiān)聽
- render()
- value = newVal
- // 執(zhí)行 watcher 的 update 方法
- dp.notify()
- }
- }
- })
- }
- }
以上所有代碼實現(xiàn)了一個簡易的數(shù)據(jù)響應(yīng)式,核心思路就是手動觸發(fā)一次屬性的 getter 來實現(xiàn)依賴收集。
總結(jié)
我們再來回顧下整個過程:
- 在 Vue 中模板編譯過程中的指令或者數(shù)據(jù)綁定都會實例化一個 Watcher 實例,實例化過程中會觸發(fā) get() 將自身指向 Dep.target;
- data在 Observer 時執(zhí)行 getter 會觸發(fā) dep.depend() 進行依賴收集;依賴收集的結(jié)果:
- data在 Observer 時閉包的dep實例的subs添加觀察它的 Watcher 實例;
- Watcher 的deps中添加觀察對象 Observer 時的閉包dep;
- 當data中被 Observer 的某個對象值變化后,觸發(fā)subs中觀察它的watcher執(zhí)行 update() 方法,***實際上是調(diào)用watcher的回調(diào)函數(shù)cb,進而更新視圖。
參考文章和書籍
- 珠峰架構(gòu)課(強烈推薦)
- 剖析 Vue.js 內(nèi)部運行機制
- 深入淺出Vue.js
- Vue官方文檔
- 前端面試之道
- 前端開發(fā)核心知識進階
- Javascript響應(yīng)式的最通俗易懂的解釋(譯)
作者介紹
浪里行舟:碩士研究生,專注于前端。個人公眾號:「前端工匠」,致力于打造適合初中級工程師能夠快速吸收的一系列優(yōu)質(zhì)文章!
【原創(chuàng)稿件,合作站點轉(zhuǎn)載請注明原文作者和出處為.com】
分享文章:深入理解vue響應(yīng)式原理
網(wǎng)頁路徑:http://www.dlmjj.cn/article/djochec.html


咨詢
建站咨詢
