Vue3.0数据响应式原理详解


Posted in Javascript onOctober 09, 2019

基于Vue3.0发布在GitHub上的第一版源码(2019.10.05)整理

预备知识

  1. ES6 Proxy,整个响应式系统的基础。
  2. 新的composition-API的基本使用,目前还没有中文文档,可以先通过这个仓库(composition-api-rfc)了解,里面也有对应的在线文档。

先把Vue3.0跑起来

先把vue-next仓库的代码clone下来,安装依赖然后构建一下,vue的package下的dist目录下找到构建的脚本,引入脚本即可。
下面一个简单计数器的DEMO:

<!DOCTYPE html>
<html lang="en">
<body>
 <div id='app'></div>
</body>
<script src="./dist/vue.global.js"></script>
<script>
const { createApp, reactive, computed } = Vue;

const RootComponent = {
 template: `
  <button @click="increment">
   Count is: {{ state.count }}
  </button>
 `,
 setup() {
  const state = reactive({
   count: 0,
  })

  function increment() {
   state.count++
  }

  return {
   state,
   increment
  }
 }
}

createApp().mount(RootComponent, '#app')
</script>
</html>

template和之前一样,同样Vue3也支持手写render的写法,template和render同时存在的情况,优先render。

setup选项是新增的主要变动,顾名思义,setup函数会在组件挂载前(beforeCreate和created生命周期之间)运行一次,类似组件初始化的作用,setup需要返回一个对象或者函数。返回对象会被赋值给组件实例的renderContext,在组件的模板作用域可以被访问到,类似data的返回值。返回函数会被当做是组件的render。具体可以细看文档。

reactive的作用是将对象包装成响应式对象,通过Proxy代理后的对象。

上面的计数器的例子,在组件的setup函数中,创建了一个响应式对象state包含一个count属性。然后创建了一个increment递增的函数,最后将state和increment返回给作用域,这样template里的button按钮就能访问到increment函数绑定到点击的回调,count也能显示在按钮上。我们点击按钮,按钮上的数值就能跟着递增。

下面切入正题,我们就来探究下按钮上count值跟着响应式更新的原理

数据结构

首先列一下主要的一些数据结构,先列在这里,后面提到可以翻回来看看。

ReactiveEffect 一个Function对象,用于执行组件的挂载和更新。

interface ReactiveEffect {
 (): any
 isEffect: true
 active: boolean
 raw: Function // 具体执行的函数
 deps: Array<Dep>
 computed?: boolean
 scheduler?: (run: Function) => void
 onTrack?: (event: DebuggerEvent) => void
 onTrigger?: (event: DebuggerEvent) => void
 onStop?: () => void
}

targetMap 类似 {target -> key -> dep}的一个Map结构,用于缓存所有响应式对象和依赖收集。

export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<string | symbol, Dep>
export const targetMap: WeakMap<any, KeyToDepMap> = new WeakMap()

Proxy代理拦截

reactive函数执行,会将传入的target对象通过Proxy包装,拦截它的get,set等,并将代理的target缓存到targetMap,targetMap.set(target, new Map())。

代理的get的时候会调用一个track函数,而set会调用一个triger函数。分别对应依赖收集和触发更新。

// Proxy get 简化
function get(target: any, key: string | symbol, receiver: any) {
 // 通过key拿到原始值res
 const res = Reflect.get(target, key, receiver)
 // 过滤不需要代理的情况
 // ...
 // 依赖收集
 track(target, OperationTypes.GET, key)
 // 如果取到的值是个对象,将对象再代理包装一下
 // Proxy只能代理对象第一层级
 return isObject(res) ? reactive(res) : res
}

// Proxy set 简化
function set(
 target: any,
 key: string | symbol,
 value: any,
 receiver: any
): boolean {
 // 一些不需要代理设置的场景
 // ...

 // 设置原始对象的值
 const result = Reflect.set(target, key, value, receiver)
 // 避免重复trigger的逻辑
 // ...
 // 触发通知更新
 trigger(target, '更新的类型, 新增key或更新key', key)
 return result
}

依赖收集和触发更新

组件在render阶段,视图会读取数据对象上的值进行渲染,此时便触发了Proxy的get,由此触发对应的track函数,记录下了对应的ReactiveEffect,也就是常说的依赖收集。

ReactiveEffect其实就可以看作是组件的更新(mount是特殊的update),数据的变更触发trigger,trigger遍历调用track收集的对应的数据的ReactiveEffect,也就是对应有关联的组件的更新。

trigger触发的组件的更新,在render阶段又触发了新一轮的track依赖收集,更新依赖。

// 简化的 track
function track(
 target: any,
 type: OperationTypes,
 key?: string | symbol
) {
 // 只有在依赖收集阶段才进行依赖收集
 // 除了render,其他场景也可能会触发Proxy的get,但不需要进行依赖收集
 // activeReactiveEffectStack栈顶包装了当前render的组件的mount和update的逻辑
 const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1]
 // 如果effect为空,说明当前不在render阶段
 if (effect) {
  // ...
  // =====>初始化对应{target -> key -> dep}的结构
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
   targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key as string | symbol)
  if (!dep) {
   depsMap.set(key as string | symbol, (dep = new Set()))
  }
  // <=====初始化对应{target -> key -> dep}的结构
  // 依赖列表里如果没有,add
  if (!dep.has(effect)) {
   // 这里将effect作为依赖,缓存到依赖列表
   dep.add(effect)
   effect.deps.push(dep)
  }
 }
}

