Vue3.0

头像
Charon
    阅读 13 分钟
    3

    之前已经了解过了vue2.0版本,现在我们来了解一下3.0版本.

    首先我们来看下变化:

    • 源码组织方式的变化.
    • Composition API
    • 性能提升
    • Vite

    首先源码的组织方式

    • 首先类型约束2.0是flow然后3.0源码采用TypeScript重写
    • 然后就是使用Monorepo管理项目结构

    3.0
    image.png

    2.0
    image.png

    然后就是Composition API

    设计动机:

    • 回顾OptionsAPI
    • 包含一个描述组件选项(data、methods、props等)的对象
    • OptionsAPI开发复杂组件,同一个功能逻辑的代码被拆分到不同选项

    image

    随着业务复杂度越来越高,代码量会不断的加大;由于相关业务的代码需要遵循option的配置写到特定的区域,导致后续维护非常的复杂,代码可复用性也不高
    image.png

    然后就是mixins混入,2.x中我们通过mixins来共享一些公共的东西.但是会带来一些问题

    • 第一命名冲突的问题
    • 第二 共享数据 会有隐式依赖的问题,mixin也可以使用假定在组件中定义的数据属性,如果我们以后想重构一个组件,改变了mixin需要的变量的名称,会发生什么情况呢?我们在看这个组件时,不会发现有什么问题。linter也不会发现它,我们只会在运行时看到错误。

    现在想象一个有很多mixin的组件。我们重构本地数据属性时,它会破坏mixin吗?我们得手动搜索才能知道。
    这太麻烦了

    CompositionAPI
    • Vue.js3.0新增的一组API
    • 一组基于函数的API
    • 可以更灵活的组织组件的逻辑

    image

    image.png

    通过compostion API显然我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起(一个函数里)

    image
    我们来看如何解决optionsApi提出的问题

    • 同一个功能逻辑我们可以放到同一个函数里,随着业务的复杂我们可以把拆分不同的业务到不同的函数里.
    • 最后组装它们到setup函数,这样再不同的组件可以导入这些函数来自由组装使用,来达到代码重用的目的.
    • 命名冲突:我们需要显式命名任何状态或从合成函数返回的方法,这样通过我们自己的命名就不会有冲突的这种问题了。
    • 隐式依赖:前面看到mixin如何使用在组件上定义的 data 属性,这可能会使代码变得脆弱,并且很难进行推理,compostionAPI可以调用组件中定义的局部变量。不过,不同之处在于,现在必须将此变量显式传递给合成函数,这样一切都是显示的,就不会有之前组件的修改,有的问题到了最后阶段才发现这种情况了.
    setup函数

    vue3中专门为组件提供的新属性.它为我们使用vue3的Composition API 新特新提供了统一的入口.之前定义的data,methods等都统一放入setup中实现。

    setup函数会在beforeCreate之后、created之前 所以它内部的this指向window

    接收外界传入的props,接受的组件必须props里需要声明否则拿不到传值.

      //home.vue里
      < setup : data = "123" ></setup >
    //setup.vue里
    export default {
      setup (props) {
        console.log(props.data)
      },
      props: {
        data: Number
      }
    }

    第二个形参context,指的是上下文对象,在这个对象中包含了一些书信,这些属性在2.x中通过this才能访问到,3.0访问方式如下,3.0中无法访问到this

    export default {
      name: 'Setup',
      setup (props, context) {
        console.log(context)
      },
      props: {
        data: Number
      }
    }
    //输出结果
    {
      root: (...) ,
      parent: (...) ,
      refs: (...) ,
      attrs: (...) ,
      listeners: (...) ,
      isServer: (...) ,
      ssrContext: (...) ,
      emit: (...) ,
      slots: { } ,
    }

    在新版的生命周期函数,可以按需导入到组件中,且只能在setup()函数中使用.

    import { onMounted, onUnmounted } from 'vue';
    export default {
      setup () {
        onMounted(() => {
          //
        });
    
        onUnmounted(() => {
          //
        });
      }
    };

    2.x和3.0的setup中生命周期钩子的比对

    image.png

    然后来了解下常用的api:

    • reactive:reactive()函数接受一个普通对象,返回一个响应式的数据对象.类似于2.x的组件里的data()函数,想要使用必须先引入reactive,模板中想要使用该数据必须return出去,此函数只能在setup函数里使用(返回一个proxy对象,不能直接解构它,直接解构就失去了响应能力,可以通过toRef转换.).
    • ref:ref()函数用来根据给定制创建一个响应式的数据对象,ref()函数调用的返回值是一个对象,这个队形上只包含一个.value的属性.与reactvie类似,对比两者区别 reactive创建一个集合响应式数据,ref创建单一响应式数据.
    • toRef:toRef()函数可以将reactive()创建出来的响应式对象,转换为普通的对象,只不过,这个对象上的每一个属性节点,都是ref()类型的响应式数据:最长见的引用场景.(reactive创建之后的数据如果返回使用扩展运算符(...),它创建的所有属性不再是响应式的.这是需要toRefs)
    • computed,Watch,watchEffect的作用可以参考官方手册.

    也可以参考这几个:
    https://vue3js.cn/vue-composi...
    https://www.jianshu.com/p/038...
    https://juejin.cn/post/689201...
    https://www.yuque.com/along-n...
    我这里只是列举了几个常用的api,具体的用法可以参考上面几个网址.

    这里我们来看如何来简易的实现vue3.0中的响应式
    核心方法:

    • reactive/ref/toRefs/computed
    • effect:watchEffect
    • track:收集依赖
    • trigger:触发更新

    首先来reactive

    • 接收一个参数,判断这参数是否是对象
    • 创建拦截器对象handler,设置get/set/deleteProperty
    • 返回Proxy对象
    const isObject = val => val !== null && typeof val === 'object' //判断是否是对象
    const convert = target => isObject(target) ? reactive(target) : target //是对象转换成响应式
    const hasOwnProperty = Object.prototype.hasOwnProperty
    const hasOwn = (target, key) => hasOwnProperty.call(target, key) //检测属性存在
    
    export function reactive (target) { //创建响应式对象
      if (!isObject(target)) return target
    
      const handler = { //创建proxy的get,set
        get (target, key, receiver) {
          // 收集依赖
          track(target, key)
          const result = Reflect.get(target, key, receiver) //获取值
          return convert(result)//查看获取的值是否是对象,是的话转换成转换响应式对象返回(proxy)
          //因为正常获取到对象的值,不是代理对象
        },
        set (target, key, value, receiver) {
          const oldValue = Reflect.get(target, key, receiver) //获取老值
          let result = true //proxy 对象set必须要返回布尔值标识是否成功
          if (oldValue !== value) { //新旧值不一样
            result = Reflect.set(target, key, value, receiver) //设置值
            // 触发更新
            trigger(target, key)
          }
          return result
        },
        deleteProperty (target, key) {//删除
          const hadKey = hasOwn(target, key) //查看属性是否存在
          const result = Reflect.deleteProperty(target, key) //删除,返回布尔值
          if (hadKey && result) { //如果存在并且删除成功
            // 触发更新
            trigger(target, key)
          }
          return result //和set一样,必须要返回布尔值
        }
      }
    
      return new Proxy(target, handler) //返回代理对象
    }

    然后我们来看依赖的收集
    image.png

    收集依赖的思路主要是

    • targetMap:(WeakMap类型,弱引用方便垃圾回收机制及时回收)

      • key是储存目标对象,也就是当前使用的未被转换响应式(porxy)的那个对象
      • value值是一个map对象

        • map的key是目标对象的属性名称,也就是当前访问的属性名
        • 对应的值是一个set对象

          • set里储存的是watcher(这里添加的是effect回调)

    其实收集依赖的过程就是我们通过储存对象,和当前访问属性,来给他添加或已有的话就是找到它添加(set对象中添加),我们effect传入的函数(或者watcher)。

    let activeEffect = null // 用来储存当前的effect的回调函数
    export function effect (callback) {
      activeEffect = callback  // 储存回调
      callback() // 执行回调,访问响应式对象属性,去收集依赖,放入对应的targetMap 对应目标对象的map对象,对应属性的set里
      activeEffect = null //已经放入对应的set对象里,清空下
    }
    
    let targetMap = new WeakMap() //用来储存目标对象的WeakMap
    
    export function track (target, key) { //收集依赖
      if (!activeEffect) return //收集过程中如果不是通过effect创建退出
      let depsMap = targetMap.get(target) //获取对应目标对象
      if (!depsMap) { //不存在
        targetMap.set(target, (depsMap = new Map())) //创建key为目标对象,值先为空map并赋值给depMap 
      }
      let dep = depsMap.get(key) //从depmap中取对应访问的属性值
      if (!dep) { //如果不存在
        depsMap.set(key, (dep = new Set())) //给这个属性创建一个set,用来储存回调effect的
      }
      dep.add(activeEffect) //给set对象添加回调
    }

    而修改时的触发更新就是找得到目标对象对应属性里的set里储存的函数,依次执行触发更新.

    export function trigger (target, key) {//触发更新
      const depsMap = targetMap.get(target) //找到修改的目标对象
      if (!depsMap) return //不存在退出
      const dep = depsMap.get(key) //找到修改的属性的那个set对象
      if (dep) {
        dep.forEach(effect => { //依次执行set的存入的回到(effect传入的)
          effect()
        })
      }
    }

    然后就是ref,根据它的特点,我们来

    export function ref (raw) {
      // 判断 raw 是否是ref 创建的对象,如果是的话直接返回
      if (isObject(raw) && raw.__v_isRef) {
        return
      }
      let value = convert(raw) //看是不是可以转换为响应式对象
      const r = {
        __v_isRef: true, //ref创建对象的标识
        get value () {// 对应value的get函数
          track(r, 'value') //收集依赖,目标对象是当前的r,属性是value,值是effect的回调函数
          return value //返回初始传入的值
        },
        set value (newValue) {//修改
          if (newValue !== value) { //如果新旧值不一样,证明修改了
            raw = newValue //赋值
            value = convert(raw) //如果是对象的话,转换为响应式对象
            trigger(r, 'value') //触发收集过依赖里的那个set对象中的回调.
          }
        }
      }
      return r  //返回我们包装过的r
    }

    然后根据规则是toRefs

    export function toRefs (proxy) { //把传入的响应式对象内部的属性,解析成ref类型的对象
      const ret = proxy instanceof Array ? new Array(proxy.length) : {}//查看是对象还是数组
    
      for (const key in proxy) { //循环 proxy 
        ret[key] = toProxyRef(proxy, key) //解析成ref类型的对象
      }
    
      return ret  //返回这个方便解构
    }
    
    function toProxyRef (proxy, key) {
      const r = {
        __v_isRef: true,
        get value () { //对应获取值的get
          return proxy[key] //获取value时,获取的是响应式对象里的值,会触发它的get,这样依赖就会触发依赖收集
          //通过它包装,解构出来属性也变成了响应式的
        },
        set value (newValue) { //设置值时
          proxy[key] = newValue //同上面一样
        }
      }
      return r //返回r
    }

    最后是computed计算属性

    export function computed (getter) {//计算属性
      const result = ref() //创建一个ref类型对象
    
      effect(() => (result.value = getter())) //借助effect 和这个ref对象储存 当前这个 函数到对应set
    
      return result //返回这个引用,之后每次取值都去从value取
    }

    然后来测试

    <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)
    
        product.price = 4000
        console.log(total.value)
    
        product.count = 1
        console.log(total.value)
    
      </script>
      
      
      <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)
        }
    
        const { price, count } = useProduct()
    
    
        let total = 0 
        effect(() => {
          total = price.value * count.value
        })
        console.log(total)
    
        price.value = 4000
        console.log(total)
    
        count.value = 1
        console.log(total)
    
      </script>

    发现都是没问题的,到这里我们一个最简单版本就完成了.

    和2.x对比来说要简单不少,不需要像2.x一样初始化响应式数据时每个属性节点一个dep对象了,而是在访问 该属性节点时,收集该节点的观察者.

    总结下reactive vs ref

    • ref可以把基本数据类型数据,转成响应式对象
    • ref返回的对象,重新赋值成对象也是响应式的
    • reactive返回的对象,重新赋值丢失响应式
    • reactive返回的对象不可以解构

    性能提升

    • 响应式系统升级
    • 编译优化
    • 源码体积的优化

    vue2.x中响应式系统的核心defineProperty

    vue3.0中使用Proxy对象重写响应式系统

    • 可以监听动态新增的属性
    • 可以监听删除的属性
    • 可以监听数组的索引和length属性
    • proxy拦截操作更多
    • 不需要一个一个侵入对象递归劫持属性,而是直接代理对象
    • proxy新标准将受到浏览器厂商重点持续的性能优化

    编译优化:
    2.x中通过标记静态根节点,优化diff的过程

    • vue2.0 标记静态根节点和静态节点,diff过程中会跳过静态根节点(因为不会变化),但是还会遍历静态节点

    3.0中标记和提升所有的静态根节点,diff的时候只需要对比动态节点内容

    • Fragments(升级vetur插件)(模板里面不用创建唯一根节点,可以直接放同级标签和文本内容,虚拟dom会默认生成 “片段”)
    • 静态提升(静态节点的提升hoistStatic,开启该选项后提升所有静态节点,静态节点将被提升到 render() 函数外面生成,并被命名为 _hoisted_x 变量.)

    image.png
    image.png

    • Patchflag( 针对动态节点的_createVNode,Vue 在运行时会生成number(大于 0)值的PatchFlag,用作标记,这个参数主要用于 diff 比较过程。当 DOM 节点有这个标志并且大于 0,就代表要更新,没有就跳过。Vue 在 diff 过程会根据不同的 patchflag 使用不同的 patch 方法 )
    • 缓存事件处理函数
    export function render(_ctx, _cache) {
              return (_openBlock(), _createBlock("div", null, [
                _createVNode("span", {
                  onClick: _cache[1] || (_cache[1] = $event => (_ctx.onClick($event)))
                }, _toDisplayString(_ctx.msg), 1 /* TEXT */)
              ]))
    }

    类似这样cache[1],会自动生成并缓存一个内联函数,“神奇”的变为一个静态节点。
    ps:有点像ReactuseCallback自动化。

    参考:
    https://www.cnblogs.com/woai3...

    https://blog.csdn.net/weixin_...

    image.png

    优化代码打包体积,Vue.js3.0中移除了一些不常用的API,例如:
    inline-template、filter等,然后就是Tree-shaking 摇树优化

    • 可以将无用模块“剪辑”,仅打包需要的(比如v-model,<transition>,用不到就不会打包)。
    • 一个简单“HelloWorld”大小仅为:13.5kb

      • 11.75kb,仅Composition API
    • 包含运行时完整功能:22.5kb

      • 拥有更多的功能,却比Vue 2更迷你。

    很多时候,我们并不需要 vue提供的所有功能,在 vue 2 并没有方式排除掉,但是 3.0 都可能做成了按需引入。

    最后是Vite

    Vite是一个面向现代浏览器的一个更轻、更快的Web应用开发工具

    它基于ECMAScript标准原生模块系统(ESModules)实现

    依赖:Vite,@vue/compiler-sfc

    首先我们来看esm

    • 现代浏览器都支持ESModule(IE不支持)
    • 通过下面的方式加载模块

      • <script type="module" src="..."></script>
    • 支持模块的script默认延迟加载,module默认延迟加载

      • 类似于script标签设置defer
      • 在文档解析完成后,触发DOMContentLoaded事件前执行

    Vite对比Vue-CLI

    • Vite在开发模式下不需要打包可以直接运行

      • 快速冷启动
      • 按需编译
      • 模块热更新
      • Vite在生产环境下使用Rollup打包

        • 基于ESModule的方式打包,打出来的包体积更小
    • Vue-CLI开发模式下必须对项目打包才可以运行

    serve
    image.png

    vue-cli-service serve

    image.png

    hmr热加载

    ViteHMR:立即编译当前所修改的文件

    WebpackHMR:会自动以这个文件为入口重写build一次,所有的涉及到的依赖也都会被加载一遍

    打包or不打包

    使用Webpack打包的两个原因:

    • 浏览器环境并不支持模块化
    • 零散的模块文件会产生大量的HTTP请求

    vite创建项目

    image.png

    基于模板创建
    image.png

    最后我们来探寻下它的大概实现原理:
    其实主要是我们本地启动一个静态服务器,然后通过拦截请求,拦截不浏览器不认识的文件.vue进行能够识别的js文件返回.
    其实主要是了解一下它实现的静态服务器。

    首先是核心功能:

    • 静态Web服务器
    • 编译单文件组件

      • 拦截浏览器不识别的模块,并处理

    这里实现的是脚手架形式,也可以直接拿到工程里进行测试

    #!/usr/bin/env node
    const path = require('path') //node path
    const { Readable } = require('stream') //node stream
    const Koa = require('koa') //koa启动 静态服务器
    const send = require('koa-send')
    const compilerSFC = require('@vue/compiler-sfc') //vue中编译器转换vue代码
    
    const app = new Koa()//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. 加载第三方模块
    app.use(async (ctx, next) => { //vue文件中加载第三方模块
      // 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')
         //从node_modules拼接当前路径获取package.json
        const pkg = require(pkgPath)
        ctx.path = path.join('/node_modules', moduleName, pkg.module)
        //获取模块最终路径
      }
      await next()
    })
    
    // 1. 静态文件服务器
    app.use(async (ctx, next) => { //中间件,读取 indenx.html
      await send(ctx, ctx.path, { root: process.cwd(), index: 'index.html' })
      await next()//下一个
    })
    
    // 4. 处理单文件组件
    app.use(async (ctx, next) => { //处理.vue,文件
      if (ctx.path.endsWith('.vue')) { //路径有vue文件
        const contents = await streamToString(ctx.body) //流转字符串
        const { descriptor } = compilerSFC.parse(contents) //编译器转换代码
        let code 
        if (!ctx.query.type) { // 我们看原始 vite转换后的vue代码会多一个type
          code = descriptor.script.content //取出转换后js代码
          // console.log(code)
          code = code.replace(/export\s+default\s+/g, 'const __script = ') // 进行修改,修改为vite一样
          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 })
          //通过最后compileTemplate解析获取完成的js代码
          code = templateRender.code  //返回
        }
        ctx.type = 'application/javascript' //设置返回格式
        ctx.body = stringToStream(code) //字符串转换为流
      }
      await next() //下一个
    })
    
    // 2. 修改第三方模块的路径
    app.use(async (ctx, next) => {
      if (ctx.type === 'application/javascript') { //如果是请求的js
        const contents = await streamToString(ctx.body)//流转换
        // import vue from 'vue'
        // import App from './App.vue'
        ctx.body = contents
          .replace(/(from\s+['"])(?![\.\/])/g, '$1/@modules/')
          .replace(/process\.env\.NODE_ENV/g, '"development"')
          //把 from 'vue'转换为 from '/@modules/vue'然后更改依赖图中的环境变量判断
      }
    })
    
    app.listen(3000)//端口监听
    console.log('Server running @ http://localhost:3000')
    
    //脚手架的形式
    

    部分图片借鉴于https://juejin.cn/post/689054...


    Charon
    57 声望16 粉丝

    世界核平