data 中的数据是如何处理的?

每一次实例化一个组件,都会调用 initData 然后调用 observe 方法,observe 方法调用了 new Observer(value), 并且返回 __ob__

在 new Observer 中做了两件事:

  1. 把当前实例挂载到数据的__ob__属性上,这个实例在后面有用处。
  2. 根据数据类型(数组还是对象)区别处理

如果是对象:

横向遍历对象属性,调用 defineReactive;

递归调用 observe 方法, 当属性值不是数组或者对象停止递归

下面对 defineReactive 方法做了详细的注释:

  1. export function defineReactive(
  2. obj: Object,
  3. key: string,
  4. val: any,
  5. customSetter ? : ? Function,
  6. shallow ? : boolean
  7. ) {
  8. const dep = new Dep(); // 闭包创建依赖对象; 每个对象的属性都有自己的dep
  9. // 下面是针对已经通过Object.defineProperty 或者Object.seal Object.freeze 处理过的数据
  10. const property = Object.getOwnPropertyDescriptor(obj, key);
  11. // 如果configurable为false ,再次Object.defineProperty(obj, key)会报错,并且不会成功;所以直接返回
  12. // 所以可以针对性的使用Object.freeze/seal优化性能。
  13. if (property && property.configurable === false) {
  14. return;
  15. }
  16. const getter = property && property.get;
  17. const setter = property && property.set;
  18. // 正常情况下 我们使用的数据getter、setter都是不存在的,并且在new Observer()中调用defineReactive的参数只有两个
  19. if ((!getter || setter) && arguments.length === 2) {
  20. val = obj[key]; // 也就是说 这行代码一般情况下会执行
  21. }
  22. // 一般情况下 shallow是false ;childOb就是返回的Observer实例,这个实例是存储在数据的__ob__属性上的
  23. //
  24. let childOb = !shallow && observe(val);
  25. Object.defineProperty(obj, key, {
  26. enumerable: true,
  27. configurable: true,
  28. get: function reactiveGetter() {
  29. const value = getter ? getter.call(obj) : val; // getter 不存在 ,直接用val
  30. if (Dep.target) {
  31. // Dep.target是一个全局数据,保存的是watcher栈(targetStack)栈顶的watcher,
  32. dep.depend(); // 闭包dep把当前watcher收集起来; 收集依赖真正发生在render方法执行的时候(也就是虚拟dom生成的时候)
  33. if (childOb) {
  34. // val不是对象(非Array 或者object) observe方法才会返回一个Observer实例,否则返回undefined
  35. // 此处为什么要执行childOb.dep.depend()呢?
  36. // 这么做的效果是:在对象上挂载的__ob__的dep对象把点前watcher添加到了依赖里,这个dep和闭包dep不是一个。
  37. // 目的在于:
  38. // 1.针对对象:要想this.$set/$del时候能够触发组件重新渲染,需要把渲染watcher保存下来,然后在$set中调用 ob.dep.notify();这里就用到了__ob__属性
  39. // 2.针对数组:数组的拦截中(调用splice push 等法法)要想触发重新渲染,调用 ob.dep.notify() 这里就用到了__ob__属性
  40. childOb.dep.depend();
  41. if (Array.isArray(value)) {
  42. // 如果value是一个数组,在observe方法中走的是数组那套程序,这些元素没有被Object.defineProperty这一系列的处理(元素当做val处理),即便元素是object/array ,没有childOb.dep.depend()这样的一个过程,导致上面this.$set/$del、数组无法触发重新渲染;
  43. // 所以调用dependArray 针对数组做处理 这里就用到了__ob__属性
  44. dependArray(value);
  45. }
  46. }
  47. }
  48. return value;
  49. },
  50. set: function reactiveSetter(newVal) {
  51. // 一般没有getter
  52. const value = getter ? getter.call(obj) : val;
  53. // 值未变化, newVal !== newVal && value !== value 应该针对的是NaN
  54. if (newVal === value || (newVal !== newVal && value !== value)) {
  55. return;
  56. }
  57. if (process.env.NODE_ENV !== "production" && customSetter) {
  58. customSetter();
  59. }
  60. // getter 和setter 要成对才行
  61. if (getter && !setter) return;
  62. if (setter) {
  63. setter.call(obj, newVal);
  64. } else {
  65. val = newVal;
  66. }
  67. // 重新设置值之后,需要重新observe ,并且更新闭包变量 childOB
  68. childOb = !shallow && observe(newVal);
  69. // 更新
  70. dep.notify();
  71. },
  72. });
  73. }

