Tiny'Wo | 小窝

网络中的一小块自留地

面试官:SSR解决了什么问题?有做过SSR吗?你是怎么做的�?

一、是什�?

Server-Side Rendering 我们称其为SSR,意为服务端渲染

指由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过�?
先来看看Web3个阶段的发展史:

  • 传统服务端渲染SSR
  • 单页面应用SPA
  • 服务端渲染SSR

*传统web开�?

网页内容在服务端渲染完成,⼀次性传输到浏览�?
img

打开页面查看源码,浏览器拿到的是全部的dom结构

单页应用SPA

单页应用优秀的用户体验,使其逐渐成为主流,页面内容由JS渲染出来,这种方式称为客户端渲染

img

打开页面查看源码,浏览器拿到的仅有宿主元素#app,并没有内容

服务端渲染SSR

SSR解决方案,后端渲染出完整的首屏的dom结构返回,前端拿到的内容包括首屏及完整spa结构,应用激活后依然按照spa方式运行

img

看完前端发展,我们再看看Vue官方对SSR的解释:

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生�?DOM 和操�?DOM。然而,也可以将同一个组件渲染为服务器端�?HTML 字符串,将它们直接发送到浏览器,最后将这些静态标�?激�?为客户端上完全可交互的应用程�?>
服务器渲染的 Vue.js 应用程序也可以被认为�?同构”�?通用”,因为应用程序的大部分代码都可以在服务器和客户端上运�?
我们从上门解释得到以下结论:

  • Vue SSR是一个在SPA上进行改良的服务端渲�?- 通过Vue SSR渲染的页面,需要在客户端激活才能实现交�?- Vue SSR将包含两部分:服务端渲染的首屏,包含交互的SPA

二、解决了什�?

SSR主要解决了以下两种问题:

  • seo:搜索引擎优先爬取页面HTML结构,使用ssr时,服务端已经生成了和业务想关联的HTML,有利于seo
  • 首屏呈现渲染:用户无需等待页面所有js加载完成就可以看到页面视图(压力来到了服务器,所以需要权衡哪些用服务端渲染,哪些交给客户端)

但是使用SSR同样存在以下的缺点:

  • 复杂度:整个项目的复杂度

  • 库的支持性,代码兼容

  • 性能问题

    • 每个请求都是n个实例的创建,不然会污染,消耗会变得很大

    • 缓存 node serve �?nginx判断当前用户有没有过期,如果没过期的话就缓存,用刚刚的结果�? - 降级:监控cpu、内存占用过多,就spa,返回单个的�?

  • 服务器负载变大,相对于前后端分离服务器只需要提供静态资源来说,服务器负载更大,所以要慎重使用

所以在我们选择是否使用SSR前,我们需要慎重问问自己这些问题:

  1. 需要SEO的页面是否只是少数几个,这些是否可以使用预渲染(Prerender SPA Plugin)实�?2. 首屏的请求响应逻辑是否复杂,数据返回是否大量且缓慢

三、如何实�?

对于同构开发,我们依然使用webpack打包,我们要解决两个问题:服务端首屏渲染和客户端激�?
这里需要生成一个服务器bundle文件用于服务端首屏渲染和一个客户端bundle文件用于客户端激�?

代码结构 除了两个不同入口之外,其他结构和之前vue应用完全相同

1
2
3
4
5
6
src
├── router
├────── index.js # 路由声明
├── store
├────── index.js # 全局状�?├── main.js # ⽤于创建vue实例
├── entry-client.js # 客户端⼊⼝,⽤于静态内容“激活�?└── entry-server.js # 服务端⼊⼝,⽤于⾸屏内容渲染

路由配置

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";

Vue.use(Router);
//导出⼯⼚函数

export function createRouter() {
return new Router({
mode: 'history',
routes: [
// 客户端没有编译器,这⾥要写成渲染函数
{ path: "/", component: { render: h => h('div', 'index page') } },
{ path: "/detail", component: { render: h => h('div', 'detail page') } }
]
});
}

主文件main.js

跟之前不同,主文件是负责创建vue实例的工厂,每次请求均会有独立的vue实例创建

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from "vue";
import App from "./App.vue";
import { createRouter } from "./router";
// 导出Vue实例⼯⼚函数,为每次请求创建独⽴实例
// 上下⽂⽤于给vue实例传递参�?export function createApp(context) {
const router = createRouter();
const app = new Vue({
router,
context,
render: h => h(App)
});
return { app, router };
}

编写服务端入口src/entry-server.js

它的任务是创建Vue实例并根据传入url指定首屏

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createApp } from "./main";
// 返回⼀个函数,接收请求上下⽂,返回创建的vue实例
export default context => {
// 这⾥返回⼀个Promise,确保路由或组件准备就绪
return new Promise((resolve, reject) => {
const { app, router } = createApp(context);
// 跳转到⾸屏的地址
router.push(context.url);
// 路由就绪,返回结�? router.onReady(() => {
resolve(app);
}, reject);
});
};

编写客户端入口entry-client.js

客户端入口只需创建vue实例并执行挂载,这⼀步称为激�?

1
2
3
4
5
6
import { createApp } from "./main";
// 创建vue、router实例
const { app, router } = createApp();
// 路由就绪,执⾏挂�?router.onReady(() => {
app.$mount("#app");
});

webpack进行配置

安装依赖

1
npm install webpack-node-externals lodash.merge -D

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
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
// 两个插件分别负责打包客户端和服务�?const VueSSRServerPlugin = require("vue-server-renderer/server-plugin");
const VueSSRClientPlugin = require("vue-server-renderer/client-plugin");
const nodeExternals = require("webpack-node-externals");
const merge = require("lodash.merge");
// 根据传⼊环境变量决定⼊⼝⽂件和相应配置项
const TARGET_NODE = process.env.WEBPACK_TARGET === "node";
const target = TARGET_NODE ? "server" : "client";
module.exports = {
css: {
extract: false
},
outputDir: './dist/'+target,
configureWebpack: () => ({
// �?entry 指向应⽤程序�?server / client ⽂件
entry: `./src/entry-${target}.js`,
// �?bundle renderer 提供 source map ⽀�? devtool: 'source-map',
// target设置为node使webpack以Node适⽤的⽅式处理动态导⼊,
// 并且还会在编译Vue组件时告知`vue-loader`输出⾯向服务器代码�? target: TARGET_NODE ? "node" : "web",
// 是否模拟node全局变量
node: TARGET_NODE ? undefined : false,
output: {
// 此处使⽤Node⻛格导出模块
libraryTarget: TARGET_NODE ? "commonjs2" : undefined
},
// https://webpack.js.org/configuration/externals/#function
// https://github.com/liady/webpack-node-externals
// 外置化应⽤程序依赖模块。可以使服务器构建速度更快,并⽣成较⼩的打包⽂件�? externals: TARGET_NODE
? nodeExternals({
// 不要外置化webpack需要处理的依赖模块�? // 可以在这⾥添加更多的⽂件类型。例如,未处�?*.vue 原始⽂件�? // 还应该将修改`global`(例如polyfill)的依赖模块列⼊⽩名�? whitelist: [/\.css$/]
})
: undefined,
optimization: {
splitChunks: undefined
},
// 这是将服务器的整个输出构建为单个 JSON ⽂件的插件�? // 服务端默认⽂件名�?`vue-ssr-server-bundle.json`
// 客户端默认⽂件名�?`vue-ssr-client-manifest.json`�? plugins: [TARGET_NODE ? new VueSSRServerPlugin() : new
VueSSRClientPlugin()]
}),
chainWebpack: config => {
// cli4项⽬添加
if (TARGET_NODE) {
config.optimization.delete('splitChunks')
}

config.module
.rule("vue")
.use("vue-loader")
.tap(options => {
merge(options, {
optimizeSSR: false
});
});
}
};

对脚本进行配置,安装依赖

1
npm i cross-env -D

定义创建脚本package.json

1
2
3
4
5
"scripts": {
"build:client": "vue-cli-service build",
"build:server": "cross-env WEBPACK_TARGET=node vue-cli-service build",
"build": "npm run build:server && npm run build:client"
}

执行打包:npm run build

