Vue2.0 的响应式原理 私

使用的Object.defineProperty()重新定义对象,给data的每个属性都添加了getter和setter方法。这时候会为对象的每个属性创建一个Dep实例  (依赖)。Dep实例可以订阅和通知相关的Watcher实例。,  这一步叫  数据劫持  或者 依赖收集

在数据发生更新后调用 set 时会通知发布者 notify 通知对应的订阅者做出数据更新,同时将新的数据根性到视图上显示。 这一步叫 派发更新

同时,为了解决对象属性添加和删除的问题,Vue提供了全局的Vue.set和Vue.delete方法,以及实例的vm.$set和vm.$delete方法。

解释:

  1. 在创建 Observer 实例的同时还会创建 Dep 实例,用于保存依赖项。因此每个数据都有 Observer 的实例,每个 Observer 实例中又都有一个 Dep 的实例。
  2. 当 Vue 解析到当解析到模板字符串 {{ }} 时中数据时,就会去创建 Watcher 实例,在 constructor 时会调用自身的 get 方法,该方法不仅将当前的 Watcher 实例赋值给了 Dep.target(表示此时处于依赖收集阶段),还让这个新实例去读取一下 {{ }} 中的数据,一旦读取,就会触发这个数据的 getter 方法。因为此时正在进行收集依赖,Dep.target 一定是为 true 的,于是顺利地把当前的这个 Watcher 实例记录到了 dep 中的 subs 数组里。再然后将 Dep.target 的值重新赋值为 null,表示退出依赖收集阶段

总结:

在Vue2中,响应式原理是通过使用Object.defineProperty方法来实现的。当一个对象被传入Vue的observe函数中时,Vue会为对象的每个属性创建一个Dep实例。Dep实例可以订阅和通知相关的Watcher实例。

当一个属性被访问时,Watcher实例会将自己添加到该属性的Dep实例的订阅列表中。当该属性的值发生变化时,Dep实例会遍历订阅列表,通知所有相关的Watcher实例进行更新。

然而,Vue无法检测到对象属性的添加和删除。为了解决这个问题,Vue提供了全局的Vue.set方法或实例的vm.$set方法来添加属性,使用Vue.delete方法或实例的vm.$delete方法来删除属性。对于数组的变动,Vue无法检测到利用索引设置数组,但可以使用Vue.set方法或实例的vm.$set方法。此外,Vue也无法检测直接修改数组长度,但可以使用splice方法来实现。

总结起来,Vue2的响应式原理通过使用Object.defineProperty方法来实现属性的劫持和侦听,同时使用Dep和Watcher实例来建立属性和依赖之间的关系,并进行更新通知。同时,为了解决对象属性添加和删除的问题,Vue提供了全局的Vue.set和Vue.delete方法,以及实例的vm.$set和vm.$delete方法。

原理:

通过数据劫持 defineProperty + 发布订阅者模式,当 vue 实例初始化后 observer 会针对实例中的 data 中的每一个属性进行劫持并通过 defineProperty() 设置值后在 get() 中向发布者添加该属性的订阅者,

使用的Object.defineProperty()重新定义对象,给data的每个属性都添加了getter和setter方法。
在get的时候会调用dep.depend; 

如果是数组,则调用dependArray(对数组里的每个元素进行递归调用dep.depend);
在set的时候会先判断数据是否更新,未更新不做操作,更新则observe(),且dep.notify()

1 在initState()方法里会对组件的props, methods, data,computed, watch等进行编译初始化=>

2 initData()会先获取组件内部的data数据,然后判断data里的数据和props,或者和methods里的名称重复,则抛出错误提示,然后就会去监听data,执行observe方法=>

3   Observer()  =>   defineReactive, dependArray =>  defineProperty()   =>   Observer()    递归

使用的Object.defineProperty()重新定义对象,给data的每个属性都添加了getter和setter方法。

在get的时候会调用dep.depend;如果是数组,则调用dependArray(对数组里的每个元素进行递归调用dep.depend);
在set的时候会先判断数据是否更新,未更新不做操作,更新则observe(),且dep.notify()

源码解析:

不同版本的vue的源码实现可能会有些不同,我这里的版本是2.6.14

首先我们要知道定义响应式是在哪个时间段实现的,从源码中我们可以看到,是在执行beforeCreate生命周期函数之后,Created之前。也就是说,这也就是我们在beforeCreate无法拿到Data中的数据的原因。