如果是数组:

修改数组的 __proto__ 属性值,指向一个新的对象;

  1. function protoAugment (target, src: Object) {
  2. target.__proto__ = src
  3. }

这个新对象中重新定义如下方法:

  1. 'push','pop','shift','unshift','splice','sort','reverse'

同时这个对象的 __proto__ 指向 Array.prototype。

  1. const arrayProto = Array.prototype;
  2. export const arrayMethods = Object.create(arrayProto);

最后项目的代码在控制台打印出下面的截图

  1. data() {
  2. return {
  3. data1: [{
  4. name: 1
  5. }]
  6. }
  7. },

同时对数组中的每个元素做 observe 递归处理。

watch 选项是如何处理的?

watch 的使用方法一般如下:

  1. watch: {
  2. a: function(newVal, oldVal) {
  3. console.log(newVal, oldVal);
  4. },
  5. b: 'someMethod',
  6. c: {
  7. handler: function(val, oldVal) {
  8. /* ... */
  9. },
  10. deep: true
  11. },
  12. d: {
  13. handler: 'someMethod',
  14. immediate: true
  15. },
  16. e: [
  17. 'handle1',
  18. function handle2(val, oldVal) {},
  19. {
  20. handler: function handle3(val, oldVal) {},
  21. }
  22. ],
  23. 'e.f': function(val, oldVal) {
  24. /* ... */
  25. }
  26. }

watch 的处理按照如下流程, 把其中的关键代码罗列出来了:

  1. -- > initData() // 初始化组件的时候调用 如果组件中有watch选项,调用initWatch
  2. -- > initWatch()
  3. if (Array.isArray(handler)) { // 这里处理数组的情况,也就是上面e的情况
  4. for (let i = 0; i < handler.length; i++) {
  5. createWatcher(vm, key, handler[i])
  6. }
  7. } else {
  8. createWatcher(vm, key, handler)
  9. }
  10. -- > createWatcher()
  11. if (isPlainObject(handler)) { // 兼容c(对象)
  12. options = handler
  13. handler = handler.handler
  14. }
  15. // 如果是b 字符串的情况,需要在vm上有对应的数据
  16. if (typeof handler === 'string') {
  17. handler = vm[handler]
  18. }
  19. // 默认是 a(函数)
  20. vm.$watch(expOrFn, handler, options)
  21. -- > vm.$watch()
  22. options.user = true // 添加参数 options.user = true ,处理immediate:true的情况
  23. const watcher = new Watcher(vm, expOrFn, cb, options)
  24. -- > new Watcher() // 创建watcher
  25. this.getter = parsePath(expOrFn) // 这个getter方法主要是get一下watch的变量,在get的过程中触发依赖收集,把当前watcher添加到依赖
  26. this.value = this.lazy // 选项lazy是false
  27. ?
  28. undefined :
  29. this.get() // 在constructor中直接调用get方法
  30. -- > watcher.get()
  31. pushTarget(this) // 把当前watcher推入栈顶
  32. value = this.getter.call(vm, vm) // 这时候这个watch的变量的依赖里就有了当前watcher
  33. -- > watcher.getter() // 依赖收集的地方

当 watch 的变量变化的时候,会执行 watcher 的 run 方法:

  1. run() {
  2. if (this.active) {
  3. const value = this.get()
  4. // 渲染watcher情况下 value是undefined
  5. // 在自定义watcher的情况下 value就是监听的值
  6. if (
  7. value !== this.value || // 当watch的值有变化的时候
  8. isObject(value) ||
  9. this.deep
  10. ) {
  11. // set new value
  12. const oldValue = this.value
  13. this.value = value
  14. // 自定义watcher的user是true ,cb就是那个handler
  15. if (this.user) {
  16. try {
  17. this.cb.call(this.vm, value, oldValue)
  18. } catch (e) {
  19. handleError(e, this.vm, `callback for watcher "${this.expression}"`)
  20. }
  21. } else {
  22. this.cb.call(this.vm, value, oldValue)
  23. }
  24. }
  25. }
  26. }

