您现在的位置是:网站首页> 编程资料编程资料
图解Vue 响应式流程及原理_vue.js_
2023-05-24
388人已围观
简介 图解Vue 响应式流程及原理_vue.js_

阅读本文能够帮助你什么?
- 在学习vue源码的时候发现组件化过程很绕?
- 在响应式过程中
Observer、Dep、Watcher三大对象傻傻分不清? - 搞不清楚对象、数组依赖收集、派发更新的流程?
dep、watcher互调造成混乱? - 学了一遍好像懂了又好像不全懂的感觉?而且缺乏大体流程概念?
- 或者像我一样,有段时间没看vue源码好像有点遗忘?但是想快速回顾却无从下手?
本文主要分为1. 组件化;2. 响应式原理;3. 彩蛋(computed和watch)进行讲解。本文调试源码的vue版本是v2.6.14。整篇将采用源码讲解 + 流程图的方式详细还原整个Vue响应式原理的全过程。你可以了解到Dep.target、pushTarget、popTarget;响应式中的三大Watcher;Dep、Wathcer多对多的,互相收集的关系。
这篇是进阶的 Vue 响应式源码解析,文章比较长,内容比较深,大家可以先mark后看。看不懂的不要强行看,可以先看看其他作者的偏简单一点的源码解析文章,然后好好消化。等过段时间再回来看这篇,相信你由浅入深后再看本文,一定会有意想不到的收获~
一、组件化流程
在讲解整个响应式原理之前,先介绍一下Vue中另一个比较核心的概念——组件化,个人认为这也是学习响应式的前置核心。搞懂组件化,响应式学习如虎添翼!
1. 整个new Vue阶段做了什么?
- 执行init操作。包括且不限制
initLifecycle、initState等 - 执行mount。进行元素挂载
- compiler步骤在runtime-only版本中没有。
- compiler步骤对template属性进行编译,生成render函数。
- 一般在项目中是在
.vue文件开发,通过vue-loader处理生成render函数。
执行render。生成vnode
{{ message }}
render (h) { return h('div', { attrs: { id: 'app' }, }, this.message) } - render例子,如下
- 对应手写的render函数
- patch。新旧vnode经过diff后,渲染到真实dom上

2. 普通dom元素如何渲染到页面?
- 执行
$mount。- 实际执行
mountComponent - 这里会实例化一个Watcher
- Watcher中会执行
get方法,触发updateComponent
- 实际执行
- 执行
updateComponent。执行vm._update(vm._render(), hydrating) - 执行
vm.render()。- render其实调用
createElment(h函数) - 根据tag的不同,生成组件、原生VNode并返回
- render其实调用
- 执行
vm.update()。createElm()到createChildren()递归调用 - 将VNode转化为真实的dom,并且最终渲染到页面

3. 组件如何渲染到页面?
这里以如下代码案例讲解更加清晰~没错,就是这么熟悉!就是一个初始化的Vue项目
// mian.js import Vue from 'vue' import App from './App.vue' new Vue({ render: h => h(App), }).$mount('#app') // App.vue{{ msg }}
主要讲解组件跟普通元素的不同之处,主要有2点:
如何生成VNode——创建组件VNodecreateComponent

