Vue 技术栈 手写响应式原理 到 探索设计模式

写在开头

学习完了ES 6基础,推荐阅读:ECMAScript 6 全套学习目录 整理 完结

现在开始逐步深入Vue 技术栈,想了想,技术栈专栏的主要内容包括:

1、Vue源码分析
2、手把手教 保姆级 撸代码
3、无惧面试,学以致用,继承创新
4、谈谈前端发展与学习心得
5、手写源码技术栈,附上详细注释
6、从源码中学习设计模式,一举两得
7、编程思想的提升及代码质量的提高
8、通过分析源码学习架构,看看优秀的框架
9、项目实战开发
10、面试准备,完善个人简历


暂时想到的就这么多,把这列举的10点做好了,我觉得也OK了,欢迎一起学习,觉得不错的话,可以关注博主,专栏会不断更新,可以关注一下,传送门~

学习目录

为了方便自己查阅与最后整合,还是打算整个目录,关于Vue技术栈前面的几篇优秀的文章:

正文

Vue 2的响应式原理


提到Vue2的响应式原理,或许你就会想到Object.defineProperty(),但Object.defineProperty()严格来说的话,并不是来做响应式的。

什么是defineProperty( )

推荐阅读:Vue 中 数据劫持 Object.defineProperty()

  • defineProperty其实是定义对象的属性,或者你可以认为是对象的属性标签
defineProperty其实并不是核心的为一个对象做数据双向绑定,而是去给对象做属性标签,只不过属性里的get和set实现了响应式
属性名 默认值
value undefined
get undefined
set undefined
writalbe true
enumerable true
configurable true

下面我们来详细了解一下:

var obj={
    a:1,
    b:2
}
//参数说明:1.对象 2.对象的某个属性 3.对于属性的配置
Object.defineProperty(obj,'a',{
	writable:false
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));

打开浏览器,按F12,将以上代码粘贴过去,查看控制台内容:


上述,打印的就是我们obj对象中a属性的一系列标签,权限方面可以看到默认的话为true

那么,我们刚刚设置了 writalbe为false,即设置了a属性不可写,进行简单测试一下:

发现我们无法对a属性进行value的修改,因为将writalbe设置了为false

当然,我们可以设置其他权限标签,例如:

var obj={
    a:1,
    b:2
}
//参数说明:1.对象 2.对象的某个属性 3.对于属性的配置
Object.defineProperty(obj,'a',{
	writable:false,
	enumerable:false,
	configurable:false
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));

因此,承接上文所述,defineProperty并不是来做响应式的,而是给对象中某个属性设置权限操作,是否可写,是否可以for in,是否可delete


get和set的使用

Vue中实现双向绑定,其实就是与get和set有很大关系

举个栗子,请看如下代码:

var obj={
    a:1,
    b:2
}
//参数说明:1.对象 2.对象的某个属性 3.对于属性的配置
Object.defineProperty(obj,'a',{
	get:function(){
		console.log('a is be get!');
	},
	set:function(){
		console.log('a is be set!');
	}
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));

我们在控制台,简单测试一下:

问题来了,细心的伙伴,应该发现了上图的问题,当我们get的时候,我们返回的是一个undefined,而且我们set一个值之后,也是获取不到新值,依旧是undefined,如下:

原因呢,其实就是我们的get函数是有返回值的,如果你不return的话,就会默认返回undefined,不管你怎么set都没用,那么如何解决这个问题呢,请看下面代码:

var obj={
    a:1,
    b:2
}
//借助外部变量存储值
let _value=obj.a;
//参数说明:1.对象 2.对象的某个属性 3.对于属性的配置
Object.defineProperty(obj,'a',{
	get:function(){
		console.log('a is be get!');
		return _value;
	},
	set:function(newVal){
		console.log('a is be set!');
		_value=newVal;
		return _value;
	}
});
console.log(Object.getOwnPropertyDescriptor(obj,'a'));

可以看到,我们必须借助一个外部变量,也就是中转站一样,才能达到我们的get和set效果,这也是vue2 中不太优雅的地方

然后,查看控制台,解决了上述问题

Vue中从改变一个数据到发生改变的过程

手写 Vue 2 中响应式原理

基于上述流程图,我们可以手写一个简单版的Vue2.0实现双向绑定的例子:

