从状态生成dom再输出到用户界面显示的流程叫做渲染,应用在运行时会不断进行重新渲染。而响应式系统赋予框架重新渲染的能力。变化侦测的作用就是侦测到数据的变化,当数据变化时,会通知视图进行相应的更新。
Vue.js的响应式原理依赖于Object.defineProperty,这个方法就是用来追踪变化的,该方法的更详细的MDN说明文档在Object.defineProperty。
Object.defineProperty(obj, prop, descriptor)
前面两个比较好理解,即对象和属性。最后的属性描述符需要另说明。
属性描述符分为
- 数据描述符 :数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的
- 存取描述符:存取描述符是由getter-setter函数对描述的属性
描述符必须是这两种形式之一;不能同时是两者
因为源码里没有用到数据描述符,就不说了
数据描述符和存取描述符均具有以下可选键值
存取描述符同时具有以下可选键值:
通过对Object.defineProperty的学习,再去理解追踪变化部分的源码就比较好理解了
function defineReactive (obj, key, val, cb) {
Object.defineProperty(obj, key, {
enumerable: true,//该属性出现在对象的枚举属性中
configurable: true,//该属性描述符可以被改变
get: ()=>{
/*....依赖收集等....*/
/*Github:https://github.com/answershuto*/
return val
},
set:newVal=> {
val = newVal;
cb();/*订阅者收到消息的回调*/
}
})
}
即追踪obj的key属性的变化,每当从obj的key中读取数据时,get函数被触发;每当往data的key中设置数据时,set函数被触发。
vue主要就是利用了每次改变对象属性,set函数都会对其进行一些处理的特性。
前面介绍的代码只能侦测数据中某一个属性,如果希望把数据中的所有属性,包括子属性都侦测到,要封装一个observer,作用是将一个数据内的所有属性(包括子属性)都转换成getter/setter形式,然后去追踪他们的变化。解释在注释上。
function observe(value, cb) {//遍历对象中的所有属性 执行defineReactive
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: ()=>{
/*....依赖收集等....*/
/*Github:https://github.com/answershuto*/
return val
},
set:newVal=> {
val = newVal;
cb();/*订阅者收到消息的回调*/
}
})
}
class Vue {
constructor(options) {//类的构造函数,options是new Vue对象时候的传参,传入的是一个对象{}
this._data = options.data;//对象.data=>{text:'text',text2:'text2'}
observe(this._data, options.render)//将data中的所有属性遍历变为可观察的。options.render 渲染的回调函数
}
}
let app = new Vue({//new一个vue对象 传入一个对象 下面是键值对
el: '#app',
data: {
text: 'text',
text2: 'text2'
},
render(){
console.log("render");
}
})
解释都在注释里面,为了更好理解说一下Object.keys,详细文档链接如下Object.keys。Object.keys也是返回该对象中的所有key组成的数组。
Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
这行代码的意思也就是:先取得value下面的所有key(属性),比如【text,text2】。遍历该数组,执行defineReactive(value, key, value[key] , cb)。即可将data对象中所有属性转成getter/setter模式
一层一层看下来,只要vue对象里的__data中某个属性改变,就会触发set。即对订阅者进行回调(在这里是render),也就是视图更新。
这里需要对class有理解,具体见js class
还有个问题是,从上面代码可以看出,需要对app._data.text操作才会触发set。为了偷懒,我们需要一种方便的方法通过app.text直接设置就能触发set对视图进行重绘。也就是我们平时用vue时候用到this.XXX=xxx,而不是this.data.XXX=xxx。那么就需要用到代理。
可以在Vue的构造函数constructor中为data执行一个代理proxy。这样就把data上面的属性代理到了vm实例上。
_proxy.call(this, options.data);/*构造函数中*/
//因为在构造函数里面。第一个传参是构造函数的this,构造函数的this就是即将生成的对象。
//可以理解为_proxy.call(即将生成的对象, options.data)
//call的作用是指定函数中的this指向第一个传参。这句代码的意思就是指定proxy中的this指向‘构造函数即将生成的对象’。options.data是传入proxy的参数
/*代理*/
function _proxy (data) {
const that = this;//即将生成的vue对象 可以理解为app
Object.keys(data).forEach(key => {//data是options.data
Object.defineProperty(that, key, {// that=>app key=>eg. text
configurable: true,
enumerable: true,
get: function proxyGetter () {//取值时
return that._data[key]; //app.text = app._data.text
},
set: function proxySetter (val) {//设值时
that._data[key] = val; //app._data.text = val
}
})
});
}