Vue3.0数据响应式原理

基于Vue3.0发布在GitHub上的初版源码(2019.10.05)整理html

预备知识

  • ES6 Proxy,整个响应式系统的基础。
  • 新的composition-API的基本使用,目前尚未中文文档,能够先经过这个仓库(composition-api-rfc)了解,里面也有对应的在线文档。

先把Vue3.0跑起来

先把vue-next仓库的代码clone下来,安装依赖而后构建一下,vue的package下的dist目录下找到构建的脚本,引入脚本便可。 下面一个简单计数器的DEMO:vue

<!DOCTYPE html>
<html lang="en">
<body>
  <div id='app'></div>
</body>
<script src="./dist/vue.global.js"></script>
<script> const { createApp, reactive, computed } = Vue; const RootComponent = { template: ` <button @click="increment"> Count is: {{ state.count }} </button> `, setup() { const state = reactive({ count: 0, }) function increment() { state.count++ } return { state, increment } } } createApp().mount(RootComponent, '#app') </script>
</html>
复制代码

template和以前同样,一样Vue3也支持手写render的写法,templaterender同时存在的状况,优先renderreact

setup选项是新增的主要变更,顾名思义,setup函数会在组件挂载前(beforeCreatecreated生命周期之间)运行一次,相似组件初始化的做用,setup须要返回一个对象或者函数。返回对象会被赋值给组件实例的renderContext,在组件的模板做用域能够被访问到,相似data的返回值。返回函数会被当作是组件的render。具体能够细看文档。git

reactive的做用是将对象包装成响应式对象,经过Proxy代理后的对象。github

上面的计数器的例子,在组件的setup函数中,建立了一个响应式对象state包含一个count属性。而后建立了一个increment递增的函数,最后将stateincrement返回给做用域,这样template里的button按钮就能访问到increment函数绑定到点击的回调,count也能显示在按钮上。咱们点击按钮,按钮上的数值就能跟着递增。web

下面切入正题,咱们就来探究下按钮上count值跟着响应式更新的原理api

数据结构

首先列一下主要的一些数据结构,先列在这里,后面提到能够翻回来看看。缓存

ReactiveEffect 一个Function对象,用于执行组件的挂载和更新。数据结构

interface ReactiveEffect {
  (): any
  isEffect: true
  active: boolean
  raw: Function // 具体执行的函数
  deps: Array<Dep>
  computed?: boolean
  scheduler?: (run: Function) => void
  onTrack?: (event: DebuggerEvent) => void
  onTrigger?: (event: DebuggerEvent) => void
  onStop?: () => void
}
复制代码

targetMap 相似 {target -> key -> dep}的一个Map结构,用于缓存全部响应式对象和依赖收集。app

export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()
复制代码

Proxy代理拦截

reactive函数执行,会将传入的target对象经过Proxy包装,拦截它的getset等,并将代理的target缓存到targetMaptargetMap.set(target, new Map())

代理的get的时候会调用一个track函数,而set会调用一个triger函数。分别对应依赖收集和触发更新。

// Proxy get 简化
function get(target: any, key: string | symbol, receiver: any) {
  // 经过key拿到原始值res
  const res = Reflect.get(target, key, receiver)
  // 过滤不须要代理的状况
  // ...
  // 依赖收集
  track(target, OperationTypes.GET, key)
  // 若是取到的值是个对象,将对象再代理包装一下
  // Proxy只能代理对象第一层级
  return isObject(res) ? reactive(res) : res
}

// Proxy set 简化
function set( target: any, key: string | symbol, value: any, receiver: any ): boolean {
  // 一些不须要代理设置的场景
  // ...

  // 设置原始对象的值
  const result = Reflect.set(target, key, value, receiver)
  // 避免重复trigger的逻辑
  // ...
  // 触发通知更新
  trigger(target, '更新的类型, 新增key或更新key', key)
  return result
}
复制代码

依赖收集和触发更新

组件在render阶段,视图会读取数据对象上的值进行渲染,此时便触发了Proxyget,由此触发对应的track函数,记录下了对应的ReactiveEffect,也就是常说的依赖收集。 ReactiveEffect其实就能够看做是组件的更新(mount是特殊的update),数据的变动触发triggertrigger遍历调用track收集的对应的数据的ReactiveEffect,也就是对应有关联的组件的更新。

trigger触发的组件的更新,在render阶段又触发了新一轮的track依赖收集,更新依赖。

// 简化的 track
function track( target: any, type: OperationTypes, key?: string | symbol ) {
  // 只有在依赖收集阶段才进行依赖收集
  // 除了render,其余场景也可能会触发Proxy的get,但不须要进行依赖收集
  // activeReactiveEffectStack栈顶包装了当前render的组件的mount和update的逻辑
  const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
  // 若是effect为空,说明当前不在render阶段
  if (effect) {
    // ...
    // =====>初始化对应{target -> key -> dep}的结构
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key as string | symbol)
    if (!dep) {
      depsMap.set(key as string | symbol, (dep = new Set()))
    }
    // <=====初始化对应{target -> key -> dep}的结构
    // 依赖列表里若是没有,add
    if (!dep.has(effect)) {
      // 这里将effect做为依赖,缓存到依赖列表
      dep.add(effect)
      effect.deps.push(dep)
    }
  }
}

// 简化的trigger
function trigger( target: any, type: OperationTypes, key?: string | symbol, extraInfo?: any ) {
  // 获取对应target在track过程当中缓存的依赖
  const depsMap = targetMap.get(target)

  const effects: Set<ReactiveEffect> = new Set()
  // 省略分类逻辑
  depsMap.forEach(dep => {
    // 将effect分类过滤添加到effects
  })
  
  const run = (effect: ReactiveEffect) => {
    // 有个异步调度的过程,nextTick
    scheduleRun(effect, target, type, key, extraInfo)
  }

  effects.forEach(run)
}

复制代码

大体流程:

vue3_reactive

总结

如今的代码只有新特性的实现,并且ES6+TS的组合可读性大大提升,编辑器支持也很好,因此相对会好读不少。这里只是简单的理了一下vue 3.0 reactive的总体流程,细节还有不少地方值得学习,继续加油。

/** PS:咱们团队还缺人,有想换工做的小伙伴能够发简历给我哦, base深圳,funkyfang(艾特)webank.com。 */

复制代码
相关文章
相关标签/搜索