这里我就只实现逻辑,不具体去弄视图渲染了

文件名:2.js

//Vue响应式手写实现
function vue(){
	this.$data={a:1};
	this.el=document.getElementById('app');
	this.virtualdom="";
	this.observer(this.$data)
	this.render();
}
//注册get和set监听
vue.prototype.observer=function(obj){
	var value; //借助外部变量
	var self=this; //缓存this
	
	/*下面代码 a可能是data里的某个对象,不是属性
	因此在vue2.0中需要for in循环找到属性*/
	//Object.defineProperty(obj,'a') 
	
	for(var key in obj){
		value=obj[key];
		//判断是否为对象
		if(typeof value === 'object'){
			this.observer(value);
		}else{
			Object.defineProperty(this.$data,key,{
				get:function(){
					//进行依赖收集
					return value;
				},
				set:function(newVal){
					value=newVal;
					//视图渲染
					self.render();
				}
			}) 
		}
	}
}
//更新渲染部分
vue.prototype.render=function(){
	this.virtualdom="i am "+this.$data.a;
	this.el.innerHTML=this.virtualdom;
}

文件名:index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title>手写Vue响应式原理</title>
	</head>
	<body>
		<div id='app'></div>
		<script type="text/javascript" src="./2.js"></script>
		<script type="text/javascript">
			var vm = new vue();
			//设置set定时器
			setTimeout(function(){
				console.log('2秒后将值改为123');
				console.log(vm.$data);
				vm.$data.a=123;
			},2000)
		</script>
	</body>
</html>

查看页面,就会有如下效果:

那么,以后面试如果遇到手写响应式原理,把上述js代码写上去就ok了

源码分析:响应式原理中的依赖收集

手写的代码里面对于依赖收集这一块我们进行了省略,下面我们从源码的角度去看依赖收集到底是什么玩意:

/**
   * Define a reactive property on an Object.
   */
  function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    //依赖收集
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    var childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
		  //进行依赖收集
          dep.depend();
		  
		  /*采用依赖收集的原因:*/
		  
          //1.data里面的数据并不是所有地方都要用到
          //2.如果我们直接更新整个视图,会造成资源浪费
		  //3.将依赖于某个变量的组件收集起来
		  
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if (getter && !setter) { return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
  }

对依赖收集的总结

在初次渲染时,会触发一次get函数,为了提高效率,节省资源,采用依赖收集,这里以之前手写的为例,get部分,我们就会对this.$data里的每一个属性(即key值)进行收集,看在哪些组件里进行了调用,以此提高效率。

而在set部分,就会更新我们收集到的依赖

	Object.defineProperty(this.$data,key,{
				get:function(){
					//进行依赖收集
					return value;
				},
				set:function(newVal){
					value=newVal;
					//视图渲染
					self.render();
				}
			}) 

额外注意——关于数组的监听(探索设计模式)

从前文我们可以了解到,defineProperty定义的ger和set是对象的属性,那么数组该怎么办呢?

对于数组呢,在Vue中,你是没有办法像C/C++、Java等语言那样直接通过操作下标来触发更新,只能通过push、pop等方法来触发数据更新

var arr=[1,2,3];
arr.push(4);
arr.pop();
arr.shift();

这里 特别重要!

关于数组这一块里面巧妙运用到了一个设计模式——装饰者模式

//装饰者模式

//先取出原型
var arraypro=Array.prototype;
//拷贝一份,原因:避免影响到了原来的原型链
var arrob=Object.create(arraypro);
//定义一个需要装饰的方法的数组,这里只例举以下三个
var arr=['push','pop','shift'];
//设置重写方法(装饰者模式)
arr.forEach(function(methods,index){
	arrob[method]=function(){
		//先调用原来的方法
		var res=arraypro[method].apply(this,arguments);
		//触发视图更新
		dep.notify();
	}
})
//接下来将数组的prototype替换到data上的prototype(此处省略)
//这样的话,例如我们push方法,既能push又能触发视图更新了

对于设计模式呢,其实并不是很难,常说难懂,很难学,可能你学设计模式,你看了书,看到的可能就是简单事例,只是一个用法,没有训练思维,正确的做法是:

  • 提高我们的思维,提高代码质量
  • 先学透,记住一些定义和一些具体使用,然后去看,去探索
  • 非常好的一种方式就是结合源码,例如上文我们从Vue数组的监听原理里面剖析出来了装饰者模式
  • 学以致用

