vue如何實現(xiàn)observer和watcher源碼解析
本文能幫你做什么?好奇vue雙向綁定的同學(xué),可以部分緩解好奇心,還可以幫你了解如何實現(xiàn)$watch。
前情回顧
我之前寫了一篇沒什么干貨的文章,并且刨了一個大坑。
今天,打算來填一天,并再刨一個。
不過話說說回來了,看本文之前,如果不知道Object.defineProperty,還必須看看解析神奇的Object.defineProperty
不得不感慨vue的作者,人長得帥,碼寫的也好,本文是根據(jù)作者源碼,摘取出來的
本文將實現(xiàn)什么
正如上一篇許下的承諾一樣,本文要實現(xiàn)一個$wacth
const v = new Vue({
data:{
a:1,
b:2
}
})
v.$watch("a",()=>console.log("哈哈,$watch成功"))
setTimeout(()=>{
v.a = 5
},2000) //打印 哈哈,$watch成功
為了幫助大家理清思路。。我們就做最簡單的實現(xiàn)。。只考慮對象不考慮數(shù)組
1. 實現(xiàn) observer
思路:我們知道Object.defineProperty的特性了,我們就利用它的set和get。我們將要observe的對象,通過遞歸,將它所有的屬性,包括子屬性的屬性,都給加上set和get。這樣的話,給這個對象的某個屬性賦值,就會觸發(fā)set。開始吧
export default class Observer{
constructor(value) {
this.value = value
this.walk(value)
}
//遞歸。。讓每個字屬性可以observe
walk(value){
Object.keys(value).forEach(key=>this.convert(key,value[key]))
}
convert(key, val){
defineReactive(this.value, key, val)
}
}
export function defineReactive (obj, key, val) {
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=>val,
set:newVal=> {
childOb = observe(newVal)//如果新賦值的值是個復(fù)雜類型。再遞歸它,加上set/get。。
}
})
}
export function observe (value, vm) {
if (!value || typeof value !== 'object') {
return
}
return new Observer(value)
}
代碼很簡單,就給每個屬性(包括子屬性)都加上get/set,這樣的話,這個對象的,有任何賦值,就會觸發(fā)set方法。。
所以,我們是不是應(yīng)該寫一個消息-訂閱器呢?
這樣的話,一觸發(fā)set方法,我們就發(fā)一個通知出來,然后,訂閱這個消息的,就會怎樣?對咯。、收到消息、觸發(fā)回調(diào)。
2. 消息-訂閱器
很簡單,我們維護一個數(shù)組,,這個數(shù)組,就放訂閱著,一旦觸發(fā)notify,訂閱者就調(diào)用自己的update方法
export default class Dep {
constructor() {
this.subs = []
}
addSub(sub){
this.subs.push(sub)
}
notify(){
this.subs.forEach(sub=>sub.update())
}
}
所以,每次set函數(shù),調(diào)用的時候,我們是不是應(yīng)該,觸發(fā)notify,對吧。所以我們把代碼補充完整
export function defineReactive (obj, key, val) {
var dep = new Dep()
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=>val,
set:newVal=> {
var value = val
if (newVal === value) {
return
}
val = newVal
childOb = observe(newVal)
dep.notify()
}
})
}
那么問題來了。誰是訂閱者。對,是Watcher。一旦 dep.notify()就遍歷訂閱者,也就是Watcher,并調(diào)用他的update()方法
3. 實現(xiàn)一個Watcher
我們想象這個Watcher,應(yīng)該用什么東西。update方法,嗯這個毋庸置疑,還有呢。
v.$watch("a",()=>console.log("哈哈,$watch成功"))
對表達式(就是那個“a”) 和 回調(diào)函數(shù),這是最基本的,所以我們簡單寫寫
export default class Watcher {
constructor(vm, expOrFn, cb) {
this.cb = cb
this.vm = vm
//此處簡化.要區(qū)分fuction還是expression,只考慮最簡單的expression
this.expOrFn = expOrFn
this.value = this.get()
}
update(){
this.run()
}
run(){
const value = this.get()
if(value !==this.value){
this.value = value
this.cb.call(this.vm)
}
}
get(){
//此處簡化。。要區(qū)分fuction還是expression
const value = this.vm._data[this.expOrFn]
return value
}
}
那么問題來了,我們怎樣將通過addSub(),將Watcher加進去呢。
我們發(fā)現(xiàn)var dep = new Dep() 處于閉包當(dāng)中,我們又發(fā)現(xiàn)Watcher的構(gòu)造函數(shù)里會調(diào)用this.get,所以,我們可以在上面動動手腳,修改一下Object.defineProperty的get要調(diào)用的函數(shù),判斷是不是Watcher的構(gòu)造函數(shù)調(diào)用,如果是,說明他就是這個屬性的訂閱者,果斷將他addSub()中去,那問題來了?
我怎樣判斷他是Watcher的this.get調(diào)用的,而不是我們普通調(diào)用的呢。
對,在Dep定義一個全局唯一的變量,跟著思路我們寫一下
export default class Watcher {
....省略未改動代碼....
get(){
Dep.target = this
//此處簡化。。要區(qū)分fuction還是expression
const value = this.vm._data[this.expOrFn]
Dep.target = null
return value
}
}
這樣的話,我們只需要在Object.defineProperty的get要調(diào)用的函數(shù)里,判斷有沒有值,就知道到底是Watcher 在get,還是我們自己在查看賦值,如果是Watcher的話就addSub(),代碼補充一下
export function defineReactive (obj, key, val) {
var dep = new Dep()
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=>{
// 說明這是watch 引起的
if(Dep.target){
dep.addSub(Dep.target)
}
return val
},
set:newVal=> {
var value = val
if (newVal === value) {
return
}
val = newVal
childOb = observe(newVal)
dep.notify()
}
})
}
最后不要忘記,在Dep.js中加上這么一句
Dep.target = null
4. 實現(xiàn)一個 Vue
還差一步就大功告成了,我們要把以上代碼配合Vue的$watch方法來用,要watch Vue實例的屬性,算了,不要理會我在說什么,直接看代碼吧
import Watcher from '../watcher'
import {observe} from "../observer"
export default class Vue {
constructor (options={}) {
//這里簡化了。。其實要merge
this.$options=options
//這里簡化了。。其實要區(qū)分的
let data = this._data=this.$options.data
Object.keys(data).forEach(key=>this._proxy(key))
observe(data,this)
}
$watch(expOrFn, cb, options){
new Watcher(this, expOrFn, cb)
}
_proxy(key) {
var self = this
Object.defineProperty(self, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return self._data[key]
},
set: function proxySetter (val) {
self._data[key] = val
}
})
}
}
非常簡單。兩件事,observe自己的data,代理自己的data,使訪問自己的屬性,就是訪問子data的屬性。。
截止到現(xiàn)在,在我們只考慮最簡單情況下,整個流程終于跑通了??隙〞泻芏郻ug,本文主要目的是展示整個工作流,幫助讀者理解。
代碼在https://github.com/georgebbbb...,
我是一萬個不想展示自己代碼,因為很多槽點,還請見諒
下一篇,有兩個方向,將聊一聊如何實現(xiàn)雙向綁定,或者是如何watch數(shù)組。
關(guān)于vue2.0的新文章
100行代碼,理解和分析vue2.0的響應(yīng)式架構(gòu)
本文已被整理到了《Vue.js前端組件學(xué)習(xí)教程》,歡迎大家學(xué)習(xí)閱讀。
關(guān)于vue.js組件的教程,請大家點擊專題vue.js組件學(xué)習(xí)教程進行學(xué)習(xí)。
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
element-plus dialog v-loading不生效問題及解決
這篇文章主要介紹了element-plus dialog v-loading不生效問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03
Jenkins?Sidebar?Link插件實現(xiàn)添加側(cè)邊欄功能詳解
這篇文章主要介紹了vue框架實現(xiàn)添加側(cè)邊欄,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12
vue獲取v-for異步數(shù)據(jù)dom的解決問題
這篇文章主要介紹了vue獲取v-for異步數(shù)據(jù)dom的解決問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-03-03
淺析vue-router中params和query的區(qū)別
這篇文章主要介紹了vue-router中params和query的區(qū)別,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-12-12
解決找不到模塊“xxx.vue”或其相應(yīng)的類型聲明問題
這篇文章主要介紹了解決找不到模塊“xxx.vue”或其相應(yīng)的類型聲明問題,具有很好的參考價值,希望對大家有所幫助。2022-10-10
vue css 引入asstes中的圖片無法顯示的四種解決方法
這篇文章主要介紹了vue css 引入asstes中的圖片 無法顯示的幾種解決方案,本文給出了四種解決方法,每種方法給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-03-03

