从源码解析vue的响应式原理

栏目: JavaScript · 发布时间: 5年前

内容简介:vue官方对响应式原理的解释:深入响应式原理总结下官方的描述,大概分为一下几点:​ 然鹅,官方的介绍只是一个大致的流程,我们还是不知道vue到底是怎样给data的每个属性设置getter、setter方法?对象属性和数组属性的实现又有什么不同?怎样实现依赖的收集和依赖的触发? 想要搞清楚这些,不得不看一波源码了。下面,请跟我从vue源码分析vue的响应式原理

vue官方对响应式原理的解释:深入响应式原理

从源码解析vue的响应式原理

总结下官方的描述,大概分为一下几点:

  • 组件实例有自己的watcher对象,用于记录数据依赖
  • 组件中的data的每个属性都有自己的getter、setter方法,用于收集依赖和触发依赖
  • 组件渲染过程中,调用data中的属性的getter方法,将依赖收集至watcher对象
  • data中的属性变化,会调用setter中的方法,告诉watcher有依赖发生了变化
  • watcher收到依赖变化的消息,重新渲染虚拟dom,实现页面响应

​ 然鹅,官方的介绍只是一个大致的流程,我们还是不知道vue到底是怎样给data的每个属性设置getter、setter方法?对象属性和数组属性的实现又有什么不同?怎样实现依赖的收集和依赖的触发? 想要搞清楚这些,不得不看一波源码了。下面,请跟我从vue源码分析vue的响应式原理

--- 下面我要开始我的表演了---

实例初始化阶段

vue源码的 instance/init.js 中是初始化的入口,其中初始化分为下面几个步骤:

//初始化生命周期
initLifecycle(vm)
//初始化事件
initEvents(vm)
//初始化render
initRender(vm)
//触发beforeCreate事件
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
//初始化状态,!!!此处划重点!!!
initState(vm)
initProvide(vm) // resolve provide after data/props
//触发created事件
callHook(vm, 'created')
复制代码

其中划重点的 initState() 方法中进行了 props、methods、data、computed以及watcher的初始化。在instance/state.js中可以看到如下代码。

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  //初始化props
  if (opts.props) initProps(vm, opts.props)
  //初始化methods
  if (opts.methods) initMethods(vm, opts.methods)
  //初始化data!!!再次划重点!!!
  if (opts.data) {
    initData(vm)
  } else {
  	//即使没有data,也要调用observe观测_data对象
    observe(vm._data = {}, true /* asRootData */)
  }
  //初始化computed
  if (opts.computed) initComputed(vm, opts.computed)
  //初始化watcher
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
复制代码

划重点的initData()方法中进行了data的初始化。代码依旧在instance/state.js中可以看到。initData()方法代码如下(删节版)。

/* 初始化data */
function initData (vm: Component) {
  //判断data是否是一个对象
  if (!isPlainObject(data)) {
    ...
  }
  //判断data中的属性是否和method重名
  if (methods && hasOwn(methods, key)) {
    ...
  }
  //判断data中的属性是否和props重名
  if (props && hasOwn(props, key)) {
    ...
  }
  //将vm中的属性转至vm._data中
  proxy(vm, `_data`, key)
  //调用observe观测data对象
  observe(data, true /* asRootData */)
}
复制代码

initData()函数中除了前面一系列对data的判断之外就是数据的代理和observe方法的调用。其中数据代 proxy(vm, `_data`, key) 作用是将vm的属性代理至vm._data上,例如:

//代码如下
const per = new VUE({
        data:{
            name: 'summer',
            age: 18,
        }
    })
复制代码

当我们访问 per.name 时,实际上访问的是 per._data.name 而下面一句 observe(data, true /* asRootData */) 才是响应式的开始。

小结

总结一下初始化过程大概如下图

从源码解析vue的响应式原理

响应式阶段

observe函数的代码在observe/index.js,observe是一个工厂函数,用于为对象生成一个Observe实例。而真正将对象转化为响应式对象的是observe工厂函数返回的Observe实例。

Observe构造函数

Observe构造函数代码如下(删减版)。