上面的的代码中 value !== this.value 和 deep 比较好理解,数值变化触发 handler;

但是 isObject(value)对应的什么情况呢?看一下下面的例子就知道了:

  1. data() {
  2. return {
  3. data1: [{
  4. name: 1
  5. }],
  6. }
  7. },
  8. computed: {
  9. data2() {
  10. let value = this.data1[0].name //
  11. return this.data1 // 返回的是一个数组,所以data2一致是不变的
  12. }
  13. },
  14. watch: {
  15. data2: function() {
  16. // 虽然data2的值一直是data1,没有变化;但是因为data2满足isObject,所以仍然能触发handler
  17. // 由此可以想到,可以在computed中主动去获取某个数据属性来触发watch,并且避免在watch中使用deep
  18. // 但是这样也不太合适,因为可以直接使用'e.f'这种例子来代替;
  19. // 所以根据要实际情况确定
  20. console.log('data2');
  21. }
  22. }
  23. created() {
  24. setInterval(() => {
  25. this.data1[0].name++
  26. }, 2000)
  27. }

computed 数据是如何处理的?

computed 首先是创建 watcher,与渲染 watcher、自定义 watcher 不同之处:初始化的时候不会执行 get 方法,也就是不会做依赖收集。

另外使用 Object.defineProperty 定义 get 方法:

  1. function createComputedGetter(key) {
  2. return function computedGetter() {
  3. const watcher = this._computedWatchers && this._computedWatchers[key];
  4. if (watcher) {
  5. // lazy=true 然后 dirty 也是true
  6. if (watcher.dirty) {
  7. watcher.evaluate(); // 把computed watcher添加到涉及到的所有的变量的依赖中;
  8. }
  9. if (Dep.target) {
  10. watcher.depend(); // 主动调用depend方法;假如这个computed是用在页面渲染上,就会把渲染watcher添加到变量的依赖中
  11. }
  12. return watcher.value;
  13. }
  14. };
  15. }

当 computed 数据在初次渲染中:

  1. -- > render // 渲染
  2. -- > computedGetter // computed Object.defineProperty 定义get方法:
  3. -- > watcher.evaluate() // 计算得到watcher.value
  4. -- > watcher.get()
  5. -- > pushTarget(this) // 把当前computed watcher 推入watcher栈顶
  6. -- > watcher.getter() // getter方法就是组件中computed定义的方法,执行的时候会做依赖收集
  7. -- > dep.depend() // 把当前computed watcher加入变量的依赖中
  8. -- > popTarget() // 把当前 computed watcher 移除栈,一般来说渲染watcher会被推出到栈顶
  9. -- > cleanupDeps() // 清除多余的watcher 和 dep
  10. -- > watcher.depend() // 这是computed比较特殊的地方。假如computed中依赖变量data中的数据,这个步骤把当前watcher添加到变量的依赖中;为什么要这么做呢?个人猜测意图是computed的目的是做一个处理数据的桥梁,真正的响应式还是需要落实到data中的数据。

当 computed 中的依赖数据变化的时候会走如下流程:

  1. -- > watcher.update() // 这是个 computed watcher,其中lazy为true,所以不会往下走
  2. if (this.lazy) {
  3. this.dirty = true
  4. }
  5. -- > watcher.update() // 渲染watcher render 之后的过程就如同初次渲染一样

渲染 watcher 的流程?

渲染 watcher 相对好理解一些

new Watcher(渲染 watcher) ->watcher.get-> pushTarget(this) ->watcher.getter()-> render -> Object.defineProperty(get) -dep.depend()-> popTarget()->watcher.cleanupDeps()

watcher.getter 是下面方法:

  1. updateComponent = () => {
  2. vm._update(vm._render(), hydrating)
  3. }

Vue 中依赖清除?

源码在 vue/src/core/observer/watcher.js 中;

需要注意到 vue 中有一套清除 watcher 和 dep 的方案;vue 中的依赖收集并不是一次性的,重新 render 会触发新一次的依赖收集,这时候会把无效的 watcher 和 dep 去除掉,这样能够避免无效的更新。

