从源码层面深入理解vue3

主要记录在跟随学习vue3源码的过程中遇到的知识点和架构思维。

接在从源码层面深入理解vue3(1)之后。

// handlers.js 
import { track,trigger } from './effect.js'; 
import { isObject } from './utils.js'; 
import { reactive } from './reactive.js'; 
export const handlers = { 
    get(target,key,receiver){ 
        track(target,key) 
        const result = Reflect.get(target,key,receiver) 
        if(isObject(result)){ return reactive(result) } 
        return result 
    }, 
    set(target,key,value,receiver){ 
        trigger(target,key) 
        return Reflect.set(target,key,value,receiver) 
    } 
}

在完成get()的雏形之后,是否能够满足所有“监听数据的读取”的情况呢?我们来看接下来的情况:

let obj = {
    a:1,
    b:2,
}
const proxy = reactive(obj)
function fn(){
    if('e' in proxy){
        // 省略可能是一些业务逻辑
    }
}

// 在以上情况中,proxy中是没有e属性的
// 所以当我们去新增e属性的时候,是触发不了get的

proxy.e = 123
fn()

这就给了我一个警示,所谓的 “监听数据的读取” 并不单纯是说obj.a 属性值这样直接读取它这么简单,包括这个数据存不存在?也属于是数据的读取。所以也应该称为是对这个对象的所有信息的读取

那么接下来要做的就是去找到in这个关键字,到底触发了什么内部方法(因为对对象的操作,最终都会去调用到内部方法),知道之后就相应在handlers中配置相应的拦截器去拦截。通过ES文档找到对象的in关键字的相关内容,找到触发的内部方法是 [[HasProperty]]

再下一步找到Proxy文档中,[[HasProperty]]用的是什么函数名,直接搜索Proxy mdn找到对应文档:

image.png

可以看到[[HasProperty]]对应的是has()。那么再在handlers中新增一个has()的拦截器:

// handlers.js 
import { track,trigger } from './effect.js'; 
import { isObject } from './utils.js'; 
import { reactive } from './reactive.js'; 
export const handlers = { 
    get(target,key,receiver){ 
        track(target,key)
        const result = Reflect.get(target,key,receiver) 
        if(isObject(result)){ return reactive(result) } 
        return result 
    }, 
    set(target,key,value,receiver){ 
        trigger(target,key) 
        return Reflect.set(target,key,value,receiver) 
    } ,
    has(target,key){
        // 依赖收集
        track(target,key)
        return Reflect.has(target,key) 
    }
}

深层拓展

接下来思考一下,下方两种场景的区别

// 场景1:原本没有e属性
let obj = {
    a:1,
    b:2,
}
const proxy = reactive(obj)
function fn(){
    if('e' in proxy){
        // 省略可能是一些业务逻辑
    }
}

proxy.e = 123
fn()
// 场景2:原本有e属性
let obj = {
    a:1,
    b:2,
    e:5
}
const proxy = reactive(obj)
function fn(){
    if('e' in proxy){
        // 省略可能是一些业务逻辑
    }
}

proxy.e = 123
fn()

场景1proxy.e = 123让proxy的e属性从无到有,那么应该触发fn()。但是场景2proxy.e = 123实际上只是改变了原有的e的值,并没有改变它存不存在的事实,因此不应该触发fn()

这代表当我们在进行 依赖收集(track) 的时候,需要告诉它我现在这个依赖是在读取属性的值还是读取属性是否存在。接下来就可以写一下依赖收集派发任务的代码雏形

2. 如何知晓数据对应的函数

依赖收集派发更新的函数放于effect.js中:

// effect.js

// 依赖收集
export function track(target,type,key){
    console.log(`依赖收集【${type}${key}`)
}
// 派发更新
export function track(target,type,key){
    console.log(`派发更新【${type}${key}`)
}

创建一个operations.js用来存储操作类型:

// operations.js

// 依赖收集的操作类型
export const TrackTypes = {
    GET: 'get', // 读取属性值
    HAS: 'has', // 判断属性是否存在
}

// 派发更新的操作类型
export const TriggerTypes = {
    SET: 'set', // 设置属性值
    ADD: 'add', // 添加属性
    DELETE: 'delete', // 删除属性
}

根据需求的更新,修改handlers:

// handlers.js 
import { track,trigger } from './effect.js'; 
import { TrackTypes,TriggerTypes } from './operations.js'; 
import { isObject } from './utils.js'; 
import { reactive } from './reactive.js'; 
export const handlers = { 
    get(target,key,receiver){ 
        track(target,TrackTypes.GET,key) //在依赖收集时传入操作类型
        const result = Reflect.get(target,key,receiver) 
        if(isObject(result)){ return reactive(result) } 
        return result 
    }, 
    set(target,key,value,receiver){ 
        // TODO:判定操作类型(暂时先放个set)
        trigger(target, TriggerTypes.SET ,key) //在派发更新时传入操作类型
        return Reflect.set(target,key,value,receiver) 
    },
    has(target,key){
        track(target,TrackTypes.HAS,key)
        return Reflect.has(target,key) 
    }
}

再次拓展

在前面学习中遇到了'e' in obj的情况是无法被get拦截的,那么除了读取属性判断属性是否存在还有什么情况呢?还有for(const key in proxy){}遍历对象的属性名

那么考虑一下,这样一个函数:

function fn(){
    for(const key in proxy){
        // 省略业务代码
    }
} 

它与proxy有关联吗?当然是有的,当我们增加/删除属性时都会影响这个函数的结果。那么就像解决in的方式一样,到ES文档中寻找ForIn相关的内容。发现它的核心行为是迭代对象中的每一个键,通过一个函数EnumberateObjectProperties,在这个函数内部可以找到用到了Reflect.ownKeys(),在上一章学习到Reflect可以直接调用对象的基本操作,因此ownKeys就是对象的基本操作(内部函数)了不需要再继续往下找。

所以需要在handlers中加上对ownKeys的拦截

// operations.js 往依赖收集中加一个操作类型
export const TrackTypes = {
    GET: 'get', // 读取属性值
    HAS: 'has', // 判断属性是否存在
    ITERATE: 'iterate', // 迭代对象
}

// handlers.js 
ownKeys(target){
    // 依赖收集 因为遍历是对整个对象进行依赖收集而不是某个key
    track(target,TrackTypes.ITERATE) // 因此不需要传递key 第三个参数
    return Reflect.ownKeys(target) // 返回对象的所有属性名
}

那么就可以简单优化一下effect.js:

// effect.js
import { TrackTypes,TriggerTypes } from './operations.js'; 

// 依赖收集
export function track(target,type,key){
    if(type === TrackTypes.ITERATE){
        // 迭代的情况下,没有key值
        console.log(`依赖收集【${type}】`)
        return
    }
    console.log(`依赖收集【${type}${key}`)
}
// 派发更新
export function track(target,type,key){
    console.log(`派发更新【${type}${key}`)
}

那么监听对象的信息被读取目前想不到别的了,接下来跟进一下监听对象的信息被更改(handlers中的set())。

// utils.js 工具函数
// 先到工具函数中加一个函数 用来判断两个值是否相同(数据是否改变)
export function hasChanged(oldValue,newValue){
    // 为什么不用 === 来做判断,因为:
    // NaN===NaN会为false 但将一个值从NaN变为NaN 我不希望它触发更新
    // +0===-0会为true,但将一个值从+0变为-0 会有区别,我希望它触发更新
    // 1/+0为正无穷  1/-0为负无穷
    return !Object.is(oldValue,newValue)
}

// handlers.js  其他代码就先省略了
import { TrackTypes,TriggerTypes } from './operations.js'; 
import { hasChanged } from './utils.js'

set(target,key,value,receiver){
    // 先判断能否修改成功
    const result = Reflect.set(target,key,value,receiver)
    if(!result) return result
    
    // 如果原本拥有这个属性就是set,原本没有就是add
    const type = target.hasOwnProperty(key) ? TriggerTypes.SET : TriggerTypes.ADD
    // 拿到原本的数据
    const oldValue = target[key]
    
    // 只有 新传入的值与原本的值不同 或者 这是一个新增操作 才会触发派发更新
    if(hasChanged(oldValue,value)||type === TriggerTypes.ADD){
        trigger(target, type ,key) //在派发更新时传入操作类型
    }
    return result
}

那么set就是用来实现对更新添加的监听,删除需要另外实现 在Proxy中叫做deleteProperty

// handlers  其他代码就先省略了
import { TrackTypes,TriggerTypes } from './operations.js'; 

deleteProperty(target,key){
    // 需要做边界判断:只有对 原来有的属性 + 删除成功的 才会触发派发更新
    const hadKey = target.hasOwnProperty(key) // 原本有没有这个属性
    const result = Reflect.deleteProperty(target,key) // 删除 并返回是否成功
    if(hadKey&&result){
        trigger(target, TriggerTypes.DELETE ,key)
    }
    return result
}
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]