1. 首页

Vue3源码分析——数据侦测

本文同步在个人博客shymean.com上,欢迎关注

Vue3.0发布beta版本了,还是来凑个热闹看看源码。本系列大概会有两篇文章,包括应用整体流程、新的响应式系统、组合式API相关内容。

Vue的一个特点就是数据响应式系统,由于这是一个比较独立的系统,因此我们先从这里开始,看看新的Proxy是如何实现数据依赖收集和通知的。

本文内容有点长,可以按目录分章节阅读。

参考:

预备知识

在学习之前需要了解一些JavaScript预备知识:ReflectProxy

Reflect

Reflect封装一些对象的操作,主要是为了整合之前JS中存在的一些不太规范的地方

感觉Reflect的作用主要是把一些零散的工具函数整合在Reflect这个对象上,如indelete等运算符和Function.prototype.apply等API

一个比较有意思的功能是Reflect.get的第三个参数receiver


var a = { name: 'a', get greet(){ return 'hello ' + this.name } } console.log(a.greet); // "hi, I am a" var receiver = { name: 'receiver' } // receiver 修改类似于 通过函数的apply修改内部this指向一样,不过这里修改的是访问对象 // 通过这个功能,可以实现新对象借助原对象部分属性和方法的功能 var res = Reflect.get(a, 'greet', receiver); // "hi, I am receiver" console.log(res) // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

JavaScript Proxy

为了收集数据依赖、以及在数据变化时更新视图,Vue2通过defineProperty劫持set和get属性访问描述符。

defineProperty存在一些问题,最常见的问题就是无法监听对象以及数组动态添加的属性,即使Vue2重写了数组原型相关方法,但仍旧无法监听到arr[1]=xxx这种形式。

Vue3使用了ES6新增的Proxy接口来替代defineProperty。在本章节主要了解Proxy的基本使用、一些特殊用法和Proxy本身存在的缺点。

Proxy 对象用于定义基本操作的自定义行为


const p = new Proxy(target, handler) // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

