拉勾前端高薪就业课程笔记第九弹(模块3-5)

本模块主要介绍Vue3.0的最新特性,并介绍了Composition API相关的方法,分析了Vue3.0中响应式的原理,最后介绍了Vite的实现原理。

Vue3.0的变化

源码和组织方式的变化
Vue3.0使用TypeScript重写了源码,并且使用Monorepo的包管理方式来管理源码,使得每个模块都能单独引入。

Composition API
Vue3.0提供了Composition API,来解决业务功能分散,以及逻辑重用等问题。在Vue2.x中,组件的功能实现一般被分布到不同的对象中,data、computed、methods。而Vue3.0中通过使用Composition API将功能封装在一个函数中,函数返回一个包含一些属性和方法的对象,提供给组件来实现功能。

性能提升
Vue3.0的性能提升主要体现在两个方面:响应式系统的升级和打包编译的优化

  1. 响应式系统的升级
    Vue3.0使用Proxy代理对象重写了响应式系统,Proxy本身性能就比defineProperty好,并且还可以监听动态新增的属性,以及数组的索引和长度。Vue2.x在初始化时就会对data中的数据进行递归处理,将其转化成响应式的数据,即使其中有些数据并没有使用到。而Vue3.0中只有访问到该属性才会对下一级属性进行处理。
  2. 打包编译的优化
    Vue3.0对编译过程进行了优化,对静态节点进行标记,diff对比新旧节点差异时将跳过静态节点的对比,只比较动态节点,新增了Fragment片段,可以允许组件的根节点存在多个同级节点,避免添加额外的节点包裹。静态提升将静态节点的创建提取到render函数外,只有初始化的时候会被创建一次,避免render执行时重复创建静态节点。缓存事件处理函数,避免了不必要的更新。
    Vue3.0还移除了一些不常用的API,如inline-template、filter等来减小打包体积。使用Tree-shaking过滤无效代码。模块按需引入,只引入需要使用的模块。

Vite的发布
Vue3.0发布时同时发布Vite,Vite是一款基于ESM的打包工具,具有快速冷启动、按需编译、模块热更新速度快等特点。在开发模式下不需要打包文件,极大的增加了页面打开的速度。

