#vue通信传值
1.props&$emit
1.1 父传子props
现在我们要从Index页面给A页面传递一个数组list
// index.vue<template><div><A:list="list"/></div></template><script>importAfrom"./components/A";exportdefault{name:"Index",components:{A},data(){return{list:["html","css","js"],};},},</script>
// A.vue<template><div><h2>PageA</h2><ul v-for="(item, index) in list":key="index"><li>{{item}}</li></ul></div></template><script>exportdefault{name:"A",props:["list"],};</script>
效果图如下, 此时list数组以自上而下的一种方式从Index页面传递给A组件, 且props只可以从上一级想下一级传输, 即所谓的单向数据流
image.png
1.2 子传父$emit
那A组件想给Index页面传送数据应该如何操作? 关于子组件给父组件传值, 一般都是通过一个事件搭配$emit进行传输
// A.vue<template><div><h2>PageA</h2><ul v-for="(item, index) in list":key="index"><!--定义事件--><li @click="onItemClick(item)">{{item}}</li></ul></div></template><script>exportdefault{name:"A",props:["list"],methods:{onItemClick(item){this.$emit("on-item-click",`'${item}' from page A`);},},};</script>
父组件监听子组件上的事件名on-item-click
// index.vue<template><div><!--监听--><A:list="list"@on-item-click="handleItemClick"/></div></template><script>importAfrom"./components/A";exportdefault{name:"Index",components:{A,},data(){return{list:["html","css","js"],};},methods:{handleItemClick(value){console.log(`In page Index get ${value}`);},},};</script>
一、为什么要使用vue-bus?
学习vue的开发者都知道,父子组件的直接的通讯直接使用vue提供的props属性和emit方法。props接受来自父组件的参数,emit将子组件的参数传递给父组件。这样一来父子组件之间的参数传递就得到了解决。(之前做项目的时候看到$parent和$children也可以进行父子组件和兄弟之间的参数传递,但是不提倡,原因在于如果组件想要替换位置就有问题)。
那么问题来了,兄弟组件如何进行通讯?有哪些方法
1、vuex全局状态管理
2、bus总线机制/发布订阅者模式/观察者模式
两者相比较,前者适用于大型项目的开发,如果项目业务没那么复杂,推荐使用bus来进行解决这类问题。
二、vue-bus如何使用?
第一步:使用npm install vue-bus --save
第二步:在main.js进行全局注册
第三步:在一个页面引用两个兄弟组件
第四步:使用emit进行参数传递
第五步:在created或mounted生命周期钩子,执行事件监听。最后记得将触发的事件销毁,不然会出现点击多次触发的情况。
三、bus总线机制/发布订阅者模式/观察者模式
比如有一个bus对象(中央事件总线),这个对象上有两个方法,一个是on(监听,也就是订阅),一个是emit(触发,也就是发布),就好比我们订阅报纸,到报社去付钱,才知道你要订阅的。
观察者模式是用来监听数据变化,改变视图层。
#vuex
1、vuex 是什么?
集中式存储管理应用的所有组件的状态(单独出来管理共用的状态,如:token的管理等,相当于一个全局对象),以相应的规则保证状态以一种可预测的方式发生变化(不能直接修改数据,需要通过特定的方法, this.
store.commit() ),无法持久化存储。
vuex 和 Cookie,localstorage 的区别
Cookie:采用键值对的方式存储( document.cookie = 键= 值; 键= 值),可以定义过期时间(document.cookie ="expires=有效期"),存储大小4k
localstorage:使用方法进行键值对的方式存储(getItem(键,值)),永久存储,但是可以根据方法清除, 存储大小5m
方法描述实例
setItem()将对应的名字和值传递进去存储localStorage.setItem('users','我是user')
getItem()名称作为参数,可以获取对应的值localStorage.getItem('users')
removeItem()名称作为参数,可以删除对应的数据localStorage.removeItem('users')
clear()可以删除所有存储的数据localStorage.clear()
sessionstorage:跟 localstorage 方法一样,但是是单页面回话存储,跳转页面被清除,
vuex:相当于全局变量,刷新页面清除,想长久保持数据存在,只能使用Cookie和localstorage存储,然后再赋值回去
如:
getters :{userName: (state,getters,rootState)=>{console.log(state.userId)let a =(state.userId?state.userId:localStorage.getItem('users'))return a}},
2、vue 使用
state 唯一数据源
像我们vue的data
定义conststate={token:'我是token'}conststore=newVuex.Store({state})直接使用this.$store.state.token=='我是token'引入使用import{mapState}from'vuex'computed:{...mapState(['token'])//不取别名 this.token== '我是token'...mapState({userToken:'token'//取别名,this.userToken == '我是token'})},
getter 计算属性
从 store 中的 state 中派生出一些状态
定义conststate={token:'我是token'}constgetter={tokenName(state,getters)=>{returnstate.token+'的getter '//在token后面加数据}}conststore=newVuex.Store({state, getter})直接使用this.$store.getter.tokenName=='我是token的getter'引入使用import{mapGetters}from'vuex'computed:{...mapGetters(['tokenName'])//不取别名 this.tokenName == '我是token的getter'...mapGetters({userTokenName:'tokenName'//取别名,this.userTokenName =='我是token的getter'})},
mutation 唯一store修改方法
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation,mutation 必须同步执行
定义conststate={token:'我是token'}constmutation={setToken(state,val){state.token=val}}conststore=new Vuex.Store({state,mutation})直接使用this.$store.commit('setToken','白日梦')// token == '白日梦'引入使用import{mapMutations}from'vuex'methods:{...mapMutations(['setToken'])//不取别名 this.setToken('白日梦') => token == '白日梦'...mapMutations({setName:'setToken'//取别名, this.setName('白日梦') => token == '白日梦'})},
Action 提交的是 mutation
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。
定义conststate={token:'我是token'}constmutation={setToken(state,val){state.token=val}}constAction={ActionToken({commit},val){commit('setToken',val)}}conststore=new Vuex.Store({state,mutation,Action})直接使用this.$store.dispatch('ActionToken','白日梦')// token == '白日梦'引入使用import{mapActions}from'vuex'methods:{...mapActions(['ActionToken'])//不取别名 this.setToken('白日梦') => token == '白日梦'...mapActions({setName:'ActionToken'//取别名, this.setName('白日梦') => token == '白日梦'})},
modules 模块化
每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
当 namespaced: false 默认值为false ,除了state取值需要添加模块名称,别的不需要添加
constuser={namespaced:false, state:()=>({name:'小青'}),getters:{names(state){returnstate.name+'青'}},mutations:{setName(state,val){state.name=val}},actions:{setAcName({commit},val){commit('setName',val)}},}conststore=new Vuex.Store({modules:{user}})使用this.$store.state.user.name=='小青'this.$store.getters.names=='小青青'this.$store.commit('setName','小周')// name == '小周'this.$store.dispatch('setAcName','小周')// name == '小周'
当 namespaced: true ,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:
constuser={namespaced:true, state:()=>({name:'小青'}),getters:{names(state){returnstate.name+'青'}},mutations:{setName(state,val){state.name=val}},actions:{setAcName({commit},val){commit('setName',val)}},}conststore=new Vuex.Store({modules:{user}})使用this.$store.state.user.name=='小青'this.$store.getters['user/names']=='小青青'this.$store.commit('user/setName','小周')// name == '小周'this.$store.dispatch('user/setAcName','小周')// name == '小周'
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
应用层级的状态应该集中到单个 store 对象中。
提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
异步逻辑都应该封装到 action 里面。
只要你遵守以上规则,如何组织代码随你便
#axios和ajax
本文主要针对Ajax,Promise,Axios三者的本质、优缺点,使用实战做了阐述,抽象了应用办法,高度横向做了对比,一起进入学习吧~
一、Ajax
AJAX:异步 JavaScript 和 XML,用来发送异步请求。有了Ajax之后,在无需重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。
Ajax是基于现有的Internet标准,联合使用了XMLHttpRequest,JS/DOM,CSS,XML等技术。
它有个较大的缺陷就是业务逻辑需要在回调函数中执行,如果多个请求存在依赖关系,就会产生回调地狱问题,对读写理解困难。
1. 创建XHR
由于IE7以下版本不支持XMLHttpRequest对象,使用 ActiveXObject,因此需要做兼容处理:
letrequest;// code for IE7+, Firefox, Chrome, Opera, Safariif(window.XMLHttpRequest){request=newXMLHttpRequest();}else{// code for IE6, IE5request=newActiveXObject("Microsoft.XMLHTTP");// 新建Microsoft.XMLHTTP对象}
2. XHR对象常用方法
(1)XMLHttpRequest.open()
初始化一个请求。已激活的请求再次调用此方法时,相当于调用了abort(),会中断上一个请求。
当设置了async(第三个参数)true之后,请规定onreadystatechange事件中的就绪状态执行响应函数。
(2)XMLHttpRequest.send()
向服务器发送http请求。如果open()中定义的是异步请求,则此方法会在请求发起后立刻返回;如果是同步,则在请求返回后才执行。
(3)XMLHttpReqeust.abort()
当请求已经发出,则立刻中断请求。将readystate设置为0,立刻调用onreadystatechange方法执行回调函数。
(4)XMLHttpRequest.setReqeustHeader()
用于给HTTP请求增加自定义请求头,在open()之后,send()之前定义。设置多个相同的请求头,在发起时会进行合并。
(5)XMLHttpRequest.getResponseHeader()
根据名称获取HTTP请求头,如果需要一次请获取全部,请使用getAllHeetResponseHeader()方法。
3. 封装一个原生的Ajax请求
结合以上的内容,封装一个兼容Post和Get的Ajax请求如下:
ajaxRequest(method,url,data,callback){letxhr;if(window.XMLHttpRequest){xhr=newXMLHttpRequest();}else{xhr=newActiveXObject("Microsoft.XMLHTTP");}if(method.toLocalCase()==="get"){url=url+this.getParams(data);xhr.open(method,url,true);xhr.send();}else{// postxhr.open(method,url,true);xhr.setRequestHeader("content-type","application/x-www-form-urlencoded");data?xhr.send(data):xhr.send()}xhr.onreadystatechange=function(){if(xhr.readyState===4){if(xhr.state===200){// 约定返回字符串格式,如是xml格式使用responseXMLcallback(xhr.responseText);}else{reject(newError(xhr.statusText))}else{if(xhr.readyStatue===0){alert("请求已取消");}}}}buildGetParams(obj){if(!obj||obj.keys.length===0)return'';letstr=Object.keys(obj).reduce(function(prev,cur,index){if(index===1){prev=prev+'='+obj[prev]}returnprev+(prev?'&':'')+cur.toString()+'='+obj[cur]})}
模拟使用post调用
this.ajaxRequest('post','http://demo-domain:8080/test/ajax_post',{name:'test'},function(data){letresult=JSON.parse(data)console.log(result)});
二、Promise
Promise对象由ES6提供,它表示一个尚未完成且预计在未来完成的异步操作。Promise 仅仅是一种决定异步任务状态确定时会出现什么结果的技术。它本身并不提供异步请求功能,更不能替代Ajax。
Promise将异步请求的回调操作,改成了同步的链式写法,最直观的就是使用Promise处理ajax请求时,可以解决回调地狱问题;另外,Promise封装了统一的接口,使得控制异步操作变得更加简单。
但它的缺点也比较明显,发起的Promise无法取消中断,其次,如果不设捕获异常的回调函数,Promise内部抛出的错误无法反应到外部出来。另外,未执行完成的Promise也无法确定是刚开始还是即将结束(即ajax的readyState在哪个过程)。
1. 创建Promise
Promise自身提供了封装好的Promise()构造器,使用new关键字创建。我们可以自定义一个异步函数,传入Promise的构造器如:Promise(asyncFn)
letpromise=newPromise((resolve,reject)=>{// 执行结束需要使用resolve或reject将结果返回})
2. Promise的基本API
Promise有三种状态,分别是pending:执行状态、fulfilled:已成功、rejected:已失败。
pending只能改变为fulfilled或rejected其中一种,且状态一旦改变,将凝固不再改变。即使再次修改,该操作也会被忽略。
(1)Promise.prototype.then(onFulfilled, onRejected)
当Promise的状态凝固后,就会调用.then方法。该方法接受两个回调函数,即成功回调函数resolve(),和失败回调函数reject(),并且返回一个Promise。通常情况下,我们通过一定的条件判断如res.code为1表示成功,将结果作为参数即resolve(res.data)传递出去。否则使用reject(res.message)返回错误。
当然,.then()的第二个参数非必选,你也可以通过.catch()API来实现
(2)Promsie.prototype.catch(onRejected)
该API表示Promise从pending状态改变为rejected状态时调用,接受一个失败回调函数,返回一个Promise,使用效果同.then()的第二个参数。
(3)Promise.prototype.all()
该方法接受一个数组,用于将多个Promise包装成一个Promise来执行,只有所有的请求状态都凝固后才改变自己的状态。并且只有在全部请求都返回fulfilled,该函数才返回fulfilled
(4)Promise.prototype.race()
竟速模式,该方法接受一个可遍历对象,将多个Promise包转成一个Promise来执行,最先成功(fulfilled)返回的请求当作该Promise的响应返回
......
3. 一种建议的Promise链式写法
虽然.then()方法很强大,但并不建议在其中定义处理异常方法,原因是如果在.then()的onFulfilled回调函数中发生了异常,其内部定义的onRejected是无法捕获到的。但是在.then()后面使用.catch()则可以捕获到之前发生的所有异常。一种健康的写法是:
functiontaskA(){...};functiontaskB(){...};functionfinalTask(){...};varpromise=newPromise();promise.then(taskA).catch(onRejectedA).then(taskB).catch(onRejectedB).then(finalTask)
【注】不要使用promise.then(taskA).then(taskB).then(finalTask).catch(onRejected),虽然这么定义所有的异常也都会被捕获到,但如果是taskA发生了异常,那taskB,fianlTask也将不被执行。
4. 使用Promise写法实现Ajax请求
为了简便以下只以Get请求为例,
getRequest(url){returnnewPromise((resolve,reject)=>{letxhr=newXMLHttpRequest();xhr.open('get',url,true);xhr.send();xhr.onreadystatechange(){if(xhr.readyState===4){if(xhr.status===200){resolve(JSON.parse(xhr.responseText));}else{reject(newError(xhr.statusText))}}}})}// 调用this.getRequest("http://demo-domain:8080?name=test").then(resp=>{console.log(resp)})
三、Axios
Axios是一个基于Promise的http库,支持node端和浏览器端,支持拦截器、自定义请求头、取消请求等高级配置,支持自动转换JSON,也支持npm包的使用。总之,优点多多,用就对了...
针对Axios,在本文笔者不想写简单通用的功能,如有需要可以去axios官网学习了解。笔者想阐述更高效的用法如下
1. 自定义axios实例
使用自定义配置新建一个简单的axios实例
letrequest=axios.create({baseURL:'http://some-domain.com/api/',timeout:1000,headers:{'X-Custom-Header':'footbar'}})
2. 常用axios的请求配置
以下配置中,只有url是必须的,如果没有配置method,将默认使用get方法。以下为常用的配置,更多请参见官网。
url: '/user'用户请求服务器资源的 URL
method: 'post'创建请求时使用的方法
baseURL: 'http://demo-domain:8080/api/'自动加在URL(非绝对路径时)前的路径
headers: { 'x-Requested-With': 'XMLHttpRequest' }自定义请求头
params: { 'ID': '12345' }与请求一起发送的URL参数,当method指定为GET时使用
data: { 'name': 'zhangfs' }请求主体参数,用于PUT,POST,PATCH方法
timeout: 8000请求超时时间,0表示无超时;超过时间请求被中断
withCredentials: false请求跨域时是否需要使用凭证
auth: { username: 'zhangfs' }用于HTTP基础验证,将复写Authorization头
proxy: {
host: '127.0.0.1',
port: 9000,
}代理服务器的主机名与端口,将设置Proxy-Authoriation
cancelToken: new CancelToken(function(cancel) { ... })指定取消请求的cancel token
全局默认配置
使用axios.defaults.xxx来配置;注意在实例中配置的优先级高于默认配置优先级
letrequest=newaxios({baseURL:'http://demo-domain.com:8080/api'})request.defaults.header.common['Authorization']=AUTH_TOKENrequest.defaults.header.post['Content-Type']='application/x-www-form-urlencoded'
3. 请求拦截器
在请求或响应被 then 或 catch 处理前拦截它们。如果自定义了axios实例如request,则使用request.interceptors.xxx
// 添加请求拦截器axios.interceptors.request.use(function(config){// 在发送请求之前做些什么returnconfig;},function(error){// 对请求错误做些什么returnPromise.reject(error);});// 添加响应拦截器axios.interceptors.response.use(function(response){// 对响应数据做点什么returnresponse;},function(error){// 对响应错误做点什么returnPromise.reject(error);});
当然,也可以对拦截器进行移除,此时需要对拦截器显命名
varmyInterceptor=axios.interceptors.request.use(function(){/*...*/});axios.interceptors.request.eject(myInterceptor);
4. 取消请求
ajax调用请求的abort()通过改变readyState=0来进行中断请求,axios则通过cancel token取消。
Axios 的 cancel token API 基于cancelable promises proposal,它还处于第一阶段。
可以使用CancelToken.source工厂方法创建 cancel token
varCancelToken=axios.CancelToken;varsource=CancelToken.source();axios.get('/user/12345',{cancelToken:source.token}).catch(function(thrown){if(axios.isCancel(thrown)){console.log('Request canceled',thrown.message);}else{// 处理错误}});// 取消请求(message 参数是可选的)source.cancel('Operation canceled by the user.');
还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token
varCancelToken=axios.CancelToken;varcancel;axios.get('/user/12345',{cancelToken:newCancelToken(functionexecutor(c){// executor 函数接收一个 cancel 函数作为参数cancel=c;})});// 取消请求cancel();
值得注意的是,可以使用同一个cancel token取消多个请求。
5. 一个axios实战实例
请求封装文件service.js
constservice=axios.create({headers:{},baseURL:Vue.prototype.configer.baseURL,timeout:8000,withCredentials:true})service.interceptors.request.use(config=>{if(config.method==='get'){if(config.url.indexOf('?')<0){config.url+='?r='+newDate().getTime()}else{config.url+='&r='+newDate().getTime()}}returnconfig},error=>{returnPromise.reject(error)})service.interceptors.response.use(response=>{// 处理返回体constresult=response.dataconstcode=Number(result.code)/**
* Desc: 接口请求业务异常统一处理
* Desc: 如:50008: 非法Token; 50012: 第三端登录; 50014: Token已过期;
*/if(code===3002||code===50008||code===50012||code===50014||code===302){alert('您已退出登录')}else{if(result.code&&result.code!=='200'){console.log(result.message)}returnresult}},errorFn// HTTP Code异常统一处理)
api封装文件 api.js
importservicefrom'@/plugins/service'importaxiosfrom'axios'constCancelToken=axios.CancelTokenexportfunctionqueryDemoApi(data,_this){returnrequest({url:'/packageRoute/queryDemoApi',method:'post',data,cancelToken:newCancelToken(functionexecutor(c){_this.cancelAjax=c})})}
组件内使用 info.vue
data(){return{cancelRequest:null}}method:{pageTriggleSearch(username){if(typeofthis.cancelRequest==='function'){this.cancelRequest()}this.queryInfoApi({name:username},this).then((data)=>{this.cancelRequest=null// do something else})}}
#vue-router
SPA
SPA单页面应用程序:只有一个页面(index.html),使用vue-router实现组件的创建与销毁,在视觉上给人一种“页面切换”的错觉。所以,对SPA应用来讲,vue-router是核心。
思想:在SPA应用程序中,一切皆组件,所有的组件都直接或间接地与“路由系统”有关。
路由规则是如何配置的?
创建router实例,在main.js挂载
什么是“嵌套路由”?在“路由规则”如何配置“嵌套路由”?
children叫做“嵌套路由”
自定义路由规则,如何理解路由规则?
所谓的路由规则,当你访问某个url时,路由系统就加载指定的组件进行显示
两个全局组件
<router-link>
常用属性to/tag/active-class/exact-active-class等
表示目标路由的链接。当被点击后,内部会立刻把 to 的值传到 router.push(),所以这个值可以是一个字符串或者是描述目标位置的对象。
<router-view>
<router-view> 组件是一个 functional 组件,渲染路径匹配到的视图组件。<router-view> 渲染的组件还可以内嵌自己的 <router-view>,根据嵌套路径,渲染嵌套组件。
其他属性 (非 router-view 使用的属性) 都直接传给渲染的组件, 很多时候,每个路由的数据都是包含在路由参数中。
因为它也是个组件,所以可以配合 <transition> 和 <keep-alive> 使用。如果两个结合一起用,要确保在内层使用 <keep-alive>
<transition>
<keep-alive>
<router-view></router-view>
</keep-alive>
</transition>
两个内置API
$route描述是当前url的信息(它是一个响应式变量,可以参与计算属性,还可以使用watch监听)
$router是路由API,常用方法有 push()入栈、replace()替换入栈、back()出栈。
this.router.push(′/hot?list=′+c.cate)改变url,接着route变化
两种路由模式
Hash模式
url上有个#,背后是监听onhashchange事件来实现的;hash值发生变化,客户端不会向服务器发起页面请求;hash模式的SPA应用部署到服务器上时不会出404问题
History模式
url上没有#,背后是使用history api来实现的;当url路径发生变化时,客户端会向服务器发起页面请求;history模式的SPA应用部署到服务器上时会出现404,怎么办?(在服务器使用Nginx做重定向处理)
两种路由跳转
声明式路由跳转,使用实现页面跳转,一般用于菜单上
编程式路由跳转,使用this.$router这个api实现页面跳转,一般用于js逻辑中
两种命名
命名路由
命名路由,就是给“路由规则”添加一个名字
在声明式导航中使用“命名路由”,<router-link :to='{name:'vip'}'>
在编程式导航中使用“命名路由”,this.push({name:'vip'})
命名视图
命名视图,给添加一个名字。那么路由规则这样配置{path,components:{xxx:'组件'}}。
两种路由传参
动态路由传参
“动态路由”一般用于从列表页跳转到详情页,“路由规则”配置一般这种风格{path:'/detail/:id'}。那么在详情页我们可以使用this.$route.params来接收这个“动态的路由参数”。还在在“路由规则”上开启props传参{path:'/detail/:id', props:true},在详情页使用props接收这个“动态的路由参数”。
Query传参
在路由跳转时使用?a=1&b=2这种查询字符串,在另一个页面中使用this.$route.query来接收
两个优化
重定向
给未定义的url添加默认跳转,跳转到指定的路由规则上,像这样配置{path:'/*',redirect:'/404'}。这是一种用户体验层面的优化。
一般放在路由规则的最后一条,并且重定向到一个已定义过的规则
路由懒加载
当页面太多时,我们要根据url访问需求按需加载组件。背后技术原理是Vue异步组件和Webpack代码分割技术。扩展:这种()=>import()代码是异步的,并且在webpack打包时只要遇到这种语法,会默认将其分割一个独立的小的.js文件。
两个高级技巧
导航守卫
我们知道每次发生url变化时,路由系统要根据“路由规则”去寻找对应的组件进行显示。vue-router把这个匹配的过程抽象成几个重要的“路由钩子”——beforeEach()/beforeResolve()/afterEach()。导航守卫经常用于登录拦截、权限设计等。
路由元信息
一种方便我们给“路由规则”添加自定义属性的方式,并且可以在$route上进行访问。在“路由规则”这样配置 {path,component,meta:{}}。通常路由元信息可以配置导航守卫实现权限设计
其它路由小技巧
路由别名
{path:'/home', alias:'/h' },路由别名的作用是给复杂的path取一个方便记忆的“小名”
过渡行为
在外面包裹一个动画,让页面切换时更加温和
数据获取
使用watch监听route的变化,当route变化成功后调接口
滚动行为
使用 new VueRouter({scrollBehavior}) 这个选项精确地控制页面的滚动位置
#proxy/reflect
Proxy 与 Reflect 是 ES6 为了操作对象引入的 API 。
Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后进行操作处理。
Reflect 可以用于获取目标对象的行为,它与 Object 类似,但是更易读,更优雅。它的方法与 Proxy 是对应的。
基本用法
var proxy = new Proxy(target, handler)
target 即目标对象, handler 是一个对象,声明了代理 target 的指定行为。
lettarget={name:'ZhangSan',age:28}lethandler={get:function(target,key){console.log('get: '+key);returntarget[key];// 不是target.key},set:function(target,key,value){console.log('set: '+key);target[key]=value;}}letproxy=newProxy(target,handler)proxy.name// 实际执行 handler.getproxy.age=25// 实际执行 handler.set
常用方法
// 在判断代理对象是否拥有某个属性handler.has()// 在读取代理对象的某个属性时触发handler.get()// 在给代理对象的某个属性赋值时触发。handler.set()// 在删除代理对象的某个属性时触发该操作handler.deleteProperty()// 在获取代理对象的所有属性键时触发handler.ownKeys()// 在调用一个目标对象为函数的代理对象时触发handler.apply()
实际运用
私有属性保护
letproxy=newProxy({},{get(target,key,receiver){if(key.startsWith('_')){thrownewErro(`Invalid attempt to get private "${key}"`);}console.log('Get:'+key);returntarget[key];}});letobj=Object.create(proxy);obj.name// Get: name
参数校验
letvalidator={set:function(obj,prop,value){if(prop==='age'){if(!Number.isInteger(value)){thrownewTypeError('The age is not an integer');}if(value>200){thrownewRangeError('The age seems invalid');}}obj[prop]=value;}};letproxy=newProxy({},validator)proxy.age='oppps'// 报错
Reflect
ES6 中将 Object 的一些明显属于语言内部的方法移植到了 Reflect 对象上
Reflect 对象对某些方法的返回结果进行了修改,使其更合理。
Reflect 对象使用函数的方式实现了 Object 的命令式操作。
方法
Reflect.get(target,name,receiver)Reflect.set(target,name,value,receiver)Reflect.apply(target,thisArg,args)Reflect.construct(target,args[,newTarget])Reflect.defineProperty(target,name,desc)Reflect.deleteProperty(target,name)Reflect.has(target,name)Reflect.ownKeys(target)Reflect.preventExtensions(target)Reflect.isExtensible(target)Reflect.getOwnPropertyDescriptor(target,name)Reflect.getPrototypeOf(target)Reflect.setPrototypeOf(target,prototype)
eg:
letexam={name:"Tom",age:24,getinfo(){returnthis.name+this.age;}}Reflect.get(exam,'name');// "Tom"// 当 target 对象中存在 name 属性的 getter 方法, getter 方法的 this 会绑定 // receiverletreceiver={name:"Jerry",age:20}Reflect.get(exam,'info',receiver);// Jerry20
与proxy组合使用
Reflect 对象的方法与 Proxy 对象的方法是一一对应的。所以 Proxy 对象的方法可以通过调用 Reflect 对象的方法获取默认行为,然后进行额外操作。
letexam={name:"Tom",age:24}lethandler={get:function(target,key){console.log("getting "+key);returnReflect.get(target,key);},set:function(target,key,value){console.log("setting "+key+" to "+value)Reflect.set(target,key,value);}}letproxy=newProxy(exam,handler)proxy.name="Jerry"proxy.name// setting name to Jerry// getting name// "Jerry"
运用
实现观察者模式
// 定义 Set 集合constqueuedObservers=newSet();// 把观察者函数都放入 Set 集合中constobserve=fn=>queuedObservers.add(fn);// observable 返回原始对象的代理,拦截赋值操作constobservable=obj=>newProxy(obj,{set});functionset(target,key,value,receiver){// 获取对象的赋值操作constresult=Reflect.set(target,key,value,receiver);// 执行所有观察者queuedObservers.forEach(observer=>observer());// 执行赋值操作returnresult;}
// 原对象letobj={name:'steve',age:20}// data 是我们要拦截的 原对象// dataProxy 是我们新生成的 拦截对象letdataProxy=newProxy(obj,{get(target,key,receiver){// target 就是我们的原对象,objconsole.log("target >>> ",target===obj);// key 就是操作的健console.log("key >>> ",key);// receiver 就是 dataProxy,拦截对象console.log("receiver >>> ",receiver);console.log(`当前值为:${target[key]}`);returntarget[key];},});// 使用拦截器dataProxy.name;// 打印结果:// -> 当前值为:steve// -> "steve"