嘗試使用es6新特性,本身來實現一個mvvm及vue的各類特性。
相關代碼放在github,會持續更新,歡迎賞個star。
本篇文章爲系列文章的第一篇,會比較容易理解,後續會持續更新後面的記錄。
文章首發於本人博客html
從開始接觸Vue開始,咱們便對它的「數據響應」讚歎不絕,那麼咱們首先,來實現一個最簡單的watcher,來監聽數據,以進行對應的操做,相似後續會涉及的dom操做等。vue
咱們都知道,Vue使用Object.defineProperty來進行數據監聽,監聽obj的get和set方法。在ES6中,Proxy能夠攔截某些操做的默認行爲,也就是對目標對象的訪問進行攔截,過濾和改寫。咱們能夠利用這個特性來實現對數據的監聽:git
const watcher = (obj, fn) => { return new Proxy(obj, { get (target, prop, receiver) { return Reflect.get(target, prop, receiver) }, set (target, prop, value) { const oldValue = Reflect.get(target, prop, receiver) const result = Reflect.set(target, prop, value) fn(value, oldValue) return result } }) }
結果:es6
let obj = watcher({ a: 1 }, (val, oldVal) => { console.log('old =>> ', oldVal) console.log('new =>> ', val) }) obj.a = 2 // old =>> , 1 // new =>> , 2
咱們已經能夠對簡單的數據操做進行監聽(雖然還有各類問題),接下來,咱們只要在監聽到dom後進行數據操做便可。解析模板什麼的咱們就先不作了,咱們能夠繼續利用Proxy實現一個dom輔助函數:github
const dom = new Proxy({}, { get (target, tagName) { return (attrs = {}, ...childrens) => { // 建立節點 const elem = document.createElement(tagName) // 添加attribute attrs.forEach(attr => elem.addAttribute(attr, attrs[attr]) // 添加子元素 childrens.forEach(child => { const child = typeof child === 'string' ? document.createTextNode(child) : child elem.appendChild(child) }) return elem } } })
也就是說,咱們爲dom的各屬性進行監聽,當訪問對應的節點時,咱們建立而且爲他添加各類屬性等:數組
dom.div( {class: 'wrap'}, 'helloworld', dom.a({ href: 'https://www.360.cn' }, 'welcome to 360') ) // 輸出 <div class="wrap"> helloworld <a href="https://www.360.cn">welcome to 360</a> </div>
咱們在這裏給咱們的這個小架子起名爲'W',讓它能夠真正的運行起來。相似Vue的語法,咱們須要在進行實例化的時候,watch咱們的data,而且更新dom。相似這樣:app
const vm = new W({ el: 'body', data () { return { msg: 'hello world' } }, render () { return dom.div({ class: 'wrap' }, dom.a({ href: 'http://www.360.cn' }, this.msg) ) } })
所以,咱們須要實現這樣一個類,來處理咱們的參數,並進行實例的初始化,監聽,以及渲染控制等。框架
export default class W { constructor (config) {} /** * observe data */ _initData () {} /** * 渲染節點 */ _renderDom () {} }
首先,咱們進行數據初始化,將數據置爲observable,在對其修改的時候進行監聽:dom
import watcher from './data.js' class W { constructor (config) { const { data = () => {} } = config this._config = config this._initData(data) return this._vm } } _initData (data) { this._vm = watcher(Object.assign({}, this, data()), this._renderDom.bind(this)) }
在這裏咱們須要注意兩點:mvvm
咱們的data參數爲一個function
這個緣由在vue官方文檔已經說過,當咱們直接使用對象的時候,不一樣的實例間會共享同一個對象,致使出現對一個組件進行修改,另外一個組件也進行修改的問題。具體能夠查看data-必須是函數
咱們返回的是this._vm而不是this
咱們這裏作了兩步操做,首先將this與data進行合併,再將整個對象進行監聽,並賦值到_vm屬性上。
這樣,咱們經過new W()初始化的實例,則能夠訪問到咱們的data屬性及方法,而且具備數據驅動的特性了。
咱們已經爲watcher的回調添加了dom更新的事件,咱們只要在這裏執行render函數,並掛載到對應的el上便可:
const { render, el } = this._config const targetEl = document.querySelector(el) const renderDom = render() targetEl.innerHTML = '' targetEl.appendChild(renderDom)
咱們會發現,咱們在config的render函數中,使用了this.msg來訪問data的msg屬性,所以咱們須要實如今各組件中,經過this能夠訪問到本實例的特性。我猜你已經想到了,咱們可使用bind,call和apply來實現它:
/** * 爲全部的函數綁定this */ bindVM () { const { _config } = this for(let key of Object.keys(_config)) { const val = _config[key] if (typeof(val) === 'function') { _config[key] = val.bind(this._vm) } } }
簡單的架子拼接完成,咱們來進行測試下咱們的成果,咱們須要實現兩點功能:
能夠按照咱們的render函數正常掛載,並可訪問到data上的數據
經過對實例進行修改,修改會自動更新到節點上
代碼:
const vm = new W({ el: 'body', data () { return { msg: 'hello world' } }, render () { return dom.div({ class: 'wrap' }, dom.a({ href: 'http://www.360.cn' }, this.msg) ) } }) // 測試修改vm setInterval(_ => { vm.msg = 'hello world =>>>' + new Date() }, 1000)
結果:
最基本的功能已經實現啦!
本次咱們只實現了最最最簡單的數據驅動功能,後續還有不少須要進行處理,咱們也會對其一一進行梳理和實現,你們能夠持續關注下,例如:
數組變更監聽
object深度監聽
更新隊列
render過程當中記錄僅相關的屬性
模板渲染
v-model
...等等
相關代碼放在github,會持續更新,歡迎賞個star。
敬請期待!