likes
comments
collection
share

Vue2.0响应式原理剖析

作者站长头像
站长
· 阅读数 52

Vue.js 实现响应式的核心是利用了 ES5 的 Object.defineProperty。这个方法在大多数浏览器下都是支持的,但是在ie8及以下浏览器是没有这个方法的,并且没有任何的补丁来兼容这个方法,这也是为什么Vue.js不能兼容ie8及以下浏览器的原因。

Object.defineProperty

Objet.defineProperty方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。 MDN链接

语法

Object.defineProperty(obj, prop, descriptor)

属性描述符

configurable

enumerable

value

writable

get

set

我们最关心的是 get 和 setget 是一个给属性提供的 getter 方法,当我们访问了该属性的时候会触发 getter 方法;set 是一个给属性提供的 setter 方法,当我们对该属性做修改的时候会触发 setter 方法。

Vue内部实现流程

当我们 new Vue后,框架内部会进行_init操作。而数据的初始化是在initState函数中,它会根据用户传入的不同类型的数据进行相应的初始化。像propsmethodsdatacomputedwatch的初始化。本篇文章主要分析data的初始化,像computedwatch的初始化会在另外章节再来分析。

注: 以下代码均为简化后的,去掉了一些非核心的流程。

function initState (vm: Component) {  
  const opts = vm.$options  
  if (opts.props) initProps(vm, opts.props)  
  if (opts.methods) initMethods(vm, opts.methods)  
  if (opts.data) initData(vm)
  if (opts.computed) initComputed(vm, opts.computed)  
  if (opts.watch) initWatch(vm, opts.watch)
}

回到initData函数中 ,主要做了下面两件事:

  1. 遍历用户传入的data数据。拿到data里的每一个键值,拿健值去判断一下propsmethods是否已经定义过了,因为最终会把键值定义到vm实例上,所以是不能重复定义的。

  2. 调用**observe**函数对数据进行观测。

function initData (vm: Component) {
  let data = vm.$options.data
  // 定义_data
  data = vm._data = typeof data === 'function' // 定义data时尽量用定义函数返回数据的方式,避免数据之间污染
    ? getData(data, vm)
    : data || {}
  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) // 做了代理,用户访问vm.key实际是访问了vm._data.key,_data在👆🏻定义
    }
  }
  observe(data, true)   // observe data
}

observe函数会对传入的数据做校验,只有是**对象类型**才会观测。接着会实例化Observer并将其返回。

export function observe (value: any): Observer | void {  
  if (!isObject(value)) {    
    return  
  }  
  let ob: Observer | void  
  ob = new Observer(value)  
  return ob
}

Observer是一个类,接收传入的data,然后判断是对象的话,会执行walk方法,walk方法会遍历该对象,拿到对象的每个key值,如果key对应的值还是一个对象,会递归调用observe函数,最后调用Object.defineProperty为每一个key添加getset函数。如果是数组的话,会修改data__proto__指向,然后遍历该数组,拿到数组每一项后,递归调用ovserve方法。

class Observer {  
  value: any;  
  dep: Dep;  
  constructor (value: any) {    
    this.value = value    
    this.dep = new Dep()    
    def(value, '__ob__', this) // value.__ob__ 指向 observer实例    
    if (Array.isArray(value)) {      
      protoAugment(value, arrayMethods) // value.__proto__ = arrayMethods      
      this.observeArray(value)    
    } else {      
      this.walk(value)    
    } 
  }
  walk (obj: Object) {     
    const keys = Object.keys(obj)    
    for (let i = 0; i < keys.length; i++) {      
      defineReactive(obj, keys[i])    
    }  
  }
  observeArray (items: Array<any>) {    
    for (let i = 0, l = items.length; i < l; i++) {      
    observe(items[i])    
  }  
 }
}
function defineReactive (  obj: Object,  key: string) {  
  const dep = new Dep()  
  if (arguments.length === 2) {    
    val = obj[key]  
  }  
  let childOb = observe(val)  
  Object.defineProperty(obj, key, {    
    enumerable: true,    
    configurable: true,    
    get: function reactiveGetter () {      
      const value = 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 = val      
      if (newVal === value) {        
        return      
      }      
      val = newVal      
      childOb = observe(newVal) // 观测用户修改后的数据,将其变为响应式      
      dep.notify()    
     }  
   })
}

依赖收集

