草庐IT

【手撕源码】vue3响应式原理解析(文末抽奖)

不叫猫先生 2023-11-26 原文

🐱 个人主页:不叫猫先生
🙋‍♂️ 作者简介:2022年度博客之星前端领域TOP 2,前端领域优质作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀!
💫优质专栏:vue3从入门到精通TypeScript从入门到实践
📢 资料领取:前端进阶资料以及文中源码可以找我免费领取
🔥 前端学习交流:博主建立了一个前端交流群,汇集了各路大神,一起交流学习,期待你的加入!(文末有我wx或者私信)。


目录

一、认识Proxy

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
new Proxy(target,handler)表示生成一个Proxy实例,target参数表示所要拦截的目标对象,它可以是任意类型的对象,包括内置的数组,函数等,handler也是一个对象,用来定制拦截行为,当发生某些操作时触发该对象。

二、原理分析

1.reactive

声明副作用变量,如果该变量没有值就不进行追踪。在 Vue2 的时候,有一个“全局变量”,叫做 Dep.target – watcher,vue3中还要有这么一个全局变量,就是activeEffect。

//副作用变量
let activeEffect; 

targetMap用来存放依赖

let targetMap = new WeakMap();

判断传入的数据data是否为对象,需要除去data为null的情况。因为typeof null === ‘object’,null的机器码都是0,object机器码为000

function isObject(data) {
	return data && typeof data === 'object'
}

声明reactive函数,返回proxy实例。proxy支持get、set、deleteProperty、has、ownKeys等方法。

  • get
    • 通过Reflect.get(target, key, receiver)获取到属性为key的值
    • track收集依赖
    • ret为对象则递归为对象创建proxy代理
  • set
    • Reflect.set(target, key, value, receiver)设置属性为key的值
    • trigger 执行更新

receiver代表当前proxy对象或者继承proxy的对象,保证传递正确的this 给 getter、setter。

export function reactive(data) {
    //判断是否为对象
	if (!isObject(data)) return 
	// 返回proxy实例
	return new Proxy(data, { 
		get(target, key, receiver) {
			const ret = Reflect.get(target, key, receiver);
			// 收集依赖
			track(target, key)
			//如果获取的数据还是对象的话就递归,继续为此对象创建 Proxy 代理
			return isObject(ret) ? reactive(ret) : ret
		},
		//set修改数据,需要返回一个布尔值
		set(target, key, value,receiver) {
           // 首先获取旧值
            const oldValue = Reflect.get(target, key, receiver)
            // 判断新值和旧值是否一样来决定是否更新setter
            let result = true;
            // 当新值不等于旧值的时候执行更新草错
            if (oldValue !== value) {
                result = Reflect.set(target, key, value, receiver)
                // 更新操作
                trigger(target, key)
            }
            //返回布尔类型
            return result
		},
		deleteProperty(target, key) {
		    //首先需要判断是否有要删除的key
		    const hasK = hasKey(target, key)
		    const ret = Reflect.deleteProperty(target, key)
		    //存在key且有值则更新
		    if(hasK&&ret){
		    //更新
			 trigger(target, key)
		    }
			return ret
		},
		has(target, key) {
			track(target, key)
			const ret = Reflect.has(target, key)
		},
		ownKeys(target, key) {
			track(target)
			return Reflect.ownKeys(targety)
		},
	})
}
// 判断对象中key是否存在
const hasKey = (target, key) => Object.prototype.hasOwnProperty.call(target, key)

2.track

track里面会收集各种依赖,把依赖关系做成各种映射的关系,映射关系就叫 targetMap, 内部拿到这个key,就可以通过映射关系找到对应的value,就可以影响这个执行函数,

