vue - 响应式原理梳理(一)
描述
我们通过一个简单的 Vue应用 来演示 Vue的响应式属性:
html:<div id="app">{{message}}</div>js:let vm = new Vue({el: '#app',data: {message: '123'}})
在应用中,message 属性即为 响应式属性。
我们通过 vm.message, vm.$data.message, 可访问 响应式属性 message。
当我们通过修改 vm.message(vm.message = '456'), 修改后的数据会 更新到UI界面中。
问题
- 为什么修改 vm.message, 即可触发 UI更新;
- vm.message 和 data.message 的关联关系;
官方介绍
vue的官网文档,对响应式属性的原理有一个介绍。
把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。
每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
官方文档
以上介绍,只是对响应式原理进行了简单描述,并没有深入细节。因此本文在源码层面,对响应式原理进行梳理,对关键步骤进行解析。
响应式原理涉及到的关键步骤如下:
- 构建vue实例;
- vue实例data属性初始化,构建响应式属性;
- 将vue实例对应的template编译为render函数;
- 构建vue实例的watcher对象;
- 执行render函数,构建VNode节点树,同时建立响应式属性和watcher对象的依赖关系;
- 将VNode节点渲染为dom节点树;
- 修改响应式属性,触发watcher的更新,重新执行render函数,生成新的VNode节点树;
- 对比新旧Vnode,重新渲染dom节点树;
构造函数 - Vue
Vue.js 给我们提供了一个 全局构造函数 Vue。
通过 new Vue(options) 生成一个 vue实例,从而可以构建一个 Vue应用。
其中,options 为构造vue实例的配置项,即为 { data, methods, computed, filter ... }
/*options:{data: {...},methods: {...},computed: {...},watch: {...}...}*/function Vue (options) {if (process.env.NODE_ENV !== 'production' &&!(this instanceof Vue)) {warn('Vue is a constructor and should be called with the `new` keyword')}// 根据options, 初始化vue实例this._init(options)}export default Vue;
vue实例 构造完毕之后,执行实例私有方法 _init(), 开始初始化。
在一个 vue应用 中,存在两种类型的 vue实例 :根vue实例 和 组件vue实例。
根vue实例,由构造函数 Vue 生成。
组件vue实例,由组件构造函数 VueComponent 生成,组件构造函数 继承 自构造函数 Vue。
// 全局方法extend, 会返回一个组件构造函数。Vue.extend = function(options) {...// 组件构造函数,用于创建组件var Sub = function VueComponent(options) {this._init(options);};// 子类的prototype继承自Vue的prototype// 相当于Sub实例可以使用Vue实例的方法Sub.prototype = Object.create(Vue.prototype);...return Sub;}
通过一个 根vue实例 和多个 组件vue实例,构成了整个 Vue应用。
Vue.prototype._init
在_init方法中,vue实例会执行一系列初始化操作。
在初始化过程中, 我们通过全局方法 initState 来初始化vue实例的 data、props、methods、computed、watch 属性。
Vue.prototype._init = function(options) {var vm = this;... // 其他初始化过程, 包括建立子vue实例和父vue实例的对应关系、给vue实例添加自定义事件、执行beforeCreated回调函数等// 初始化props属性、data属性、methods属性、computed属性、watch属性initState(vm);... // 其他初始化过程,比如执行created回调函数// vue实例初始化完成以后,挂载vue实例,将模板渲染成htmlif(vm.$options.el) {vm.$mount(vm.$options.el);}};function initState (vm: Component) {vm._watchers = [];// new Vue(options) 中的 optionsconst opts = vm.$options; // 将props配置项中属性转化为vue实例的响应式属性if (opts.props) initProps(vm, opts.props); // 将 methods配置项中的方法添加到 vue实例对象中if (opts.methods) initMethods(vm, opts.methods);// 将data配置项中的属性转化为vue实例的响应式属性if (opts.data) {initData(vm)} else {observe(vm._data = {}, true /* asRootData */)}...}
其中,initData 方法会将 data配置项 中的属性全部转化为 vue实例 的 响应式属性。
initData
initData 方法的主要过程:
- 根据data配置项,创建vue实例的私有属性: _data。
- 通过 observe 方法,将 _data 对象中的属性转化为 响应式属性。
- 通过全局方法proxy, 建立 vue实例 和 _data 的关联关系。
function initData(vm) {// 获取data配置项对象var data = vm.$options.data;// 组件实例的data配置项是一个函数data = vm._data = typeof data === 'function'? getData(data, vm): data || {};// 获取data配置项的属性值var keys = Object.keys(data);// 获取props配置项的属性值var props = vm.$options.props;// 获取methods配置项的属性值;var methods = vm.$options.methods;var i = keys.length;while(i--) {var key = keys[i];{// methods配置项和data配置项中的属性不能同名if(methods && hasOwn(methods, key)) {warn(("method \"" + key + "\" has already been defined as a data property."),vm);}}// props配置项和data配置项中的属性不能同名if(props && hasOwn(props, key)) {"development" !== 'production' && warn("The data property \"" + key + "\" is already declared as a prop. " +"Use prop default value instead.",vm);} else if(!isReserved(key)) { // 如果属性不是$,_ 开头(vue的保留属性)// 建立 vue实例 和 _data 的关联关系性proxy(vm, "_data", key);}}// 观察data对象, 将对象属性全部转化为响应式属性observe(data, true /* asRootData */);}
observe
全局方法 observe 的作用是用来观察一个对象,将_data对象的属性全部转化为 响应式属性。
// observe(_data, true)function observe(value, asRootData) {if(!isObject(value)) {return}var ob;...// ob = new Observer(value);...return ob;}var Observer = function Observer(value) {...if(Array.isArray(value)) {// 如果value是数组,对数组每一个元素执行observe方法this.observeArray(value);} else {// 如果value是对象, 遍历对象的每一个属性, 将属性转化为响应式属性this.walk(value);}};// 如果要观察的对象时数组, 遍历数组,然后调用observe方法将对象的属性转化为响应式属性Observer.prototype.observeArray = function observeArray(items) {for(var i = 0, l = items.length; i < l; i++) {observe(items[i]);}};// 遍历obj的属性,将obj对象的属性转化为响应式属性Observer.prototype.walk = function walk(obj) {var keys = Object.keys(obj);for(var i = 0; i < keys.length; i++) {// 给obj的每一个属性都赋予getter/setter方法。// 这样一旦属性被访问或者更新,这样我们就可以追踪到这些变化defineReactive(obj, keys[i], obj[keys[i]]);}};
defineReactive
通过 defineProperty 方法, 提供属性的 getter/setter 方法。
读取 属性时,触发 getter,将与响应式属性相关的vue实例保存起来。
修改 属性时,触发 setter,更新与响应式属性相关的vue实例。
function defineReactive(obj, key, val, customSetter, shallow) {// 每一个响应式属性都会有一个 Dep对象实例, 该对象实例会存储订阅它的Watcher对象实例var dep = new Dep();// 获取对象属性key的描述对象var property = Object.getOwnPropertyDescriptor(obj, key);// 如果属性是不可配置的,则直接返回if(property && property.configurable === false) {return}// 属性原来的getter/settervar getter = property && property.get;var setter = property && property.set;// 如果属性值是一个对象,递归观察属性值,var childOb = !shallow && observe(val);// 重新定义对象obj的属性keyObject.defineProperty(obj, key, {enumerable : true,configurable : true,get : function reactiveGetter() {// 当obj的某个属性被访问的时候,就会调用getter方法。var value = getter ? getter.call(obj) : val;// 当Dep.target不为空时,调用dep.depend 和 childOb.dep.depend方法做依赖收集if(Dep.target) {// 通过dep对象, 收集依赖关系dep.depend();if(childOb) {childOb.dep.depend();}// 如果访问的是一个数组, 则会遍历这个数组, 收集数组元素的依赖if(Array.isArray(value)) {dependArray(value);}}return value},set : function reactiveSetter(newVal) {// 当改变obj的属性是,就会调用setter方法。这是就会调用dep.notify方法进行通知var value = getter ? getter.call(obj) : val;/* eslint-disable no-self-compare */if(newVal === value || (newVal !== newVal && value !== value)) {return}/* eslint-enable no-self-compare */if("development" !== 'production' && customSetter) {customSetter();}if(setter) {setter.call(obj, newVal);} else {val = newVal;}childOb = !shallow && observe(newVal);// 当响应式属性发生修改时,通过dep对象通知依赖的vue实例进行更新dep.notify();}});}
响应式属性, 通过一个 dep 对象, 收集依赖响应式属性的vue实例,在属性改变时 通知vue实例更新。
一个 响应式属性, 对应一个 dep 对象。
Dep
在观察者设计模式中,有两种角色:Subject 和 Observer。
Subject 会维护一个 Observer的依赖列表。当 Subject 发生变化时,会通知 Observer 更新。
在vue中,响应式属性作为Subject, vue实例作为Observer, 响应式属性的更新会通知vue实例更新。
响应式属性通过 dep 对象来收集 依赖关系 。一个响应式属性,对应一个dep对象。
var Dep = function Dep() {// dep对象的idthis.id = uid++;// 数组,用来存储依赖响应式属性的Observerthis.subs = [];};// 将Observer添加到dep对象的依赖列表中Dep.prototype.addSub = function addSub(sub) {// Dep对象实例添加订阅它的Watcherthis.subs.push(sub);};// 将Observer从dep对象的依赖列表中删除Dep.prototype.removeSub = function removeSub(sub) {// Dep对象实例移除订阅它的Watcherremove(this.subs, sub);};// 收集依赖关系Dep.prototype.depend = function depend() {// 把当前Dep对象实例添加到当前正在计算的Watcher的依赖中if(Dep.target) {Dep.target.addDep(this);}};// 通知Observer更新Dep.prototype.notify = function notify() {// stabilize the subscriber list firstvar subs = this.subs.slice();// 遍历所有的订阅Watcher,然后调用他们的update方法for(var i = 0, l = subs.length; i < l; i++) {subs[i].update();}};
proxy
通过 defineProperty 方法, 给vue实例对象添加属性,提供属性的 getter/setter 方法。
读取vue实例的属性( data配置项中的同名属性 ), 触发 getter,读取 _data 的同名属性。
修改vue实例的属性( data配置项中的同名属性 ), 触发 setter,修改 _data 的同名属性。
// proxy(vm, _data, 'message')function proxy(target, sourceKey, key) {sharedPropertyDefinition.get = function proxyGetter() {return this[sourceKey][key]};sharedPropertyDefinition.set = function proxySetter(val) {this[sourceKey][key] = val;};Object.defineProperty(target, key, sharedPropertyDefinition);}
通过 proxy 方法,vue实例 可代理私有属性 _data, 即通过 vue实例 可以访问/修改 响应式属性。
总结
结合源码理解, 响应式属性 的原理为:
vue - 响应式原理梳理(一)相关推荐
- Vue响应式原理的简单模型
1.前言 最近在梳理vue响应式的原理,有一些心得,值得写一篇博客出来看看. 其实之前也尝试过了解vue响应式的原理,毕竟现在面试看你用的是vue的话,基本上都会问你几句vue响应式的原理.以往学习这 ...
- 深入了解 Vue 响应式原理(数据拦截)
前言 在上一章节我们已经粗略的分析了整个的Vue 的源码(还在草稿箱,需要梳理清楚才放出来),但是还有很多东西没有深入的去进行分析,我会通过如下几个重要点,进行进一步深入分析. 深入了解 Vue 响应 ...
- vue 数组删除 dome没更新_详解Vue响应式原理
摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...
- Vue 响应式原理(双向数据绑定) 怎样实现 响应式原理?
Vue 响应式原理(双向数据绑定) 怎样实现 响应式原理? 我们在Vue里面,定义在Data里的属性,叫做响应式属性. 每一个vue组件被创建的时候,同时还有一个对象被创建出来了,这个对象我们是看不到 ...
- 手把手教你剖析vue响应式原理,监听数据不再迷茫
Object.defineProperty实现vue响应式原理 一.组件化基础 1."很久以前"的组件化 (1)asp jsp php 时代 (2)nodejs 2.数据驱动视图( ...
- Vue响应式原理(看这一篇就够了)
你肯定听说过Object.denfineProperty或是Proxy\reflect,这的确是在VUE响应式原理中起重要作用的一部分代码,但这远远不能代表整个流程的精妙.上图: 不懂没关系,请往下看 ...
- Vue源码--解读vue响应式原理
原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/ Vue的官方说明里有深入响应式原理这一节.在此官方也提到过: 当你把一个普通的 ...
- Vue响应式原理详细讲解
面试官:请你简单的说说什么是Vue的响应式. 小明:mvvm就是视图模型模型视图,只有数据改变视图就会同时更新. 面试官:说的很好,回去等通知吧. 小明:.... Vue响应式原理 先看官方的说法 简 ...
- 一篇文章带你吃透VUE响应式原理
本篇响应式原理介绍比较长,全文大概1w+字.虽然内容繁杂,但阅读过后,绝对会让你对vue的响应式有更加深刻的理解. 分块阅读,效果更佳.(建议读者有一定vue使用经验和基础再食用) 首先上图,下面这张 ...
最新文章
- java.io.file jar_IDEA Maven 打包运行 jar java.io.FileNotFoundException: 问题?
- c++类与类的关联(Association)关系
- c++中的全排列函数next_permutation()
- 隐藏的图片在浏览器中的请求
- vue如何写原生js_纯技巧向:React, Vue, Rxjs 和原生 JS 代码大乱斗
- linux如何让普通用户有root权限
- HDU3790 最短路径问题【Dijkstra算法】
- 雀巢联手京东,发布人工智能家庭营养健康助手
- mysql主从怎么确保数据一致_如何保证主从复制数据一致性
- 苹果屏蔽更新描述文件_安装iOS屏蔽更新描述文件教程方法
- 来个水帖:下载Dev
- centos Unison+Inotify双向同步
- 干货 | 4步带你完成私有云盘搭建
- 使用LocalStorage存储用户已填写的表单信息(意外刷新后自动填充)
- 中国移动通信互联网短信网关接口协议 (China Mobile Peer to Peer, CMPP) (V2.0)
- jquery获取元素索引值index()
- adb shell dumpsys appops
- BrightlyPro - 照片视屏后期自动调色增亮工具
- 自动批量处理人像照片
- 带宽、速率(波特率、比特率)和码元宽度简述