《Vue.js的设计与实现》快速阅读-第二篇

100 阅读7分钟

本文将讲述Vue3响应系统的设计理念和最终实现,其实用到的知识点不多,但是因为书中举例的变量命名和讲述顺序的问题,导致很容易跟不上节奏,我会重新整理举例内容和拆分解题思路,使内容更清晰。

一. 全书结构,分6篇

  1. 框架设计概览:先全局分析框架的选择、细节建设,最后全局介绍了设计思路、目前Vue3各模块之间的协作。(类似一个项目团队的建设过程,先结合项目特点确定组织架构,然后建设团队,每个工种的人员确定,最后全面介绍为什么选择这些人加入团队,以及团队如何协作)
  2. 响应系统:响应是Vue面世时的最大亮点,而Vue3响应的实现也是3和2的最大区别之一,面试常问。本书先全局介绍Vue3如何从0到1的实现响应———()—————,然后细节介绍实现过程中的重要技术点和难点。
  3. 渲染器:数据层变化-响应,之后就要“渲染”到页面层,否则用户看不到变化,白响应了。本篇先介绍了响应系统和渲染器是如何协作的(类似项目团队中不同工种之间的配合,比如前端和后台要互相响应配合),详细介绍了渲染器的基础信息:名词和概念、自定义如何实现。然后介绍了渲染器的实现过程中的技术重点:挂载和更新。最后介绍了3种算法工作原理,包括:简单Diff、双端Diff、快速Diff。
  4. 组件化:也是Vue面世时最大亮点。1.实现原理、组件的状态。2.异步组件和函数组件。3.Vue内建的3个重点组件:KeepAlive、Teleport、Transtion。
  5. 编译器:使用Vue开发是全js代码,如何转换成浏览器可识别的HTML等内容?就需要编译器了。1.Vue的模板编译器工作流程,parser、AST,然后输出了具体的生成渲染函数代码。2.HTML的解析器。3.优化:Vue3编译器的优化部分。
  6. 服务端渲染:Vue同构渲染的原理。先介绍了CSR、SSR及同构渲染的优缺点。然后探讨了Vue服务端渲染+客户端激活的原理。最后强调了编写同构代码的注意事项。

第二篇 响应系统

第4章 响应系统的作用与实现
第5章 非原始值的响应式方案
第6章 原始值的响应式方案

第4章 响应系统的作用与实现

4.1 副作用函数和响应式数据
  • 副作用函数的执行会直接或间接的影响其他函数的执行,这就是函数宣产生了副作用。
  • 响应式数据:当值修改时,我们希望它相关的副作用函数自动重新执行。
// 基础数据
let data = {text: 'hello'}

// 副作用函数 fn
function fn(){
    document.body.innerText = data.text
}

以上代码,数据data和函数fn,我们希望data.text修改时,可以自动执行fn重新赋值。如何实现呢? 不难发现,

  • 副作用函数执行时 -> 触发字段的读取操作get
  • 修改data.text时, -> 触发字段的设置操作set 如此,我们可以拦截字段data.text,以此作为桥梁,串通我们的data修改和副作用函数的执行。如下:
    分析上文代码可知,
    fn()执行时,
    赋值innerText需要先取值data.text,这会触发它的读取get操作;
    而修改data.text,会触发它的设置set操作。
    那么,很容易想到拦截一个对象的get和set操作,现有2种方式:Object.definePropertyProxy。
    以Proxy举例,实现每次修改data.text都重新执行它的副作用函数-赋值innerText。
    const obj = new Proxy(data, {
        get( target, key ){
            return target[key]
        },
        set( target, key, newVal ){
            target[key] = newVal  //默认set操作
            fn()  //重新赋值
            return true   // 代表设置操作成功
        },
    })

4.2 响应式数据的基本实现

需要考虑到,相关的副作用函数可能有多个,我们需要设置一个变量用于存储副作用函数。什么时间把副作用函数存入变量呢?set时已经执行了,选个时机存入,可以在get时存入。

//变量bucket,用于存储副作用函数
let bucket = new Set()

//data代理到obj,此后data相关操作要使用obj.xx
const obj = new Proxy(data, {
    get( target, key ){
        bucket.add(fn)
        return target[key]
    },
    set( target, key, newVal ){
        target[key] = newVal
        bucket.forEach(fn => fn())
        return true   // 代表设置操作成功
    },
})

