Vue3答疑解惑

vue3做了哪些优化

1. 项目代码管理层面

采用monorepo的代码管理方式,代码各个模块间的依赖关系更明确,且各模块可单独被开发者引用。比如,vue3的响应式模块@vue/reactivity,是可以单独使用的,如果开发者只想用这块的代码,不想用别的代码,可以只引用这个包。

2. 更好的TypeScript支持

无论是源码层面,还是使用者开发项目层面,都全面拥抱的TypeScript

3. 性能优化

  • 源码体积的优化
  • tree-shaking
  • 响应式优化

vue2的Object.defineProperty(data, key, {})的缺点

(1)对于嵌套层级很深的对象,需要循环遍历每一层的对象的key,把其都变成响应式的,这是非常大的性能开销

(2)不能监听对象key的新增和删除,要用vue2提供的hack方法去实现响应式更新

vue3的new Proxy(data, {}),因为监听的整个data对象,所以能很好的解决监听不到属性新增和删除的问题。但是,需要注意的是,Proxy API并不能监听到内部深层次的对象变化,因此vue3的处理方式是在getter中去递归响应式,这样的好处是,真正访问到的内部对象,才会去递归响应式,而不是无脑递归,这在很大程度上提升了性能

4. 编译优化

通过编译,优化虚拟DOM比对过程。如下,是vue3的tempalte compiler编译template的结果。大家可以自己在Vue template Explore这个网站自己体验一下。
截屏2021-09-09 下午10.38.20.png

截屏2021-09-09 下午10.39.03.png

  1. 对所有的节点做了动态节点与静态节点的区分,只对比动态节点。

block tree是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,每个区块只需要一个Array来追踪自身包含的动态节点。

<span :text="ss">static</span>
复制代码
_createElementVNode("span", { text: _ctx.ss }, "static", 8 /* PROPS */, _hoisted_2),
复制代码
  1. 新增了hoistStatic静态节点提升的功能
  2. 对事件监听函数做了缓存优化

5. 语法层面优化,提供了composition API

  1. 有利于组件逻辑的关注点分离,代码的逻辑复用
  2. 解决mixins存在的可能造成命名冲突,引用来源不清晰等问题

composition API和options API

这两者是共存的,setup是对options API的一个更好的补充,如果你想在一个组件中整合一些逻辑。setup完全有它自己的一方小世界,当他return一个data出去后,这个data对象可以被整个组件用到。

composition API适合大型的复杂组件,如果你的组件比较简单,直接用vue2 的options API即可。

setup中 watch、watchEffect的区别

首先我们来看一下watch的用法,与options API的用法非常相似,当count的值发生变化后,才会调用。但是,有一种极端的情况是,count经过一系列操作后,最终值与初始值相同,就不会触发watch。

const state = reactive({
    count: 0
})
const count = ref(0)
const plusOne = computed(() => state.count + 1)
watch([() => state.count, count, plusOne], ([newCount, newCount2, newPlusOne], [oldCount, oldCount2, oldPlusOne]) => {})
复制代码

watchEffect的用法如下:

const state = reactive({
    count: 0
})
watchEffect(() => {
    console.log(state.count)
})
state.count++
复制代码

首先会立即执行一次watchEffect里面的副作用函数,用于给其中的响应式依赖对象收集副作用函数。然后,每次响应式对象被赋值,都会触发副作用函数的调用。

说到,watchwatchEffect的区别,我有一点自己的理解。watchEffect的触发机制更像computed的触发机制,但是与computed的作用不同。computed更倾向于返回一个实时计算的属性值,而wathcEffect不会有返回值,只是单纯的针对副作用函数中的响应式对象变更后,会触发该副作用函数。

以前,有很多人在computed里面写监听多个响应式对象变动的代码,而并不是想真正返回一个computed对象。我总觉得这有点骚操作的感觉,而watchEffect的出现正好是对这方面功能需求的一个补充。

下面给一个demo,当propsid,或者propsname参数发生变化时,都要重新请求接口,更新页面:

vue2的写法是:

props: {
    name: {
        type: String
    },
    id: {
        type: Number
    }
},
methods: {
    getNews() {
        fetch(url, {id: this.id, name: this.name}).then(() => {
            ...
        })
    }
},
watch: {
    id(newValue) {
        this.getNews()
    },
    name(newValue) {
        this.getNews()
    }
},
created() {
    this.getNews()
}
复制代码

而vue3,只需要写一个watchEffect就行了

props: {
    name: {
        type: String
    },
    id: {
        type: Number
    }
},
setup(props) {
    watchEffect(() => {
        fetch(url, {id: props.id, name: props.name}).then(() => {
            ...
        })
    })
}
复制代码

一个组件如何重新渲染的

  1. watchEffect用于依赖收集
  2. 获取组件的vnode
  3. 渲染组件的新的vnode
  4. 对比vnode更新
watchEffect(() => {
    const oldTree = component.vnode
    const newTree = component.render.call(renderContext)
    patch(oldTree, newTree)
})
复制代码

为什么要重写mount和createApp

为了支持跨多端的渲染器,不单单只是web端,比如webgl的渲染器,甚至小程序的渲染器。因此,mountcreateApp抽象出与平台无关的部分,针对web端,就要再重新封装一次函数。

