一些可能会问的面试内容
简介: 暂不全,会持续更新
模块化
除了esm是es规范化,其余都是约定俗成的一种规范,即本质上都是通过js实现的模块化,大家都根据某一个规范写,逐渐流行起来后才算得上是模块化。
接触过的,esm(es6官方),commonjs(node官方),umd(库打包兼容)
ESM(es6官方)
ECMAScript Module,es6模块化
- 使用 import 导入模块;
- 使用 export 导出模块;
CommonJS(node官方)
CommonJS 在NodeJS 环境用,不适用于浏览器;
- 使用 exports.xx = … 或者 module.exports ={} 暴露模块;
- 使用 require() 方法引入一个模块;
- require()是同步执行的;
AMD(Require.js)
AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。
- 使用 define(…) 定义一个模块;
- 使用require(…) 加载一个模块;
CMD(Sea.js)
CMD(Common Module Definition - 通用模块定义)规范主要是Sea.js推广中形成的,一个文件就是一个模块,可以像Node.js一般书写模块代码。主要在浏览器中运行,当然也可以在Node.js中运行。
它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。
UMD(通用)
全称 Universal Module Definition(万能模块定义),从名字就可以看出 UMD 做的是一统的工作。Webpack 打包代码就有 UMD 这个选项。
主要是兼容commonjs和amd
- 判断是否有define以及define.amd,兼容amd模块化
- 判断是否有exports和module,或者只有exports,兼容node模块化
- 如果都没有则挂载到全局变量里面(window或者global)
- 疑问:umd有兼容cmd吗。还是说cmd和amd类似,兼容amd约等于也兼容了cmd了。
if(typeof exports === 'object' && typeof module === 'object')
// CommonJS规范 node 环境 判断是否支持 module.exports 支持 require 这种方法
module.exports = factory(require("vue"));
else if(typeof define === 'function' && define.amd)
// AMD 如果环境中有define函数,并且define函数具备amd属性,则可以判断当前环境满足AMD规范
define(["vue"], factory);
else if(typeof exports === 'object')
// 不支持 module 但是支持 exports 的情况下使用 exports导出 是CommonJS 规范
exports["Billd"] = factory(require("vue"));
else
// 都不支持则挂载到全局对象
root["Billd"] = factory(root["Vue"]);
react生命周期
挂载,当组件实例被创建并插入 DOM 中时
componentDidMount
更新,当组件的 props 或 state 发生变化时会触发更新
componentDidUpdate
componentWillUpdate(即将过时)
卸载,当组件从 DOM 中移除时会调用如下方法:
componentWillUnmount
性能优化
防抖节流
防抖:触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间,防抖重在定时器清零
function debounce(f, wait) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
f(...args);
}, wait);
};
}
节流:高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率。节流重在加锁 ,也就是重置这个定时器。
function throttle(f, wait) {
let timer;
return (...args) => {
if (timer) {
return;
}
timer = setTimeout(() => {
f(...args);
timer = null;
}, wait);
};
}
Ahooks
useUpdate
作用是重新渲染。
主要利用了 js 里面的{}不等于{}。使用useState定义一个初始化的空对象,然后返回一个函数,这个函数setSate一个空对象。返回把这个函数返回即可。当执行这个函数的时候,因为两个空对象的引用不一样,因此会重新渲染这个组件。
import { useState } from "react";
const useUpdate = () => {
const [, setState] = useState({});
return () => setState({});
};
export default useUpdate;
useLocalStorageState
作用是使用 useState 的时候同时把数据缓存到 localStorage 里。
主要实现就是对 localStorage 进行了一层 useState 包装,然后导出一个 state 和设置 state 回调函数,在调用这个回调函数的时候,同时设置 localStorage.setItem 以及更新内部的 useState。以此达到statte和localStorage同步。
useDebounce
用来处理防抖值的 Hook。
useThrottle
用来处理节流值的 Hook。
浏览器差异
js兼容性
- new Date()构造函数使用,‘2018-07-05’是无法被各个浏览器中,使用new Date(str)来正确生成日期对象的。 正确的用法是’2018/07/05’。
css兼容性
各大浏览器前缀
- -moz代表firefox浏览器私有属性
- -ms代表IE浏览器私有属性
- -webkit代表chrome、safari私有属性
- -o代表opera私有属性
flex布局
最后一行左对齐
https://www.zhangxinxu.com/wordpress/2019/08/css-flex-last-align/
每一行列数固定
下面案例都是基于一行4个元素。
方法1:模拟space-between和间隙
// 一行4个
.list:not(:nth-child(4n)) {
margin-right: calc(4% / 3);
}
方法2:根据个数最后一个元素动态margin
/* 如果最后一行是3个元素 */
.list:last-child:nth-child(4n - 1) {
margin-right: calc(24% + 4% / 3);
}
/* 如果最后一行是2个元素 */
.list:last-child:nth-child(4n - 2) {
margin-right: calc(48% + 8% / 3);
}
每一行子项宽度不固定
方法一:最后一项margin-right:auto
/* 最后一项margin-right:auto */
.list:last-child {
margin-right: auto;
}
方法二:创建伪元素并设置flex:auto或flex:1
/* 使用伪元素辅助左对齐 */
.container::after {
content: '';
flex: auto; /* 或者flex: 1 */
}
每一行列数不固定
如果每一行的列数不固定,则上面的这些方法均不适用,需要使用其他技巧来实现最后一行左对齐。
这个方法其实很简单,也很好理解,就是使用足够的空白标签进行填充占位,具体的占位数量是由最多列数的个数决定的,例如这个布局最多7列,那我们可以使用7个空白标签进行填充占位,最多10列,那我们需要使用10个空白标签。
<div class="container">
<div class="list"></div>
<div class="list"></div>
<div class="list"></div>
<div class="list"></div>
<div class="list"></div>
<div class="list"></div>
<div class="list"></div>
<i></i><i></i><i></i><i></i><i></i>
</div>
/* 和列表一样的宽度和margin值 */
.container > i {
width: 100px;
margin-right: 10px;
}
如果列数不固定HTML又不能调整
Grid布局
.container {
display: grid;
justify-content: space-between;
grid-template-columns: repeat(auto-fill, 100px);
grid-gap: 10px;
}
.list {
width: 100px; height:100px;
background-color: skyblue;
margin-top: 5px;
}
总结
首先最后一行需要左对齐的布局更适合使用CSS grid布局实现,但是,repeat()
函数兼容性有些要求,IE浏览器并不支持。如果项目需要兼容IE,则此方法需要斟酌。
然后,适用范围最广的方法是使用空的元素进行占位,此方法不仅适用于列表个数不固定的场景,对于列表个数固定的场景也可以使用这个方法。但是有些人代码洁癖,看不惯这种空的占位的html标签,则可以试试一开始的两个方法,一是动态计算margin,模拟两端对齐,另外一个是根据列表的个数,动态控制最后一个列表元素的margin值实现左对齐。
Redux和Vuex区别
Vuex是和vue强绑定的。
Redux就是一个状态管理库,理论可以在任何地方用。
Redux所有操作都是dispatch触发action,异步操作得借助中间件,redux-thunk,redux-saga等。
Vuex同步操作是commit触发mutation,异步操作是dispatch触发action,action里面在再通过commit触发mutation更新数据
Redux
reducer
reducer名称由来,它和es6的reduce很像,第一个参数是初始化status值,第二个参数是action,reducer里面通过判断action的type来执行不同逻辑返回最新status。reducer要求是一个纯函数。
combineReducers(区分模块)
通过拆分reducer,一个reducer就是一个模块,然后通过redux导入的combineReducers合并所有reducer。这个combineReducers返回值也是一个reducer。
import { combineReducers } from 'redux';
const reducer = combineReducers({
counterInfo: counterReducer,
homeInfo: homeReducer
});
connect/Provider
如果是类组件,需要通过react-redux的connect来连接react和redux,通过Provider包裹。
这个connect其实是一个高阶函数,它接收mapStateToProps和mapDispatchToProps,并且返回值也是一个函数。
内部使用了React.createContext,在componentDidMount的时候subscribe监听redux的store变化并且setState。以此触发重新渲染更新组件。最后在componentWillUnmount的时候取消订阅。
import { connect } from 'react-redux';
export default connect(mapStateToProps, mapDispatchToProps)(Home);
如果是函数式组件,则不需要connect,通过react-redux的useSelector这个hooks来实现。
import { useSelector, shallowEqual } from 'react-redux';
const { banners, recommends, counter } = useSelector(state => ({
banners: state.banners,
recommends: state.recommends,
counter: state.counter
}), shallowEqual);
applyMiddleware
https://redux.js.org/tutorials/fundamentals/part-4-store#middleware
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer'
import { print1, print2, print3 } from './exampleAddons/middleware'
const middlewareEnhancer = applyMiddleware(print1, print2, print3)
// Pass enhancer as the second arg, since there's no preloadedState
const store = createStore(rootReducer, middlewareEnhancer)
export default store
中间件作用
Redux 使用一种称为中间件的特殊插件来让我们自定义dispatch
功能。
中间件在发出action和执行reducer之间添加了一些功能。
其实中间件是一个高阶函数,这个函数嵌套返回了两个函数。在这两个函数里面可以做一些操作。
自定义中间件
// Middleware written as ES5 functions
// Outer function:
function exampleMiddleware(storeAPI) {
return function wrapDispatch(next) {
return function handleAction(action) {
// Do anything here: pass the action onwards with next(action),
// or restart the pipeline with storeAPI.dispatch(action)
// Can also use storeAPI.getState() here
return next(action)
}
}
}
redux-thunk
redux的操作都是通过dispatch,默认的dispatch的reducer一般都是一个对象,因此很难做到异步,thunk这个中间件将
源码只有14行(js版本的时候就是14行,后面换成ts后多了类型和注释也只有53行)。
解释:根据自定义中间件的范式,仅仅是对action做了一个判断
- 如果action是函数类型,则调用action,把storeAPI,storeAPI即dispatch和getState,和额外参数extraArgument传给action并执行。
- 如果不是函数类型,则直接next(action)。
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
Es6新增
- symbol类型
- let/const
- filter、map、reduce、some、every、find、findIndex
- set和map
- Proxy代理
- Class类
- 迭代器和生成器
- 装饰器(还没有定案)
- 扩展运算符
- promise
- async/await
- Module模块化
Webpack
解析各种资源,样式,图片,js/jsx/ts/tsx
Eslint
热更新
CompressionPlugin===>gzip压缩
babel===》plugin-syntax-dynamic-import,路由懒加载
Externals+HtmlWebpackTagsPlugin,cdn加载第三方库
Treeshaking,移除没有引用的代码
optimization—》splitChunks,优化bundle大小
TerserPlugin,压缩js
PreloadPlugin,预加载js
Billd-UI
项目目录
|-- README.md // 说明文件
|-- babel.config.js // babel配置,当使用webpack构建umd版本时会使用该配置
|-- index.js // 所有组件的入口文件,当使用webpack构建umd版本时会使用该入口
|-- package.json
|-- postcss.config.js // postcss配置,当使用webpack构建umd版本时会使用该配置
|-- tsconfig.json
|-- build-tools // 构建目录
| |-- getBabelCommonConfig.js // babel配置,当使用gulp构建es和lib版本时会使用该配置
| |-- getPostcssConfig.js // postcss配置,当使用gulp构建es和lib版本时会使用该配置
| |-- gulpfile.js // gulp配置文件
| |-- webpack.common.js // webpack通用配置
| |-- webpack.dev.js // webpack开发环境配置
| |-- webpack.prod.js // webpack生产环境配置,当使用webpack构建umd版本时会使用该配置
| |-- webpack.prod.min.js // webpack生产环境配置,当使用webpack构建umd版本的压缩版本时会使用该配置
| |-- svgo // svgo配置
| | |-- svgOptions.js // svg优化选项
| | |-- template // svg模板
| | |-- icon.ejs
| |-- utils // 常用方法
| |-- chalkTip.js // 自定义console
| |-- paths.js // 处理路径
| |-- transformLess.js // 编译less
|-- components // 组件目录
| |-- index.js // 所有组件的入口文件,当使用gulp构建es和lib版本时会使用该入口
| |-- modal
| | |-- foot.jsx
| | |-- index.jsx // modal组件入口文件
| | |-- style // modal组件样式目录
| | |-- index.js // modal组件样式的入口文件
| | |-- index.less // modal组件样式
| |-- switch
| | |-- index.jsx // switch组件入口文件
| | |-- style // switch组件样式目录
| | |-- index.js // switch组件样式的入口文件
| | |-- index.less // switch组件样式
|-- src // 本地开发调试
|-- es // 构建好的es版本
|-- lib // 构建好的lib版本
|-- dist // 构建好的umd版本
Gulp
基于stream流的自动化构建工具
task
- 公开任务(Public tasks) 从 gulpfile 中被导出(export),可以通过
gulp
命令直接调用。 - 私有任务(Private tasks) 被设计为在内部使用,通常作为
series()
或parallel()
组合的组成部分。
gulp.src
通过glob匹配文件流
gulp.pipe
管道,传输文件流
gulp.dest
创建/输出文件流
gulp.parallel
并行执行任务
gulp.series
连续执行任务
http状态码
200,请求已经成功
201,成功,并且创建了资源(比如数据库新增了一条记录)
301,永久重定向(比如http永久重定向到https)
server {
listen 80;
server_name hsslive.cn;
location / {
# 把当前域名的请求,跳转到新域名上,域名变化但路径不变
# permanent:返回301永久重定向,浏览器地址栏会显示跳转后的URL地址
rewrite ^/(.*) https://hsslive.cn/$1 permanent;
}
}
302,临时重定向
304,Not Modified,资源没改变,缓存
400,参数错误
401,未授权
403,权限不足
404,not found,没找到
429,频繁请求
431,请求中的首部字段的值过大,服务器拒绝接受客户端的请求。一般是post数据太多,或者是上传base64之类的。解决:nginx设置
http {
#....
client_header_buffer_size 10240k;
large_client_header_buffers 6 10240k;
server {
# ...
}
}
500,服务器错误
502,网关错误
504,网关超时
移动端适配
Koa和express区别
相同点
两个框架都对http进行了封装。相关的api都差不多,同一批人所写。
不同点
express内置了许多中间件可供使用,而koa没有。
express包含路由,视图渲染等特性,而koa只有http模块。
express的中间件模型为线型,而koa的中间件模型洋葱模型构造中间件。
express通过回调实现异步函数,在多个回调、多个中间件中写起来容易逻辑混乱。
// express写法
app.get('/test', function (req, res) {
fs.readFile('/file1', function (err, data) {
if (err) {
res.status(500).send('read file1 error');
}
fs.readFile('/file2', function (err, data) {
if (err) {
res.status(500).send('read file2 error');
}
res.type('text/plain');
res.send(data);
});
});
});
koa通过generator 和 async/await 使用同步的写法来处理异步,明显好于 callback 和 promise。
app.use(async (ctx, next) => {
await next();
var data = await doReadFile();
ctx.response.type = 'text/plain';
ctx.response.body = data;
});
总结
Express
优点:线性逻辑,通过中间件形式把业务逻辑细分、简化,一个请求进来经过一系列中间件处理后再响应给用户,清晰明了。
缺点:基于 callback 组合业务逻辑,业务逻辑复杂时嵌套过多,异常捕获困难。
Koa
优点:首先,借助 co 和 generator,很好地解决了异步流程控制和异常捕获问题。其次,Koa 把 Express 中内置的 router、view 等功能都移除了,使得框架本身更轻量。
缺点:社区相对较小。
Vue
优先级
2.x 版本中在一个元素上同时使用 v-if
和 v-for
时,v-for
会优先作用。
3.x 版本中 v-if
总是优先于 v-for
生效。
props>methods>data>computed>watch
keepalive
activated:在首次挂载,以及每次从缓存中被重新插入的时候调用
deactivated:在从 DOM 上移除、以及组件卸载时调用
create=>mounted=>activated=>deactivated=>destroyed
几类Watcher
常见的场景有下面这几个:
- 数据变 → 使用数据的视图变
- 数据变 → 使用数据的计算属性变 → 使用计算属性的视图变
- 数据变 → 开发者主动注册的watch回调函数执行
三个场景,对应三种watcher:
- 负责敦促视图更新的render-watcher
- 执行敦促计算属性更新的computed-watcher
- 用户注册的普通watcher(watch-api或watch属性)
作者:晒了个太阳
链接:https://juejin.cn/post/6844904128435470350
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
new Vue做了什么
- 执行了this._init
- _init里面做了:合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。
响应式
Vue采用数据劫持(Object.defineProperty)结合发布者-订阅者模式的方式来实现数据的响应式
- 初始化数据,执行了observer,调用walk,这个walk,这个walk对数据遍历调用defineReactive,而defineReactive内部主要使用了Object.defineProperty,它对初始化的数据进行了
Observer
- 通过defineReactive定义响应式,递归遍历,将每一个数据分别定义响应式,分别监听
Compile解析器
解析模板,将模板里面的变量替换成数据,将变量替换成数据的这个过程其实就给节点
Watcher订阅者
Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁 ,主要的任务是订阅 Observer 中的属性值变化的消息,当收到属性值变化的消息时,触发解析器 Compile 中对应的更新函数。
Dep订阅器收集依赖
数据代理
循环遍历的对options.data进行一层Object.defineProperty代理,方便的将this.xxx代理到this.$data.xxx,简洁方便。
diff算法
如何比较
同层比较,新旧节点(新旧节点都是指虚拟dom对象)的比较是同层级的比较,不会跨层比较。
- 先判断oldNode是普通dom节点还是虚拟dom节点
- 如果是是普通dom节点,包装成虚拟dom节点
- 判断新旧节点是不是同一个节点,不是同一个节点,则暴力删除旧的,插入新的。
- 如果新旧节点是同一个节点,开始精细化比较
- 精细化比较如果是同一个引用,什么都不做
- 精细化比较如果是不同一个引用
- 这个新节点是文本(即类似一个p标签里面的文字),新节点的文本和旧文本节点的文本一样,啥都不干;新节点的文本和旧文本节点的文本不一样,直接innerText。
- 这个新节点不是文本(即类似ul下面没有文字,只有li或者其他标签节点),判断旧的节点是不是文本
- 如果是文本的话,就直接innerText设置空字符串,然后遍历创建新节点,并追加到旧的节点里面。
- 如果不是文本,即节点和节点之间的比较,这个就很复杂了,还没看。
节点复用
1, 两个节点相同,但不在相同层级上,无法复用
2,两个节点相同,在同一层级,但父节点不同,无法复用
3,同层同父节点,可以复用
我的业务
换肤hook/获取活动配置
我这里的换肤指的不是在客户端预制了几套样式在线换,而是在页面mounted的时候,通过请求获取数据,然后设置对应样式,对于用户来说无感。
- 判断缓存,先读取地址栏的活动模块名version,判断是否需要重新请求数据
- 请求的数据是key-value的形式,通过遍历累加成一个字符串,然后再将字符串结果拼在:root{}里面,再把最终的结果作为textContent,设置在新建的style元素里面,最后将这个style通过appendChild追加到head标签最后。这样来实现换肤。
分页hooks
接收url以及option参数(limit,actId,page)
在hooks里面使用axios发送请求获取数据
返回ReqStatus(当前请求状态,loading/loaded),hasMore(是否还能加载下一页,Boolean),loadMore(加载下一页),List(当前的数据),Refresh(刷新)。
billd-ui
组件设计
- 所有组件都在components目录里面,里面的每个文件夹都是一个单独的组件。
- components目录里有一个index.js入口文件,这个入口文件导入了所有组件,默认导出了install函数(调用这个install函数就会注册所有组件),方便全局导入所有组件。而且还单独导出了每一个组件,方便按需导入。
- 每个组件都有一个index.jsx文件作为入口文件。
- 每个组件的样式都在style文件夹里面,style文件夹里面又有index.js作为入口文件,里面引入了所有的该组件需要的less文件。
- 项目的根目录有一个index.js文件作为入口文件,当使用webpack构建umd版本时会使用该入口,它通过require.context导入components里面所有的组件样式,以及默认导出了component目录的index.js入口文件。因此这样就可以将组件的所有样式以及文件都构建到一个文件里面。
主要内容
- transformLess方法,主要读取components里面的所有样式文件,使用less以及postcss进行处理,最终生成css。
- compile任务,主要使用babel对项目的js/jsx文件进行解析编译,编译出es以及lib版本;同时将components里面所有组件的style文件的样式的入口index.js都复制一份然后重新生成一个css.js,并且将css.js里面的import的xxx.less文件都替换成css后缀(这是配合babel-plugin-import这个按需加载插件做的额外处理)。
- dist任务,使用webpack的 multicompiler 编译组件,打包出通用的umd版本以及对应的压缩版本。
billd-icons
作用就是替代图片,因为组件库使用图片这种静态资源的话,除非打成base64,不然如果是生成单独的图片的话,会有路径问题,因此使用了icons替代一些图片。这个库的目的就是解析svg文件里面的dom节点并且把解析好的svg节点数据输出到js里面。还有根据这个svg节点数据,进一步的构建成vue组件。
1,icons-svg-asn任务,将svg文件解析为Abstract Node,生成icons-svg的asn。
- 匹配svg资源目录里面的svg,因为匹配到的都是流,所以用了through2这个库来操作流,使用这个库将文件(或者说文件流)内容进行toString拿到字符串,然后用svgo这个插件对svg内容进行优化,删掉一些没用的属性,最终得到精简版的字符串后,再用一个parseXML这个库解析里面的dom结构,把这个dom结构保存起来就是asn里。总结来说就是将svg文件转换成解析好并且优化过的抽象节点保存在js文件里面。
2,封装icons-vue-icons任务,根据icons-svg的asn生成icons-vue的icons以及对应的入口文件。
- 有了svg的节点数据之后,就很好办了,因为是用jsx写的组件,就可以再render函数里面自己新建一个svg标签元素,然后将解析好的svg节点里面的path节点以及他的节点属性给遍历的渲染出来。总结来说就是将asn转换成jsx组件。
3,封装replaceLib方法(babel插件),将构建出来的es包里面所有导入的lib路径全部替换为es。
- 因为最终生成vue组件不止一个svg组件,是上百个组件,而且其实这上百个组件其实都是同一个模板,只是引用的svg路径不一样,因此,所有的vue组件的jsx代码,其实是根据一个模板然后批量生成的,但是因为最终构建了es和lib两个版本,但是模板只有一个,所以es版本里面组件也引用的是lib里面的路径,因此用了这个方法对es版本的所以组件jsx进行替换。
- 他其实是一个babel插件,Babel 插件本质上就是编写各种 visitor 去访问 AST 上的节点。当遇到对应类型的节点,visitor 就会做出相应的处理,从而将原本的代码 transform 成最终的代码。我这里就是添加了ImportDeclaration(ˌdekləˈreɪʃn)和ExportNamedDeclaration两个方法,即当解析到import或者export的时候,我用正则匹配将这个lib给替换成es。
- 总结来说就是对jsx组件的es版本进行特殊处理,把里面的import的lib路径替换成对应的es。
7,封装svgo模块,用于处理以及优化组件里的svg资源,将svg文件解析成dom对象,并通过render函数将dom对象渲染成icon-vue组件。
职业规划
- 技术方面:保持学习,与时俱进。这个还是比较重要的。
- 业务方面:暂时来说比较倾向做一些通用的业务,而不是一些针对性很强的业务,比如游戏(cocos、egret等)、地图(gis、three等)、图表(echart)等。
最后更新于:2022-05-27 22:15:31
22222222
2342342342342342
2223213412423423
标题
111
回复666666666
回复55555555555
回复55555555555
回复3333333333