//因为Proxy代理了,所以fn内的data改为obj
// 副作用函数 fn
function fn(){
    document.body.innerText = obj.text;
    // console.log(1, bucket);
}

开始使用

// 先触发读取,存入副作用函数
fn()
setTimeout(() => {
    obj.text = 2
}, 1000);

到目前为止,我们的Proxy.get里是有硬编码fn的,用什么来代替这个硬编码呢?只要写一个方法用于注册副作用函数,而Proxy内部写注册函数就可以了。如下-proxy3:

// 基础数据
let data = {text: 'hello'}
let bucket = new Set()
let active  //存储被注册的副作用函数
//注册函数
function effect(fn){    
    active = fn
    fn()
}
const obj = new Proxy(data, {
    get( target, key ){
        if(active)  bucket.add(active)
        return target[key]
    },
    set( target, key, newVal ){
        target[key] = newVal
        bucket.forEach(fn => fn())
        return true   // 代表设置操作成功
    },
})
// 先触发读取,存入副作用函数,因为代理到obj了,所以fn的内容也需要改一下
// 副作用函数 fn
function fn(){
    document.body.innerText = obj.text;
}
effect(fn)
setTimeout(() => {
    obj.text = 'active'
}, 1000);

4.3 完善响应系统

思考一个问题,如果我们设置data.text2 = '佩奇',副作用函数会执行吗?如下-proxy4:

// 先触发读取,存入副作用函数,因为代理到obj了,所以fn的内容也需要改一下
// 副作用函数 fn
function fn(){
    console.log(1);
    document.body.innerText = obj.text;
}
fn()
setTimeout(() => {
    obj.text2 = '佩奇'
}, 1000);

这也是defineProperty和Proxy的区别,前者监听对象现有的、某一个属性,后者监听整个对象,不论新旧属性。
这个问题是因为我们没有建立属性与副作用函数的联系。出现的角色有:

  • Proxy代理对象 obj
  • 对象的属性名 text
  • 副作用函数 fn 我们暂定一些标识,用target表示代理对象所代理的原始对象(data),用key标识被操作的字(text),用fn标识副作用函数。我们来捋捋可能出现的情况(xmind可以画):
  1. 最基础的target - key - fn
  2. 2个副作用函数内都读取了某属性的值
  3. 1个副作用函数内读取了2个属性
  4. 不同副作用函数读取了不同属性

综上,这是一个树形结构,这个联系建立起来就可以解决我们上面的“佩奇”问题了。

  1. 设置一个变量,存储项目中所有的对象,obj123...,new WeakMap()
    • WeakMap对数据是弱引用,不影响垃圾回收机制,target在用户侧没有引用时就会完成回收,Map可能导致内存溢出。
  2. 每个对象都是一个map,data = new Map()
  3. map内存储属性对应副作用函数,text = new Set()
  4. set内是该属性相关的所有函数,forEach取出后遍历执行,#4.2已经实现

现在实现的思路已经理清,实现如下proxy5:

// 基础数据
let data = {text: 'hello'}
let bucket = new WeakMap()
let active  //存储被注册的副作用函数
//注册函数
function effect(fn){    
    active = fn
    fn()
}
const obj = new Proxy(data, {
    get( target, key ){
        // target == data   key == text
        // 没有注册副作用函数,则直接返回
        if(!active)  return target[key]
        // 1.获取target的MAp, 如果不存在就new Map并与target关联
        let depsMap = bucket.get(target)
        if(!depsMap) bucket.set(target, (depsMap = new Map()))

        // 2.根据key继续取值Set类型, 如果不存在 同上 new一个
        let deps = depsMap.get(key)
        if(!deps) depsMap.set(target, (deps = new Set()))

        // 3.把当前激活的副作用函数 添加到桶里, 这一步是我们之前实现了的
        deps.add(active)

        return target[key]
    },
    set( target, key, newVal ){
        target[key] = newVal
        // 取值相应的函数
        const depsMap = bucket.get(target)
        if(!depsMap) return 
        const effects = depsMap.get(key)
        // 空值和forEach会报错
        effects && effects.forEach(fn => fn())
    },
})

effect(() =>{
    console.log('1');
    document.body.innerText = obj.text;
})

setTimeout(() => {
    obj.text2 = '佩奇'
}, 1000);