@vue/runtime-dom是与web平台有关的包,即web平台,而@vue/runtime-core是与平台无关的。所以在@vue/runtime-dom中,对mountcreateApp进行了平台特性的封装。

// @vue/runtime-dom
let renderer
function ensureRenderer () {
    return (
        renderer || 
        // 此处的rendererOptions是与DOM操作相关的API:insert,remove,createElement,createText等
        (renderer = createRenderer(rendererOptions))
    )
}
export const createApp = (...args) => {
    // 确定渲染器
    const app = ensureRenderer().createApp(...args)
    const { mount } = app
    // 这个mount函数就包含了浏览器相关的API
    app.mount = (containerOrSelector) => {
        const container = normalizeContainer(containerOrSelector)
        if (!container) return
        // 清空container里面的DOM元素
        container.innerHTML = ""
        // 此处对实例的instance.ctx进行了代理
        const proxy = mount(container, false, container instanceof SVGElement)
        return proxy
    }
    return app
}
复制代码

vnode

vnode的类型:

export type VNodeTypes =
  | string
  | VNode
  | Component // 组件标签
  | typeof Text // 纯文本
  | typeof Static // 静态节点
  | typeof Comment // 注释
  | typeof Fragment
  | typeof TeleportImpl
  | typeof SuspenseImpl
复制代码

一个vnode可包含哪些参数

export interface VNode<
  HostNode = RendererNode,
  HostElement = RendererElement,
  ExtraProps = { [key: string]: any }
> {
  __v_isVNode: true
  [ReactiveFlags.SKIP]: true
  type: VNodeTypes
  props: (VNodeProps & ExtraProps) | null
  key: string | number | symbol | null
  ref: VNodeNormalizedRef | null
  scopeId: string | null
  slotScopeIds: string[] | null
  children: VNodeNormalizedChildren
  component: ComponentInternalInstance | null
  dirs: DirectiveBinding[] | null
  transition: TransitionHooks<HostElement> | null
  // DOM
  el: HostNode | null
  anchor: HostNode | null // fragment anchor
  target: HostElement | null // teleport target
  targetAnchor: HostNode | null // teleport target anchor
  /**
   * number of elements contained in a static vnode
   * @internal
   */
  staticCount: number

  // suspense
  suspense: SuspenseBoundary | null
  /**
   * @internal
   */
  ssContent: VNode | null
  /**
   * @internal
   */
  ssFallback: VNode | null

  // optimization only
  shapeFlag: number
  patchFlag: number
  /**
   * @internal
   */
  dynamicProps: string[] | null
  /**
   * @internal
   */
  dynamicChildren: VNode[] | null

  // application root node only
  appContext: AppContext | null

  /**
   * @internal attached by v-memo
   */
  memo?: any[]
  /**
   * @internal __COMPAT__ only
   */
  isCompatRoot?: true
  /**
   * @internal custom element interception hook
   */
  ce?: (instance: ComponentInternalInstance) => void
}
复制代码

举例说明,把如下节点改写成vnode

<custom-component msg="test">
    <h1></h1>
</custom-component>
复制代码

首先如上template会被编译成渲染函数:

import { createElementVNode as _createElementVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock } from "vue"

const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, null, -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_custom_component = _resolveComponent("custom-component")

  return (_openBlock(), _createBlock(_component_custom_component, { msg: "test" }, {
    default: _withCtx(() => [
      _hoisted_1
    ], undefined, true),
    _: 1 /* STABLE */
  }))
}

复制代码

运行render函数,生成vnode

{
    "__v_isVNode": true,
    "__v_skip": true,
    "type": "custom-component",
    "props": {
        "msg": "test"
    },
    "key": null,
    "ref": null,
    "scopeId": null,
    "slotScopeIds": null,
    "children": [{
        "__v_isVNode": true,
        "__v_skip": true,
        "type": "h1",
        "props": null,
        "key": null,
        "ref": null,
        "scopeId": null,
        "slotScopeIds": null,
        "children": null,
        "component": null,
        "suspense": null,
        "ssContent": null,
        "ssFallback": null,
        "dirs": null,
        "transition": null,
        "el": null,
        "anchor": null,
        "target": null,
        "targetAnchor": null,
        "staticCount": 0,
        "shapeFlag": 1,
        "patchFlag": -1,
        "dynamicProps": null,
        "dynamicChildren": null,
        "appContext": null
    }],
    "component": null,
    "suspense": null,
    "ssContent": null,
    "ssFallback": null,
    "dirs": null,
    "transition": null,
    "el": null,
    "anchor": null,
    "target": null,
    "targetAnchor": null,
    "staticCount": 0,
    "shapeFlag": 17,
    "patchFlag": 0,
    "dynamicProps": null,
    "dynamicChildren": [],
    "appContext": null
}
复制代码

setup 写法偏爱

尤雨溪更喜欢用ref而不是reactive的原因:

  • reactive生成的data被解构引入后,响应式的功能就消失了。为了解决这个问题,在导出时,必须要使用toRefs转换reactive生成的data。此处同理处理其他响应式对象,如props的解构问题。
const state = reactive({
    x: 0,
    y: 0
})

return {
    ...toRefs(state)
}

复制代码
  • ref能更方便的移动组织代码,因为他是单个单个的,而不是一个整体的reactive对象
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享