vue3和vite原理

101 阅读7分钟

一、vue3原理

1. vue响应式原理比较

实现原理definePropertyProxyvalue setter
实际场景vue2 响应式vue3 reactivevue3 ref
优势兼容性基于Proxy实现真正的拦截实现简单
劣势数组和属性删除等拦截不了兼容不了IE11只拦截value属性
实际应用vue2vue3 复杂数据vue3 简单数据

2. Vue3的挂载和更新流程

挂载流程

  • 创建应用实例:获取渲染器renderer,renderer是含createApp方法的对象,调用renderer创建实例
  • 创建根节点的vnode:传入的有template模板时,会执行compile编译模板为render函数
  • 执行render函数,将生成的vnode传递给path函数,转换为dom
  • 追加到宿主元素

更新流程

  • 更新机制建立
  • 数据更新,set方法调用trigger函数
  • 将副作用函数添加到更新队列queueJob里
  • promise异步执行更新队列queueFlush
  • 循环执行相关副作用函数

3. vue 响应式设计思想

使用观察者模式

vue3_01.png

  • 订阅者(观察者) -- 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')
    }
}

响应式整体流程

vue3_02.png

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 只需要在浏览器请求源码时进行转换并按需提供源码。根据情景动态导入代码,即只在当前屏幕上实际使用时才会被处理。

vite01.png

vite02.png

如何解决缓慢的更新?

  • 在 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')
})

vie03.png

  • *.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')
})

vite04.png

vite05.png

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')
})