首先我们要知道定义响应式是在哪个时间段实现的,从源码中我们可以看到,是在执行beforeCreate生命周期函数之后,Created之前。也就是说,这也就是我们在beforeCreate无法拿到Data中的数据的原因。

第一步: 在initState方法里会对组件的props, methods, data,computed, watch等进行编译初始化

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

第二步: initData()会先获取组件内部的data数据,然后判断data里的数据和props,或者和methods里的名称重复,则抛出错误提示,然后就会去监听data,执行observe方法

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key) //将data上的属性代理到vm实例上。
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

第三步:在这里observe()中,会先判断data中的数据是否是对象,然后判断data中是否已经有了ob(也就是Observer实例)最后判断是否满足监听的条件。才会创建一个新的Observer对象

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}


第四步:

每一个observer实例都有自己的一个Dep, 在new Oberver后,会判断传入的value也就是vm.data是不是数组。

如果是数组,会采用函数劫持的方法重写数组的方法,先判断数组支不支持原型链,支持就将当前数组的原型指向已经重写了Array里的7种方法的arrayMethod,当数组里的方法被调用时,Dep会notify通知视图更新,然后执行ObserveArray方法,如果数组里的数据是对象,则继续回调observe();
如果是对象,则调用this.walk(),在walk()中,会遍历data的属性执行defineReactive()定义响应式
 

/**
 * Observer class that is attached to each observed
 * object. Once attached, the observer converts the target
 * object's property keys into getter/setters that
 * collect dependencies and dispatch updates.
 */
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

第五层:

