Vue 实现响应式的机制简单来说就是
Object.defineProperty
实现的访问拦截和观察者模式. 其他关键词包括:
Observer Dep Watcher
和依赖收集. 这篇文章将会分析 Vue.js 的源码以解释这些概念, 讲解响应式原理, 还会给出一个简单的例子以在 Chrome 开发工具中验证这篇文章的内容.
你可以在 https://github.com/wendzhue/lets-read-vue 中找到注释后的源码以及文末例子的源码.
Vue.js 项目的结构如下:
- src
- compiler // template 编译
- codegen
- create-compiler.js
- directives
error-detector.js
helpers.js
- index.js
- optimizer.js
- parser
- to-function.js
- core // 所有的核心代码, 重中之重
- components // 主要是 keep-alive 抽象组件
- config.js
- global-api
- index.js
- instance // 主要模块, 实现生命周期, 状态, 事件, 渲染等等
- observer // 响应式核心代码
- util
- vdom // Virual DOM
- platforms
- web
- weex
- server // 服务端渲染相关
- bundle-renderer
- create-basic-renderer.js
- create-renderer.js
- optimizing-compiler
render-context.js
render-stream.js
render.js
- template-renderer
- util.js
- webpack-plugin
- write.js
- sfc
- parser.js
- shared
- constants.js
- util.js
这篇文章相关的代码都在 src/core 底下.
响应式模型
先给出一个 Vue.js 的响应式原理抽象成的模型.
接下来我们深入代码来讲解这个模型.
响应式初始化
当我们通过 new Vue({}) 创建 Vue 实例时, 构造函数会调用 Vue._init 方法, 其中会调用 initState, 而在这个方法会按序初始化
props methods data computed watch
, 响应式初始化就发生在这里. 我们会着重讲解 data 和 computed 的初始化过程. computed 依赖 props 或者 data, 所以是订阅者, 想要知道某个被订阅者的变化, 正好构成一个响应式关系!
initData
该方法将 data 变为响应式的, 它做了以下这些事情:
从 data 函数中获取返回值作为 data, 这就是为什么在 Vue 中 data 应当是一个返回对象的函数
检查 data 中的属性有没有和 props 重名的
将 data 中的属性全部代理到 Vue 实例上以进行访问
观察 data 对象
- function initData(vm: Component) {
- let data = vm.$options.data
- data = vm._data = typeof data === 'function'
- ? getData(data, vm)
- : data || {}
- if (!isPlainObject(data)) {
- data = {}
- process.env.NODE_ENV !== 'production' && warn(
- 'data functions should return an object:\n' +
- 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
- vm
- )
- }
- // proxy data on instance
- // 遍历 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)) {
- // 将 data 中的属性全部代理到 Vue 实例上以进行访问
- proxy(vm, `_data`, key)
- }
- }
- // observe data
- // 使得 data 变为响应式的, 由于 asRootData 为 true, 可以想象有个 Observer 的 vmCount 会 + 1
- observe(data, true /* asRootData */)
- }
observe Observer defineReactive Dep
observe 尝试为一个对象创建 Observer, 或者返回已有的 Observer.
- export function observe(value: any, asRootData: ?boolean): Observer | void {
- if (!isObject(value) || value instanceof VNode) {
- return
- }
- let ob: Observer | void
- if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
- ob = value.__ob__
- } else if (
- shouldObserve &&
- !isServerRendering() &&
- (Array.isArray(value) || isPlainObject(value)) &&
- Object.isExtensible(value) &&
- !value._isVue
- ) {
- ob = new Observer(value)
- }
- // 如果作为根数据要加上 vmCount
- if (asRootData && ob) {
- ob.vmCount++
- }
- return ob
- }
Observer 被附加到被观察的对象上, 一旦添加, 就会尝试将该对象的属性全部转化为 get/set 以实现依赖收集和触发更新.
- export class Observer {
- value: any;
- dep: Dep;
- vmCount: number; // number of vms that has this object as root $data
- constructor(value: any) {
- this.value = value
- this.dep = new Dep() // 创建 Dep, 这个 Dep 是对象自己而非它的属性的 Dep
- this.vmCount = 0
- def(value, '__ob__', this)
- // 如果对象是一个数列, 用 Vue 更新后的数组方法实现响应式, 这就是为什么在 Vue 中用数组下标访问无法实现响应式的效果
- if (Array.isArray(value)) {
- const augment = hasProto
- ? protoAugment
- : copyAugment
- augment(value, arrayMethods, arrayKeys)
- this.observeArray(value)
- } else {
- // 如果是一个对象, 就转化 get/set
- this.walk(value)
- }
- }
- /**
- * Walk through each property 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.
- *
- * 如果对象是一个数组, 就 observe 数组中的每一个元素
- */
- observeArray(items: Array<any>) {
- for (let i = 0, l = items.length; i <l; i++) {
- observe(items[i])
- }
- }
- }
Dep 对象是什么? 它是一个用来记录 A 对 B 的变化感兴趣数据结构, 其中 B 是某个对象或对象的某个属性, 而 A 是一个 Watcher. 当 A 需要在 B 的数据的变化时收到通知, 就会在 B 的 Dep 中注册自己, 当 B 发现数据更新的时候, 就会通知所有感兴趣的 A. 这就是观察者模式.
- export default class Dep {
- static target: ?Watcher;
- id: number;
- subs: Array<Watcher>;
- constructor() {
- this.id = uid++
- this.subs = []
- }
- // 将会被某个 Watcher 调用, 修改自己的订阅者数组
- addSub(sub: Watcher) {
- this.subs.push(sub)
- }
- removeSub(sub: Watcher) {
- remove(this.subs, sub)
- }
- // 将会被某个 getter 调用, 收集 Dep.target 指向的 Watcher
- 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()
- }
- }
- }
defineReactive 通过
Object.defineProperty
方法设置了某个属性的 get/set, 并在自己的作用域中创建了一个 Dep 对象. 它可以把某个属性变为响应式的, 原理就是
Object.definePropery
提供的 get 和 set. 当 Watcher 访问这个属性的时候, 首先会把自己标记为依赖收集的目标, 然后触发 get, get 会让自己闭包内保存的 Dep 进行依赖收集. 当这个属性被修改的时候, 会触发 set, set 会通知 Dep 让它去更新所有对它感兴趣的 Watcher.
- export function defineReactive(
- obj: Object,
- key: string,
- val: any,
- customSetter?: ?Function,
- shallow?: boolean
- ) {
- // 每个属性都会有一个依赖者对象
- const dep = new Dep()
- // 如果属性值已经被设置为不可配置, 就直接返回, 什么都不做
- const property = Object.getOwnPropertyDescriptor(obj, key)
- if (property && property.configurable === false) {
- return
- }
- // cater for pre-defined getter/setters
- // 如果开发者定义的属性原本就有 setter/getter, 要对它们予以保留
- const getter = property && property.get
- const setter = property && property.set
- if ((!getter || setter) && arguments.length === 2) {
- val = obj[key]
- }
- // 如果不是浅观察, 而且被观察值是一个对象的话, 就会返回一个 Observer 对象
- let childOb = !shallow && observe(val)
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get: function reactiveGetter() {
- const value = getter ? getter.call(obj) : val // 真实的值使用闭包进行存储的
- if (Dep.target) {
- dep.depend() // 进行依赖搜集
- if (childOb) {
- // 对该对象的属性也要进行依赖搜集, 因为这个 watcher 很可能就是对这些属性有依赖
- // 问题在于: Vue 会为属性的属性的属性实现响应式吗?
- childOb.dep.depend()
- if (Array.isArray(value)) {
- dependArray(value) // 如果值是数组, 递归进行数组的依赖搜集
- }
- }
- }
- return value
- },
- set: function reactiveSetter(newVal) {
- // 如果没有改变, 就不要 set
- 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
- }
- // 要观察一下对象, 因为这里的 setter 是整个对象被替换掉了
- childOb = !shallow && observe(newVal)
- // 通知该属性的 Dep 属性值已经改变, 对应的 Watcher 应该收到通知
- dep.notify()
- }
- })
- }
到这里, 被观察的一侧 (Dep) 需要做的工作就做好了.
initComputed
这个函数主要做了如下事情:
为每一个计算属性创建 Watcher 对象并添加到 _watchers 数组中
在 Vue 实例上代理访问计算属性
- const computedWatcherOptions = { computed: true }
- function initComputed(vm: Component, computed: Object) {
- // $flow-disable-line
- const watchers = vm._computedWatchers = Object.create(null)
- // computed properties are just getters during SSR
- const isSSR = isServerRendering()
- for (const key in computed) {
- const userDef = computed[key]
- // computed 可以是一个有 get 和 set 两个函数的对象, 这里找到正确的 getter
- const getter = typeof userDef === 'function' ? userDef : userDef.get
- if (process.env.NODE_ENV !== 'production' && getter == null) {
- warn(
- `Getter is missing for computed property "${key}".`,
- vm
- )
- }
- if (!isSSR) {
- // create internal watcher for the computed property.
- // 为计算属性创建 Watcher, 并且在创建的时候特别声明为计算属性而创建
- watchers[key] = new Watcher(
- vm,
- getter || noop,
- noop,
- computedWatcherOptions
- )
- }
- // component-defined computed properties are already defined on the
- // component prototype. We only need to define computed properties defined
- // at instantiation here.
- if (!(key in vm)) {
- // 在 Vue 实例上代理访问计算属性
- defineComputed(vm, key, userDef)
- } else if (process.env.NODE_ENV !== 'production') {
- if (key in vm.$data) {
- warn(`The computed property "${key}" is already defined in data.`, vm)
- } else if (vm.$options.props && key in vm.$options.props) {
- warn(`The computed property "${key}" is already defined as a prop.`, vm)
- }
- }
- }
- }
相比于被观察者 Dep, 观察者 Watcher 要复杂得多! 所以我们就不把代码贴在这里了, 请去 https://github.com/wendzhue/lets-read-vue 中查看. 我们这里就讲对响应式来说很重要的几个方法.
- // 这个方法用来对 computed 实际求值
- get() {
- pushTarget(this) // 先将自己设置为依赖搜集的对象
- let value
- const vm = this.vm
- try {
- // 这里调用了 getter 实现了依赖收集! 因为 getter 里面必然访问了某个对象的属性, 看 defineReactive
- 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
- }
- // 这个方法和 Dep 中的方法协作, 将会被一个 Dep 调用, Dep 会把自己传过来
- // 更新 Dep 的过程, 是记录这一次更新过程中自己需要的依赖, 与上一次更新的依赖作比较
- // 订阅新的依赖, 将不再需要的依赖剔除掉 (通过 cleanupDeps 方法)
- addDep(dep: Dep) {
- const id = dep.id
- // 记录新的依赖
- if (!this.newDepIds.has(id)) {
- this.newDepIds.add(id)
- this.newDeps.push(dep)
- // 如果自己没有订阅过这个 Dep, 就订阅
- if (!this.depIds.has(id)) {
- dep.addSub(this)
- }
- }
- // 当 setter 被触发的时候, 就会调用 Dep 的 update, Dep 再来调用 update 方法
- update() {
- if (this.computed) {
- // A computed property watcher has two modes: lazy and activated.
- // It initializes as lazy by default, and only becomes activated when
- // it is depended on by at least one subscriber, which is typically
- // another computed property or a component's render function.
- // 如果 Watcher 作为计算属性的 Watcher, 那么它会有两种模式, 当它没有订阅者的时候就是 lazy
- // 模式, 仅仅将 Watcher 设置为 dirty, 然后当计算属性被访问的时候, 才会重新计算
- // 如果有订阅者的时候, 就是 activated 模式, 立即计算新值, 但只有在值真的发生变化的时候
- // 才去通知自己的订阅者
- if (this.dep.subs.length === 0) {
- // In lazy mode, we don't want to perform computations until necessary,
- // so we simply mark the watcher as dirty. The actual computation is
- // performed just-in-time in this.evaluate() when the computed property
- // is accessed.
- this.dirty = true
- } else {
- // In activated mode, we want to proactively perform the computation
- // but only notify our subscribers when the value has indeed changed.
- this.getAndInvoke(() => {
- this.dep.notify()
- })
- }
- } else if (this.sync) {
- // 如果是渲染函数指令中的 Watcher 且有 .sync 修饰符, 就立即更新, 以后再讲
- this.run()
- } else {
- // 否则进入更新队列, 之后在讲 Vue 的异步更新策略的时候会讲
- queueWatcher(this)
- }
- }
例子
接下来我们根据一个非常简单的例子来串讲我们之前覆盖的内容, 代码可以在 https://github.com/wendzhue/lets-read-vue 的
playground/responsive-demo.html
中找到.
- <body>
- <div id="app">
- </div>
- <script src="https://vuejs.org/js/vue.js">
- </script>
- <script>
- var vm = new Vue({
- data: () =>({
- message: 'Wendell'
- }),
- computed: {
- helloMessage() {
- return 'Hello' + this.message
- }
- },
- el: '#app'
- })
- </script>
- </body>
在 Vue 实例初始化的时候, 先处理 data. initData 调用 observe 方法为 data 对象创建 Observer, 然后 Observer 调用自己的 walk 方法, walk 对每一个属性调用 defineReactive 把 message 变成响应式的. 现在 $data 和 message 都有自己的一个 Dep. 然后处理 computed, 为 computed 创建了一个 Watcher 并添加到 _watchers 数组中.
message 属性的 Dep 保存在 defineReactive 函数调用时构成的闭包内, id 为 3.
helloMessage 的 Watcher 被创建, 但它没有依赖, 因为我们还没对它求值, 因为它也没有触发 message 的 get.
当我们在 console 访问
vm.$data.helloMessage
的时候, Watcher 的 get 将会被调用, 这时候就通过触发 message 的 get 实现依赖收集, message 的 Dep 的 subs 就有了 helloMessage 的 Watcher, 与之对应 helloMessage 的 Watcher 也会记录 message 的 Dep.
再次放上模型以供你温习.
当我们修改 message 的时候, 就会触发 message 的 set, 此时 message 的 Dep 就会去更新依赖, 调用 Watcher 的 update 方法. 而 Watcher 如果属于某个计算属性, 仅仅会把自己设置为脏值, 仅有计算属性重新被访问的时候才会去实际求值 (这一点之前没有讲).
其他
当然了, 实现响应式的方式并不只有 data 到 computed, 还有模板中的表达式, computed 相互的依赖和 watch 等等, 但原理都是如此, 就不再赘述了, 请自己阅读源码吧.
来源: https://juejin.im/entry/5ad0433a51882555867fd53a