其中handler对象是一个容纳一批特定属性的占位符对象,它包含有 Proxy 的各个捕获器trap,如setget`等

一些需要注意的细节

  • set方法应该返回一个布尔值,在严格模式下,如果set方法返回falsish(包括undefined、false等),会抛出异常,这些细节比较麻烦,可以通过Reflect来处理

  • 如果代理对象是数组,当调用pushpop等方法时,不仅会改变数组元素,也会改变length等属性,此时如果代理了set,则会被触发多次。


let arr = [100,200,300] let p = new Proxy(arr, { get(target, key) { return target[key] }, set(target, key, value, receiver) { console.log('set value', key) target[key] = value return true } }) p.push(400) // set value 3 第一次data[3] = 400 // set value length 第二次 data.length = 4 // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

这种多次触发handler的特性在某些场景下是冗余的,如果在set后会重新渲染视图,则多次set可能导致多次渲染(在不考虑flushQueue入队列去重的情况下)。

  • proxy只能代理一层

let o = {x:{y:100},z:10} let p = new Proxy(o, { get(target, key) { console.log('get value', key) return target[key] }, set(target, key, value, receiver) { console.log('set value', key) target[key] = value return true } }) p.x.y =100 // 只输出了get value x, 无法监听到set value // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

因此,当代理对象是多层嵌套结构时,需要开发者自己实现将嵌套的属性对象转换为Proxy对象


let handler = { get(target, key) { console.log('get value', key) return target[key] }, set(target, key, value, receiver) { console.log('set value', key) target[key] = value return true } } let x = {y:100} let o = {x:new Proxy(x, handler),z:10} let p = new Proxy(o, handler) p.x.y =100 // get value x // set value y // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

综上就是Proxy存在的一些问题

  • 代理数组时可能存在多次触发trap的情况,需要实现去重
  • 嵌套对象需要手动转换为Proxy,可以通过递归实现

TypeScript

Vue2使用flow进行类型检测,Vue3全面拥抱TypeScript,因此阅读源码需要具备一点TS相关知识,参考之前整理的

lerna

Vue3采用了lerna组织项目源码(目前很多大型项目都采用lerna了),之前写过一篇使用verdaccio和lerna和管理npm包,这里就不过多介绍了。

开发环境

接下来搭建源码开发环境


# 如果下载比较慢的话可以从gitee上面克隆备份仓库 git clone git+https://github.com/vuejs/vue-next.git # 安装依赖,需要一会 yarn install # 开始rollup watch npm run dev cd packages/vue/examples/ // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

examples下可以查看各种demo代码,这里我们直接选择composition目录下的代码查看新的语法,强烈建议在阅读源码之前先看看composition-api文档,这对了解Vue3中的API设计有非常重要的帮助!!

然后修改一下源码vue/src/index.ts,刷新页面,就可以看见改动后的效果,接下来就开始愉快的源码时间~

rective

文档里面展示了一个关于rective的基本用例

const { reactive, watchEffect, computed } = Vue
const state = reactive({
    count: 0
})

function render() {
    document.body.innerHTML = `count is ${state.count}`
}

watchEffect(render) // 初始化会调用一次render

setTimeout(() => {
    state.count = 100 // state.count发生变化,会通过watchEffect重新触发render
}, 1000)

看起来reactive方法会代理返回代理对象,而watchEffect会在对象属性发生变化时重新执行注册的回调函数render

接下来就从这两个方法开始一探究竟


function reactive(target: object) { return createReactiveObject(target,false,mutableHandlers,mutableCollectionHandlers) } function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { // 检测target是否是对象,是否已经设置__v_isReactive、__v_skip、__v_isReadonly等 // ... const observed = new Proxy( target, // 根据target是否是Set, Map, WeakMap, WeakSet对象来判断使用哪一种handler collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers ) return observed } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

先看看普通对象的baseHandlers,也就是传入的mutableHandlers配置项

export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(),
  set: createSetter(),
  // 下面三个方法均是通过`Reflect`来实现相关操作
  deleteProperty,
  has,
  ownKeys
}


createGetter 收集依赖


const targetMap = new WeakMap<any, KeyToDepMap>() function createGetter(isReadonly = false, shallow = false) { return function get(target: object, key: string | symbol, receiver: object) { // ... 根据 key与ReactiveFlags 返回特殊值 // ... 处理target为数组时的get操作 const res = Reflect.get(target, key, receiver) // ... 处理res是Ref类型的操作 if (isRef(res)) { if (targetIsArray) { !isReadonly && track(target, TrackOpTypes.GET, key) return res } else { // ref unwrapping, only for Objects, not for Arrays. return res.value } } // 收集依赖 !isReadonly && track(target, TrackOpTypes.GET, key) // 判断属性值类型,递归调用reactive处理,返回新的Proxy return isObject(res) ? reactive(res) : res } }

可以看见,只有当触发get的时候,才会检测属性值的类型,然后对属性值进行reactive操作,整体性能就Vue2在初始化时就递归劫持所有get而言,应该有不少提升。

我们知道,在触发get时需要收集依赖,可以看见track就是处理这个工作的


export function track(target: object, type: TrackOpTypes, key: unknown) { // 初始化target的依赖列表,通过Map保存,每个依赖可能依赖target某个或某些属性,因此该Map的键值是target的每个属性 let depsMap = targetMap.get(target) if (!depsMap) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) // 对于每个属性key而言,通过Set保存依赖该key的activeEffect if (!dep) { depsMap.set(key, (dep = new Set())) } // activeEffect是一个全局变量 if (!dep.has(activeEffect)) { dep.add(activeEffect) activeEffect.deps.push(dep) } } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

在渲染视图时,在render方法中,会通过get触发track,然后将activeEffect添加到数据属性的依赖列表中,这样就完成了依赖收集

effect 变化的抽象

我们这里接触到了一个新的概念effect,包括前面的effectwatchEffect等方法。类似于Watcher对象,用于封装各种变化。

在示例中的watchEffect注册的回调函数,就可以理解为一个effect。


// 从类型声明可以看出,effect是一个包含如下属性的函数 export interface ReactiveEffect<T = any> { (...args: any[]): T _isEffect: true id: number active: boolean raw: () => T deps: Array<Dep> options: ReactiveEffectOptions } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

然后来看看watchEffect的实现


export function watchEffect( effect: WatchEffect, options?: WatchOptionsBase ): WatchStopHandle { return doWatch(effect, null, options) } function doWatch( source: WatchSource | WatchSource[] | WatchEffect, cb: WatchCallback | null, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): WatchStopHandle { const instance = currentInstance // 根据source的类型封装getter,内部执行source的调用 let getter: () => any = () => {return callWithErrorHandling(source, instance) } // 根据 flush字符串初始化调度器,决定何时调用getter let scheduler: (job: () => any) => void = job => queuePostRenderEffect(job, instance && instance.suspense) // 调用effect注册 const runner = effect(getter, { lazy: true, // 由于下面会直接调用runner,因此lazy传入了true computed: true, onTrack, onTrigger, scheduler: applyCb ? () => scheduler(applyCb) : scheduler }) recordInstanceBoundEffect(runner) runner() // 返回一个取消effect的方法 return () => { stop(runner) if (instance) { remove(instance.effects!, runner) } } } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

我们在这里看见了一个与mountComponentinstance.update类似的runner方法,他们实际上都是一个通过effect包装的函数。


const effectStack: ReactiveEffect[] = [] let activeEffect: ReactiveEffect | undefined export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { if (isEffect(fn)) { fn = fn.raw } // 创建effect对象(函数) const effect = createReactiveEffect(fn, options) if (!options.lazy) { // 在前面初始化instance.update时会先调用一次 componentEffect,从而完成页面的初始化渲染 effect() } return effect } // 下面这段源码基本是原样copy过来的,没有做删减 function createReactiveEffect<T = any>( fn: (...args: any[]) => T, options: ReactiveEffectOptions ): ReactiveEffect<T> { const effect = function reactiveEffect(...args: unknown[]): unknown { if (!effect.active) { return options.scheduler ? undefined : fn(...args) } if (!effectStack.includes(effect)) { cleanup(effect) // 清除effect.deps try { enableTracking() effectStack.push(effect) activeEffect = effect // 将全局变量activeEffect 设置为当前运行的effect,然后调用effect return fn(...args) } finally { // finally中的代码始终都能执行 effectStack.pop() resetTracking() activeEffect = effectStack[effectStack.length - 1] // 将activeEffect重置为上一个effect } } } as ReactiveEffect effect.id = uid++ effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

整理一下watchEffect(cb)的流程

  • doWatch中将cb封装在getter中,
  • 调用effect(getter),通过createReactiveEffect返回一个真正的ReactiveEffect函数,赋值给runner
  • 回到doWatch中,直接调用runner执行
    • 在ReactiveEffect运行时会通过effectStack设置当前全局变量activeEffect

activeEffect是一个非常重要的全局变量,在前面的watchEffect(render)中,会在初始化时运行render

function render() {
  document.body.innerHTML = `count is ${state.count}`
}

由于内部方法内部访问了state.count,会触发state代理的get操作,这样就能够在track的时候通过activeEffect访问到封装了render方法的这个effect,这样当state.count发生变化时,才能够再次运行相关的effect,我们修改一下render方法

function render() {
  // document.body.innerHTML = `count is ${state.count}`
  console.log('render') // 只有第一次初始化的时候回打印render
  document.body.innerHTML = '123'
}

watchEffect(render)
state.count // 触发一个get,然而并没有activeEffect,因此不会收集到相关的依赖
setTimeout(() => {
  state.count = 100 // state.count更新时也不会触发render
}, 1000)

可以看见,如果在回调中没有触发state.count,则无法正确track到依赖。结合getactiveEffect,可以精确到收集每个属性变化时对应的的effect,这是非常高效的。

另外在watchEffect的源码中可以看见,由于runner是同步执行的,在执行完毕后会将activeEffect进行重置,如果我们在render方法中通过异步的方式访问state.count,也无法正确track依赖。

function render() {
  setTimeout(()=>{
    document.body.innerHTML = `count is ${state.count}` // 放在回调里面
  })
}
watchEffect(render)
setTimeout(() => {
  state.count = 100 // 也不会更新视图
}, 1000)


createSetter 通知变化

最后,我们来看看set代理


function createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { const oldValue = (target as any)[key] // 处理 ... ref const hadKey = hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) // 判断target === toRaw(receiver) ,不处理target原型链更新的情况 if (target === toRaw(receiver)) { if (!hadKey) { // 动态添加属性的情况 trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { // 属性更新的情况 trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result } }

get中的track同理,trigger应该就是更新数据并通知依赖进行处理的逻辑了


export function trigger( target: object, type: TriggerOpTypes, // 表示不同的变化,如ADD、SET等 key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { // 找到target某个key的依赖 const depsMap = targetMap.get(target) const effects = new Set<ReactiveEffect>() const computedRunners = new Set<ReactiveEffect>() // 将对应key的变化添加到effects或者computedRunners中 const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { if (effect !== activeEffect || !shouldTrack) { if (effect.options.computed) { computedRunners.add(effect) } else { effects.add(effect) } } }) } } // 根据type和key找到需要处理的depsMap,这里处理了各种特殊情况 add(depsMap.get(key)) // ... // 遍历effects和computedRunners const run = (effect: ReactiveEffect) => { // 如果effect自己配置了scheduler,则使用调度器运行effect if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() // 可以看见effect实际上是一个函数 } } // 计算属性先运行,这样可以保证其他属性在运行时能够获取到计算属性的值 computedRunners.forEach(run) effects.forEach(run) } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

小结:实现极简版reactive

前面的代码忽略了shallow浅reactive、readonly等情况,我们可以实现一个50行代码的reactive


let activeEffect let targetMap = new Map() function reactive(obj){ return new Proxy(obj, { get(target, key){ track(target, key) return target[key] }, set(target, key, value){ target[key] = value trigger(target, key) return true } }) } function track(target, key){ let depMap = targetMap.get(target) if(!depMap) { targetMap.set(target, (depMap = new Map())) } let dep = depMap.get(key) if(!dep) { depMap.set(key, ( dep = new Set())) } if(!dep.has(activeEffect)){ dep.add(activeEffect) } } function watchEffect(cb){ activeEffect = cb cb() } function trigger(target, key){ let depMap = targetMap.get(target) if(!depMap) return let effects = depMap.get(key) if(!effects) return effects.forEach((effect)=>{ effect() }) } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

然后测试一下

let {reactive, watchEffect} = require('./reactive')


let state = reactive({
    x: 100
})

function render(){
  let msg = `render template with state.x = ${state.x}`
  console.log(msg)
}

watchEffect(render)

setTimeout(()=>{
    state.x = 200
}, 1000)


上面的代码有意忽略了属性嵌套、数组set多次执行等很多细节问题,主要是为了展示了reactive最基本的结构。

避免effect重复执行

我们还忽略了一个比较重要的特点,当依赖的状态连续变化时,如何避免中间不比较的trigger呢?比如在上面的demo代码中


setTimeout(() => { state.x = 100 console.log(state.x) // 100 state.x = 200 console.log(state.x) // 200 }, 1000) // 会连续打印两次 render template with state.x = 100 | 200

在某些时候,比如渲染视图,第一次set时就触发render方法是完全没有必要的且很浪费性能的事情。

在Vue2中,Watcher会被加入到一个队列中,并在加入时进行去重,在nextTick中统一运行队列。在上面的demo中,由于activeEffect都是render方法,我们可以通过debounce的方式实现只调用一次render

那么Vue3是如何实现的呢?我们来研究一下

在trgger的时候可以看见


const run = (effect: ReactiveEffect) => { // 如果effect自己配置了scheduler,则使用调度器运行effect if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() // 可以看见effect实际上是一个函数 } } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

doWatch中可以看见下面的代码


scheduler = job => queuePostRenderEffect(job, instance && instance.suspense) const runner = effect(getter, { //... 其他配置 scheduler }) // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

可以看见在doWatch构造的effect中,会传入一个scheduler配置,当effect run的时候,会调用这个scheduler,此处就会执行queuePostRenderEffect


// 使用了两个全局队列来维护 const queue: (Job | null)[] = [] const postFlushCbs: Function[] = [] export const queuePostRenderEffect = __FEATURE_SUSPENSE__ ? queueEffectWithSuspense : queuePostFlushCb export function queuePostFlushCb(cb: Function | Function[]) { // 将effect放入postFlushCbs队列 if (!isArray(cb)) { postFlushCbs.push(cb) } else { postFlushCbs.push(...cb) } queueFlush() } export function queueJob(job: Job) { if (!queue.includes(job)) { queue.push(job) queueFlush() } } function queueFlush() { if (!isFlushing && !isFlushPending) { isFlushPending = true nextTick(flushJobs) // nextTick直接使用的Promise.then,在nextTick中执行flushJobs } } function flushJobs(seen?: CountMap) { isFlushPending = false isFlushing = true let job // 组件由父组件向子组件更新,父组件的id始终比子组件小(先构造) queue.sort((a, b) => getId(a!) - getId(b!)) while ((job = queue.shift()) !== undefined) { if (job === null) { continue } // 依次运行effect callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) } flushPostFlushCbs(seen) isFlushing = false // 如果在运行过程中调用了queueJob或者queuePostRenderEffect,则继续执行flushJobs if (queue.length || postFlushCbs.length) { flushJobs(seen) } } // 依次运行postFlushCbs中的回调,在前面scheduler中添加的job就会通过queuePostRenderEffect放在postFlushCbs中 export function flushPostFlushCbs(seen?: CountMap) { if (postFlushCbs.length) { // 关键的异步,对postFlushCbs进行去重,这意味着即使postFlushCbs存在多个相同的effect,也只会被执行一次 const cbs = [...new Set(postFlushCbs)] postFlushCbs.length = 0 for (let i = 0; i < cbs.length; i++) { cbs[i]() } } } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

这里的代码比较简单的展示了我们的effect是如何执行的

  • 首先通过注册配置项scheduler,在trigger的时候调用scheduler(effect)
  • scheduler调用queuePostFlushCb将effect放入全局队列postFlushCbs中,同时将flushJobs注册到nextTick
  • 在flushJobs中会清空queue和postFlushCbs,在清空postFlushCbs之前,会通过Set对postFlushCbs进行去重
  • 这样相同的effect在同一次flushJobs中,只会被执行一次,而不论之前trigger的时候调用了多少次scheduler(effect)

Ref

前面的源码分析让我们了解了Vue3中是如何通过Proxy代理对象,并在get的时候track effect,在set的时候trigger effect。

在某些时候需要一个依赖于其他状态的数据,可以通过计算属性来获得,计算属性作为一个函数,可以返回各种类型的值,并且当计算属性依赖的状态发生变化时,会自动重新计算并通知依赖该计算实现的地方。

官网阐述了一种最简单的实现方案,通过闭包和watchEffect实现computed(强烈建议大家先去看看官网这篇文章,对于理解Vue3的设计有很大帮助)


function computed(getter) { let value watchEffect(() => { value = getter() }) return value } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

那么问题来了,当计算属性返回的是基础类型的值,虽然能够重新运行watchEffect的回调并更新值,但无法通知之前那个oldValue的依赖, 这是因为JavaScript中普通类型是按值而非引用传递的。

解决这个问题的办法就是返回一个对象,然后代理响应值收集依赖,Vue3把这种新的类型叫做Ref


function computed(getter) { const ref = { value: null, } watchEffect(() => { ref.value = getter() }) return ref } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

接下来就从computed开始,看看Ref的作用与实现

下面是基本的demo代码

const state = reactive({
  count: 1
})
const doubleCount = computed(() => {
  return state.count * 2
})

function render() {
  document.body.innerHTML = `count is ${doubleCount.value}`
}

watchEffect(render)

setTimeout(() => {
  state.count = 100
}, 1000)

computed

下面是computed的源码


export function computed<T>( getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T> ) { let getter: ComputedGetter<T> let setter: ComputedSetter<T> if (isFunction(getterOrOptions)) { getter = getterOrOptions } else { getter = getterOrOptions.get setter = getterOrOptions.set // 处理set computed } let dirty = true // 判断getter是否需要重新运算 let value: T let computed: ComputedRef<T> const runner = effect(getter, { lazy: true, // 只有当用到computed的时候才运行求职 computed: true, // 将effect.computed标志为true,这样会在普通的effect之前运行 // 自定义调度器,在trigger时调用 scheduler: () => { // 通知使用了computed.value的effect,只有当计算属性的getter已经被运行过才进行通知 if (!dirty) { dirty = true trigger(computed, TriggerOpTypes.SET, 'value') } } }) computed = { __v_isRef: true, effect: runner, get value() { if (dirty) { value = runner() // 调用runner 完成activeEffect设置,这样当计算属性依赖的其他状态发生变化时,可以重新触发getter, dirty = false // 缓存已经计算过的值 } // runner运行完毕后会重置activeEffect为上一个effect,然后将依赖该computed的activeEffect添加到依赖中 track(computed, TrackOpTypes.GET, 'value') return value }, set value(newValue: T) { setter(newValue) } } as any // 可以看见,计算属性返回的是一个特殊的对象 return computed } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

OK,看起来就比较清楚了,结合上面的例子

const doubleCount = computed(() => {
  return state.count * 2
})
function render() {
  document.body.innerHTML = `count is ${doubleCount.value}`
}
watchEffect(render)


把整个流程简化为


上游数据 -> computed -> 下游数据

则具体过程

  • 调用computed(getter)时会初始化一个封装了getter的effect
  • 当触发计算属性get value时,会运行的effect
    • 此时触发getter中计算属性依赖的上游数据的get,同时通过调用track收集依赖于当前计算属性的下游数据
    • 当上游数据发生变化时,会重新触发effect,由于这里自定义了scheduler,因此会使用scheduler(effect)的方式运行effect,
    • scheduler(effect)中重置dirty,然后调用trigger通知下游数据

Ref

同理,我们可以看看普通的Ref的实现


export function ref(value?: unknown) { return createRef(value) } function createRef(rawValue: unknown, shallow = false) { if (isRef(rawValue)) { return rawValue } let value = shallow ? rawValue : convert(rawValue) const r = { __v_isRef: true, get value() { track(r, TrackOpTypes.GET, 'value') return value }, set value(newVal) { if (hasChanged(toRaw(newVal), rawValue)) { rawValue = newVal value = shallow ? newVal : convert(newVal) trigger( r, TriggerOpTypes.SET, 'value' ) } } } return r } // Js中文网 -前端Vue3源码解析 https://www.javascriptc.com/

有了computed的经验,这里这里看起来就比较容易了,在get value的时候track收集activeEffect,在set value的时候,如果值发生了变化,就通过trigger通知effect。

小结

本文主要整理了Vue3中新的响应式系统,

  • 通过effect封装变化,在Vue2中是通过Watcher负责的
  • 通过Proxy代理对象每个属性的set和get,在get时通过track收集activeEffect,在set时通过trigger通知对应属性的所有依赖更新
  • 通过Map保存每个reactive对象中每个属性的依赖
  • 通过effect队列在nextTick中统一运行effect,并通过Set进行effect去重
  • 为了解决普通类型按值传递的问题,Vue3实现了Ref类型对象,computed也可以当做是一种特殊的Ref。

至此,对于Vue3的响应式系统有了一定的了解,接下来去看组合式API应该就比较方便了。

作者:橙红年代
链接:https://juejin.im/post/6844904165324357646

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

标题:Vue3源码分析——数据侦测

链接:https://www.javascriptc.com/4150.html

« 跟着尤大大一起Vue3源码解读(一)-createApp
LeetCode 028. 实现strStr()»
Flutter 中文教程资源

相关推荐

QR code