likes
comments
collection
share

vue3源码解析(一)

作者站长头像
站长
· 阅读数 15

vue3源码解析

简介

起因:一个遇到的问题--使用reactive定义数组对象类型,传[]时无法触发响应式 解决方案: 1. 可以使用对象包裹数组,使其成为对象的一个属性,就不影响后续的赋值2. 改为ref定义3. 使用数组的push方法,不使用=直接赋值,使用push

这个问题引发了思考,为什么reactive定义的需要这样处理,它的内部是如何实现的? 带着这些问题,我clone了vue的源码去查看

模块划分

vue3源码解析(一)

如上图所示,按照模块可以分为三个部分,Reactivity Module/Complier Module/Renderer Module,分别对应的是响应式处理、编译、渲染。 本次重点在于Reactivity Module中的reactive的实现

几个概念-有助于理解源码

weakMap

mdn上对于weakMap的解释是

对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意。它的键被弱保持,也就是说,当其键所指对象没有其他地方引用的时候,它会被 GC 回收掉。WeakMap提供的接口与Map相同。

总结来说就是:

  1. 只接受对象作为键名
  2. 键名所引用的对象是弱引用

reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handler (en-US) 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。

是一个内置对象,可简化的创建 Proxy 可以将操作转发到原始对象

proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

let proxy = new Proxy(target, {})

响应式--依赖收集和派发更新

举一个简单的发布-订阅模式的例子

比如小明最近在淘宝网上看上一双鞋子,但是联系到卖家后,才发现这双鞋卖光了。下单的按钮变成了到货提醒,于是小明点击了到货提醒的按钮。但与此同时,小王,小张等也喜欢这双鞋,也点击了到货提醒的按钮;等来货的时候就依次会通知他们;

在上面的故事中,可以看出是一个典型的发布订阅模式,卖家是属于发布者,小王,小明等属于订阅者,订阅该店铺,卖家作为发布者,当鞋子到了的时候,会依次通知小明,小王等,依次使用旺旺等工具给他们发布消息;

带入到代码中要实现响应性,就需要在合适的时机,依赖收集(track),触发改变时再次执行副作用 effect-派发更新(trigger) vue3源码解析(一) vue3源码解析(一)

product为例,targetMap用于存储每个响应式的依赖(product),depsMap用于存储每个属性的依赖(price、quantity),dep是effect集。

我们尝试手写响应式模型

一个最基本的响应式

let targetMap = new WeakMap()

function track(target,key){
    let depsMap = targetMap.get(target)
    if(!depsMap){
        targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if(!dep){
        depsMap.set(key,(dep = new Set()))
    }
    dep.add(effect)
}

function trigger(target,key){ 
    let depsMap = targetMap.get(target)
    if(!depsMap) return
    let dep = depsMap.get(key)
    if(dep){
        dep.forEach(effect=>{effect()})
    }
}

let product = {
    price: 3,
    quantity: 2
}

let total = 0

var effect = ()=>{
    total = product.price * product.quantity 
}
track(product, 'price')
effect()
console.log("🚀 ~ file: index.js:60 ~ effect ~ total:", total)
product.price = 5
trigger(product, 'price')
console.log("🚀 ~ file: index.js:63 ~ effect ~ total:", total)

打印的结果是

🚀 ~ file: index.js:36 ~ effect ~ total: 6
🚀 ~ file: index.js:36 ~ effect ~ total: 10

这里需要手动的去收集依赖(track),手动触发trigger,可以优化为自动的执行

优化后的

let targetMap = new WeakMap()

function track(target,key){
    let depsMap = targetMap.get(target)
    if(!depsMap){
        targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if(!dep){
        depsMap.set(key,(dep = new Set()))
    }
    dep.add(effect)
}

function trigger(target,key){ 
    let depsMap = targetMap.get(target)
    if(!depsMap) return
    let dep = depsMap.get(key)
    if(dep){
        dep.forEach(effect=>{effect()})
    }
}

function reactive(target) {
    const handlers = {
      get(target, key, receiver) {
        let result = Reflect.get(target, key, receiver)
        track(target, key)
        return result
      },
      set(target, key, value, receiver) {
        let oldValue = target[key]
        let result = Reflect.set(target, key, value, receiver)
        if (result && oldValue != value) {
          trigger(target, key)
        }
        return result
      },
    }
    return new Proxy(target, handlers)
}

let product = reactive({
    price: 3,
    quantity: 2
})

let total = 0

var effect = ()=>{
    total = product.price * product.quantity 
}
effect()
console.log("🚀 ~ file: index.js:54 ~ effect ~ total:", total)
product.price = 5
console.log("🚀 ~ file: index.js:56 ~ effect ~ total:", total)

打印的结果是

🚀 ~ file: index.js:54 ~ effect ~ total: 6
🚀 ~ file: index.js:56 ~ effect ~ total: 10

这里发现,effect还不能自动收集,需要做成灵活的

继续优化effect

const targetMap = new WeakMap()
let activeEffect = null

function track(target, key){
    if(activeEffect){
        let depsMap = targetMap.get(target)
        if(!depsMap){
            targetMap.set(target,(depsMap=new Map()))
        }
        let dep = depsMap.get(key)
        if(!dep){
            depsMap.set(key,(dep=new Set()))
        }
        dep.add(activeEffect)
    }
}

function trigger(target,key){
    let depsMap = targetMap.get(target)
    if(!depsMap) return
    let dep = depsMap.get(key)
    if(dep){
        dep.forEach(effect=>{effect()})
    }
}

function reactive(target) {
    const handlers = {
      get(target, key, receiver) {
        let result = Reflect.get(target, key, receiver)
        track(target, key)
        return result
      },
      set(target, key, value, receiver) {
        let oldValue = target[key]
        let result = Reflect.set(target, key, value, receiver)
        if (result && oldValue != value) {
          trigger(target, key)
        }
        return result
      },
    }
    return new Proxy(target, handlers)
}

function effect(eff) {
    activeEffect = eff
    activeEffect()
    activeEffect = null
  }

let product = reactive({
  price: 5,
  quantity: 2
})
let total = 0
let salePrice = 0

effect(() => {
    total = product.price * product.quantity
})
effect(() => {
    salePrice = product.price * 0.9
})
console.log(
    `Before updated quantity total (should be 10) = ${total} salePrice (should be 4.5) = ${salePrice}`
  )
  product.quantity = 3
  console.log(
    `After updated quantity total (should be 15) = ${total} salePrice (should be 4.5) = ${salePrice}`
  )
  product.price = 10
  console.log(
    `After updated price total (should be 30) = ${total} salePrice (should be 9) = ${salePrice}`
  )

打印的结果是

Before updated quantity total (should be 10) = 10 salePrice (should be 4.5) = 4.5
After updated quantity total (should be 15) = 15 salePrice (should be 4.5) = 4.5
After updated price total (should be 30) = 30 salePrice (should be 9) = 9

一个最基本的reactive就已经实现了,当然还有很多场景和细节需要处理,这里用到的变量和方法都和vue3中的保持一致。

感兴趣的可以去看源码部分一起交流!