首發(fā)于SirM2z的博客
配合ppt食用更佳
實現(xiàn)的最終目標(biāo)
const demo = new Vue({
data: {
text: "before",
},
// 對應(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)
對應(yīng)的虛擬DOM會從
<div><span>before</span></div>
變成
<div><span>after</span></div>
第一步,監(jiān)聽data下邊的所有屬性畦徘,轉(zhuǎn)換為響應(yīng)式
思路
- 當(dāng)data下的某個屬性變化時担扑,如何觸發(fā)相應(yīng)的函數(shù)聋涨?
方案:ES5中新添加了一個方法:Object.defineProperty,通過這個方法溉躲,可以自定義getter
和setter
函數(shù)榜田,那么在獲取對象屬性或者設(shè)置對象屬性時就能夠執(zhí)行相應(yīng)的回調(diào)函數(shù)
代碼如下:
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
observer(options.data, this._update.bind(this))
this._update()
}
_update(){
this.$options.render()
}
}
function observer(obj, cb) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key], cb)
})
}
function defineReactive(obj, key, val, cb) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
console.log('你訪問了' + key)
return val
},
set: newVal => {
if (newVal === val)
return
console.log('你設(shè)置了' + key)
console.log('新的' + key + ' = ' + newVal)
val = newVal
cb()
}
})
}
var demo1 = new Vue({
el: '#demo',
data: {
text: "before"
},
render(){
console.log("我要render了")
}
})
- 引發(fā)了第二個問題,如果
data
中的屬性是一個對象還能觸發(fā)我們的回掉函數(shù)么签财?比如說下邊的demo
var demo2 = new Vue({
el: '#demo',
data: {
text: "before",
o: {
text: "o-before"
}
},
render(){
console.log("我要render了")
}
})
方案:用遞歸完善上邊的響應(yīng)式串慰,需要在它開始對屬性進(jìn)行響應(yīng)式轉(zhuǎn)換的時候,前邊加個判斷唱蒸,即如下
function observer(obj) {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'object') {
new observer(obj[key], cb)
}
defineReactive(obj, key, obj[key])
})
}
- 實際寫的過程中發(fā)現(xiàn)調(diào)用data的屬性時需要這樣寫
demo._data.text
邦鲫,肯定是沒有demo.text
這樣寫來的方便,所以就需要加一層代理進(jìn)行轉(zhuǎn)換
代碼如下:
_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
}
})
}
然后在構(gòu)造函數(shù)中加上這么一句話
Object.keys(options.data).forEach(key => this._proxy(key))
到此神汹,我們的data
屬性已經(jīng)變?yōu)轫憫?yīng)式的了庆捺,只要data
的屬性發(fā)生變化,那么就會觸發(fā)render
函數(shù)屁魏。這也是為什么只有vue組件中的data
屬性才是響應(yīng)式的滔以,其他地方聲明的值均不是響應(yīng)式的原因。但是這里有個問題氓拼,即觸發(fā)render
函數(shù)的準(zhǔn)確度問題你画!
第二步,解決準(zhǔn)確度問題桃漾,引出虛擬dom
比如下邊的demo
new Vue({
template: `
<div>
<span>name:</span> {{name}}
<div>`,
data: {
name: 'js',
age: 24
}
})
setTimeout(function(){
demo.age = 25
}, 3000)
template
中只用到了data
中的name
屬性坏匪,但是當(dāng)修改age
屬性的時候,會不會觸發(fā)渲染呢撬统?答案是:會适滓。但實際是不需要觸發(fā)渲染機(jī)制的
解決這個問題,先要簡單說下虛擬dom恋追。vue有兩種寫法:
// template模板寫法(最常用的)
new Vue({
data: {
text: "before",
},
template: `
<div>
<span>text:</span> {{text}}
</div>`
})
// render函數(shù)寫法凭迹,類似react的jsx寫法
new Vue({
data: {
text: "before",
},
render (h) {
return (
<div>
<span>text:</span> {{text}}
</div>
)
}
})
由于vue2.x引入了虛擬dom的原因,這兩種寫法最終都會被解析成虛擬dom苦囱,但在這之前嗅绸,他們會先被解析函數(shù)轉(zhuǎn)換成同一種表達(dá)方式,即如下:
new Vue({
data: {
text: "before",
},
render(){
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__toString__(this.text)])
])
}
})
透過上邊的render
函數(shù)中的this.__h__
方法沿彭,可以簡單了解下虛擬dom
function VNode(tag, data, children, text) {
return {
tag: tag, // html標(biāo)簽名
data: data, // 包含諸如 class 和 style 這些標(biāo)簽上的屬性
children: children, // 子節(jié)點
text: text // 文本節(jié)點
}
}
寫一個簡單的虛擬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)])
])
}
})
回頭看問題朽砰,也就是說,我需要知道render
函數(shù)中依賴了data
中的哪些屬性,只有這些屬性變化瞧柔,才需要去觸發(fā)render
函數(shù)
第三步漆弄,依賴收集,準(zhǔn)確渲染
思路:在這之前造锅,我們已經(jīng)把data
中的屬性改成響應(yīng)式了撼唾,當(dāng)去獲取或者修改這些變量時便能夠觸發(fā)相應(yīng)函數(shù)。那這里就可以利用這個相應(yīng)的函數(shù)做些手腳了哥蔚。當(dāng)聲明一個vue對象時倒谷,在執(zhí)行render
函數(shù)獲取虛擬dom的這個過程中,已經(jīng)對render
中依賴的data
屬性進(jìn)行了一次獲取操作糙箍,這次獲取操作便可以拿到所有依賴渤愁。
其實不僅是render
,任何一個變量的改別深夯,是因為別的變量改變引起抖格,都可以用上述方法,也就是computed
和watch
的原理
首先需要寫一個依賴收集的類咕晋,每一個data
中的屬性都有可能被依賴雹拄,因此每個屬性在響應(yīng)式轉(zhuǎn)化(defineReactive
)的時候,就初始化它掌呜。代碼如下:
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, {
// 省略
})
}
那么執(zhí)行過程就是:
- 當(dāng)執(zhí)行
render
函數(shù)的時候滓玖,依賴到的變量的get
就會被執(zhí)行,然后就把這個render
函數(shù)加到subs
里面去质蕉。 - 當(dāng)
set
的時候,就執(zhí)行notify
势篡,將所有的subs
數(shù)組里的函數(shù)執(zhí)行,其中就包含render
的執(zhí)行模暗。
注:代碼中有一個
Dep.target
值殊霞,這個值時用來區(qū)分是普通的get
還是收集依賴時的get
最后完整代碼如下:
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[key] = 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(obj) {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'object') {
new observer(obj[key])
}
defineReactive(obj, key, obj[key])
})
}
function defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
if (Dep.target) {
dep.add(Dep.target)
Dep.target = null
}
console.log('你訪問了' + key)
return val
},
set: newVal => {
if (newVal === val)
return
console.log('你設(shè)置了' + key)
console.log('新的' + key + ' = ' + newVal)
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",
test: {
a: '1'
},
t: 1
},
render() {
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__toString__(this.text)]),
this.__h__('span', {}, [this.__toString__(this.test.a)])
])
}
})
vue react響應(yīng)式簡單對比
綜上發(fā)現(xiàn),利用Object.defineProperty
這個特性可以精確的寫出訂閱發(fā)布模式汰蓉,從這點來說,vue
是優(yōu)于react
的棒卷,在沒經(jīng)過優(yōu)化之前顾孽,vue
的渲染機(jī)制一定是比react
更加準(zhǔn)確的,為了驗證這一說法比规,我用兩個框架同時寫了兩個相同的簡單項目進(jìn)行對比若厚。
沒有對比就沒有傷害:
- react項目地址:http://sirm2z.github.io/react-vue-test/react/index.html
react 結(jié)果 - vue項目地址:http://sirm2z.github.io/react-vue-test/vue/index.html
vue 結(jié)果
通過對比發(fā)現(xiàn),react
在正常使用的過程中產(chǎn)生了多余的渲染蜒什,在移動端或者組件嵌套非常深的情況下會產(chǎn)生非常大的性能消耗测秸,因此在使用react
的過程中,寫好react
生命周期中的shouldComponentUpdate
是非常重要的!