【VUE】 Function-based API 尝鲜

早在六月初,vue做者尤雨溪在知乎上发布了一篇vue3.0的RFC [Vue Function-based API RFC], 这篇文章指明了在19年底即将发布的vue3.0的初步路线。
复制代码

RFC (Request For Comments),中文翻译为 "意见征求稿"javascript

一. 设计目的

咱们能够思考一下,为何 vue团队 要选择function-based API?vue

1. 减小面条代码,提升灵活性。

  • 众所周知, vue.js 的 api 对开发者十分的友好。在开发中,vue.js 的API强制要求开发者将组件代码基于选项切分开来。java

  • 理想很美好,现实很骨感。缺少经验的新手在项目不断迭代中可能会将逻辑写在同一个文件,使得代码逻辑很是不易阅读及抽离。react

  • 新的API制约不多,它提倡开发者根据逻辑去抽离成函数,经过返回值将逻辑数据返回回来,也不仅是能够根据逻辑去抽离代码,也能够为了写出更好的漂亮代码而抽离函数。webpack

2. 减小mixin,提升代码重复利用率。

  • 在咱们的平常开发中,咱们发现一个组件变的很大的时候,会将组件拆分红各个小组件,逻辑也会拆成一个个的 mixin 来复用(表格分页mixin,对话框mixin等)。但在引入了多个 mixin 的状况下,会出现引用赋值混乱/命名重复的困扰,虽然解决了快速开发的问题,但这使得事情更加的糟糕。
