DDR爱好者之家 Design By 杰米

前言

Vue区别于传统的JS库,例如JQuery,其中一个最大的特点就是不用手动去操作DOM,只需要对数据进行变更之后,视图也会随之更新。 比如你想修改div#app里的内容:

/// JQuery
<div id="app"></div>
<script>
 $('#app').text('lxb')
</script>
<template>
	<div id="app">{{ message }}</div>
  <button @click="change">点击修改message</button>
</template>
<script>
export default {
	data () {
  	return {
    	message: 'lxb'
    }
  },
  methods: {
  	change () {
    	this.message = 'lxb1' // 触发视图更新
    }
	}
}
</script>

在代码层面上的最大区别就是,JQuery直接对DOM进行了操作,而Vue则对数据进行了操作,接下来我们通过分析源码来进一步分析,Vue是如何做到数据驱动的,而数据驱动主要分成两个部分依赖收集和派发更新。

数据驱动

// _init方法中
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 重点分析
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

在Vue初始化会执行_init方法,并调用initState方法. initState相关代码在src/core/instance/state.js下

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

我们具体看看initData是如何定义的。

function initData (vm: Component) {
 let data = vm.$options.data
 data = vm._data = typeof data === 'function' // 把data挂载到了vm._data上
  "${key}" has already been defined as a data property.`,
     vm
    )
   }
  }
  if (props && hasOwn(props, key)) { // 避免props的key与data的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) // 代理
  }
 }
 // observe data
 observe(data, true /* asRootData */) // 响应式处理
}

其中有两个重要的函数分别是proxy跟observe,在往下阅读之前,如果还有不明白Object.defineProperty作用的同学,可以点击这里进行了解,依赖收集跟派发更新都需要依靠这个函数进行实现。

proxy

proxy分别传入vm,'_data',data中的key值,定义如下:

const sharedPropertyDefinition = {
 enumerable: true,
 configurable: true,
 get: noop,
 set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
 sharedPropertyDefinition.get = function proxyGetter () {
  return this[sourceKey][key]
 }
 sharedPropertyDefinition.set = function proxySetter (val) {
  this[sourceKey][key] = val
 }
 Object.defineProperty(target, key, sharedPropertyDefinition)
}

proxy函数的逻辑很简单,就是对vm._data上的数据进行代理,vm._data上保存的就是data数据。通过代理的之后我们就可以直接通过this.xxx访问到data上的数据,实际上访问的就是this._data.xxx。

observe

oberse定义在src/core/oberse/index.js下,关于数据驱动的文件都存放在src/core/observe这个目录中:

export function observe (value: any, asRootData: "htmlcode">
export class Observer {
 value: any; // 观察的数据
 dep: 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
  // 把__ob__变成不可枚举的,因为没有必要改变watcher本身
  def(value, '__ob__', this) 会执行 value._ob_ = this(watcher实例)操作
  if (Array.isArray(value)) { // 当value是数组
   if (hasProto) {
    protoAugment(value, arrayMethods) // 重写Array.prototype的相关方法
   } else {
    copyAugment(value, arrayMethods, arrayKeys) // 重写Array.prototype的相关方法
   }
   this.observeArray(value)
  } else {
   this.walk(value) // 当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]) // 遍历value数组的每一项并调用observe函数,进行响应式处理
  }
 }
}

Observe类要做的事情通过查看源码也是清晰明了,对数据进行响应式处理,并对数组的原型方法进行重写!defineReactive函数就是实现依赖收集和派发更新的核心函数了,实现代码如下。

依赖收集

defineReactive

export function defineReactive (
 obj: Object, // data数据 
 key: string, // data中对应的key值
 val: any, // 给data[key] 赋值 可选
 customSetter"htmlcode">
let uid = 0
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
 static target: "htmlcode">
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
   const value = getter "htmlcode">
class Watcher {
	addDep (dep: Dep) {
   const id = dep.id
   if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep) // 
    if (!this.depIds.has(id)) {
     dep.addSub(this) // 会把watcher插入到dep.subs数组中
    }
   }
 }
}

可以通过下图以便理解data、Dep、Watcher的关系:

详解Vue数据驱动原理

回到代码中,其中dep.addSub(this)就是会把当前的wathcer实例插入到dep.subs的数组中,为之后的派发更新做好准备,这样依赖收集就完成了。但是到现在为止,我们只分析了依赖收集是怎么实现的,但是依赖收集的时机又是在什么时候呢?什么时候会触发getter函数进而实现依赖收集的"htmlcode">

class Wachter {
	get () {
   pushTarget(this) // Dep.target = this
   let value
   const vm = this.vm
   try {
    value = this.getter.call(vm, vm) // 更新视图
   } catch (e) {
    if (this.user) {
     handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
     throw e
    }
   } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
     traverse(value)
    }
    popTarget()
    this.cleanupDeps()
   }
   return value
  }
}

new Watcher对于渲染watcher而言,会直接执行this.get()方法,然后执行pushTarget(this),所以当前的Dep.target为渲染watcher(用于更新视图)。 而在我们执行this.getter的时候,会调用render函数,此时会读取vm实例上的data数据,这个时候就触发了getter函数了,从而进行了依赖收集,这就是依赖收集的时机,比如

{{ message }} // 会读取vm._data.message, 触发getters函数

派发更新

我们继续来看defineReactive函数里

export function defineReactive (
 obj: Object,
 key: string,
 val: any,
 customSetter"text-align: center">详解Vue数据驱动原理

总结

  1. 通过Object.defineProperty函数改写了数据的getter和setter函数,来实现依赖收集和派发更新。
  2. 一个key值对应一个Dep实例,一个Dep实例可以包含多个Watcher,一个Wathcer也可以包含多个Dep。
  3. Dep用于依赖的收集与管理,并通知对应的Watcher执行相应的操作。
  4. 依赖收集的时机是在执行render方法的时候,读取vm上的数据,触发getter函数。而派发更新即在变更数据的时候,触发setter函数,通过dep.notify(),通知到所收集的watcher,执行相应操作。

以上就是详解Vue数据驱动原理的详细内容,更多关于Vue数据驱动原理的资料请关注其它相关文章!

DDR爱好者之家 Design By 杰米
广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
DDR爱好者之家 Design By 杰米

稳了!魔兽国服回归的3条重磅消息!官宣时间再确认!

昨天有一位朋友在大神群里分享,自己亚服账号被封号之后居然弹出了国服的封号信息对话框。

这里面让他访问的是一个国服的战网网址,com.cn和后面的zh都非常明白地表明这就是国服战网。

而他在复制这个网址并且进行登录之后,确实是网易的网址,也就是我们熟悉的停服之后国服发布的暴雪游戏产品运营到期开放退款的说明。这是一件比较奇怪的事情,因为以前都没有出现这样的情况,现在突然提示跳转到国服战网的网址,是不是说明了简体中文客户端已经开始进行更新了呢?