本文将讲述Vue3响应系统的设计理念和最终实现,其实用到的知识点不多,但是因为书中举例的变量命名和讲述顺序的问题,导致很容易跟不上节奏,我会重新整理举例内容和拆分解题思路,使内容更清晰。
一. 全书结构,分6篇
- 框架设计概览:先全局分析框架的选择、细节建设,最后全局介绍了设计思路、目前Vue3各模块之间的协作。(类似一个项目团队的建设过程,先结合项目特点确定组织架构,然后建设团队,每个工种的人员确定,最后全面介绍为什么选择这些人加入团队,以及团队如何协作)
- 响应系统:响应是Vue面世时的最大亮点,而Vue3响应的实现也是3和2的最大区别之一,面试常问。本书先全局介绍Vue3如何从0到1的实现响应———()—————,然后细节介绍实现过程中的重要技术点和难点。
- 渲染器:数据层变化-响应,之后就要“渲染”到页面层,否则用户看不到变化,白响应了。本篇先介绍了响应系统和渲染器是如何协作的(类似项目团队中不同工种之间的配合,比如前端和后台要互相响应配合),详细介绍了渲染器的基础信息:名词和概念、自定义如何实现。然后介绍了渲染器的实现过程中的技术重点:挂载和更新。最后介绍了3种算法工作原理,包括:简单Diff、双端Diff、快速Diff。
- 组件化:也是Vue面世时最大亮点。1.实现原理、组件的状态。2.异步组件和函数组件。3.Vue内建的3个重点组件:KeepAlive、Teleport、Transtion。
- 编译器:使用Vue开发是全js代码,如何转换成浏览器可识别的HTML等内容?就需要编译器了。1.Vue的模板编译器工作流程,parser、AST,然后输出了具体的生成渲染函数代码。2.HTML的解析器。3.优化:Vue3编译器的优化部分。
- 服务端渲染: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.defineProperty和Proxy。
以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可以画):
- 最基础的target - key - fn
- 2个副作用函数内都读取了某属性的值
- 1个副作用函数内读取了2个属性
- 不同副作用函数读取了不同属性
综上,这是一个树形结构,这个联系建立起来就可以解决我们上面的“佩奇”问题了。
- 设置一个变量,存储项目中所有的对象,obj123...,new WeakMap()
- WeakMap对数据是弱引用,不影响垃圾回收机制,target在用户侧没有引用时就会完成回收,Map可能导致内存溢出。
- 每个对象都是一个map,data = new Map()
- map内存储属性对应副作用函数,text = new Set()
- 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);