vue官方对响应式的实现方式是这样解释的:
当你把一个普通的 JavaScript 对象传给 Vue 实例的
data
选项,Vue 将遍历此对象所有的属性,并使>>>用 Object.defineProperty 把这些属性全部转为 >getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是为什么 Vue 不支持 IE8 以及更低版本浏览器。
这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化。这里需要注意的问题是浏览器控制台在打印数据对象时 getter/setter 的格式化并不同,所以你可能需要安装 vue-devtools 来获取更加友好的检查接口。
每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter
被调用时,会通知watcher
重新计算,从而致使它关联的组件得以更新。
第一步,监听data下边的所有属性,转换为响应式
思路
- 当data下的某个属性变化时,如何触发相应的函数?
方案:ES5中新添加了一个方法:Object.defineProperty,通过这个方法,可以自定义getter
和setter
函数,那么在获取对象属性或者设置对象属性时就能够执行相应的回调函数
代码如下:
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
observer(options.data, this._update.bind(this))
this._update()
}
_update(){
this.$options.render()
}
}
function observer(obj, cb) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key], cb)
})
}
function defineReactive(obj, key, val, cb) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
console.log('你访问了' + key)
return val
},
set: newVal => {
if (newVal === val)
return
console.log('你设置了' + key)
console.log('新的' + key + ' = ' + newVal)
val = newVal
cb()
}
})
}
var demo1 = new Vue({
el: '#demo',
data: {
text: "before"
},
render(){
console.log("我要render了")
}
})
- 引发了第二个问题,如果
data
中的属性是一个对象还能触发我们的回掉函数么?比如说下边的demo
var demo2 = new Vue({
el: '#demo',
data: {
text: "before",
o: {
text: "o-before"
}
},
render(){
console.log("我要render了")
}
})
方案:用递归完善上边的响应式,需要在它开始对属性进行响应式转换的时候,前边加个判断,即如下
function observer(obj) {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'object') {
new observer(obj[key], cb)
}
defineReactive(obj, key, obj[key])
})
}
- 实际写的过程中发现调用data的属性时需要这样写
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
}
})
}
然后在构造函数中加上这么一句话
Object.keys(options.data).forEach(key => this._proxy(key))
到此,我们的data
属性已经变为响应式的了,只要data
的属性发生变化,那么就会触发render
函数。这也是为什么只有vue组件中的data
属性才是响应式的,其他地方声明的值均不是响应式的原因。但是这里有个问题,即触发render
函数的准确度问题!
第二步,解决准确度问题,引出虚拟dom
比如下边的demo
new Vue({
template: `
<div>
<span>name:</span> {{name}}
<div>`,
data: {
name: 'js',
age: 24
}
})
setTimeout(function(){
demo.age = 25
}, 3000)
template
中只用到了data
中的name
属性,但是当修改age
属性的时候,会不会触发渲染呢?答案是:会。但实际是不需要触发渲染机制的
解决这个问题,先要简单说下虚拟dom。vue有两种写法:
// template模板写法(最常用的)
new Vue({
data: {
text: "before",
},
template: `
<div>
<span>text:</span> {{text}}
</div>`
})
// render函数写法,类似react的jsx写法
new Vue({
data: {
text: "before",
},
render (h) {
return (
<div>
<span>text:</span> {{text}}
</div>
)
}
})
由于vue2.x引入了虚拟dom的原因,这两种写法最终都会被解析成虚拟dom,但在这之前,他们会先被解析函数转换成同一种表达方式,即如下:
new Vue({
data: {
text: "before",
},
render(){
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__toString__(this.text)])
])
}
})
透过上边的render
函数中的this.__h__
方法,可以简单了解下虚拟dom
function VNode(tag, data, children, text) {
return {
tag: tag, // html标签名
data: data, // 包含诸如 class 和 style 这些标签上的属性
children: children, // 子节点
text: text // 文本节点
}
}
写一个简单的虚拟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)])
])
}
})
回头看问题,也就是说,我需要知道render
函数中依赖了data
中的哪些属性,只有这些属性变化,才需要去触发render
函数
第三步,依赖收集,准确渲染
思路:在这之前,我们已经把data
中的属性改成响应式了,当去获取或者修改这些变量时便能够触发相应函数。那这里就可以利用这个相应的函数做些手脚了。当声明一个vue对象时,在执行render
函数获取虚拟dom的这个过程中,已经对render
中依赖的data
属性进行了一次获取操作,这次获取操作便可以拿到所有依赖。
其实不仅是render
,任何一个变量的改别,是因为别的变量改变引起,都可以用上述方法,也就是computed
和watch
的原理
首先需要写一个依赖收集的类,每一个data
中的属性都有可能被依赖,因此每个属性在响应式转化(defineReactive
)的时候,就初始化它。代码如下:
class Dep {
constructor() {
this.subs = []
}
add(cb) {
this.subs.push(cb)
}
notify() {
console.log(this.subs)
this.subs.forEach((cb) => cb())
}
}
function defineReactive(obj, key, val, cb) {
const dep = new Dep()
Object.defineProperty(obj, key, {
// 省略
})
}
那么执行过程就是:
- 当执行
render
函数的时候,依赖到的变量的get
就会被执行,然后就把这个render
函数加到subs
里面去。 - 当
set
的时候,就执行notify
,将所有的subs
数组里的函数执行,其中就包含render
的执行。
注:代码中有一个
Dep.target
值,这个值时用来区分是普通的get
还是收集依赖时的get
最后完整代码如下:
function VNode(tag, data, children, text) {
return {
tag: tag,
data: data,
children: children,
text: text
}
}
class Vue {
constructor(options) {
this.$options = options
this._data = options.data
Object.keys(options.data).forEach(key => this._proxy(key))
observer(options.data)
const vdom = watch(this, this._render.bind(this), this._update.bind(this))
console.log(vdom)
}
_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
}
})
}
_update() {
console.log("我需要更新");
const vdom = this._render.call(this)
console.log(vdom);
}
_render() {
return this.$options.render.call(this)
}
__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);
}
}
function observer(obj) {
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'object') {
new observer(obj[key])
}
defineReactive(obj, key, obj[key])
})
}
function defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
if (Dep.target) {
dep.add(Dep.target)
Dep.target = null
}
console.log('你访问了' + key)
return val
},
set: newVal => {
if (newVal === val)
return
console.log('你设置了' + key)
console.log('新的' + key + ' = ' + newVal)
val = newVal
dep.notify()
}
})
}
function watch(vm, exp, cb) {
Dep.target = cb
return exp()
}
class Dep {
constructor() {
this.subs = []
}
add(cb) {
this.subs.push(cb)
}
notify() {
this.subs.forEach((cb) => cb())
}
}
Dep.target = null
var demo = new Vue({
el: '#demo',
data: {
text: "before",
test: {
a: '1'
},
t: 1
},
render() {
return this.__h__('div', {}, [
this.__h__('span', {}, [this.__toString__(this.text)]),
this.__h__('span', {}, [this.__toString__(this.test.a)])
])
}
})