使用的Object.defineProperty()重新定义对象,给data的每个属性都添加了getter和setter方法。
在get的时候会调用dep.depend;如果是数组,则调用dependArray(对数组里的每个元素进行递归调用dep.depend);
在set的时候会先判断数据是否更新,未更新不做操作,更新则observe(),且dep.notify()
以下是Dep的代码,我们可以将Dep看作一个观察者。
 

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const 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 (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

第六层 解释   第五层:

depend方法就是将当前dep的实例添加到对应的Watcher中,
notify方法就是通知所有收集的Wacher进行更新,subs[i].update()

/* @flow */

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = [] //存储所有订阅的Watcher
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

第七层.Watcher.js

当解析到模板字符串 {{ }} 时,会默认去 new Watcher 实例。

/**
 * 每一次的 new Watcher 都是独立的,因此构造器接收的三个参数,虽然名字一样但确实不同的数据,就像是 vm.$watch() 接收的参数一样,
 * @param {*} target 需要监视的对象,当做修改时,他就是
 * @param {*} expression 这个对象中的某个属性,它是一个表达式 比如 obj.a.b.c
 * @param {*} callback 回调函数,需要执行的操作 
 */

import Dep from "./Dep";

// 这个 uid 用于对每一个的 Watcher 实例添加唯一的 id
var uid = 0

// 在这里哪一步算是调用了 get 方法???????,解析到模板的时候

export default class Watcher {
    constructor(target, expression, callback) {
        console.log('我是 Watcher 构造器');
        this.id = uid++;

        // 模板字符串中的整个表达式
        this.target = target;

        // 通过拆分表达式(对象中的对象...),获得需要 Watch 的那个数据。比如传入的是 a.b.c.d 我们需要监视属性 d,就需要拆分
        this.getter = parsePath(expression)  // 有两种方法供使用 parsePath 会返回一个函数;如果用 reduce 方法,那么 getter 就会是一个具体的值,此时一定要修改下边的 get 方法!!!

        this.callback = callback

        // 调用该方法,进入依赖收集阶段
        this.value = this.get()
    }
    // 当更新 dep 中的依赖项时,会调用每一个 Watcher 实例身上的 update 方法
    update() {
        console.log('我是Watcher实例身上的update方法');
        this.run()
    }

    // 进入依赖收集阶段,让全局的 Dep.target 设置为 Watcher 本身
    get(){
        // Webpack 在打包的时候 Dep 是全局唯一的,不管多少个JS 文件在用 dep 的时候,都是这一个文件
        // 因此执行到这里
        console.log(this);  // Watcher 实例
        Dep.target = this;   
        // debugger;
        const obj = this.target;

        var value;

        // 防止找不到,用try catch一下,只要能找,就一直找
        try {
            value = this.getter(obj)    // 获取需要监视的那个值。这里因为constructor 的时候 this.get() 返回的是一个函数
        } finally {
            Dep.target = null   // 清空全局 target 的指向,同时也表示退出依赖收集阶段
        }
        return value        
    }

    // 其实可以直接 getAndInvoke,但是 Vue 源码时这样写的
    run(){
        this.getAndInvoke(this.callback)
    }

    //
    getAndInvoke(callback){
        // 获取到修改后的新值   旧值是 this.value
        const value = this.get()
        if(value !== this.value || typeof value == 'object'){
            const oldValue = this.value;
            this.value = value;
            callback.call(this.target, value, oldValue)
        }
    }
}





// 拆分表达式:
// 方法一:将 str 用 . 分割成数组 segments,然后循环数组,一层一层去读取数据,最后拿到的 obj 就是 str 中想要读的数据
// 假设 let o = {a:{b:{c:{d:55}}}},我想要取得 d 的值,经过拆分后的 segments 数组的值为 ['a', 'b', 'c', 'd']
// 第一次循环后 obj = {b:{c:{d:55}}}, 第二次 obj = {c:{d:55}}, 第三次 obj = {d:55}, 第四次 obj = 55
function parsePath(str) {
    let segments = str.split(".");
    return function (obj) {
        for (let key of segments) {
            if (!obj) return;   // 当没有传入 obj 时,直接 return
            obj = obj[key];
        }
        return obj;
    };
}
// 方法二 用 reduce 方法实现
// function parsePathReduce(str) {
//     let segments = str.split(".");
//     let result = segments.reduce((total, item) => {
//         total = total[item]
//         return total
//     }, str)
//     return result
// }

前置知识:


首先要了解三个最重要的对象:
Observer 对象:将 Vue 中的数据对象在初始化过程中转换为 Observer 对象。
Watcher 对象:将模板和 Observer 对象结合在一起生成 Watcher 实例,Watcher 是订阅者中的订阅者。
Dep对象:Watcher 对象和 Observer 对象之间纽带,每一个 Observe r都有一个 Dep 实例,用来存储订阅者 Watcher。
 

过程:

  1. 在生命周期的 initState 方法中将 data,prop,method,computed,watch等所有数据全部进行数据劫持,将所有数据变为 Observer 实例,并且每个数据身上还有 Dep 实例。
  2. 然后在 initRender 方法中也就是模板编译过程,遇到的指令和数据绑定都会生成 Watcher 实例,并且把这个实例存入对应数据的 Dep 实例中的 subs 数组里。这样每一个数据的 Dep 实例里就都存放了依赖关系。
  3. 当数据变化时,数据的 setter 方法被调用,触发 dep.notify 方法,就会通知 Dep 实例依赖列表,执行 update 方法通知 Watcher,Watcher 会执行 run 方法去更新视图。
  4. 更新视图的过程,我猜是 Vue 接下来要进行 diff 算法,对比新旧模板,然后重新渲染页面。
     

缺陷:只能够监听初始化实例中的 data 数据,动态添加值不能响应,要使用对应的 Vue.set()。

  • Vue 是无法检测到对象属性的添加和删除,但是可以使用全局 Vue.set 方法(或 vm.$set 实例方法)。
  • Vue 无法检测利用索引设置数组,但是可以使用全局 Vue.set方法(或 vm.$set 实例方法)。
  • 无法检测直接修改数组长度,但是可以使用 splice。

vue2.0重写数组

		// 数组方法重写
        let oldArrayPrototy = Array.prototype
        // 使用Object.create 将数组原型上的方法放到newArrayPrototy.prototype上
        let newArrayPrototy = Object.create(arrayFn)
        // 需要重写的数组方法列表
        let method = [
            'push',
            'pop',
            'shift',
            'unshift',
            'reverse',
            'sort',
            'splice'
        ]
        method.forEach((item) => {
        	//newArrayPrototy[item] 就是 arr.某一个方法
            newArrayPrototy[item] = function (...args) {
                // 关键部分
                let result = oldArrayPrototy[item].call(this, ...args)
                // 对传进来的数据做一些处理
                let insterted
                switch (item) {
                    case 'push':
                    case 'unshift':
                        insterted = args
                    case 'splice':
                        insterted = args.slice(2)
                    default:
                        break
                }
                return result
            }
        })

Vue2.0响应式原理源码解析_玛已的博客-CSDN博客

Vue2的响应式原理_vue2响应式原理_高等数学真简单的博客-CSDN博客

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值