Vue数据驱动原理+源码阅读

实际问题

大家在利用Vue进行前端开发过程中,是否遇到过这样的问题:



request(url).then(({ data }) => {
	this.tableData = data && data.items;
	this.tableData.forEach((itemData) => {
		itemData.disable = true;  //发现这句竟然不起作用,页面上没有显示item.name;
	});
});

又或者遇到这种情况:


后来,才在Vue的官方文档中,找到了问题的原因:
Vue深入响应式原理

那么我们接下来就来扒一扒Vue的响应式是如何做的吧?

Vue响应式原理

要了解Vue响应式原理,我们要从两个方向去了解,一个是Vue如何将数据渲染到页面上的;第二个就是Vue如何在数据变化的时候来更新视图的。

数据渲染到页面

举一个例子:

{{name}}

那么我们接下来就了解下模版与数据是如何最终渲染成视图/DOM的?

new Vue(options)

/src/core/instance/index.js

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

可以看到,要使用Vue实例,必须通过new 关键字来生成,new之后,就调了vue内部方法_init(options)函数,那么我们跟进看下_init函数。

/src/core/instance/index.js 部分关键代码

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
   
    // a flag to avoid this being observed
    vm._isVue = true
    
    // 合并配置
    if (options && options._isComponent) {
      
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      
      initInternalComponent(vm, options)
    } else {
    // 主要将 Vue默认的option 与我们自定义的选项对象合并
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
   
    // expose real self
    vm._self = vm
    // 一些初始化操作
    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')

    if (vm.$options.el) {  // 进行挂载
      vm.$mount(vm.$options.el)
    }
  }

由于渲染与构建方式跟平台有关,因此,vm.$mount分布在了很多地方,比如:/src/platforms/web/entry-runtime-with-compiler.js和/src/platforms/web/runtime/index.js;/src/platforms/weex/runtime/index.js

在Vue 2.0中,引入了服务端渲染(SSR),需讲编译器和运行时分开来,因此出现了两种版本: 独立构建和运行时构建。独立构建就是包括编译和运行时,比如

new Vue({
	el: '#app',
	template: '
' });

使用template时,需将template渲染为render函数,运行时再执行render函数;
运行时构建不包括编译阶段,比如

new Vue({
	el: '#app',
	render: h => h(App)
});

由于独立构建的$mount过程实质与运行时构建的$mount过程具有相似性,这里我们只对运行时的$mount过程做一个梳理

/src/platforms/web/runtime/index.js 代码

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

可以看出,$mount方法主要调了mountComponent方法。

/src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 运行时构建需传入render属性
  if (!vm.$options.render) {
  // 如果没有render属性,提示warning;
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  // 生命周期相关,暂时先跳过
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

这里主要是通过new Watcher来运行回调函数updateComponent,而updateComponent主要进行了两个操作:vm._render(), vm._update(),分别是生成vdom和将vdom转换成DOM元素,这里不做展开,下次在vDOM分析的时候再细展开。最终,在vm._isMounted = true时,代表整个mount过程已经完成。

数据变化触发页面视图变化

看过Vue官方文档的同学,应该对数据驱动有所了解,数据驱动主要利用了Object.defineProperty()来产生一个访问器属性,即定义get和set函数,从而在获取和改变数据时,能插入自定义的一些操作。

那么我们通过VUE源码来了解下其中具体的机制:

首先,new Vue之后还是执行了this._init,_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')

其中,initState就是处理一些相关数据的操作。

/src/core/instance/state.js

if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
	/* 这里主要是处理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)
}

处理data: {}里面的数据主要是在initData中来做:

/src/core/instance/state.js

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
 
  // 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]
    proxy(vm, `_data`, key)
}
  // observe data
  observe(data, true /* asRootData */)
}

proxy(vm, _data, key)主要是将this._data代理到vm实例上面来,使得我们可以直接通过this来访问data里面的数据;
最核心的部分就在于observe()函数了,observe()函数主要就做了下面一件事:

/src/core/observer/index.js

ob = new Observer(value);
return ob;

/src/core/observer/index.js

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)
    // 主要是walk函数
    this.walk(value)
  }

/src/core/observer/index.js

walk (obj: Object) {
// 对data里面每一个值执行了一次defineReactive()函数
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

那么我们主要来看看defineReactive函数干了什么?
在这之前,我们了解下Dep Class和Watcher Class,以及二者之间的关系。

watcher和dep是很典型的发布订阅模式,每个数据都有一个dep对象,这个dep对象记录这个数据所订阅的watcher;当该数据发生变化时,会触发每个watcher的更新,执行watcher上面的回调函数。具体通过代码来看看如何实现的:

dep.js

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

  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 () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
  
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

可以看到,dep对象只有两个属性,一个id,另一个是subs,这个主要是用于记录某个数据依赖的watcher有哪些的;
addSubs => 将某个数据依赖的watcher记录到dep.subs数组里面;
notify => 主要是当数据变化的时候,执行watcher的update方法;
depend => 每一个watcher都对应有很多个订阅者,所以该方法用于记录订阅某个watcher的dep有哪些。
这里值得一提的是:static target 用于记录全局唯一的watcher,也就是说 任何时刻,只能有一个watcher执行更新。

再来看watcher:
watcher.js

// watcher对象有很多属性
  vm: Component;
  expression: string;  // 由外部传入,构造函数会调用该计算表达式或方法
  cb: Function; // 回调函数
  id: number;  
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array;   // 记录依赖的数据的dep
  newDeps: Array; 
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;  
  value: any;
// watcher构造函数
// 	去掉了一些判断和特殊处理,只留了最重要的一部分代码
// watcher的构造函数其实主要是执行get()方法
constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
 
    vm._watchers.push(this)
  
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  // get方法
  // 主要执行new Watcher(vm, ex, cb)时传入的计算表达式或函数ex; 可以看到在执行的时候,先把当前的watcher压栈,中间执行一些函数,后面再进行弹栈,保证每一次全局只有一个激活的Watch对象。
	get () {
    pushTarget(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
  }

	// 记录订阅该Watcher的deps;
	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)
      }
    }
  }

大概了解dep和watcher的概念之后,我们接着来看defineReactive函数:
defineReactive():

{
  const dep = new Dep()   // 每个数据都有一个dep对象

  let childOb = !shallow && observe(val)
  // 重点来了!!
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {  // 定义访问器属性,在设置某个数据的时候,对其进行依赖收集,将当前的激活Watcher push到该数据对应的dep.subs中
      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(),将调用该数据的dep.subs中所有Watcher的update方法,也就是说,所有依赖于该数据的数据/视图都将发生变化。
      dep.notify()
    }
  })
}

以上就是整个数据驱动的全过程,从数据到视图的绑定,到数据改变触发setter从而改变视图。这篇文章只是大概的理解和梳理,后面将对具体某块进行分析和理解,有不同的意见和建议希望大家可以提出。。

你可能感兴趣的:(JavaScript,Vue)