0%

每次渲染都有它自己的Effects

count是某个特定渲染中的常量。事件处理函数“看到”的是属于它那次特定渲染中的count状态值。对于effects也同样如此:

并不是count的值在“不变”的effect中发生了改变,而是effect 函数本身在每一次渲染中都不相同,概念上它是组件输出的一部分,可以看到属于某次特定渲染的props和state

Effects的清除

effects有时候需要有一个清理步骤,目的是消除副作用,React只会在浏览器绘制后运行effects,这使得你的应用更流畅,因为大多数effects不会阻塞屏幕的更新,effects的清除同样被延迟,上一次的effect会在重新渲染后被清除

  • React 渲染{id: 20}的UI。
  • 浏览器绘制。我们在屏幕上看到{id: 20}的UI。
  • React 清除{id: 10}的effect。
  • React 运行{id: 20}的effect。

effect的清除不会读取最新的props,它只能读取到定义它的那次渲染中华的prop值

告诉React去比对你的Effects

这是为什么你如果想要避免effects不必要的重复调用,你可以提供给useEffect一个依赖数组参数(deps):

1
2
3
useEffect(() => {
document.title = 'Hello, ' + name;
}, [name]); // Our deps

这好比你告诉React:“Hey,我知道你看不到这个函数里的东西,但我可以保证只使用了渲染中的name,别无其他。”

移除依赖

1
2
3
4
5
6
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, [count]);

定时器会在每一次count改变后清除和重新设定

1
2
3
4
5
6
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);

React已经知道当前的count,我们需要告知React的仅仅是去递增状态,不管它现在具体是什么值

解耦来自Actions的更新

当你写setSomething(something=>…)这种代码时可以考虑使用reducer,reducer可以让你把组件内发生了什么和状态如何响应并更新分开描述

我们用一个dispatch依赖去替换effect的step依赖

1
2
3
4
5
6
7
8
9
const [state, dispatch] = useReducer(reducer, initialState);
const { count, step } = state;

useEffect(() => {
const id = setInterval(() => {
dispatch({ type: 'tick' }); // Instead of setCount(c => c + step);
}, 1000);
return () => clearInterval(id);
}, [dispatch]);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const initialState = {
count: 0,
step: 1,
};

function reducer(state, action) {
const { count, step } = state;
if (action.type === 'tick') {
return { count: count + step, step };
} else if (action.type === 'step') {
return { count, step: action.step };
} else {
throw new Error();
}
}

React保证dispatch在每次渲染中都是一样的,所以可以在依赖中去掉它,不会引起effect不必要的重复执行,当dispatch时,React只记住了action,它会在下一次渲染中再次调用reducer

无限重复请求问题:

1 没有依赖数组,那么每次渲染都会触发这个副作用

1
2
3
useEffect(()=>{
fetchData()
})

2 设置了依赖数组,但是依赖数组里的变量一直在变

1
2
3
4
5
6
7
8
9
10
const [data,setData] = useState()

useEffect(()=>{
const fetchData = async() => {
const res = await fetchNewData()
setData(res.data)
}
fetchData()
},[data])

定义函数请求

  • 某些函数只在effect中使用,那就在effect中定义

  • 某些函数在多个地方使用,就独立定义,最好用useCallBack包裹,并且在依赖数组里把依赖项写全

    eg:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SearchResults() {
const [query, setQuery] = useState('react');

// ✅ Preserves identity until query changes
const getFetchUrl = useCallback(() => {
return 'https://hn.algolia.com/api/v1/search?query=' + query;
}, [query]); // ✅ Callback deps are OK

useEffect(() => {
const url = getFetchUrl();
// ... Fetch data and do something ...
}, [getFetchUrl]); // ✅ Effect deps are OK

// ...
}

如果query不变,getFetchUrl也会保持不变,effect也不会重新运行,反之,query改变了,getFetchUrl也会随之改变

参考:

发布订阅模式的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

class EventEmitter {
constructor(){
this.events={};
}
on(type,handler){
if(!this.events[type]){
this.events[type]=[]
}
this.events[type].push(handler)
}
addListener(type,handler){
this.on(type,handler)
}
prependListener(type,handler){
if(!this.events[type]){
this.events[type]=[]
}
this.events[type].unshift(handler)
}
removeListener(type,handler){
if(!this.events[type]){
return;
}
this.events[type]=this.events[type].filter(item=>item !== handler)
}
off(type,handler){
this.removeListener(type,handler)
}
emit(type,...args){
if(!events[type]||events[type].length==0){
return;
}
this.events[type].forEach(item=>{
Reflect.apply(item,this,args);
})
}
once(type,handler){
function temp(...args){
handler(args)
this.off(type,handler)
}
this.on(type,temp)//为事件注册单次监听器
}
}

优点:

  • 时间解耦
  • 对象之间解耦
  • 应用上:可以用在异步编程
  • 架构上:MVC和MVVM都有发布订阅模式的参与,JavaScript本身是一门基于事件驱动的语言

缺点:

创建订阅者本身需要消耗一定的时间和内存,而且当你订阅一个消息后,如果该消息最后都未发生,那么这个订阅者会始终存储在内存中。另外,发布订阅模式虽然会弱化对向之间的联系,但是过度使用,,对象和对象之间的必要联系会被深埋在背后,导致程序难以维护和理解

虚拟代理

虚拟代理会把一些开销很大的对象,延迟到真正需要它的时候再执行

虚拟代理实现图片预加载

代理负责预加载图片,预加载操作完成后将请求重新交给本体MyImage,降低耦合度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let myImage=(function(){
let ImageNode = document.createElement('img')
document.body.appendChild(ImageNode)
return function(src){
ImageNode.src = src
}
})()
let proxyImage = (function(){
let img = new Image
img.onload=function(){
myImage(this.src)
}
return function(){
myImage('file://C:/Users/sevenaeng/Desktop/loading.jpg')
img.src=src
}
})()
proxyImage('http://imgcache.qq.com/music/a.jpg')

虚拟代理合并https请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//虚拟代理合并http请求
let synchronusFile = function(id){
console.log('开始同步文件,id为:'+id)
}
let proxySynchronousFile=(function(){
let cache = [],timer;
return function(){
cache.push(id)
if(timer){
return;
}
timer=setTimeout(function(){
synchronusFile(cache.join(','))//两秒内向本体发送需要同步的ID集合
clearTimeout(timer)
timer=null
cache.length=0//清空ID集合
},2000)
}
})()
let checkbox = document.getElementsByTagName('input')
for(let i=0;c;c=checkbox[i++]){
c.onclick=function(){
if(this.checked === true){
proxySynchronousFile(this.id)
}
}
}

缓存代理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//缓存代理计算乘积
let mult =function(){
let a = 1
for(let i=0;i<arguments.length;i++){
a= a*arguments[i]
}
return a
}
//缓存代理工厂
let createProxyFactory=function(fn){
let cache={}
return function(){
let args = Array.prototype.join.call(arguments,',')
if(args in cache){
return cache[args]
}
return cache[args]=fn.apply(this,arguments)
}
}
let proxyMult = createProxyFactory(mult)