最后修改宿主文件/public/index.html

1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--vue-ssr-outlet-->
</body>
</html>
是服务端渲染入口位置,注意不能为了好看而在前后加空�?

安装vuex

1
npm install -S vuex

创建vuex工厂函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export function createStore () {
return new Vuex.Store({
state: {
count:108
},
mutations: {
add(state){
state.count += 1;
}
}
})
}

main.js文件中挂载store

1
2
3
4
5
6
7
8
9
10
import { createStore } from './store'
export function createApp (context) {
// 创建实例
const store = createStore()
const app = new Vue({
store, // 挂载
render: h => h(App)
})
return { app, router, store }
}

服务器端渲染的是应用程序�?快照”,如果应用依赖于⼀些异步数据,那么在开始渲染之前,需要先预取和解析好这些数据

store进行一步数据获�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function createStore() {
return new Vuex.Store({
mutations: {
// 加⼀个初始化
init(state, count) {
state.count = count;
},
},
actions: {
// 加⼀个异步请求count的action
getCount({ commit }) {
return new Promise(resolve => {
setTimeout(() => {
commit("init", Math.random() * 100);
resolve();
}, 1000);
});
},
},
});
}

组件中的数据预取逻辑

1
2
3
4
5
export default {
asyncData({ store, route }) { // 约定预取逻辑编写在预取钩⼦asyncData�? // 触发 action 后,返回 Promise 以便确定请求结果
return store.dispatch("getCount");
}
};

服务端数据预取,entry-server.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
import { createApp } from "./app";
export default context => {
return new Promise((resolve, reject) => {
// 拿出store和router实例
const { app, router, store } = createApp(context);
router.push(context.url);
router.onReady(() => {
// 获取匹配的路由组件数�? const matchedComponents = router.getMatchedComponents();

// 若⽆匹配则抛出异�? if (!matchedComponents.length) {
return reject({ code: 404 });
}

// 对所有匹配的路由组件调⽤可能存在的`asyncData()`
Promise.all(
matchedComponents.map(Component => {
if (Component.asyncData) {
return Component.asyncData({
store,
route: router.currentRoute,
});
}
}),
)
.then(() => {
// 所有预取钩�?resolve 后,
// store 已经填充⼊渲染应⽤所需状�? // 将状态附加到上下⽂,�?`template` 选项⽤于 renderer 时,
// 状态将⾃动序列化为 `window.__INITIAL_STATE__`,并注⼊ HTML
context.state = store.state;

resolve(app);
})
.catch(reject);
}, reject);
});
};

客户端在挂载到应用程序之前,store 就应该获取到状态,entry-client.js

1
2
3
4
5
6
7
// 导出store
const { app, router, store } = createApp();
// 当使�?template 时,context.state 将作�?window.__INITIAL_STATE__ 状态⾃动嵌⼊到最终的 HTML
// 在客户端挂载到应⽤程序之前,store 就应该获取到状态:
if (window.__INITIAL_STATE__) {
store.replaceState(window.__INITIAL_STATE__);
}

客户端数据预取处理,main.js

1
2
3
4
5
6
7
8
9
10
11
12
Vue.mixin({
beforeMount() {
const { asyncData } = this.$options;
if (asyncData) {
// 将获取数据操作分配给 promise
// 以便在组件中,我们可以在数据准备就绪�? // 通过运⾏ `this.dataPromise.then(...)` 来执⾏其他任�? this.dataPromise = asyncData({
store: this.$store,
route: this.$route,
});
}
},
});

修改服务器启动文�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 获取⽂件路径
const resolve = dir => require('path').resolve(__dirname, dir)
// �?1 步:开放dist/client⽬录,关闭默认下载index⻚的选项,不然到不了后⾯路由
app.use(express.static(resolve('../dist/client'), {index: false}))
// �?2 步:获得⼀个createBundleRenderer
const { createBundleRenderer } = require("vue-server-renderer");
// �?3 步:服务端打包⽂件地址
const bundle = resolve("../dist/server/vue-ssr-server-bundle.json");
// �?4 步:创建渲染�?const renderer = createBundleRenderer(bundle, {
runInNewContext: false, // https://ssr.vuejs.org/zh/api/#runinnewcontext
template: require('fs').readFileSync(resolve("../public/index.html"), "utf8"), // 宿主⽂件
clientManifest: require(resolve("../dist/client/vue-ssr-clientmanifest.json")) // 客户端清�?});
app.get('*', async (req,res)=>{
// 设置url和title两个重要参数
const context = {
title:'ssr test',
url:req.url
}
const html = await renderer.renderToString(context);
res.send(html)
})

小结

  • 使用ssr不存在单例模式,每次用户请求都会创建一个新的vue实例
  • 实现ssr需要实现服务端首屏渲染和客户端激�?- 服务端异步获取数据asyncData可以分为首屏异步获取和切换组件获�? - 首屏异步获取数据,在服务端预渲染的时候就应该已经完成
    • 切换组件通过mixin混入,在beforeMount钩子完成数据获取

参考文�?

面试官:说下你的vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢�?

一、为什么要划分

使用vue构建项目,项目结构清晰会提高开发效率,熟悉项目的各种配置同样会让开发效率更�?
在划分项目结构的时候,需要遵循一些基本的原则�?

  • 文件夹和文件夹内部文件的语义一致�?- 单一入口/出口
  • 就近原则,紧耦合的文件应该放到一起,且应以相对路径引�?- 公共的文件应该以绝对路径的方式从根目录引�?- /src 外的文件不应该被引入

文件夹和文件夹内部文件的语义一致�?

我们的目录结构都会有一个文件夹是按照路由模块来划分的,如pages文件夹,这个文件夹里面应该包含我们项目所有的路由模块,并且仅应该包含路由模块,而不应该有别的其他的非路由模块的文件�?
这样做的好处在于一眼就�?pages文件夹看出这个项目的路由有哪�?

单一入口/出口

举个例子,在pages文件夹里面存在一个seller文件夹,这时候seller 文件夹应该作为一个独立的模块由外部引入,并且 seller/index.js 应该作为外部引入 seller 模块的唯一入口

1
2
3
4
5
// 错误用法
import sellerReducer from 'src/pages/seller/reducer'

// 正确用法
import { reducer as sellerReducer } from 'src/pages/seller'

这样做的好处在于,无论你的模块文件夹内部有多乱,外部引用的时候,都是从一个入口文件引入,这样就很好的实现了隔离,如果后续有重构需求,你就会发现这种方式的优点

就近原则,紧耦合的文件应该放到一起,且应以相对路径引�?

使用相对路径可以保证模块内部的独立�?

1
2
3
4
// 正确用法
import styles from './index.module.scss'
// 错误用法
import styles from 'src/pages/seller/index.module.scss'

举个例子

假设我们现在�?seller 目录是在 src/pages/seller,如果我们后续发生了路由变更,需要加一个层级,变成 src/pages/user/seller�?
如果我们采用第一种相对路径的方式,那就可以直接将整个文件夹拖过去就好,seller 文件夹内部不需要做任何变更�?
但是如果我们采用第二种绝对路径的方式,移动文件夹的同时,还需要对每个 import 的路径做修改

公共的文件应该以绝对路径的方式从根目录引�?

公共指的是多个路由模块共用,如一些公共的组件,我们可以放在src/components�?
在使用到的页面中,采用绝对路径的形式引用

1
2
3
4
// 错误用法
import Input from '../../components/input'
// 正确用法
import Input from 'src/components/input'

同样的,如果我们需要对文件夹结构进行调整。将 /src/components/input 变成 /src/components/new/input,如果使用绝对路径,只需要全局搜索替换

再加上绝对路径有全局的语义,相对路径有独立模块的语义

/src 外的文件不应该被引入

vue-cli脚手架已经帮我们做了相关的约束了,正常我们的前端项目都会有个src文件夹,里面放着所有的项目需要的资源,js, css, png, svg 等等。src 外会放一些项目配置,依赖,环境等文件

这样的好处是方便划分项目代码文件和配置文�?

二、目录结�?

