實現一個簡單數據綁定的Vue類

實現一個簡單數據綁定的Vue類

只在修改視圖中綁定的data數據時重新渲染頁面

用形象的例子作比喻,此文面向小白…此文面向小白…此文面向小白…囉嗦了很多廢話…求大佬輕噴

先貼一段代碼 表示要實現哪些功能

new Vue({
    el: "#app",
    data() {
        return {
            infos: {
                title: 'vue實現',
                default: '默認'
            },
            price: 27
        }
    },
    render(createElement) {
        return createElement('div', {
            attrs: {
                title: this.infos.title
            }
        }, [
            createElement('span', {}, this.price)
        ])
    }
})
思路
  1. 創建一個Vue類,然後保存一下對應上方傳入Vue實例中的數據
class Vue {
    constructor(options) {
        this.$el = document.querySelector(options.el)
        this._data = options.data && options.data()
        this.render = options.render
    }
}
  1. 要渲染頁面少不了render中的 createElement 函數
// 此方法在Vue類中
_createElement(tagName, data, children) {
    const tag = document.createElement(tagName)
    const { attrs = {} } = data
    for (let attrName in attrs) {
        tag.setAttribute(attrName, attrs[attrName])
    }
    // createElement傳入的第三個參數如果不是數組呢麼直接渲染文本節點,如果是數組則循環遍歷將child添加爲當前創建tag的子節點
    // 這裏可能有些人不理解說數組裏的child不是createElement函數嗎爲什麼可以直接添加爲tag的子節點?
    // 因爲函數執行前要先處理參數,意未參數中的createElement函數先於外部createElement函數執行,當外部createElement函數執行時形參中第三個參數的createElement數組已經變成了dom節點數組,即可直接添加爲當前tag子節點
    // 此爲一個樹形渲染,無論套多少層,都是從樹梢末端處先執行,然後一層一層向上,即上一層可直接添加爲子節點
    if (Object.prototype.toString.call(children) !== '[object Array]') {
        let child = document.createTextNode(children)
        tag.appendChild(child)
    } else {
        children.forEach(child => tag.appendChild(child))
    }
    return tag
}
  1. 現在做一個小的代理函數,將data的數據代理到this實例上
// 做一個小代理函數
function proxy(target, data, key) {
    // 簡單解釋一下這個函數 當調用target[key]是會觸發獲取的get函數返回data[key]值
    // 我們下面會將this作爲第一個參數, data爲第二個參數,data裏的每一個key是第三個參數
    // 意思就是當調用this[key]的時候會觸發get此時返回的是data中的數據,就是把data對象中所有第一層的數據代理到this上,因爲對象是引用類型,this代理data只需要代理第一層
    Object.defineProperty(target, key, {
        get() {
            return data[key]
        },
        set(newValue) {
            data[key] = newValue
        }
    })
}
// 修改Vue類
class Vue {
    constructor(options) {
        this.$el = document.querySelector(options.el)
        this._data = options.data && options.data()
        this.render = options.render
        // 加了下面三行 
		for (let key in this._data) {
			proxy(this, this._data, key)
		}
    }
}
  1. 剛剛把data數據代理到了this上,現在只差一個update更新視圖函數就能在頁面上看到要渲染的數據了
// update簡單來說就做了三件事 
// 1. 把createElement函數放到render函數裏執行一下,生成dom節點
// 2. 然後用生成的節點代替一開始傳進來的$el生成的節點
// 3. 更新一下現在的this.$el爲生成的新節點
// 此方法也在Vue類內部
_update() {
    const $root = this.render(this._createElement)
    api.replaceChild(this.$el, $root)
    this.$el = $root
}
// 外部定義的一個換節點的工具函數
const api = {
    replaceChild: (oldElement, element) => {
        return oldElement.parentElement.replaceChild(element, oldElement)
    }
} 
  1. 現在可以在Vue類中調用this._update了,貼一下現在Vue類上完整的代碼
class Vue {
    constructor(options) {
        this.$el = document.querySelector(options.el)
        this._data = options.data && options.data()
        this.render = options.render
        for (let key in this._data) {
            proxy(this, this._data, key)
        }
        this._update()
    }

    _update() {
        const $root = this.render(this._createElement)
        api.replaceChild(this.$el, $root)
        this.$el = $root
    }

    _createElement(tagName, data, children) {
        const tag = document.createElement(tagName)
        const { attrs = {} } = data
        for (let attrName in attrs) {
            tag.setAttribute(attrName, attrs[attrName])
        }
        if (Object.prototype.toString.call(children) !== '[object Array]') {
            let child = document.createTextNode(children)
            tag.appendChild(child)
        } else {
            children.forEach(child => tag.appendChild(child))
        }
        return tag
    }
}
  1. 現在頁面上已經可以顯示出要渲染的數據了,現在開始有點難了,修改數據更新視圖,而且只能在綁定到視圖上的數據發生變化時才更新視圖
