H5W3
当前位置:H5W3 > 其他技术问题 > 正文

Vue3.0响应式原理

该文章只是为了理解响应式的原理,大家不要过分注重代码中的细节,抓住重点理解原理即可。

Vue2.0使用的是options Api,我们要配置的响应式的data、computed以及methods都是配置在一个options对象中,就像下面这样:

var vm = new Vue({
// 选项
data: {
// ...
},
computed: {
// ...
},
methods: {
// ...
}
})
 

Vue3.0使用的是composition Api,通过提供一系列Api给用户自己调用,用户可以自由组合这些Api。例如让数据变响应式的Api(reactive),当数据改变后触发对应函数执行的Api(effect)。

下面我们通过对reactive和effect这两个api的解析和实现来梳理下vue3.0的响应式原理:

reactive的原理

reactive这个api就是让数据变成响应式,使用的是ES6的proxy方法。其基本使用方式如下:

// 从vue里import该api
import {reactive} from 'vue3';
// 需要设置为响应式的对象
const needReactiveObject = {
param1: '',
param2: ''
}
// 使用reactiveapi将needReactiveObject设置为响应式
// 返回代理后的对象,对obj的获取和改变会触发对应的get set函数
const obj = reactive(needReactiveObject)
 

响应式的含义:

我们只需要调用reactive,然后把需要设置为响应式的对象作为参数传进去,然后该对象就变为响应式的了。这里的变成响应式是什么个意思呢?就是说只要依赖了该对象里的值,当该值发生了变化,那么依赖的地方就会全部重新执行。还是有一点点抽象,具体到代码中看下到底是个什么意思。比如下面的代码:

<template>
<div>{{obj.param1}}</div>
</template>
<script>
import {reactive} from 'vue3';
const needReactiveObject = {
param1: ''
}
const obj = reactive(needReactiveObject)
</script>
 

reactive(needReactiveObject)将needReactiveObject变成了响应式,<div>{{obj.param1}}</div>依赖了obj.param1,所以当param1变化的时候,就会触发<div>{{obj.param1}}</div>重新渲染更新。这里是怎么触发渲染更新的呢?实际上<div>{{obj.param1}}</div>会被编译成渲染函数,这里的重点是函数!就是说我们虽然看到的是一个模板<div>{{obj.param1}}</div>,然而实际上它是会被编译成一个函数的,就像下面这样:

<div id="id1">{{obj.param1}}</div>
被转化为:
// 这里只是举个栗子,了解其原理即可,实际转化的渲染函数当然不是这样的
// 实际渲染函数执行后是生成虚拟dom,这里为了方便理解,直接用真实dom演示
function render() {
document.getElementById('id1').innerHtml = obj.param1
}
 

就是说当obj.param1变化的时候,那么依赖于param1的函数,如上代码中即是render就会执行。每次param1变化渲染函数都会自动执行,然后渲染函数内部会调用dom的相关api更新页面上的内容,这就是响应式更新的基本原理。

所以我们把数据变成了响应式,实际是为了该数据可以收集依赖,即收集哪些函数依赖了它,当数据发生变化的时候,就可以执行这些函数。收集依赖是用一个大的对象来保存依赖关系,相关原理如下:

const needReactiveObject = {
param1: ''
}
// 将needReactiveObject 设置为响应式
reactive(needReactiveObject)
// depsMap用来保存所有的依赖关系
// depsMap[needReactiveObject].param1是一个数组,保存了依赖param1的函数
// 当param1变化的时候,其数组里的函数就会执行,在这里即为render函数
const depsMap = {
[needReactiveObject]: {
param1: [render]
}
}
 

如上所述,reactive就是为了将对象设置为响应式,然后该对象里的每个key都对应一个数组,该数组保存的是依赖于该key的函数,每当key变化的时候,数组里的函数就会依次执行。如果是模版里依赖了该key,那么就会添加一个模版对应的渲染函数到数组里,在本文中即是上面的render函数。

以上功能reactive代码实现:

// reactive函数使用proxy将target转化为响应式,并返回代理过后的对象observed
// 每当外部访问observed对象里的值,就会走到getter函数里面
function reactive(target){
const observed = new Proxy(target, {
get(target,key){
const res = target[key]
// 当获取值needReactiveObject.param1, 会触发getter
// 然后在getter里进行依赖收集,依赖收集的代码写在track里面
track(target,key) // !!!重要,该函数进行依赖收集
return typeof res==='object'?reactive(res):res
}
})
return observed;
}
 