export class Observer {
  constructor (value: any) {
  	//对象本身
    this.value = value
    //依赖收集器
    this.dep = new Dep()
    this.vmCount = 0
    //为对象添加__ob__属性
    def(value, '__ob__', this)
    //若对象是array类型
    if (Array.isArray(value)) {
     	...
    } else {
      //若对象是object类型
      ...
    }
  }
复制代码

从代码分析,Observe构造函数做了三件事:

  • 为对象添加 __ob__ 属性, __ob__ 中包含value数据对象本身、dep依赖收集器、vmCount。数据经过这个步骤以后的变化如下:
//原数据
	const data = {
        name: 'summer'
	}
	//变化后数据
	const data = {
        name: 'summer',
        __ob__: {
            value: data, //data数据本身
            dep: new Dep(), //dep依赖收集器
            vmCount: 0
        }
	}
复制代码
  • 若对象是array类型,则进行array类型操作
  • 若对象是object类型,则进行object类型操作

数据是object类型

当数据是object类型时,调用了一个walk方法,在walk方法中遍历数据的所有属性,并调用defineReactive方法。defineReactive方法的代码仍然在observe/index.js中,删减版如下:

export function defineReactive (...) {
  //dep存储依赖的变量,每个属性字段都有一个属于自己的dep,用于收集属于该字段的依赖
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  //缓存原有的get、set方法
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 为每个属性创建childOb,并且对每个属性进行observe递归
  let childOb = !shallow && observe(val)
  //为属性加入getter/setter方法
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      ...
    },
    set: function reactiveSetter (newVal) {
      ...
  })
}
复制代码

defineReactive方法主要做了以下几件事:

__ob__

经过defineReactive处理的数据变化如下, 每个属性都有自己的dep、childOb、getter、setter,并且每个object类型的属性都有 __ob__

//原数据
const data = {
    user: {
        name: 'summer'
    },
    other: '123'
}
//处理后数据
const data = {
    user: {
        name: 'summer',
        [name dep,]
        [name childOb: undefined]
        name getter,//引用name dep和name childOb
        name setter,//引用name dep和name childOb
        
        __ob__:{data, user, vmCount}
    },
    [user dep,]
    [user childOb: user.__ob__,]
    user getter,//引用user dep和user childOb
    user setter,//引用user dep和user childOb
    
    other: '123',
    [other dep,]
    [other childOb: undefined,]
    other getter,//引用other dep和other childOb
    other setter,//引用other dep和other childOb
    
    __ob__:{data, dep, vmCount}
}
复制代码

刚刚讲到defineReactive函数的最后一步是每一个属性都加上getter、setter方法。那么getter和setter函数到底做了什么呢?

getter方法中:

getter函数内部代码如下:

get: function reactiveGetter () {
	//调用原属性的get方法返回值
	const value = getter ? getter.call(obj) : val
    //如果存在需要被收集的依赖
    if (Dep.target) {
        /* 将依赖收集到该属性的dep中 */
        dep.depend()
        if (childOb) {
          //每个对象的obj.__ob__.dep中也收集该依赖
          childOb.dep.depend()
          //如果属性是array类型,进行dependArray操作
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
    }
	return value
},
复制代码

getter方法主要做了两件事:

  • 调用原属性的get方法返回值
  • 收集依赖
    1. Dep.target表示一个依赖,即观察者,大部分情况下是一个依赖函数。
    2. 如果存在依赖,则收集依赖到该属性的dep依赖收集器中
    3. 如果存在childOb(即属性是对象或者数组),则将该依赖收集到childOb也就是 __ob__ 的依赖收集器 __ob__.dep 中,这个依赖收集器在使用$set 或 Vue.set 给属性对象添加新属性时触,也就是说Vue.set 或 Vue.delete 会触发 __ob__.dep 中的依赖。
    4. 如果属性的值是数组,则调用dependArray函数,将依赖收集到数组中的每一个对象元素的 __ob__.dep 中。确保在使用$set 或 Vue.set时,数组中嵌套的对象能正常响应。代码如下:
//数据
const data = {
    user: [
        {
            name: 'summer'
        }
    ]
}
// 页面显示
{{user}}
<Button @click="addAge()">addAge</Button>
//addAge方法,为数组中的嵌套对象添加age属性
change2: function(){
	this.$set(this.user[0], 'age', 18)
}
复制代码
//dependArray函数
function dependArray (value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i]
    //将依赖收集到每一个子对象/数组中
    e && e.__ob__ && e.__ob__.dep.depend()
    if (Array.isArray(e)) {
      dependArray(e)
    }
  }
}
复制代码
//转化后数据
const data = {
    user: [
       {
            name: 'summer',
            __ob__: {user[0], dep, vmCount}
        }
        __ob__: {user, dep, vmCount}
    ]
}
复制代码

