vue3.0CompositionAPIs及3.0原理
一、3.0介绍
- 源码组织方式的改变
- Composition API
- 性能提升
- Vite
1、源码组织方式
- 源码采用TypeScript重写
提高了代码的可维护性。大型项目的开发都推荐使用类型化的语言,在编码的过程中检查类型的问题。 - 使用Monorepo管理项目结构
使用一个项目管理多个包,把不同功能的代码放到不同的package中管理,每个功能模块都可以单独发布,单独测试,单独使用
2、不同构建版本
Vue3中不再构建UMD模块化的方式,因为UMD会让代码有更多的冗余,它要支持多种模块化的方式。Vue3中将CJS、ESModule和自执行函数的方式分别打包到了不同的文件中。在packages/vue/dist中有Vue3的不同构建版本。
- cjs(两个版本都是完整版,包含编译器)
- vue.cjs.js
- vue.cjs.prod.js(生产版,代码进行了压缩)
- global(这四个版本都可以在浏览器中直接通过scripts标签导入,导入js之后会增加一个全局的Vue对象)
- vue.global.js(完整版,包含编译器和运行时,开发版本)
- vue.global.prod.js(完整版,包含编译器和运行时,代码进行了压缩)
- vue.runtime.global.js(只包含运行时的版本)
- vue.runtime.global.prod.js
- browser(四个版本都包含esm,浏览器的原生模块化方式,可以直接通过
3、Composition API
- RFC (Request For Coments)
- https://github.com/vuejs/rfcs
- Composition API RFC
- https://composition-api.vuejs.org
设计动机
- Options API
- 包含一个描述组件选项(data、methods、props等)的对象
- Options API 开发复杂组件,同一个功能逻辑的代码被拆分到不同选项
- Composition API
- Vue.js 3.0 新增的一组API
- 一组基于函数的API
- 可以更灵活的组织组件的逻辑
4、性能提升
1、响应式系统升级
Vue3使用Proxy对象重写了响应式系统。
- Vue.js 2.x中响应式系统的核心 defineProperty,初始化的时候递归遍历所有的属性,转化为getter、setter,若data中属性又是对象的话,需要递归处理子对象属性(初始化时)
- Vue.js 3.0中使用Proxy对象重写响应式系统
- 可监听动态新增的属性
- 可以监听删除的属性
- 可以监听数组的索引和length属性
2. 编译优化
重写了DOM提高渲染的性能。
- Vue.js 2.x中通过标记静态根节点,优化diff的过程
- Vue.js 3.0 中标记和提升所有的静态根节点,diff的时候只需 要对比动态节点内容
- Fragments(升级vetur插件)
- 静态提升
- Patch flag
- 缓存事件处理函数
3.源码体积的优化
通过优化源码的体积和更好的TreeShaking的支持,减少大打包的体积
- Vue.js 3.0中移除了一些不常用的API
- 例如:inline-template、filter等
- Tree-shaking
- 例如:Vue3中的没用到的模块不会被打包,但是核心模块会打包。Keep-Alive、transition等都是按需引入的内置指令v-model都是按需引入的。
5 Vite
Vue的打包工具。Vite是法语中的"快"的意思
1.ES Module
- 现代浏览器都支持ES Module(IE不支持)
- 通过下面的方式加载模块
- 支持模块的script默认延迟加载
- 有了type="module"的模块是延迟加载的,类似于script标签设置defer
- 在文档解析完成后,也就是DOM树生成之后,触发DOMContentLoaded事件前执行
相关代码查看gtee
- 在文档解析完成后,也就是DOM树生成之后,触发DOMContentLoaded事件前执行
2.Vite as Vue-CLI
Vite 快就是使用浏览器支持的ES Modules的方式,避免开发环境打包,从而提升开发速度
- Vite 在开发模式下不需要打包可以直接运行
- Vue-Cli 开发模式下必须对项目打包才可以运行
3.Vite特点
- 快速冷启动(因为不需要打包)
- 按需编译(当前代码需要加载时才编译)
- 模块热更新(更新性能和模块总数无关)
- Vite 在生产环境下使用Rollup 打包(打包体积小)
- 基于ES Module 的方式打包
- Vue-Cli 使用 Webpack 打包
4.Vite创建项目
- Vite 创建项目
- npm init vite-app
- cd
- npm install
- npm run dev
- 基于模版创建项目
- npm init vite-app --template react
- npm init vite-app --template preact
二、Composition API
1.Composition API
启动本地http打开html,需要安装
Live Server
安装Vue3.0 ,体验 createApp 的使用npm install vue@3.0.0-rc.9
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> x: {{ position.x }} y: {{ position.y }} </div> <script type="module"> import { createApp } from './node_modules/vue/dist/vue.esm-browser.js' const app = createApp({ data () { return { position: { x: 0, y: 0 } } } }) console.log(app) app.mount('#app') </script> </body> </html>
Vue 3.0 和 Vue2.0 的区别,成员要少很多,没有$开头, 使用方式和以前一样
setUp 函数:Composition api的入口
createApp:創建vue对象
reactive:创建响应式对象<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="app"> x: {{ position.x }} <br> y: {{ position.y }} </div> <script type="module"> import { createApp, reactive} from './node_modules/vue/dist/vue.esm-browser.js' const app = createApp({ setup () { // 第一个参数 props // 第二个参数 context, attrs, emit, slots const position = reactive({ x: 0, y: 0 }) return { position } } }) console.log(app) app.mount('#app') </script> </body> </html>
2.生命周期钩子函数
setup中使用钩子函数
当鼠标移动的时候显示鼠标移动的位置,当组件卸载时,鼠标移动的事件也要移除<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="app"> x: {{ position.x }} <br> y: {{ position.y }} </div> <script type="module"> import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js' const app = createApp({ setup () { // 第一个参数 props // 第二个参数 context, attrs, emit, slots 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 } } }) console.log(app) app.mount('#app') </script> </body> </html>
将获取鼠标位置的方法封装到一个函数中,优化代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="app"> x: {{ position.x }} <br> y: {{ position.y }} </div> <script type="module"> import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js' function useMousePosition(){ // 第一个参数 props // 第二个参数 context, attrs, emit, slots 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 } const app = createApp({ setup () { const position = useMousePosition(); return { position } } }) console.log(app) app.mount('#app') </script> </body> </html>
3.reactive-toRefs-ref
先看上一节出现的小问题
import { createApp, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js' function useMousePosition () { // 第一个参数 props // 第二个参数 context, attrs, emit, slots 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 } const app = createApp({ setup () { // const position = useMousePosition() const { x, y} = useMousePosition() return { x, y } } }) console.log(app) app.mount('#app')
对
useMousePosition
进行解构后,数据不是响应式的了这里的
position
是响应式对象,因为在useMousePosition
中调用了reactive 函数,把传入的对象包装成了Proxy
对象,也就是说position
就是proxy
对象,当position
访问 x, y的时候会调用代理中的getter
拦截收集依赖,变化的时候会调用setter
。
当把代理对象解构之后,x,y就是两个基本类型的变量。当重新复制时,无法调用代理对象的getter
和setter
,故无法进行更新操作
解决方法:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="app"> x: {{ x }} <br> y: {{ y }} </div> <script type="module"> import { createApp, reactive, onMounted, onUnmounted ,toRefs} from './node_modules/vue/dist/vue.esm-browser.js' function useMousePosition(){ // 第一个参数 props // 第二个参数 context, attrs, emit, slots 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) }) console.log(position); return toRefs(position) } const app = createApp({ setup () { // const position = useMousePosition() const { x, y} = useMousePosition() return { x, y } } }) console.log(app) app.mount('#app') </script> </body> </html>
toRefs原理
toRefs 要求传入的参数 必须为代理对象,当前的 position 就是 reactive返回的代理对象,如果不是的话,会发出警告,提示传递代理对象,
内部会创建一个新的对象,然后遍历传入代理对象的所有属性,把所有属性的值都转换成响应式对象,相当于将postion 的x, y属性转换成响应式对象,
挂在到新创建的对象上,最后把新创建的对象返回。内部为代理对象的每一个属性创建一个具有 value 属性,value属性具有 getter,setter, getter 中返回对象属性的值,
setter中给代理对象赋值。所以返回的每一个属性都是响应式的ref 把普通数据转转换成响应式数据,ref 可以把基本类型包装成响应式数据
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="app"> <button @click="increase">按钮</button> <span>{{count}}</span> </div> <script type="module"> import { createApp, ref} from './node_modules/vue/dist/vue.esm-browser.js' function useCount(){ const count = ref(0) return { count, increase:()=>{ count.value++; } } } const app = createApp({ setup () { return { ...useCount() } } }).mount('#app') </script> </body> </html>
4.computed
作用:简化模板中的代码,可以缓存计算的结果,当数据变化后,才会重新计算
- 第一种用法:
wacth(() => { count.value + 1})
- 第二种用法
const count = ref(1) const plusOne = computed({ get: () => { count.value + 1 }, set: val => { conut.value = val - 1 } })
案例展示:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> <button @click="push">按钮</button> 未完成:{{ activeCount }} </div> <script type="module"> import { createApp, reactive, computed } from './node_modules/vue/dist/vue.esm-browser.js' const data = [ { text: '看书', completed: false }, { text: '敲代码', completed: false }, { text: '约会', completed: true } ] createApp({ setup(){ const todos = reactive(data);//将data转化为响应式对象 const activeCount = computed(()=>{ return todos.filter(item=>!item.completed).length }) return { activeCount, push:()=>{ todos.push({ text:'开会', completed:false }) } } } }).mount("#app") </script> </body> </html>
5.watch
- Watch 的三个参数
- 第一个参数: 要监听的数据(可谓获取值得函数,ref ,reactive返回的数据,或者是数组)
- 第二个参数: 监听到数据变化后执行的函数,这个函数有两个参数分别是新- 值和旧值
- 第三个参数: 选项对象, deep 和 immediate,
- Watch 的返回值
- 取消监听的函数
使用案例
- 取消监听的函数
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> <p> 请问一个 yes/no 的问题: <input v-model="question"> </p> <p>{{ answer }}</p> </div> <script type="module"> // https://www.yesno.wtf/api import { createApp, ref, watch } from './node_modules/vue/dist/vue.esm-browser.js' createApp({ setup () { const question = ref('') const answer = ref('') watch(question, async (newValue, oldValue) => { const response = await fetch('https://www.yesno.wtf/api') const data = await response.json() answer.value = data.answer }) return { question, answer } } }).mount('#app') </script> </body> </html>
6.WatchEffect
- 是watch 函数的简化版本,也用来监视数据的变化
- 内部实现和watch调用的同一个函数dowatch,不同的是watchEffect没有第二个回调函数的参数
- 接收一个函数作为参数,监听函数内响应式数据的变化,会立即执行这个函数,当数据变化会重新运行此函数
演示:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <div id="app"> <button @click="increase">increase</button> <button @click="stop">stop</button> <br> {{ count }} </div> <script type="module"> import { createApp, ref, watchEffect } from './node_modules/vue/dist/vue.esm-browser.js' createApp({ setup () { const count = ref(0) const stop = watchEffect(() => { console.log(count.value) }) return { count, stop, increase: () => { count.value++ } } } }).mount('#app') </script> </body> </html>
三、TodoList
代办事情清单:使用Composition API实现此案例
ToDoList 案例:- 添加代办事项
- 删除待办事项
- 编辑待办事项
- 切换待办事项
- 存储待办事项
1.TodoList-项目结构
升级 Vue-cli 需要升级到4.5 版本以上,可以选择Vue3.0 进行安装
- 使用
vue -V
查看目前的版本 - 卸载之前安装的
npm uninstall vue-cli -g
- 重新安装
npm install -g @vue/cli
main.js 导入了 vue 的模块,从 Vue 模块又导入了createApp 函数,导入 app组件,将app 组件挂在到div 上
2. 添加待办事项
给输入框绑定事件
<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>
js文件内容
import './assets/index.css' // 添加待办事项 import { ref } from 'vue' 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 { addTodo, input } } export default { setup () { const todos = ref([]) return { ...useAdd(todos), todos } } }
循环遍历,展示添加的todo事件
<section class="main"> <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 todos" :key="todo.text" > <div class="view"> <input class="toggle" type="checkbox"> <label>{{ todo.text }}</label> <button class="destroy"></button> </div> <input class="edit" type="text" > </li> </ul> </section>
3. 删除代办事项
<div class="view"> <input class="toggle" type="checkbox"> <label>{{ todo.text }}</label> <!-- 删除当前的待办事项 --> <button class="destroy" @click="remove(todo)"></button> </div>
const useRemove = todos => { const remove = todo => { const index = todos.value.indexOf(todo) todos.value.splice(index, 1) } return { remove } } export default { setup () { const todos = ref([]) return { todos, ...useAdd(todos), ...useRemove(todos) } } }
4. 编辑代办事项
- 双击待办事项,展示编辑文本框
- 按回车或者编辑文本框失去焦点修改数据
- 按ESC 取消编辑
- 把编辑文本框清空按回车,删除这一项
- 显示编辑文本框的时候获取焦点
// 编辑待办事项 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 } } export default{ name:'App', setup(){ const todos = ref([]) const {remove} = useRemove(todos)//因useEdit使用remove函数,故需要先调用useRemove函数,解构remove函数 return { todos, remove, ...useAdd(todos), ...useEdit(remove) } } }
<ul class="todo-list"> <li v-for="todo in todos" :key="todo.text" :class="{ editing: todo === editingTodo}" > <!-- <li v-for="todo in todos" :key="todo.text" :class="{ editing: todo === editingTodo}" > --> <div class="view"> <input class="toggle" type="checkbox"> <label @dblclick="editTodo(todo)">{{ todo.text }}</label> <button class="destroy" @click="remove(todo)"></button> </div> <input class="edit" type="text" v-model="todo.text" @keyup.enter="doneEdit(todo)" @blur="doneEdit(todo)" @keyup.esc="cancelEdit(todo)" > </li> </ul>
5. 编辑文本框获取焦点
自定义指令
实际使用:
通过判断编辑文本框是否是当前要编辑的文本框<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)" >
自定义指令的编写
export default { setup () { const todos = ref([]) const { remove } = useRemove(todos) return { todos, remove, ...useAdd(todos), ...useEdit(remove) } }, directives: { editingFocus: (el, binding) => { binding.value && el.focus() } } }
6. 切换待办事项的状态
- 点击checkboxd,改变所有待办项状态
- All/Active(w未完成的代办项)/Completed(已完成的待办项)
其它 - 显示未完成待办项个数
- 移除所有完成的项目
- 如果没有待办项,隐藏main和footer
6.1 点击check,改变状态
// 4 切换代办项,完成状态 const useFilter = todos=>{ const allDone = computed({ get(){ return !todos.value.filter(todo=>!todo.completed).length //有未完成的代办项返回false }, set(value){ todos.value.forEach(todo=>{ todo.completed = value }) } }) return { allDone } }
自定义指令
export default{ name:'App', setup(){ const todos = ref([]) const {remove} = useRemove(todos) return { todos, remove, ...useAdd(todos), ...useEdit(remove), ...useFilter(todos) } }, directives:{ editingFocus:(el,binding)=>{ binding.value && el.focus() } } }
html代码
<section class="main" > <input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone"> <label for="toggle-all">Mark all as complete</label> <ul class="todo-list"> <li v-for="todo in todos" :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 ="cancleEdit(todo)" ><!-- 编辑代办事项的按钮 --> </li> </ul> </section>
实现效果图:
6.2 all active completed三个按钮切换
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 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, filteredTodos } }
<li v-for="todo in todos" :key="todo" :class="{ editing: todo === editingTodo , completed:todo.completed}" >
修改为:
<li v-for="todo in filteredTodos" :key="todo" :class="{ editing: todo === editingTodo , completed:todo.completed}" >
<footer class="footer"> <span class="todo-count"> item left </span> <ul class="filters"> <!-- 不使用路由 实现:监控地址中hash的变化 当组件挂载完毕,注册hashchanges事件 当组件卸载时,将hashchange事件移除 在hashchange事件中,获取当点锚点的值 --> <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"> Clear completed </button> </footer>
active:
completed
all
6.3 处理其他问题:个数显示,删除已完成的代办事项,
- 个数显示:
// 在useFilter 中加入 const remainingCount = computed(() => filter.active(todos.value).length) // 使用 return { allDone, filteredTodos, remainingCount }
html中使用:
<span class="todo-count"> <strong>{{ remainingCount }}</strong> {{ remainingCount > 1 ? 'items' : 'item' }} left </span>
- 删除已经完成的代办事项
onst 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 } }
html:
<button class="clear-completed" @click="removeCompleted" > Clear completed </button>
- 没有数据的情况下隐藏 main 和footer
js
//在useFilter函数中 const count = computed(() => todos.value.length) return { allDone, count, filteredTodos, remainingCount }
html:
6.4 存储待办事项
utils/useLocalStorage.js
function parse (str) { let value try { value = JSON.parse(str) } catch { value = null } return value } function stringify (obj) { let value try { value = JSON.stringify(obj) } catch { value = null } return value } export default function useLocalStorage () { function setItem (key, value) { value = stringify(value) window.localStorage.setItem(key, value) } function getItem (key) { let value = window.localStorage.getItem(key) if (value) { value = parse(value) } return value } return { setItem, getItem } }
App.vue导入
import useLocalStorage from './utils/useLocalStorage' const storage = useLocalStorage() //5.存储本地数据 const useStorage = () => { const KEY = 'TODOKEYS' const todos = ref(storage.getItem(KEY) || []) //先从本地数据中加载 watchEffect(() => { //监视todos的变化 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() } } }
四 响应式原理
1.vuejs响应式回顾
- Proxy对象实现属性监听
- 多层属性嵌套,在访问属性过程中处理下一级属性
- 默认监听动态添加的属性
- 默认监听属性的删除操作
- 默认监听数组索引和length属性
- 可以作为单独的模块使用
核心方法 - reactive / ref / toRefs / computed
- effect
- track
- trigger
2. 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 Reflect.set(target, key, value, receiver) }, deleteProperty (target, key) { // delete target[key] Reflect.deleteProperty(target, key) } }) proxy.foo = 'zzz' // delete proxy.foo //若不出现错误则修改为 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) }
问题1::错误
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) } }) console.log(proxy.foo)
打印结果:
js
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)
3. 响应式原理 reactive
- reactive 接受一个参数,判断这个参数是否是响应式对象
- 创建拦截器对象 handler, 设置 get / set / deleteProperty
- 返回Proxy 对象
步骤:
- 在页面上加载ES Module 的方式来实现,在 index.js 中, 先创建 reactive 的形式,直接导出一个 reactive 函数,它接收一个参数 target,
- 在函数中首先要判断是否是对象,如果不是的话直接返回,否则将target 对象转化为代理对象,定义辅助函数isObject用来判断变量是否是对象。
- 定义一个handler 对象 里面有 get / set / deleteProperty 方法, 首先在 get 方法中要去收集依赖,然后返回target 对应key 的值,通过 Reflect.get 来获取
- 注意: 如果当前key 属性对应的值还是对象,r若是对象,则将它转化为响应式对象, 如果有嵌套对象,会在get 中递归
- 在 set 中,首先要去获取get 函数的值,判断值是否相等,不相等则调用Reflect更新值,在 deleteProperty 判断是否有自己的key 属性
//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) { // 收集依赖 console.log('get ',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) // 触发更新 console.log('set ',key,value) } return result }, deleteProperty (target, key) { const hadKey = hasOwn(target, key) const result = Reflect.deleteProperty(target, key) if (hadKey && result) { // 触发更新 console.log('deleteProperty',target,key) } return result } } return new Proxy(target, handler) }
测试代码:
index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script type="module"> import { reactive } from './reactivity/index.js' const obj = reactive({ name: 'zs', age: 18 }) obj.name = 'lisi' delete obj.age console.log(obj) </script> </body> </html>
4. 响应式系统原理 - 收集依赖
通过 reactive 创建了一个响应式对象, effect 接收一个函数,和 watchEffect 用发一样,effect 内部首先会执行一次,当内部响应式数据变化,会再次执行
在收集依赖过程中会创建三个集合,targetMap、depMaps、dep, targetMap的作用是用来记录目标对象和一个字典,也就是depMaps, targetMap中key也就是目标对象
5.响应式原理 effect / track
effect 接收一个函数作为参数, 在effect 中首先要执行一次effect,在callback 中会访问响应式对象的属性,在这个过程中去收集依赖在收集依赖中,需要将callback 存储起来,定义一个activeEffect来存储callback,收集完毕,需要将 activeEffect `值设为初始值,因为在收集依赖的过程,如果有嵌套属性,是一个递归
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) }
在代理对象中调用这个函数, reactive get
get (target, key, receiver) { // 收集依赖 track(target, key) console.log('get ',key) const result = Reflect.get(target, key, receiver) return convert(result) }
6.响应式原理 触发更新 trigger
export function trigger (target, key) { const depsMap = targetMap.get(target) if (!depsMap) return const dep = depsMap.get(key) if (dep) { dep.forEach(effect => { effect() }) } }
在 reactive set 中触发更新 调用trigger, deleteProperty 调用
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) console.log('set ',key,value) } return result }, deleteProperty (target, key) { const hadKey = hasOwn(target, key) const result = Reflect.deleteProperty(target, key) if (hadKey && result) { // 触发更新 trigger(target,key) console.log('deleteProperty',target,key) } return result }
html 测试:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <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> </body> </html>
7.响应式原理- Ref
接收一个参数,可以是原始值,也可以是对象,
如果传入的是对象,并且是ref创建的对象,则直接返回
如果是普通对象,内部会调用reactive 创建响应式对象,
否则创建一个只有value 属性对象返回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 }
测试:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <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> </body> </html>
结果:
总结:
reactive 和 ref- ref 可以把基本数据类型数据,转成响应式对象
- ref 返回的对象,重新赋值成对象也是响应式的
- reactive 返回的对象,重新赋值丢失响应式
- reactive 返回的对象不可以解构
如果一个对象成员非常多的时候,使用ref 并不方便,因为总要带着value 属性,如果一个函数内部只有一个响应式数据,这个时候使用ref 会比较方便,因为可以解构返回
8.响应式系统原理- toRefs
函数的作用:函数接收一个reactive返回的响应式对象,也就是一个Proxy对象,如果传入的参数不是reactive创建的响应式对象,则直接返回,然后把传入对象的所有属性转化为一个类似于ref返回的对象,把转化后的属性挂载到一个新的对象上返回
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 }
测试:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <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> </body> </html>
9.响应式系统原理- computed
export function computed (getter) { const result = ref() effect(() => (result.value = getter())) return result }
测试数据
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <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> </body> </html>
五 Vite
1. Vite的概念
- Vite 是一个面向现代化浏览器的一个更轻、更快的 web 应用应用开发工具
- 它基于 ECMAScript 标准原生模块系统(ES Module)实现的
它的出现是为了解决
Webpack
在开发阶段,使用webpack-dev-server
冷启动时间过长和Webpack MHR
热更新反应慢的问题。使用
Vite
创建的项目,默认就是一个普通的Vue3
应用,相比于Vue CLI
创建的项目,会少了很多文件和依赖。2. Vite 项目依赖
Vite 创建的默认项目,开发依赖很少也很简单,只包含了:
- Vite
- @vue/compiler-sfc(用来编译.vue 结尾的单文件文件)
需要注意的是,Vite 目前创建的 Vue 项目只支持 3.0 的版本。在创建项目的时候,通过指定不同的模板,也可以创建其他框架的项目。
3. Vite 提供的命令
Vite 提供的命令
工作原理
用于启动一个开发的 web 服务器,在启动服务器的时候不需要编译所有的模块启动速度非常的快。
图示
在运行vite serve
的时候,不需要打包,直接开启了一个 web 服务器。当浏览器请求服务器时,例如是一个css
,或者是一个单文件组件,这个时候在服务器会把这个浏览器请求的文件先编译,然后直接把编译后的结果返回给浏览器。
这里的编译是在服务器端,并且,模块的处理是在请求到服务器端处理的。我们来回顾一下,
Vue CLI
创建的应用
Vue CLI
创建的项目启动 web 服务器用的是vue-cli-service
,当运行它的时候,它内部会使用Webpack
去打包所有的模块(如果模块很多的情况下,编译的速度会很慢),打包完成后会将编译好的模块存储到内存中,然后启动一个 web 服务器,浏览器请求web
服务器,最后才会从内存中把编译好的内容,返回到浏览器。像
Webpack
这样的工具,它的做法是将所有的模块提前都编译打包进内存里,不管模块是否被执行是否被调用,它会都打包编译,随着项目越来越大,打包后的内容也会越来越大,打包的速度也会越来越慢。而
Vite
使用现代化浏览器原生支持的ES Module
模块化的特性,省略了模块的打包环节。对于需要编译的文件,例如样式模块和单文件组件等,vite
采用了即时编译,也就是说当加载到这个文件的时候,才会去服务端编译好这个文件。所有,这种即时编译的好处体现在按需编译,速度会更快。
HMR- Vite HMR
立即编译当前所修改的文件 - Webpack HMR
会自动以这个文件为入口重新编译一次,所有的涉及到的依赖也会被加载一次
Vite 默认也支持 HMR 模块热更新,相对于 Webpack 中的 HMR 效果会更好,因为 Webpack 的 HMR 模块热跟新会从你修改的文件开始全部在编译一遍
-
vite build
- Rollup
- Dynamic import
- Polyfill
Vite 创建的项目使用 Vite build 进行生产模式的打包,这个命令内部使用过的是 Rollup 打包,最终也是把文件都打包编译在一起。对于代码切割的需求,Vite 内部采用的是原生的动态导入的方式实现的,所以打包的结果只能支持现代化的浏览器(不支持 ie)。不过相对应的 Polyfill 可以解决
- Polyfill
是否还需要打包?
随着 Vite 的出现,我们需要考虑一个问题,是否还必要打包应用。之前我们使用 Webpack 进行打包,会把所有的模块都打包进 bundle.js 中,主要有两个原因:
- 浏览器环境对原生 ES Module 的支持
- 零零散散的模块文件会产生大量的 HTTP 请求
但是,现在目前大部分的浏览器都已经支持了 ES Module。并且我们也可以使用 HTTP2 长链接去解决大量的 HTTP 请求。那是否还需要对应用进行打包,取决于你的团队和项目应用的运行环境。
浏览器对ES Module的支持
IE不支持esModule- TypeScript - 内置支持
- less/sass/stylus/postcss - 内置支持(需要单独安装)
- JSX
- Web Assemby
Vite 的特性
- 快速冷启动
- 模块热更新
- 按需编译
- 开箱即用
4. 实现Vite
4.1 vite 的工作原理,分为以下五个步骤
- 静态 web 服务器
- 编译单文件组件
- 拦截浏览器不识别的模块,并处理
- HMR(通过 WebSocket 实现,跳过)
5. 静态 web 服务器
vite
内部使用过的是koa
来开启静态服务器的,这里我们也使用 koa 来开启一个静态服务器,把当前运行的目录作为静态服务器的根目录创建一个名为
vite-cli
的空文件夹,进入该文件夹初始化package.json
,并且安装koa
和koa-send
mkdir vite-cli cd vite-cli npm init --yes npm i koa koa-send
在 package.json 来配置 bin 字段:
新建 index.js 文件,并且在第一行配置 node 的运行环境(因为我们要开发的是一个基于 node 的命令行工具,所以要指定运行 node 的位置)#!/usr/bin/env node
基于 koa 启动一个 web 静态服务器:
#!/usr/bin/env node const Koa = require('koa') const send = require('koa-send') const app = new Koa() // 1.开启静态文件服务器 app.use(async (ctx, next) => { await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' }) await next() }) app.listen(3000) console.log('Serve running @ http://localhost:3000')
接着,使用
npm link
到全局,然后打开一个使用vue3
写的项目(可以用 vite 创建一个默认项目),进入命令行终端,输入vite-cli
。如果没有报错的话,会打印出"Serve running @ http://localhost:3000"这句话,我们打开浏览器,打开这个网址。不过是一片空白的,接着我们打开 F12,会看到一个报错,报错的信息的意思是,解析 vue 模块的时候失败了,使用
import
导入模块的时候,模块的开头必须是 “/”, “./”, or “…/” 这三种其中的一个。
我们来做一个对比,我们把使用 vite 创建的项目启动后, vite-cli 创建的项目启动后的 main.js 在浏览器响应中的区别:
vite:
vite-cli:
通过上面两幅图的对比,你会发现,vite 它会处理这个模块引入的路径,它会加载一个不存在的路径 @modules,并且请求这个路径的 js 文件也是可以请求成功的。这是 vite 创建的项目启动后的 vue.js 的请求,观察响应头中的 Content-Type 字段,他是 application/javascript;所以我们可以通过这个类型,在返回的时候去处理这个 js 中的第三方路劲问题。
6. 修改第三方模块的路径
把
"/", "./", or "../"
开头的引用,全部替换成“/@modules/”
。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 } // 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' /** * 这里进行分组的全局匹配 * 第一个分组匹配以下内容: * from 匹配 from * \s+ 匹配空格 * ['"]匹配单引号或者是双引号 * 第二个分组匹配以下内容: * ?! 不匹配这个分组的结果 * \.\/ 匹配 ./ * $1表示第一个分组的结果 */ ctx.body = contents .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/') } })
7. 加载第三方模块
现在我们要做的是,将 /@modules/ 开头的引用,去 node_modules 中找到并且替换它的返回内容。我们需要在创建一个中间件,这个中间件需要在创建静态服务器之前被调用
const path = require('path') // 3.加载第三方模块 app.use(async (ctx, next) => { // ctx.path --> /@modules/vue // 判断第三方模块的路径是否有/@modules/开头 if (ctx.path.startsWith('/@modules/')) { // 对字符串进行截取,获取到模块名称 const moduleName = ctx.path.substr(10) // 找到该模块名称在node_moduls中的package.json路径 const pkgPath = path.join( process.cwd(), 'node_modules', moduleName, 'package.json' ) // 通过require加载当前package.json const pkg = require(pkgPath) // 将内容替换成node_modules中的内容 ctx.path = path.join('/node_modules', moduleName, pkg.module) } // 返回执行下一个中间件 await next() })
编写完后,我们需要重新启动一下服务器,启动完成后,我们重新打开 network 网络面板,看看 vue 这个模块是否被加载了进来。
我们看到,vue 这个模块已经被加载进来了。但是,我们发现 /@modules/@vue/runtime-dom 和 /@modules/@vue/shared 却没有被加载进来,并且控制台却报了两个错误:加载模块 App.vue 和 index.css 失败
8. 编译单文件组件
原本的 vite 启动后,查看 单文件夹组件的请求是如何处理的
我们来看 app.vue 的响应内容,它引入了一些组件,然后把它编译成一个选项对象,然后它又去加载了 app.vue 并且在后面加上了一个参数 type=template,并且解构出了一个 render 函数,然后把 render 函数挂载到选项对象上,然后又设置了两个属性(这两个属性不模拟),最后导出这个选项对象。从这段代码我们可以观察到,当请求到单文件组件的时候,服务器会来编译这个单文件组件,并把相对应的结果返回给浏览器。
我们在来编写一个中间件,在编写中间件的时候,我们需要安装一个模块 @vue/compiler-sfc 并且导入,这个模块的作用主要是编译单文件组件的。
// 4. 处理单文件组件 const { Readable } = require('stream') const compilerSFC = require('@vue/compiler-sfc') // 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() })
然后,重启一下服务,需要注意的是,需要把图片和其他和 js 或者 vue 无关的文件都注释掉,因为我们这里只处理了 vue 文件。
9. 完成版代码
#!/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')
- RFC (Request For Coments)