共计 22700 个字符,预计需要花费 57 分钟才能阅读完成。
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 install npm run dev
-
基于模板创立我的项目
npm init vite-app --template react npm 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
# OR
yarn 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.hasOwnProperty
function 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 = 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)
}
六、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 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. 加载第三方模块。判断申请门路中是否以 `@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 = __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/') // 分组匹配,第一个分组中,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 中的图片和款式模块导入代码正文掉了。