大家可以看到,在defineReactive函数和实例化Observe类的时候,都会实例化一个Dep。这个dep是用来收集当前key对应的watcher。当我们执行渲染流程的时候,首先会实例化一个渲染watcher,然后执行其内部的get方法(会用Dep.target标识当前watcher的类型),然后会执行_render方法去访问定义在模板中的数据,访问数据就会触发该数据对应的getter方法,该数据的dep实例就会把当前正在渲染的Dep.target对应的watcher收集起来(添加到subs数组中)。

Dep

class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

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

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    subs.sort((a, b) => a.id - b.id)
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

watcher类: 

class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,   
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    this.cb = cb
    this.id = ++uid
    this.active = true
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  get () {
    pushTarget(this)   // targetStack.push(target)  Dep.target = target
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // 让传入的getter函数执行
    } catch (e) {
    } finally {
      popTarget()  //  targetStack.pop()   Dep.target = targetStack[targetStack.length - 1]
    }
    return value
  }
  
  addDep (dep: Dep) {
    dep.addSub(this)
  }
 }

让我们用一个简单的demo来跑一个上述流程: 

 new Vue({  
  el: '#app',  
  render(h) {   
    return h('div', this.msg)  
  },  
  data: {   msg: 'Hello Vue!'  } 
 })

手绘流程图如下:

Vue2.0响应式原理剖析

如果Dep.target存在,会调用dep.dependdepend函数会调用watcher.addDep(当前dep)方法,并把当前dep传入,最终会执行该dep上的addSub方法,把当前的watcher添加到dep对应的subs数组中。

派发更新

当我们修改data中定义的数据时,会触发该数据的setter方法。setter方法主要做了2件事:

1、给数据赋值,并把新赋值的数据变为响应式。

2、找到当前数据对应的dep,触发dep.notify。 代码如下

Object.defineProperty(obj, key, {
    ...,
    set: function reactiveSetter (newVal) {
      const value = val
      if (newVal === value) {
        return
      }
      val = newVal
      childOb = observe(newVal)
      dep.notify()
    }
}

nofity会遍历subs数组中的watcher,依次调用watcherupdate方法:

notify () {
    const subs = this.subs.slice()
    subs.sort((a, b) => a.id - b.id)
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // watcher.update()
    }
}

回到Watcher类中,我们找到update方法,因为计算属性和监听属性也是基于watcher实现的,只不过传入的配置项和更新的方式不同,所以update函数内部针对不同的watcher做了不同的操作,代码如下:

update () {
    if (this.lazy) { // computed watcher
      this.dirty = true
    } else if (this.sync) { // 同步watcher
      this.run()
    } else {
      queueWatcher(this) // 普通watcher
    }
}

lazy为计算属性watcher使用,sync为同步watcher(同步更新,不需要经过nextTick),我们这里的逻辑会执行queueWatcher,并把当前watcher做为参数传入,我们来看一下queueWatcher核心流程:

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0

function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

function flushSchedulerQueue () {
  flushing = true
  let watcher, id
  
  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
  }
  ...
}

queueWatcher函数内部主要做了一下工作: 定义全局变量queue,用来存放当前需要重新渲染的watcher,相同的watcher只能存放一次(id相同)。用waiting变量来控制nextTick函数只会只执行一次。 flushSchedulerQueue函数会遍历queue队列,拿到每一个watcher,调用watcherrun方法。我们去来看一下run函数做了那些事情:

run () {
    if (this.active) {
      const value = this.get()
      if (value !== this.value || isObject(value) || this.deep) {
        const oldValue = this.value
        this.value = value
        if (this.user) {
          const info = `callback for watcher "${this.expression}"`
          invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

可以看到,run函数内部会重新执行get方法,针对渲染watcher而言,我们分析一下是如何实例化的:


updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
}, true)

class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,   
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    this.cb = cb
    this.active = true
    ...
    
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } 
    this.value = this.get()
  }
  get() {
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // 让updateComponent函数执行
    }
    return value
  }

实例化会执行一次get函数(内部会执行传入的updateComponent,依次执行_render(执行render函数,触发依赖收集),_update(patch为真实dom))。当我们对数据修改后,会执行dep.notify()-> wacther.update()-> queueWatcher(watcher) -> flushSchedulerQueue -> watcher.run() -> 执行get方法(重新执行_render,因为数据已经改变了,此时拿到的是最新值,重新_update patch成真实dom)。 手绘流程如如下:

Vue2.0响应式原理剖析 至此派发更新的流程也分析完了。

转载自:https://juejin.cn/post/7123575720973959182
评论
请登录