function track(target, key) {
	// 如果当前没有effect就不执行追踪
    if (!activeEffect) return
    //找target有么有被追踪
	let depsMap = targetMap.get(target);
	//判断target是否为空,如果target为空则没被追踪,则set设置一个值
	if (!depsMap) targetMap.set(target, (depsMap = new Map()));
	//判断depsMap中有没有key,没有key就set一个(判断target.key有没有被追踪)
	let dep = depsMap.get(key)
	// 如果key没有被追踪,那么添加一个
	if (!dep) depsMap.set(key, (dep = new Set()))
	//触发副作用函数
	trackEffect(dep)
}
//副作用函数
function trackEffect(dep) {
	//相当于 Dep.target && dep.add(Dep.target)
	//如果key没有添加activeEffect,则添加一个
	if (!dep.has(activeEffect)) dep.add(activeEffect);
}

3.trigger

修改数据时通过 trigger目标对象找到key,根据映射关系找到cb函数执行更新视图。

function trigger(target, key) {
    // 获取依赖数据,对依赖数据循环,
	const depsMap = targetMap.get(target)
	console.log(depsMap,'depsMap')
	if (!depsMap) return
	//如果effect存在则执行run方法,run方法就是执行的视图更新回调
	depsMap.get(key).forEach(effect =>
		effect && effect.run()
	);
}

4.ref

ref中声明了一个RefImpl类,初始化时传入参数init。

  • get

    • 追踪变量,收集依赖
    • 返回初始化变量的值
  • set

    • 修改旧值,赋新值
    • trigger 更新
export function ref(init) {
	class RefImpl {
		constructor(init) {
		// 接收传过来的参数
			this.__value = init;
		}
		//获取数据,直接返回传过来的数据
		get value() {
			track(this, 'value')
			return this.__value
		}
		//更新数据
		set value(newVal) {
			this.__value = newVal;
			trigger(this, 'value')
		}
	}
	return new RefImpl(init);
}

5.effect

effect第一个参数是函数,如果这个函数中有使用 ref/reactive 对象,当该对象的值改变的时候effect就会执行。

function effect(fn,option={}){
   //effect 数据类型为ReactiveEffect,一上来就会执行run方法,之后可以自定义执行run方法,即设置option的内容
  let __effect = new ReactiveEffect(fn);
  if(!option.lazy){
	  __effect.run();
  }
  return __effect
}

6.ReactiveEffect

声明ReactiveEffect类,间接定义effect的数据类型。

class ReactiveEffect{
	constructor(fn){
		this.fn = fn;
	}
	// 依赖收集之前触发
	run(){
		activeEffect = this;
		return this.fn()
	}
}

7.computed

计算属性

export function computed(fn){
//只考虑函数情况
let __computed;
const e = effect(fn,{lazy:true })
__computed = {
	get value(){
		return e.run();
	}
}
return __computed
}

8.mount

mount的参数instance相当于整个app,el相当于挂在的节点

export function mount(instance,el){
   // 执行effect
	effect(function(){
		instance.$data && update(instance,el)
	})
	instance.$data = instance.setup();
	//更新节点
	update(instance,el)
	function update(instance,el){
	    //挂载节点的render函数返回值内容复制给节点的innerHTML,进行更新
		el.innerHTML = instance.render();
	}
}

三、源码地址

附:源码地址

🌟粉丝福利(抽奖)

《低代码开发实战——基于低代码平台构建企业级应用》
抽奖规则:抽奖助手小程序随机抽奖
活动时间:即日起至2023年4月18日 12:00
温馨提示:参与活动者提前参加博主wx(zbsguilai),以避免错过中奖通知。