Composition API的使用

  1. createApp():接受一个选项对象,并返回一个Vue实例。选项对象中的data必须是一个函数。

  2. setup(props, context):compositionAPI的入口,在props被解析完毕,组件实例创建之前执行,即beforeCreate和created之间执行。由于Vue实例还未创建,所以没有this对象。props是外部传入的参数,是一个响应式的对象,不能被解构。context是运行上下文对象,包括attrs、emit、slots等属性。setup返回一个对象,可以在组件内部使用。类似于data中的数据。
    setup中使用生命周期钩子函数,只需要引入on+生命周期函数名即可使用

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <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, reactive, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js';
    
        const 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.addEventListener('mousemove', update)
          })
    
          return position
        }
    
        const app = createApp({
          setup(props, context) {
            // props:接收外部传入数据,是个响应式的对象,不能被解构
            // context: attrs、emit、slots
            const position = useMousePosition()
            return {
              position
            }
          },
          mounted() {
            this.position.x = 100
          }
        });
        app.mount('#app');
      </script>
    </body>
    </html>
    
  3. reactive():将对象设置成响应式对象。不使用observe是为了防止和rsts库函数重名

    const position = reactive({
      x: 0,
      y: 0
    })
    
  4. toRefs(proxyObj):proxyObj必须是代理对象,将对象的属性转换成响应式的ref对象。原理就是为对象的每一个属性创建一个具有value属性的对象,value具有setter和getter。普遍用于解构响应式对象的处理。

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        x: {{ x }}
        y: {{ y }}
      </div>
      <script type="module">
        import { createApp, reactive, toRefs, onMounted, onUnmounted } from './node_modules/vue/dist/vue.esm-browser.js';
    
        const 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.addEventListener('mousemove', update)
          })
    
          return toRefs(position)
        }
    
        const app = createApp({
          setup(props, context) {
            // props:接收外部传入数据,是个响应式的对象,不能被解构
            // context: attrs、emit、slots
            const { x, y } = useMousePosition()
            return {
              x,
              y
            }
          }
        });
        app.mount('#app');
      </script>
    </body>
    </html>
    
  5. ref(val):将数据转化为响应式的ref对象,对象具有一个value属性。如果在差值表达式中使用该变量,则可以省略.value

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
    </head>
    <body>
      <div id="app">
        <span>{{ count }}</span>
        <button @click="increase">增加</button>
      </div>
      <script type="module">
        import { createApp, ref } from './node_modules/vue/dist/vue.esm-browser.js';
        const useCount = () => {
          const count = ref(0)
          return {
            count,
            increase() {
              count.value++
            }
          }
        }
        createApp({
          setup() {
            return {
              ...useCount()
            }
          }
        }).mount('#app');
      </script>
    </body>
    </html>
    
  6. computed():计算属性,缓存计算结果。

    1. 接收一个getter函数,当函数的依赖项发生改变时,会重新调用该函数。返回一个不变的响应式ref对象。

      <script type="module">
          import { createApp, reactive, computed } from './node_modules/vue/dist/vue.esm-browser.js';
            const todoList = () => {
              const todos = reactive([
                { text: '吃饭', complete: true},
                { text: '睡觉', complete: false},
                { text: '打豆豆', complete: false},
              ]);
              const unCompletes = computed(() => {
                return todos.filter(item => !item.complete).length
              })
              return {
                unCompletes,
                add: () => {
                  todos.push({text: '找老婆', complete: false})
                }
              }
            }
          createApp({
            setup() {
              return {
                ...todoList()
              }
            }
          }).mount('#app');
        </script>
      
    2. 接收一个具有set和get函数的对象来创建可写的ref对象。

  7. Watch(data, cb(newVal, oldVal), options): 监听数据变化,并执行cb。options为选项对象,包含deep和immediate。返回一个取消监听的函数。

    import { createApp, ref, watch } from './node_modules/vue/dist/vue.esm-browser.js';
    createApp({
      setup() {
        const question = ref('')
        const answer = ref('')
        const img = ref('')
        let timer = 0
        watch(question, (newVal, oldVal) => {
          if (timer) clearTimeout(timer)
          timer = setTimeout(async () => {
            const resopne = await fetch('https://www.yesno.wtf/api')
            const data = await resopne.json()
            answer.value = data.answer
            img.value = data.image
          }, 500)
        })
        return {
          question,
          answer,
          img
        }
      }
    }).mount('#app');
    
  8. watchEffect: 和watch类似,但只接收一个函数,函数会立即执行一次,当函数中依赖的变量发生变化时,会再次执行该函数,返回一个取消watch的函数。

Vue3.0响应式原理

Vue3.0响应式系统的特点

  1. 使用Proxy对象实现属性监听,初始化时不用遍历data属性进行响应式处理
  2. 对于多层属性嵌套,只在访问过程中处理下一级属性
  3. 默认监听动态属性的添加
  4. 默认监听属性的删除
  5. 默认监听数组索引和length属性
  6. 可以作为单独的模块使用

Proxy的两个需要注意的问题

  1. set和deleteProperty方法在严格模式下必须返回boolean类型的值,否则将提示以下错误。esm默认情况下会开启严格模式。

    const obj = {
     a: '12',
      b: 34
    }
    const p = new Proxy(obj, {
      get(target, key, receiver) {
        return Reflect.get(target, key, receiver)
      },
      set(target, key, receiver) {
        return Reflect.set(target, key, receiver)
      },
      deleteProperty(target, key) {
        return Reflect.deleteProperty(target, key)
      }
    })
    

    在这里插入图片描述
    在这里插入图片描述

  2. Reflect.get()方法如果没有传入receiver,原生对象obj的get方法中this将指向原生对象obj

    const obj = {
     get foo() {
        console.log('obj get');
        return this.bar
      }
    }
    const p = new Proxy(obj, {
      get(target, key) {
        console.log('proxy get');
        if (key === 'bar') {
          return 'proxy-bar'
        }
        return Reflect.get(target, key)
      }
    })
    

    在这里插入图片描述
    如果传入了receiver对象,则原生对象obj的get方法中this将指向Proxy代理对象。

    const obj = {
     get foo() {
        console.log('obj get');
        return this.bar
      }
    }
    const p = new Proxy(obj, {
      get(target, key, receiver) {
        console.log('proxy get');
        if (key === 'bar') {
          return 'proxy-bar'
        }
        return Reflect.get(target, key, receiver)
      }
    })
    

    在这里插入图片描述

