玩命加载中 . . .

快速进阶 Vue 3.0

一、前言

2020年09月18日,Vue.js 3.0 正式发布,俺寻思才混熟 Vue 2 咋就又双叒升级了,恰好公司也要转成 Vue 3 了,正所谓“山不过来,我就过去”,这就去瞅瞅咋肥事改了哪些地方吧。

1.1 历程

  • 3.0版本代表了2年多的开发工作
  • 具有30多个RFC
  • 2600多个提交
  • 来自99个贡献者
  • 628个拉取请求
  • 大量开发和文档工作

1.2 Vue 的使命

成为任何人都可以快速学习的易于接近的框架

二、Vue 3.0 项目搭建

2.1 项目初始化

第一步,安装 Vue-cli

npm install -g @vue/cli

注意以下命令是错误的!

npm install -g vue
npm install -g vue-cli

安装成功后,我们即可使用 vue 命令,测试方法:

$ vue -V
@vue/cli 4.3.1

第二步,初始化 Vue 项目:

vue create vue-next-test

输入命令后,会出现命令行交互窗口,在这里我们选择 Manually select features:

Vue CLI v4.3.1
? Please pick a preset: 
  default (babel, eslint) 
❯ Manually select features 
随后我们勾选:Router、Vuex、CSS Pre-processors和Linter/Formatter
当然部分初始化配置也可选其他替代方案不一一赘述
Vue CLI v4.3.1
? Please pick a preset: Manually select features
? Check the features needed for your project: 
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
 ◉ Vuex
 ◉ CSS Pre-processors
❯◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
注意:Vue 3.0 项目目前需要从 Vue 2.0 项目升级而来,所以为了直接升级到 Vue 3.0 全家桶。
我们需要在 Vue 项目创建过程中勾选 Router 和 Vuex,所以避免手动写初始化代码。

2.2 升级 Vue 3.0 项目

目前创建 Vue 3.0 项目需要通过插件升级的方式实现。

Vue-cli 还没有直接支持,我们进入项目目录,并输入以下指令:

cd vue-next-test
vue add vue-next

执行自动安装 vue-cli-plugin-vue-next 插件(查看项目代码),该插件会完成以下操作:

  • 安装 Vue 3.0 依赖
  • 更新 Vue 3.0 webpack loader 配置,使其能够支持 .vue 文件构建(这点非常重要)
  • 创建 Vue 3.0 的模板代码
  • 自动将代码中的 Vue Router 和 Vuex 升级到 4.0 版本,如果未安装则不会升级
  • 自动生成 Vue Router 和 Vuex 模板代码

完成上述操作后,项目正式升级到 Vue 3.0

注意该插件还不支持 typescript,用 typescript 的童鞋还得再等等。(就是目前还不太支持TS)

2.3 Vue 3.0 基本特性体验

下面我们从项目开发的角度逐步体验 Vue 3.0 的开发流程

2.3.1 创建路由

项目开发中,我们通常需要创建新页面,然后添加路由配置。

我们在 /src/views 目录下创建 Test.vue:

<template>
  <div class="test">
    <h1>vue3.0 体验</h1>
  </div>
</template>

<script>
 export default {}
</script>

<style lang="less" scoped></style>

之后在 /src/router/index.js 中创建路由配置:

import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/test',
    name: 'Test',
    component: () => import(/* webpackChunkName: "test" */ '../views/Test.vue')
  }
]

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

export default router

初始化 Vue Router 的过程与 3.0 版本变化不大,只是之前采用构造函数的方式,这里改为使用 createRouter 来创建 Vue Router 实例,配置的方法基本一致,配置完成后我们还需要在 App.vue 中增加链接到 Test.vue 的路由:

<template>
  <div id="app">
    <div id="nav">
      <router-link to="/">Home</router-link> |
      <router-link to="/about">About</router-link> |
      <router-link to="/test">Test</router-link>
    </div>
    <router-view/>
  </div>
</template>

启动项目:

npm run serve

2.3.2 状态和事件绑定

Vue 3.0 中定义状态的方法改为类似 React Hooks 的方法,下面我们在 Test.vue 中定义一个状态 count:

<template>
  <div class="test">
    <h1>test count: {{count}}</h1>
  </div>
</template>

<script>
  import { ref } from 'vue'

  export default {
    setup () {
      const count = ref(0)
      return {
        count
      }
    }
  }
</script>