如下 computed ,只要有一次 temp<=0.5 , 改变 b 都不再会在打印 temp ;原因在于当 temp<0.5 之后, this.b 不会把当前 a 放进自己的 dep 中,也就不会再触发这个 computed watcher 了

  1. data() {
  2. return {
  3. b: 1
  4. }
  5. },
  6. computed: {
  7. a() {
  8. var temp = Math.random()
  9. console.log(temp); // 只要有一次a<=0.5 接下来就不会打印temp了
  10. if (temp > 0.5) {
  11. return this.b
  12. } else {
  13. return 1
  14. }
  15. }
  16. },
  17. created() {
  18. setTimeout(() => {
  19. this.b++
  20. }, 5000)
  21. },

这里面主要是 watcher.js 中的 cleanupDeps 方法在处理;

  1. cleanupDeps() {
  2. let i = this.deps.length
  3. // 遍历上次保存的deps
  4. while (i--) { // i--
  5. const dep = this.deps[i]
  6. // newDepIds 是在本次依赖收集中加入的新depId集合
  7. // 把不在newDepIds中的dep清除
  8. if (!this.newDepIds.has(dep.id)) {
  9. dep.removeSub(this)
  10. }
  11. }
  12. // depIds是一个es6 set集合 ,是引用类数据
  13. // newDepIds类似于一个临时保存的地方,最终需要把数据保存到depIds。左手到右手的把戏
  14. // newDeps 和 newDepIds 是一样的
  15. let tmp = this.depIds
  16. this.depIds = this.newDepIds
  17. this.newDepIds = tmp
  18. this.newDepIds.clear()
  19. tmp = this.deps
  20. this.deps = this.newDeps
  21. this.newDeps = tmp
  22. this.newDeps.length = 0
  23. }

vuex 响应式原理?

依赖于 vue 自身的响应式原理,通过构建一个 Vue 实例,在 render 过程中完成依赖的收集。

  1. store._vm = new Vue({
  2. data: {
  3. $$state: state, // 自定义的state数据
  4. },
  5. computed,
  6. });

vue-router 响应式处理?

  1. Vue.mixin({
  2. beforeCreate() {
  3. if (isDef(this.$options.router)) {
  4. this._routerRoot = this;
  5. this._router = this.$options.router;
  6. this._router.init(this);
  7. // 关键是这行代码,把_route属性进行响应式处理
  8. Vue.util.defineReactive(
  9. this,
  10. "_route",
  11. this._router.history.current
  12. );
  13. } else {
  14. this._routerRoot =
  15. (this.$parent && this.$parent._routerRoot) || this;
  16. }
  17. registerInstance(this, this);
  18. },
  19. destroyed() {
  20. registerInstance(this);
  21. },
  22. });
  23. Object.defineProperty(Vue.prototype, "$route", {
  24. get() {
  25. return this._routerRoot._route;
  26. },
  27. });

渲染 router-view 的时候会触发上面的

  1. // 该组件渲染的时候render方法
  2. render() {
  3. ...
  4. // 当调用$route的时候会触发依赖收集
  5. var route = parent.$route;
  6. }

