不管读再多的文档,感觉还是自己写(抄)一遍记得牢。。
实现目标
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 = ''
})
}
}
参考(嗯90%):
[理解vue2.0的响应式架构.md](https://segmentfault.com/a/1190000007334535)