vue深入响应式原理
当一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。
vue存在不能检测数组和对象的变化,
- 对于对象 vue会在初始化实例时,对 property 执行 getter/setter 转化,所以 property 必须在data 对象上存在
new Vue({
data:{
a:1
b:{}
}
})
this.a = 2 //响应式
this.b.name = 'Jack' //非响应式
已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。例如,对于:
this.$set(this.someObject,'b',2)
this.someObject = Object.assign({}, this.someObject ,{a:1,b:2})
- 数组 vue不能检测一下数组的变动
- 利用索引直接设置一个数组项时
this.items[indexOfItem] = newValue - 当你修改数组的长度时
this.items.length = newLength
可用以下方式代替
this.$set(this.items, indexOfItem, newValue)
this.items.splice(indexOfItem, 1 ,newValue)
this.splice(newlength)
用 pop、push、shift、unshift、splice、sort、reverse会触发视图更新,
数据动态绑定简单实现
DOM结构
<div id="app">
<form>
<input type="text" v-model="name.age.value" />
<input type="text" v-model="name.value" />
<button type="button" v-click="increment">increment</button>
<button type="button" v-click="alert">alert</button>
</form>
<p v-bind="name.age.value"></p>
<p v-bind="name.value"></p>
</div>
用类似Vue的语法创建一个实例:
window.onload=function(){
var app=new Example({
el:"#app",
data:{
count:0,
name:{
value:'lowesyang',
age:{
value:20
}
}
},
methods:{
increment:function(){
this.name.age.value++;
},
alert:function(){
alert(this.name.value)
}
}
})
}
1. 实例配置、根节点、数据、函数方法名
function Example(options){
this._init(options);
}
Example.prototype._init=function(options){
this.$options=options; //传入的实例配置
this.$el=document.querySelector(options.el); //实例绑定的根节点
this.$data=options.data; //实例的数据
this.$methods=options.methods; //实例的函数
/** 对象深层次属性的取值和修改
* 直接对Object进行扩展$get和$set
*/
Object.prototype.$get=function(path){
var getter=new Function('return this.'+path);
return getter.call(this);
};
Object.prototype.$set=function(path,val){
var setter=new Function('newVal','this.'+path+' = newVal;');
setter.call(this,val);
}
//与DOM绑定的数据对象集合
//每个成员属性有一个名为_directives的数组,用于在数据更新时触发更新DOM的各directive
this._binding={};
this._parseData(this.$data);
this._compile(this.$el); //编译DOM节点
};
2. data中的数据对象进行遍历调用,使用Object.defineProperty对data中的数据对象进行改造,添加getter/setter函数
//遍历
Example.prototype._parseData=function(obj){
var OBJECT = 0, DATA =1
var value;
path = path || '';
for(var key in obj){
//排除原型链上的属性,仅仅遍历对象本身拥有的属性
if(obj.hasOwnProperty(key)){
this._binding[path+key]={ //初始化与DOM绑定的数据对象
_directives:[]
};
value=obj[key];
//值为对象 递归解析
if(typeof value ==='object'){
this.convert(obj,key,value,path+key,OBJECT);
this._parseData(value,path+key+'.');
}
else this.convert(obj,key,value,path+key,DATA);
}
}
};
//添加getter setter函数,数组重写原型方法
Example.prototype.convert=function(obj,path,val,absolute,type){
var binding=this._binding[absolute];
if(type==1) { //如果不是深层次对象
Object.defineProperty(obj, path, {
get: function () {
console.log(`获取${val}`);
return val;
},
set: function (newVal) {
console.log(`更新${newVal}`);
if(val!=newVal){
val=newVal;
binding._directives.forEach(function(item){
item.update();
})
}
}
})
}
else{ //如果是深层次对象
var subObj=obj[path]||{};
// 绑定每个子对象
Object.defineProperty(obj,path,{
get:function(){
console.log(`获取${subObj}`);
return subObj;
},
set:function(newVal){
console.log(`更新${newVal}`);
if(typeof newVal==='object') {
//不能直接subObj=newVal,否则将无法触发已有属性的响应式更新
for (var kkey in newVal) {
subObj[kkey] = newVal[kkey]
}
}
else subObj=newVal;
binding._directives.forEach(function(item){
item.update();
})
}
});
}
};
// 重写数组原型的方法,Object.defineProperty不具备监听数组的方法
const oldArrayProperty = Array.prototype;
const arrProto = Object.create(oldArrayProperty);
["push","pop","shift","unshift","splice"].forEach(
methodName =>
(arrProto[methodName] = function() {
updateView();
oldArrayProperty[methodName].call(this, ...arguments);
})
)
3. 绑定函数改造, 将参数与函数名分隔开,this.$method调用
//attVal alert('Hello world')
Example.prototype._parseFunc=function(attrVal){
var args=/(.*)/.exec(attrVal);
if(args) { //如果函数带参数,将参数字符串转换为参数数组
args=args[0];
//('Hello world')
attrVal=attrVal.replace(args,"");
//'alert'
args=args.replace(/[\(\)\'\"]/g,'').split(",");
//alert
}
else args=[];
return this.$methods[attrVal].bind(this.$data,args);
};
4. 实例化一个_binding对象,Directive建立一个DOM节点和对应数据的映射关系,数据初始化与DOM绑定的数据对象, 之后去setter方法中添加directives,这样设置value时,就能引起对应DOM节点更新。
if(obj.hasOwnProperty(key)){
this._binding[key]={ //初始化与DOM绑定的数据对象
_directives:[]
};
function Directive(name,el,vm,exp,attr){
this.name=name; //指令名称,例如文本节点,该值设为"text"
this.el=el; //指令对应的DOM元素 input
this.vm=vm; //指令所属Lue实例
this.exp=exp; //指令对应的值,count
this.attr=attr; //绑定的属性值 value
this.update(); //首次绑定时更新
}
Directive.prototype.update=function(){
//更新DOM节点的预设属性值
this.el[this.attr]=this.vm.$data[this.exp];
};
5. 编译DOM节点,初始化时,对DOM进行编译compile,获取子节点,遍历属性(v-click \ v-model),给节点添加相对应的onclick/addEventListener属性,对绑定的函数进行改造
//解析DOM的指令
Example.prototype._compile=function(root){
var _this=this;
//获取指定作用域下的所有子节点
var nodes=root.children;
for(var i=0;i<nodes.length;i++){
var node=nodes[i];
//若该元素有子节点,则先递归编译其子节点
if(node.children.length){
this._compile(node);
}
if(node.hasAttribute("v-click")) {
node.onclick = (function () {
var attrVal=nodes[i].getAttribute("v-click");
// attrVal = alert('Hello world')
return _this._parseFunc(attrVal);
})()
}
if(node.hasAttribute("v-model")&& (node.tagName=="INPUT" || node.tagName=="TEXTAREA")){
//如果是input或textarea标签
node.addEventListener("input", (function (key) {
var attrVal=node.getAttribute("v-model");
//将value值的更新指令添加至_directives数组
_this._binding[attrVal]._directives.push(new Directive(
"input",
node,
_this,
attrVal,
"value"
))
return function () {
//_this.$data[attrVal] = nodes[key].value;
_this.$data.$set(attrVal,nodes[key].value);
}
})(i));
}
if(node.hasAttribute("v-bind")){
var attrVal=node.getAttribute("v-bind");
//将innerHTML的更新指令添加至_directives数组
_this._binding[attrVal]._directives.push(new Directive(
"text",
node,
_this,
attrVal,
"innerHTML"
))
}
}
}
Object.defineProperty
Object.defineProperty()会在一个对象上定义属性
Object.defineProperty(定义属性的对象, 定义或修改的属性的名称, 定义或修改的属性描述符);
const object = {}
Object.defineProperty(object, 'property', {
value:42
writable:false
});
object.property = 77
- writable writable 属性设置为 false时,不能被重新赋值。
- enumerable enumerable 定义了对象的属性是否可以在 for...in循环和 Object.keys()中被枚举。
- 自定义setter和getter