100行代碼理解和分析vue2.0響應(yīng)式架構(gòu)
分享前啰嗦
我之前介紹過(guò)vue1.0如何實(shí)現(xiàn)observer和watcher。本想繼續(xù)寫下去,可是vue2.0橫空出世..所以直接看vue2.0吧。這篇文章在公司分享過(guò),終于寫出來(lái)了。我們采用用最精簡(jiǎn)的代碼,還原vue2.0響應(yīng)式架構(gòu)實(shí)現(xiàn)。
以前寫的那篇 vue 源碼分析之如何實(shí)現(xiàn) observer 和 watcher可以作為本次分享的參考。
不過(guò)不看也沒(méi)關(guān)系,但是最好了解下Object.defineProperty
本文分享什么
理解vue2.0的響應(yīng)式架構(gòu),就是下面這張圖

順帶介紹他比react快的其中一個(gè)原因
本分實(shí)現(xiàn)什么
const demo = new Vue({
data: {
text: "before",
},
//對(duì)應(yīng)的template 為 <div><span>{{text}}</span></div>
render(h){
return h('div', {}, [
h('span', {}, [this.__toString__(this.text)])
])
}
})
setTimeout(function(){
demo.text = "after"
}, 3000)
對(duì)應(yīng)的虛擬dom會(huì)從
<div><span>before</span></div> 變?yōu)?span style="color: #800000"> <div><span>after</span></div>
好,開(kāi)始吧?。?!
第一步,講data 下面所有屬性變?yōu)閛bservable
來(lái)來(lái)來(lái)先看代碼吧
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
observer(options.data, this._update)
this._update()
}
_update(){
this.$options.render()
}
}
function observer(value, cb){
Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
}
function defineReactive(obj, key, val, cb) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=>{},
set:newVal=> {
cb()
}
})
}
var demo = new Vue({
el: '#demo',
data: {
text: 123,
},
render(){
console.log("我要render了")
}
})
setTimeout(function(){
demo._data.text = 444
}, 3000)
為了好演示我們只考慮最簡(jiǎn)單的情況,如果看了vue 源碼分析之如何實(shí)現(xiàn)observer和watcher可能就會(huì)很好理解,不過(guò)沒(méi)關(guān)系,我們?nèi)詢烧Z(yǔ)再說(shuō)說(shuō),這段代碼要實(shí)現(xiàn)的功能就是將
var demo = new Vue({
el: '#demo',
data: {
text: 123,
},
render(){
console.log("我要render了")
}
})
中data 里面所有的屬性置于 observer,然后data里面的屬性,比如 text 以改變,就引起_update()函數(shù)調(diào)用進(jìn)而重新渲染,是怎樣做到的呢,我們知道其實(shí)就是賦值的時(shí)候就要改變對(duì)吧,當(dāng)我給data下面的text 賦值的時(shí)候 set 函數(shù)就會(huì)觸發(fā),這個(gè)時(shí)候 調(diào)用 _update 就ok了,但是
setTimeout(function(){
demo._data.text = 444
}, 3000)
demo._data.text沒(méi)有demo.text用著爽,沒(méi)關(guān)系,我們加一個(gè)代理
_proxy(key) {
const 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
}
})
}
然后在Vue的constructor加上下面這句
Object.keys(options.data).forEach(key => this._proxy(key))
第一步先說(shuō)到這里,我們會(huì)發(fā)現(xiàn)一個(gè)問(wèn)題,data中任何一個(gè)屬性的值改變,都會(huì)引起
_update的觸發(fā)進(jìn)而重新渲染,屬性這顯然不夠精準(zhǔn)啊
第二步,詳細(xì)闡述第一步為什么不夠精準(zhǔn)
比如考慮下面代碼
new Vue({
template: `
<div>
<section>
<span>name:</span> {{name}}
</section>
<section>
<span>age:</span> {{age}}
</section>
<div>`,
data: {
name: 'js',
age: 24,
height: 180
}
})
setTimeout(function(){
demo.height = 181
}, 3000)
template里面只用到了data上的兩個(gè)屬性name和age,但是當(dāng)我改變height的時(shí)候,用第一步的代碼,會(huì)不會(huì)觸發(fā)重新渲染?會(huì)!,但其實(shí)不需要觸發(fā)重新渲染,這就是問(wèn)題所在??!
第三步,上述問(wèn)題怎么解決
簡(jiǎn)單說(shuō)說(shuō)虛擬 DOM
首先,template最后都是編譯成render函數(shù)的(具體怎么做,就不展開(kāi)說(shuō)了,以后我會(huì)說(shuō)的),然后render 函數(shù)執(zhí)行完就會(huì)得到一個(gè)虛擬DOM,為了好理解我們寫寫最簡(jiǎn)單的虛擬DOM
function VNode(tag, data, children, text) {
return {
tag: tag,
data: data,
children: children,
text: text
}
}
class Vue {
constructor(options) {
this.$options = options
const vdom = this._update()
console.log(vdom)
}
_update() {
return this._render.call(this)
}
_render() {
const vnode = this.$options.render.call(this)
return vnode
}
__h__(tag, attr, children) {
return VNode(tag, attr, children.map((child)=>{
if(typeof child === 'string'){
return VNode(undefined, undefined, undefined, child)
}else{
return child
}
}))
}
__toString__(val) {
return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
}
}
var demo = new Vue({
el: '#demo',
data: {
text: "before",
},
render(){
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__toString__(this.text)])
])
}
})
我們運(yùn)行一下,他會(huì)輸出
{
tag: 'div',
data: {},
children:[
{
tag: 'span',
data: {},
children: [
{
children: undefined,
data: undefined,
tag: undefined,
text: '' // 正常情況為 字符串 before,因?yàn)槲覀優(yōu)榱搜菔揪筒粚懘淼拇a,所以這里為空
}
]
}
]
}
這就是 虛擬最簡(jiǎn)單虛擬DOM,tag是html 標(biāo)簽名,data 是包含諸如class和style這些標(biāo)簽上的屬性,childen就是子節(jié)點(diǎn),關(guān)于虛擬DOM就不展開(kāi)說(shuō)了。
回到開(kāi)始的問(wèn)題,也就是說(shuō),我得知道,render 函數(shù)里面依賴了vue實(shí)例里面哪些變量(只考慮render 就可以,因?yàn)閠emplate 也會(huì)是幫你編譯成render)。敘述有點(diǎn)拗口,還是看代碼吧
var demo = new Vue({
el: '#demo',
data: {
text: "before",
name: "123",
age: 23
},
render(){
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__toString__(this.text)])
])
}
})
就像這段代碼,render 函數(shù)里其實(shí)只依賴text,并沒(méi)有依賴name和age,所以,我們只要text改變的時(shí)候,我們自動(dòng)觸發(fā)render 函數(shù) 讓它生成一個(gè)虛擬DOM就ok了(剩下的就是這個(gè)虛擬DOM和上個(gè)虛擬DOM做比對(duì),然后操作真實(shí)DOM,只能以后再說(shuō)了),那么我們正式考慮一下怎么做
第三步,'touch' 拿到依賴
回到最上面那張圖,我們知道data上的屬性設(shè)置defineReactive后,修改data 上的值會(huì)觸發(fā)set。
那么我們?nèi)ata上值是會(huì)觸發(fā)get了。
對(duì),我們可以在上面做做手腳,我們先執(zhí)行一下render,我們看看data上哪些屬性觸發(fā)了get,我們豈不是就可以知道 render 會(huì)依賴data 上哪些變量了。
然后我么把這些變量做些手腳,每次這些變量變的時(shí)候,我們就觸發(fā)render。
上面這些步驟簡(jiǎn)單用四個(gè)子概括就是 計(jì)算依賴。
(其實(shí)不僅是render,任何一個(gè)變量的改別,是因?yàn)閯e的變量改變引起,都可以用上述方法,也就是computed 和 watch 的原理,也是mobx的核心)
第一步:
我們寫一個(gè)依賴收集的類,每一個(gè)data 上的對(duì)象都有可能被render函數(shù)依賴,所以每個(gè)屬性在defineReactive時(shí)候就初始化它,簡(jiǎn)單來(lái)說(shuō)就是這個(gè)樣子的。
class Dep {
constructor() {
this.subs = []
}
add(cb) {
this.subs.push(cb)
}
notify() {
console.log(this.subs);
this.subs.forEach((cb) => cb())
}
}
function defineReactive(obj, key, val, cb) {
const dep = new Dep()
Object.defineProperty(obj, key, {
// 省略
})
}
然后,當(dāng)執(zhí)行render 函數(shù)去'touch'依賴的時(shí)候,依賴到的變量get就會(huì)被執(zhí)行,然后我們就可以把這個(gè) render 函數(shù)加到 subs 里面去了。
當(dāng)我們,set 的時(shí)候 我們就執(zhí)行 notify 將所有的subs數(shù)組里的函數(shù)執(zhí)行,其中就包含render 的執(zhí)行。
至此就完成了整個(gè)圖,好我們將所有的代碼展示出來(lái)
function VNode(tag, data, children, text) {
return {
tag: tag,
data: data,
children: children,
text: text
}
}
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
Object.keys(options.data).forEach(key => this._proxy(key))
observer(options.data)
const vdom = watch(this, this._render.bind(this), this._update.bind(this))
console.log(vdom)
}
_proxy(key) {
const self = this
Object.defineProperty(self, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return self._data[key]
},
set: function proxySetter (val) {
self._data.text = val
}
})
}
_update() {
console.log("我需要更新");
const vdom = this._render.call(this)
console.log(vdom);
}
_render() {
return this.$options.render.call(this)
}
__h__(tag, attr, children) {
return VNode(tag, attr, children.map((child)=>{
if(typeof child === 'string'){
return VNode(undefined, undefined, undefined, child)
}else{
return child
}
}))
}
__toString__(val) {
return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
}
}
function observer(value, cb){
Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
}
function defineReactive(obj, key, val, cb) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=>{
if(Dep.target){
dep.add(Dep.target)
}
return val
},
set: newVal => {
if(newVal === val)
return
val = newVal
dep.notify()
}
})
}
function watch(vm, exp, cb){
Dep.target = cb
return exp()
}
class Dep {
constructor() {
this.subs = []
}
add(cb) {
this.subs.push(cb)
}
notify() {
this.subs.forEach((cb) => cb())
}
}
Dep.target = null
var demo = new Vue({
el: '#demo',
data: {
text: "before",
},
render(){
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__toString__(this.text)])
])
}
})
setTimeout(function(){
demo.text = "after"
}, 3000)
我們看一下運(yùn)行結(jié)果

