分享前啰嗦
我之前介紹過vue1.0如何實作observer和watcher。本想繼續寫下去,可是vue2.0橫空出世..是以
直接看vue2.0吧。這篇文章在公司分享過,終于寫出來了。我們采用用最精簡的代碼,還原vue2.0響應式架構實作
以前寫的那篇 vue 源碼分析之如何實作 observer 和 watcher可以作為本次分享的參考。
不過不看也沒關系,但是最好了解下object.defineproperty
本文分享什麼
了解vue2.0的響應式架構,就是下面這張圖
順帶介紹他比react快的其中一個原因
本分實作什麼
const demo = new vue({
data: {
text: "before",
},
//對應的template 為 <div><span>{{text}}</span></div>
render(h){
return h('div', {}, [
h('span', {}, [this.__tostring__(this.text)])
])
}
})
settimeout(function(){
demo.text = "after"
}, 3000)
對應的虛拟dom會從
<div><span>before</span></div> 變為 <div><span>after</span></div>
好,開始吧!!!
第一步, 講data 下面所有屬性變為observable
來來來先看代碼吧
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)
為了好示範我們隻考慮最簡單的情況,如果看了vue 源碼分析之如何實作 observer 和 watcher可能就會很好了解,不過沒關系,我們三言兩語再說說,這段代碼要實作的功能就是将
var demo = new vue({
el: '#demo',
data: {
text: 123,
},
render(){
console.log("我要render了")
}
})
中data 裡面所有的屬性置于 observer,然後data裡面的屬性,比如 text
以改變,就引起_update()函數調用進而重新渲染,是怎樣做到的呢,我們知道其實就是指派的時候就要改變對吧,當我給data下面的text
指派的時候 set 函數就會觸發,這個時候 調用_update 就ok了,但是
settimeout(function(){
demo._data.text = 444
}, 3000)
demo._data.text沒有demo.text用着爽,沒關系,我們加一個代理
_proxy(key) {
const self = this
object.defineproperty(self, key, {
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))
第一步先說到這裡,我們會發現一個問題,data中任何一個屬性的值改變,都會引起
_update的觸發進而重新渲染,屬性這顯然不夠精準啊
第二步,詳細闡述第一步為什麼不夠精準
比如考慮下面代碼
new vue({
template: `
<div>
<section>
<span>name:</span> {{name}}
</section>
<span>age:</span> {{age}}
<div>`,
name: 'js',
age: 24,
height: 180
})
settimeout(function(){
demo.height = 181
}, 3000)
template裡面隻用到了data上的兩個屬性name和age,但是當我改變height的時候,用第一步的代碼,會不會觸發重新渲染?會!,但其實不需要觸發重新渲染,這就是問題所在!!
第三步,上述問題怎麼解決
簡單說說虛拟 dom
首先,template最後都是編譯成render函數的(具體怎麼做,就不展開說了,以後我會說的),然後render 函數執行完就會得到一個虛拟dom,為了好了解我們寫寫最簡單的虛拟dom
function vnode(tag, data, children, text) {
return {
tag: tag,
data: data,
children: children,
text: text
class vue {
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);
text: "before",
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__tostring__(this.text)])
])
})
我們運作一下,他會輸出
{
tag: 'div',
data: {},
children:[
{
tag: 'span',
data: {},
children: [
{
children: undefined,
data: undefined,
tag: undefined,
text: '' // 正常情況為 字元串 before,因為我們為了示範就不寫代理的代碼,是以這裡為空
}
]
]
這就是 虛拟最簡單虛拟dom,tag是html 标簽名,data 是包含諸如 class 和 style 這些标簽上的屬性,childen就是子節點,關于虛拟dom就不展開說了。
回到開始的問題,也就是說,我得知道,render 函數裡面依賴了vue執行個體裡面哪些變量(隻考慮render 就可以,因為template 也會是幫你編譯成render)。叙述有點拗口,還是看代碼吧
name: "123",
age: 23
就像這段代碼,render 函數裡其實隻依賴text,并沒有依賴 name和 age,是以,我們隻要text改變的時候
我們自動觸發 render 函數 讓它生成一個虛拟dom就ok了(剩下的就是這個虛拟dom和上個虛拟dom做比對,然後操作真實dom,隻能以後再說了),那麼我們正式考慮一下怎麼做
第三步,'touch' 拿到依賴
回到最上面那張圖,我們知道data上的屬性設定definereactive後,修改data 上的值會觸發 set。
那麼我們取data上值是會觸發 get了。
對,我們可以在上面做做手腳,我們先執行一下render,我們看看data上哪些屬性觸發了get,我們豈不是就可以知道 render 會依賴data 上哪些變量了。
然後我麼把這些變量做些手腳,每次這些變量變的時候,我們就觸發render。
上面這些步驟簡單用四個子概括就是 計算依賴。
(其實不僅是render,任何一個變量的改别,是因為别的變量改變引起,都可以用上述方法,也就是computed 和 watch 的原理,也是mobx的核心)
第一步,
我們寫一個依賴收集的類,每一個data 上的對象都有可能被render函數依賴,是以每個屬性在definereactive
時候就初始化它,簡單來說就是這個樣子的
class dep {
constructor() {
this.subs = []
add(cb) {
this.subs.push(cb)
notify() {
console.log(this.subs);
this.subs.foreach((cb) => cb())
const dep = new dep()
// 省略
然後,當執行render 函數去'touch'依賴的時候,依賴到的變量get就會被執行,然後我們就可以把這個 render 函數加到 subs 裡面去了。
當我們,set 的時候 我們就執行 notify 将所有的subs數組裡的函數執行,其中就包含render 的執行。
至此就完成了整個圖,好我們将所有的代碼展示出來
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({
text: "before",
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__tostring__(this.text)])
])
settimeout(function(){
demo.text = "after"
我們看一下運作結果
好我們解釋一下 dep.target 因為我們得區分是,普通的get,還是在查找依賴的時候的get,
所有我們在查找依賴時候,我們将
function watch(vm, exp, cb){
dep.target = cb
return exp()
dep.target 指派,相當于 flag 一下,然後 get 的時候
get: () => {
if (dep.target) {
dep.add(dep.target)
return val
},
判斷一下,就好了。到現在為止,我們再看那張圖是不是就清楚很多了?
總結
我非常喜歡,vue2.0 以上代碼為了好展示,都采用最簡單的方式呈現。
不過整個代碼執行過程,甚至是命名方式都和vue2.0一樣
對比react,vue2.0 自動幫你監測依賴,自動幫你重新渲染,而
react 要實作性能最大化,要做大量工作,比如我以前分享的
<a href="https://segmentfault.com/a/1190000004290333">react如何性能達到最大化(前傳),暨react為啥非得使用immutable.js</a>
<a href="https://segmentfault.com/a/1190000004295639">react 實作pure render的時候,bind(this)隐患。</a>
而 vue2.0 天然幫你做到了最優,而且對于像萬年不變的 如标簽上靜态的class屬性,
vue2.0 在重新渲染後做diff 的時候是不比較的,vue2.0比 達到性能最大化的react 還要快的一個原因
然後源碼在此,喜歡的記得給個 star 哦
後續,我會簡單聊聊,vue2.0的diff。
作者:楊川寶
來源:51cto