如上,reactive函数内部通过proxy将对象进行代理,然后返回代理后对象observed,每当访问observed里的值,就会触发get函数。在get函数里我们可以进行依赖收集。

依赖收集的基本原理是这样的:

以我们上面的模版函数举例:

<div id="id1">{{obj.param1}}</div>
被转化为:
// 这里只是举个栗子,了解其原理即可,实际转化的渲染函数当然不是这样的
// 实际渲染函数执行后是生成虚拟dom,这里为了方便理解,直接用真实dom演示
function render() {
document.getElementById('id1').innerHtml = obj.param1
}
 

我们页面中的模板会被转换成渲染函数,页面生成的时候会执行渲染函数,当执行渲染函数的时候我们把这个函数存起来,存在一个全局的变量当中,比如:

let effectStack = []
// 当要执行渲染函数的时候,将渲染函数push到effectStack里面
effectStack.push(render)
// ... 执行渲染函数
// 执行完渲染函数再将render pop
effectStack.pop()
 

所以每当我们执行到一个渲染函数的时候,该渲染函数就会被保存到effectStack中,然后执行渲染函数内部会获取obj.param1,这时候就会走到proxy代理的get方法,在get方法内部会调用track方法,track方法内部会收集依赖,看下track方法的基本原理:

function track(target, key){
const effect =effectStack[effectStack.length-1]; // 获取当前执行的render函数
// 将该函数添加到key对应的依赖当中
// 我们上文定义了一个depsMap来收集所有的依赖关系
depsMap[target].key.push(effect);
这样依赖就收集好了,当然这里为了方便理解原理简写了很多
实际我们可能要先判断depsMap[target]是否存在,不存在则初始化一个对象
然后判断depsMap[target].key是否存在,不存在则初始化一个数组
然后判断effect是否已经在depsMap[target].key对应的数组里了,如果存在的话就不需要重复添加
不过这些都是一些实现细节,我们知道原理就行了
这里最终的原理就是我们执行渲染函数的时候,会现在全局保存这个渲染函数,然后执行渲染函数内部逻辑的时候,
需要获取响应式的数据的时候,就会走到代理的get方法里,在get方法里把这个函数添加到该响应式数据的依赖里,
后面每当该数据变化的时候,就执行依赖里的所有函数
}
 

上面只讲了响应式数据get的时候如何收集依赖,还没有讲响应式数据变化的时候如何执行,如果上面的都理解了的话,其实响应式数据变化的处理逻辑就很简单了,直接看下代码:

// reactive函数使用proxy将target转化为响应式,并返回代理过后的对象observed
// 每当外部访问observed对象里的值,就会走到getter函数里面
function reactive(target){
const observed = new Proxy(target, {
get(target,key){
const res = target[key]
// 当获取值needReactiveObject.param1, 会触发getter
// 然后在getter里进行依赖收集,依赖收集的代码写在track里面
track(target,key) // !!!重要,该函数进行依赖收集
return typeof res==='object'?reactive(res):res
},
set(target,key,val){
target[key] = val
// 响应式去通知变化 触发执行effect
trigger(target,key) //!!!重要代码
}
})
return observed;
}
 

上面代码中,每当改变数据,就会走到proxy的set方法,在set方法里调用trigger执行相关更新函数:

function trigger(target, key,info){
// 将依赖该数据的所有函数都执行一遍
depsMap[target].key.forEach(effect => effect())
}
 

以上就是reactive Api实现响应式的原理,下面来看下另一个重要的api:

efect的原理

我们在代码中可能会这么写:

// 从vue里import该api
import {reactive, effect} from 'vue3';
// 需要设置为响应式的对象
const needReactiveObject = {
param1: ''
}
// 使用reactiveapi将needReactiveObject设置为响应式
const obj = reactive(needReactiveObject)
// !!!关键代码
effect(() => {
console.log('输出param1:', obj.param1)
})
 

副作用