Vue 3的响应式原理


对于2.0响应式原理,我们暂告一段落,接下来,我们讨论Vue 3中的技巧,众所周知,Vue 3将defineProperty替换成了proxy

什么是proxy

用于定义基本操作的自定义行为

和defineProperty类似,功能几乎一样,只不过用法上有所不同

和上文一样,我们依旧写一个响应式,不过下面的代码是有问题的,读者可以先思考一下。

var obj={
    a:1,
    b:2
}
//无需借助外部变量

new Proxy(obj,{
	get(target,key,receiver){
		console.log(target,key,receiver);
		return target[key];
	},
	set(target,key,value,receiver){
		return Reflect.set(target,key,value);
		//return target[key]=value;
		/*上面注释的代码和上一行意思相同*/
	}
})

我们在控制台跑一下上述代码,发现它并没有输出console.log的内容,因此是有问题的

正确代码如下:

var obj={
    a:1,
    b:2
}
//无需借助外部变量
//对于vue 2,提高效率,无需for in 遍历找属性
//不会污染原对象,会返回一个新的代理对象,原对象依旧是原对象
//也是软件工程里的重要知识,尽量不要"污染"原对象,不用给原对象做任何操作
//只需对代理对象进行操作
var objChildren=new Proxy(obj,{
	get(target,key,receiver){
		console.log(target,key,receiver);
		return target[key];
	},
	set(target,key,value,receiver){
		return Reflect.set(target,key,value);
		//return target[key]=value;
		/*上面注释的代码和上一行意思相同*/
	}
})

总结:为什么Vue 3中使用proxy

  • defineProperty只能监听某个属性,不能对全对象进行监听
  • 可以省去for in遍历找对象中的属性,提高效率,省去很多代码
  • 可以监听数组,不用再去单独的对数组进行特异性操作
  • 不会污染原对象,会返回一个新的代理对象,原对象依旧是原对象
  • 只需对代理对象进行操作

手写 Vue 3 中响应式原理

下面代码,是在上文手写 Vue 2 响应式原理基础上修改的,通过对比,可以发现,我们省去了好多代码,不需要进行for in循环比较复杂、耗时间的操作了

//Vue响应式手写实现
function vue(){
	this.$data={a:1};
	this.el=document.getElementById('app');
	this.virtualdom="";
	this.observer(this.$data)
	this.render();
}
//注册get和set监听
vue.prototype.observer=function(obj){
	var self=this;
	this.$data=new Proxy(this.$data,{
		get(target,key){
			return target[key];
		},
		set(target,key,value){
			target[key]=value;
			self.render();
		}
	})
}
//更新渲染部分
vue.prototype.render=function(){
	this.virtualdom="i am "+this.$data.a;
	//this.el.innerHTML=this.virtualdom;
	this.el.innerHTML=this.virtualdom;
}

查看页面,就会有如下效果:

proxy这么好用,还能做什么呢?(再遇设计模式)

我们学习知识并不只是为了应付面试那种程度,对于面试应该作为我们的最低要求,接下来,我们接着去深度研究proxy还能干什么呢?

在 Vue 3 基本上已经不兼容IE8了,这里简单提及一下

  • 类型验证

这里我们就自定义一个实例:创建一个成人的对象,拥有name和age两个属性

要求:name必须是中文,age必须是数字,并且大于18

如果用纯原生js做验证的话,可想有多难去验证上述需求,或许你想到的是在构造函数里面去实现,但也不会简单,那么我们看看proxy怎么实现的:

//类型验证
//外部定义一个验证器对象
var validator={
	name:function(value){
		var reg=/^[\u4E00-\u9FA5]+$/;
		if(typeof value=='string'&&reg.test(value)){
			return true;
		}
		return false;
	},
	age:function(value){
		if(typeof value=='number'&&value>=18){
			return true;
		}
		return false;
	}
}

function person(name,age){
	this.name=name;
	this.age=age;
	return new Proxy(this,{
		get(target,key){
			return target[key];
		},
		set(target,key,value){
			if(validator[key](value)){
				return Reflect.set(target,key,value);
			}else{
				throw new Error(key+' is not right!');
			}
		}
	})
}

这里 特别重要!