// 先做一個依賴收集,把視圖上用到的data中的數據統計出來,由於之前把data代理了一層到this上,修改深層數據set函數監聽不到,所以現在要做一個遞歸函數,能監聽到所有值的變化的類
class Observe {
    constructor(obj) {
        this.walk(obj)
    }
    // walk函數遞歸將所有的屬性都代理到了自己的上一層
    walk(obj) {
        Object.keys(obj).forEach(key => {
            if (typeof obj[key] === 'object'
                && obj[key] !== null
               ) {
                this.walk(obj[key])
            }
            defineReactive(obj, key, obj[key])
        })
        return obj
    }
}
// 代理 原理同上面的proxy
function defineReactive(target, key, value) {
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            value = newValue
        }
    })
}

 // 寫了上面的函數現在就可以去監聽一下Vue中的data了
 class Vue {
     constructor(options) {
         this.$el = document.querySelector(options.el)
         this._data = options.data && options.data()
         // 加了下面這一行
         new Observe(this._data)
         this.render = options.render
         for (let key in this._data) {
             proxy(this, this._data, key)
         }
     }
 }
  1. 現在每一層的數據變化都能監聽到了,現在需要做一個小的發佈訂閱模式的類來訂閱哪些屬性更新的時候更新視圖
// 這個類 不是在一次render函數的執行中把this調用的數據放到下面Dep類的數組裏,而是在遞歸代理的時候給每一個屬性聲明一個Dep類,有調了屬性的get方法的,把當前可以更新視圖的函數push到當前屬性的dep數組裏,給大佬(屬性的Dep)遞我(可以更新視圖的類)
// 下面的我,都代指可以更新視圖的類
// 可能這裏有點反向的思想不好理解
// Dep大佬在此
class Dep {
    constructor() {
        // 小迷弟隊列
        this.subs = []
    }
	// 添加到隊列
    addSub(sub) {
        // 去重
        if (this.subs.indexOf(sub) < 0) {
            this.subs.push(sub)
        }
    }
	// 清空隊列,讓小迷弟們去更新視圖
    notify() {
        const subs = this.subs.slice()
        subs.forEach(sub => sub.update())
    }
}
  1. 遞歸代理的時候要把我(可以更新視圖的類)扔到當前屬性的dep裏,呢怎麼拿到當前的我呢,我們做一個全局變量,在獲取前,把我放上去,獲取結束了再把我扔了
// 我代指可以更新視圖的類
// 來一個全局靜態變量
Dep.targets = []
// 把我掛上全局
function pushTarget(instance) {
    Dep.targets.push(instance)
}
// 把我從全局扔了
function popTarget() {
    return Dep.targets.pop()
}
// 然後改一下遞歸代理
function defineReactive(target, key, value) {
    // 每個屬性一個dep
    const dep = new Dep()
    Object.defineProperty(target, key, {
        get() {
            // 判斷一下我(this)在不在全局上
            if (Dep.targets.length) {
                // 下面這個方法 正是給大佬(屬性的Dep)遞我(可以更新視圖的類) 但是還沒寫
                // dep.addDepend()
            }
            return value
        },
        set(newValue) {
            value = newValue
        }
    })
}
  1. 現在來寫我,我是一個有試圖更新功能的watcher
// watcher 我在此
class Watcher {
    // 這裏的getter函數就是傳進來update函數
    constructor(getter, callback) {
        this.callback = callback
        this.getter = getter
        this.value = this.get()
    }
    // getter函數做一個小封裝
    // 因爲之前說了,在更新視圖的時候要調很多this上的數據,要給每個大佬遞我
    // 所以在update之前就要吧我(this,就是現在這個可以更新視圖的watcher扔到全局去)
	// 然後getter函數也就是update函數執行的過程中有使用this數據觸發代理的get函數了
    // 而上面代理的get函數判斷了一下,要是全局有我,就執行大佬來喊我的addDepend函數 下面會加
    get() {
        pushTarget(this)
        let value = this.getter()
        popTarget()
        return value
    }
	// 這裏的addDep就是給大佬遞我
    addDep(dep) {
        dep.addSub(this)
    }
	// 更新視圖的方法
    update() {
        console.log('更新視圖')
        let newValue = this.get()
        return newValue
    }
}

