分享前啰嗦
我之前介绍过vue1.0如何实现observer和watcher。本想继续写下去,可是vue2.0横空出世..所以直接看vue2.0吧。这篇文章在公司分享过,终于写出来了。我们采用用最精简的代码,还原vue2.0响应式架构实现。
以前写的那篇 vue 源码分析之如何实现 observer 和 watcher可以作为本次分享的参考。
不过不看也没关系,但是最好了解下Object.defineProperty
本文分享什么
理解vue2.0的响应式架构,就是下面这张图

顺带介绍他比react快的其中一个原因
本分实现什么
const demo = new Vue({
data: {
text: "before",
},
//对应的template 为 <div><span>{{text}}</span></div>
render(h){
return h('div', {}, [
h('span', {}, [this.__toString__(this.text)])
])
}
})
setTimeout(function(){
demo.text = "after"
}, 3000)
</div>
对应的虚拟dom会从
<div><span>before</span></div> 变为 <div><span>after</span></div>
好,开始吧!!!
第一步,讲data 下面所有属性变为observable
来来来先看代码吧
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
observer(options.data, this._update)
this._update()
}
_update(){
this.$options.render()
}
}
function observer(value, cb){
Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
}
function defineReactive(obj, key, val, cb) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=>{},
set:newVal=> {
cb()
}
})
}
var demo = new Vue({
el: '#demo',
data: {
text: 123,
},
render(){
console.log("我要render了")
}
})
setTimeout(function(){
demo._data.text = 444
}, 3000)
</div>
为了好演示我们只考虑最简单的情况,如果看了vue 源码分析之如何实现observer和watcher可能就会很好理解,不过没关系,我们三言两语再说说,这段代码要实现的功能就是将
var demo = new Vue({
el: '#demo',
data: {
text: 123,
},
render(){
console.log("我要render了")
}
})
</div>
中data 里面所有的属性置于 observer,然后data里面的属性,比如 text 以改变,就引起_update()函数调用进而重新渲染,是怎样做到的呢,我们知道其实就是赋值的时候就要改变对吧,当我给data下面的text 赋值的时候 set 函数就会触发,这个时候 调用 _update 就ok了,但是
setTimeout(function(){
demo._data.text = 444
}, 3000)
</div>
demo._data.text没有demo.text用着爽,没关系,我们加一个代理
_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
}
})
}
</div>
然后在Vue的constructor加上下面这句
Object.keys(options.data).forEach(key => this._proxy(key))
第一步先说到这里,我们会发现一个问题,data中任何一个属性的值改变,都会引起
_update的触发进而重新渲染,属性这显然不够精准啊
第二步,详细阐述第一步为什么不够精准
比如考虑下面代码
new Vue({
template: `
<div>
<section>
<span>name:</span> {{name}}
</section>
<section>
<span>age:</span> {{age}}
</section>
<div>`,
data: {
name: 'js',
age: 24,
height: 180
}
})
setTimeout(function(){
demo.height = 181
}, 3000)
</div>
template里面只用到了data上的两个属性name和age,但是当我改变height的时候,用第一步的代码,会不会触发重新渲染?会!,但其实不需要触发重新渲染,这就是问题所在!!
第三步,上述问题怎么解决
简单说说虚拟 DOM
首先,template最后都是编译成render函数的(具体怎么做,就不展开说了,以后我会说的),然后render 函数执行完就会得到一个虚拟DOM,为了好理解我们写写最简单的虚拟DOM
function VNode(tag, data, children, text) {
return {
tag: tag,
data: data,
children: children,
text: text
}
}
class Vue {
constructor(options) {
this.$options = options
const vdom = this._update()
console.log(vdom)
}
_update() {
return this._render.call(this)
}
_render() {
const vnode = this.$options.render.call(this)
return vnode
}
__h__(tag, attr, children) {
return VNode(tag, attr, children.map((child)=>{
if(typeof child === 'string'){
return VNode(undefined, undefined, undefined, child)
}else{
return child
}
}))
}
__toString__(val) {
return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);
}
}
var demo = new Vue({
el: '#demo',
data: {
text: "before",
},
render(){
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__toString__(this.text)])
])
}
})
</div>
我们运行一下,他会输出
{
tag: 'div',
data: {},
children:[
{
tag: 'span',
data: {},
children: [
{
children: undefined,
data: undefined,
tag: undefined,
text: '' // 正常情况为 字符串 before,因为我们为了演示就不写代理的代码,所以这里为空
}
]
}
]
}
</div>
这就是 虚拟最简单虚拟DOM,tag是html 标签名,data 是包含诸如class和style这些标签上的属性,childen就是子节点,关于虚拟DOM就不展开说了。
回到开始的问题,也就是说,我得知道,render 函数里面依赖了vue实例里面哪些变量(只考虑render 就可以,因为template 也会是帮你编译成render)。叙述有点拗口,还是看代码吧
var demo = new Vue({
el: '#demo',
data: {
text: "before",
name: "123",
age: 23
},
render(){
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__toString__(this.text)])
])
}
})
</div>
就像这段代码,render 函数里其实只依赖text,并没有依赖name和age,所以,我们只要text改变的时候,我们自动触发render 函数 让它生成一个虚拟DOM就ok了(剩下的就是这个虚拟DOM和上个虚拟DOM做比对,然后操作真实DOM,只能以后再说了),那么我们正式考虑一下怎么做
第三步,'touch' 拿到依赖
回到最上面那张图,我们知道data上的属性设置defineReactive后,修改data 上的值会触发set。
那么我们取data上值是会触发get了。
对,我们可以在上面做做手脚,我们先执行一下render,我们看看data上哪些属性触发了get,我们岂不是就

