vue2框架简易版响应式设计(观察者模式)

对于vue.js中的属性值我们要格外关注:

$attrs 获取当前传递的参数

$listeners 获取当前组件的自定义事件

$children 获取当前组件所有子组件

$parent 获取当前组件所有父组件

$options 获取当前vue实例参数信息

$refs 获取ref所有的引用节点

设计原则:单一职责,一个类或一个函数,只做一件事。

数据定义后页面更新

页面数据切换操作data数据

添加数据劫持方法

// Vue.js 
//Observer专门用于数据劫持
class Observer {data;constructor(data) {this.data = datathis.walk()}defineProperty(data, key, value) {Object.defineProperty(data, key, {get() {console.log(`使用了${key}这个属性`);return value},set(val) {console.log(`修改了${key}属性`, val);value = val}})}walk() {Object.keys(this.data).forEach(el => {this.defineProperty(user, el, this.data[el])})}
}const user = {username:'xiaowang',age:11
}new Observer(user)
console.log(user.username);

对于data来说,我们自己数据劫持存了一份在$data,并且还将$data中每个数据都再次存在了vue实例上,创建Vue类。

class Vue {constructor(options) {this.$options = optionsthis.$data = options.data()this.$el = options.el//$data上的所有数据都要数据劫持new Observer(this.$data)//$data存放所有的数据//会将$data的数据挂并挂载到this身上this.proxy()}proxy() {Object.keys(this.$data).forEach(key => {Object.defineProperty(this, key, {get() {return this.$data[key]},set(val) {this.$data[key] = val}})})}
}

进行模版渲染,这里仅仅渲染一层,如果多层可以执行递归。

//模版渲染
class Compile{constructor(el,data){this.$el = document.querySelector(el)this.$data = datathis.compiler()}compiler(){[...this.$el.children].forEach(item=>{if(/\{\{([a-zA-z0-9]+)\}\}/.test(item.innerHTML)){const key =RegExp.$1.trim()item.innerHTML=this.$data[key]}})}
}

观察者模式

观察者模式是一种设计模式,就是一种代码规范。有两个非常重要的元素:发布者和订阅者。一个发布者可能对应多个订阅者。

对应到我们的代码,使用{{}}的标签,意味着是我们需要标记的标签,作为订阅者,在项目中提供一个发布者,一旦数据发生变化,发布者通知订阅者更新页面。

//订阅者
class Watcher {constructor(callback) {Dep.target = thisthis.callback = callbackthis.update()Dep.target = null}update() {//这一步并不是直接修改,而是更新虚拟dom,这里只是简化了this.callback()}
}//发布者
class Dep {constructor() {this.subs = []}notify() {this.subs.forEach(item => {item.update()})}
}

于此同时,在set get中需要收集wacher以及通知watcher更新数据,那么也要修改,这里有个难点,就是在数据劫持的时候怎么获取对应的watcher,这里我们采用在watcher生成实例时,将watcher挂载到发布者身上,在获取属性的劫持过程中,使用target属性,那么就生成一个watcher,并将他进行收集,在修改属性的时候,可以使用notify的方法,通知watcher使用update方法更新对应的render函数进行渲染。

Object.defineProperty(data, key, {get() {if (Dep.target) { //只收集编译时的watcher//依赖收集,使用这个属性就生成一个watcherdep.subs.push(Dep.target)}return value},set(val) {//一旦数据更新,Dep通知watcher更新value = valdep.notify()}})

此刻,我们简易的响应式就算完成了。

这里的执行顺序比较绕,先调用数据劫持,然后调用compile,每个有{{}}的标签生成watcher,并传入render函数,在watcher的constructor中挂到发布者dep的身上,执行update,render函数得以执行,然后会触发数据劫持get方法,dep的subs进行收集watcher,同时update结束后,清除挂载在dep上的watcher。修改数据时,使用notify通知watcher然后调用update方法,触发render方法,同时继续触发get方法获取数据,但是这个时候,就不会再继续获取wacher,因为整个watcher的生成只有一次,就是在第一次编译的时候。

附完整代码:

//vue.js
// Observer专门用于数据劫持
class Observer {data;constructor(data) {this.data = datathis.walk()}defineProperty(data, key, value) {const dep = new Dep()Object.defineProperty(data, key, {get() {if (Dep.target) { //只收集编译时的watcher//依赖收集,使用这个属性就生成一个watcherdep.subs.push(Dep.target)}return value},set(val) {//一旦数据更新,Dep通知watcher更新value = valdep.notify()}})}walk() {Object.keys(this.data).forEach(el => {this.defineProperty(this.data, el, this.data[el])})}
}class Vue {constructor(options) {this.$options = optionsthis.$data = options.data()this.$el = options.el//$data上的所有数据都要数据劫持new Observer(this.$data)//$data存放所有的数据//会将$data的数据挂并挂载到this身上this.proxy()new Compile(this.$el, this.$data)}proxy() {Object.keys(this.$data).forEach(key => {Object.defineProperty(this, key, {get() {return this.$data[key]},set(val) {this.$data[key] = val}})})}
}//模版渲染
class Compile {constructor(el, data) {this.$el = document.querySelector(el)this.$data = datathis.compiler()}compiler() {[...this.$el.children].forEach(item => {if (/\{\{([a-zA-z0-9]+)\}\}/.test(item.innerHTML)) {const key = RegExp.$1.trim()//实际上vue底层不是直接innerHTML// item.innerHTML=this.$data[key]const render = () => item.innerHTML = this.$data[key]new Watcher(render)}})}
}
//订阅者
class Watcher {constructor(callback) {Dep.target = thisthis.callback = callbackthis.update()Dep.target = null}update() {//这一步并不是直接修改,而是更新虚拟dom,这里只是简化了this.callback()}
}//发布者
class Dep {constructor() {this.subs = []}notify() {this.subs.forEach(item => {item.update()})}
}
<!DOCTYPE html>
<html lang="en">//HTML
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><div id="app"><p>{{username}}</p><p>{{age}}</p></div>
</body>
<script src="./vue.js"></script>
<script>const app = new Vue({el: '#app',data() {return {username: 'xiaoming',age: 10}}})console.log(app);</script></html>