effect是一个函数,其参数也是一个函数,其执行的是一些副作用的代码,这个怎么理解呢?当我们改变了obj.param1的值,正常来讲改变obj.param1的值的结果就是obj.param1的值改变了,比如说从1变成了2,但是呢因为effect里面的函数依赖了param1,当obj.param1的值改变的时候,它产生的行为不仅仅是它的值改变了,它还执行了effect里的函数,输出了console.log。这就是副作用,就是说obj.param1改变这个行为还引发了其它的事情,可以理解为引发了其它的副作用。

effect Api的作用

当effect里面的函数,依赖的可响应值发生变化的时候,会执行effect里的函数。在上面的代码中,也就是当obj.param1变化的时候,就会执行一次console.log

effect原理

如果你理解了上文说的reactive中执行渲染函数的原理,那么就能很容易理解effect的原理了。当我们执行effect函数的时候,会将effect里的函数保存在一个全局变量里面effectStack,然后执行effect函数,当effect函数里面获取obj.param1的时候,就会走到proxy的get,然后就会进行依赖收集,将effect收集到obj.param1的依赖数组里面,当obj.param1改变的时候就会从数组里面取出effect执行。这个和上文说的渲染函数的逻辑是一样的,可以认为渲染函数就是一个effect,当值改变的时候就会执行渲染函数更新页面,这也是执行了副作用。

以上就是我对reactive和effect的理解。

完整参考代码:

const baseHandler = {
get(target,key){
// Reflect.get
// const res = target[key]
const res = Reflect.get(target,key)
// 尝试获取值obj.age, 触发了getter
track(target,key)
return typeof res==='object'?reactive(res):res
},
set(target,key,val){
const info = {oldValue:target[key],newValue:val}
// Reflect.set
// target[key] = val
const res = Reflect.set(target,key,val)
// @todo 响应式去通知变化 触发执行effect
trigger(target,key,info)
}
}
// o.age+=1
function reactive(target){
// vue3还需要考虑Map这些对象
const observed = new Proxy(target, baseHandler)
// 返回proxy代理后的对象
console.log(targetMap)
return observed
}
function computed(fn){
// 特殊的effect
const runner = effect(fn, {computed:true, lazy:true})
return {
effect:runner,
get value(){
return runner()
}
}
}
function effect(fn,options={}){
// 依赖函数 
let e = createReactiveEffect(fn,options)
// lazy仕computed配置的
if(!options.lazy){
// 不是懒执行
e()
}
return e
}
function createReactiveEffect(fn, options){
// 构造固定格式的effect
const effect = function effect(...args){
return run(effect,fn,args)
}
// effect的配置
effect.deps = []
effect.computed = options.computed
effect.lazy = options.lazy
return effect
}
function run(effect,fn,args){
// 执行effect
// 取出effect 执行
if(effectStack.indexOf(effect)===-1){
try{
effectStack.push(effect)
return fn(...args) // 执行effect
}finally{
effectStack.pop() // effect执行完毕
}
}
}
let effectStack = [] // 存储effect
let targetMap = new WeakMap()
function track(target, key){
// 收集依赖
const effect =effectStack[effectStack.length-1]
if(effect){
let depMap = targetMap.get(target)
if(depMap===undefined){
depMap = new Map()
targetMap.set(target,depMap)
}
let dep = depMap.get(key)
if(dep===undefined){
dep = new Set()
depMap.set(key,dep)
}
// 容错
if(!dep.has(effect)){
// 新增依赖
// 双向存储 方便查找优化
dep.add(effect)
effect.deps.push(dep)
}
}
}
// 怎么收集依赖,用一个巨大的map来手机
// {
//   target1:{
// age,name
//     key: [包装之后的effect依赖的函数1,依赖的函数2]
//   }
//   target2:{
//     key2:
//   }
// }
function trigger(target, key,info){
// 数据变化后,通知更新 执行effect
// 1. 找到依赖
const depMap = targetMap.get(target)
if(depMap===undefined){
return
}
// 分开,普通的effect,和computed又一个优先级
// effects先执行,computet后执行
// 因为computed会可能依赖普通的effects
const effects = new Set()
const computedRunners = new Set()
if(key){
let deps = depMap.get(key)
deps.forEach(effect=>{
if(effect.computed){
computedRunners.add(effect)
}else{
effects.add(effect)
}
})
effects.forEach(effect=>effect())
computedRunners.forEach(computed=>computed())
}
}
 

本文地址:H5W3 » Vue3.0响应式原理

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址