// mixin-mouse.js
export default {
  data () {
    return {
      x: 0,
      y: 0
    }
  },
  
  methods: {
    update(e) {
      x = e.pageX;
      y = e.pageY;
    }
  },
  
  mounted() {
    window.addEventListener('mousemove', this.update)
  },
  
  destroyed() {
    window.removeEventListener('mousemove', this.update)
  }
}
复制代码
// mouse.js
import { binding, onMounted, onUnmouted } from "vue";
export const useMouse = () => {
  const x = binding(0)
  const y = binding(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}
复制代码
// app.js
import { binding } from "vue";
import { useMouse } from "./mouse";

export default {
  setup () {
    const { x, y } = useMouse();
    return { x, y };
  }
}
复制代码

引入的时候,开发者可以更清晰的识别及处理合并进来的值,这样更容易维护。git

二. 方案

1. setup函数

  • 这个函数将会是咱们 setup 咱们组件逻辑的地方,它会在一个组件实例被建立时,初始化了 props 以后调用。setup() 会接收到初始的 props 做为参数:
import { reactive, toBindings } from "vue";

export default {
  // props, ctx不必定会有
  setup(props) {
    const state = reactive({
      name: "lxs"
    });
    
    return {
      ...toBindings(state)
    }
  }
}
复制代码
  • 尤大大指出,setup 函数里面可使用 this 获取到当前上下文,但不必用。因此liximomo的vue-function-api版本,setup 函数里面的 this 为 undefined,并且不只仅有 props 参数,还有 context 参数,里面有如下参数:github

    attrs: Object
    emit: ƒ ()
    parent: VueComponent
    refs: Object
    root: Vue
    slots: Objectweb

export default {
  props: {
    visible: {
      type: Boolean,
      default: false
    }
  },
  
  setup(props, { attrs, emit, parent, refs, root, slots }) {
    return {}
  }
}

复制代码

2. Binding函数 -> value

binding()返回的是一个包装对象(value wrapper), 里面只有一个 .value 属性,该属性指向内部被包装的值。算法

import { reactive, binding, isBinding, toBindings } from "vue";

export default {
  setup() {
    const name = binding("lxs");
    
    console.log(name);
    // ValueWrapper {
    // value: Object
    // _internal: {__ob__: Observer}
    // __proto__: AbstractWrapper
    // } 
    
    const state = reactive({
      name: "test"
    })
    
    const changeName = () => {
      if (isBinding(name)) {
        name.value = "new lxs";
      }
    }
    
    return {
      ...toBindings(state),
      name,
      changeName
    }
  }
}
复制代码

binding函数附带了两个功能性函数:vue-router

isBinding: 判断是否为包装对象
toBindings: 将原始值转化成包裹对象
复制代码

这里引起两个思考

为何须要包装对象?
  • 咱们知道在 JavaScript 中,原始值类型如 string 和 number 是只有值,没有引用的。若是在一个函数中返回一个字符串变量,接收到这个字符串的代码只会得到一个值,是没法追踪原始变量后续的变化的。

  • 所以,包装对象的意义就在于提供一个让咱们可以在函数之间以引用的方式传递任意类型值的容器。这有点像 React Hooks 中的 useRef —— 但不一样的是 Vue 的包装对象同时仍是响应式的数据源。有了这样的容器,咱们就能够在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展现(追踪依赖),组合函数负责管理状态(触发更新)

  • 声明数据和更新数据更加清晰。

在template下时候须要使用.value来展开数据?
  • 否。虽然在 setup() 中返回的是一个包装对象,可是在模版渲染的过程或者嵌套在另外一个包装对象的时候,若是判断类型为包装对象,都会被自动展开为内部的值。

3. Reactive函数 -> state

  • reactive() 返回一个没有包装的响应式对象,等同于 vue 2.6 版本之后的 Vue.observable() 函数。Vue.observable() 提供了让 data 块里面的值可以在外面定义的功能。
import { reactive } from 'vue'

const object = reactive({
  count: 0
})

object.count++
复制代码

当一个包装对象被做为另外一个响应式对象的属性引用的时候也会被自动展开:

const count = binding(0)
const obj = reactive({
  count
})

console.log(obj.count) // 0

obj.count++
console.log(obj.count) // 1
console.log(count.value) // 1

count.value++
console.log(obj.count) // 2
console.log(count.value) // 2

复制代码
  • 以上这些关于包装对象的细节可能会让你以为有些复杂,但实际使用中你只须要记住一个基本的规则:只有当你直接以变量的形式引用一个包装对象的时候才会须要用.value 去取它内部的值 —— 在模版中你甚至不须要知道它们的存在。

4. Computed value

除了直接包装一个可变的值,咱们也能够包装经过计算产生的值:

import { binding, computed } from "vue";

import dayjs from "dayjs";
export const timeHook = () => {
   const time = binding(new Date().getTime());
   
   const formatTime = computed(() => time.value, val => {
     return dayjs(val).format("YYYY-MM-DD hh:mm:ss");
   });
   
   return {
       time,
       formatTime
   }
}
复制代码
  • 计算值的行为跟计算属性 (computed property) 同样:只有当依赖变化的时候它才会被从新计算。

  • computed()返回的是一个只读的包装对象,它能够和普通的包装对象同样在 setup()中被返回 ,也同样会在渲染上下文中被自动展开。默认状况下,若是用户试图去修改一个只读包装对象,会触发警告。

  • 双向计算值能够经过传给computed 第二个参数做为 setter来建立:

import { binding, computed } from "vue";
import dayjs from "dayjs";

export const timeHook = () => {
   const time = binding(new Date().getTime());
   
   const formatTime = computed(
     // read
     () => time.value + 2000, 
     // write
     val => {
     return dayjs(val).format("YYYY-MM-DD hh:mm:ss");
   });
   
   return {
       time,
       formatTime
   }
}
复制代码

5. Watchers

watch()函数与旧API的 $watch同样提供了观察状态变化的能力。它的做用相似React Hooks 的 useEffect,但实现原理和调用时机其实彻底不同。

不一样之处:

1. 它能够接收多种数据源:
一个返回任意值的函数
一个包装对象
一个包含上述两种数据源的数组
复制代码
2. watch()函数的回调,会在建立时就执行一次,至关于2.x的 watcher 的immediate: true
3. 默认状况下,watch()的回调在触发时,DOM总会在一个更新过的状态。
export default {
  props: {
    tableHeight: {
      type: [String, Number],
      default: 200
    }
   },
   
  setup(props) {
    watch(
      () => props.tableHeight, 
       val => {
        console.log("DOM render flush")
      }, 
      {
        flush: 'post', // default, fire after renderer flush
        flush: 'pre', // fire right before renderer flush
        flush: 'sync' // fire synchronously
      }
    )    
  }
}

复制代码
4. 观察多个数据源
  • 任意一个数据源发生变化时,回调函数都会被触发
watch(
  [valueA, () => valueB.value],
  ([a, b], [prevA, prevB]) => {
    console.log(`a is: ${a}`)
    console.log(`b is: ${b}`)
  }
)

复制代码
5. 中止观察
const stop = watch(...)

// stop watching
stop()
复制代码
  • 若是 watch()是在一个组件的setup() 或是生命周期函数中被调用的,那么该 watcher 会在当前组件被销毁时也一同被自动中止,不然将要本身自动中止。
6. 清理反作用
  • 有时候当观察的数据源变化后,咱们可能须要对以前所执行的反作用进行清理。举例来讲,一个异步操做在完成以前数据就产生了变化,咱们可能要撤销还在等待的前一个操做。为了处理这种状况,watcher 的回调会接收到的第三个参数是一个用来注册清理操做的函数。调用这个函数能够注册一个清理函数。清理函数会在下属状况下被调用,咱们常常须要在 watcher 的回调中用async function 来执行异步操做:

    在回调被下一次调用前
      在 watcher 被中止前
    复制代码
watch(idValue, (id, oldId, onCleanup) => {
  const token = performAsyncOperation(id)
  onCleanup(() => {
    // id 发生了变化,或是 watcher 即将被中止.
    // 取消还未完成的异步操做。
    token.cancel()
  })
})
复制代码
const data = value(null)
watch(getId, async (id) => {
  data.value = await fetchData(id)
}

复制代码

生命周期函数

  • 全部现有的生命周期钩子都会有对应的 onXXX 函数(只能在setup() 中使用)-> destroyed 调整为 unmounted
import { onMounted, onUpdated, onUnmounted } from 'vue'

const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    // destroyed 调整为 unmounted
    onUnmounted(() => {
      console.log('unmounted!')
    })
  }
}
复制代码
  • 现有的 vue-function-api是在 install 里面 设置Vue.config.optionMergeStrategies.setup,让 setup 函数在beforeCreate 中注入。
// setup.ts
Vue.mixin({
  beforeCreate: functionApiInit,
});

复制代码

依赖注入

// hook.js
import { provide, binding } from 'vue'

export const randomKeyHook = () => {
  const randomKey = binding(
    Math.random()
      .toString(36)
      .substr(2)
  )

  provide({
    randomKey
  })

  return {
    randomKey
  }
}
复制代码
import { inject } from 'vue';

export default {
  setup() {
  
    const randomKey = inject('randomKey')
    return {
      randomKey
    }
  }
}
复制代码
  • 若是注入的是一个包装对象,则该注入绑定会是响应式的

Typescript支持

正确类型推导

  • 3.0 的一个主要设计目标是加强对 TypeScript 的支持。本来vue团队指望经过Class API 来达成这个目标,可是通过讨论和原型开发,vue团队认为 Class 并非解决这个问题的正确路线,基于 Class 的 API 依然存在类型问题。

  • 基于函数的 API 自然对类型推导很友好,由于TS对函数的参数、返回值和泛型的支持已经很是完备。更值得一提的是基于函数的 API 在使用 TS 或是原生 JS 时写出来的代码几乎是彻底同样的。

三. 调整

标准版剔除如下选项

  1. el (应用将再也不由 new Vue()来建立,而是经过新的 createApp 来建立,)
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
复制代码
  1. 疑似要废除.sync, 添加 v-model 指令参数

可替代项

data(由 setup() + binding) + reactive) 取代)
computed(由 computed 取代)
methods( 由 setup() 中声明的函数取代)
watch (由 watch() 取代)
provide/inject(由 provide() 和 inject() 取代)
mixins (由组合函数取代)
extends (由组合函数取代)
复制代码
  • 全部的生命周期选项 (由 onXXX 函数取代)

四. vue-router 的 Functional API 猜测

以依赖注入的形式

// main.ts
import { provideRouter, useRouter } from 'vue-router'
import router from './router'

new Vue({
  setup() {
    provideRouter(router)    
  }
})

复制代码
// ... in a child component
export default {
  setup() {
    const { route /*, router */ } = useRouter()
    const isActive = computed(() => route.name === 'myAwesomeRoute')
    return {
      isActive
    }
  }
}
复制代码

以hook的形式导给上下文

import router from './router'

new Vue({
  setup() {
    router.use()
  }
})

复制代码

五. vuex 的 Functional API 猜测

采用函数转化成module

// useState, useGetter, useMutation, useAction, useModule 
import { useModule } from 'vuex'
import { value, computed } from 'vue';
export const itemsModule = () => {
    const items = value([])
    const size = computed(() => items.value.length);

    const addItem = (content) => {
        items.value.push(content)
    }
    return {
        items,
        size,
        addItem
    }
}

useModule(itemsModule)
复制代码

效仿nuxt.js,在vue-cli4中提供统一的 store 目录,若是store 目录存在,程序将本身作如下事情

引用 vuex 模块
将 vuex 模块 加到 vendors 构建配置中去
设置 Vue 根实例的 store 配置项
复制代码

六. 和React的区别

  • 尤大大文章刚出的时候,不少技术帖子对于 vue3.0 的语法更新表示不满,他们表示,若是是大规模模仿react,倒不如去学习 react,这样也是学习新的语法。

  • 这样大错特错,vuereact 的实现有很大的差异, vue 实际上模仿的是hooks,而不是 react

Template 机制仍是没变

  • vue仍是将 TemplateSetup 分开,react 则是写在了一块儿。

减小 GC 压力

  • vuesetup 函数只在初始化以前执行一次,而 React 在更新数据的时候,都会执行一次 render

  • vuehooksmutable 深度结合,在数据更新的时候,经过包装对象的 obj.value,在obj的数据变动的时候,引用保持不变,只有值改变了,vue 经过 新的API Proxy 监听数据的变化,能够作到 setup 函数不从新执行。而Template 的重渲染则彻底继承 Vue 2.0的依赖收集机制,它无论值来自哪里,只要用到的值变了,就能够从新渲染了。

  • React Hooks 存在的问题:全部Hooks都在渲染闭包中执行,每次重渲染都有必定性能压力,并且频繁的渲染会带来许多闭包,虽然能够依赖 GC 机制回收,但会给 GC 带来不小的压力。

v8的垃圾回收算法

  从宏观上来看,V8的堆分为3部分,年轻分代/年老分代/大对象空间。对于各类类型,v8有对应的处理方式:

  1. 年轻分代(短暂)

 分为两堆,一半使用,一半用于垃圾清理。在堆中分配而内容不够时,会将超出生命期的垃圾对象清除出去,释放掉空间。

  2. 年老分代 
 主要类型:

 (1) 从年轻分代中移动过来的对象
 (2) JIT (即时编辑器) 以后产生的代码
 (3) 全局对象

 标记清除和标记整理算法将可清除的垃圾对象和有效的对象区分开来,但超过度配给年老分代但空间时,V8会清除垃圾代码,造成了碎片的内存块,而后压缩内存块。固然,这个过程是走走停停的。(上述问题)

  3. 大对象空间

 整块分配,一次性回收。
复制代码

七. 总结

  • vue3.0 对 vue 的主要3个特色:响应式、模板、对象式的组件声明方式,进行了全面的更改,底层的实现和上层的api都有了明显的变化,基于 Proxy从新实现了响应式,基于 treeshaking 内置了更多功能,提供了类式的组件声明方式。并且源码所有用 Typescript 重写。以及进行了一系列的性能优化。

  • 取其精华,去其糟糠, vue团队但愿剔除掉代码灵活性差的帽子,大胆的改变了原有的开发模式,更加亲和于 vue生态javascript 生态。vue一直在不断的进步,让开发者减小框架的约束,放飞开发者的思想。

  • 感谢 vue 团队一向的高出产率~

八. 观察

2019-07-29在github上的截的路线图

1. vue对兼容模式的开发目前完成了80%;
2. 还没有进行破坏性更新的讨论;
3. vue3.0会伴着vue-cli4的出现 -> webpack5;
4. 各生态目前正在同步更新;
5. Q3季度将要开始vue3.0的测试;
等
复制代码

期待吧~

参考文章:

尤雨溪 - Vue Function-based API RFC (https://zhuanlan.zhihu.com/p/68477600)
黄子毅 - 精读《Vue3.0 Function API》(https://zhuanlan.zhihu.com/p/71667382)
vuejs - rfcs (https://github.com/vuejs/rfcs/issues)
复制代码

ps: 我的公众号求加个阅读量哈,二维码以下:

相关文章
相关标签/搜索