有关【手撕源码】vue3响应式原理解析(文末抽奖)的更多相关文章

  1. Ruby 解析字符串 - 2

    我有一个字符串input="maybe(thisis|thatwas)some((nice|ugly)(day|night)|(strange(weather|time)))"Ruby中解析该字符串的最佳方法是什么?我的意思是脚本应该能够像这样构建句子:maybethisissomeuglynightmaybethatwassomenicenightmaybethiswassomestrangetime等等,你明白了......我应该一个字符一个字符地读取字符串并构建一个带有堆栈的状态机来存储括号值以供以后计算,还是有更好的方法?也许为此目的准备了一个开箱即用的库?

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - 用逗号、双引号和编码解析 csv - 2

    我正在使用ruby​​1.9解析以下带有MacRoman字符的csv文件#encoding:ISO-8859-1#csv_parse.csvName,main-dialogue"Marceu","Giveittohimóhe,hiswife."我做了以下解析。require'csv'input_string=File.read("../csv_parse.rb").force_encoding("ISO-8859-1").encode("UTF-8")#=>"Name,main-dialogue\r\n\"Marceu\",\"Giveittohim\x97he,hiswife.\"\

  4. ruby-on-rails - 每次我尝试部署时,我都会得到 - (gcloud.preview.app.deploy) 错误响应 : [4] DEADLINE_EXCEEDED - 2

    我是Google云的新手,我正在尝试对其进行首次部署。我的第一个部署是RubyonRails项目。我基本上是在关注thisguideinthegoogleclouddocumentation.唯一的区别是我使用的是我自己的项目,而不是他们提供的“helloworld”项目。这是我的app.yaml文件runtime:customvm:trueentrypoint:bundleexecrackup-p8080-Eproductionconfig.ruresources:cpu:0.5memory_gb:1.3disk_size_gb:10当我转到我的项目目录并运行gcloudprevie

  5. ruby-on-rails - 我更新了 ruby​​ gems,现在到处都收到解析树错误和弃用警告! - 2

    简而言之错误:NOTE:Gem::SourceIndex#add_specisdeprecated,useSpecification.add_spec.Itwillberemovedonorafter2011-11-01.Gem::SourceIndex#add_speccalledfrom/opt/local/lib/ruby/site_ruby/1.8/rubygems/source_index.rb:91./opt/local/lib/ruby/gems/1.8/gems/rails-2.3.8/lib/rails/gem_dependency.rb:275:in`==':und

  6. UE4 源码阅读:从引擎启动到Receive Begin Play - 2

    一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame

  7. 计算机毕业设计ssm+vue基本微信小程序的小学生兴趣延时班预约小程序 - 2

    项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU

  8. ruby - 用 YAML.load 解析 json 安全吗? - 2

    我正在使用ruby2.1.0我有一个json文件。例如:test.json{"item":[{"apple":1},{"banana":2}]}用YAML.load加载这个文件安全吗?YAML.load(File.read('test.json'))我正在尝试加载一个json或yaml格式的文件。 最佳答案 YAML可以加载JSONYAML.load('{"something":"test","other":4}')=>{"something"=>"test","other"=>4}JSON将无法加载YAML。JSON.load("

  9. ruby-on-rails - 在 Ruby on Rails 中发送响应之前如何等待多个异步操作完成? - 2

    在我做的一些网络开发中,我有多个操作开始,比如对外部API的GET请求,我希望它们同时开始,因为一个不依赖另一个的结果。我希望事情能够在后台运行。我找到了concurrent-rubylibrary这似乎运作良好。通过将其混合到您创建的类中,该类的方法具有在后台线程上运行的异步版本。这导致我编写如下代码,其中FirstAsyncWorker和SecondAsyncWorker是我编写的类,我在其中混合了Concurrent::Async模块,并编写了一个名为“work”的方法来发送HTTP请求:defindexop1_result=FirstAsyncWorker.new.async.

  10. ruby - 如何使用 Nokogiri 解析纯 HTML 表格? - 2

    我想用Nokogiri解析HTML页面。页面的一部分有一个表,它没有使用任何特定的ID。是否可以提取如下内容:Today,3,455,34Today,1,1300,3664Today,10,100000,3444,Yesterday,3454,5656,3Yesterday,3545,1000,10Yesterday,3411,36223,15来自这个HTML:TodayYesterdayQntySizeLengthLengthSizeQnty345534345456563113003664354510001010100000344434113622315

随机推荐