一、vue3原理
1. vue响应式原理比较
| 实现原理 | defineProperty | Proxy | value setter |
|---|---|---|---|
| 实际场景 | vue2 响应式 | vue3 reactive | vue3 ref |
| 优势 | 兼容性 | 基于Proxy实现真正的拦截 | 实现简单 |
| 劣势 | 数组和属性删除等拦截不了 | 兼容不了IE11 | 只拦截value属性 |
| 实际应用 | vue2 | vue3 复杂数据 | vue3 简单数据 |
2. Vue3的挂载和更新流程
挂载流程
- 创建应用实例:获取渲染器renderer,renderer是含createApp方法的对象,调用renderer创建实例
- 创建根节点的vnode:传入的有template模板时,会执行compile编译模板为render函数
- 执行render函数,将生成的vnode传递给path函数,转换为dom
- 追加到宿主元素
更新流程
- 更新机制建立
- 数据更新,set方法调用trigger函数
- 将副作用函数添加到更新队列queueJob里
- promise异步执行更新队列queueFlush
- 循环执行相关副作用函数
3. vue 响应式设计思想
使用观察者模式
- 订阅者(观察者) -- Watcher
- update(): 当事件发生时,具体要做的事情
- 发布者(目标) -- Dep
- subs数组: 存储所有观察者
- addSub(): 添加观察者
- notify(): 当事件发生,调用所有观察者的update()方法
// 发布者-目标
class Dep {
constructor() {
this.subs = []
}
// 添加订阅者
addSub(sub) {
if(sub && sub.update) {
this.subs.push(sub)
}
}
// 发布通知
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 订阅者-观察者
class Watcher {
update() {
console.log('update')
}
}
响应式整体流程
4. Mini-Vue的实现
1. 视图初始化
- 应用程序实例创建
- createApp()返回App实例对象,实现一个mount()
- 挂载
- 实例的mount方法传入的组件渲染结果追加到宿主元素
export function createApp(rootComponent) {
// 接受根组件,返回App实例
return {
mount(selector) {
// 1.获取宿主
const container = document.querySelector(selector)
// 2. 渲染视图
const el = rootComponent.render.call(rootComponent.data())
// 3. 追加到宿主
container.appendChild(el)
}
}
}
2. 渲染器实现
将传入的组件转换为Dom
- 渲染器工厂
- createRenderer
- 所属runtime-core
- 渲染器实例
- render
- createApp
// runtime-dom
let renderer
// dom平台特有的节点操作
const rendererOptions = {
querySelector(selector) {
document.querySelector(selector)
},
insert(child, parent, anchor) {
// anchor设为null, 则为appendChild
parent.insertBefore(child, anchor || null)
},
setElementText(el, text) {
el.textContent = text
}
}
// 确保renderer单例
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions))
}
export function createApp(rootComponent) {
return ensureRenderer().createApp(rootComponent)
}
// runtime-core
// ------------- custom renderer api -------------
export function createRenderer(options) {
// 渲染组件内容
const render = () => {
// 1.获取宿主
const container = options.querySelector(selector)
// 2. 渲染视图
const el = rootComponent.render.call(rootComponent.data())
// 3. 追加到宿主
options.insert(el, container)
}
// 返回渲染器实例
return {
render,
createApp: createAppAPI(options)
}
}
export function createAppAPI(render) {
return function createApp(rootComponent) {
const app = {
mount(selector) {
render(rootComponent)
}
}
return app
}
}
3. 数据响应式实现
reactive
// reactive.js
let activeEffect
export function effect(fn) {
activeEffect = fn
}
export function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
const vlaue = Reflect.get(target, key)
// 依赖跟踪
track(target, key)
return value
},
set(target, key, value) {
const result = Reflect.set(target, key, value)
// 通知更新
trigger(target, key)
return result
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
// 通知更新
trigger(target, key)
return result
}
})
}
const targetMap = new WeakMap()
function track(target, key) {
if(activeEffect) {
let depsMap = targetMap.get(target)
// 首次depsMap不存在
if(!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key)
// 首次depsMap不存在
if(!deps) {
depsMap.set(key, (deps = new Set()))
}
// 添加激活的副作用
deps.add(activeEffect)
}
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if(depsMap) {
let deps = depsMap.get(key)
if(deps) {
deps.forEach(dep => dep())
}
}
}
import { reactive } from './reactive'
...
export function createRenderer(options) {
...
const render = () => {
// 1. 获取宿主
const container = options.querySelector(selector)
// 2. 渲染视图
const observed = reactive(rootComponent.data())
// 3. 组件更新函数
const componentUpdateFn = () => {
const el = rootComponent.render.call(observed)
options.setElementText(el, '')
// 3. 追加到宿主
options.insert(el, container)
}
effect(componentUpdateFn)
// 初始化
componentUpdateFn()
if(rootComponent.mounted) {
rootComponent.mounted.call(observed)
}
}
...
}
...
ref
class RefImpl {
dep = undefined
construct(value, _shallow) {
this._rawValue = _shallow ? value : toRaw(value)
this._value = _shallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
if(hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
二、Vite原理
1. Vite相比之前的打包工具解决了哪些问题?
- 缓慢的服务器启动
- 缓慢的更新
如何解决缓慢的服务器启动?
Vite 通过在一开始将应用中的模块区分为 依赖 和 源码 两类,改进了开发服务器启动时间
- 依赖 Vite 使用 Go 编写的esbuild ,比以 JavaScript 编写的打包器预构建依赖快 10-100 倍。未来会使用rust编写的Rolldown。
- 源码 Vite 以 原生 ESM方式提供源码。Vite 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。
如何解决缓慢的更新?
-
在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失活(大多数时候只是模块本身),使得无论应用大小如何,HMR 始终能保持快速更新。
-
Vite 同时利用 HTTP 缓存:源码模块的请求会根据
304 Not Modified进行协商缓存,而依赖模块请求则会通过Cache-Control: max-age=31536000,immutable进行强缓存,因此一旦被缓存它们将不需要再次请求。
2. 原生 ESM方式提供源码,需要解决哪些问题
- 基本功能支持html和js
- 第三方库的支持
- Vue单文件组件的支持
- css文件的支持
3. 原生 ESM方式的原理
实现步骤
- 基于koa的node服务
- 基本功能支持html和js
- 第三方库的支持
- Vue单文件组件的支持
- css文件的支持
源码实现
基本功能支持html和js
- 改写from 'xxx'
- 读取第三方库package.json的module属性,获取es模块入口
- 通过es模块入口,读取第三方库的文件
#!/usr/bin/env node
const Koa = require('koa')
const fs = require('fs')
const path = require('path')
const app = new Koa()
app.use(async (ctx) => {
const { url, query } = ctx.request
// '/' => index.html
if (url === '/') {
ctx.type = 'text/html'
const content = fs.readFileSync('./index.html', 'utf-8')
ctx.body = content
}
// *.js => src/*js
else if (url.endsWith('.js')) {
const p = path.resolve(__dirname, url.slice(1))
const content = fs.readFileSync(p, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = content
}
})
app.listen(3000, () => {
console.log('server running at http://localhost:3000')
})
第三方库的支持
- 改写from 'xxx'
- 读取第三方库package.json的module属性,获取es模块入口
- 通过es模块入口,读取第三方库的文件
#!/usr/bin/env node
const Koa = require('koa')
const fs = require('fs')
const path = require('path')
const app = new Koa()
// 改写from 'xxx'
// 'vue' => '/@modules/vue'
function rewriteImport(content) {
return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0, s1) {
if(s1[0] !== '.' && s1[1] !== '/') {
// 不是绝对路径 / 或相对路径 ../
return ` from '/@modules/${s1}'`
} else {
return s0
}
})
}
app.use(async (ctx) => {
const { url, query } = ctx.request
// '/' => index.html
if (url === '/') {
ctx.type = 'text/html'
let content = fs.readFileSync('./index.html', 'utf-8')
content = content.replace('<script', `
<script>
const process = {
env: {
NODE_ENV: 'dev'
}
}
</script>
<script
`)
ctx.body = content
}
// *.js => src/*js
else if (url.endsWith('.js')) {
const p = path.resolve(__dirname, url.slice(1))
const content = fs.readFileSync(p, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(content)
}
// 第三方库的支持
else if(url.startsWith('/@modules')) {
// /@modules/vue => 代码位置/node_modules/vue/ es模块入口
const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
// 读取package.json的module属性
const module = require(prefix + '/package.json').module
const p = path.resolve(prefix, module)
const ret = fs.readFileSync(p, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(ret)
}
})
app.listen(3000, () => {
console.log('server running at http://localhost:3000')
})
Vue单文件组件的支持
- @vue/compiler-sfc编译代码
#!/usr/bin/env node
const Koa = require('koa')
const fs = require('fs')
const path = require('path')
const compilerSfc = require('@vue/compiler-sfc')
const compilerDom = require('@vue/compiler-dom')
const app = new Koa()
// 改写from 'xxx'
// 'vue' => '/@modules/vue'
function rewriteImport(content) {
return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0, s1) {
if(s1[0] !== '.' && s1[1] !== '/') {
// 不是绝对路径 / 或相对路径 ../
return ` from '/@modules/${s1}'`
} else {
return s0
}
})
}
app.use(async (ctx) => {
const { url, query } = ctx.request
// '/' => index.html
if (url === '/') {
ctx.type = 'text/html'
let content = fs.readFileSync('./index.html', 'utf-8')
content = content.replace('<script', `
<script>
const process = {
env: {
NODE_ENV: 'dev'
}
}
</script>
<script
`)
ctx.body = content
}
// *.js => src/*js
else if (url.endsWith('.js')) {
const p = path.resolve(__dirname, url.slice(1))
const content = fs.readFileSync(p, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(content)
}
// 第三方库的支持
else if(url.startsWith('/@modules')) {
// /@modules/vue => 代码位置/node_modules/vue/ es模块入口
const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
// 读取package.json的module属性
const module = require(prefix + '/package.json').module
const p = path.resolve(prefix, module)
const ret = fs.readFileSync(p, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(ret)
}
// 支持SFC组件,单文件组件
// *.vue => render 函数
else if (url.indexOf('.vue') > -1) {
const p = path.resolve(__dirname, url.split('?')[0].slice(1))
const ret = compilerSfc.parse(fs.readFileSync(p, 'utf-8'))
console.log(' ~ app.use ~ ret', ret)
}
})
app.listen(3000, () => {
console.log('server running at http://localhost:3000')
})
- *.vue => template script
- template script => render 函数
#!/usr/bin/env node
const Koa = require('koa')
const fs = require('fs')
const path = require('path')
const compilerSfc = require('@vue/compiler-sfc')
const compilerDom = require('@vue/compiler-dom')
const app = new Koa()
// 改写from 'xxx'
// 'vue' => '/@modules/vue'
function rewriteImport(content) {
return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0, s1) {
if(s1[0] !== '.' && s1[1] !== '/') {
// 不是绝对路径 / 或相对路径 ../
return ` from '/@modules/${s1}'`
} else {
return s0
}
})
}
app.use(async (ctx) => {
const { url, query } = ctx.request
// '/' => index.html
if (url === '/') {
ctx.type = 'text/html'
let content = fs.readFileSync('./index.html', 'utf-8')
content = content.replace('<script', `
<script>
const process = {
env: {
NODE_ENV: 'dev'
}
}
</script>
<script
`)
ctx.body = content
}
// *.js => src/*js
else if (url.endsWith('.js')) {
const p = path.resolve(__dirname, url.slice(1))
const content = fs.readFileSync(p, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(content)
}
// 第三方库的支持
else if(url.startsWith('/@modules')) {
// /@modules/vue => 代码位置/node_modules/vue/ es模块入口
const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
// 读取package.json的module属性
const module = require(prefix + '/package.json').module
const p = path.resolve(prefix, module)
const ret = fs.readFileSync(p, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(ret)
}
// 支持SFC组件,单文件组件
// *.vue => render 函数
else if (url.indexOf('.vue') > -1) {
const p = path.resolve(__dirname, url.split('?')[0].slice(1))
const { descriptor} = compilerSfc.parse(fs.readFileSync(p, 'utf-8'))
// 第一步 *.vue => template script
if (!query.type) {
ctx.type = 'application/javascript'
ctx.body = `
${rewriteImport(
descriptor.script.content.replace("export default ", "const __script = ")
)}
import { render as __render } from "${url}?type=template"
__script.render = __render
export default __script
`
} else {
// 第二步 template script => render 函数
const template = descriptor.template
const render = compilerDom.compile(template.content, {
mode: 'module'
})
ctx.type = 'application/javascript'
ctx.body = rewriteImport(render.code)
}
}
})
app.listen(3000, () => {
console.log('server running at http://localhost:3000')
})
css文件的支持
- css 转化为 js 代码
- 利用 js 添加 style 标签
#!/usr/bin/env node
const Koa = require('koa')
const fs = require('fs')
const path = require('path')
const compilerSfc = require('@vue/compiler-sfc')
const compilerDom = require('@vue/compiler-dom')
const app = new Koa()
// 改写from 'xxx'
// 'vue' => '/@modules/vue'
function rewriteImport(content) {
return content.replace(/ from ['|"]([^'"]+)['|"]/g, function(s0, s1) {
if(s1[0] !== '.' && s1[1] !== '/') {
// 不是绝对路径 / 或相对路径 ../
return ` from '/@modules/${s1}'`
} else {
return s0
}
})
}
app.use(async (ctx) => {
const { url, query } = ctx.request
// '/' => index.html
if (url === '/') {
ctx.type = 'text/html'
let content = fs.readFileSync('./index.html', 'utf-8')
content = content.replace('<script', `
<script>
const process = {
env: {
NODE_ENV: 'dev'
}
}
</script>
<script
`)
ctx.body = content
}
// *.js => src/*js
else if (url.endsWith('.js')) {
const p = path.resolve(__dirname, url.slice(1))
const content = fs.readFileSync(p, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(content)
}
// 第三方库的支持
else if(url.startsWith('/@modules')) {
// /@modules/vue => 代码位置/node_modules/vue/ es模块入口
const prefix = path.resolve(__dirname, 'node_modules', url.replace('/@modules/', ''))
// 读取package.json的module属性
const module = require(prefix + '/package.json').module
const p = path.resolve(prefix, module)
const ret = fs.readFileSync(p, 'utf-8')
ctx.type = 'application/javascript'
ctx.body = rewriteImport(ret)
}
// 支持SFC组件,单文件组件
// *.vue => render 函数
else if (url.indexOf('.vue') > -1) {
// 第一步 *.vue => template script
const p = path.resolve(__dirname, url.split('?')[0].slice(1))
const { descriptor } = compilerSfc.parse(fs.readFileSync(p, 'utf-8'))
// 第二步 template script => render 函数
if (!query.type) {
if (descriptor.script) {
ctx.type = 'application/javascript'
ctx.body = `
${rewriteImport(
descriptor.script.content.replace("export default ", "const __script = ")
)}
import { render as __render } from "${url}?type=template"
__script.render = __render
export default __script
`
}
} else {
const template = descriptor.template
const render = compilerDom.compile(template.content, {
mode: 'module'
})
ctx.type = 'application/javascript'
ctx.body = rewriteImport(render.code)
}
}
// css 文件
else if (url.endsWith('.css')) {
const p = path.resolve(__dirname, url.slice(1))
const file = fs.readFileSync(p, 'utf-8')
// css 转化为 js 代码
// 利用 js 添加 style 标签
const content = `
const css = "${file.replace(/\n/g, '')}"
let link = document.createElement("style")
link.setAttribute("type", "text/css")
document.head.appendChild(link)
link.innerHTML = css
export default css
`
ctx.type = 'application/javascript'
ctx.body = content
}
})
app.listen(3000, () => {
console.log('server running at http://localhost:3000')
})