代理的意义:

实现单一职责原则,指的是一个类,应该仅有一个引起它变化的原因,如果一个对象承担了多个职责,这个对象将会变得巨大,引起它变化的原因也会很多,这些耦合在一起会导致脆弱和低内聚的设计

Vue和React的相同点很多:

都使用Virtual DOM

都使用组件化的思想

都是响应式,使用MVVM模式

都有自己的构建工具,Vue的vue-cli,React的create-react-app

区别:

数据流向不同

react从诞生开始就推崇单向数据流,而Vue是双向数据流

Vue基于template模板+Option API/Compnent API,React本质核心只有一个Virtual DOM+Diff算法,Api非常少

响应式原理不同:

Vue依赖收集,自动优化,数据可变

Vue递归监听data的所有属性,直接修改

当数据改变时,自动找到引用组件重新渲染

React:

React基于状态机,手动优化,数据不可变,需要setState驱动新的State替换旧的State

当数据改变时,以组件为根目录,默认全部重新渲染

diff算法不同:

两者思维相似,都是基于两个假设:

不同的组件产生不同的DOM结构,当type不同时,对应DOM操作就是直接销毁老的DOM,创建新的DOM

同一层次的一组子节点,可以通过唯一的key区分

源码实现上有区分:

Vue Diff使用双向指针,边对比边更新DOM

React主要使用diff队列保存需要更新的DOM,得到patch树,再统一操作批量更新DOM

事件机制不同

Vue

  • Vue原生事件使用标准Web事件
  • Vue组件自定义事件机制,是父子组件通信基础
  • Vue合理利用了snabbdom库的模块插件

React

  • React原生事件被包装,所有事件都冒泡到顶层document监听,然后在这里合成事件下发。基于这套,可以跨端使用事件机制,而不是和Web DOM强绑定。
  • React组件上无事件,父子组件通信使用props

用node中的express搭建服务器,写接口,登录方式采用jwt登录验证,用mysql存储各个人员信息,用soket.io实现聊天功能,webpack打包项目,以及动态展示功能,支持修改密码,注册用户,一对一实时聊天,发表情包,图片等功能,各个页面之间的跳转用vue-router实现,跳转使用路由懒加载,只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。搭配使用Weui样式库。

jwt登录认证的实现:

jwt组成部分:

头部.有效荷载.签名

Header.PayLoad.Signature

头部:包含了JWT类型和使用的Hash算法

负载:包含了一些声明,通常是一个User信息

签名:是对前两部分的签名,防止数据篡改

原理:

当用户使用凭据成功登录后,将返回一个json web token,由于token是凭据,不应该将token保留超过所需要的时间,也不应该将敏感数据存储在浏览器存储中,token在Authotization标头中放,跨域资源共享不会成为问题,因为它不使用cookie,使用jwt认证,程序可以使用access token去访问受保护的资源,比如在express中的使用,可以