单页面目录结�?

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
project
�? .browserslistrc
�? .env.production
�? .eslintrc.js
�? .gitignore
�? babel.config.js
�? package-lock.json
�? package.json
�? README.md
�? vue.config.js
�? yarn-error.log
�? yarn.lock
�?├─public
�? favicon.ico
�? index.html
�?|-- src
|-- components
|-- input
|-- index.js
|-- index.module.scss
|-- pages
|-- seller
|-- components
|-- input
|-- index.js
|-- index.module.scss
|-- reducer.js
|-- saga.js
|-- index.js
|-- index.module.scss
|-- buyer
|-- index.js
|-- index.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
my-vue-test:.
�? .browserslistrc
�? .env.production
�? .eslintrc.js
�? .gitignore
�? babel.config.js
�? package-lock.json
�? package.json
�? README.md
�? vue.config.js
�? yarn-error.log
�? yarn.lock
�?├─public
�? favicon.ico
�? index.html
�?└─src
├─apis //接口文件根据页面或实例模块化
�? index.js
�? login.js
�? ├─components //全局公共组件
�? └─header
�? index.less
�? index.vue
�? ├─config //配置(环境变量配置不同passid等)
�? env.js
�? index.js
�? ├─contant //常量
�? index.js
�? ├─images //图片
�? logo.png
�? ├─pages //多页面vue项目,不同的实例
�? ├─index //主实�? �? �? �? index.js
�? �? �? index.vue
�? �? �? main.js
�? �? �? router.js
�? �? �? store.js
�? �? �? �? �? ├─components //业务组件
�? �? └─pages //此实例中的各个路�? �? �? ├─amenu
�? �? �? index.vue
�? �? �? �? �? └─bmenu
�? �? index.vue
�? �? �? └─login //另一个实�? �? index.js
�? index.vue
�? main.js
�? ├─scripts //包含各种常用配置,工具函�? �? �? map.js
�? �? �? └─utils
�? helper.js
�? ├─store //vuex仓库
�? �? index.js
�? �? �? ├─index
�? �? actions.js
�? �? getters.js
�? �? index.js
�? �? mutation-types.js
�? �? mutations.js
�? �? state.js
�? �? �? └─user
�? actions.js
�? getters.js
�? index.js
�? mutation-types.js
�? mutations.js
�? state.js
�? └─styles //样式统一配置
�? components.less
�? ├─animation
�? index.less
�? slide.less
�? ├─base
�? index.less
�? style.less
�? var.less
�? widget.less
�? └─common
index.less
reset.less
style.less
transition.less

小结

项目的目录结构很重要,因为目录结构能体现很多东西,怎么规划目录结构可能每个人有自己的理解,但是按照一定的规范去进行目录的设计,能让项目整个架构看起来更为简洁,更加易用

参考文�?

面试官:什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路

一、什么是虚拟DOM

虚拟 DOM (Virtual DOM )这个概念相信大家都不陌生,�?React �?Vue ,虚�?DOM 为这两个框架都带来了跨平台的能力(React-Native �?Weex�?
实际上它只是一层对真实DOM的抽象,以JavaScript 对象 (VNode 节点) 作为基础的树,用对象的属性来描述节点,最终可以通过一系列操作使这棵树映射到真实环境上

Javascript对象中,虚拟DOM 表现为一�?Object 对象。并且最少包含标签名 (tag)、属�?(attrs) 和子元素对象 (children) 三个属性,不同框架对这三个属性的名命可能会有差别

创建虚拟DOM就是为了更好将虚拟的节点渲染到页面视图中,所以虚拟DOM对象的节点与真实DOM的属性一一照应

vue中同样使用到了虚拟DOM技�?
定义真实DOM

1
2
3
4
<div id="app">
<p class="p">节点内容</p>
<h3>{{ foo }}</h3>
</div>

实例化vue

1
2
3
4
5
6
const app = new Vue({
el:"#app",
data:{
foo:"foo"
}
})

观察renderrender,我们能得到虚拟DOM

1
2
3
4
(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',{staticClass:"p"},
[_v("节点内容")]),_v(" "),_c('h3',[_v(_s(foo))])])}})

通过VNodevue可以对这颗抽象树进行创建节点,删除节点以及修改节点的操作, 经过diff算法得出一些需要修改的最小单�?再更新视图,减少了dom操作,提高了性能

二、为什么需要虚拟DOM

DOM是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM操作引起�?
真实的DOM节点,哪怕一个最简单的div也包含着很多属性,可以打印出来直观感受一下:

由此可见,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体�?
*举个例子�?

你用传统的原生apijQuery去操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流�?
当你在一次操作时,需要更�?0个DOM节点,浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执�?0次流�?
而通过VNode,同样更�?0个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attachDOM树上,避免大量的无谓计算

很多人认为虚�?DOM 最大的优势�?diff 算法,减�?JavaScript 操作真实 DOM 的带来的性能消耗。虽然这一个虚�?DOM 带来的一个优势,但并不是全部。虚�?DOM 最大的优势在于抽象了原本的渲染过程,实现了跨平台的能力,而不仅仅局限于浏览器的 DOM,可以是安卓�?IOS 的原生组件,可以是近期很火热的小程序,也可以是各种GUI

三、如何实现虚拟DOM

首先可以看看vueVNode的结�?
源码位置:src/core/vdom/vnode.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
export default class VNode {
tag: string | void;
data: VNodeData | void;
children: ?Array<VNode>;
text: string | void;
elm: Node | void;
ns: string | void;
context: Component | void; // rendered in this component's scope
functionalContext: Component | void; // only for functional component root nodes
key: string | number | void;
componentOptions: VNodeComponentOptions | void;
componentInstance: Component | void; // component instance
parent: VNode | void; // component placeholder node
raw: boolean; // contains raw HTML? (server only)
isStatic: boolean; // hoisted static node
isRootInsert: boolean; // necessary for enter transition check
isComment: boolean; // empty comment placeholder?
isCloned: boolean; // is a cloned node?
isOnce: boolean; // is a v-once node?

constructor (
tag?: string,
data?: VNodeData,
children?: ?Array<VNode>,
text?: string,
elm?: Node,
context?: Component,
componentOptions?: VNodeComponentOptions
) {
/*当前节点的标签名*/
this.tag = tag
/*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
this.data = data
/*当前节点的子节点,是一个数�?/
this.children = children
/*当前节点的文�?/
this.text = text
/*当前虚拟节点对应的真实dom节点*/
this.elm = elm
/*当前节点的名字空�?/
this.ns = undefined
/*编译作用�?/
this.context = context
/*函数化组件作用域*/
this.functionalContext = undefined
/*节点的key属性,被当作节点的标志,用以优�?/
this.key = data && data.key
/*组件的option选项*/
this.componentOptions = componentOptions
/*当前节点对应的组件的实例*/
this.componentInstance = undefined
/*当前节点的父节点*/
this.parent = undefined
/*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
this.raw = false
/*静态节点标�?/
this.isStatic = false
/*是否作为跟节点插�?/
this.isRootInsert = true
/*是否为注释节�?/
this.isComment = false
/*是否为克隆节�?/
this.isCloned = false
/*是否有v-once指令*/
this.isOnce = false
}

// DEPRECATED: alias for componentInstance for backwards compat.
/* istanbul ignore next https://github.com/answershuto/learnVue*/
get child (): Component | void {
return this.componentInstance
}
}

这里对VNode进行稍微的说明:

  • 所有对象的 context 选项都指向了 Vue 实例
  • elm 属性则指向了其相对应的真实 DOM 节点

vue是通过createElement生成VNode

源码位置:src/core/vdom/create-element.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function createElement (
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (Array.isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}

上面可以看到createElement 方法实际上是�?_createElement 方法的封装,对参数的传入进行了判�?

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
export function _createElement(
context: Component,
tag?: string | Class<Component> | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): VNode | Array<VNode> {
if (isDef(data) && isDef((data: any).__ob__)) {
process.env.NODE_ENV !== 'production' && warn(
`Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
'Always create fresh vnode data objects in each render!',
context`
)
return createEmptyVNode()
}
// object syntax in v-bind
if (isDef(data) && isDef(data.is)) {
tag = data.is
}
if (!tag) {
// in case of component :is set to falsy value
return createEmptyVNode()
}
...
// support single function children as default scoped slot
if (Array.isArray(children) &&
typeof children[0] === 'function'
) {
data = data || {}
data.scopedSlots = { default: children[0] }
children.length = 0
}
if (normalizationType === ALWAYS_NORMALIZE) {
children = normalizeChildren(children)
} else if ( === SIMPLE_NORMALIZE) {
children = simpleNormalizeChildren(children)
}
// 创建VNode
...
}

可以看到_createElement接收5个参数:

  • context 表示 VNode 的上下文环境,是 Component 类型

  • tag 表示标签,它可以是一个字符串,也可以是一�?Component

  • data 表示 VNode 的数据,它是一�?VNodeData 类型

  • children 表示当前 VNode 的子节点,它是任意类型的

  • normalizationType 表示子节点规范的类型,类型不同规范的方法也就不一样,主要是参�?render 函数是编译生成的还是用户手写�?
    根据normalizationType 的类型,children会有不同的定�?

    1
    2
    3
    4
    5
    if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
    } else if ( === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
    }

simpleNormalizeChildren方法调用场景�?render 函数是编译生成的

normalizeChildren方法调用场景分为下面两种�?

  • render 函数是用户手写的
  • 编译 slotv-for 的时候会产生嵌套数组

无论是simpleNormalizeChildren还是normalizeChildren都是对children进行规范(使children 变成了一个类型为 VNode �?Array),这里就不展开说了

规范化children的源码位置在:src/core/vdom/helpers/normalzie-children.js

在规范化children后,就去创建VNode

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
let vnode, ns
// 对tag进行判断
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 如果是内置的节点,则直接创建一个普通VNode
vnode = new VNode(
config.parsePlatformTagName(tag), data, children,
undefined, undefined, context
)
} else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// component
// 如果是component类型,则会通过createComponent创建VNode节点
vnode = createComponent(Ctor, data, context, children, tag)
} else {
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// direct component options / constructor
vnode = createComponent(tag, data, context, children)
}

createComponent同样是创建VNode

源码位置:src/core/vdom/create-component.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
export function createComponent (
Ctor: Class<Component> | Function | Object | void,
data: ?VNodeData,
context: Component,
children: ?Array<VNode>,
tag?: string
): VNode | Array<VNode> | void {
if (isUndef(Ctor)) {
return
}
// 构建子类构造函�?
const baseCtor = context.$options._base

// plain options object: turn it into a constructor
if (isObject(Ctor)) {
Ctor = baseCtor.extend(Ctor)
}

// if at this stage it's not a constructor or an async component factory,
// reject.
if (typeof Ctor !== 'function') {
if (process.env.NODE_ENV !== 'production') {
warn(`Invalid Component definition: ${String(Ctor)}`, context)
}
return
}

// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
if (Ctor === undefined) {
return createAsyncPlaceholder(
asyncFactory,
data,
context,
children,
tag
)
}
}

data = data || {}

// resolve constructor options in case global mixins are applied after
// component constructor creation
resolveConstructorOptions(Ctor)

// transform component v-model data into props & events
if (isDef(data.model)) {
transformModel(Ctor.options, data)
}

// extract props
const propsData = extractPropsFromVNodeData(data, Ctor, tag)

// functional component
if (isTrue(Ctor.options.functional)) {
return createFunctionalComponent(Ctor, propsData, data, context, children)
}

// extract listeners, since these needs to be treated as
// child component listeners instead of DOM listeners
const listeners = data.on
// replace with listeners with .native modifier
// so it gets processed during parent component patch.
data.on = data.nativeOn

if (isTrue(Ctor.options.abstract)) {
const slot = data.slot
data = {}
if (slot) {
data.slot = slot
}
}

// 安装组件钩子函数,把钩子函数合并到data.hook�? installComponentHooks(data)

//实例化一个VNode返回。组件的VNode是没有children�? const name = Ctor.options.name || tag
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
if (__WEEX__ && isRecyclableComponent(vnode)) {
return renderRecyclableComponentTemplate(vnode)
}

return vnode
}

稍微提下createComponent生成VNode的三个关键流程:

  • 构造子类构造函数Ctor
  • installComponentHooks安装组件钩子函数
  • 实例�?vnode

小结

createElement 创建 VNode 的过程,每个 VNode �?childrenchildren 每个元素也是一个VNode,这样就形成了一个虚拟树结构,用于描述真实的DOM树结�?

参考文�?

面试官:有使用过vue吗?说说你对vue的理�?

一、从历史说起

Web是World Wide Web的简称,中文译为万维网我们可以将它规划成如下的几个时代来进行理解

  • 石器时代
  • 文明时代
  • 工业革命时代
  • 百花齐放时代

石器时代

石器时代指的就是我们的静态网页,可以欣赏一�?997的Apple官网

最早的网页是没有数据库的,可以理解成就是一张可以在网络上浏览的报纸,直到CGI技术的出现通过 CGI Perl 运行一小段代码与数据库或文件系统进行交互,如当时的Google�?998年)

文明时代

ASP,JSP大家应该都不会太陌生,最早出现于 2005 年左右,先后出现了微软的 ASP �?Java Server Pages [JSP] 等技�?取代�?CGI ,增强了 WEB 与服务端的交互的安全性,类似于下面这样,其实就是Java + HTML

`<%@ page language=”java” contentType=”text/html; charset=utf-8”
    pageEncoding=”utf-8”%>

     JSP demo    `

JSP有一个很大的缺点,就是不太灵活,因为JSP是在服务器端执行的,通常返回该客户端的就是一个HTML文本。我们每次的请求:获取的数据、内容的加载,都是服务器为我们返回渲染完成之后的 DOM,这也就使得我们开发网站的灵活度大打折扣在这种情况下,同年:Ajax火了(小细节,这里为什么说火了,因�?Ajax 技术并不是 2005 年出现的,他的雏形是 1999 年),现在看来很常见的技术手段,在当时可是珍贵无�?

工业革命时代

到这里大家就更熟悉了,移动设备的普及,Jquery的出现,以及SPA(Single Page Application 单页面应用)的雏形,Backbone EmberJS AngularJS 这样一批前端框架随之出现,但当时SPA的路不好走,例如SEO问题,SPA 过多的页面、复杂场景下 View 的绑定等,都没有很好的处理经过这几年的飞速发展,节约了开发人员大量的精力、降低了开发者和开发过程的门槛,极大提升了开发效率和迭代速度,我们可以称之其为工业时�?

百花齐放时代

这里没有文字,放一张图感受一�?

PS:这里为什么要说这么多Web的历史,我们可以看到Web技术的变化之大与快,每一种新的技术出现都是一些特定场景的解决方案,那我们今天的主角Vue又是为了解决什么呢?我们接着往下看

二、vue是什�?

Vue.js�?vjuː/,或简称为Vue)是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的Web应用框架�?016年一项针对JavaScript的调查表明,Vue有着89%的开发者满意度。在GitHub上,该项目平均每天能收获95颗星,为Github有史以来星标数第3多的项目同时也是一款流行的JavaScript前端框架,旨在更好地组织与简化Web开发。Vue所关注的核心是MVC模式中的视图层,同时,它也能方便地获取数据更新,并通过组件内部特定的方法实现视图与模型的交互PS: Vue作者尤雨溪是在为AngularJS工作之后开发出了这一框架。他声称自己的思路是提取Angular中为自己所喜欢的部分,构建出一款相当轻量的框架最早发布于2014�?�?

三、Vue核心特�?

数据驱动(MVVM)

MVVM表示的是 Model-View-ViewModel

  • Model:模型层,负责处理业务逻辑以及和服务器端进行交�?- View:视图层:负责将数据模型转化为UI展示出来,可以简单的理解为HTML页面
  • ViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁

这时候需要一张直观的关系图,如下
image.png

组件�?

1.什么是组件化一句话来说就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组�?.组件化的优势

  • 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现
  • 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简�?- 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级

指令系统

解释:指�?(Directives) 是带�?v- 前缀的特殊属性作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM

  • 常用的指�?
    • 条件渲染指令 v-if
    • 列表渲染指令v-for
    • 属性绑定指令v-bind
    • 事件绑定指令v-on
    • 双向数据绑定指令v-model

没有指令之前我们是怎么做的?是不是先要获取到DOM然后�?…干点�?

四、Vue跟传统开发的区别

没有落地使用场景的革命不是好革命,就以一个高频的应用场景来示意吧注册账号这个需求大家应该很熟悉了,如下

jquery来实现大概的思路就是选择流程dom对象,点击按钮隐藏当前活动流程dom对象,显示下一流程dom对象如下图(代码就不上了,上了就篇文章就没了..)

vue来实现,我们知道vue基本不操作dom节点�?双向绑定使dom节点跟视图绑定后,通过修改变量的值控制dom节点的各类属性。所以其实现思路为:视图层使用一变量控制dom节点显示与否,点击按钮则改变该变量,如下�?

总结就是�?

  • Vue所有的界面事件,都是只去操作数据的,Jquery操作DOM
  • Vue所有界面的变动,都是根据数据自动绑定出来的,Jquery操作DOM

五、Vue和React对比

这里就做几个简单的类比吧,当然没有好坏之分,只是使用场景不�?

相同�?

  • 都有组件化思想
  • 都支持服务器端渲�?- 都有Virtual DOM(虚拟dom�?- 数据驱动视图
  • 都有支持native的方案:VueweexReactReact native
  • 都有自己的构建工具:Vuevue-cliReactCreate React App

区别

  • 数据流向的不同。react从诞生开始就推崇单向数据流,而Vue是双向数据流
  • 数据变化的实现原理不同。react使用的是不可变数据,而Vue使用的是可变的数�?- 组件化通信的不同。react中我们通过使用回调函数来进行通信的,而Vue中子组件向父组件传递消息有两种方式:事件和回调函数
  • diff算法不同。react主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。Vue 使用双向指针,边对比,边更新DOM

参考文�?

面试官:vue3有了解过吗?能说说跟vue2的区别吗�?

一、Vue3介绍

关于vue3的重构背景,尤大是这样说的:

「Vue 新版本的理念成型�?2018 年末,当�?Vue 2 的代码库已经有两岁半了。比起通用软件的生命周期来这好像也没那么久,但在这段时期,前端世界已经今昔非比�?
在我们更新(和重写)Vue 的主要版本时,主要考虑两点因素:首先是新的 JavaScript 语言特性在主流浏览器中的受支持水平;其次是当前代码库中随时间推移而逐渐暴露出来的一些设计和架构问题�?
简要就是:

  • 利用新的语言特�?es6)
  • 解决架构问题

哪些变化

从上图中,我们可以概览Vue3的新特性,如下�?

  • 速度更快
  • 体积减少
  • 更易维护
  • 更接近原�?- 更易使用

速度更快

vue3相比vue2

  • 重写了虚拟Dom实现

  • 编译模板的优�?

  • 更高效的组件初始�?

  • undate性能提高1.3~2�?

  • SSR速度提高�?~3�?

体积更小

通过webpacktree-shaking功能,可以将无用模块“剪辑”,仅打包需要的

能够tree-shaking,有两大好处�?

  • 对开发人员,能够对vue实现更多其他的功能,而不必担忧整体体积过�?
  • 对使用者,打包出来的包体积变小�?
    vue可以开发出更多其他的功能,而不必担忧vue打包出来的整体体积过�?

更易维护

compositon Api

  • 可与现有的Options API一起使�?- 灵活的逻辑组合与复�?- Vue3模块可以和其他框架搭配使�?

更好的Typescript支持

VUE3是基于typescipt编写的,可以享受到自动的类型定义提示

编译器重�?

更接近原�?

可以自定义渲�?API

更易使用

响应�?Api 暴露出来

轻松识别组件重新渲染原因

二、Vue3新增特�?

Vue 3 中需要关注的一些新功能包括�?

  • framents
  • Teleport
  • composition Api
  • createRenderer

framents

�?Vue3.x 中,组件现在支持有多个根节点

1
2
3
4
5
6
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>

Teleport

Teleport 是一种能够将我们的模板移动到 DOM �?Vue app 之外的其他位置的技术,就有点像哆啦A梦的“任意门�?
vue2中,�?modals,toast 等这样的元素,如果我们嵌套在 Vue 的某个组件内部,那么处理嵌套组件的定位、z-index 和样式就会变得很困难

通过Teleport,我们可以在组件的逻辑位置写模板代码,然后�?Vue 应用范围之外渲染�?

1
2
3
4
5
6
7
<button @click="showToast" class="btn">打开 toast</button>
<!-- to 属性就是目标位�?-->
<teleport to="#teleport-target">
<div v-if="visible" class="toast-wrap">
<div class="toast-msg">我是一�?Toast 文案</div>
</div>
</teleport>

createRenderer

通过createRenderer,我们能够构建自定义渲染器,我们能够�?vue 的开发模型扩展到其他平台

我们可以将其生成在canvas画布�?

关于createRenderer,我们了解下基本使用,就不展开讲述�?

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createRenderer } from '@vue/runtime-core'

const { render, createApp } = createRenderer({
patchProp,
insert,
remove,
createElement,
// ...
})

export { render, createApp }

export * from '@vue/runtime-core'

composition Api

composition Api,也就是组合式api,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管�?

关于compositon api的使用,这里以下图展开

简单使�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
setup() {
const count = ref(0)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
onMounted(() => console.log('component mounted!'))
return {
count,
double,
increment
}
}
}

三、非兼容变更

Global API

  • 全局 Vue API 已更改为使用应用程序实例
  • 全局和内�?API 已经被重构为�?tree-shakable

模板指令

  • 组件�?v-model 用法已更�?- <template v-for>�?�?v-for节点上key用法已更�?- 在同一元素上使用的 v-if �?v-for 优先级已更改
  • v-bind="object" 现在排序敏感
  • v-for 中的 ref 不再注册 ref 数组

组件

  • 只能使用普通函数创建功能组�?- functional 属性在单文件组�?(SFC)
  • 异步组件现在需�?defineAsyncComponent 方法来创�?

渲染函数

  • 渲染函数API改变
  • $scopedSlots property 已删除,所有插槽都通过 $slots 作为函数暴露
  • 自定义指�?API 已更改为与组件生命周期一�?- 一些转�?class 被重命名了:
    • v-enter -> v-enter-from
    • v-leave -> v-leave-from
  • 组件 watch 选项和实例方�?$watch不再支持点分隔字符串路径,请改用计算函数作为参数
  • �?Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模�?(如果根组件没有模�?渲染选项,则最终编译为模板)。VUE3.x 现在使用应用程序容器�?innerHTML�?

其他小改�?

  • destroyed 生命周期选项被重命名�?unmounted
  • beforeDestroy 生命周期选项被重命名�?beforeUnmount
  • [prop default工厂函数不再有权访问 this 是上下文
  • 自定义指�?API 已更改为与组件生命周期一�?- data 应始终声明为函数
  • 来自 mixin �?data 选项现在可简单地合并
  • attribute 强制策略已更�?- 一些过�?class 被重命名
  • 组建 watch 选项和实例方�?$watch不再支持以点分隔的字符串路径。请改用计算属性函数作为参数�?- <template> 没有特殊指令的标�?(v-if/else-if/elsev-for �?v-slot) 现在被视为普通元素,并将生成原生�?<template> 元素,而不是渲染其内部内容�?- 在 Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模�?(如果根组件没有模�?渲染选项,则最终编译为模板)。Vue 3.x 现在使用应用容器�?innerHTML,这意味着容器本身不再被视为模板的一部分�?

移除 API

  • keyCode 支持作为 v-on 的修饰符
  • $on$off $once 实例方法
  • 过滤filter
  • 内联模板 attribute
  • $destroy 实例方法。用户不应再手动管理单个 Vue 组件的生命周期�?

参考文�?

面试官:Vue3.0 所采用�?Composition Api �?Vue2.x 使用�?Options Api 有什么不同?

开始之�?Composition API 可以说是Vue3的最大特点,那么为什么要推出Composition Api,解决了什么问题?

通常使用Vue2开发的项目,普遍会存在以下问题�?

  • 代码的可读性随着组件变大而变�?- 每一种代码复用的方式,都存在缺点
  • TypeScript支持有限

以上通过使用Composition Api都能迎刃而解

正文

一、Options Api

Options API,即大家常说的选项API,即以vue为后缀的文件,通过定义methodscomputedwatchdata等属性与方法,共同处理页面逻辑

如下图:

可以看到Options代码编写方式,如果是组件状态,则写在data属性上,如果是方法,则写在methods属性上…

用组件的选项 (datacomputedmethodswatch) 组织逻辑在大多数情况下都有效

然而,当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解

二、Composition Api

�?Vue3 Composition API 中,组件根据逻辑功能来组织的,一个功能所定义的所�?API 会放在一起(更加的高内聚,低耦合�?
即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所�?API

三、对�?

下面对Composition Api Options Api进行两大方面的比�?

  • 逻辑组织
  • 逻辑复用

逻辑组织

Options API

假设一个组件是一个大型组件,其内部有很多处理逻辑关注点(对应下图不用颜色�?

可以看到,这种碎片化使得理解和维护复杂组件变得困�?
选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项�?

Compostion API

Compositon API正是解决上述问题,将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳�?
下面举个简单例子,将处理count属性相关的代码放在同一个函数了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function useCount() {
let count = ref(10);
let double = computed(() => {
return count.value * 2;
});

const handleConut = () => {
count.value = count.value * 2;
};

console.log(count);

return {
count,
double,
handleConut,
};
}

组件上中使用count

1
2
3
4
5
6
7
8
9
10
export default defineComponent({
setup() {
const { count, double, handleConut } = useCount();
return {
count,
double,
handleConut
}
},
});

再来一张图进行对比,可以很直观地感受到 Composition API 在逻辑组织方面的优势,以后修改一个属性功能的时候,只需要跳到控制该属性的方法中即�?

逻辑复用

Vue2中,我们是用过mixin去复用相同的逻辑

下面举个例子,我们会另起一个mixin.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
export const MoveMixin = {
data() {
return {
x: 0,
y: 0,
};
},

methods: {
handleKeyup(e) {
console.log(e.code);
// 上下左右 x y
switch (e.code) {
case "ArrowUp":
this.y--;
break;
case "ArrowDown":
this.y++;
break;
case "ArrowLeft":
this.x--;
break;
case "ArrowRight":
this.x++;
break;
}
},
},

mounted() {
window.addEventListener("keyup", this.handleKeyup);
},

unmounted() {
window.removeEventListener("keyup", this.handleKeyup);
},
};

然后在组件中使用

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
Mouse position: x {{ x }} / y {{ y }}
</div>
</template>
<script>
import mousePositionMixin from './mouse'
export default {
mixins: [mousePositionMixin]
}
</script>

使用单个mixin似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时�?

1
mixins: [mousePositionMixin, fooMixin, barMixin, otherMixin]

会存在两个非常明显的问题�?

  • 命名冲突
  • 数据来源不清�?

现在通过Compositon API这种方式改写上面的代�?

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
import { onMounted, onUnmounted, reactive } from "vue";
export function useMove() {
const position = reactive({
x: 0,
y: 0,
});

const handleKeyup = (e) => {
console.log(e.code);
// 上下左右 x y
switch (e.code) {
case "ArrowUp":
// y.value--;
position.y--;
break;
case "ArrowDown":
// y.value++;
position.y++;
break;
case "ArrowLeft":
// x.value--;
position.x--;
break;
case "ArrowRight":
// x.value++;
position.x++;
break;
}
};

onMounted(() => {
window.addEventListener("keyup", handleKeyup);
});

onUnmounted(() => {
window.removeEventListener("keyup", handleKeyup);
});

return { position };
}

在组件中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
Mouse position: x {{ x }} / y {{ y }}
</div>
</template>

<script>
import { useMove } from "./useMove";
import { toRefs } from "vue";
export default {
setup() {
const { position } = useMove();
const { x, y } = toRefs(position);
return {
x,
y,
};

},
};
</script>

可以看到,整个数据来源清晰了,即使去编写更多�?hook 函数,也不会出现命名冲突的问�?

小结

  • 在逻辑组织和逻辑复用方面,Composition API是优于Options API
  • 因为Composition API几乎是函数,会有更好的类型推断�?- Composition API �?tree-shaking 友好,代码也更容易压�?- Composition API中见不到this的使用,减少了this指向不明的情�?- 如果是小型组件,可以继续使用Options API,也是十分友好的

面试官:Vue3.0的设计目标是什么?做了哪些优化

一、设计目�?

不以解决实际业务痛点的更新都是耍流氓,下面我们来列举一下Vue3之前我们或许会面临的问题

  • 随着功能的增长,复杂组件的代码变得越来越难以维护

  • 缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机�?

  • 类型推断不够友好

  • bundle的时间太久了

�?Vue3 经过长达两三年时间的筹备,做了哪些事情?

我们从结果反�?

  • 更小
  • 更快
  • TypeScript支持
  • API设计一致�?- 提高自身可维护�?- 开放更多底层功�?
    一句话概述,就是更小更快更友好�?

更小

Vue3移除一些不常用�?API

引入tree-shaking,可以将无用模块“剪辑”,仅打包需要的,使打包的整体体积变小了

更快

主要体现在编译方面:

  • diff算法优化
  • 静态提�?- 事件监听缓存
  • SSR优化

下篇文章我们会进一步介�?

更友�?

vue3在兼顾vue2options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能�?
这里代码简单演示下�?
存在一个获取鼠标位置的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { toRefs, reactive } from 'vue';
function useMouse(){
const state = reactive({x:0,y:0});
const update = e=>{
state.x = e.pageX;
state.y = e.pageY;
}
onMounted(()=>{
window.addEventListener('mousemove',update);
})
onUnmounted(()=>{
window.removeEventListener('mousemove',update);
})

return toRefs(state);
}

我们只需要调用这个函数,即可获取xy的坐标,完全不用关注实现过程

试想一下,如果很多类似的第三方库,我们只需要调用即可,不必关注实现过程,开发效率大大提�?
同时,VUE3是基于typescipt编写的,可以享受到自动的类型定义提示

三、优化方�?

vue3从很多层面都做了优化,可以分成三个方面:

  • 源码
  • 性能
  • 语法 API

源码

源码可以从两个层面展开�?

  • 源码管理
  • TypeScript

源码管理

vue3整个源码是通过 monorepo 的方式维护的,根据功能将不同的模块拆分到packages 目录下面不同的子目录�?

这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护�?
另外一�?package(比�?reactivity 响应式库)是可以独立�?Vue 使用的,这样用户如果只想使用 Vue3 的响应式能力,可以单独依赖这个响应式库而不用去依赖整个 Vue

TypeScript

Vue3是基于typeScript编写的,提供了更好的类型检查,能支持复杂的类型推导

性能

vue3是从什么哪些方面对性能进行进一步优化呢�?

  • 体积优化
  • 编译优化
  • 数据劫持优化

这里讲述数据劫持�?
vue2中,数据劫持是通过Object.defineProperty ,这�?API 有一些缺陷,并不能检测对象属性的添加和删�?

1
2
3
4
5
6
7
8
Object.defineProperty(data, 'a',{
get(){
// track
},
set(){
// trigger
}
})

尽管 Vue为了解决这个问题提供�?set delete 实例方法,但是对于用户来说,还是增加了一定的心智负担

同时在面对嵌套层级比较深的情况下,就存在性能问题

1
2
3
4
5
6
7
8
9
10
11
default {
data: {
a: {
b: {
c: {
d: 1
}
}
}
}
}

相比之下,vue3是通过proxy监听整个对象,那么对于删除还是监听当然也能监听到

同时Proxy 并不能监听到内部深层次的对象变化,�?Vue3 的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归

语法 API

这里当然说的就是composition API,其两大显著的优化:

  • 优化逻辑组织
  • 优化逻辑复用

逻辑组织

一张图,我们可以很直观地感受到 Composition API 在逻辑组织方面的优�?

相同功能的代码编写在一块,而不像options API那样,各个功能的代码混成一�?

逻辑复用

vue2中,我们是通过mixin实现功能混合,如果多个mixin混合,会存在两个非常明显的问题:命名冲突和数据来源不清晰

而通过composition这种形式,可以将一些复用的代码抽离出来作为一个函数,只要的使用的地方直接进行调用即可

同样是上文的获取鼠标位置的例�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { toRefs, reactive, onUnmounted, onMounted } from 'vue';
function useMouse(){
const state = reactive({x:0,y:0});
const update = e=>{
state.x = e.pageX;
state.y = e.pageY;
}
onMounted(()=>{
window.addEventListener('mousemove',update);
})
onUnmounted(()=>{
window.removeEventListener('mousemove',update);
})

return toRefs(state);
}

组件使用

1
2
3
4
5
6
7
import useMousePosition from './mouse'
export default {
setup() {
const { x, y } = useMousePosition()
return { x, y }
}
}

可以看到,整个数据来源清晰了,即使去编写更多的hook函数,也不会出现命名冲突的问�?

参考文�?

面试官:用Vue3.0 写过组件吗?如果想实现一�?Modal你会怎么设计�?

一、组件设�?

组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式

现在有一个场景,点击新增与编辑都弹框出来进行填写,功能上大同小异,可能只是标题内容或者是显示的主体内容稍微不�?
这时候就没必要写两个组件,只需要根据传入的参数不同,组件显示不同内容即�?
这样,下次开发相同界面程序时就可以写更少的代码,意义着更高的开发效率,更少�?Bug 和更少的程序体积

二、需求分�?

实现一个Modal组件,首先确定需要完成的内容�?

  • 遮罩�?
  • 标题内容
  • 主体内容
  • 确定和取消按�?
    主体内容需要灵活,所以可以是字符串,也可以是一�?html 代码

特点是它们在当前vue实例之外独立存在,通常挂载于body之上

除了通过引入import的形式,我们还可通过API的形式进行组件的调用

还可以包括配置全局样式、国际化、与typeScript结合

三、实现流�?

首先看看大致流程�?

  • 目录结构

  • 组件内容

  • 实现 API 形式

  • 事件处理

  • 其他完善

目录结构

Modal组件相关的目录结�?

1
2
3
4
5
6
7
8
9
10
11
12
├── plugins
�? └── modal
�? ├── Content.tsx // 维护 Modal 的内容,用于 h 函数�?jsx 语法
�? ├── Modal.vue // 基础组件
�? ├── config.ts // 全局默认配置
�? ├── index.ts // 入口
�? ├── locale // 国际化相�?�? �? ├── index.ts
�? �? └── lang
�? �? ├── en-US.ts
�? �? ├── zh-CN.ts
�? �? └── zh-TW.ts
�? └── modal.type.ts // ts类型声明相关

因为 Modal 会被 app.use(Modal) 调用作为一个插件,所以都放在plugins目录�?

组件内容

首先实现modal.vue的主体显示内容大致如�?

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
<Teleport to="body" :disabled="!isTeleport">
<div v-if="modelValue" class="modal">
<div
class="mask"
:style="style"
@click="maskClose && !loading && handleCancel()"
></div>
<div class="modal__main">
<div class="modal__title line line--b">
<span>{{ title || t("r.title") }}</span>
<span
v-if="close"
:title="t('r.close')"
class="close"
@click="!loading && handleCancel()"
>�?/span
>
</div>
<div class="modal__content">
<Content v-if="typeof content === 'function'" :render="content" />
<slot v-else>
{{ content }}
</slot>
</div>
<div class="modal__btns line line--t">
<button :disabled="loading" @click="handleConfirm">
<span class="loading" v-if="loading"> �?</span>{{ t("r.confirm") }}
</button>
<button @click="!loading && handleCancel()">
{{ t("r.cancel") }}
</button>
</div>
</div>
</div>
</Teleport>

最外层上通过Vue3 Teleport 内置组件进行包裹,其相当于传送门,将里面的内容传送至body之上

并且从DOM结构上来看,把modal该有的内容(遮罩层、标题、内容、底部按钮)都实现了

关于主体内容

1
2
3
4
5
6
7
<div class="modal__content">
<Content v-if="typeof content==='function'"
:render="content" />
<slot v-else>
{{content}}
</slot>
</div>

可以看到根据传入content的类型不同,对应显示不同得到内容

最常见的则是通过调用字符串和默认插槽的形�?

1
2
3
4
5
6
7
8
9
// 默认插槽
<Modal v-model="show"
title="演示 slot">
<div>hello world~</div>
</Modal>

// 字符�?<Modal v-model="show"
title="演示 content"
content="hello world~" />

通过 API 形式调用Modal组件的时候,content可以使用下面两种

  • h 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
$modal.show({
title: '演示 h 函数',
content(h) {
return h(
'div',
{
style: 'color:red;',
onClick: ($event: Event) => console.log('clicked', $event.target)
},
'hello world ~'
);
}
});
  • JSX
1
2
3
4
5
6
7
8
9
10
11
12
$modal.show({
title: '演示 jsx 语法',
content() {
return (
<div
onClick={($event: Event) => console.log('clicked', $event.target)}
>
hello world ~
</div>
);
}
});

实现 API 形式

那么组件如何实现API形式调用Modal组件呢?

Vue2中,我们可以借助Vue实例以及Vue.extend的方式获得组件实例,然后挂载到body�?

1
2
3
4
import Modal from './Modal.vue';
const ComponentClass = Vue.extend(Modal);
const instance = new ComponentClass({ el: document.createElement("div") });
document.body.appendChild(instance.$el);

虽然Vue3移除了Vue.extend方法,但可以通过createVNode实现

1
2
3
4
5
6
import Modal from './Modal.vue';
const container = document.createElement('div');
const vnode = createVNode(Modal);
render(vnode, container);
const instance = vnode.component;
document.body.appendChild(container);

Vue2中,可以通过this的形式调用全局 API

1
2
3
4
5
export default {
install(vue) {
vue.prototype.$create = create
}
}

而在 Vue3 �?setup 中已经没�?this 概念了,需要调用app.config.globalProperties挂载到全局

1
2
3
4
5
export default {
install(app) {
app.config.globalProperties.$create = create
}
}

事件处理

下面再看看看Modal组件内部是如何处理「确定」「取消」事件的,既然是Vue3,当然采用Compositon API 形式

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
// Modal.vue
setup(props, ctx) {
let instance = getCurrentInstance(); // 获得当前组件实例
onBeforeMount(() => {
instance._hub = {
'on-cancel': () => {},
'on-confirm': () => {}
};
});

const handleConfirm = () => {
ctx.emit('on-confirm');
instance._hub['on-confirm']();
};
const handleCancel = () => {
ctx.emit('on-cancel');
ctx.emit('update:modelValue', false);
instance._hub['on-cancel']();
};

return {
handleConfirm,
handleCancel
};
}

在上面代码中,可以看得到除了使用传统emit的形式使父组件监听,还可通过_hub属性中添加 on-cancelon-confirm方法实现在API中进行监�?

1
2
3
4
5
app.config.globalProperties.$modal = {
show({}) {
/* 监听 确定、取�?事件 */
}
}

下面再来目睹下_hub是如何实�?

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
// index.ts
app.config.globalProperties.$modal = {
show({
/* 其他选项 */
onConfirm,
onCancel
}) {
/* ... */

const { props, _hub } = instance;

const _closeModal = () => {
props.modelValue = false;
container.parentNode!.removeChild(container);
};
// 往 _hub 新增事件的具体实�? Object.assign(_hub, {
async 'on-confirm'() {
if (onConfirm) {
const fn = onConfirm();
// 当方法返回为 Promise
if (fn && fn.then) {
try {
props.loading = true;
await fn;
props.loading = false;
_closeModal();
} catch (err) {
// 发生错误时,不关闭弹�? console.error(err);
props.loading = false;
}
} else {
_closeModal();
}
} else {
_closeModal();
}
},
'on-cancel'() {
onCancel && onCancel();
_closeModal();
}
});
}
};

其他完善

关于组件实现国际化、与typsScript结合,大家可以根据自身情况在此基础上进行更�?

参考文�?

面试官:Vue3.0性能提升主要是通过哪几方面体现的?

一、编译阶�?

回顾Vue2,我们知道每个组件实例都对应一�?watcher 实例,它会在组件渲染的过程中把用到的数据property记录为依赖,当依赖发生改变,触发setter,则会通知watcher,从而使关联的组件重新渲�?

试想一下,一个组件结构如下图

1
2
3
4
5
6
7
8
9
10
<template>
<div id="content">
<p class="text">静态文�?/p>
<p class="text">静态文�?/p>
<p class="text">{{ message }}</p>
<p class="text">静态文�?/p>
...
<p class="text">静态文�?/p>
</div>
</template>

可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很�?diff 和遍历其实都是不需要的,造成性能浪费

因此,Vue3在编译阶段,做了进一步优化。主要有如下�?

  • diff算法优化
  • 静态提�?- 事件监听缓存
  • SSR优化

diff算法优化

vue3diff算法中相比vue2增加了静态标�?
关于这个静态标记,其作用是为了会发生变化的地方添加一个flag标记,下次发生变化的时候直接找该地方进行比�?
下图这里,已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提�?

关于静态类型枚举如�?

1
2
3
4
5
6
7
8
9
10
11
12
13
export const enum PatchFlags {
TEXT = 1,// 动态的文本节点
CLASS = 1 << 1, // 2 动态的 class
STYLE = 1 << 2, // 4 动态的 style
PROPS = 1 << 3, // 8 动态属性,不包括类名和样式
FULL_PROPS = 1 << 4, // 16 动�?key,当 key 变化时需要完整的 diff 算法做比�? HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的节点
STABLE_FRAGMENT = 1 << 6, // 64 一个不会改变子节点顺序�?Fragment
KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没�?key �?Fragment
NEED_PATCH = 1 << 9, // 512
DYNAMIC_SLOTS = 1 << 10, // 动�?solt
HOISTED = -1, // 特殊标志是负整数表示永远不会用作 diff
BAIL = -2 // 一个特殊的标志,指代差异算�?}

静态提�?

Vue3中对不参与更新的元素,会做静态提升,只会被创建一次,在渲染时直接复用

这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用

1
2
3
<span>你好</span>

<div>{{ message }}</div>

没有做静态提升之�?

1
2
3
4
5
6
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_createVNode("span", null, "你好"),
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}

做了静态提升之�?

1
2
3
4
5
6
7
8
9
10
const _hoisted_1 = /*#__PURE__*/_createVNode("span", null, "你好", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock(_Fragment, null, [
_hoisted_1,
_createVNode("div", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}

// Check the console for the AST

静态内容_hoisted_1被放置在render 函数外,每次渲染的时候只要取 _hoisted_1 即可

同时 _hoisted_1 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用�?Diff

事件监听缓存

默认情况下绑定事件行为会被视为动态绑定,所以每次都会去追踪它的变化

1
2
3
<div>
<button @click = 'onClick'>点我</button>
</div>

没开启事件监听器缓存

1
2
3
4
5
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
// PROPS=1<<3,// 8 //动态属性,但不包含类名和样�? ]))
})

开启事件侦听器缓存�?

1
2
3
4
5
6
7
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "点我")
]))
}

上述发现开启了缓存后,没有了静态标记。也就是说下次diff算法的时候直接使�?

SSR优化

当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染

1
2
3
4
5
6
7
8
div>
<div>
<span>你好</span>
</div>
... // 很多个静态属�? <div>
<span>{{ message }}</span>
</div>
</div>

编译�?

1
2
3
4
5
6
7
8
9
10
11
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"

export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
const _cssVars = { style: { color: _ctx.color }}
_push(`<div${
_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
}><div><span>你好</span>...<div><span>你好</span><div><span>${
_ssrInterpolate(_ctx.message)
}</span></div></div>`)
}

二、源码体�?

相比Vue2Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking

任何一个函数,如refreavtivedcomputed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
setup(props, context) {
const age = ref(18)

let state = reactive({
name: 'test'
})

const readOnlyAge = computed(() => age.value++) // 19

return {
age,
state,
readOnlyAge
}
}
});

三、响应式系统

vue2中采�?defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加gettersetter,实现响应式

vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍�?

  • 可以监听动态属性的添加
  • 可以监听到数组的索引和数组length属�?- 可以监听删除属�?
    关于这两�?API 具体的不同,我们下篇文章会进行一个更加详细的介绍

参考文�?

面试官:Vue3.0里为什么要�?Proxy API 替代 defineProperty API �?

一、Object.defineProperty

定义:Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

为什么能实现响应�?

通过defineProperty 两个属性,getset

  • get

属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传�?this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的�?

  • set

属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时�?this 对象。默认为 undefined

下面通过代码展示�?
定义一个响应式函数defineReactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function update() {
app.innerText = obj.foo
}

function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
}
})
}

调用defineReactive,数据发生变化触发update方法,实现数据响应式

1
2
3
4
5
const obj = {}
defineReactive(obj, 'foo', '')
setTimeout(()=>{
obj.foo = new Date().toLocaleTimeString()
},1000)

在对象存在多个key情况下,需要进行遍�?

1
2
3
4
5
6
7
8
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}

如果存在嵌套对象的情况,还需要在defineReactive中进行递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
}
})
}

当给key赋值为对象的时候,还需要在set属性中进行递归

1
2
3
4
5
set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是对象的情�? notifyUpdate()
}
}

上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问�?
现在对一个对象进行删除与添加属性操作,无法劫持�?

1
2
3
4
5
6
7
const obj = {
foo: "foo",
bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok

当我们对一个数组进行监听的时候,并不那么好使�?

1
2
3
4
5
6
7
const arrData = [1,2,3,4,5];
arrData.forEach((val,index)=>{
defineProperty(arrData,index,val)
})
arrData.push() // no ok
arrData.pop() // no ok
arrDate[0] = 99 // ok

可以看到数据的api无法劫持到,从而无法实现数据响应式�?
所以在Vue2中,增加了setdelete API,并且对数组api方法进行一个重�?
还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问�?

小结

  • 检测不到对象属性的添加和删�?- 数组API方法无法监听�?- 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

二、proxy

Proxy的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了

ES6系列中,我们详细讲解过Proxy的使用,就不再述说了

下面通过代码进行展示�?
定义一个响应式方法reactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦�? const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
return res
}
})
return observed
}

测试一下简单数据的操作,发现都能劫�?

1
2
3
4
5
6
7
8
const state = reactive({
foo: 'foo'
})
// 1.获取
state.foo // ok
// 2.设置已存在属�?state.foo = 'fooooooo' // ok
// 3.设置不存在属�?state.dong = 'dong' // ok
// 4.删除属�?delete state.dong // ok

再测试嵌套对象情况,这时候发现就不那�?OK �?

1
2
3
4
5
const state = reactive({
bar: { a: 1 }
})

// 设置嵌套对象属�?state.bar.a = 10 // no ok

如果要解决,需要在get之上再进行一层代�?

1
2
3
4
5
6
7
8
9
10
11
12
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦�? const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return isObject(res) ? reactive(res) : res
},
return observed
}

三、总结

Object.defineProperty只能遍历对象属性进行劫�?

1
2
3
4
5
6
7
8
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}

Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦�? const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
return res
}
})
return observed
}

Proxy可以直接监听数组的变化(pushshiftsplice�?

1
2
3
const obj = [1,2,3]
const proxtObj = reactive(obj)
obj.psuh(4) // ok

Proxy有多�?3种拦截方�?不限于applyownKeysdeletePropertyhas等等,这是Object.defineProperty不具备的

正因为defineProperty自身的缺陷,导致Vue2在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外setdelete方法�?

1
2
3
4
5
6
7
8
9
10
11
12
13
// 数组重写
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
arrayProto[method] = function () {
originalProto[method].apply(this.arguments)
dep.notice()
}
});

// set、delete
Vue.set(obj,'bar','newbar')
Vue.delete(obj),'bar')

Proxy 不兼容IE,也没有 polyfill, defineProperty 能支持到IE9

参考文�?- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

0%