博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
从源码分析VUE的响应式原理
阅读量:6873 次
发布时间:2019-06-26

本文共 9050 字,大约阅读时间需要 30 分钟。

hot3.png

前言

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)//初始化renderinitRender(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 */)才是响应式的开始。

小结

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

响应式阶段

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方法主要做了以下几件事:

  • 为每个属性实例化一个dep依赖收集器,用于收集该属性的相关依赖。【通过getter、setter引用】
  • 缓存属性原有的get和set方法,保证后面重写get、set方法时行为正常。
  • 为每个属性创建childOb。(其实是一个对属性进行进行observe递归的过程,并将结果保存在childOb中。对象或数组属性的childOb为__ob__,其他属性的childOb为undefined)。【通过getter、setter引用】
  • 将对象中的每一个属性都加上getter、setter方法。

经过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}}//addAge方法,为数组中的嵌套对象添加age属性change2: function(){ this.$set(this.user[0], 'age', 18)}
//dependArray函数function dependArray (value: Array
) { 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.prototypeexport 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中做了如下几件事:

  • 需要拦截的修改数组的方法有:push、pop、shift、unshift、splice、sort、reverse
  • 当数组有新增元素时,使用observeArray对新增的元素进行观测
  • 拦截了修改数组的方法,当修改数组方法被调用时触发数组中的__ob__.dep的所有依赖
observeArray递归观测数组中的每一项

observeArray代码如下:

observeArray (items: Array
) { 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])时观测一个基本类型数据,不可以被观测。

小结

响应式阶段流程图

参考文章:

转载于:https://my.oschina.net/u/2600761/blog/1922874

你可能感兴趣的文章
Catnut 微博app第一个版本发布了
查看>>
python实现linux下指定目录下文件中的单词个数统计
查看>>
SQL SERVER存储过程中如何使用事务与try catch
查看>>
我的友情链接
查看>>
常见算法的记录
查看>>
ssh 问题
查看>>
Android源代码下载编译
查看>>
nhmicro添加信审功能
查看>>
eclipse安装maven插件-解决requires ‘bundle org.slf4j.api
查看>>
jsp---语句对象Statement
查看>>
java进阶之路
查看>>
优化Android Studio
查看>>
zabbix二次开发-flask-获取告警
查看>>
我的友情链接
查看>>
java实现MD5加密处理
查看>>
实用JVM参数总结
查看>>
oracle 11g R2 64位 安装详细步骤
查看>>
Jpeg 库的解码OpenCL优化
查看>>
正则表达式
查看>>
『中级篇』docker之虚拟机创建vagrant技巧(番外篇)(81)
查看>>