前言:最近学了vue的响应式原理,可是学了以后,感受模模糊糊的,不知道本身是否真的理解其中的精髓,因此就本身动手简单的实现一下vue的响应式原理。毕竟概念终究仍是概念,实践才是检验本身会不会的硬道理!javascript
在开始以前,咱们须要了解一下基础的知识:
- Object.defineProperty():它的做用是直接在一个对象上定义一个属性,或者去修改一个已经存在的属性。
obj
:表示须要定义属性的当前对象。prop
:当前须要定义的属性名。desc
:属性描述符,就是更精确的控制对象属性。
Object.defineProperty(obj,prop,desc)
- vue的数据响应式原理:就是vm中的data数据在页面上有渲染,当data数据改变的时候,页面上渲染的数据也跟着改变。
例如页面上我用了data中的msg渲染了,如图:
当我data.msg的值改变了的时候:
进入正题:
目标:成品就跟上面截图的那样,当你的数据发生变化,页面的也跟着变化。
须要知识:
Object.defineProperty()
- es6部分语法
实践:
html页面:html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title></title> </head> <body> <div id="app"> msg </div> </body> </html>
开始编写js代码:vue
第一步:定义咱们要渲染的数据
let data = { msg: '你好' }
解释:定义一个咱们要渲染到页面的数据。java
第二步:获取页面的变量并渲染
class Render { constructor() { this.app = document.querySelector('#app') this.arg = this.app.innerText this.render() } render() { this.app.innerText = data[this.arg] } }
解释:为了方便获取参数,咱们就使用一个参数简单模拟,不像vue那样用{ {}}来包括变量,若是用{ {}}包裹变量,就须要利用正则表达式获取里面内容。node
第三步:监听数据的改变。
class Observe { constructor() { this.init(data.msg) } init(value) { Object.defineProperty(data, 'msg', { set(newVal) { if (newVal !== value) { value = newVal dep.notify() } }, get() { return value } }) } }
解释:咱们想要知道一个变量的值有没有改变呢,就须要监听数据的变化。vue中就是使用Object.defineProperty(obj,prop,desc)
这个方法中的set()
和get()
来监听的。set()
就是当值要改变的时候,就触发。而get()
就是当你获取这个变量的时候触发。咱们利用这个方法就能够监听获得数据的变化了。es6
第三步:建立收集者
class Dep { constructor() { this.subs = [] } addSub(watcher) { this.subs.push(watcher) } notify() { this.subs.forEach(w => w.update()) } }
解释:收集者的做用就存储观察者和通知观察者去更新页面的。(观察者在下面。)web
第三步:建立观察者
class Watcher { constructor(node, arg, callback) { this.node = node // 变量所在的节点 this.arg = arg // 变量名 this.oldVal = this.getOldVal() // 没更新前的值 this.callback = callback // 值更新后执行的操做 } getOldVal() { Dep.target = this let oldVal = data[this.arg] Dep.target = null return oldVal } update() { this.callback(data.msg) } }
解释:咱们想要知道一个值有没有改变,改变以后从新渲染的操做是怎么样的。这个时候就须要一个观察者了。在数据渲染到页面的时候,为这个数据添加一个观察者,当这个数据改变的时候,就执行回调函数,去更新页面。正则表达式
第五步:调用收集者和观察者
let data = { msg: '你好' } class Dep { constructor() { this.subs = [] } addSub(watcher) { this.subs.push(watcher) } notify() { this.subs.forEach(w => w.update()) } } class Watcher { constructor(node, arg, callback) { this.node = node this.arg = arg this.oldVal = this.getOldVal() this.callback = callback } getOldVal() { Dep.target = this let oldVal = data[this.arg] Dep.target = null return oldVal } update() { this.callback(data.msg) } } class Render { constructor() { this.app = document.querySelector('#app') this.arg = this.app.innerText this.render() } render() { // 安排观察者监视数据 new Watcher(this.app, this.arg, (newVal) => { this.app.innerText = newVal }) this.app.innerText = data[this.arg] } } class Observe { constructor() { this.init(data.msg) } init(value) { let dep = new Dep() // 建立收集者 Object.defineProperty(data, 'msg', { set(newVal) { if (newVal !== value) { value = newVal dep.notify() // 通知观察者 } }, get() { Dep.target && dep.addSub(Dep.target) // 添加观察者 return value } }) } } new Observe() new Render()
解释:在有注释的方法,就是使用收集者和观察者的地方。app
代码的基本运行逻辑:
- 首先咱们先使用
Object.defineProperty
去监听全部数据,而后获取页面中的内容,看看页面有没有使用data中的属性,若是有,就将对应的变量渲染成对应的值。 - 渲染的时候,咱们要帮他建立一个观察者(watcher),传入当前的dom节点、属性名、还有一个回调函数,观察者内部就会获取参数的值,保存在
oldval
中。回调函数,就是当数据更新以后才触发的。 - 咱们在获取
oldVal
,先为Dep.target设置为this,而后再获取oldVal
,获取的时候就会触发get方法,Dep.target
有值就会添加进收集者(Dep)中,只会把Dep.target
该成null,由于get方法会在不少时候触发,添加进收集者中,我就们就不要要添加了。 - 在值改变的时候,就会触发set()方法,
dep.notify()
就会通知它里面的观察者就进更新页面的操做,watcher就会调用他们的callback函数更新页面。
总的代码:dom
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title></title> </head> <body> <div id="app"> msg </div> </body> <script> let data = { msg: '你好' } class Dep { constructor() { this.subs = [] } addSub(watcher) { this.subs.push(watcher) } notify() { this.subs.forEach(w => w.update()) } } class Watcher { constructor(node, arg, callback) { this.node = node this.arg = arg this.oldVal = this.getOldVal() this.callback = callback } getOldVal() { Dep.target = this let oldVal = data[this.arg] Dep.target = null return oldVal } update() { this.callback(data.msg) } } class Render { constructor() { this.app = document.querySelector('#app') this.arg = this.app.innerText this.render() } render() { new Watcher(this.app, this.arg, (newVal) => { this.app.innerText = newVal }) this.app.innerText = data[this.arg] } } class Observe { constructor() { this.init(data.msg) } init(value) { let dep = new Dep() Object.defineProperty(data, 'msg', { set(newVal) { if (newVal !== value) { value = newVal dep.notify() } }, get() { Dep.target && dep.addSub(Dep.target) return value } }) } } new Observe() new Render() </script> </html>
本文分享 CSDN - 冬天爱吃冰淇淋。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。