如何patch——组件new Vue到patch流程createComponent
$vnode:占位符vnode。最终渲染vnode挂载的地方。所有的组件通过递归调用createComponent直至不再存在组件VNode,最终都会转化成普通的dom。
{ tag: 'vue-component-1-App', componentInstance: {组件实例}, componentOptions: {Ctor, ..., } } _vnode:渲染vnode。
{ tag: 'div', { "attrs": { "id": "app" } }, // 对应占位符vnode: $vnode parent: { tag: 'vue-component-1-App', componentInstance: {组件实例}, componentOptions: {Ctor, ..., } }, children: [ // 对应p标签 { tag: 'p', // 对应p标签内的文本节点{{ msg }} children: [{ text: 'hello world' }] }, { // 如果还有组件VNode其实也是一样的 tag: 'vue-component-2-xxx' } ] } (注意:这一步对应上图render流程的紫色块的展开!!!)
区分普通元素VNode
- 普通VNode:tag是html的保留标签,如
tag: 'div' - 组件VNode:tag是以
vue-component开头,如tag: 'vue-component-1-App'
(注意:这一步对应上图patch流程的紫色块的展开!!!)

4. Vue组件化简化流程
相信你看完细粒度的Vue组件化过程可能已经晕头转向了,这里会用一个简化版的流程图进行回顾,加深理解

二、响应式流程
案例代码
// 案例 export default { name: 'App', data () { return { msg: 'hello world', arr = [1, 2, 3] } } } 1. 依赖收集
这里会从Observer、Dep、Watcher三个对象进行讲解,分 object、array 两种依赖收集方式。
- 一定要注意!数组 的依赖收集 跟 对象的属性 是不一样的。对象属性经过深度遍历后,最终就是以一个基本类型的数据为单位收集依赖,但是数组仍然是一个引用类型。
- 如果这里不懂,先想一个问题: 我们用
this.msg = 'xxx'能触发setter派发更新,但是我们修改数组并不是用this.arr = xxx,而是用this.arr.push(xxx)等修改数组的方法。很显然,这时候并不是通过触发arr的setter去派发更新的。那是怎么做的呢?先带着这个问题继续往下看吧!
三个核心对象:Observer(蓝)、Dep(绿)、Watcher(紫)

依赖收集准备阶段——Observer、Dep的实例化
// 以下是initData调用的方法讲解,排列遵循调用顺序 function observe (value, asRootData) { if (!isObject(value)) return // 非对象则不处理 // 实例化Observer对象 var ob; ob = new Observer(value); return ob } function Observer (value) { this.value = value; // 保存当前的data this.dep = new Dep(); // 实例化dep,数组进行依赖收集的dep(对应案例中的arr) def(value, '__ob__', this); if (Array.isArray(value)) { if (hasProto) { // 这里会改写数组原型。__proto__指向重写数组方法的对象 protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } } // 遍历数组元素,执行对每一项调用observe,也就是说数组中有对象会转成响应式对象 Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } } // 遍历对象的全部属性,调用defineReactive Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); // 如案例代码,这里的 keys = ['msg', 'arr'] for (var i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]); } } function defineReactive (obj, key, val) { // 产生一个闭包dep var dep = new Dep(); // 如果val是object类型,递归调用observe,案例代码中的arr会走这个逻辑 var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { get: function reactiveGetter () { // 求value的值 var value = getter ? getter.call(obj) : val; if (Dep.target) { // Dep.target就是当前的Watcher // 这里是闭包dep dep.depend(); if (childOb) { // 案例代码中arr会走到这个逻辑 childOb.dep.depend(); // 这里是Observer里的dep,数组arr在此依赖收集 if (Array.isArray(value)) { dependArray(value); } } } return value }, set: function reactiveSetter (newVal) { // 下文派发更新里进行讲解 } }); } 注意 对象 、 数组 的不同处理方式。这里以 核心代码 + 图 进行讲解
接下来核心分析 defineReactive 做了什么。注意 childOb ,这是数组进行依赖收集的地方(也就是为什么我们 this.arr.push(4) 能找到 Watcher 进行派发更新)

依赖收集触发阶段——Wather实例化、访问数据、触发依赖收集
// new Wathcer核心 function Watcher (vm, expOrFn, cb, options, isRenderWatcher) { if (typeof expOrFn === 'function') { // 渲染watcher中,这里传入的expOrFn是updateComponent = vm.update(vm.render()) // this.getter等价于vm.update(vm.render()) this.getter = expOrFn; } else { ... } // 这里进行判断,lazy为true时(计算属性)则什么都不执行,否则执行get this.value = this.lazy ? undefined : this.get(); // 本次为渲染Watcher,执行get,继续往下看~ } // Watcher的get方法 Watcher.prototype.get = function get () { // 这里很关键,pushTarget就是把当前的Wather赋值给“Dep.target” pushTarget(this); var value; var vm = this.vm; try { // 1. 这里调用getter,也就是执行vm.update(vm.render()) // 2. 执行vm.render函数就会访问到响应式数据,触发get进行依赖收集 // 3. 此时的Dep.target为当前的渲染Watcher,数据就可以理所应当的把Watcher加入自己的subs中 // 4. 所以此时,Watcher就能监测到数据变化,实现响应式 value = this.getter.call(vm, vm); } catch (e) { ... } finally { popTarget(); /* * cleanupDeps是个优化操作,会移除Watcher对本次render没被使用的数据的观测 * 效果:处于v-if为false中的响应式数据改变不会触发Watcher的update * 感兴趣的可以自己去debugger调试,这里就不展开了 */ this.cleanupDeps(); } return value } Dep.target相关讲解
- targetStack:栈结构,用来保存
Watcher - pushTarget:往
targetStack中push当前的Watcher(排在前一个Watcher的后面),并把Dep.target赋值给当前Watcher - popTarget:先把
targetStack最后一个元素弹出(.pop),再把Dep.target赋值给最后一个Watcher(也就是还原了前一个Watcher) - 通过上述实现,vue保证了
全局唯一的Watcher,准确赋值在Dep.target中

细节太多绕晕了?来个整体流程,从宏观角度再过一遍(computed部分可看完彩蛋后再回来重温一下)

相关内容
- Vue.nextTick纯干货使用方法详解_vue.js_
- 微信小程序自定义滚动选择器_javascript技巧_
- 非常全面的12种js数组去重的方法_javascript技巧_
- jquery实现手风琴展开效果_jquery_
- 前端取消请求及取消重复请求方式_JavaScript_
- js前端实现word excel pdf ppt mp4图片文本等文件预览_JavaScript_
- vue中使用echarts实现动态数据绑定以及获取后端接口数据_vue.js_
- 前端vue-cropperjs实现图片裁剪方案_vue.js_
- jquery实现全选功能_jquery_
- React.js前端导出Excel的方式_React_
