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'&®.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
- 收藏
- 分享
- 文章举报
- Vue 技术栈 教你玩"坏" v8引擎 吃透 js 内存回收机制
- Vue插件开发一系列api Vue.util.defineReactive 手写实现
- Vue 路由工作流程 hash 与 history
- Vue 技术栈 带你探究 vue-router 源码 手写vue-router
- 2020 零基础到快速开发 Vue全家桶开发电商管理系统(Element-UI)【目录】
- 什么场景下使用vuex
- 新建Vue项目,报错Expected indentation of 2 spaces but found 8
- Vue 设置背景图片样式
- Vue 新手学习笔记:vue-element-admin 给每个页面设置相应的 Title
- 第一个vue项目
- 关于vue-cli创建的项目位置的问题
- 安装vue-cli脚手架
- ngnix 部署 vue项目-基础篇
- Vue项目问题与分析
- Vue项目创建
- 首次加载前端vue项目浅谈
- 浅谈Vue组件的生命周期
- 浅谈vue-router
- Spring Boot+Vue前后端分离,如何避免前端页面 404
- vue一直报般配不匹配