// 简化的trigger
function trigger(
 target: any,
 type: OperationTypes,
 key?: string | symbol,
 extraInfo?: any
) {
 // 获取对应target在track过程中缓存的依赖
 const depsMap = targetMap.get(target)

 const effects: Set<ReactiveEffect> = new Set()
 // 省略分类逻辑
 depsMap.forEach(dep => {
  // 将effect分类过滤添加到effects
 })
 
 const run = (effect: ReactiveEffect) => {
  // 有个异步调度的过程,nextTick
  scheduleRun(effect, target, type, key, extraInfo)
 }

 effects.forEach(run)
}

大致流程:

Vue3.0数据响应式原理详解

总结

现在的代码只有新特性的实现,而且ES6+TS的组合可读性大大提高,编辑器支持也很好,所以相对会好读很多。这里只是简单的理了一下vue 3.0 reactive的整体流程,细节还有很多地方值得学习,继续加油。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
一组JS创建和操作表格的函数集合
May 07 Javascript
js优化针对IE6.0起作用(详细整理)
Dec 25 Javascript
用js写了一个类似php的print_r输出换行功能
Feb 18 Javascript
js动态为代码着色显示行号
May 29 Javascript
jQuery点击按钮弹出遮罩层且内容居中特效
Dec 14 Javascript
JavaScript优化专题之Loading and Execution加载和运行
Jan 20 Javascript
利用select实现年月日三级联动的日期选择效果【推荐】
Dec 13 Javascript
基于JS实现移动端向左滑动出现删除按钮功能
Feb 22 Javascript
简单的JS控制button颜色随点击更改的实现方法
Apr 17 Javascript
vue-cli脚手架打包静态资源请求出错的原因与解决
Jun 06 Javascript
关于vue里页面的缓存详解
Nov 04 Javascript
VUE之图片Base64编码使用ElementUI组件上传
Apr 09 Vue.js
Vue分页插件的前后端配置与使用
Oct 09 #Javascript
vue3修改link标签默认icon无效问题详解
Oct 09 #Javascript
将RGB值转换为灰度值的简单算法
Oct 09 #Javascript
基于Vue 撸一个指令实现拖拽功能
Oct 09 #Javascript
解决Vue动态加载本地图片问题
Oct 09 #Javascript
Vue3 中的数据侦测的实现
Oct 09 #Javascript
vue3实现v-model原理详解
Oct 09 #Javascript
You might like
PHP 源代码压缩小工具
2009/12/22 PHP
php表单提交问题的解决方法
2011/04/12 PHP
PHP和Shell实现检查SAMBA与NFS Server是否存在
2015/01/07 PHP
PHP的图像处理实例小结【文字水印、图片水印、压缩图像等】
2019/12/20 PHP
特殊字符、常规符号及其代码对照表
2006/06/26 Javascript
JavaScript词法作用域与调用对象深入理解
2012/11/29 Javascript
jquery必须知道的一些常用特效方法及使用示例(整理)
2013/06/24 Javascript
JavaScript使用HTML5的window.postMessage实现跨域通信例子
2014/04/11 Javascript
TinyMCE提交AjaxForm获取不到数据的解决方法
2015/03/05 Javascript
js简单的点击返回顶部效果实现方法
2015/04/10 Javascript
gameboy网页闯关游戏(riddle webgame)--仿微信聊天的前端页面设计和难点
2016/02/21 Javascript
JS命令模式例子之菜单程序
2016/10/10 Javascript
jQuery学习笔记之入门
2016/12/14 Javascript
微信小程序 scroll-view组件实现列表页实例代码
2016/12/14 Javascript
关于vue.extend和vue.component的区别浅析
2017/08/16 Javascript
seajs下require书写约定实例分析
2018/05/16 Javascript
深入理解JavaScript 中的执行上下文和执行栈
2018/10/23 Javascript
使用webpack搭建vue项目实现脚手架功能
2019/03/15 Javascript
jQuery ajax仿Google自动提示SearchSuggess功能示例
2019/03/28 jQuery
Vue SSR 即时编译技术的实现
2020/05/06 Javascript
[01:39:42]Fnatic vs Mineski 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/18 DOTA
python妹子图简单爬虫实例
2015/07/07 Python
Python的string模块中的Template类字符串模板用法
2016/06/27 Python
解决pytorch报错:AssertionError: Invalid device id的问题
2020/01/10 Python
python中os包的用法
2020/06/01 Python
亚马逊墨西哥站:Amazon.com.mx
2018/08/26 全球购物
PUMA澳大利亚官方网站:德国运动品牌
2018/10/19 全球购物
美国在线家具网站:GDFStudio
2021/03/13 全球购物
董事长职责范文
2013/11/08 职场文书
大学生职业生涯规划书模板
2014/01/18 职场文书
创业计划书模版
2014/02/05 职场文书
蟋蟀的住宅教学反思
2014/04/26 职场文书
庆祝国庆节演讲稿2014
2014/09/19 职场文书
单方离婚协议书范本2014
2014/10/28 职场文书
公司股份转让协议书范本
2015/01/28 职场文书
Nginx禁止ip访问或非法域名访问
2022/04/07 Servers