dependArray的作用就是将user的依赖收集到它内部的user[0]对象的 __ob__.dep 中,使得进行addAge操作时,页面可以正常的响应变化。

setter方法中:

setter函数内部代码如下:

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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      //由于属性的值发生了变化,则为属性创建新的childOb,重新observe
      childOb = !shallow && observe(newVal)
      //在set方法中执行依赖器中的所有依赖
      dep.notify()
      }
})
复制代码

setter方法主要做了三件事:

  • 为属性设置正确的值
  • 由于属性的值发生了变化,则为属性创建新的childOb,重新observe
  • 执行依赖器中的所有依赖

数据是纯对象类型的处理讲完了,下面看下数据是array类型的操作。

数据是array类型

observer/index.js中对array处理的部分:

if (Array.isArray(value)) {
	const augment = hasProto
		? protoAugment
		: copyAugment
	//拦截修改数组方法
	augment(value, arrayMethods, arrayKeys)
	//递归观测数组中的每一个值
	this.observeArray(value)
}
复制代码

当数据类型是array类型时

  1. 使用protoAugment方法为数据指定构造函数 __proto 为arrayMethods,出于兼容性考虑如果浏览器不支持 __proto__ ,则使用arrayMethods重写数组数据中的所有相关方法。
  2. 递归观测数组中的每一个值

arrayMethods拦截修改数组方法

arrayMethods中的定义在observe/array.js中,代码如下:

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

//修改数组的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  //拦截修改数组的方法,当修改数组方法被调用时触发数组中的__ob__.dep中的所有依赖
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    //对新增元素使用observeArray进行观测
    if (inserted) ob.observeArray(inserted)
    //触发__ob__.dep中的所有依赖
    ob.dep.notify()
    return result
  })
})

复制代码

在arrayMethods中做了如下几件事:

__ob__.dep

observeArray递归观测数组中的每一项

observeArray代码如下:

observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
}
复制代码

在observeArray方法,对数组中的所有属性进行observe递归。然而这里有一个问题就是无法观测数组中的所有非Object的基本类型。observe方法的第一句就是

if (!isObject(value) || value instanceof VNode) {
	return
}
复制代码

也就是说数组中的非Object类型的值是不会被观测到的,如果有数据:

const data = {
    arr: [{
    	test: 0
   	}, 1, 2],
}
复制代码

此时如果改变arr[0].test=3可以被触发响应,而改变arr[1]=4不能触发响应,因为observeArray观测数据中的每一项时,observe(arr[0])是一个观测一个对象可以被观测。observe(arr[1])时观测一个基本类型数据,不可以被观测。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

游戏编程入门

游戏编程入门

莫里森 / 人民邮电出版社 / 2005-9 / 49.00元

本书介绍如何设计和构建自己的计算机游戏。书中从零开始,引导读者开发一个“即插即用”的游戏引擎,并基于该引擎,循序渐进地开发7个完整的游戏。全书分为8个部分,共24章,内容包括游戏编程基础知识、如何与玩家交互、使用子画面动画、使用声音和音乐、高级动画、游戏人工智能、增添游戏的趣味性和附加练习。此外,在随书光盘中提供有附录,包括C++语言和windows编程的入门指导、游戏开发工具以及游戏图形创建的介......一起来看看 《游戏编程入门》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具