好我們解釋一下 Dep.target 因?yàn)槲覀兊脜^(qū)分是,普通的get,還是在查找依賴的時(shí)候的get,所有我們?cè)诓檎乙蕾嚂r(shí)候,我們將
function watch(vm, exp, cb){
Dep.target = cb
return exp()
}
Dep.target 賦值,相當(dāng)于 flag 一下,然后 get 的時(shí)候
get: () => {
if (Dep.target) {
dep.add(Dep.target)
}
return val
},
判斷一下,就好了。到現(xiàn)在為止,我們?cè)倏茨菑垐D是不是就清楚很多了?
總結(jié)
我非常喜歡,vue2.0 以上代碼為了好展示,都采用最簡(jiǎn)單的方式呈現(xiàn)。
不過(guò)整個(gè)代碼執(zhí)行過(guò)程,甚至是命名方式都和vue2.0一樣。
對(duì)比react,vue2.0 自動(dòng)幫你監(jiān)測(cè)依賴,自動(dòng)幫你重新渲染,而react 要實(shí)現(xiàn)性能最大化,要做大量工作,比如我以前分享的:
react如何性能達(dá)到最大化(前傳),暨react為啥非得使用immutable.js
react 實(shí)現(xiàn)pure render的時(shí)候,bind(this)隱患。
而vue2.0 天然幫你做到了最優(yōu),而且對(duì)于像萬(wàn)年不變的 如標(biāo)簽上靜態(tài)的class屬性,vue2.0 在重新渲染后做diff 的時(shí)候是不比較的,vue2.0比達(dá)到性能最大化的react 還要快的一個(gè)原因。
然后源碼在此,喜歡的記得給個(gè)star 哦
后續(xù),我會(huì)簡(jiǎn)單聊聊,vue2.0的diff。
本文已被整理到了《Vue.js前端組件學(xué)習(xí)教程》,歡迎大家學(xué)習(xí)閱讀。
關(guān)于vue.js組件的教程,請(qǐng)大家點(diǎn)擊專題vue.js組件學(xué)習(xí)教程進(jìn)行學(xué)習(xí)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
使用Vue3+ElementPlus前端實(shí)現(xiàn)分片上傳的全過(guò)程
ElementPlus是一套為開(kāi)發(fā)者、設(shè)計(jì)師和產(chǎn)品經(jīng)理準(zhǔn)備的基于Vue?3.0的組件庫(kù),提供了配套設(shè)計(jì)資源,幫助你的網(wǎng)站快速成型,下面這篇文章主要給大家介紹了關(guān)于使用Vue3+ElementPlus前端實(shí)現(xiàn)分片上傳的相關(guān)資料,需要的朋友可以參考下2022-11-11
Vue模仿ElementUI的form表單實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于Vue模仿ElementUI的form表單的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03
vue實(shí)現(xiàn)指定日期之間的倒計(jì)時(shí)
這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)指定日期之間的倒計(jì)時(shí),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-05-05
使用props傳值時(shí)無(wú)法在mounted處理的解決方案
這篇文章主要介紹了使用props傳值時(shí)無(wú)法在mounted處理的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-04-04
Vue+Echarts實(shí)現(xiàn)繪制動(dòng)態(tài)折線圖
這篇文章主要為大家詳細(xì)介紹了如何利用Vue和Echarts實(shí)現(xiàn)繪制動(dòng)態(tài)折線圖,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-03-03
vue項(xiàng)目移動(dòng)端實(shí)現(xiàn)ip輸入框問(wèn)題
這篇文章主要介紹了vue項(xiàng)目移動(dòng)端實(shí)現(xiàn)ip輸入框問(wèn)題,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-03-03
詳解mpvue實(shí)現(xiàn)對(duì)蘋果X安全區(qū)域的適配
這篇文章主要介紹了詳解mpvue實(shí)現(xiàn)對(duì)蘋果X安全區(qū)域的適配,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07