Vue 3.0 中初始化状态通过 setup 方法

定义状态需要调用 ref 方法。接下来我们定义一个事件,用来更新 count 状态:

<template>
  <div class="test">
    <h1>test count: {{count}}</h1>
    <button @click="add">add</button>
  </div>
</template>

<script>
  import { ref } from 'vue'

  export default {
    setup () {
      const count = ref(0)
      const add = () => {
        count.value++
      }
      return {
        count,
        add
      }
    }
  }
</script>

这里的 add 方法不再需要定义在 methods 中,但注意更新 count 值的时候不能直接使用 count++,而应使用 count.value++,

更新代码后,点击按钮,count 的值就会更新了:

2.3.3 计算属性和监听器

Vue 3.0 中计算属性和监听器的实现依赖 computed 和 watch 方法:

<template>
  <div class="test">
    <h1>test count: {{count}}</h1>
    <div>count * 2 = {{doubleCount}}</div>
    <button @click="add">add</button>
  </div>
</template>

<script>
  import { ref, computed, watch } from 'vue'

  export default {
    setup () {
      const count = ref(0)
      const add = () => {
        count.value++
      }
      watch(() => count.value, val => {
        console.log(`count is ${val}`)
      })
      const doubleCount = computed(() => count.value * 2)
      return {
        count,
        doubleCount,
        add
      }
    }
  }
</script>

计算属性 computed 是一个方法,里面需要包含一个回调函数,当我们访问计算属性返回结果时,会自动获取回调函数的值:

const doubleCount = computed(() => count.value * 2)

监听器 watch 同样是一个方法,它包含 2 个参数,2 个参数都是 function:

watch(() => count.value, 
  val => {
    console.log(`count is ${val}`)
  })

第一个参数是监听的值,count.value 表示当 count.value 发生变化就会触发监听器的回调函数,即第二个参数,第二个参数可以执行监听时候的回调。如果是2 个以上的监听属性 就这样:

watch(
  [refA, () => refB.value],
  ([a, b], [prevA, prevB]) => {
    console.log(`a is: ${a}`)
    console.log(`b is: ${b}`)
  }
)

2.3.4 获取路由

Vue 3.0 中通过 getCurrentInstance 方法获取当前组件的实例,然后通过 ctx 属性获得当前上下文,

ctx.$router 是 Vue Router 实例,里面包含了 currentRoute 可以获取到当前的路由信息。

<script>
  import { getCurrentInstance } from 'vue'

  export default {
    setup () {
      const { ctx } = getCurrentInstance()
      console.log(ctx.$router.currentRoute.value)
    }
  }
</script>

2.4 Vuex 集成

Vuex 的集成方法如下:

2.4.1 定义 Vuex 状态

第一步,修改 src/store/index.js 文件:

import Vuex from 'vuex'

export default Vuex.createStore({
  state: {
    test: {
      a: 1
    }
  },
  mutations: {
    setTestA(state, value) {
      state.test.a = value
    }
  },
  actions: {
  },
  modules: {
  }
})

Vuex 的语法和 API 基本没有改变,我们在 state 中创建了一个 test.a 状态,在 mutations 中添加了修改 state.test.a 状态的方法: setTestA

2.4.2 引用 Vuex 状态

第二步,在 Test.vue 中,通过计算属性使用 Vuex 状态:

<template>
  <div class="test">
    <h1>test count: {{count}}</h1>
    <div>count * 2 = {{doubleCount}}</div>
    <div>state from vuex {{a}}</div>
    <button @click="add">add</button>
  </div>
</template>

<script>
  import { ref, computed, watch, getCurrentInstance } from 'vue'

  export default {
    setup () {
      const count = ref(0)
      const add = () => {
        count.value++
      }
      watch(() => count.value, val => {
        console.log(`count is ${val}`)
      })
      const doubleCount = computed(() => count.value * 2)
      const { ctx } = getCurrentInstance()
      console.log(ctx.$router.currentRoute.value)
      const a = computed(() => ctx.$store.state.test.a)
      return {
        count,
        doubleCount,
        add,
        a
      }
    }
  }
</script>

这里我们通过计算属性来引用 Vuex 中的状态:

const a = computed(() => ctx.$store.state.test.a)

ctx 是上节中我们提到的当前组件实例。

2.4.3 更新 Vuex 状态

更新 Vuex 状态仍然使用 commit 方法,这点和 Vuex 3.0 版本一致:

