面試之vue小實現

vue小實現

面試之vue小實現

實現目標

 var demo = new Vue({
el: '#demo',
data: {
text: "before change text",
text2: "before change text2",
},
render() {
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__toString__(this.text)]),
this.__h__('span', {}, [this.__toString__(this.text2)])
])
}
})
setTimeout(function() {
demo.text = "after change text"
demo.text2 = "after change text2"
}, 2000)
setTimeout(function() {
demo.text = "after after change text"
demo.text2 = "after after change text2"
}, 3000)

先實現一個小目標,text和text2能在頁面上呈現出來,在實現一個大點的,2秒後和3秒後頁面中的文本改變。

模擬一個Vue的構造函數

底板先擺出來:

class Vue {
constructor(options) {
先將傳入的data參數放到實例的_data屬性上以供調用
this._data = options.data
}
}

下面要乾的第一步是將new Vue實例時傳入的參數處理下。怎麼個處理法?例如:我們的text和text2屬性是放到_data這個屬性上的,那麼調用的時候可能就要寫demo._data.text。這樣寫太複雜,不如demo.text方便。

 constructor(options) {
this.$options = options
this._data = options.data
Object.keys(options.data).forEach(key => this._proxy(key))
}
_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
}
})
}

接下來就是實現數據與頁面綁定的關鍵了===>defineReactive方法。按照觀察者模式,我們希望知道text和text2是否被調用,如果他們被調用,那麼當他們改變的時候我們就需要重新刷新頁面了。恰好,Object.defineProperty就提供對象被調用或被改變的回調。那麼class Dep是用來幹嘛的呢,簡單的說是為了收集依賴:當vue遍歷data的參數時,會在每次循環的函數閉包中生成一個Dep的實例,可以認為每個參數(text和text2)都有一個對應的Dep實例,當text或者text2被調用(即get()方法被調用)時,會將註冊的事件(也就是代碼裡的Dep.target)添加到Dep實例的subs數組裡,然後當text或者text2改變時,取出subs數組裡收集到的訂閱事件,然後循環執行所有的訂閱。這樣就實現了data改變到頁面刷新的自動過程。

 constructor(options) {
...
observer(options.data)
}
function observer(value, cb) {
Object.keys(value).forEach((key) => defineReactive(value, key, value[key], cb))
}
function defineReactive(obj, key, val, cb) {
// 每個屬性都創建了一個dep實例,所以update方法被添加到了各自的dep.subs數組裡
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()
}
})
}
class Dep {
constructor() {
this.subs = []
}
add(cb) {
if(this.subs.indexOf(cb) === -1) {
console.log('被添加到監聽了')
this.subs.push(cb)
}
}
notify() {
console.log('notify被觸發了')
this.subs.forEach((cb) => cb())
}
}

接著看訂閱事件(也就是上面的Dep.target)具體指的是啥。

 constructor(options) {
this.$options = options
this._data = options.data
Object.keys(options.data).forEach(key => this._proxy(key))
observer(options.data)
watch(this, this._render.bind(this), this._update.bind(this))
}
_render() {
let VNode = this.$options.render.call(this)
document.getElementById(this.$options.el).innerHTML = JSON.stringify(VNode)
return VNode
}
_update() {
console.log("我將要更新");
const vdom = this._render.call(this)
}
function watch(vm, exp, cb) {
// exp==>_render
// cb==>update
// 先執行一下render,並且讓update方法watch this對象
// 這一步比較巧妙,先把update這個cb放到target對象上。執行_render時,如果使用到了data上的對象,那麼update就會被添加到dep裡,也就實現了update watch data.
Dep.target = cb
let vdom = exp()
Dep.target = null
return vdom
}

想想也就知道了,當然是數據改變,頁面要重新渲染了。watch方法裡有個很牛叉的地方:首先Dep.target = cb,將_update這個訂閱事件賦給了Dep.target,然後執行了exp也就是_render方法,到最後執行的就是我們new Vue實例時傳入的render方法,

 render() {
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__toString__(this.text)]),
this.__h__('span', {}, [this.__toString__(this.text2)])

])
}

看,這個方法裡調用了this.text和this.text2哎,於是text和text2的get回調被觸發。

 get: () => {
if (Dep.target) {
dep.add(Dep.target)
}
return val
},

由於Dep.target在上一刻神奇的被賦值(_update方法)了,所以_update被收進了text和text2的dep實例裡,當render執行完後,Dep.target = null又被神奇的置為了空。

就這樣當執行到demo.text = "after change text"時,_update方法被執行了,頁面被重新渲染了。

小缺陷:_update方法被多次重複執行

當我們在一次setTimeout()裡既改變text,又改變text2時,由於_update既被添加到了text的dep實例中,又被添加到了text2的dep實例中,所以_render會被執行兩次。第一次_render後text被改變成新的了,document.getElementById(this.options.el).innerHTML = ...執行,頁面又刷新;這看起來是期望得到了。但是由於js是同步執行的,這兩次頁面的改變的間隔幾乎可以忽略不計,人眼肯定是無法差覺得。當頁面複雜時,頁面會由於迴流或重繪造成性能問題。

此處解決的方法是使用Promise,在同步任務執行完後執行Microtask時更新頁面:

 constructor(options) {
this.queueNextTick = ''
}

_update() {
if(!this.queueNextTick) {
this.queueNextTick = new Promise((resolve)=>{
resolve()
})
this.queueNextTick.then(()=>{
console.log("我將要更新");
const vdom = this._render.call(this)
console.log(vdom);
this.queueNextTick = ''
})
}
}

這些技術如何學習,有沒有免費資料?

對前端的技術,架構技術感興趣的同學關注我的頭條號,並在後臺私信發送關鍵字:“前端”即可獲取免費的架構師學習資料

知識體系已整理好(源碼,筆記,PPT,學習視頻),歡迎免費領取。還有面試視頻分享可以免費獲取。關注我,可以獲得沒有的架構經驗哦!!


分享到:


相關文章: