- 本阶段围绕当下国内最主流的前端核心框架 Vue.js 展开,深入框架内部,通过解读源码或者手写实现的方式,剖析 Vue.js 框架的内部实现原理,让你做到知其所以然。同时我们还会介绍 Vue.js 的进阶用法、周边生态以及性能优化,让你轻松应对更加复杂的项目业务需求。
模块五 Vue.js 3.0 Composition APIs 及 3.0 原理剖析
- 本模块中围绕 Vue.js 3.0,重点介绍 Vue.js 3.0 的设计初衷以及 Vue.js 3.0 的新特性,Vue.js 3.0 和 Vue.js 2.0 的对比,Composition APIs 快速入门,Vue.js 3.0 源码解析。让你可以更快上手,把握 Vue.js 3.0 的新特性。
任务一:Vue 3.0介绍
- Vue.js 3.0 源码组织方式
- 源码组织方式的变化
- Vue3全部采用TypeScript重写,使用Monorepo的方式来组织代码结构,把独立的功能模块都提取到不同的包中。90%以上API都兼容2.x
- Composition API 根据社区反馈新增了组合API,是为了解决V2.x 开发大型项目时遇到超大组件,options API 不好拆分和重用的问题
- 性能方面
- 使用proxy代理对象重写了响应式代码,并且对编译器做了优化,重写了虚拟DOM,从而让渲染和update的性能都有了大幅度提升
- 官方介绍,服务端渲染的性能也提高了2-3倍
- Vite
- 在开发阶段,测试项目的时候,不需要打包,可以直接去运行项目
- packages 目录结构
- compiler-core 和平台无关的编译器
- compiler-dom 浏览器平台下的编译器,依赖前者
- compiler-sfc single-file-component 用来编译单文件组件,依赖前两者
- compiler-ssr 服务端渲染编译器,依赖于compiler-dom
- reactivity 数据响应式系统,可以独立运行
- runtime-core 和平台无关的运行时
- runtime-dom 针对浏览器的运行时
- runtime-test
- 专门为测试编写的轻量级运行时,这个运行时渲染出来的DOM树,其实是一个js对象,所以这个运行时可以运行在任何js环境中,可以测试渲染是否正确
- 还可以用于序列化DOM,触发DOM事件,以及记录某次DOM中的更新操作
- server-renderer 用于服务端渲染
- shared 是Vue内部使用的一些公共的API
- size-check 私有的包,不会发布到NPM,在tree sharking 后检查包的大小
- template-explorer 在浏览器运行的时时编译组件,它会输出render函数
- vue 构建完整版vue
- 不同的构建版本
- 构建版本
- packages/vue
- 分为四类
- cjs commonJs
- vue.cjs.js 开发版,代码没有被压缩
- vue.cjs.prod.js 生成版,被压缩
- 都是完整版,包含运行时和编译器
- global
- vue.global.js
- vue.global.prod.js
- vue.runtime.global.js
- vue.runtime.global.prod.js
- 前两个完整版vue,包含编译器和运行时,后面两个是只包含运行时的构建版本
- 这四个都可以在浏览器中通过script标签直接导入,导入js后,会增加一个全局vue对象
- browser
- vue.esm-browser.js
- vue.esm-browser.prod.js
- vue.runtime.esm-browser.js
- vue.runtime.esm-browser.prod.js
- 都包含ESM,浏览器原生模块化方式,在浏览器中可以直接通过script type=module的方式导入这些模块
- bundler
- vue.esm-bundler.js
- vue-runtime.esm-bundler.js
- 没有打包所有的代码,要配合打包工具来使用,使用ESM模块化的方式
- 使用脚手架创建的项目中,默认导入第二个,这个文件只有运行时,是vue的最小版本
- 在项目开发完毕,重新打包的时候,只会打包使用到的代码,可以让vue体积更小
- cjs commonJs
- Composition API 设计动机
- RFC(Request For Comments)
- 首先官方给出一些提案,然后收集社区反馈并讨论,最后确认
- http; rfc仓库
- Composition API RFC 文档,介绍Composition的使用
- http;
- 设计动机
- Options API
- 包含一个描述组件选项(data、methods、props等)的对象
- Options API 开发复杂组件,同一个功能逻辑的代码被拆分到不同选项
- Options API Demo
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8iJX8lCa-1628175828313)(./img/1627893477037.jpg)] - 需要在多个选项中增加代码,如果代码比较多,就需要上下拖动滚动条
- 同一逻辑的代码,被拆分到不同位置,不方便提取重用代码
- 使用Options API,还难以提取组件中可重用的逻辑,输入Vue2有mixin混入机制,可以把组件中重复的代码提取并重用,但是mixin使用也有问题,比如命名冲突,或者数据来源不清晰
- Options API
- Composition API
- Vue.js 3.0 新增的一组API
- 一组基于函数的API
- 可以更灵活的组织组件的逻辑
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kaPoh3uD-1628175828315)(./img/1627893931881.jpg)] - 相比于Options API,这样的好处在于,查看某个逻辑的时候,只需要关注具体的函数即可,当前的逻辑代码,都封装在函数内部
- 开发的组件中,需要提取可复用的逻辑,这个时候可以选择Composition API
- 性能提升
- 响应式系统升级
- 使用proxy对象,重写响应式系统
- Vue.js 2.x 中响应式系统的核心defineProperty
- Vue.js 3.0 中使用Proxy对象重写响应式系统
- 可以监听动态新增的属性
- 可以监听删除的属性
- 可以监听数组的索引和length属性
- proxy返回的是一个新对象,可以只操作新对象达到目的,不需要初始化的时候深度遍历监听,性能高于Object.defineProperty;而Object.defineProperty只能遍历对象属性直接修改。有多层属性嵌套,只有访问这属性的时候,才会递归处理下一级的属性。
- 使用proyx默认就可以监听到动态添加的属性,而Vue2 需要调用Vue.set 方法来处理,
- 编译优化
- 通过重写编译过程和优化虚拟DOM,提升渲染性能
- vue2中渲染的最小单位是组件
- Vue.js 2.x 中通过标记静态根节点,优化diff的过程,静态节点还需要diff,这个过程没有被优化
- Vue.js 3.0 中标记和提升所有的静态根节点,diff 的时候只需要对比动态节点内容
- Fragments(vscode中升级vetur插件,不然模板中没有唯一根节点,提示错误) 片段的特性,模版中不需要创建唯一的一个根节点,模版中可以直接放文本内容或者同级标签
- 静态提升 静态节点都会被提升到render的外层,只有在初始化的时候才会被创建一次,当再次调用render的时候不需要再次创建静态节点,直接重用上一次创建的vnode
- Patch flag(标记动态节点) 值为1,表示只有文本内容是动态的,变化时,只会比较文本内容。9代表 text和 props是动态内容
- 缓存事件处理函数(减少了不必要的更新操作) 首次渲染会生成一个新的函数,会把新的函数缓存到_cache中,缓存的函数,永远不会发生变化,绑定不会发生变化,但是运行这个函数的时候,会去获取最新的 handler,避免来不必要的更新
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EE4h5Hlu-1628175828316)(./img/1627896757018.jpg)]
- 源码体积的优化
- Vue.js 3.0 中移除了一些不常用的API
- 例如:inline-template、filer等
- 通过优化源码体积和更好tree shaking 的支持,减少打包的体积,依赖ESM,模块化的静态语法结构,通过编译阶段的静态分析,找到没有引入的模块,在打包的时候,直接过滤掉。内置的指令如keep-alive 都是按需引入,很多api都是支持tree shaking,一些新增的api,如果没有使用,是不会打包的,默认vue的核心模块都会被打包
- Vue.js 3.0 中移除了一些不常用的API
- Vite (来自于法语,意思是快)
- Vue 作者开发的构建工具,意味着比过去基于webpack的vue-cli更快
- ES Module
- 现代浏览器都支持ESModule (IE不支持)
- 通过下面的方式加载模块
- 支持模块的script默认延迟加载
- 类似于script标签设置defer
- 在文档解析完成后,触发DOMContentLoaded 事件前执行
- index.js
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mtppBeY4-1628175828317)(./img/1627898077785.jpg)]const app = document.querySelector('#app') console.log(app.innerHTML)
- Vite
- Vite as Vue-cli
- Vite 在开发模式下不需要打包可以直接运行,因为在开发模式下使用浏览器原生支持的ESM加载模块。因为不需要打包代码,在开发模式下秒开
- Vite 会开启一个测试的服务器,会拦截浏览器发送的请求。浏览器会向服务器发送请求,获取相应的模块,vite会对浏览器不识别的模块进行处理
- 比如import单文件组件的时候,后缀为.vue,会在服务器上,对.vue文件进行编译,把编译结果返回给浏览器
- .vue的会修改响应头的content-type:application/javascript,Response 已经是编译好的js代码
- 如果是单文件组件,会调用compiler-sfc编译单文件组件,并把编译的结果返回给浏览器
- Vite 特点
- 快速冷启动
- 按需编译
- 模块热更新 模块热更新的性能与总数无关
- Vite在生产环境下使用Rollup打包
- 基于 ESModule的方式打包,不需要使用babel 把inport转换为require以及一些辅助函数,因此打包体积更小
- Vue-CLI 开发模式下必须对项目打包才可以运行
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3Go8kztH-1628175828318)(./img/1627898902125.jpg)]
- Vite 在开发模式下不需要打包可以直接运行,因为在开发模式下使用浏览器原生支持的ESM加载模块。因为不需要打包代码,在开发模式下秒开
任务二:Composition API
-
Composition API
-
生命周期钩子函数
<div id="app">
{{ x }}
{{ y }}
</div>
<script type="module">
// options api 生命周期的钩子函数转换到 setup 中的写法,只需要在钩子面前加个on,然后首字母大写
import { createApp, reactive, onMounted, onUnmounted, toRefs } from './node_modules/vue/dist/vue.esm-browser.js'
function useMousePosition() {
const position = reactive({ // 创建响应式对象
x: 0,
y: 0
})
const update = e => {
position.x = e.pageX
position.y = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
// return position
return toRefs(position)
}
const app = createApp({ // 创建vue实例
// 第一个参数 props
// 第二个参数 context,attrs、emit、slots
setup() { // Composition API 入口
// const position = useMousePosition()
const { x, y } = useMousePosition()
return {
x,
y
}
}
})
app.mount('#app')
</script>
- reactive-toRefs-ref
- reactive 通过proxy把一个对象包装为响应式对象
- toRefs 可以把一个响应式对象中的所有属性都转换为响应式的带有value属性的对象
- 要求传入的必须是一个代理对象
- 内部会创建一个新的对象,然后遍历这个代理对象的所有属性,把所有属性的值都转换为响应式对象,挂载到新创建的对象上,最后返回新创建的对象
- 内部会为代理对象的每一个属性创建一个具有value属性的对象,该对象是响应式的,vulue属性有get和set
- 通过 toRefs 处理 reactive 返回对象,可以进行解构操作
- ref 能把基本类型的数据包装成响应式对象
- ref 有一个vulue属性,如果在模版中使用可以省略。在Composition API必须要。
- 如果传入是个对象,调用reactive方法,如果基本类型,则创建一个只有value属性的对象,在value属性的get,set方法中处理值
const count = ref(0) count.value++
- computed
- 第一种用法
computed(() => count.value + 1)
- 第二种用法
const count = ref (1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
- Watch
- Watch 的三个参数
- 第一个参数:要监听的数据 是 ref或者reactive返回的对象
- 第二个参数:监听到数据变化后执行的函数,这个函数有两个值分别是新值和旧值
- 第三个参数:选项对象,deep 和immediate
- Watch 的返回值
- 取消监听的函数
- watchEffect
- 是watch函数的简化版本,也用来监视数据的变化
- 接收一个函数作为参数,监听函数内响应式数据的变化,也会返回一个取消监听的函数
- todolist-功能演示
- 添加待办事项
- 删除待办事项
- 编辑待办事项
- 切换待办事项
- 存储待办事项
- todolist-项目结构
- 使用vue-cli创建vue3项目(需4.5以上版本)vue create my-project
-
todolist-添加待办事项
-
todolist-删除待办事项
-
todolist-编辑待办事项
- 编辑待办事项
- 双击待办项,展示编辑文本框
- 按回车或者编辑文本框失去焦点,修改数据
- 按ese取消编辑
- 把编辑文本框清空按回车,删除这一项
- 显示编辑文本框的时候获取焦点
- todolist-编辑待办事项-编辑文本框获取焦点
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BHo8ZQNK-1628175828320)(./img/1628037206241.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3N2UFnJE-1628175828320)(./img/1628037282688.jpg)]
- 第二个参数是函数的时候用法vue2和vue3是一样的,vue3这个函数是在mounted 和uodated 之间去执行,和vue2执行时机一样,vue2这个函数是在bind和update的时候执行,el是指令绑定的元素,binding这个元素可以获取指令对应的值
- todolist-切换待办事项-演示效果
- 切换待办事项状态
- 点击checkbox,改变所有待办项状态
- All/Active/Completed
- 其它
- 显示未完成待办项个数
- 移除所有完成的项目
- 如果没有待办项,隐藏main 和footer
-
todolist-切换待办事项-改变待办事项完成状态
-
todolist-切换待办事项-切换状态
-
todolist-切换待办事项-其它
-
todolist-存储待办事项
<template>
<section id="app" class="todoapp">
<header class="header">
<h1>todos</h1>
<input
class="new-todo"
placeholder="What needs to be done?"
autocomplete="off"
autofocus
v-model="input"
@keyup.enter="addTodo"
>
</header>
<section class="main" v-show="count">
<input id="toggle-all" class="toggle-all" v-model="allDone" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo"
:class="{ editing: todo === editingTodo, completed: todo.completed }"
>
<div class="view">
<input class="toggle" type="checkbox" v-model="todo.completed">
<label @dblclick="editTodo(todo)">{{ todo.text }}</label>
<button class="destroy" @click="remove(todo)"></button>
</div>
<input
class="edit"
type="text"
v-editing-focus="todo === editingTodo"
v-model="todo.text"
@keyup.enter="doneEdit(todo)"
@blur="doneEdit(todo)"
@keyup.esc="cancelEdit(todo)"
>
</li>
</ul>
</section>
<footer class="footer" v-show="count">
<span class="todo-count">
<strong>{{ remainingCount }}</strong> {{ remainingCount > 1 ? 'items' : 'item' }} left
</span>
<ul class="filters">
<li><a href="#/all">All</a></li>
<li><a href="#/active">Active</a></li>
<li><a href="#/completed">Completed</a></li>
</ul>
<button class="clear-completed" @click="removeCompleted" v-show="count > remainingCount">
Clear completed
</button>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<!-- Remove the below line ↓ -->
<p>Template by <a href="http://sindresorhus.com">Sindre Sorhus</a></p>
<!-- Change this out with your name and url ↓ -->
<p>Created by <a href="https://www.lagou.com">教瘦</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
</template>
<script>
import './assets/index.css'
import useLocalStorage from './utils/useLocalStorage'
import { ref, computed, onMounted, onUnmounted, watchEffect } from 'vue'
const storage = useLocalStorage()
// 1. 添加待办事项
const useAdd = todos => {
const input = ref('')
const addTodo = () => {
const text = input.value && input.value.trim()
if (text.length === 0) return
todos.value.unshift({
text,
completed: false
})
input.value = ''
}
return {
input,
addTodo
}
}
// 2. 删除待办事项
const useRemove = todos => {
const remove = todo => {
const index = todos.value.indexOf(todo)
todos.value.splice(index, 1)
}
const removeCompleted = () => {
todos.value = todos.value.filter(todo => !todo.completed)
}
return {
remove,
removeCompleted
}
}
// 3. 编辑待办项
const useEdit = remove => {
let beforeEditingText = ''
const editingTodo = ref(null)
const editTodo = todo => {
beforeEditingText = todo.text
editingTodo.value = todo
}
const doneEdit = todo => {
if (!editingTodo.value) return
todo.text = todo.text.trim()
todo.text || remove(todo)
editingTodo.value = null
}
const cancelEdit = todo => {
editingTodo.value = null
todo.text = beforeEditingText
}
return {
editingTodo,
editTodo,
doneEdit,
cancelEdit
}
}
// 4. 切换待办项完成状态
const useFilter = todos => {
const allDone = computed({
get () {
return !todos.value.filter(todo => !todo.completed).length
},
set (value) {
todos.value.forEach(todo => {
todo.completed = value
})
}
})
const filter = {
all: list => list,
active: list => list.filter(todo => !todo.completed),
completed: list => list.filter(todo => todo.completed)
}
const type = ref('all')
const filteredTodos = computed(() => filter[type.value](todos.value))
const remainingCount = computed(() => filter.active(todos.value).length)
const count = computed(() => todos.value.length)
const onHashChange = () => {
const hash = window.location.hash.replace('#/', '')
if (filter[hash]) {
type.value = hash
} else {
type.value = 'all'
window.location.hash = ''
}
}
onMounted(() => {
window.addEventListener('hashchange', onHashChange)
onHashChange()
})
onUnmounted(() => {
window.removeEventListener('hashchange', onHashChange)
})
return {
allDone,
count,
filteredTodos,
remainingCount
}
}
// 5. 存储待办事项
const useStorage = () => {
const KEY = 'TODOKEYS'
const todos = ref(storage.getItem(KEY) || [])
watchEffect(() => {
storage.setItem(KEY, todos.value)
})
return todos
}
export default {
name: 'App',
setup () {
const todos = useStorage()
const { remove, removeCompleted } = useRemove(todos)
return {
todos,
remove,
removeCompleted,
...useAdd(todos),
...useEdit(remove),
...useFilter(todos)
}
},
directives: {
editingFocus: (el, binding) => {
binding.value && el.focus()
}
}
}
</script>
<style>
</style>
任务三:Vue.js 3.0 响应式系统原理
- 响应式系统原理-介绍
- Vue.js 响应式回顾
- proxy 对象实现属性监听
- 多层属性嵌套,在访问属性过程中处理下一级属性
- 默认监听动态添加的属性
- 默认监听属性的删除操作
- 默认监听数组索引和length属性
- 可以作为单独的模块使用
- 核心函数
- reactive/ref/toRefs/computed
- effect
- track
- trigger
- 响应式系统原理-Proxy对象回顾
'use strict'
// 问题1: set 和 deleteProperty 中需要返回布尔类型的值
// 在严格模式下,如果返回 false 的话会出现 Type Error 的异常
const target = {
foo: 'xxx',
bar: 'yyy'
}
// Reflect.getPrototypeOf()
// Object.getPrototypeOf()
const proxy = new Proxy(target, {
get (target, key, receiver) {
// return target[key]
return Reflect.get(target, key, receiver)
},
set (target, key, value, receiver) {
// target[key] = value
return Reflect.set(target, key, value, receiver)
},
deleteProperty (target, key) {
// delete target[key]
return Reflect.deleteProperty(target, key)
}
})
proxy.foo = 'zzz'
// delete proxy.foo
// 问题2:Proxy 和 Reflect 中使用的 receiver
// Proxy 中 receiver:Proxy 或者继承 Proxy 的对象
// Reflect 中 receiver:如果 target 对象中设置了 getter,getter 中的 this 指向 receiver
const obj = {
get foo() {
console.log(this)
return this.bar
}
}
const proxy = new Proxy(obj, {
get (target, key, receiver) {
if (key === 'bar') {
return 'value - bar'
}
return Reflect.get(target, key, receiver)
}
})
console.log(proxy.foo)
- 响应式系统原理-reactive
- reactive
- 接收一个参数,判断这个参数是否是对象
- 创建拦截器对象handler,设置get/set/deleteProperty
- 返回这个对象
- 响应式系统原理-收集依赖
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zinxg7NB-1628175828321)(./img/1628135478749.jpg)]
- 响应式系统原理-effect-track
<script type="module">
import { reactive, effect } from './reactivity/index.js'
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
let total = 0
effect(() => {
total = product.price * product.count
})
console.log(total)
product.price = 4000
console.log(total)
product.count = 1
console.log(total)
</script>
-
响应式系统原理-trigger
-
响应式系统原理-ref
- reactive vs ref
- ref 可以把基本数据类型数据,转换为响应式对象
- ref 返回的对象,重新赋值为对象也是响应式的
- reactive 返回的对象,重新赋值丢失响应式
- reactive 返回的对象不可以解构
- 如果一个对象成员比较多,使用ref响应式并不方便
<script type="module">
import { reactive, effect, ref } from './reactivity/index.js'
const price = ref(5000)
const count = ref(3)
let total = 0
effect(() => {
total = price.value * count.value
})
console.log(total)
price.value = 4000
console.log(total)
count.value = 1
console.log(total)
</script>
- 响应式系统原理-toRefs
- toRefs 的作用reactive对象的每一个属性转换为类似ref返回的对象,这样就可以对reactive对象进行解构
<script type="module">
import { reactive, effect, toRefs } from './reactivity/index.js'
function useProduct () {
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
return toRefs(product)
}
const { price, count } = useProduct()
let total = 0
effect(() => {
total = price.value * count.value
})
console.log(total)
price.value = 4000
console.log(total)
count.value = 1
console.log(total)
</script>
- 响应式系统原理-computed
<script type="module">
import { reactive, effect, computed } from './reactivity/index.js'
const product = reactive({
name: 'iPhone',
price: 5000,
count: 3
})
let total = computed(() => {
return product.price * product.count
})
console.log(total.value)
product.price = 4000
console.log(total.value)
product.count = 1
console.log(total.value)
</script>
// ./reactivity/index.js
const isObject = val => val !== null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target) : target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => hasOwnProperty.call(target, key)
export function reactive (target) {
if (!isObject(target)) return target
const handler = {
get (target, key, receiver) {
// 收集依赖
track(target, key)
const result = Reflect.get(target, key, receiver)
return convert(result)
},
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver)
let result = true
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
// 触发更新
trigger(target, key)
}
return result
},
deleteProperty (target, key) {
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
// 触发更新
trigger(target, key)
}
return result
}
}
return new Proxy(target, handler)
}
let activeEffect = null
export function effect (callback) {
activeEffect = callback
callback() // 访问响应式对象属性,去收集依赖
activeEffect = null
}
let targetMap = new WeakMap()
export function track (target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
export function trigger (target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => {
effect()
})
}
}
export function ref (raw) {
// 判断 raw 是否是ref 创建的对象,如果是的话直接返回
if (isObject(raw) && raw.__v_isRef) {
return
}
let value = convert(raw)
const r = {
__v_isRef: true,
get value () {
track(r, 'value')
return value
},
set value (newValue) {
if (newValue !== value) {
raw = newValue
value = convert(raw)
trigger(r, 'value')
}
}
}
return r
}
export function toRefs (proxy) {
const ret = proxy instanceof Array ? new Array(proxy.length) : {}
for (const key in proxy) {
ret[key] = toProxyRef(proxy, key)
}
return ret
}
function toProxyRef (proxy, key) {
const r = {
__v_isRef: true,
get value () {
return proxy[key]
},
set value (newValue) {
proxy[key] = newValue
}
}
return r
}
export function computed (getter) {
const result = ref()
effect(() => (result.value = getter()))
return result
}
任务四:Vite 实现原理
- Vite
- Vite 概念
- Vite 是一个面向现代浏览器的一个更轻、更快的web应用开发工具
- 它基于ECMAScript 标准原生模块系统 (ES Module)实现
- 它的出现是为了解决wabpack在开发阶段使用webpack devserver冷启动时间过长,另外,webpack HMR热更新速度慢的问题
- 使用 Vite创建的项目,就是一个普通的Vue3应用,相比与vue-cli创建的项目,也少了很多配置和依赖
- Vite 项目依赖
- Vite 命令行工具
- @vue/compiler-sfc 用来编译.vue结尾的单文件组件,vue2使用的是vue-template-compiler
- 只支持vue3,也可以通过模版创建其它类型项目
- 基础使用
- vite serve
- vite build
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e5b09BQo-1628175828322)(./img/1628154064066.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-neynugIs-1628175828323)(./img/1628154099071.jpg)]
- HMR
- Vite HMR 立即编译当前所修改的文件
- webpack HMR
- 会自动以这个文件为入口重写build一次,所有的涉及到的依赖也都会被加载一遍
- Build
- vite build
- Rollup 最终还是会把文件打包编译到一起,对于代码切割的需求,Vite采用的是原生的动态导入的特性实现的,打包结果只支持现代浏览器,动态导入有相应的Polyfill
- Dynamic import(动态导入)
- Polyfill
- vite build
- 打包 or 不打包
- 使用 Webpack 打包的两个原因
- 浏览器环境并不支持模块化 (IE11不支持)
- 零散的模块文件会产生大量的HTTP请求 (http2 已经解决,可以复用链接)
- 使用 Webpack 打包的两个原因
- 开箱即用
- TypeScript 内置支持
- less/sass/stylus/postcss- 内置支持(需要单独安装)
- jsx
- Web Assembly
- Vite 特性
- 快速冷启动
- 模块热更新
- 按需编译
- 开箱即用
- Vite 实现原理-静态Web服务器
- Vite 核心功能
- 静态Web服务器
- 编译单文件组件
- 拦截浏览器不识别模块,并处理
- HMR
- 步骤
- 安装依赖 koa koa-send
- 配置 package.json
{ "name": "vite-cli", "bin": "index.js" }
- 编写 index.js
- 把写好的包链接到npm npm link
- 到需要开启静态web服务器项目的根目录下执行 vite-cli
-
Vite 实现原理-修改第三方模块的路径
-
Vite 实现原理-加载第三方模块
-
Vite 实现原理-编译单文件组件
- npm i @vue/compiler-sfc
- Vite 实现原理-编译单文件组件
#!/usr/bin/env node
const path = require('path')
const { Readable } = require('stream')
const Koa = require('koa')
const send = require('koa-send')
const compilerSFC = require('@vue/compiler-sfc')
const app = new Koa()
const streamToString = stream => new Promise((resolve, reject) => {
const chunks = []
stream.on('data', chunk => chunks.push(chunk))
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')))
stream.on('error', reject)
})
const stringToStream = text => {
const stream = new Readable()
stream.push(text)
stream.push(null)
return stream
}
// 3. 加载第三方模块
app.use(async (ctx, next) => {
// ctx.path --> /@modules/vue
if (ctx.path.startsWith('/@modules/')) {
const moduleName = ctx.path.substr(10)
const pkgPath = path.join(process.cwd(), 'node_modules', moduleName, 'package.json')
const pkg = require(pkgPath)
ctx.path = path.join('/node_modules', moduleName, pkg.module)
}
await next()
})
// 1. 静态文件服务器
app.use(async (ctx, next) => {
await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
await next()
})
// 4. 处理单文件组件
app.use(async (ctx, next) => {
if (ctx.path.endsWith('.vue')) {
const contents = await streamToString(ctx.body)
const { descriptor } = compilerSFC.parse(contents)
let code
if (!ctx.query.type) {
code = descriptor.script.content
// console.log(code)
code = code.replace(/export\s+default\s+/g, 'const __script = ')
code += `
import { render as __render } from "${ctx.path}?type=template"
__script.render = __render
export default __script
`
} else if (ctx.query.type === 'template') {
const templateRender = compilerSFC.compileTemplate({ source: descriptor.template.content })
code = templateRender.code
}
ctx.type = 'application/javascript'
ctx.body = stringToStream(code)
}
await next()
})
// 2. 修改第三方模块的路径
app.use(async (ctx, next) => {
if (ctx.type === 'application/javascript') {
const contents = await streamToString(ctx.body)
// import vue from 'vue'
// import App from './App.vue'
ctx.body = contents
.replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
.replace(/process\.env\.NODE_ENV/g, '"development"')
}
})
app.listen(3000)
console.log('Server running @ http://localhost:3000')