Vue.js 3.0 Composition APIs 及 3.0 原理分析
Vue.js 3.0 介绍
一、Vue.js 源码组织形式
1. 源码采纳TypeScript重写
进步了代码的可维护性。大型项目的开发都举荐应用类型化的语言,在编码的过程中查看类型的问题。
2. 应用Monorepo治理我的项目构造
应用一个项目管理多个包,把不同性能的代码放到不同的package中治理,每个功能模块都能够独自公布,独自测试,独自应用。
3. 不同构建版本
Vue3中不再构建UMD模块化的形式,因为UMD会让代码有更多的冗余,它要反对多种模块化的形式。Vue3中将CJS、ESModule和自执行函数的形式别离打包到了不同的文件中。在packages/vue中有Vue3的不同构建版本。
cjs(两个版本都是完整版,蕴含编译器)
- vue.cjs.js
- vue.cjs.prod.js(开发版,代码进行了压缩)
global(这四个版本都能够在浏览器中间接通过scripts标签导入,导入之后会减少一个全局的Vue对象)
- vue.global.js(完整版,蕴含编译器和运行时)
- vue.global.prod.js(完整版,蕴含编译器和运行时,这是开发版本,代码进行了压缩)
- vue.runtime.global.js
- vue.runtime.global.prod.js
browser(四个版本都蕴含esm,浏览器的原生模块化形式,能够间接通过
<script type="module" />
的形式来导入模块)- vue.esm-browser.js
- vue.esm-browser.prod.js
- vue.runtime.esm-browser.js
- vue.runtime.esm-browser.prod.js
bundler(这两个版本没有打包所有的代码,只会打包应用的代码,须要配合打包工具来应用,会让Vue体积更小)
- vue.esm-bundler.js
- bue.runtime.esm-bundler.js
二、Composition API
RFC (Request For Coments)
- Https://github.com/vuejs/rfcs
Composition API RFC
- Https://composition-api.vuejs.org
1. 设计动机
Options API
- 蕴含一个形容组件选项(data、methods、props等)的对象
- Options API 开发简单组件,同一个性能逻辑的代码被拆分到不同选项
Composition API
- Vue.js 3.0 新增的一组API
- 一组基于函数的API
- 能够更灵便的组织组件的逻辑
三、性能晋升
1. 响应式系统升级
Vue3应用Proxy对象重写了响应式零碎。
- Vue.js 2.x中响应式零碎的外围
defineProperty
,初始化的时候递归遍历所有的属性,转化为getter、setter 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等都是按需引入的。
四、Vite
Vue的打包工具。Vite是法语中的"快"的意思
1. ES Module
- 古代浏览器都反对ES Module(IE不反对)
- 通过上面的形式加载模块
<script type="module" src="..."></script>
- 反对模块的script默认提早加载
有了type="module"的模块是提早加载的,相似于script标签设置defer
- 在文档解析实现后,也就是DOM树生成之后,触发DOMContentLoaded事件前执行
2. Vite as Vue-CLI
- Vite 在开发模式下不须要打包能够间接运行
- Vue-CLI开发模式下必须对我的项目打包才能够运行
Vite在生产环境下应用Rollup打包
- 基于ES Module的形式打包
- Vue-CLI应用Webpack打包
3. Vite特点
- 疾速冷启动
- 按需编译
- 模块热更新
4. Vite创立我的项目
Vite创立我的项目
npm init vite-app <project-name>cd <project-name>npm installnpm run dev
基于模板创立我的项目
npm init vite-app --template reactnpm init vite-app --template preact
Compositon API
一、Composition API应用
1. 应用Vue3.0
先创立一个空文件夹,而后进入文件夹执行npm init -y
,再执行npm install vue@3.0.0-rc.1
装置vue3.0
创立index.html,vue3.0的应用
<body> <div id="app"> x: {{ position.x }} <br> y: {{ position.y }} <br> </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>
2. setup、reactive的应用
- createAPP:创立Vue对象
- setup:CompositionAPI的入口
- reactive:创立响应式对象
<body> <div id="app"> x: {{ position.x }} <br> y: {{ position.y }} <br> </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 } }, mounted () { this.position.x = 2 } }) console.log(app) app.mount('#app') </script></body>
二、 setup中的生命周期钩子函数
只须要在vue钩子函数首字母大写,并且钩子函数后面加上on,就能够了。非凡:本来的生命周期中的destroy对应的是onUnmounted。
<body> <div id="app"> x: {{ position.x }} <br> y: {{ position.y }} <br> </div> <script type="module"> import { createApp, reactive, onMounted, onUnmounted } 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 } const app = createApp({ setup () { const position = useMousePosition() return { position } }, mounted () { this.position.x = 2 } }) console.log(app) app.mount('#app') </script></body>
三、reactive-toRefs-ref
reactive创立的响应式数据解构后不再是响应式,toRefs能够把响应式对象的所有属性也转化成响应式的,所以能够解构toRefs返回的对象,解构之后还是响应式数据。
reactive是将一般对象转化成响应式对象,而ref是将根本类型数据包装成了响应式对象。
ref的应用:
<body> <div id="app"> <button @click="increase">Button</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++ } } } createApp({ setup () { return { ...useCount() } } }).mount('#app') </script></body>
四、Computed
computed能够创立一个响应式数据,这个响应式数据依赖于其余响应式数据,就是计算属性。
第一种用法
- computed(() => count.value + 1)
第二种用法
const count = ref(1)const plusOne = computed({ get: () => count.value + 1, set: val => { count.value = val - 1 }})
应用:
<body> <div id="app"> <button @click="push">Button</button> <span>未实现:{{activeCount}}</span> </div> <script type="module"> import { createApp, reactive, computed } from './node_modules/vue/dist/vue.esm-browser.js' const data = [ { text: '看书', complated: false }, { text: '敲代码', complated: false }, { text: '约会', complated: true }, ] createApp({ setup () { const todos = reactive(data) const activeCount = computed(() => { return todos.filter(item => !item.complated).length }) return { activeCount, push: () => { todos.push({ text: '散会', complated: false }) } } } }).mount('#app') </script></body>
五、watch
1. watch的三个参数
- 第一个参数:要监听的数据,得是reactive或ref返回的对象
- 第二个参数:监听到数据变动后执行的函数,这个函数有两个参数别离是新值和旧值
- 第三个参数:选项对象,deep和immediate
2. watch的返回值
- 勾销监听的函数
应用:
<body> <div id="app"> 请抉择一个yes/no的问题: <input v-model.lazy="question"> <p>{{answer}}</p> </div> <script type="module"> 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>
六、WatchEffect
- 是watch函数的简化版本,也用来监督数据的变动
- 承受一个函数作为参数,监听函数内响应式数据的变动
<body> <div id="app"> <button @click="increase">increase</button> <button @click="stop">stop</button> <p>{{count}}</p> </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>
六、实现todolist案例
1. ToDoList性能列表
- 增加待办事项
- 删除待办事项
- 编辑待办事项
- 切换待办事项
- 存储待办事项
2. 我的项目构造
应用vue脚手架创立Vue我的项目,先降级vue-cli,4.5.6版本的vue-cli创立我的项目时能够抉择vue版本。
Vue CLI 的包名称由 vue-cli
改成了 @vue/cli
。 如果曾经全局装置了旧版本的 vue-cli
(1.x 或 2.x),你须要先通过 npm uninstall vue-cli -g
或 yarn global remove vue-cli
卸载它
能够应用下列任一命令装置这个新的包:
npm install -g @vue/cli# ORyarn global add @vue/cli
先应用vue create创立我的项目,创立的时候抉择3.0。
vue create 04-todolist
而后抉择Default (Vue 3 Preview) ([Vue 3] babel, eslint)
就会主动创立我的项目了。
3. 增加待办事项
在输入框输出文本按下enter键提交待办事项
// 1. 增加待办事项const useAdd = todos => { const input = ref('') const addTodo = () => { const text = input.value?.trim() if (text.length === 0) return todos.value.unshift({ text, completed: false }) input.value = '' } return { input, addTodo }}
4. 删除待办事项
点击待办事项右侧的叉号能够删除待办事项
// 2. 删除待办事项const useRemove = todos => { const remove = todo => { const index = todos.value.indexOf(todo) todos.value.splice(index, 1) } return { remove }}
5. 编辑待办事项
双击进入编辑状态,按esc退出编辑,按enter提交编辑,如果删光了文本,则为删除这一项。
- 双击待办事项,展现编辑文本框
- 按回车或者编辑文本框失去焦点,批改数据
- 按esc勾销编辑
- 把编辑文本框清空按回车,删除这一项
- 显示编辑文本框的时候获取焦点
// 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() if (todo.text === '') remove(todo) editingTodo.value = null } const cancelEdit = todo => { editingTodo.value = null todo.text = beforeEditingText } return { editingTodo, editTodo, doneEdit, cancelEdit }}
模板中:
<ul class="todo-list"> <li v-for="todo in todos" :key="todo" :class="{editing: todo === editingTodo}"> <div class="view"> <input type="checkbox" class="toggle"> <label @dblclick="editTodo(todo)">{{ todo.text }}</label> <button class="destroy" @click="remove(todo)"></button> </div> <input type="text" class="edit" v-model="todo.text" @keyup.enter="doneEdit(todo)" @blur="doneEdit(todo)" @keyup.esc="cancelEdit(todo)"> </li></ul>
6. 编辑文本框获取焦点 - vue3.0自定义指令
传对象模式:
Vue 2.x
Vue.directive('editingFocus', { bind(el, binding, vnode, prevVnode) {}, inserted() {}, update() {} // remove, componentUpdated() {}, unbind() {}})
Vue 3.0
app.directive('editingFocus', { beforeMount(el, binding, vnode, prevVnode) {}, mounted() {}, breforeUpdate() {}, // new updated() {}, beforeUnmount() {}, // new unmounted() {}})
传函数模式:
Vue 2.x
Vue.directive('editingFocus', (el, binding) => { binding.value && el.focus()})
Vue 3.0
app.directive('editingFocus', (el, binding) => { binding.value && el.focus()})
代码实现自定义事件获取正在编辑的文本框焦点:
export default { name: 'App', // 省略了setup() {}, directives: { editingFocus: (el, binding) => { // binding能够获取到一些参数 binding.value && el.focus() } }}
应用:
<input v-editing-focus="todo === editingTodo" type="text" class="edit" v-model="todo.text" @keyup.enter="doneEdit(todo)" @blur="doneEdit(todo)" @keyup.esc="cancelEdit(todo)">
. 切换待办事项
- 点击CheckBox能够扭转所有代办项状态
- All/Active/Completed
其余
- 显示未实现代办项个数
- 移除所有实现的我的项目
- 如果没有待办项,暗藏main和footer
// 4. 切换待办项实现状态const useFilter = todos => { const allDone = computed({ get: () => { return !todos.value.filter(item => !item.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, filteredTodos, remainingCount, count }}
8. 本地存储
import useLocalStorage from './utils/useLocalStorage'const storage = useLocalStorage()// 5. 存储待办事项const useStorage = () => { const KEY = 'TODOKEYS' const todos = ref(storage.getItem(KEY) || []) watchEffect(() => { storage.setItem(KEY, todos.value) }) return todos}
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 }}
9. 残缺代码
代码地址:https://gitee.com/jiailing/la...
<template><section id="app" class="todoapp"> <header class="header"> <h1>todos</h1> <input type="text" 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" type="checkbox" v-model="allDone"> <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 type="checkbox" class="toggle" v-model="todo.completed"> <label @dblclick="editTodo(todo)">{{ todo.text }}</label> <button class="destroy" @click="remove(todo)"></button> </div> <input v-editing-focus="todo === editingTodo" type="text" class="edit" 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></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?.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() if (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(item => !item.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, filteredTodos, remainingCount, count }}// 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能够获取到一些参数 binding.value && el.focus() } },}</script><style></style>
Vue.js 3.0响应式零碎原理
一、介绍
1. Vue.js响应式回顾
- Proxy对象实现属性监听
- 多层属性嵌套,在拜访属性过程中解决下一级属性
- 默认监听动静增加的属性
- 默认监听属性的删除操作
- 默认监听数组索引和length属性
- 能够作为独自的模块应用
2. 外围函数
- eactive/ref/toRefs/computed
- effect
- track
- trigger
二、Proxy对象回顾
1. 在严格模式下,Proxy的函数得返回布尔类型的值,否则会报TypeError
Uncaught TypeError: 'set' on proxy: trap returned falsish for property 'foo'
'use strict'// 问题1: set和deleteProperty中须要返回布尔类型的值// 严格模式下,如果返回false的话,会呈现TypeError的异样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) // 这里得写return }, deleteProperty(target, key) { // delete target[key] return Reflect.deleteProperty(target, key) // 这里得写return }})proxy.foo = 'zzz'
2. Proxy和Reflect中应用receiver
Proxy中receiver:Proxy或者继承Proxy的对象
React中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) // 执行this.bar的时候,this指向代理对象,也就是获取target.bar }})console.log(proxy.foo) // value - bar
如果return Reflect.get(target, key, receiver)
写成return Reflect.get(target, key)
的话,则响应式属性foo外面的this还是指向本来的对象obj,this.bar就是undefined,而传入了receiver之后,响应式属性里的this就指向新的响应式对象proxy,this.bar返回value - bar
。
三、reactive
- 承受一个参数,判断这个参数是否是对象
- 创立拦截器对象handler,设置get/set/deleteProperty
- 返回Proxy对象
本人实现reactive
function isObject(value) { return value !== null && typeof value === 'object'}function convert(target) { return isObject(target) ? reactive(target) : target}const hasOwnProperty = Object.prototype.hasOwnPropertyfunction hasOwn(target, key) { return hasOwnProperty.call(target, key)}export function reactive(target) { if (!isObject(target)) return target const handler = { get (target, key, receiver) { // 收集依赖 // track(target, key) // 稍后解正文 console.log('get', key) const ret = Reflect.get(target, key, receiver) return convert(ret) }, set (target, key, value, receiver) { const oldValue = Reflect.get(target, key, receiver) let ret = true if (oldValue !== value) { ret = Reflect.set(target, key, value, receiver) // 触发更新 // trigger(target, key) // 稍后解正文 console.log('set', key, value) } return ret }, deleteProperty (target, key) { const hasKey = hasOwn(target, key) const ret = Reflect.deleteProperty(target, key) if (hasKey && ret) { // 触发更新 // trigger(target, key) // 稍后解正文 console.log('detele', key) } return ret } } return new Proxy(target, handler)}
应用:
<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>
输入后果为:
set name lisi
index.js:39 detele age
index.html:17 Proxy {name: "lisi"}
四、收集依赖
五、effect、track
let activeEffect = nullexport 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)}
六、trigger
export function trigger(target, key) { // 触发依赖 const depsMap = targetMap.get(target) if(!depsMap)return const dept = depsMap.get(key) if(dept) { dept.forEach(effect => { effect() }) }}
应用:
<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) // 15000 product.price = 4000 console.log(total) // 12000 product.count = 1 console.log(total) // 4000 </script></body>
七、ref
eactive vs ref
- ref能够把根本数据类型数据转换成响应式对象
- ref返回的对象,从新赋值成对象也是响应式的
- reactive返回的对象,从新赋值失落响应式
- reactive返回的对象不可解构
reactive
const product = reactive({ name: 'iPhone', price: 5000, count: 3})
ref
const price = ref(5000)const count = ref(3)
实现ref:
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}
应用:
<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) // 15000 price.value = 4000 console.log(total) // 12000 count.value = 1 console.log(total) // 4000 </script></body>
八、toRefs
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}
应用
<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) // 间接返回解构的product不是响应式对象,所以调用toRefs将reactive对象的每个属性都转化成ref对象 } const { price, count } = useProduct() let total = 0 effect(() => { total = price.value * count.value }) console.log(total) // 15000 price.value = 4000 console.log(total) // 12000 count.value = 1 console.log(total) // 4000 </script></body>
九、computed
export function computed(getter) { const result = ref() effect(() => (result.value = getter())) return result}
应用
<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) // 15000 product.price = 4000 console.log(total.value) // 12000 product.count = 1 console.log(total.value) // 4000 </script></body>
备注:trigger/track/effct是低层的函数,个别不必。应用computed代替effect的应用
Vite实现原理
一、Vite介绍
1. Vite概念:
- Vite是一个面向古代浏览器的一个更轻更快的web利用开发工具
- 它基于ECMAScript规范原生模块零碎(ES Modules)实现
2. Vite我的项目依赖:
- Vite
- @vue/compiler-sfc
3. 根底应用:
vite serve / vite build
在运行vite serve
的时候不须要打包,间接开启一个web服务器,当浏览器申请服务器,比方申请一个单文件组件,这个时候在服务器端编译单文件组件,而后把编译的后果返回给浏览器,留神这里的编译是在服务器端,另外模块的解决是在申请到服务器端解决的。
而vue-cli-service serve
:
当运行vue-cli-service serve的时候,它外部会应用webpack,首先去打包所有的模块,如果模块数量比拟多的话,打包速度会十分的慢,把打包的后果存储到内存中,而后才会开启开发的web服务器,浏览器申请web服务器,把内存中打包的后果间接返回给浏览器,像webpack这种工具,它的做法是将所有的模块提前编译打包进bundle里,也就是不论模块是否被执行,是否应用到,都要被编译和打包到bundle。随着我的项目越来越大,打包后的bundle也越来越大,打包的速度天然也就越来越慢。
Vite利用古代浏览器原生反对的ESModule这个模块化的个性省略了对模块的打包,对于须要编译的文件,比方单文件组件、款式模块等,vite采纳的另一种模式即时编译,也就是说只有具体去申请某个文件的时候,才会在服务端编译这个文件,所以这种即时编译的益处次要体现在按需编译,速度会更快。
4. HMR:
Vite HMR
- 立刻变异以后所批改的文件
Webpack HMR
- 会主动以这个文件位入口从新build一次,所有的波及到的依赖也会被从新加载一次,所以反应速度会慢一些
5. Build:
Vite build
- Rollup
Dynamic import
- polyfill
6. 打包OR不打包:
应用Webpack打包的两个起因:
- 浏览器环境并不反对模块化(而当初大部分浏览器都反对ESM模块化了)
- 零散的模块文件会产生大量的HTTP申请(HTTP2能够长连贯)
7. 浏览器对ESModule的反对:
8. 开箱即用:
- TypeScript - 内置反对
- less/sass/stylus/postcss - 内置反对(须要独自装置)
- JSX
- Web Assembly
9. Vite个性
- 疾速冷启动
- 模块热更新
- 按需编译
- 开箱即用
二、动态Web服务器
1. Vite外围性能
- 动态web服务器
- 编译单文件组件:拦挡浏览器不辨认的模块,并解决
- HMR
三、批改第三方模块的门路
创立两个中间件,一个中间件是把加载第三方模块中的import中的门路扭转,改成加载@modules/模块文件名
,另一个中间件是当申请过去之后,判断申请门路中是否有@modules/模块名称
,如果有的话,去node_modules加载对应的模块
四、加载第三方模块
当申请过去之后,判断申请门路中是否以@modules
结尾,如果是的话,去node_modules加载对应的模块
五、编译单文件组件
发送两次申请,第一次申请是把单文件组件编译成一个对象,第二次申请是编译单文件组件的模板,返回一个render函数,并且把render函数挂载到对象的render办法上。
最终代码
#!/usr/bin/env nodeconst 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. 加载第三方模块。判断申请门路中是否以`@modules`结尾,如果是的话,去node_modules加载对应的模块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) // 返回一个对象,成员descriptor、errors let code if (!ctx.query.type) { // 第一次申请,把单文件组件编译成一个对象 code = descriptor.script.content // console.log('code', code) code = code.replace(/export\s+default\s+/g, 'const __script = ') code += `import { render as __render } from "${ctx.path}?type=template"__script.render = __renderexport 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/') // 分组匹配,第一个分组中,from原样匹配form,\s+匹配一至多个空格,['"]匹配单引号或双引号。第二个分组中,?!标识不匹配这个分组的后果,也就是排除点结尾或者\结尾的状况 .replace(/process\.env\.NODE_ENV/g, '"development"')// 替换process对象 }})app.listen(4000)console.log('Server running @ http://localhost:4000')
应用时先将cli我的项目link到全局,npm link
而后在vue3我的项目中执行my-vite-cli
运行我的项目。vue3中的图片和款式模块导入代码正文掉了。