vue2 响应式细节的更多相关文章

  1. Vue2响应式原理

    vue2响应式原理 vue的特性:数据驱动视图和双向数据绑定.vue官方文档也提供了响应式原理的解释: 深入响应式原理 Object.defineProperty() Object.definePro ...

  2. [切图仔救赎]炒冷饭--在线手撸vue2响应式原理

    --图片来源vue2.6正式版本(代号:超时空要塞)发布时,尤雨溪推送配图. 前言 其实这个冷饭我并不想炒,毕竟vue3马上都要出来.我还在这里炒冷饭,那明显就是搞事情. 起因: 作为切图仔搬砖汪,长 ...

  3. Vue2 响应式原理

    我们经常用vue的双向绑定,改变data的某个属性值,vue就马上帮我们自动更新视图,下面我们看看原理. Object的响应式原理: 可以看到,其实核心就是把object的所有属性都加上getter. ...

  4. vue2响应式原理与vue3响应式原理对比

    VUE2.0 核心 对象:通过Object.defineProtytype()对对象的已有属性值的读取和修改进行劫持 数组:通过重写数组更新数组一系列更新元素的方法来实现元素的修改的劫持 Object ...

  5. 由浅入深,带你用JavaScript实现响应式原理(Vue2、Vue3响应式原理)

    由浅入深,带你用JavaScript实现响应式原理 前言 为什么前端框架Vue能够做到响应式?当依赖数据发生变化时,会对页面进行自动更新,其原理还是在于对响应式数据的获取和设置进行了监听,一旦监听到数 ...

  6. vue3剖析:响应式原理——effect

    响应式原理 源码目录:https://github.com/vuejs/vue-next/tree/master/packages/reactivity 模块 ref: reactive: compu ...

  7. 实现vue2.0响应式的基本思路

    最近看了vue2.0源码关于响应式的实现,以下博文将通过简单的代码还原vue2.0关于响应式的实现思路. 注意,这里只是实现思路的还原,对于里面各种细节的实现,比如说数组里面数据的操作的监听,以及对象 ...

  8. es6 Object.assign ECMAScript 6 笔记(六) ECMAScript 6 笔记(一) react入门——慕课网笔记 jquery中动态新增的元素节点无法触发事件解决办法 响应式图像 弹窗细节 微信浏览器——返回操作 Float 的那些事 Flex布局 HTML5 data-* 自定义属性 参数传递的四种形式

    es6 Object.assign   目录 一.基本用法 二.用途 1. 为对象添加属性 2. 为对象添加方法 3. 克隆对象 4. 合并多个对象 5. 为属性指定默认值 三.浏览器支持 ES6 O ...

  9. 【Vue2.x笔记1】数据响应式原理

    1.Object.defineProperty Vue2.x 使用Object.defineProperty 将 Vue 实例中的data对象全部转为getter/setter.在内部让 Vue 能够 ...

随机推荐

  1. 通过js给某个标签添加内容或者删除标签

    添加内容 //先保存div中原来的html var tag = document.getElementById("tag").innerHTML; //构造新的内容 var cou ...

  2. CODING 再携手腾讯云 Serverless,让开发者跑步上云

    近年来,腾讯云持续在云原生领域打磨和完善产品矩阵,致力于为开发者上云提供更好的产品和服务.继前段时间 CODING CI 助力腾讯云 Serverless 全新应用控制台.持续保障 Serverles ...

  3. 一次I/O问题引发的P0重大故障[改版重推] 原创 二马读书 二马读书 8月16日 这是前段时间发的一篇文章,很多读者反馈,文章没有揭示故障发生的详细

    一次I/O问题引发的P0重大故障[改版重推] 原创 二马读书 二马读书 8月16日 这是前段时间发的一篇文章,很多读者反馈,文章没有揭示故障发生的详细

  4. Advanced Go Concurrency Patterns

    https://talks.golang.org/2013/advconc.slide#5 It's easy to go, but how to stop? Long-lived programs ...

  5. maven pom文件的 name 标签 和 url标签到底是什么作用

  6. 采用pandas读取文件,进行自动化统计小程序

    自己完成的第二个自动化统计小程序,完成之后感觉:命名不够规范,造成可读性比较没那么好,幸好给自己很多地方都加了注释#coding:utf-8import os,sysimport reimport x ...

  7. Java——变量类型

    Java变量类型: 在Java中,所有的变量在使用前必须声明.格式: type identifier [ = value ][, identifier [ =value]-.]; type为Java数 ...

  8. spring MVC 3.2中@ResponseBody(Post接口)返回乱码的完美解决方案

    本来因为ajax跨域http远程调用时有问题,在服务端响应时用以下方式解决了,但IE8及下有问题. response.addHeader("Access-Control-Allow-Orig ...

  9. linux最初配置( vimrc设置 、tab键设置 inputrc、中文输入法等等)

    1..vimrc设置   syntax on set tabstop=4 set softtabstop=4 set autoindent set cindent set nu set ruler & ...

  10. Yacc使用优先级

    Yacc使用优先级 本示例是龙书4.9.2的示例,见图4-59. 和前一章一样,新建xUnit项目,用F#语言.起个名C4F59安装NuGet包: Install-Package FSharpComp ...