// 現在在Dep大佬身上多一個喊我的方法
class Dep {
	// 大佬來喊全局的我,快把你遞給我
    addDepend() {
       	// 全局的我就執行了把給大佬遞我的addDep方法
        Dep.targets[Dep.targets.length - 1].addDep(this)
    }
}
  1. 現在爲止…但凡在render裏調了this上數據的大佬,我都會大佬的迷弟隊列裏待命,一旦大佬的數據改了,我就會跑去更新下視圖…
// 再來改一下遞歸代理這裏
function defineReactive(target, key, value) {
    const dep = new Dep()
    Object.defineProperty(target, key, {
        get() {
            if (Dep.targets.length) {
                dep.addDepend()
            }
            return value
        },
        set(newValue) {
            value = newValue
			// set設置值的時候清空當前大佬dep的小迷弟隊列
            dep.notify()
        }
    })
}
  1. 最後一步Watcher加到Vue裏
class Vue {
    constructor(options) {
        this.$el = document.querySelector(options.el)
        this._data = options.data && options.data()
        new Observe(this._data)
        this.render = options.render
        for (let key in this._data) {
            proxy(this, this._data, key)
        }
        new Watcher(() => {
            this._update()
        }, () => {
            console.log('callback')
        })
    }
}

數據綁定完成! 我把實例掛到window上了

可以修改window.app.price = xxx查看

修改window.app.infos.default視圖上沒有的屬性則不更新視圖

下面貼一波完整的源碼
<div id="app"></div>

<script type="text/javascript">
	class Vue {
		constructor(options) {
			this.$el = document.querySelector(options.el)
			this._data = options.data && options.data()
			new Observe(this._data)
			this.render = options.render
			for (let key in this._data) {
				proxy(this, this._data, key)
			}
			new Watcher(() => {
				this._update()
			}, () => {
				console.log('callback')
			})
		}

		_update() {
			const $root = this.render(this._createElement)
			api.replaceChild(this.$el, $root)
			this.$el = $root
		}

		_createElement(tagName, data, children) {
			const tag = document.createElement(tagName)
			const { attrs = {} } = data
			for (let attrName in attrs) {
				tag.setAttribute(attrName, attrs[attrName])
			}
			if (Object.prototype.toString.call(children) !== '[object Array]') {
				let child = document.createTextNode(children)
				tag.appendChild(child)
			} else {
				children.forEach(child => tag.appendChild(child))
			}
			return tag
		}
	}

	class Dep {
		constructor() {
			this.subs = []
		}

		addSub(sub) {
			if (this.subs.indexOf(sub) < 0) {
				this.subs.push(sub)
			}
		}

		notify() {
			const subs = this.subs.slice()
			subs.forEach(sub => sub.update())
		}

		addDepend() {
			Dep.targets[Dep.targets.length - 1].addDep(this)
		}
	}

	Dep.targets = []

	function pushTarget(instance) {
		Dep.targets.push(instance)
	}

	function popTarget() {
		return Dep.targets.pop()
	}

	class Watcher {
		constructor(getter, callback) {
			this.callback = callback
			this.getter = getter
			this.value = this.get()
		}

		get() {
			pushTarget(this)
			let value = this.getter()
			popTarget()
			return value
		}

		addDep(dep) {
			dep.addSub(this)
		}

		update() {
			console.log('更新視圖')
			let newValue = this.get()
			return newValue
		}
	}

	class Observe {
		constructor(obj) {
			this.walk(obj)
		}

		walk(obj) {
			Object.keys(obj).forEach(key => {
				if (typeof obj[key] === 'object'
					&& obj[key] !== null
				) {
					this.walk(obj[key])
				}
				defineReactive(obj, key, obj[key])
			})
			return obj
		}
	}

	function proxy(target, data, key) {
		Object.defineProperty(target, key, {
			get() {
				return data[key]
			},
			set(newValue) {
				data[key] = newValue
			}
		})
	}

	function defineReactive(target, key, value) {
		const dep = new Dep()
		Object.defineProperty(target, key, {
			get() {
				if (Dep.targets.length) {
					dep.addDepend()
				}
				return value
			},
			set(newValue) {
				value = newValue
				dep.notify()
			}
		})
	}

	const api = {
		replaceChild: (oldElement, element) => {
			return oldElement.parentElement.replaceChild(element, oldElement)
		}
	} 

	window.app = new Vue({
		el: "#app",
		data() {
			return {
				infos: {
					title: 'vue實現',
					default: '默認'
				},
				price: 27
			}
		},
		render(createElement) {
			return createElement('div', {
				attrs: {
					title: this.infos.title
				}
			}, [
				createElement('span', {}, this.price)
			])
		}
	})
</script>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章