Vue3.x响应式系统的核心方法实现

  1. reactive:接受一个普通对象,并返回一个Proxy对象。

    const isObj = val => val !== null && typeof val === 'object'
    const convert = target => isObj(target) ? reactive(target) : target
    
    // 将对象转化成响应式的对象
    export function reactive(obj) {
      // 判断是否是对象,如果不是对象,直接返回
      if (!isObj(obj)) return obj
      const handler = {
        get(target, key, receiver) {
          // 收集依赖
          track(target, key)
          const result = Reflect.get(target, key, receiver) 
          // 如果result是一个对象,需要再次调用reactive将对象转化为响应式的对象
          return convert(result)
        },
        set(target, key, value, receiver) {
          const oldVal = Reflect.get(target, key, receiver)
          let result = true
          if (oldVal !== value) {
            result = Reflect.set(target, key, value, receiver)
            // 触发更新
            trigger(target, key)
          }
          return result
        },
        deleteProperty(target, key) {
          const hasKey = hasOwn(traget, key) // 判断对象中是否存在该属性
          const result = Reflect.deleteProperty(target, key) // 是否删除成功
          if (hasKey && result) {
          	// 触发更新
            trigger(target, key)
          }
          return result
        }
      }
      return new Proxy(obj, handler)
    }
    
  2. effect:和watchEffect类似,首先会执行传入的回调函数,然后当函数中的依赖项发生改变时,会再次执行回调函数

    let activeEffect = null
    // 注册依赖事件 
    export function effect(callback) {
      activeEffect = callback
      callback()
      activeEffect = null
    }
    
  3. track:收集依赖,即记录effect中传入的回调函数

    let targetMap = new WeakMap() // 弱引用Map
    // 收集依赖 WeakMap(target: Map(key: Set(activeEffect)))
    function track(target, key) {
      if (!activeEffect) return // 主要是收集activeEffect
      // 首先找对象对应的Map存不存在
      let depsMap = targetMap.get(target) 
      if (!depsMap) { // 不存在向targetMap中设置一个target,并为其创建一个Map
        targetMap.set(target, (depsMap = new Map()))
      }
      let dep = depsMap.get(key) // 获取对象中key对应的activeEffect
      if (!dep) {
        depsMap.set(key, (dep = new Set()))
      }
      dep.add(activeEffect) // 添加对应的依赖
    }
    
  4. trigger:触发更新,执行effect中传入的回调函数

    // 触发更新, 依次找到key对应的activeEffect
    function trigger(target, key) {
      const depsMap = targetMap.get(target)
      if (!depsMap) return
      const dep = depsMap.get(key)
      if (dep) {
        dep.forEach(effect => {
          effect()
        })
      }
    }
    
  5. ref:将数据转化为包含value属性的响应式对象,属性访问时触发依赖收集

    export function ref(raw) {
      // 判断是否是ref对象,如果是直接返回
      if (isObj(raw) && row.__v_isRef) return
      // 如果传入的参数是一个对象,则将对象转化成响应式的对象
      let value = convert(raw)
      const r = {
        __v_isRef: true,
        get value() {
          // 收集依赖
          track(r, 'value')
          return value
        },
        set value(newVal) {
          if (newVal === value) return
          value = convert(newVal)
          trigger(r, 'value')
        }
      }
      return r
    }
    
  6. toRefs:将proxy对象的属性都转化成响应式的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) {
      return {
        __v_isRef: true,
        get value() { // proxy是响应式的数据,访问proxy[key]时会自动收集依赖
          return proxy[key]
        },
        set value(newVal) {
          proxy[key] = newVal
        }
      }
    }
    
  7. computed:接受一个getter函数,返回一个响应式的ref对象,getter中的依赖项发生改变时,会重新调用getter函数,并将结果赋值给ref的value属性。

    export function computed(getter) {
      const r = ref()
      // 执行callback时,访问r.value时触发了依赖收集
      effect(() => r.value = getter())
      return r
    }
    

