這是我參與8月更文挑戰的第10天,活動詳情查看:8月更文挑戰javascript
本文從vue系列的基本設計思路開始,到手寫基本api的實現。讓你們從實踐中體會vue的數據驅動的神祕之處。html
你們都知道,vue是一個典型的MVVM框架。那什麼是MVVM、在vue中又是怎麼體現的呢?vue
M表明的是模型Model。是展現在頁面中的數據,在vue中指向的是data中的數據模型。java
V表明的是視圖View。是展現的頁面,指向的是vue中的模板引擎(template模板)。git
VM表明的是ViewModel。不須要經過咱們的操做將數據解析展現到視圖上,以及數據發生改變,頁面上的視圖自動會發生相應改變。github
那麼vue是如何將視圖和邏輯操做分開,這就頗有必要提到vue中數據驅動的特色。那麼這些數據又是如何在視圖中展現的呢? vue經過數據響應式監聽數據的變化並在視圖中更新;
模板引擎提供描述視圖的模板語法(類html。提供一些vue特有指令、插值);
渲染:將模板語法轉爲html(AST=>vdom=>dom)。
web
vue中的監聽數據變化:
vue2: Object.defineProperty()
vue3: Proxy
二者簡單的響應數據案例能夠查看這篇文章。api
<div id="app"></div>
<script> function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { return val }, set(newVal) { if (val != newVal) val = newVal // 數據發生變化通知視圖更新 update() } }) } function update() { content.innerHTML = `<h1>${obj.name}</h1>` } let obj = { name: 'clying' } let content = document.getElementById('app') update() // 響應式處理 defineReactive(obj, 'name', 'clying') setTimeout(() => { obj.name = 'deng' console.log(obj.name); }, 1000) </script>
複製代碼
運行代碼,咱們能夠看到頁面一開始展現clying
,通過1秒以後變爲deng
。 經過這個簡單的案例,咱們能夠實現簡單的obj對象響應式地對頁面進行渲染。數組
上述案例中只是對deep=1
的對象進行了數據監聽。那麼具備深度的obj對象又是如何監聽變化的呢?
markdown
這時候就須要遍歷obj對象,對對象中的每一個屬性進行數據監聽。經過Object.keys
返回一個obj中全部元素爲字符串的數組,對其進行setter和getter攔截。
// 先來一個具備深度的對象
let obj = {
name: 'clying',
arr: [
1,
{
namearr: '2',
},
],
children: {
name1: 'deng',
children: {
name2: 'clying deng',
},
},
}
function defineReactive(obj, key, val) {
observe(val) // 子屬性可能仍爲對象,在對其進行攔截
Object.defineProperty(obj, key, {
get() {
console.log('獲取', key)
return val
},
set(newVal) {
if (newVal !== val) val = newVal
console.log('設置新值', key, obj[key])
},
})
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return
Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]))
}
observe(obj)
obj.children.name1 = 'l'
複製代碼
當設置obj.children.name1
時,先獲取到children屬性,發現children還是一個對象,繼續遍歷。獲取到children中的name1屬性時,發現不是對像,對name1進行攔截,設置新值。
當我想要根據上例,對obj動態添加一個age屬性時,實際上是沒有做用的。defineProperty
沒法檢測新增屬性,這也涉及到vue2的一個弊端。這就要額外使用到vue2中的set API方法。
function set(obj, key, val) {
defineReactive(obj, key, val)
}
set(obj, 'age', 22)
obj.age
複製代碼
能夠看出set方法其實也是利用defineProperty
去添加新屬性,只是須要用戶手動調用。經過調用set,使obj中新屬性age能夠被攔截到。
注意set用法:
set方法對於接收的目標參數必須是響應式的,能夠在源碼set方法中看到,一開始就會去判斷傳入的目標值是不是原始值、undefined或null,若是是這些狀況直接警告。若是想要刪除屬性,相同的須要手動調用delete方法。
注:observer文件夾index.js 201行
defineProperty
方法實際上是能夠攔截到像arr[0] = 1
這種,經過index下標賦值的數組。可是它沒法支持數組中push、pop等數組的原型方法。咱們須要攔截數組的7個方法,重寫他們,就是幹!
由於咱們只是簡單的模仿,沒有寫Observe類。在此我將Observe類中拆成observe方法(判斷是數組仍是對象,分別監聽)、observeArray循環遍歷監聽數組屬性。
function observeArray(arr) {
arr.forEach((_) => observe(_))
}
function observe(obj) {
if (typeof obj !== 'object' || obj == null) return
if (Array.isArray(obj)) {
obj.__proto__ = arrayMethods // 繼承原型方法屬性
observeArray(obj)
} else {
Object.keys(obj).forEach((key) => defineReactive(obj, key, obj[key]))
}
}
複製代碼
比較核心的仍是在 obj.__proto__ = arrayMethods
中,使當前遍歷到的數組的原型鏈能夠指向咱們重寫的數組方法。
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
]
let oldArrayMethods = Array.prototype
let arrayMethods = Object.create(oldArrayMethods) //arrayMethods的原型指向Array數組的原型,能夠獲取數組原型方法
methodsToPatch.forEach((method) => {
arrayMethods[method] = function (...args) {
const result = oldArrayMethods[method].apply(this, args)
let inserted // 當前用戶插入的元素
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
// 3個 新增屬性 splice 有刪除 新增的功能 arr.splice(0,1,{name:1})
case 'splice':
inserted = args.slice(2)
default:
break
}
// let ob = this.__ob__;
// ob.observeArray(inserted); // 插入的是對象或者數組的話還須要再次遞歸監聽
// update通知更新
return result
}
})
複製代碼
當咱們經過obj.arr.push(1);obj.arr[1].namearr = 2
時,能夠看到控制檯輸出:
說明在push數組方法,和修改數組值時,數組均可以走到defineProperty
中,被其攔截。
在此,數組插入的值多是對象或數組時,仍須要對其插入的值進行監聽。應該在Observe類中,先將這個實例保存(內部會含有observeArray方法)。而後,在arrayMethods中使用其observeArray方法,繼續進行深度劫持。
至此,關於數據響應式就能夠告一段落拉。若有不足,歡迎你們指正。