<template>
  <div class="test">
    <h1>test count: {{count}}</h1>
    <div>count * 2 = {{doubleCount}}</div>
    <div>state from vuex {{a}}</div>
    <button @click="add">add</button>
    <button @click="update">update a</button>
  </div>
</template>

<script>
  import { ref, computed, watch, getCurrentInstance } from 'vue'

  export default {
    setup () {
      const count = ref(0)
      const add = () => {
        count.value++
      }
      watch(() => count.value, val => {
        console.log(`count is ${val}`)
      })
      const doubleCount = computed(() => count.value * 2)
      const { ctx } = getCurrentInstance()
      console.log(ctx.$router.currentRoute.value)
      const a = computed(() => ctx.$store.state.test.a)
      const update = () => {
        ctx.$store.commit('setTestA', count)
      }
      return {
        count,
        doubleCount,
        add,
        a,
        update
      }
    }
  }
</script>

这里我们点击 update a 按钮后,会触发 update 方法,此时会通过 ctx.$store.commit 调用 setTestA 方法,将 count 的值覆盖 state.test.a 的值。

2.5 小结

(1)Vue3.0 都写在 setup 里,以前的所有数据状态都写在 data 里。
(2)所有方法都写在 methods 里,而现在可根据功能模块把状态和方法等划分在一起,更利于模块化。不过这样对代码习惯和质量要求更高,初学者用 3.0 可能会写的更混乱。

三、深入剖析

3.1 剖析 Vue Composition API

可以去看官方地址

  • Vue 3 使用ts实现了类型推断,新版 API 全部采用普通函数,在编写代码时可以享受完整的类型推断(避免使用装饰器)
  • 解决了多组件间逻辑重用问题 (解决:高阶组件、mixin、作用域插槽)
  • Composition API 使用简单

先尝鲜Vue3.0看看效果

<script src="vue.global.js"></script>
<div id="container"></div>
<script>
    function usePosition(){ // 实时获取鼠标位置
        let state = Vue.reactive({x:0,y:0});
        function update(e) {
            state.x= e.pageX
            state.y = e.pageY
        }
        Vue.onMounted(() => {
            window.addEventListener('mousemove', update)
        })
        Vue.onUnmounted(() => {
            window.removeEventListener('mousemove', update)
        })
        return Vue.toRefs(state);
    }
    const App = {
        setup(){ // Composition API 使用的入口
            const state  = Vue.reactive({name:'xxx'}); // 定义响应数据
            const {x,y} = usePosition(); // 使用公共逻辑
            Vue.onMounted(()=>{
                console.log('当组挂载完成')
            });
            Vue.onUpdated(()=>{
                console.log('数据发生更新')
            });
            Vue.onUnmounted(()=>{
                console.log('组件将要卸载')
            })
            function changeName(){
                state.name = 'xxx';
            }
            return { // 返回上下文,可以在模板中使用
                state,
                changeName,
                x,
                y
            }
        },
        template:`<button @click="changeName">{{state.name}} 鼠标x: {{x}} 鼠标: {{y}}</button>`
    }
    Vue.createApp().mount(App,container);
</script>

到这里你会发现响应式才是Vue的灵魂

3.2 源码目录剖析

packages目录中包含着 Vue3.0 所有功能

├── packages
│   ├── compiler-core # 所有平台的编译器
│   ├── compiler-dom # 针对浏览器而写的编译器
│   ├── reactivity # 数据响应式系统
│   ├── runtime-core # 虚拟 DOM 渲染器 ,Vue 组件和 Vue 的各种API
│   ├── runtime-dom # 针对浏览器的 runtime。其功能包括处理原生 DOM API、DOM 事件和 DOM 属性等。
│   ├── runtime-test # 专门为测试写的runtime
│   ├── server-renderer # 用于SSR
│   ├── shared # 帮助方法
│   ├── template-explorer
│   └── vue # 构建vue runtime + compiler

compiler
compiler-core主要功能是暴露编译相关的API以及baseCompile方法
compiler-dom基于compiler-core封装针对浏览器的compiler (对浏览器标签进行处理)

runtime
runtime-core 虚拟 DOM 渲染器、Vue 组件和 Vue 的各种API
runtime-testDOM结构格式化成对象,方便测试
runtime-dom 基于runtime-core编写的浏览器的runtime (增加了节点的增删改查,样式处理等),返回rendercreateApp方法

reactivity
单独的数据响应式系统,核心方法reactiveeffectrefcomputed

vue
整合 compiler + runtime

到此我们解析了Vue3.0结构目录,整体来看整个项目还是非常清晰的

我们可以根据官方的测试用例来看下如何使用Vue3.0

const app = {
    template:`<div>{{count}}</div>`,
    data(){
        return {count:100}
    },
}
let proxy = Vue.createApp().mount(app,container);
setTimeout(()=>{
    proxy.count = 200;
},2000)

接下来我们来对比 Vue 2 和 Vue 3 中的响应式原理区别

3.3 Vue2.0 响应式原理机制 - defineProperty

这个原理老生常谈了,就是拦截对象,给对象的属性增加setget方法,因为核心是 defineProperty 所以还需要对数组的方法进行拦截

3.3.1 对对象进行拦截

function observer(target){
    // 如果不是对象数据类型直接返回即可
    if(typeof target !== 'object'){
        return target
    }
    // 重新定义key
    for(let key in target){
        defineReactive(target,key,target[key])
    }
}
function update(){
    console.log('update view')
}
function defineReactive(obj,key,value){
    observer(value); // 有可能对象类型是多层,递归劫持
    Object.defineProperty(obj,key,{
        get(){
            // 在get 方法中收集依赖
            return value
        },
        set(newVal){
            if(newVal !== value){
                observer(value);
                update(); // 在set方法中触发更新
            }
        }
    })
}
let obj = {name:'xxx'}
observer(obj);
obj.name = 'xxx';

3.3.2 数组方法劫持

let oldProtoMehtods = Array.prototype;
let proto = Object.create(oldProtoMehtods);
['push','pop','shift','unshift'].forEach(method=>{
    Object.defineProperty(proto,method,{
        get(){
            update();
            oldProtoMehtods[method].call(this,...arguments)
        }
    })
})
function observer(target){
    if(typeof target !== 'object'){
        return target
    }
    // 如果不是对象数据类型直接返回即可
    if(Array.isArray(target)){
        Object.setPrototypeOf(target,proto);
        // 给数组中的每一项进行observr
        for(let i = 0 ; i < target.length;i++){
            observer(target[i])
        }
        return
    };
    // 重新定义key
    for(let key in target){
        defineReactive(target,key,target[key])
    }
}

测试

let obj = {hobby:[{name:'xxx'},'喝']}
observer(obj)
obj.hobby[0].name = 'xxx'; // 更改数组中的对象也会触发试图更新
console.log(obj)

这里依赖收集的过程就不详细描述了,我们把焦点放在Vue3.0

  • Object.defineProperty缺点
    • 无法监听数组的变化
    • 需要深度遍历,浪费内存

3.4 Vue3.0 数据响应机制 - Proxy

在学习 Vue3.0 之前,你必须要先熟练掌握ES6中的 ProxyReflect 及 ES6中为我们提供的 MapSet 两种数据结构

先应用再说原理:

let p = Vue.reactive({name:'xxx'});
Vue.effect(()=>{ // effect方法会立即被触发
    console.log(p.name);
})
p.name = 'xxx';; // 修改属性后会再次触发effect方法

源码是采用ts编写,为了便于大家理解原理,这里我们采用js来从0编写,之后再看源码就非常的轻松啦!

3.4.1 reactive 方法实现

通过 proxy 自定义获取、增加、删除等行为:

function reactive(target){
    // 创建响应式对象
    return createReactiveObject(target);
}
function isObject(target){
    return typeof target === 'object' && target!== null;
}
function createReactiveObject(target){
    // 判断target是不是对象,不是对象不必继续
    if(!isObject(target)){
        return target;
    }
    const handlers = {
        get(target,key,receiver){ // 取值
            console.log('获取')
            let res = Reflect.get(target,key,receiver);
            return res;
        },
        set(target,key,value,receiver){ // 更改 、 新增属性
            console.log('设置')
            let result = Reflect.set(target,key,value,receiver);
            return result;
        },
        deleteProperty(target,key){ // 删除属性
            console.log('删除')
            const result = Reflect.deleteProperty(target,key);
            return result;
        }
    }
    // 开始代理
    observed = new Proxy(target,handlers);
    return observed;
}
let p = reactive({name:'xxx'});
console.log(p.name); // 获取
p.name = 'xxx'; // 设置
delete p.name; // 删除

我们继续考虑多层对象如何实现代理

let p = reactive({ name: "xxx", age: { num: 10 } });
p.age.num = 11

由于我们只代理了第一层对象,所以对age对象进行更改是不会触发set方法的,但是却触发了get方法,这是由于 p.age会造成 get操作

get(target, key, receiver) {
      // 取值
    console.log("获取");
    let res = Reflect.get(target, key, receiver);
    return isObject(res) // 懒代理,只有当取值时再次做代理,vue2.0中一上来就会全部递归增加getter,setter
    ? reactive(res) : res;
}

这里我们将p.age取到的对象再次进行代理,这样在去更改值即可触发set方法

我们继续考虑数组问题
我们可以发现Proxy默认可以支持数组,包括数组的长度变化以及索引值的变化

let p = reactive([1,2,3,4]);
p.push(5);

但是这样会触发两次set方法,第一次更新的是数组中的第4项,第二次更新的是数组的length

我们来屏蔽掉多次触发,更新操作

set(target, key, value, receiver) {
    // 更改、新增属性
    let oldValue = target[key]; // 获取上次的值
    let hadKey = hasOwn(target,key); // 看这个属性是否存在
    let result = Reflect.set(target, key, value, receiver);
    if(!hadKey){ // 新增属性
        console.log('更新 添加')
    }else if(oldValue !== value){ // 修改存在的属性
        console.log('更新 修改')
    }
    // 当调用push 方法第一次修改时数组长度已经发生变化
    // 如果这次的值和上次的值一样则不触发更新
    return result;
}

解决重复使用reactive情况

// 情况1.多次代理同一个对象
let arr = [1,2,3,4];
let p = reactive(arr);
reactive(arr);

// 情况2.将代理后的结果继续代理
let p = reactive([1,2,3,4]);
reactive(p);

通过hash表的方式来解决重复代理的情况

const toProxy = new WeakMap(); // 存放被代理过的对象
const toRaw = new WeakMap(); // 存放已经代理过的对象
function reactive(target) {
  // 创建响应式对象
  return createReactiveObject(target);
}
function isObject(target) {
  return typeof target === "object" && target !== null;
}
function hasOwn(target,key){
  return target.hasOwnProperty(key);
}
function createReactiveObject(target) {
  if (!isObject(target)) {
    return target;
  }
  let observed = toProxy.get(target);
  if(observed){ // 判断是否被代理过
    return observed;
  }
  if(toRaw.has(target)){ // 判断是否要重复代理
    return target;
  }
  const handlers = {
    get(target, key, receiver) {
      // 取值
      console.log("获取");
      let res = Reflect.get(target, key, receiver);
      return isObject(res) ? reactive(res) : res;
    },
    set(target, key, value, receiver) {
      let oldValue = target[key];
      let hadKey = hasOwn(target,key);
      let result = Reflect.set(target, key, value, receiver);
      if(!hadKey){
        console.log('更新 添加')
      }else if(oldValue !== value){
        console.log('更新 修改')
      }
      return result;
    },
    deleteProperty(target, key) {
      console.log("删除");
      const result = Reflect.deleteProperty(target, key);
      return result;
    }
  };
  // 开始代理
  observed = new Proxy(target, handlers);
  toProxy.set(target,observed);
  toRaw.set(observed,target); // 做映射表
  return observed;
}

到这里reactive方法基本实现完毕,接下来就是与Vue2中的逻辑一样实现依赖收集和触发更新

get(target, key, receiver) {
    let res = Reflect.get(target, key, receiver);
+   track(target,'get',key); // 依赖收集
    return isObject(res) 
    ?reactive(res):res;
},
set(target, key, value, receiver) {
    let oldValue = target[key];
    let hadKey = hasOwn(target,key);
    let result = Reflect.set(target, key, value, receiver);
    if(!hadKey){
+     trigger(target,'add',key); // 触发添加
    }else if(oldValue !== value){
+     trigger(target,'set',key); // 触发修改
    }
    return result;
}

track的作用是依赖收集,收集的主要是effect,我们先来实现effect原理,之后再完善 tracktrigger方法

3.4.2 effect实现

effect意思是副作用,此方法默认会先执行一次。如果数据变化后会再次触发此回调函数。

let school = {name:'xxx'}
let p = reactive(school);
effect(()=>{
    console.log(p.name);  // xxx
})

我们来实现effect方法,我们需要将effect方法包装成响应式effect

function effect(fn) {
  const effect = createReactiveEffect(fn); // 创建响应式的effect
  effect(); // 先执行一次
  return effect;
}
const activeReactiveEffectStack = []; // 存放响应式effect
function createReactiveEffect(fn) {
  const effect = function() {
    // 响应式的effect
    return run(effect, fn);
  };
  return effect;
}
function run(effect, fn) {
    try {
      activeReactiveEffectStack.push(effect);
      return fn(); // 先让fn执行,执行时会触发get方法,可以将effect存入对应的key属性
    } finally {
      activeReactiveEffectStack.pop(effect);
    }
}

当调用fn()时可能会触发get方法,此时会触发track

const targetMap = new WeakMap();
function track(target,type,key){
    // 查看是否有effect
    const effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1];
    if(effect){
        let depsMap = targetMap.get(target);
        if(!depsMap){ // 不存在map
            targetMap.set(target,depsMap = new Map());
        }
        let dep = depsMap.get(target);
        if(!dep){ // 不存在set
            depsMap.set(key,(dep = new Set()));
        }
        if(!dep.has(effect)){
            dep.add(effect); // 将effect添加到依赖中
        }
    }
}

当更新属性时会触发trigger执行,找到对应的存储集合拿出effect依次执行

function trigger(target,type,key){
    const depsMap = targetMap.get(target);
    if(!depsMap){
        return
    }
    let effects = depsMap.get(key);
    if(effects){
        effects.forEach(effect=>{
            effect();
        })
    }
}

我们发现如下问题

let school = [1,2,3];
let p = reactive(school);
effect(()=>{
    console.log(p.length);
})
p.push(100);

新增了值,effect方法并未重新执行,因为push中修改length已经被我们屏蔽掉了触发trigger方法,所以当新增项时应该手动触发length属性所对应的依赖。

function trigger(target, type, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  let effects = depsMap.get(key);
  if (effects) {
    effects.forEach(effect => {
      effect();
    });
  }
  // 处理如果当前类型是增加属性,如果用到数组的length的effect应该也会被执行
  if (type === "add") {
    let effects = depsMap.get("length");
    if (effects) {
      effects.forEach(effect => {
        effect();
      });
    }
  }
}

3.4.3 ref实现

ref可以将原始数据类型也转换成响应式数据,需要通过.value属性进行获取值

function convert(val) {
  return isObject(val) ? reactive(val) : val;
}
function ref(raw) {
  raw = convert(raw);
  const v = {
    _isRef:true, // 标识是ref类型
    get value() {
      track(v, "get", "");
      return raw;
    },
    set value(newVal) {
      raw = newVal;
      trigger(v,'set','');
    }
  };
  return v;
}

问题又来了我们再编写个案例

let r = ref(1);
let c = reactive({
    a:r
});
console.log(c.a.value);

这样做的话岂不是每次都要多来一个.value,这样太难用了

get方法中判断如果获取的是ref的值,就将此值的value直接返回即可

let res = Reflect.get(target, key, receiver);
if(res._isRef){
  return res.value
}

3.4.4 computed实现

computed 实现也是基于 effect 来实现的,特点是computed中的函数不会立即执行,多次取值是有缓存机制的

先来看用法:

let a = reactive({name:'xxx'});
let c = computed(()=>{
  console.log('执行次数')
  return a.name +'xxx';
})
// 不取不执行,取n次只执行一次
console.log(c.value);
console.log(c.value);
function computed(getter){
  let dirty = true;
  const runner = effect(getter,{ // 标识这个effect是懒执行
    lazy:true, // 懒执行
    scheduler:()=>{ // 当依赖的属性变化了,调用此方法,而不是重新执行effect
      dirty = true;
    }
  });
  let value;
  return {
    _isRef:true,
    get value(){
      if(dirty){
        value = runner(); // 执行runner会继续收集依赖
        dirty = false;
      }
      return value;
    }
  }
}

修改effect方法

function effect(fn,options) {
  let effect = createReactiveEffect(fn,options);
  if(!options.lazy){ // 如果是lazy 则不立即执行
    effect();
  }
  return effect;
}
function createReactiveEffect(fn,options) {
  const effect = function() {
    return run(effect, fn);
  };
  effect.scheduler = options.scheduler;
  return effect;
}

trigger时判断

deps.forEach(effect => {
  if(effect.scheduler){ // 如果有scheduler 说明不需要执行effect
    effect.scheduler(); // 将dirty设置为true,下次获取值时重新执行runner方法
  }else{
    effect(); // 否则就是effect 正常执行即可
  }
});
let a = reactive({name:'xxx'});
let c = computed(()=>{
  console.log('执行次数')
  return a.name +'xxx';
})
// 不取不执行,取n次只执行一次
console.log(c.value);
a.name = 'asd'; // 更改值 不会触发重新计算,但是会将dirty变成true

console.log(c.value); // 重新调用计算方法

四、总结

4.1 架构改进和新功能:

  • 编译器(Compiler)
    • 使用模块化架构
    • 优化 “Block tree”
    • 更激进的 static tree hoisting 功能 (检测静态语法,进行提升)
    • 支持 Source map
    • 内置标识符前缀(又名”stripWith”)
    • 内置整齐打印(pretty-printing)功能
    • 移除 Source map 和标识符前缀功能后,使用 Brotli 压缩的浏览器版本精简了大约10KB
  • 运行时(Runtime)
    • 速度显著提升
    • 同时支持 Composition API 和 Options API,以及 typings
    • 基于 Proxy 实现的数据变更检测
    • 支持 Fragments (允许组件有从多个根结点)
    • 支持 Portals (允许在DOM的其它位置进行渲染)
    • 支持 Suspense w/ async setup()

4.2 性能方面的优化

  • 路由懒加载
  • keep-alive缓存页面
  • 使用v-show复用DOM
  • v-for 遍历避免同时使用 v-if
  • 长列表性能优化
  • 事件的销毁
  • 图片懒加载
  • 第三方插件按需引入
  • 无状态的组件标记为函数式组件
  • 子组件分割
  • 变量本地化
  • SSR

4.3 更好的 TypeScript 集成

  • 改进的TypeScript支持,编辑器能提供强有力的类型检查和错误及警告
  • 更好的调试支持

4.4 用于处理大规模用例的新 API

  • reactive
  • ref
  • computed
  • readonly
  • watchEffect
  • watch
  • unref
  • toRef
  • toRefs
  • isRef
  • isProxy
  • isReactive
  • isReadonly
  • customRef
  • markRaw
  • shallowReactive
  • shallowReadonly
  • shallowRef
  • toRaw

4.5 分层内部模块

Vue 3.0 内核仍然可以通过一个简单的<script>标签使用,但其内部结构已被彻底重写为一组解耦的模块。新的体系结构提供了更好的可维护性,并允许最终用户通过摇树来减少运行时大小的一半。
模块如下:

模块提供的功能点:
  1. 编译器支持用于构建时自定义的自定义 AST 转换(例如,构建时i18n)

  2. 核心运行时提供API用于创建针对不同渲染目标(例如本机移动设备WebGL终端)的自定义渲染器。默认DOM渲染器使用相同的 API 构建。

  3. @vue/reactivity模块导出的功能可以直接访问Vue的反应系统,并且可以用作独立程序包。它可以与其他模板解决方案(例如lit-html)配对,甚至可以用于非 UI 方案。

4.6 Composition API 设计目的

旨在解决大型应用程序中 Vue 使用的难点。
Composition API 建立在反应性 API 之上,与 2.x 基于对象的 API 相比,可实现类似于 React 挂钩的逻辑组成和重用,更灵活的代码组织模式以及更可靠的类型推断。

4.7 性能改进(相对 Vue 2.0)

  • 通过摇树:(减轻了多达41%的资源大小)
  • 初始渲染:(加快了多达55%的速度)
  • 更新速度:(加快了133%的速度)
  • 内存占用:(最多减少54%)

4.8 提供的两个实验性功能

  • <script setup>

用于在SFC中使用Composition API的语法糖

  • <style vars>

SFC 中状态驱动的 CSS 变量

迁移3.0指♂南:https://v3.vuejs.org/guide/migration/introduction.html

4.9 兼容 IE 11

计划在2020年第四季度重点关注它。因此,计划迁移现有 V2 应用程序的用户或需要 IE11 支持人员此时应意识到这些限制。

4.10 后续计划

到此为止关于Vue3.0核心的所有内容就暂时的谢幕了,内容还是相当多的,在这引用德华的歌——如果想要飞得高,就该把 Vue 2.0 忘掉 ~ 希望对大家有所帮助!


  目录