記錄一些糾結了好久好久的問題html
按照VUE傳遞數據的方式 若是讓我來暴露變量 我會經過這樣的方式:vue
function myVue(config){
this.data = config.data
}
const vm = new myVue({
data: {
msg: "xxx"
}
})
//獲取到實例裏面的變量
console.log(vm.data.msg)
複製代碼
那麼在vue中是如何處理的呢?數組
const vm = new myVue({
data: {
msg: "xxx"
}
})
console.log(vm.data.msg)//undefined
複製代碼
可見在vm實例對象上並無vm.data.msg這樣的屬性。直接打印vm以後發現 其實msg直接被就放在vm的實例對象上,使用vm.msg
就能夠直接獲取到變量值。也就是說 建立vue實例的時候,vue會將data當中的成員帶到vm實例上。瀏覽器
可是爲何要這麼作呢?我能想到的幾個緣由以下app
總結: 目的是爲了更方便監控數據變化,而後執行某個監聽函數,實現響應式。
目前只想到這麼多 之後有想到什麼再補充一下dom
爲何咱們一改變數據中的內容VUE就會知道 並且從新渲染頁面嘞?異步
首先,VUE是怎麼監控數據變化的? 在vue2.0中,經過Object.defineProperty來監控數據的變化。在vue3.0中,經過proxy來監控數據的變化。(proxy下次抽時間搞明白了再模擬)函數
先給出一個基本結構:一個監聽數據的變化性能
const data = {
name: "小餅"
}
var value = data.name
//給data.name賦值的時候實際上是在給data._name賦值
//若是在裏面使用了data.name會形成死循環
Object.defineProperty(data, "name", {
//讀取name屬性的時候執行的方法
get() {
console.log("讀")
//return的值就是讀取到的值
return value
},
//設置name屬性的時候執行的方法
//參數是被從新賦予的值
set(newVal) {
console.log("寫")
value = newVal
}
})
複製代碼
這樣子咱們就實現了監聽數據"小餅"的變化 接下來須要進行三點改進:測試
綜上 咱們能夠獲得改進以後的代碼:
// 傳入鍵和對應的變量 用於監聽
function defineReactive(obj, key) {
var value = obj[key]
Object.defineProperty(obj, key, {
get() {
return value
},
set(newVal) {
//若是修改以前和修改以後是同樣的值 就不渲染頁面
if(value === newVal){
return;
}
//模擬頁面的渲染
render()
value = newVal
}
})
}
//簡陋的模擬頁面的渲染
function render(){
console.log("頁面渲染啦")
}
for (key in data) {
defineReactive(data, key)
}
複製代碼
接下來又有一個問題 當data中的某個屬性的值是一個對象的時候 咱們沒法監聽到他的改變 沒法觸發寫操做 問題以下:
const data = {
name: "小餅",
blog: {
name: "快點吃餅"
}
}
data.blog.name = "仙女"
console.log(data.blog.name)
// 會觸發data.blog的讀操做 輸出"仙女"
// 這時候能從新賦值 可是咱們是直接拿出對象裏面的屬性來賦值的
// 而不是經過defineProperty來賦值的 因此沒法觸發寫操做
複製代碼
這時候咱們就要進行遞歸了 讓他可以觀察對象中的對象
function defineReactive(obj, key) {
var value = obj[key]
//若是是對象就先去監控對象裏面的每個屬性
recursive(value)
Object.defineProperty(obj, key, {
get() {
return value
},
set(newVal) {
value = newVal
render()
}
})
}
//遍歷監控對象中的每個屬性
function recursive(obj) {
//簡單判斷一下 沒有區分null和Array
if (typeof obj === "object") {
for (key in obj) {
defineReactive(obj, key)
}
}
}
複製代碼
這樣就基本實現了利用defineProperty進行對數據的監聽
利用defineProperty的特性 咱們能夠解答一個問題:
在VUE中 不能響應式更新數據的操做有下面幾種:
咱們能夠先用上面寫好的代碼測試一下可否進行這些操做:
首先測試他是否可以監聽到對象的增刪
//添加一個對象中沒有的數據
data.blog.articleNum = 3 //這時沒有執行寫操做 也就說明沒有執行頁面渲染
//根本緣由是使用for(let key in data)的時候並不會遍歷到這個屬性 天然也就沒法監聽
//刪除一個對象中存在的數據
delete data.shan.age
//刪除的時候不可能執行寫操做 因此依然監聽不到 也沒法渲染頁面
複製代碼
再測試他是否能監聽到數組的操做
data.arr[0] = 1 //觸發了寫的操做 頁面從新渲染了
data.arr[100] = 100 //沒有觸發寫的操做 頁面不會從新渲染
//其實原理和對象是同樣的 他能夠監聽已有的屬性的變化
//可是並無辦法遍歷到新增的屬性而且進行監聽
複製代碼
因此咱們能夠得出利用Object.defineProperty實現響應式的劣勢
雖然這時候可以用下標去修改一個數組 也能被監聽到 可是當咱們在Vue中使用下標去修改數組中的某個數據的時候是沒辦法修改爲功的 緣由是數組的數據太多了 若是要遍歷去監聽數組中每個數據的變化 是很浪費性能的 因此咱們直接不監聽數組的變化
因此咱們再改進一下代碼
function observer(data){
//看到數組就返回
if(Array.isArray(data)){
return;
}
if(typeof data === "object"){
for(let key in data){
defineReactive(data, key, data[key])
}
}
}
複製代碼
可是你數組變了 不渲染頁面也不行吧~因此VUE直接給咱們提供了一套改造過的API 這些API都會在改變數據以後當即從新渲染頁面
以push操做爲例:
//保存原來的數組方法
const oldPush = Array.prototype.push
Array.prototype.push = function(){
//首先要執行本來的數組方法 而後再執行咱們加入的渲染操做
//傳入this和參數值
oldPush.call(this, ...arguments);
//從新渲染頁面
render()
}
//執行
data.arr.push(100)
複製代碼
可是要修改的原型方法不僅這一個 咱們要修改全部須要重寫的原型方法 這時候要對數組的原型進行修改
//克隆一套原型鏈上的方法 在克隆的方法上去重寫
const arrayProto = Array.prototype
//由於咱們重寫的方法只是針對data中的數組
//對於普通的數組使用原來提供的數組方法就能夠了
//不要污染原來的原型方法
const arrayMethods = Object.create(arrayProto)
//要修改以前observer中的代碼
function observer(data){
if(Array.isArray(data)){
//改變數組的原型 這樣就能使用咱們重寫的原型方法了
data.__proto__ = arrayMethods;
return;
}
if(typeof data === "object"){...}
}
//指定要修改的方法名 經過遍從來修改裏面的方法
["push", "pop", "shift", "unshift", "sort", "splice", "reverse"].forEach(method = >{
arrayMethods[method] = function(){
//先執行原來的方法
arrayProto[method].call(this, ...arguments);
//渲染頁面
render()
}
})
複製代碼
$set
方法和$delete
方法既然都模擬變異方法了 $set
和$delete
方法就一塊兒模擬一下吧(抱着順便寫寫的寫法 不知不覺寫這麼長了...並且好像有點偏離主題了..) $set
修改數據的時候讓他直接渲染頁面 而後給他搞一個defineReactive
監聽一下 讓他可以一修改就自動渲染頁面 不然就要老是手動渲染
function $set(data, key, value){
data[key] = value
//監聽設置的值 若是設置的是一個對象 還要遞歸監聽對象中的值
defineReactive(data, key)
//渲染頁面
render()
return value
}
//使用
const value = $set(data.blog, "otherBlog", "仙女")
複製代碼
可是若是是數組在$set 那就不須要使用defineReactive 直接使用splice就能夠了 因此針對數組的狀況還要單獨判斷一下
function $set(data, key, value){
if(Array.isArray(Data)){
//簡單寫寫 這裏只是修改值 若是要指定下標添加值就不能這麼作了
//這裏就不用執行render了 由於splice方法裏面會幫咱們執行render
data.splice(key, 1, value)
return value
}
data[key] = value
defineReactive(data, key, value)
render()
return value
}
//使用
$set(data.arr, 0, 100)
複製代碼
模擬$delete
同樣的原理
function $delete(data, key){
if(Array.isArray(Data)){
data.splice(key, 1)
return
}
delete data[key]
render()
return
}
//數組使用
$delete(data.arr, 0)
console.log(data.arr)
//對象使用
$delete(data.shan, "name")
console.log(data.shan)
複製代碼
測試:
for(let i = 0; i < 10000; i++){
vm.msg = i
}
//若是每次改變數據都會從新渲染 那麼頁面至關於渲染了10000次 會卡死
//因此他其實是等循環執行結束以後 等到i變成9999了 再去渲染頁面
複製代碼
由此可得 vue會記錄改變的數據 把原來的數據和最後一次改變進行對比 不同就從新渲染 同樣就不渲染了
事實上 若是vue是同步更新dom的 那麼每一次更改數據就要渲染頁面 而操做dom實際上很是浪費性能 可是若是把渲染頁面做爲異步操做 他就能先執行完同步任務 等到同步任務改完數據了 而後再去任務隊列裏面把渲染頁面的任務拿出來執行 減小操做DOM的次數
所以vue更新DOM的操做是異步執行的
只要偵聽到數據變化 VUE將開啓一個異步隊列 即便一個數據被屢次改變 最終也只會把這個渲染任務推入到隊列中一次 這樣能夠避免沒必要要的計算和DOM操做
任務執行的流程: 同步執行棧執行完畢後 會執行異步隊列的任務 在異步隊列中先執行微任務再執行宏任務 若是用戶的瀏覽器支持微任務的話 VUE就把渲染頁面的函數放到微任務中去 若是不支持就只能放到宏任務裏面了
另外一種方式證實頁面是異步渲染的:
<div id="app">{{ msg }}</div>
複製代碼
const vm = new Vue({
el: '#app',
data: {
msg: '小餅'
}
})
vm.msg = '不知道說什麼好';
console.log(vm.msg); // 輸出不知道說什麼好 說明此時數據已更改
console.log(vm.$el.innerHTML); // 輸出小餅 說明此時頁面還未從新渲染
//在宏任務中去執行 這時候微任務確定執行完了
setTimeout(()=>{
console.log(vm.$el.innerHTML); // 輸出不知道說什麼好 說明此時頁面已從新渲染
})
複製代碼
先研究一下 明天再寫