>[success] # 響應式模型
1. 從代碼運行角度來看這個問題,如果代碼正常執行的順序來說,JavaScript是程序性的,不是反應性的
2. vue2 利用 `Object.defineProperty` 進行收集「**依賴收集**」的**響應劫持**使得程序成為了具備反應性功能
>[info] ## js 執行角度例子
1. 下面正常的js邏輯代碼是否能像vue一樣,當price 發生改變期待著total 也可以重新計算,但實際結果卻是打印出來的結果為10 而不是12
2. JavaScript是程序性的,**不是反應性**的,為了使total反應性,我們必須使用JavaScript使事物表現不同
~~~
let price = 5
let quantity = 2
let total = price * quantity
price++
console.log(`total is ${total}`) // 10
~~~
>[danger] ##### 讓程序具備響應性
total 可以在每次price 發生變化后**接著調用**,就可以實現**響應式的效果**
~~~
let price = 5
let quantity = 2
let total = price * quantity
price++
total = price * quantity
console.log(`total is ${total}`) // 12
~~~
讓程序具備響應式,就是在指定位置去觸發效果的改變,先收集行為,在指定觸發即可
~~~
/**
* 設計思路,記錄操作步驟=》在某些觸發節點觸發記錄步驟=》數據改變
*/
let price = 5
let quantity = 2
let total = 0
// 收集步驟的空間
const storage = []
// 收集步驟方法
function record(step) {
storage.push(step)
}
// 觸發步驟方法
function replay() {
storage.forEach((run) => run())
}
// 定義一些需要進行步驟方法例如 價格*數量 - 固定金額 為了方便演示拆兩個方法
const all = () => {
total = price * quantity
}
const subtraction = () => {
total = total - 8
}
// 第一次收集需要執行步驟形式
record(all)
record(subtraction)
// 收集完開始觸發
replay()
console.log(total) // 2
// 再次觸發需要調用改變數據行為的方法
price++ // 改變價格
replay()
console.log(total) // 4
~~~
* 發布訂閱來優化代碼
~~~
// 發布者
class Dep {
// 初始化構造收集
constructor() {
this.sub = []
}
// 收集
addSub(target) {
this.sub.push(target)
}
// 觸發
notify() {
this.sub.forEach((fun) => {
fun()
})
}
}
// 訂閱(觀察者)
function watcher(myFunc) {
target = myFunc // 在接受到調用者指令時候執行的回調函數
dep.addSub(target)
target() // 上來先調用一次
target = null
}
const dep = new Dep()
let price = 5
let quantity = 2
let total = 0
watcher(() => {
total = price * quantity
})
console.log(`total is ${total}`) // 10
price++
dep.notify()
console.log(`total is ${total}`) // 12
~~~
>[info] ## 利用 defineProperty 解決 手動觸發
上面問題是需要每次自己找到觸發點去手動觸發,隨著邏輯越來越多這種穿插的行為會越來越多,需要找到一個方法可以幫助我們自動去觸發我們注冊想執行的邏輯,,vue2.0采用了
`Object.defineProperty`這是一個函數,它允許我們為屬性**定義getter和setter函數**
~~~
let person = {
name: 'w',
}
Object.defineProperty(person, 'name', {
get() {
console.log('調用時候執行')
},
set(val) {
console.log('賦值時候執行')
}
})
person.name // 執行get
person.name = 'y' // 這里執行set
~~~
現在可以利用對**數據變化進行監控**,這樣就可以做到對應觸發**發布者**
~~~
type VueOpt = {
data: Record<any, any>
}
function cb(v: any) {
console.log('更新')
}
class Vue {
_data: Record<any, any>
constructor(opt: VueOpt) {
this._data = opt.data
this.observer(this._data)
}
// observer 觀察者
observer(data: Record<any, any>) {
if (!data || typeof data !== 'object') return
Object.keys(data).forEach((key) => {
this.defineReactive(data, key, data[key])
})
}
// 轉換賦值Object.defineProperty
defineReactive(data: Record<any, any>, key: string, value: any) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
return value
},
set(v) {
if (v === data[key]) return
value = v
// 簡單相應攔截
cb(v)
},
})
}
}
const vue = new Vue({
data: {
test: '測試',
},
})
vue._data.test = '123'
console.log(vue._data.test)
~~~
>[danger] ##### 簡單的渲染案例
1. 這里做個說明Object.defineProperty 對屬性進行get 和 set 是時候,在沒有調用情況僅僅只是一個綁定。
2. 僅僅是綁定因此'**watcher**' 先觸發有了統一的**target
**
3. 這個案例就說明了抽離觀察這和訂閱者的好處 可以通過外部去控制
~~~
<!DOCTYPE html>
<html lang="en" style="height: 100%;">
<body style="height: 100%;">
<div id="app">hello</div>
<div id="app1">hello</div>
<button id="changeViwe">觸發視圖更新 </button>
<button id="changeViwe1">觸發視圖更新1 </button>
</body>
<script>
/**
* 描述 響應代理
* @param {Object} vm 代理后的對象
* @param {Object} data 被代理的對象
* @returns {any}
*/
function proxy(vm,data){
Object.keys(data).forEach(key => {
// 注冊觀察者
const dep = new Dep()
Object.defineProperty(vm,key,{
enumerable:true,
configurable:true,
get(){
console.log(1234);
// 在get 時候記錄這個訂閱
// Object.defineProperty 在第一次注冊的時候是不會執行get的
// 因此其實第一遍給每個屬性綁定上get 和set時候只是注冊了 dep 觀察者對象,并沒有執行內部get 和set 方法
// 所以也就并沒有往里面填入實際的觀察方法
dep.depend()
return data[key];
},
set(val){
console.log(1234);
if(val===data[key]) return;
data[key] = val;
// 原來簡易版本的執行是寫死的只能固定某個位置渲染
// document.getElementById('app').textContent = Object.values(data).join()
// 現在利用觀察者模型在dep.depend() 去注冊了 外部希望執行的回調函數代碼,代碼靈活
dep.notify() // 在賦值時候觸發訂閱動作
},
})
})
}
/**
* 描述 發布者 -- 主要是用來收集,和統一執行被收集來的訂閱者
*/
class Dep{
constructor(){
this.subs = []
}
// 這里有點區別之前的做的觀察者模型,那時候是有個函數參數
// 這個函數參數通過調用depend 注冊
// 但是現在觸發他的方法被在Object.defineProperty get 提前聲明
// 你在最初的時候不知道數據會根據什么數據動態變化
// 因此需要更為動態的形式去傳遞 方法
depend(){
if(target && !this.subs.includes(target)){
this.subs.push(target)
}
}
// 去調用
notify(msg){
const { length } = this.subs
length && this.subs.forEach(fun=>fun())
}
}
/**
* 描述 訂閱者
* @param {Function} fun 執行的訂閱回調函數
*/
function watcher(fun){
target = fun
// 最開始的進階案例我們在 wather 這個函數調用了 Dep 中depend 來進行觀察的注冊
// 現在 因為通過Object.defineProperty 做了響應模型,這樣get 時候就能夠自動
// 幫助我們注冊了 Dep 中depend 來進行觀察
// 因此只用當fun 這個回調方法中使用了Object.defineProperty 包裹的屬性才會觸發
// 其實就是為解決在get 方法中自動添加監聽回調觸發
fun()
target = null
}
const data = {msg:"123",age:123}
const vm = {}
proxy(vm,data)
/**
* 描述 id為app 的dom 節點插入內容
*/
function appendAppRoot(){
document.getElementById('app').textContent = vm.msg + vm.age
}
/**
* 描述 id為app1 的dom 節點插入內容
*/
function appendApp1Root(){
document.getElementById('app1').textContent = vm.msg + vm.age
}
watcher(
// 更加動態的在外部執行訂閱的方法
// watcher 內部會自調用 回調函數,回調函數中 vm.msg + vm.age 這兩段就會觸發get
// 形成訂閱
appendAppRoot
)
document.getElementById('changeViwe').onclick = function(){
// 當賦值的時候觸發set ,set 會觸發在調用watcher 時候注冊進入觀者某型中subs里面的回調
vm.msg = "測試"
}
watcher(
// 更加動態的在外部執行訂閱的方法
appendApp1Root
)
document.getElementById('changeViwe1').onclick = function(){
vm.msg = "測試666"
}
</script>
</html>
~~~
>[danger] ##### 參考
[在 Vue Mastery 觀看視頻講解](https://www.vuemastery.com/courses/advanced-components/build-a-reactivity-system "Vue Reactivity")
[關于 Object.definePropert 可以看我另外一篇文章](http://www.hmoore.net/cyyspring/more/1246692)
- 工程化 -- Node
- vscode -- 插件
- vscode -- 代碼片段
- 前端學會調試
- 谷歌瀏覽器調試技巧
- 權限驗證
- 包管理工具 -- npm
- 常見的 npm ci 指令
- npm -- npm install安裝包
- npm -- package.json
- npm -- 查看包版本信息
- npm - package-lock.json
- npm -- node_modules 層級
- npm -- 依賴包規則
- npm -- install 安裝流程
- npx
- npm -- 發布自己的包
- 包管理工具 -- pnpm
- 模擬數據 -- Mock
- 頁面渲染
- 渲染分析
- core.js && babel
- core.js -- 到底是什么
- 編譯器那些術語
- 詞法解析 -- tokenize
- 語法解析 -- ast
- 遍歷節點 -- traverser
- 轉換階段、生成階段略
- babel
- babel -- 初步上手之了解
- babel -- 初步上手之各種配置(preset-env)
- babel -- 初步上手之各種配置@babel/helpers
- babel -- 初步上手之各種配置@babel/runtime
- babel -- 初步上手之各種配置@babel/plugin-transform-runtime
- babel -- 初步上手之各種配置(babel-polyfills )(未來)
- babel -- 初步上手之各種配置 polyfill-service
- babel -- 初步上手之各種配置(@babel/polyfill )(過去式)
- babel -- 總結
- 各種工具
- 前端 -- 工程化
- 了解 -- Yeoman
- 使用 -- Yeoman
- 了解 -- Plop
- node cli -- 開發自己的腳手架工具
- 自動化構建工具
- Gulp
- 模塊化打包工具為什么出現
- 模塊化打包工具(新) -- webpack
- 簡單使用 -- webpack
- 了解配置 -- webpack.config.js
- webpack -- loader 淺解
- loader -- 配置css模塊解析
- loader -- 圖片和字體(4.x)
- loader -- 圖片和字體(5.x)
- loader -- 圖片優化loader
- loader -- 配置解析js/ts
- webpack -- plugins 淺解
- eslit
- plugins -- CleanWebpackPlugin(4.x)
- plugins -- CleanWebpackPlugin(5.x)
- plugin -- HtmlWebpackPlugin
- plugin -- DefinePlugin 注入全局成員
- webapck -- 模塊解析配置
- webpack -- 文件指紋了解
- webpack -- 開發環境運行構建
- webpack -- 項目環境劃分
- 模塊化打包工具 -- webpack
- webpack -- 打包文件是個啥
- webpack -- 基礎配置項用法
- webpack4.x系列學習
- webpack -- 常見loader加載器
- webpack -- 移動端px轉rem處理
- 開發一個自己loader
- webpack -- plugin插件
- webpack -- 文件指紋
- webpack -- 壓縮css和html構建
- webpack -- 清里構建包
- webpack -- 復制靜態文件
- webpack -- 自定義插件
- wepack -- 關于靜態資源內聯
- webpack -- source map 對照包
- webpack -- 環境劃分構建
- webpack -- 項目構建控制臺輸出
- webpack -- 項目分析
- webpack -- 編譯提速優護體積
- 提速 -- 編譯階段
- webpack -- 項目優化
- webpack -- DefinePlugin 注入全局成員
- webpack -- 代碼分割
- webpack -- 頁面資源提取
- webpack -- import按需引入
- webpack -- 搖樹
- webpack -- 多頁面打包
- webpack -- eslint
- webpack -- srr打包后續看
- webpack -- 構建一個自己的配置后續看
- webpack -- 打包組件和基礎庫
- webpack -- 源碼
- webpack -- 啟動都做了什么
- webpack -- cli做了什么
- webpack - 5
- 模塊化打包工具 -- Rollup
- 工程化搭建代碼規范
- 規范化標準--Eslint
- eslint -- 擴展配置
- eslint -- 指令
- eslint -- vscode
- eslint -- 原理
- Prettier -- 格式化代碼工具
- EditorConfig -- 編輯器編碼風格
- 檢查提交代碼是否符合檢查配置
- 整體流程總結
- 微前端
- single-spa
- 簡單上手 -- single-spa
- 快速理解systemjs
- single-sap 不使用systemjs
- monorepo -- 工程
- Vue -- 響應式了解
- Vue2.x -- 源碼分析
- 發布訂閱和觀察者模式
- 簡單 -- 了解響應式模型(一)
- 簡單 -- 了解響應式模型(二)
- 簡單 --了解虛擬DOM(一)
- 簡單 --了解虛擬DOM(二)
- 簡單 --了解diff算法
- 簡單 --了解nextick
- Snabbdom -- 理解虛擬dom和diff算法
- Snabbdom -- h函數
- Snabbdom - Vnode 函數
- Snabbdom -- init 函數
- Snabbdom -- patch 函數
- 手寫 -- 虛擬dom渲染
- Vue -- minVue
- vue3.x -- 源碼分析
- 分析 -- reactivity
- 好文
- grpc -- 瀏覽器使用gRPC
- grcp-web -- 案例
- 待續