1
2
3
4
5
//以/api/开头的不用权限,配置成功了express-jwt这个中间件,就可以把解析出来的用户信息挂载到req.user上
app.use(expressJWT({
secret:secretkey,
algorithms: ['HS256'],
}).unless({path:[/^\/api\//]}))

JWT优点:

不需要在服务端保存会话信息,所以易于应用的扩展,即信息不保存在服务端,不会存在Session扩展不方便的情况

JWT中的负载可以存储常用信息,用于信息交换,有效地使用JWT,可以降低服务端查询数据库的次数

JWT缺点:

到期问题:由于服务器不保存Session状态,因此无法在使用过程中废除某个Token,或者更改token的权限,也就是说,一旦JWT一旦签发,在到期之前就会始终有效,除非服务器部署额外的逻辑

关于jwt认证:

https://zhuanlan.zhihu.com/p/164696755

jwt与token区别:

Token和JWT(JSON Web Token)是两种常见的身份验证和授权机制,它们之间存在一些区别。

  1. 格式:Token通常是一个简单的字符串,以某种特定的格式表示。而JWT是一个基于JSON的标准,它将用户的相关信息以JSON对象的形式进行编码,并使用签名来保证数据的完整性。

  2. 安全性:JWT具有更好的安全性。JWT使用数字签名或加密算法来验证和保护数据的完整性,并确保只有经过授权的人可以访问。而Token通常只是一个简单的字符串,可能更容易受到伪造或篡改的风险。

  3. 可扩展性:JWT具有更好的可扩展性。由于JWT是基于JSON的标准,可以在其Payload中添加自定义的字段来存储额外的用户信息。而Token通常只包含最基本的身份验证信息。

  4. 无状态性:JWT是无状态的,即服务器端不需要存储任何信息来验证或跟踪用户。服务器只需通过验证JWT的签名即可确认其有效性。而Token通常需要服务器端存储相关的信息,来验证和跟踪用户。

综上所述,JWT是一种更强大、更安全、更可扩展的身份验证和授权机制,它基于JSON的标准,使用数字签名来验证和保护数据的完整性。而Token通常是一个简单的字符串,没有JWT那么多的安全和扩展特性。

注意:json web token怎么解决客户端请求想要提前失效的功能

  1. 设置短过期时间,在生成JWT时,将其过期时间设置得比较短,例如只有几分钟或者几个小时,然后在每次请求服务器时,重新获取新的JWT,如果客户端需要让当前JWT失效 只需要不再使用它
  2. 使用黑名单机制,在服务端维护一个有效JWT黑名单,当客户端想要让某个JWT失效时,就将该JWT添加进黑名单,在每次验证JWT时,先检查该JWT是否在黑名单中,如果在则拒绝该jWT,这种方式需要额外的存储空间和逻辑复杂度。

性能优化:

路由懒加载

1 什么叫路由懒加载?

也叫延迟加载,即在需要的时候进行加载

2 为什么需要路由懒加载?

  • 首先,路由通常会定义很多不同的页面
  • 这个页面在项目build打包后,一般情况下,会放在一个单独的js文件中
  • 但是,如果很多页面都放在同一个js文件中,必然会造成这个页面非常大
  • 如果我们一次性地从服务器中请求这个页面,可能会花费一定的时间,用户体验不好
  • 为了避免这种情况,我们把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这就是路由懒加载

实现方式:

Vue异步组件

1
2
3
4
5
{
path:'/problem',
name:'problem',
component:resolve=>require(['../pages/problemList'],resolve)
}

ES6中的import()—推荐使用

1
2
3
4
5
6
routes:[{
path:'/',
name:"通讯录",
component:()=>Promise.resolve(import("../components/contact/contact.vue"))
},
]

webpack的require.ensure()

多个路由指定相同的chunkName,会合并打包成一个js文件,require.ensure可实现按需加载资源,包括js,css,它会给里面的require文件单独打包,不会和主文件打包在一起。

第一个参数是数组,表明第二个参数里需要加载的模块,这些会提前加载,

第二个是回调函数,在这个回调函数里面requrie的文件会被单独打包成一个chunk,不会和主文件打包在一起,这样就生成两个chunk,第一次加载时只加载主文件

第三个参数是错误回调

第四个参数是单独打包的chunk的文件名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import Vue from 'vue';
import Router from 'vue-router';
const HelloWorld=resolve=>{
require.ensure(['@/components/HelloWorld'],()=>{
resolve(require('@/components/HelloWorld'))
})
}
Vue.use('Router')
export default new Router({
routes:[{
{path:'./',
name:'HelloWorld',
component:HelloWorld
}
}]
})

element-plus按需加载:配置vue.config.js

1
2
3
4
5
6
7
configureWebpack:{
plugins:[
require('unplugin-element-plus/webpack')({

}),
]
}

UglifyPlugin Webpack Plugin 插件用来缩小js文件

vue.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
configureWebpack:{
plugins:[
require('unplugin-element-plus/webpack')({

}),
//代码压缩
new UglifyJsPlugin({
uglifyOptions:{
compress:{
drop_debugger:true,
drop_console:true,
pure_funcs:['console.log']
}
},
sourceMap:false,
parallel:true
})
],
},

封装axios:

特征:

  • 从浏览器创建XMLHttpRequests
  • 从node.js创建http请求
  • 支持Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防御XSRF

具体:https://coloey.github.io/2022/04/11/Vue%E4%B8%AD%E5%B0%81%E8%A3%85axios%E8%AF%B7%E6%B1%82/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import axios from "axios";
import { ElLoading, ElMessage } from "element-plus";
import router from "../router";
import store from "../store";
import CHAT from "../client";
//创建一个axios实例
var instance = axios.create({
baseURL: "http://127.0.0.1:3007",
timeout: 10000, //设置超时
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
let loading;
//多次请求时
let requestCount = 0;
//显示Loading
const showLoading = () => {
if (requestCount === 0 && !loading) {
//第一次发送请求并且没有loading加载loaing
loading = ElLoading.service({
text: "Loading",
background: "rgba(0,0,0,0.7)",
spinner: "el-icon-loading",
});
}
requestCount++; //多次请求
};
//隐藏loading
const hideLoading = () => {
requestCount--;
if (requestCount === 0) {
loading.close(); //直到请求都结束Loading才关闭
}
};
//请求拦截器
instance.interceptors.request.use(
(config) => {
showLoading();
//每次发送请求前判断是否存在token如果存在则在header加上token
const token = window.localStorage.getItem("token");
token && (config.headers.Authorization = token);

return config;
},
(error) => {
Promise.reject(error);
}
);

//响应拦截器
instance.interceptors.response.use(
(response) => {
hideLoading();
//响应成功
const status = response.data.status;
if (status != 1) {
//策略模式
let stragtegy = {
0: function (res) {
//响应成功后如果是登录成功有token把token存储在本地
if (res.data.token !== undefined)
window.localStorage.setItem("token", res.data.token);
},
200: function (res) {
//获取用户信息成功后存储在localStorage里和store
store.commit("saveUserInfo", res.data.data);
window.localStorage.setItem(
"userInfo",
JSON.stringify(res.data.data)
);
},
401: function () {
//登录过期,清空token,跳转到登录页面
console.log("1");
window.localStorage.removeItem("token");
window.localStorage.removeItem("userInfo");
CHAT.logout();
router.push("/login");
},
201: function () {
//退出登录清空token,跳转登录页面
window.localStorage.removeItem("token");
const userInfo = window.localStorage.getItem("userInfo");
if (userInfo) {
CHAT.logout();
window.localStorage.removeItem("userInfo");
}
router.push("/login");
},
};
stragtegy[status] && stragtegy[status](response);
if (response.data.message) ElMessage.success(response.data.message);
return Promise.resolve(response);
} else {
ElMessage.error(response.data.message);
return Promise.reject(response);
}
},
(error) => {
let stragtegy = {
500: function () {
return "服务器错误(500)";
},
501: function () {
return "服务未实现(501)";
},
502: function () {
return "网络错误(502)";
},
503: function () {
return "服务不可用(503)";
},
504: function () {
return "网络超时(504)";
},
505: function () {
return "HTTP版本不受支持(505)";
},
};
//响应错误
if (error.response && error.response.status) {
const status = error.response.status;
let message =
(stragtegy[status] && stragtegy[status]()) || `连接出错(${status})!`;
ElMessage.error(message);
return Promise.reject(error);
}
return Promise.reject(error);
}
);
export default instance;

request.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import instance from "./index"
const axios=({
method,
url,
data,
config
})=>{
method=method.toLowerCase();
if(method=='post'){
return instance.post(url,data,{...config})
}else if(method=='get'){
return instance.get(url,{
params:data,
...config
})
}else if(method=='delete'){
return instance.delete(url,{
params:data,
...config
})
}else if(method=='put'){
return instance.put(url,data,{...config})
}else{
console.log('未知的方法'+method)
return false
}
}
export default axios

加缓存的axios封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import axios, { AxiosResponse, AxiosRequestConfig, AxiosInstance } from 'axios';
import { trackFMP, trackBeforeFMP } from '@/fmp';
import router from '@/router';
import { NOT_LOGIN_RESULTS } from './const';
import { handleLogin } from './utils/user';

interface CustomConfig {
cacheTime: number;
}

let instance: AxiosInstance;
export function initInstance() {
instance = axios.create({
timeout: 10 * 1000,
withCredentials: true,
});

const errorHandler = (error: Error) => {
throw error;
};

const requestHandler = (config: AxiosRequestConfig) => {
return config;
};

const responseHandler = (response: AxiosResponse<Response>) => {
return new Promise((resolve) => {
const config = response.config;
const body = response.data;
resolve(body);
}).then(null, errorHandler);
};

const notLoginResponseHandler = (
response: AxiosResponse<{
result: number;
}>,
) => {
if (response.config.useDefaultLoginHandle && NOT_LOGIN_RESULTS.includes(response.data.result)) {
handleLogin();
}
return response;
};

instance.interceptors.request.use(requestHandler);
instance.interceptors.response.use(notLoginResponseHandler);
instance.interceptors.response.use(responseHandler);
}

const baseUrl = '';
//TODO 等正式上线后修改为线上域名,前端打散
// const serverUrl = ['https://qatar2022-c.staging.kuaishou.com'];
// function getServerUrl(): string {
// //这里后续可以增加策略,目前只是简单随机,可以参考cdn的,通过url下发权重
// return serverUrl[Math.floor(Math.random() * serverUrl.length)] as string;
// }
//立即执行,同一个页面中的接口走同一个域名,减少重复计算
// const baseUrl = process.env.NODE_ENV === 'production' ? getServerUrl() : '';

const axiosQuee: {
[params: string]: Promise<any>;
} = {};

export const get = <T>(
url: string,
params?: object | string,
config?: AxiosRequestConfig,
custConfig?: CustomConfig,
) => {
url = baseUrl + url;
const key = url + JSON.stringify(params || {});
url && !config?.preload && trackBeforeFMP(router, url as string);
if (!axiosQuee[key]) {
axiosQuee[key] = instance.get<T, Promise<T>>(url, { params, ...config }).then(
(res) => {
if (custConfig && custConfig.cacheTime > 0) {
axiosQuee[key] = new Promise((resolve) => {
resolve(res);
});
setTimeout(() => {
delete axiosQuee[key];
}, custConfig.cacheTime);
} else {
delete axiosQuee[key];
}

return res;
},
() => {
delete axiosQuee[key];
},
);
}
return (axiosQuee[key] as Promise<T>).then((res) => {
url && !config?.preload && trackFMP(router, url as string);
return res;
});
};

export const post = <T>(url: string, params?: any, config?: AxiosRequestConfig, custConfig?: CustomConfig) => {
url = baseUrl + url;
const key = url + JSON.stringify(params || {});
url && !config?.preload && trackBeforeFMP(router, url as string);
if (!axiosQuee[key]) {
axiosQuee[key] = instance.post<T, Promise<T>>(url, params, config).then(
(res) => {
if (custConfig && custConfig.cacheTime > 0) {
axiosQuee[key] = new Promise((resolve) => {
resolve(res);
});
setTimeout(() => {
delete axiosQuee[key];
}, custConfig.cacheTime);
} else {
delete axiosQuee[key];
}
return res;
},
() => {
delete axiosQuee[key];
},
);
}
return (axiosQuee[key] as Promise<T>).then((res) => {
url && !config?.preload && trackFMP(router, url as string);
return res;
});;
};

跨域处理方法:

服务端配置cors:

1
2
3
4
5
6
7
const cors=require('cors')
const io=require('socket.io')(server,{
//服务端配置cors
cors: {
origin: "https://coloey.github.io"
}
})

本地配置proxy代理转发路由表:

原理:在本地运行的npm run serve 等命令实际上是用node运行了一个服务器,因此,proxyTable实际上是将请求转发给自己的服务器,再由服务器转发给后台服务器,做了一层代理,vue的proxyTable用了proxy-middleware中间件,不会出现跨域问题

如果接口里面有个公共的名字,比如security,可以拿出来做来做跨域配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
devServer: {
open: true,
host: 'localhost',
port: 8000,
https: false,
//以上的ip和端口是我们本机的;下面为需要跨域的
proxy: { //配置跨域
'/security': {
target: 'http://127.0.0.1:8000/', //要代理到的api地址
ws: true,
changOrigin: true, //允许跨域
pathRewrite: {
'^/security/': '/security'
}
}
}
}
}

如果接口中没有公共部分,另外设置一个代理名称,用api设置代理转发,接口请求就需要带上api.HTTP请求代理中多了ws:false属性,如果不加这个属性,浏览器控制台会一直报连接不上socket的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

let proxyObj = {};
proxyObj['/ws'] = {
ws: true,
target: "ws://localhost:8081",//目标服务器
};
proxyObj['/api'] = {
ws: false,
target: "http://localhost:8081",
changeOrigin: true,
pathRewrite: {
'^/api': '/api'
}
};
module.exports = {
devServer: {
host: 'localhost',
port: 8080,
proxy: proxyObj

这个设置只有在本地的时候是起作用的,当项目编译打包时,配置不会打包进去。

生产环境让后台配置cors或者nginx将前端代码拷贝到后端,在nginx.conf中配置,以Vue为例,如果是SPA应用,项目打包后是一个index.html还有几个js,css等文件,这些静态文件上传到服务器,在nginx.conf中配置静态资源访问:

1
2
3
4
location ~ .*\.(js|css|ico|png|jpg|eot|svg|ttf|woff|html|txt|pdf|) {
root /usr/local/nginx/html/;#所有静态文件直接读取硬盘
expires 30d; #缓存30
}

即后缀为js,css,ico等文件统统不进行请求转发,直接从本地的/usr/local/nginx/html目录下读取并返回到前端(我们需要将静态资源文件上传到/usr/local/nginx/html/目录下)

聊天功能实现:

socket.io的主要靠emit()和on()实现实时聊天,关键在于设计对话的数据结构,对话用一个对象保存,里面包含发送者姓名,接收者姓名,信息,将对话展示在对话页面中主要靠fromUser,toUser识别,根据路由参数显示对话列表,每个用户登录将用户名作为参数通过emit触发服务端addUser,服务端用一个onLineUsers对象存储socket对象,键为用户名,值为用户socket对象,每个socket对象是唯一的,服务端on监听sendMessage事件,客户端emit触发sendMessage事件,携带obj对象参数,包含对话信息和用户信息,当服务端的sendMessage事件被客户端触发后回调函数执行,以用户名为回调函数名,将obj对象作为参数,触发客户端展示消息的函数,表情包发送是通过写一个emoji表情包子组件,通过和对话列表父组件进行通信,点击表情包Icon,展示一个表情包列表,点击里面任意一个表情,子组件触发emit(chooseEmoji,emoji),传递emoji信息给父组件的chooseEmoji函数,父组件就能在输入框中显示被选中的emoji

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
//const store=require("../src/store")
const express = require("express");
const cors = require("cors");
const app = express();
const Joi = require("joi");
app.use(cors());
//解析application/x-www-form-urlencoded数据
app.use(express.urlencoded({ extended: false }));
//在路由之前封装res.cc函数,中间件处理函数处理响应
app.use((req, res, next) => {
res.cc = function (resData, status = 1) {
res.send({
status,
message: resData instanceof Error ? resData.message : resData, //resData的值可能是一个错误对象也可能是一个描述字符串
});
};
next();
});

//解析token的中间件
//api开头的接口都不需要进行token解析,配置成功了express-jwt这个中间件,可以把解析出来的用户信息挂载到req.user上
const expressJWT = require("express-jwt");
const config = require("./config");
app.use(
expressJWT({
secret: config.jwtSecrectKey,
algorithms: ["HS256"],
}).unless({ path: [/^\/api\//] })
);

//导入用户路由模块
const userRouter = require("./router/user");
app.use("/api", userRouter);
//导入并使用用户信息模块
const userInfoRouter = require("./router/userInfo");
app.use("/my", userInfoRouter);
//错误中间件
app.use(function (err, req, res, next) {
//数据验证失败
if (err instanceof Joi.ValidationError) return res.cc(err);
if (err.name === "UnauthorizedError")
return res.send({ status: 401, message: "登录已过期,请重新登录" });
//未知错误
res.cc(err);
});
const { createServer } = require("http");
const { on } = require("events");
const server = createServer(app);
const io = require("socket.io")(server, {
//服务端配置cors
cors: {
origin: "https://coloey.github.io",
// origin: "http://localhost:8081",
},
});
let onlineUsers = {}; //存储在线用户的对象
//let onlineCount = 0;
let user = "";
io.on("connection", function (socket) {
socket.emit("open");
let toUser = "";
let fromUser = "";
let date = "";
socket.on("addUser", function (username) {
// eslint-disable-next-line no-prototype-builtins
if (!onlineUsers[username]) {
//onlineCount += 1;
onlineUsers[username] = socket;
}
console.log(onlineUsers[username].id);
console.log("onlineCount", Object.keys(onlineUsers).length);
});
socket.on("sendMsg", (obj) => {
//console.log(obj);
(toUser = obj.toUser), (fromUser = obj.fromUser);
date = obj.date;
if (toUser in onlineUsers) {
//接收方
onlineUsers[toUser].emit(toUser, obj); //两边都显示信息
onlineUsers[fromUser].emit(fromUser, obj);
} else {
onlineUsers[fromUser].emit(fromUser, obj);
console.log(toUser + "不在线");
}
});
socket.on("sendPost", (obj) => {
// console.log(obj);
for (let username in onlineUsers) {
// console.log(username);
onlineUsers[username].emit("post" + username, obj);
}
});
socket.on("disconnect", () => {
console.log(`${socket.id}断开连接`);
delete onlineUsers[fromUser];
});
});

server.listen(3007, () => {
console.log("run in http://127.0.0.1:3007");
});

client.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { io } from "socket.io-client";
import store from "../src/store";

let socket = io();
const CHAT = {
username: null,
date: null,
init: function (username, flush = false) {
socket = io("http://127.0.0.1:3007", {
"force new connection": true,
});
socket.on("open", function () {
console.log("已连接");
//console.log(`client:${socket.connected}`);
if (flush) {
socket.on(username, function (obj) {
console.log("CHAT.msgArr", obj);
store.commit("ChangeMsg", obj);
});
flush = false;
}
});
socket.emit("addUser", username);
},

logout: function () {
if (socket) {
socket.disconnect();
socket.close();
}
},
scrollToBottom: function () {
window.scrollTo(0, 900000);
},
sendPost: function (obj) {
socket.emit("sendPost", obj); //触发发送朋友圈事件
},
submit: function (obj) {
console.log("submit");
socket.emit("sendMsg", obj); //触发事件
},
message: function (username) {
socket.on(username, function (obj) {
console.log("CHAT.msgArr", obj);
store.commit("ChangeMsg", obj);
});
},
post: function (username) {
socket.on("post" + username, function (obj) {
console.log("UploadPost:", obj);
store.commit("UploadPost", obj);
});
},
};
export default CHAT;

消息列表展示:

将当前登录用户信息和聊天记录信息,动态消息存储到vuex,因为组件共享该信息,通过判断toUser即为当前页面的用户并且fromUser是登录用户信息item.toUser==$route.query.username(发给页面用户信息)&&item.fromUser==$store.state.userInfo.username(来自于当前用户)得出是登录用户发送的信息,v-show=”item.toUser==$store.state.userInfo.username&&item.fromUser==$route.query.username” 得出是接收用户的发送信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div v-for="(item, index) in msg"
:key="index" >
<li
class="self clearfix"
v-show="item.toUser===$route.query.username&&item.fromUser===$store.state.userInfo.username"
>
<p class="text" v-more>
{{ item.text }}
<img :src=item.imageUrl alt="" v-show="item.imageUrl"/>
</p>
<div class="header"><img :src="$store.state.userInfo.user_pic" /></div>
</li>
<li
class="other clearfix"
v-show="item.toUser===$store.state.userInfo.username&&item.fromUser===$route.query.username"
>
<!-- 获取当前聊天的人的头像 -->
<div class="header"><img :src="otherpic.headerUrl" /></div>
<p class="text" v-more>
{{ item.text }}
<img :src=item.imageUrl alt="" v-show="item.imageUrl"/>
</p>
</li>
</div>

断线重连和持久存储

1.用vuex-persistedstate插件实现聊天消息持久存储 实现原理就是在Vuex的mutation操作中监听状态变更,并将最新的状态数据保存在本地存储中,以便下次用于程序启动时可以恢复之前的状态,这样可以方便地实现Vuex状态的持久存储,使得应用程序可以在刷新页面或重新打开应用时保持之前的状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
export default createStore({
state: {
...
},
mutations,
actions,
getters,
plugins: [
persistedState({
key: "state",
render(state) {
return state.msg;
},
}),
], //默认使用localStorage存储数据
});

实现刷新页面断线重连

因为socket.io会在页面刷新时自动断开socket连接,因此会删除onLineUsers[fromUser],在页面挂载时监听页面刷新完成load事件,页面刷新挂载时重新建立socket连接,将对象加入,客户端部分监听连接是异步回调使用nextTick,避免多次刷新页面,多次初始化用户信息,提高性能

监听到页面刷新时 => 执行回调函数,从localStorage中取出用户信息重新建立socket连接,执行message方法,再拉一次聊天信息 => 在执行message方法时发现socket还是undefined,因为重新建立连接是异步方法,而onMounted中直接执行message是同步方法,此时socket还未建立。因此,刷新时拉取消息要单独执行,可以在重新建立连接init方法中监听事件open事件(服务端连接成功触发open事件)并且用flush标识是否是刷新建立的连接,是的话再监听服务端发过来的信息,才会成功建立新的socket连接

app.js:

1
2
3
4
socket.on("disconnect", () => {
console.log(`${socket.id}断开连接`);
delete onlineUsers[fromUser];
});

contact-dialogue.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const handler = () => {      
const username = JSON.parse(window.localStorage.getItem("userInfo")).username
console.log('刷新',username)
//刷新页面时,socket.io主动断开连接,因此如果localStorage里面有用户信息,则可以直接再为用户初始化一个socket对象,加入onlineUsers,
if(username){
nextTick(()=>{
CHAT.init(username,true)
})
}
}
onMounted(()=>{
//监听刷新事件
window.addEventListener('load',handler)
CHAT.message(store.state.userInfo.username)
})
onUnmounted(()=>{
//卸载刷新事件
window.removeEventListener('load',handler)
})

client.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
init: function (username, flush = false) {
socket = io("http://127.0.0.1:3007", {
"force new connection": true,
});
socket.on("open", function () {
//如果
console.log("已连接");
//console.log(`client:${socket.connected}`);
//刷新成功后,重新建立socket连接,但是message方法是同步的,这时候socket还是undefined,
//因此要在刷新后成功建立连接后再监听服务端发过来的信息,才会成功建立新的socket连接
if (flush) {
socket.on(username, function (obj) {
console.log("CHAT.msgArr", obj);
store.commit("ChangeMsg", obj);
});
flush = false;
}
});
socket.emit("addUser", username);
},
...
message: function (username) {
socket.on(username, function (obj) {
console.log("CHAT.msgArr", obj);
store.commit("ChangeMsg", obj);
});
},
...

mutations.js:

1
2
3
4
//改变聊天信息
ChangeMsg(state,obj){
state.msg.push(obj);
},

性能优化

vue.config.js:

UglifyPlugin Webpack Plugin 插件用来缩小js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
const UglifyJsPlugin=require('uglifyjs-webpack-plugin')
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? '/Vue-chat/'
: '/' ,
// 输出文件目录
outputDir: 'dist',
// eslint-loader 是否在保存的时候检查
lintOnSave: true,
// use the full build with in-browser compiler?
// https://vuejs.org/v2/guide/installation.html#Runtime-Compiler-vs-Runtime-only
// compiler: false,
// webpack配置
// see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md
chainWebpack: () => {},
//代码压缩
configureWebpack:{
plugins:[
new UglifyJsPlugin({
uglifyOptions:{
compress:{
drop_debugger:true,
drop_console:true,
pure_funcs:['console.log']//删除console.log语句
}
},
sourceMap:false,
parallel:true
})
],
},
// vue-loader 配置项
// https://vue-loader.vuejs.org/en/options.html
// vueLoader: {},
// 生产环境是否生成 sourceMap 文件
productionSourceMap: true,
// css相关配置
// css: {
// // // 是否使用css分离插件 ExtractTextPlugin
// extract: true,
// // 开启 CSS source maps?
// sourceMap: false,
// // // css预设器配置项
// loaderOptions: {
// css:{},//这里的选项会传递给css-loader
// postcss:{}//这里的选项会传递给postcss-loader
// },
// // // 启用 CSS modules for all css / pre-processor files.
// requireModuleExtension: false
// },
// use thread-loader for babel & TS in production build
// enabled by default if the machine has more than 1 cores
parallel: require('os').cpus().length > 1,// 是否为 Babel 或 TypeScript 使用 thread-loader。该选项在系统的 CPU 有多于一个内核时自动启用,仅作用于生产构建。
// 是否启用dll
// See https://github.com/vuejs/vue-cli/blob/dev/docs/cli-service.md#dll-mode
// dll: false,
// PWA 插件相关配置
// see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa
pwa: {},
// webpack-dev-server 相关配置
devServer: {
open: process.platform === 'darwin',
disableHostCheck: true,
port:8081,
https: false,
hotOnly: false,

before: app => {}
},

// 第三方插件配置
pluginOptions: {
// ...
}
}

2 路由懒加载:

把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加,以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件

1
2
3
4
5
6
routes:[{
path:'/',
name:"通讯录",
component:()=>Promise.resolve(import("../components/contact/contact.vue"))

},

3 UI框架按需加载

在日常使用的UI框架,例如element-plus,我们经常直接使用整个UI库

1
import ElemtPlus from 'element-plus'

但实际上我用到的组件只有按钮,分页,表格,输入与警告,所以我们需要按需引用

1
2
import {Button,Input,Table,TableColumn,MessageBox} from 'element-plus'

element-plus官网:

配置vue.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const AutoImport = require("unplugin-auto-import/webpack");
const Components = require("unplugin-vue-components/webpack");
const { ElementPlusResolver } = require("unplugin-vue-components/resolvers");
module.exports = {
publicPath: process.env.NODE_ENV === "production" ? "/Vue-chat/" : "/",
// 输出文件目录
outputDir: "dist",
// eslint-loader 是否在保存的时候检查
lintOnSave: true,
configureWepeack:{
plugins:[
AutoImport({
resolvers:[ElementPlusResolver()],
}),
Components({
resolvers:[ElementPlusResolver()]
})
]
},

productionSourceMap: true,
};

4 静态资源本地缓存

后端返回资源问题:

  • 采用HTTP缓存,设置Cache-ControlLast-ModifiedEtag等响应头
  • 采用Service Worker离线缓存

前端合理利用localStorage

5 图片资源的压缩

图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素

对于所有的图片资源,我们可以进行适当的压缩

对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力,打包过程中,如果图片尺寸小于等于1KB,会被转成base64编码直接嵌入到bundle.js文件中,它会被保存到output目录中,并在bundle.js中引用图片文件的路径,注意的是,使用base64编码可以减少http请求,但也会增加bundle.js文件的大小,因此需要根据实际情况权衡是否使用base64编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
// ...
module: {
rules: [
// ...
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 1024, // 设置图片大小限制为1KB
name: 'img/[name].[hash:8].[ext]' // 文件名和路径配置
}
},
// ...
]
},
// ...
}

开启GZip压缩

拆完包之后,我们再用gzip做一下压缩 安装compression-webpack-plugin

1
npm i compression-webpack-plugin -D

vue.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const CompressionPlugin = require('compression-webpack-plugin')

configureWebpack: (config) => {
if (process.env.NODE_ENV === 'production') {
// 为生产环境修改配置...
config.mode = 'production'
return {
plugins: [new CompressionPlugin({
test: /\.js$|\.html$|\.css/, //匹配文件名
threshold: 10240, //对超过10k的数据进行压缩
deleteOriginalAssets: false //是否删除原文件
})]
}
}

6 使用SSR

SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器

从头搭建一个服务端渲染是很复杂的,vue应用建议使用Nuxt.js实现服务端渲染

对数组元素或者属性的读取操作:

通过索引访问数组元素值:arr[0]

访问数组的长度:arr.length

把数组作为对象,使用for…in循环遍历

使用for…of迭代遍历数组

数组的原型方法,如concat/join/every/some/find/findIndex/includes等,以及不改变原数组的原型方法

1 数组索引与length

通过索引设置数组元素的值时,会执行内部方法[[Set]],内部方法[[Set]]依赖于[[DefineOwnProperty]],当设置的索引值大于数组当前长度,更新数组length属性,触发与length属性相关联的副作用函数重新执行,修改set拦截函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
set(target,key,newValue){
//如果是只读的打印警告信息
if(isReadOnly){
console.warn(`属性${key}是只读的`)
}
//获取旧值
const oldValue=target[key]
//如果代理目标对象是数组,则检测被设置的索引值是否小于数组长度
//如果是,则为SET操作,否则为ADD操作
const type=Array.isArray(target)
?Number(key)<length?'SET':'ADD'
:Object.prototype.hasOwnProperty.call(target,key)?'SET':'ADD'

//设置属性值
const res=Reflect.set(target,key,receiver,newValue)
//说明receiver是target的代理对象
if(target===receiver.raw){
//比较新值和旧值,只有当它们不全等并且都不是NAN才触发响应
if(oldValue!==newValue&&(oldValue===oldValue||newValue===newValue)){
//假如设置数组length属性为0,会影响数组元素,因此要触发新的响应
trigger(target,key,type,newValue)
}
}

return true;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function trigger(target,key){
const depsMap=bucket.get(target)
if(!depsMap)return
const effects=depsMap.get(key)
const effectsToRun=new Set(effects)
//当操作类型是ADD或者DELETE,需要触发与length相关的副作用函数执行
if(type==='ADD'||type==='DELETE'){
const iterateEffects=depsMap.get('length')
lengthEffects&&lengthEffects.forEach(effectfn=>{
if(effectfn!==activeEffect){
effectsToRun.add(effectfn)
}
})
}
effects&&effects.forEach(effectfn=>{
if(activeEffect!=effectfn){//只有当trigger触发执行的副作用函数和当前正在执行的副作用函数不相同时才触发执行,否则会出现栈溢出
effectsToRun.add(effectfn)
}
})
effectsToRun.forEach(effectfn=>effectfn())
//effect&&effect.forEach(fn=>fn())//会产生无限执行
}

2 数组查找方法

arr.includes(arr[0])中arr是代理对象,includes函数执行时this指向的是代理对象,即arr,includes方法会通过索引读取数组元素值,如果值时可以被代理的,那么得到的值就是新的代理对象,

1
2
3
4
5
function reactive(obj){
//每次调用reactive时都会创建新的代理对象
return createReactive(obj)

}

解决方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//当参数Obj是相同的不用创建新的代理对象
//存储原始对象到代理对象的映射
const reactiveMap=new Map()
function reactive(obj){
//优先通过原始对象obj查找之前创建的代理对象,如果找到了,直接返回已有的代理对象
const existionProxy=reactiveMap.get(obj)
if(existionProxy)return existionProxy
//否则创建新的代理对象
proxy=createReactive(obj)
reactiveMap.set(obj,proxy)
return proxy

}

然而,下面这段代码

1
2
3
const obj = {};
const arr = reactive([obj])
console.log(arr.includes[obj])//false

includes内部的this指向的是代理对象arr,并且在获取数组元素时得到的也是代理对象,所以用原始对象obj去查找找不到,返回false,因此我们需要重写includes方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const arrInstrumentations={}
//重写方法
['includes','indexOf','lastIndexOf'].forEach(method=>{
const originMethod=Array.prototype[method]
arrInstrumentations[method]=function(...args){
//先在代理对象中查找,结果存储在res实现了arr.includes(obj)的默认方法
//找不到就去原始数组上查找
const res=originMethod.apply(this,args)
//找不到则在原始对象中查找
if(res===false){
res=originMethod.apply(this.raw,args)
}
}
return res;
})
1
2
3
4
5
6
7
8
9
10
11
12
get(target,key,receiver){
//通过"raw”属性访问原始对象
if(key==='raw'){
return target
}
//如果操作对象存在于arrInstrumentations上,返回定义在arrInstrumentation上的值
if(Array.isArray(target)&&arrInstrumentations.hasOwnProperty(key)){
return Reflect.get(arrInstrumentations,key,receiver)
}
...
}

3 push/pop/shift/unshift等方法

当调用数组的push方法时,即会读取数组length属性值也会设置数组length属性值,会导致两个独立的副作用函数相互影响,就像

1
2
3
const arr=reactive([])
effect=(()=>{arr.push(1)})
effect=(()=>{arr.push(1)})

会得到栈溢出的错误

分析:

  • 第一个副作用函数执行,在该函数内,调用arr.push方法向数组中添加一个元素,调用数组push方法时会间接读取数组的length属性,所以第一个副作用函数执行完毕会与length属性建立响应联系
  • 第二个副作用函数执行,同样,与length属性建立响应联系,同时调用arr.push会设置length属性,于是响应式系统尝试把与length有关的副作用函数全部取出执行,就包括第一个副作用函数,此时,第二个副作用函数还未执行完毕就去调用第一个副作用函数
  • 第一个副作用函数再次执行,也会间接设置数组的length属性,于是响应系统又尝试把所以与length属性相关联娿副作用取出执行,其中包括第二个副作用函数
  • 循环往复导致栈溢出

因此,我们可以通过屏蔽对length属性的读取,避免在它与副作用函数之间建立联系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//一个标记变量,代表是否追踪
let shouldTrack = true
['push'].forEach(method=>{
//取得原始push方法
const originMethod = Array.prototype[method]
//重写
arrInstrumentations[method] = function(...args){
//在调用方法前禁止追踪
shouldTrack=false;
let res = originMethod.apply(this,args)
//调用原始方法后,恢复原来行为,即允许追踪
shouldTrack=true
return res
}
})

在执行默认行为之前先将shouldTrack置false,禁止追踪,当push方法默认行为执行完毕后,将shouldTrack还原为true,

1
2
3
4
5
function track(target,key){
//禁止追踪时直接返回
if(!activeEffect || !shouldTrack)return
...
}

当push方法间接读取length属性,由于此时是禁止追踪状态,所以length属性与副作用函数之间不会建立响应联系,也就不会产生栈溢出

对一个普通对象所有可能的读取操作:

  • 访问属性:obj.foo
  • 判断对象原型上是否存在给定属性key in obj
  • 使用for…in循环遍历对象:for(const key in obj){}

1、对于属性的读取直接用get拦截函数

2、对于in 操作符,用has拦截函数代理

1
2
3
4
5
6
7
const obj={foo:1}
const p= new Proxy(obj,{
has(target,key){
track(target,key)
return Reflect.has(target,key)
}
})

3、对于for…in的拦截,使用Reflect.ownKeys()

在使用track函数进行追踪时,将ITERATE_KEY作为追踪的key,这是因为ownKeys拦截函数,只能拿到目标对象target,用ownKeys来获取一个对象中的所有属于自己的键值时,这个操作明显不与任何键绑定,因此我们只能够构造唯一的key作为标识(用Symbol构造),即ITERATE_KEY

export const ITERATE_KEY = Symbol(DEV ? ‘iterate’ : ‘’)

修改属性不会对for循环产生影响,因为无论怎么修改一个值,对于for…in循环来说都只会循环一次,如果是添加属性或者删除属性,就会触发副作用函数重新执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const ITERATE_KEY=Symbol()
function trigger(target,key){
const depsMap=bucket.get(target)
if(!depsMap)return
//取得与key相关联的副作用函数
const effects=depsMap.get(key)
//取得与ITERATE_KEY相关联的副作用函数
const iterateEffects = depMaps.get(ITER)
const effectsToRun=new Set(effects)
//当操作类型是ADD或者DELETE,需要触发与ITERATE_KEY相关的副作用函数执行
if(type==='ADD'||type==='DELETE'){
const iterateEffects=depsMap.get(ITERATE_KEY)
iterateEffects&&iterateEffects.forEach(effectfn=>{
if(effectfn!==activeEffect){
effectsToRun.add(effectfn)
}
})
}
effects&&effects.forEach(effectfn=>{
if(activeEffect!=effectfn){//只有当trigger触发执行的副作用函数和当前正在执行的副作用函数不相同时才触发执行,否则会出现栈溢出
effectsToRun.add(effectfn)
}
})
effectsToRun.forEach(effectfn=>effectfn())
//effect&&effect.forEach(fn=>fn())//会产生无限执行
}

如何屏蔽由原型引起的更新:

1
2
3
4
5
6
7
8
9
10
const obj={}
const proto={bar:1}
const child=reactive(obj)
const parent=reactive(proto)
//使用parent作为child的原型
Object.setPrototypeOf(child,parent)
effect(()=>{
console.log(child.bar)//1
})
child.bar=2//会导致副作用重新执行两次

根据规范10.1.9.2:

如果ownDesc是undefined,那么:

a.让parent的值为O.[[GetPrototypeOf]] ()

b.如果parent不是null,则

​ 返回?parent.[[Set]] (P,V,Receiver)

c.将ownDesc设置为{[[Value]]:undefined,[[Writable]]:true,[[Enumerable]]:true,[[Configurable]]:true}

如果设置的属性不在对象上,那么就会取得原型,并调用其原型上的方法,也就是parent[[Set]]内部方法,由于parent是代理对象,所以相当于执行了它的set拦截函数,因此读取child.bar时,副作用函数被child.bar收集,还被parent.bar收集

需要屏蔽一次,而parent代理对象的set函数执行时,此时target是原始对象proto,receiver是代理对象child,不再是target的代理对象,由于最初设置child.bar,receiver一直都是child,target是变化的

只有当receiver是target的代理对象时才触发更新,就能屏蔽由原型引起的更新

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function reactive(obj){
return new Proxy(obj,{
get(target,key,receiver){
//通过"raw”属性访问原始对象
if(key==='raw'){
return target
}
track(target,key)
return Reflect.get(target,key,receiver)

},
set(target,key,newValue){
//获取旧值
const oldValue=target[key]
const type=Object.prototype.hasOwnProperty.call(target,key)?'SET':'ADD'

//设置属性值
const res=Reflect.set(target,key,receiver,newValue)
//说明receiver是target的代理对象
if(target===receiver.raw){
//比较新值和旧值,只有当它们不全等并且都不是NAN才触发响应
if(oldValue!==newValue&&(oldValue===oldValue||newValue===newValue)){
trigger(target,key)
}
}

return true;
}
})
}

理解Proxy:

1、使用Proxy可以代理一个对象,它能够实现对其他对象的代理,代理指的是对一个对象的基本语义的代理

基本语义:可以对一个对象进行读取,设置属性值的操作。

2、根据ECMAScript规范,在JavaScript中有两种对象,一种是常规对象,一种是异质对象,这两种对象包含了JavaScript世界中所有对象。

在JavaScript中,对象的实际语义是由对象的内部方法指定的,内部方法指的是对一个对象进行操作时在引擎内部调用的方法,这些方法对于js使用者来说是不可见的,比如[[Get]],[[Set]],[[GetPrototyeOf]],[[SetPrototypeOf]]等方法,

3、区分普通对象和函数:函数会部署内部方法[[Call]],而普通对象不会

4、常规对象需满足:

对于内部[[Get]],[Set],必须使用ECMA规范10.1.x节给出的定义实现

对于内部方法[[Call]],必须使用ECMA规范10.2.1节给出的定义实现

对于内部方法[[Construct]],必须使用ECMA规范10.2.2节给出的定义实现

Proxy对象的内部方法[[Get]]没有使用ECMA规范10.1.8给出的定义实现,所以Proxy是一个异质对象,因此代理对象与普通对象的区别是[[Get]]的实现,代理对象会在没有指定对应拦截函数,例如没有指定get函数,当我们通过代理随性访问属性值时,代理对象内部方法[[Get]]会调用原始对象内部方法[[Get]]来获取属性值,这其实就是代理透明性质

使用Reflect:

Reflect对象的设计目的有这样几个。

(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

(2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

在响应式中使用Reflect的原因:

假如没有使用Reflect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj={
foo:1,
get bar(){
return this.foo
}
}
const p= new Proxy(obj,{
get(target,key){
track(target,key)
return target[key]
},
set(targte,key,newVal,receiver){
target[key]=newVal;
trigger(target,key)
}
})

当在effect函数中通过代理对象p访问bar属性

1
2
3
effect(()=>{
console.log(p.bar)
})

当effect注册的副作用函数执行时,会读取p.bar属性,它发现p.bar是一个访问器属性,因此执行getter函数,由于在getter函数中通过this.foo读取了foo的属性值,因此我们认为副作用函数与属性foo之间会建立联系,当我们修改p.foo的值时却没有使得副作用函数重新执行

原因在于this.foo中的this指向的是target,在代理对象中get函数返回的target[key]相当于obj.bar,在副作用函数中通过原始对象访问它的某个属性不会触发响应,使用Reflect;

1
2
3
4
5
6
7
8
9
10
11
12
const p= new Proxy(obj,{
get(target,key){
track(target,key)
return Reflect.get(target,key)
},
set(targte,key,newVal,receiver){

Reflect.set(target,key,newVal,receiver)
trigger(target,key)
return true
}
})

receiver代表谁在读取属性,这里就是代理对象p,访问器属性bar的getter函数内this指向代理对象p,这会在副作用函数与响应式数据之间建立响应联系,创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为的,不是用来指定被代理对象的内部方法和行为

一个非prop的attribute是指传向一个组件,但是该组件并没有相应props或emits定义的attribute,常见的包括class,style,id attribute,可以通过$attrs property访问那些attribute

Attribute继承

当组件返回单个根结点,非prop的attribute将自动添加到根结点的attribute中,

1
2
3
4
5
6
7
app.component('date-picker',{
template:`
<div class="data-picker">
<input type="datetime-local">
</div>
`
})
1
2
3
4
5
6
7
//具有非prop的attribute的date-picker组件
<date-picker data-status="activated"></date-picker>
//渲染后的date-picker组件
<div class="data-picker" data-status="activated">
<input type="datetime-local"/>
</div>

同理,事件监听器也会从父组件传递到子组件

1
<date-picker @change="submitChange"></date-picker>
1
2
3
4
5
6
7
8
9
app.component('date-picker', {
template: `
<select>
<option value="1">Yesterday</option>
<option value="2">Today</option>
<option value="3">Tomorrow</option>
</select>
`
})

change事件在原生

禁用Attribute继承

如果不希望组件的根元素继承attribute,可以在组件的选项设置inheritAttrs:false

1
2
3
4
5
6
7
8
app.component('date-picker',{
inheritAttrs:false,
template:`
<div class="date-picker">
<input type="datetime-local" v-bind="$attrs" />
</div>
`
})

data-status attribute将用于Input元素

1
2
3
4
5
6
7
<!-- date-picker 组件使用非 prop 的 attribute -->
<date-picker data-status="activated"></date-picker>

<!-- 渲染后的 date-picker 组件 -->
<div class="date-picker">
<input type="datetime-local" data-status="activated" />
</div>

多个根结点的Attribute继承

具有多个根结点的组件不具有自动attribute贯穿的行为,如果没有显示绑定$attrs,将发出运行时警告

参考vue官网:https://v3.cn.vuejs.org/guide/component-attrs.html