之前学习vue的时候就对$set很感兴趣,但奈何一直都是“小打小闹”,本以为莫非这玩意根本用不到而渐渐淡忘没想到最近项目中接二连三的出现类似的问题让我不得不重视起来。决心探探这“小雷音寺”。

本文主要通过源码解决两个疑惑:

  1. 可以在created中直接挂载属性到data中?这么做有啥好处?
  2. 为什么很多时候数据改变了但是并没有在页面上展示出来?或者说数据就没被更改。数据去哪了?

先解答:
1:created时其实已经可以访问data,数据响应式触发完成。在这里首次挂载属性可以避免这个属性变成响应式的从而增加性能损耗,而且可以全局访问this.xxx
2:数据流和视图流的问题,本质也是数据响应式的问题。解决方案:①新开缓存对象使得属性不是首次挂载的,可以检查数据确保存储的对象存在时某属性就存在;②set、update、splice等强制更新

见微知著:new Vue()时都做了什么?

首先找到vue的构造函数

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')}this._init(options)
}

options是用户传递过来的配置项,如data、methods等常用的方法

vue构建函数调用_init方法,这里不能直接找到这个方法,但是往下看会发现文件下方定定义了很多初始化方法:

initMixin(Vue);     // 定义 _init
stateMixin(Vue);    // 定义 $set $get $delete $watch 等
eventsMixin(Vue);   // 定义事件  $on  $once $off $emit
lifecycleMixin(Vue);// 定义 _update  $forceUpdate  $destroy
renderMixin(Vue);   // 定义 _render 返回虚拟dom

首先看 initMixin 方法,发现该方法在Vue原型上定义了_init方法

Vue.prototype._init = function (options?: Object) {const vm: Component = this// a uidvm._uid = uid++let startTag, endTag/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {startTag = `vue-perf-start:${vm._uid}`endTag = `vue-perf-end:${vm._uid}`mark(startTag)}// a flag to avoid this being observedvm._isVue = true// merge options// 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法if (options && options._isComponent) {// optimize internal component instantiation// since dynamic options merging is pretty slow, and none of the// internal component options needs special treatment.initInternalComponent(vm, options)} else { // 合并vue属性vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)}/* istanbul ignore else */if (process.env.NODE_ENV !== 'production') {// 初始化proxy拦截器initProxy(vm)} else {vm._renderProxy = vm}// expose real selfvm._self = vm// 初始化组件生命周期标志位initLifecycle(vm)// 初始化组件事件侦听initEvents(vm)// 初始化渲染方法initRender(vm)callHook(vm, 'beforeCreate')// 初始化依赖注入内容,在初始化data、props之前initInjections(vm)// 初始化props/data/method/watch/methodsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {vm._name = formatComponentName(vm, false)mark(endTag)measure(`vue ${vm._name} init`, startTag, endTag)}// 挂载元素if (vm.$options.el) {vm.$mount(vm.$options.el)}
}

OK可以停一下了。从上面的代码中可以发现三件事:

  • 在调用 beforeCreate 之前,数据初始化并未完成,像data、props这些属性无法访问到
  • 到了 created 的时候,数据已经初始化完成,能够访问data、props这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素
  • 然后是挂载方法vm.$mount

上面这三点完全可以作为一些面试题诸如“为什么beforeCreate中无法访问this.xxx”、“created中可以访问dom吗”的完美答案。不过个人觉得更应该专注于“它对优化代码质量的作用”。

到这里我们还大概知道了另一件事:“ 因为created中数据初始化(其中最重要的是响应式)已经完成,所以如果在此生命周期中直接将一个新属性挂载到this上,那么它将不是响应式的! ”。

什么是响应式数据?
笔者在另一篇文章中有过说明:“响应式原理是一种单向行为:它是数据到 DOM (也就是view视图)的映射”。这里可以简单的归结为“ 和渲染流相关的数据 ”。

那么使用起来就很明了了:我们可以把一些非主动直接触发的数据或者是不需要在页面上展示的数据在created生命周期中首次定义。这样既可以在js中获取到又不必担心页面上的展示问题。
比如 —— 在笔者近期的项目中对缓存函数就是这样使用的:

//js文件
export function cachedMemory(fn, wait = 10){const cache = Object.create(null);let last = Date.now();return async (...args) => {const _args = JSON.stringify(args);let now = Date.now();const hit = cache[_args];if(now - last < wait*1000 && hit) {   //缓存10s,因为场景不同,这里一般来说是要每次点击都请求的,但是如果点的特别快就会造成短时间内大量重复请求return hit;}// 只缓存成功的promise,失败直接重新请求last = now;return (cache[_args] = await fn.apply(fn, args));}
}
//调用方
<script>
import { cachedMemory } from 'xxx.js';
export default {data() {return {}},created() {this.requestListResData = cachedMemory(data => {   //注册缓存函数return this.$$APIS.getMarkupId(data);})this.init();},methods: {xxx() {const data = await this.requestListResData(item_data);//下一步操作}}
}
</script>

步入正题:$set和数据响应

依然提前“透题”:既然数据响应式一定是和defineProperty相关(vue3换成proxy了,但基本逻辑不变)。那么$set中也一定是主动触发了这个函数!

由上面的生命周期的例子可以知道:vue对于this,或者说(响应式的)data上没有声明过的属性是不会有响应式的;那么依然大胆推广开来 —— “初始时没有在某个对象上存在的属性,渲染(该对象)时不会将其作为响应式数据!”

事实证明,这个假设是完全正确的。
官方文档中说:“如果在实例创建之后添加新的属性到实例上,它不会触发视图更新”。

why?
简单来说,vue初始流程走完以后会在你要渲染的对象中绑上一个__ob__原型,里面至少会有一个id,这相当于一个缓存;和一个value对象,保存响应式处理的数据。在后面会有用到。
如果是再次挂载的属性,新的属性并不会在 ob的value 中出现。如果将这个属性拿到页面上渲染则再次改变时并不会触发!(但数据会改变)

笔者遇到这个问题是因为在使用elementui时一堆数据中点击每一条数据前面的checkbox发现竟然没有点击效果!但断点后发现表示选中状态的属性值已然改变,,,开始还以为是数据没有渲染或者中间 深拷贝 了源数据导致更改的不是源数据。

所以,响应式的数据一定要保证对象初始化时就已经存在,可以给一个毫不相干的默认值嘛。(这是笔者所遇问题的解决方法,应该也可以说是大部分类似问题的解决方法吧)

笔者的项目中并没有用到set方法是因为我在改变了数组属性后再去调用$set竟然没有效果。我发现,这里面有一个内部的缓存问题 —— 在笔者的项目中,每一行是一个单独的组件,所有行是一个大组件。(上面的解决方法就是直接在大组件的‘源数据’处做修改)大组件中循环数组给小组件传入每一个item
在这种情况下,即使用set触发了响应式,将新增数组挂载到ob原型的value对象上,也不能渲染到页面上!
这时候可以尝试将小组件的数据重新“拿一遍”就好。不过太过麻烦。一个组件中有太多的数据流向简直是灾难!

但是,个例的情况并不能掩盖set的优秀。让我们看下在set中做了什么:

set源码分析

if (process.env.NODE_ENV !== 'production' &&(isUndef(target) || isPrimitive(target))) {warn('Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}')
}
// 数组
if (Array.isArray(target) && isValidArrayIndex(key)) {target.length = Math.max(target.length, key)target.splice(key, 1, val)return val
}
// 对象
if (key in target && !(key in Object.prototype)) {target[key] = valreturn val
}
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {process.env.NODE_ENV !== 'production' && warn('Avoid adding reactive properties to a Vue instance or its root $data ' +'at runtime - declare it upfront in the data option.')return val
}
if (!ob) {target[key] = valreturn val
}
defineReactive(ob.value, key, val)
ob.dep.notify()

代码中我们只看最重要的两点:
一个是

const ob = (target: any).__ob__

拿到 ob 原型,证实了我刚刚说的一点。如果一个对象有这个__ob__属性,那么就说明这个对象是响应式对象,我们修改里面已有属性的时候就会触发页面渲染。

另一个是最后两步

defineReactive(ob.value, key, val)
ob.dep.notify()

这个是vue.set()真正处理对象的地方。defineReactive(ob.value, key, val)是尤大封装的方法,内部逻辑就是循环调用defineProperty给新加的属性添加依赖,以后再直接修改这个新的属性的时候就会触发页面渲染。 ob.dep.notify()这句代码的意思是触发当前的依赖(观察者模式),所以页面就会进行重新渲染。

有的文章不知道在哪抄的,竟然说可以直接用xxx.splice()解决问题,先不说splice方法可不可以丢失参数。这么做也只是触发了数组的“重新改变”。
与此原理相似的是在开头处做:let xxx=[...xxx]; 如果你的这个“开头处”是数组的第一次渲染,也就是需要响应式属性的“一次挂载”,那么确实可以。关于数据是“一次挂载”还是“二次挂载”的,这个需要注意!
当然,上面的说法是相对于vue2来说的,因为他本质上是因为Object.defineProperty API无法监听到数组/对象内部(新)元素的改变导致的。但是在vue3中尤大将其替换为更高维度的Proxy API。这个问题就不会出现了。

误区一

有些人可能会以为“既然$nextTick会强制触发下一次渲染,那么直接在nextTick方法中改变数据不就可以了?”

其实简单来说可以认为“vue知道该渲染数据了但是不知道你改变了哪些数据”;复杂点说其实这根本就是两码事,它们(数据流和渲染流)之间还有一个顺序和时间问题。

一般笔者在vue的项目中是这样使用这个API的:在接口数据和封装的组件内部有数据交互时,在组件中暴露一个方法。外部在$nextTick中调用此方法并传入数据 ——

//子组件
<script>
export default {methods: {$_setData(list) {this.list = list || [];},}
}
</script>
//父组件中
this.$xxx.$_setData(purchaseList);

误区二

得益于js对象的强大特性,我们不仅可以随意地给对象增删属性,还可以对其进行 深拷贝浅拷贝。这可能是一些问题的导火索 —— (源)数据为什么没改变?

前面加了。这很简单,也很关键。深拷贝开辟了一个新的内存地址,最简单也是最常用的如:JSON.parse(JSON.stringify(xxx)) 可以创造一个完全的新对象。在这个对象上做的修改不会“反馈到”xxx数据中!

vue源码解析之『我的数据去哪了』相关推荐

  1. 【vuejs深入三】vue源码解析之二 htmlParse解析器的实现

    写在前面 一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残. 昨天博主分析了一下在vue中,最为基础核心的api,parse函数,它的作用是将vue的模板字符串转换成ast,从而 ...

  2. [Vue源码解析] patching算法

    [Vue源码解析] patching算法 pathching算法:通过对比新旧VNode的不同,然后找出需要更新的节点进行更新 操作:1.创建新增节点 2.删除废弃节点 3.修改需要更新的节点 创建节 ...

  3. Vue源码解析(尚硅谷)

    视频地址:Vue源码解析系列课程 一.Vue源码解析之mustache模板引擎 1. 什么是模板引擎 模板引擎是将数据要变为视图最优雅的解决方案 历史上曾经出现的数据变为视图的方法 2. mustac ...

  4. Vue源码解析(一)

    前言:接触vue已经有一段时间了,前面也写了几篇关于vue全家桶的内容,感兴趣的小伙伴可以去看看,刚接触的时候就想去膜拜一下源码~可每次鼓起勇气去看vue源码的时候,当看到几万行代码的时候就直接望而却 ...

  5. Vue源码解析之数组变异

    力有不逮的对象 众所周知,在 Vue 中,直接修改对象属性的值无法触发响应式.当你直接修改了对象属性的值,你会发现,只有数据改了,但是页面内容并没有改变. 这是什么原因? 原因在于: Vue 的响应式 ...

  6. Vue源码解析:虚拟dom比较原理

    通过对 Vue2.0 源码阅读,想写一写自己的理解,能力有限故从尤大佬2016.4.11第一次提交开始读,准备陆续写: 模版字符串转AST语法树 AST语法树转render函数 Vue双向绑定原理 V ...

  7. Vue源码解析之Template转化为AST的实现方法

    什么是AST 在Vue的mount过程中,template会被编译成AST语法树,AST是指抽象语法树(abstract syntax tree或者缩写为AST),或者语法树(syntax tree) ...

  8. Vue源码解析之函数入口

    从入口开始看起 写博客就是记录自己撸码的过程和问题,好了~废话就不多说了,直接源码撸起,通过上一篇博客咱们大致知道了Vue源码目录设计,下面我们要一步步找到vue的入口 通过查看package.jso ...

  9. Vue源码解析(笔记)

    github vue源码分析 认识flow flow类型检查 安装flow sudo npm install -g flow-bin 初始化flow flow init 运行flow命令 命令: fl ...

最新文章

  1. 几个软件研发团队管理的小问题
  2. 浙江大学计算机视频 百度云,浙江大学 数据结构与算法 全40讲 徐镜春 视频教程...
  3. Java - Java集合中的安全失败Fail Safe机制 (CopyOnWriteArrayList)
  4. python代码编辑器下载_编程猫Python编辑器
  5. 字符串的模式匹配--BF算法KMP算法
  6. (计算机组成原理)第六章总线-第一节:总线概述(概念,分类,系统总线的结构和性能指标)
  7. django 1.8 官方文档翻译:9-1-4 格式本地化
  8. ILI9486 和 stm32F407 cortex-M4
  9. Mac源码安装使用OpenCV
  10. S实现控制图片显示大小的方法【图片等比例缩放功能】
  11. 自己创建DXperience的本地资源文件
  12. 方德系统服务器,国产方德桌面操作系统介绍
  13. Java如何提高poi的user模式解析excel大小上限
  14. QT学习教程(全面)
  15. 生产车间问题频发如何解决?
  16. 网吧服务器软件维护合同范本,网吧电脑维护合同范本
  17. 中值定理证明题解题思路
  18. 25000linux集群源码,一文看懂 Redis5 搭建集群
  19. 冒泡排序从左到右 从右到左方法实现(三种方法)
  20. 计算机毕业设计Java南京新东方学校家校通系统(源码+系统+mysql数据库+lw文档)

热门文章

  1. 身份证最后一位检验算法
  2. Revit使用过程中打开闪退解决办法
  3. 京东退货简直是差的一塌糊涂,今天把他写出来,也算是给消费者一个善意的提醒
  4. 如何创建海外 Apple ID?
  5. 善于赞美别人的人,运气不会差
  6. 【博学谷学习记录】超强总结,用心分享|【探花交友】MongoDB
  7. 亚马逊删差评的常用技巧
  8. Oracle项目业务表单设计:Oracle PrimaveraUnifier BP
  9. EP | 南农赵方杰组揭示水稻根系多种内生菌对砷的转化
  10. uni-app微信小程序封装一个request请求接口