用周末两天时间,手写了一下简易版vue(vue2版本),使用简易版vue可以达到的效果就是,引入myvue.js文件以后,可以像使用官网的vue.js文件一样,实现了简易的模板编辑,支持我们自定义的v-指令,监听页面data变化,实时渲染dom,对理解vue核心设计思想很有帮助,这东西虽然一年前就写过,但是早就忘的差不多了,还是需要认真总结一下,用自己的话在阐述一遍,加深印象!
提到vue2响应式原理,首先要讲到 Object.defineProperty(),Object.defineProperty() 这个简单易用的api使我们前端产生了数据驱动的思想,Object.defineProperty用法:
接受三个参数object , propName , descriptor
1.object 对象 => 想要操作谁
2.propName 属性名 => 要加的属性的名字 【类型:String】
3.descriptor 属性描述 => 加的这个属性有什么样的特性【类型:Object】
我们要利用defineProperty,去拦截对象中的属性,来实现数据驱动视图的变化。
我们先实现一下vue.util.defineReactive();
//数据响应式实现
function defineReactive(obj,key,val){
Object.defineProperty(obj,key,{
get(){
console.log('get',key);
return val
},
set(newVal){
console.log('set',key);
if(newVal !== val){
val = newVal
}
}
})
}
const obj = {};
defineReactive(obj,'foo','foo');
obj.foo;
obj.foo = '111';
写完以后,node运行这个js文件可以观察到,在我们访问obj.foo和设置obj.foo的时候,我们的get和set就成功拦截到了,做到这我们就可以考虑去更新视图了,接下来我们去建一个html,页面里dom中innerHTML一个我们的data,来实现在修改data的时候,dom也来跟着更新
新建一个html,body中就放一个div#app,里面放我们可能修改的data
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script>
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log('get', key);
return val
},
set(newVal) {
console.log('set', key);
if (newVal !== val) {
val = newVal;
//变化的地方===》》》 这里调用update方法 修改页面内容
update();
}
}
})
}
const obj = {};
defineReactive(obj, 'foo', '');
obj.foo = new Date().toLocaleTimeString();
setInterval(() => {
obj.foo = new Date().toLocaleTimeString();
}, 1000);
function update() {
app.innerHTML = obj.foo;
}
</script>
</body>
</html>
defineReactive中的变化就是新增在set拦截器中调用update方法,他去更新我们的#app的innerHTML,写到这里,我们可以看到右边控制台可以每秒都 get 和 set foo,页面也在实时变化最新的时间值,但是现在我们是手动调用了一次defineReactive(obj, 'foo', ''); 来把它的foo属性变响应式的,但我们使用框架时候,我们data的的变量都是响应式的,不需要用户手动调用,在框架底层,一定是有一个遍历data的操作是框架帮我们做的,这就是接下来提到的observe,我们来定义一下observe方法
// 遍历obj所有key 做响应式处理
function observe(obj){
if(typeof(obj) !== 'object' || obj === null){
return
}
Object.keys(obj).forEach(key=>{
defineReactive(obj,key,obj[key]);
})
}
const obj = {
foo: 'foo',
bar: 'bar'
};
observe(obj);
obj.foo;
obj.foo = '1111';
有了observe以后,我们只需要调用一次,我们obj的所有属性就会被遍历,我们就不需要依次去调用给每一个属性指定响应式了,但是这两个方法还有不足,比如我们这样访问,就会发现一点小问题
// 问题1 嵌套问题 需要递归处理
const obj = {
foo: 'foo',
bar: 'bar',
baz:{
a: '1'
}
};
observe(obj);
obj.baz.a ==> get baz //问题是没有拦截到访问a 而是拦截到了baz
//问题2 对象覆盖
obj.baz = {
a : 10
}
obj.baz.a ==> get baz //问题是没有拦截到访问a 而是拦截到了baz
这两个问题分别是对象里的属性如果是一个对象就会有问题1,如果用户对一个属性用一个对象去覆盖就会有问题2,我们需要改写一下defineReactive,在两个地方递归处理,可以解决这些问题
//数据响应式实现
function defineReactive(obj,key,val){
//递归
observe(val);
Object.defineProperty(obj,key,{
get(){
console.log('get',key);
return val
},
set(newVal){
console.log('set',key);
//保证nweval是新对象 做响应式处理
observe(newVal);
if(newVal !== val){
val = newVal
}
}
})
}
到此为止,我们的defineReactive算是比较完善了,但是对于对象来讲,我们用户新增的属性依旧没有响应式的,vue是怎么处理这个问题的呢,vue给用户提供了$set方法,可以给新增的属性添加响应式,我们也来仿写一下
function set(obj, key, val){
defineReactive(obj, key, val);
}
// 这样我们添加新属性的时候需要这样使用
set(obj,'newProp','newValue');
有了这些准备工作,对于数据响应式有了一个初步入门的认识,接下来我们可以开始动手写自己的简易版vue了!