Vite实现原理

vite介绍
Vite是一个面向现代浏览器的更轻、更快的Web应用开发工具。基于ESM实现。Vite的出现是为了解决Webpack冷启动时间过长,HMR反应速度慢的问题。
Vite只依赖Vite命令行工具Vite和用于单文件组件编译@vue/compiler-sfc。
Vite默认支持TypeScript,css预编译 less/sass/stylus/postcss(需要单独安装插件来完成编译),jsx以及Web Assembly。

Vite命令

  1. Vite serve:开启一个web server服务,启动时不会编译所有的文件,当http请求文件时,会在服务端编译文件,然后将编译结果返回。按需编译。
    在这里插入图片描述
    webpack中会编译所有的文件,所以慢
    Vite HMR:立即编译当前所修改的文件
    webpack HMR:会自动以修改文件为入口,重新build一次,所有涉及的依赖都会被加载一遍

  2. Vite build:使用Rollup打包。代码切割采用Dynamic Import(动态导入)特性实现,结果只支持现代浏览器。但有相应的polyfill支持。

Vite的原理实现

  1. Vite会基于KOA实现一个web服务器
  2. 接收到请求时,会根据请求读取对应的静态文件
  3. 将静态文件中的第三方模块的请求路径进行特殊处理
  4. 根据处理后的第三方模块的请求路径加载第三方模块
  5. 对.vue、.css以及文件进行编译处理,将编译后的结果返回给客户端
#!/usr/bin/env node
const { Readable } = require('stream')
const path = require('path')
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 = []
  // 接收到chunk分片时,将数据片段暂存到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) => {
  // 是否是加载第三方模块
  if (ctx.path.startsWith('/@modules/')) {
    //截取模块名
    const moduleName = ctx.path.substr(10)
    // 拼接package.json的路径
    const pkgPath = path.resolve(process.cwd(), 'node_modules', moduleName, 'package.json')
    // 读取package.json配置
    const pkg = require(pkgPath)
    // 将第三方模块的加载位置修改为第三方模块中package.json的module属性
    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')) { // 判断vue文件是否是.vue结尾
    const contents = await streamToString(ctx.body) // 将流转化成字符串
    const { descriptor } = compilerSFC.parse(contents) // 返回descriptor和error对象
    let code
    if(!ctx.query.type) { // 是否带有type参数
      code = descriptor.script.content // 获取编译后的js源码 
      // console.log(code);
      // 修改源码内容
      code = code.replace(/export\s+default\s+/g, 'const __script = ')
      code += `
      import { render as _sfc_render } from "${ctx.path}?type=template"
      __script.render = _sfc_render
      export default __script
      `
    } else if (ctx.query.type === 'template') {
      const templateRender = compilerSFC.compileTemplate({
        id: '#app',
        source: descriptor.template.content
      }) // 将模板转化成render函数
      code = templateRender.code
    }
    // 修改文件类型为js文件
    ctx.type = 'application/javascript'
    ctx.body = stringToStream(code) // 将字符串转化成流,交给下一个中间件处理
  }
  await next()
})

// 2.修改第三方模块的路径
app.use(async (ctx, next) => {
  // 针对js文件进行处理
  if (ctx.type === 'application/javascript') {
    const contents = await streamToString(ctx.body)
    // 使用正则替换 from 后面的第三方地址
    // import Vue from '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');
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值