关于类型验证这一块里面又巧妙运用到了一个设计模式——策略模式

关于设计模式这一块,此专栏不会细讲,但会在探索源码时发现了好的实例,会提出来一下。

上述用到了一个正则表达式,关于这个可能面试会问到,这是之前ES 6 里的内容,大家可以看看这篇简单易懂的文章:

推荐阅读:ES6 面试题:你能说出浏览器上到此支持多少个中文字吗?

  • 私有变量

关于私有变量这一块,我们就拿 vue-router 源码来进行分析:

//vue-router源码分析

Object.defineProperty(this,'$router',{//Router的实例
	get(){
		return this._root._router;
	}
});
Object.defineProperty(this,'$route',{
	get(){
		return{
			//当前路由所在的状态
			current:this._root._router.history.current
		}
	}
})

通过查看源码,提出疑问:为什么要为$router写get方法呢,而且没做什么操作,只是一个return?

原因:这样可以使得$router不可修改。避免程序员通过set修改了路由,导致路由失效的情况。这里就体现了数据安全思想,前端程序员或许考虑的没有Java程序员多,甚至没有为变量想过某个变量设置不可修改。由于工作的需要,我们也要努力提升自己的代码质量!让自己的职业生涯更加辉煌!

virtual dom 和 diff算法


关于diff算法和虚拟dom,也是面试常见的问题,平常容易忽视,这里我也就深入研究了一下:

虚拟dom

所谓虚拟dom,如字面意思,它是虚拟的,只在概念里面存在,并不真的存在,在vue中是ast语法树,关于这个语法树本文就不详细介绍了,有兴趣的读者可以深入研究一下。

下面代码,是一个简单vue template模板,那么解析成虚拟dom是怎样的呢?

<template>
	<div id='dd'>
		<p>{{msg}}</p>
		<p>abc</p>
		<p>123</p>
	</div>
</template>

解析成虚拟dom:

diff <div>
	props:{
		id:dd
	},
	children:[
		diff <p>
		props:
		children:[
			
		],
		text:xxx,
	]

上述代码就是概念上的介绍,如果懂一点算法知识的应该就明白了,就是不断地嵌套,但为了让更多伙伴读懂学会虚拟dom,下面来手写一个对象的形式:

<template>
	<div id='dd'>
		<p><span></span></p>
		<p>abc</p>
		<p>123</p>
	</div>
</template>

var virtual=
{
	dom:'div',
	props:{
		id:dd
	},
	children:[
		{
			dom:'p',
			children:[
				dom:'span',
				children:[]
			]
		},
		{
			dom:'p',
			children:[
			]
		},
		{
			dom:'p',
			children:[
			]
		}
	]
}

上述代码应该就很清晰了,简单来说,就是将最上面的dom结构,解析成下面用js解析成的对象,每一个对象都有一个基础的结构:

  • dom元素标签
  • props记录挂载了哪些属性
  • children记录有哪些子元素(子元素拥有和父元素相同的结构)

diff算法的比对机制

下面部分采用了伪代码形式介绍diff算法的比对机制,已经给出了详细的注释说明:

//diff算法匹配机制
patchVnode(oldVnode,vnode){
	//先拿到真实的dom
	const el=vnode.el=oldVnode.el;
	//分别拿出旧节点和新节点的子元素
	let i,oldCh=oldVnode.children,ch=vnode.children;
	//如果新旧节点相同,直接return
	if(oldVnode==vnode) return;
	/*分四种情况讨论*/
	//1.只有文字节点不同的情况
	if(oldVnode.text!==null&&vnode.text!==null&&oldVnode.text!==vnode.text){
		api.setTextContent(el,vnode.text);
	}else{
		updateEle();
		//2.如果新旧节点的子元素都存在,那么发生的是子元素变动
		if(oldCh&&ch&&oldCh!==ch){
			updateChildren();
		//3.如果只有新节点有子元素,那么发生的是新增子元素
		}else if(ch){
			createEl(vnode);
		//4.如果只有旧节点有子元素,那么发生的是新节点删除了子元素
		}else if(oldCh){
			api.removeChildren(el);
		}
	}
}

总结

学如逆水行舟,不进则退
发布了572 篇原创文章 · 获赞 1651 · 访问量 26万+

猜你喜欢

转载自blog.csdn.net/weixin_42429718/article/details/104712536