您的位置:首页 > Web前端 > Vue.js

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

2020-04-07 18:32 1486 查看

写在开头

学习完了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);
}
}
}

总结

学如逆水行舟,不进则退
  • 点赞 4
  • 收藏
  • 分享
  • 文章举报
一百个Chocolate 博客专家 发布了600 篇原创文章 · 获赞 2156 · 访问量 34万+ 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: