Vue Latest

基本介绍

Vue 是一个让然充满惊喜的框架,也是目前国内使用人数最多的前端框架。

在 vue 3 之后,写出的程序,很多时候已经和 react 看不出明显的区别。

例如下面这段程序:

type HelloWorldProps = {
	msg: string
}
export default ({ msg }: HelloWorldProps) => {
	return <h1>{ msg }</h1>
}

// babel/ts 支持 JSX
// JSX 并不意味着 react,JSX 是 HTTP 0.9 就设计出来的 HTML
// 程序语言本身扩展性是最好的
tsx

react 和 vue 并不决定你的前端架构,它只是作为一个工具去支持你的渲染。MVVM 和 函数式都是前端框架中重要的组成部分。

特性解读

vue 3,即 vue-next。

vue 3 可以说是对 vue 的程序应该如何写,重新下了定义:

  • JSX
  • Typescript
  • Composition API
  • reactivity

上面这几个更新并不全是 vue3 带来的,但是我们可以放到一起分析,算是对 vue-next 程序的定义。

why not sfc

可能有人会问?SFC 不香嘛?

首先我们来看一下,在 TS 环境下,SFC 需要一个 shim 文件:

declare module "*.vue" {
  import { DefineComponent } from 'vue'
  const Component: DefineComponent
  export default Component
}
tsx

declare 的作用:告诉 TypeScript 编译器 declare 的部分在源代码之外提供,不需要编译器处理。当遇到 *.vue 文件的时候,TS 编译时会将他们当作一个会 export default Component 的类型。

如果用 tsx 写,就不需要这个 shim。不过多一个 shim 少一个 shim 重要吗?

从架构角度来说很重要。通常你的项目概念越多,意味着设计越差。

在 SFC 中编写 template 和 script 标签,这个方式有两个缺点:

  • 不够灵活
    • 需要 v-show/v-if/v-for 等;
    • 关注点被分离(模板也好、script 也好,都是解决某个关注点的一部分,在 SFC 中被强行分离);
  • ts 类型检查
    • 函数组件可以在最大程度作用 TS 的类型检查(比如属性检查)

我们来看一个 vue 的计时器程序:

function useCounter(): [Ref<number>, () => void] {
  const counter = ref(0)
  
  function increment() {
  	counter.value++
  }
  
  return [counter, increment]
}

export default {
  setup() {
    const [counter, increment] = useCounter()
    return () => <div>
    	count is: {counter.value}
      <button onClick={increment}add></button>
    </div>
  }
}
tsx

在这个程序中,我们可以看到:

  • counter 逻辑的集中管理;
  • 强大的封装能力;
  • 少量的记忆要求。

compostion api

composition api 是一系列函数式 API 的合集。

有用来初始化的、定义组件的:

  • setup
  • defineComponent

有支持响应式数据的:

  • ref
  • reactive
  • toRefs
  • computed
  • watch
  • watchEffect

有支持生命周期管理的:

  • onMounted
  • onUnmounted

总结来说,Composition API:

  • 提升组合能力(自定义的 Composition API)
  • 提供 Reactive Programming
  • 提供函数式(简化 API 设计)

vue 3.0 性能

Vue3 并没有很很明显的提升性能,这个和 vue3 的渲染机制有关(没有时间切片)。

在一些大型前端系统,没有时间切片会导致页面打开执行周期过久。

Reactivity

R eactivity 是 Vue3 提供的核心能力,配合函数式的 Composition API 使用非常方便。

响应式编程

Reactive Programming - 让类型自发的响应环境的变化。

**Reactive:**一个值是 Reactive,那么这个值可以被监听;一个对象是 Reactive,那么这个对象可以被监听。一个函数是 Reactive,那么这个函数在提供 Reactive 的能力,比如创造一个 Reactive 的值或者对象。

Be Reactive !!

让程序变的 Reactive 是很好的一个思路。程序如果不是 Reactive,那么往往是 Passive(被动的)。响应的的反义词为什么是被动?因为 Reactive 代表一部分程序(类型)主动的去通知周边自己做了什么,另一部分类型主动监听变化,主动做出判断并完成操作。

程序变的 Reactive 之后,每个模块好像就活了一样,不需要程序主动下命令,而是程序主动完成工作。从这个角度来看 Reactive 的反义词就是 Passive。

声明式(Declarative)

Reactive 的程序往往就是声明式的。所谓声明式,就是程序员的声明要做什么?不重要做什么?而不是写一大堆计算逻辑。

声明式需要更好的封装:

const arr = []
for (let i = 0; i < 1000; i++) {
	arr[i] = i  
}

// 声明式
const arr = range(0, 1000)
typescript

声明式会创造语言:

const div = document.createElement('div')
div.style.width = 100

// 声明式
const div = <div style={{ width: 100 }} />
typescript

创造这个词并不够贴切。

声明式往往是 Reactive:

export default {
  setup() {
    const logined = ref(userStore.logined())
    
    useStore.on(e) {
      switch (e.type) {
        case 'logined':
          logined.value = true
          break
        case 'logout':
          logined.value = false
          break
      }
    }
    
    return () => {
      if (logined.value) {
        return <div>您好,欢迎光临</div>
      } else {
        return <div>请登录...</div>
      }
    }
  }
}
tsx

在上面的程序中组件在响应全局用户状态的变化,这样就将组件和全局状态解耦:

  • 组件迭代不用关心 useStore 迭代;
  • useStore 迭代,组件不需要调整。

其实上面代码还可以做优化,可以实现业务逻辑和组件的强分离:

function useLoginStatus() {
 	const logined = ref(userStore.logined())
    
  useStore.on(e) {
    switch (e.type) {
      case 'logined':
        logined.value = true
        break
      case 'logout':
        logined.value = false
        break
    }
  }
  
  return login.value
}

export default  {
  setup() {
    const loggedIn = useLoginStatus()
    return () => <LoginStatus loggedIn={loggedIn} />
  }
}

const LoginStatus = ({ loggedIn }) => {if 
tsx

还有一个最大的好处就是声明式可以帮助我们更好的阅读代码。

最后也是最重要的、上升到哲学高度的思考:Reactive 让程序模块自己懂得如何做事。

总结

从前端看:如何理解 Reactivity?和 React 是一个词义吗?

从哲学角度去看,Reactivity 就是让程序知道如何自主做事,应该怎样去做。

个人认为 React 在设计之初就是期望程序可以 Reactivity,所以可以认为是一个词义。

组合能力是什么?Compostion API 提供更好的组合能力,这样说对吗?

组合能力其实就是最大能力的复用,即 “搭积木” 能力。这样肯定是对的。

Ref 和 Reactive

一个值如果是 Reactive,那么这个值应该:

  • 可以通知(trigger)
    • vue 更新
    • vue 做其他标准行为
    • 完成自定义行为
  • 可以监听(track)
    • vue 发生的变化(依赖)

ref(referece)

ref 是一个工厂方法,本质是创造一个 Ref 对象。ref 的目标是管理值。

首先,ref 可以像一个正常值一样被使用:

import { ref } from 'vue'

export default {
  setup() {
    const msg = ref('hello')
    return () => {
      return <div>{msg.value}</div>
    }
  }
}
tsx

在上面的程序中:

  • setup 是一个 vue3 新特性(初始化组件);
  • setup 可以返回一个 render 函数,render 函数返回的 VNode。

jsx 的语法 div 会被 babel 翻译成 createVNode。

ref 是值的代理

ref 是一个 setter 和 getter,例如下面这段代码就可以很好诠释 ref 的内部实现:

function createRef<T>(val: T) {
  let _val: T = val
  	
  const refObj = {
    set value(v: T) {
      console.log('setter called')
      _val = v
    },
    get value() {
      console.log('getter called')
      return _val
    }
  }
  return refObj
}

const a = createRef(0)
a.value = 10
a.value++ // 非原子操作,一次读写操作
console.log(a.value)
tsx

不过仅有 setter 和 getter 是不够的,还需要一些 reactive 的机制:

function trigger() {}
function track() {}

// ...

const refObj = {
  set value(v: T) {
    console.log('setter called')
    _val = v
    trigger()
  },
  get value() {
    console.log('getter called')
    track()
    return _val
  }
}
tsx

当 set 的时候 trigger,get 的时候 track。

  • trigger:驱动 vue 更新
  • track:跟踪 vue 更新
    • 一个 ref 可以给多个 vue 组件使用,因此依赖是不确定的;
    • 依赖(Deps)
      • vue 组件依赖 ref,因此是 ref 的依赖;
      • ref 的依赖应该是一个数组。
    • 为什么不能在 ref 的构造函数中确定依赖?
      • 构造函数和实际使用可能并不是一个位置,或同一个组件。
    • 为什么不自己操作依赖而是封装一个 track 方法?
      • 只有 get 的时候才需要操作依赖,实际时机很重要。
    • 为什么不是 vue 检查依赖,而是 ref track 更新依赖?
      • 发现能力更出色,更 Reactive。

ref 驱动更新示例

import { ref } from 'vue'

export default {
  setup() {
    const counter = ref(0)
    return () => (
    	<div>
      	{counter.value}
        <button onClick={() => counter.value++}>add</button>
      </div>
    )
  }
}
tsx

Reactive

Reactive 和 Ref 类似,都是代理模式的 Reactive 值。

代理一个值用 getter 和 setter 很方便,代理一个对象,js 提供了 Proxy 类。

如何代理一个对象

function createReactive(obj: any) {
  return new Proxy(obj, {
    get: (target, name, receiver) => {
      if (name ==== 'c') {
        return 'this is a proxy value'
      }
      return Reflect.get(target, name, receiver)
    },
    set: (target, name, value, receiver): boolean => {
      if (!(name in target)) return false
      Reflect.set(target, name, receiver)
      return true
    }
  })
}

const o = createReactive({
  a: 1,
  b: 2,
  foo: function() {
    console.log('a is', this.a)
  }
})

o.a = 100
console.log(o.c)

o.foo()
tsx

为什么用 Reflect.set、Reflect.get 而不用 target[name] 这种形式?

  • 可以在 getter 和 setter 间同步 receiver(this 指针)

比较坑的地方:

const p = new Proxy({
  a: 1
}, {
  get(target, property, receiver) {
    console.log('get trap', ...arguments)
    return Reflect.get(receiver, property, receiver)
  }
})
tsx

上面这段程序会造成栈溢出,因为 receiver 其实就是 proxy 代理对象,会造成循环读取。

Reactive 是一个代理对象

const state = reactive({
  counter: 0
})

state.counter++
tsx

上面的程序首先会触发代理对象的 getter,然后再触发 setter,因为 ++ 不是 atomic 原子操作。

具体实现和 ref 一致,Reactive 也会在 getter 中 track,在 setter 中 trigger。

Reactive 实现 Counter

import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({
      counter: 0
    })
    
    return () => {
      <div>
      	{state.counter}
        <button
          onClick={() => {
            state.counter++
          }}
        >
          add
        </button>
      </div>
    }
  }
}
tsx

Reactive vs Ref

它们都是 vue 提供的 reactive 值,Ref 维护一个值/对象,Reactive 维护一个对象的所有成员。

const obj = ref({
  a: 1,
  b: 2
})

obj.value.a++ // 不会触发重绘
obj.value = { ...obj.value, a: 1 } // 触发重绘

const obj1 = reactive({
  a: 1,
  b: 2
})
obj1.a++ // 触发重绘
tsx

有一个函数叫做 toRef,还有一个函数叫做 toRefs,这两个函数可以将值转换为 ref。

import { defineComponent, reactive, toRef, toRefs } from 'vue'

export default defineComponent({
  setup() {
    const state = reactive({
      a: 1,
      b: '-'
    })
    
    const aRef = toRef(state, 'a')
    const bRef = toRef(state, 'b')
    // =>
    // const refs = toRefs(state)
    // const aRef === refs.a
    // cosnt bRef === refs.b
    
    return () => {
      return <>
      	<h1>aRef: { aRef.value }</h1>
        <h1>aRef: { aRef }</h1>
        <h1>bRef: { bRef.value }</h1>
        <h1>bRef: { bRef }</h1>
      	<button onClick={() => state.a++}>state.a++</button>
      	<button onClick={() => aRef.value++}>aRef.a++</button>
      </>
    }
  }
})
tsx

Reactive 和 Ref 的类型

在 Reactive 和 Ref 中,通过 UnwrapRef 的定义配合 infer 关键字,可以做到两种类型合一。

因此从类型的角度来看 ref 和 reactive 是同一类东西。

export declare function ref<T>(value: T): Ref<UnwrapRef<T>>
export declare function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
 
export declare type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
 
// 拆包
export declare type UnwrapRef<T> = T extends Ref<infer V> ? UnwrapRefsSimple<V> : UnwrapRefSimple<T>
  
declare type UnwrapRefSimple<T> = 
	T extends
		Function |
		CollectionTypes |
		BaseTypes |
		Ref |
		RefUnwrapBailTypes[keyof RefUnwrapbBailTypes] ? 
			T :
			T extends Array<any> ?
				{
          [K in keyof T]: UnwraoRefSimple<T[K]>
        } : 
				T extends object ? UnwrappedObject<T>: T
tsx

总结

vue 3 提供的 Reactive 这种编程模式和之前 vue.observable 有什么区别?

其实是整体编程风格的转变。

const state = Vue.observable({ count: 0 })

const Demo = {
  render(h) {
    return h('button', {
      on: {
        click: () => { state.count++ }
      },
      `count is: ${ state.count }`
    })
  }
}
tsx

什么是 observable?reactive is observable?

  • 可以被观察到的(observable);
    • rxjs 中一个 observable 同时依然是 observer。
  • 可以观察别人(observer)。