Vue.js 源码解析 1 - 响应式原理

2018-04-15 admin

Vue 实现响应式的机制简单来说就是 Object.defineProperty 实现的访问拦截观察者模式. 其他关键词包括: Observer Dep Watcher 和依赖收集. 这篇文章将会分析 Vue.js 的源码以解释这些概念, 讲解响应式原理, 还会给出一个简单的例子以在 Chrome 开发工具中验证这篇文章的内容.

你可以在 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 的响应式原理抽象成的模型.

responsive

接下来我们深入代码来讲解这个模型.

响应式初始化

当我们通过 new Vue({}) 创建 Vue 实例时, 构造函数会调用 Vue._init 方法, 其中会调用 initState, 而在这个方法会按序初始化 props methods data computed watch, 响应式初始化就发生在这里. 我们会着重讲解 datacomputed 的初始化过程. computed 依赖 props 或者 data, 所以是订阅者, 想要知道某个被订阅者的变化, 正好构成一个响应式关系!

initData

该方法将 data 变为响应式的, 它做了以下这些事情:

  1. data 函数中获取返回值作为 data, 这就是为什么在 Vue 中 data 应当是一个返回对象的函数
  2. 检查 data 中的属性有没有和 props 重名的
  3. data 中的属性全部代理到 Vue 实例上以进行访问
  4. 观察 data 对象

<pre>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 */) }</pre>

observe Observer defineReactive Dep

observe 尝试为一个对象创建 Observer, 或者返回已有的 Observer.

<pre>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 }</pre>

Observer 被附加到被观察的对象上, 一旦添加, 就会尝试将该对象的属性全部转化为 get/set 以实现依赖收集和触发更新.

<pre>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]) } } }</pre>

Dep 对象是什么? 它是一个用来记录A 对 B 的变化感兴趣数据结构, 其中 B 是某个对象或对象的某个属性, 而 A 是一个 Watcher. 当 A 需要在 B 的数据的变化时收到通知, 就会在 B 的 Dep 中注册自己, 当 B 发现数据更新的时候, 就会通知所有感兴趣的 A. 这就是观察者模式.

<pre>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() } } }</pre>

defineReactive 通过 Object.defineProperty 方法设置了某个属性的 get/set, 并在自己的作用域中创建了一个 Dep 对象. 它可以把某个属性变为响应式的, 原理就是 Object.definePropery 提供的 get 和 set. 当 Watcher 访问这个属性的时候, 首先会把自己标记为依赖收集的目标, 然后触发 get, get 会让自己闭包内保存的 Dep 进行依赖收集. 当这个属性被修改的时候, 会触发 set, set 会通知 Dep 让它去更新所有对它感兴趣的 Watcher.

<pre>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() } }) }</pre>

到这里, 被观察的一侧 (Dep) 需要做的工作就做好了.

initComputed

这个函数主要做了如下事情:

  1. 为每一个计算属性创建 Watcher 对象并添加到 _watchers 数组中
  2. 在 Vue 实例上代理访问计算属性

<pre>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)
  }
}

} }</pre>

相比于被观察者 Dep, 观察者 Watcher 要复杂得多! 所以我们就不把代码贴在这里了, 请去 lets-read-vue 中查看. 我们这里就讲对响应式来说很重要的几个方法.

<pre> // 这个方法用来对 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) } }</pre>

例子

接下来我们根据一个非常简单的例子来串讲我们之前覆盖的内容, 代码可以在 lets-read-vueplayground/responsive-demo.html 中找到.

<pre><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></pre>

responsive

在 Vue 实例初始化的时候, 先处理 data. initData 调用 observe 方法为 data 对象创建 Observer, 然后 Observer 调用自己的 walk 方法, walk 对每一个属性调用 defineReactivemessage 变成响应式的. 现在 $datamessage 都有自己的一个 Dep. 然后处理 computed, 为 computed 创建了一个 Watcher 并添加到 _watchers 数组中.

2018-04-13 13 23 53

message 属性的 Dep 保存在 defineReactive 函数调用时构成的闭包内, id 为 3.

2018-04-13 13 25 43

helloMessageWatcher 被创建, 但它没有依赖, 因为我们还没对它求值, 因为它也没有触发 messageget.

当我们在 console 访问 vm.$data.helloMessage 的时候, Watcherget 将会被调用, 这时候就通过触发 messageget 实现依赖收集, messageDepsubs 就有了 helloMessageWatcher, 与之对应 helloMessageWatcher 也会记录 messageDep.

2018-04-13 13 25 01

再次放上模型以供你温习.

2018-04-13 13 25 43

当我们修改 message 的时候, 就会触发 messageset, 此时 messageDep 就会去更新依赖, 调用 Watcherupdate 方法. 而 Watcher 如果属于某个计算属性, 仅仅会把自己设置为脏值, 仅有计算属性重新被访问的时候才会去实际求值 (这一点之前没有讲).

其他

当然了, 实现响应式的方式并不只有 datacomputed, 还有模板中的表达式, computed 相互的依赖和 watch 等等, 但原理都是如此, 就不再赘述了, 请自己阅读源码吧.

原文链接:https://github.com/wendzhue/blog/issues/3

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-27601.html

文章标题:Vue.js 源码解析 1 - 响应式原理

相关文章
10个强大的纯CSS3动画案例分享
我们的网页外观主要由CSS控制,编写CSS代码可以任意改变我们的网页布局以及网页内容的样式。CSS3的出现,更是可以让网页增添了不少动画元素,让我们的网页变得更加生动有趣,并且更易于交互。本文分享了10个非常炫酷的CSS3动画案例,希望大家...
2015-11-16
2015年JavaScript或“亲库而远框架”
2014年过去了,作为一个JavaScript开发者很难满怀信心的去“挽回”一个特定的库或技术,即便是强大的Angular,似乎也因为最近的一些事情而动摇。 2014年10月的ng-europe会议上,Angular开发者团队透露了一个关于...
2015-11-12
Node.js 2014这一年发生了什么
Node.js 的 2014 年充满了不幸和争议. 这一年 Noder 们经历了太多的伤心事, 经历了漫长的等待, 经历了沉重的分裂之痛. 也许 Noder 们不想回忆14年 Node.js land 发生的事情, 但正因为痛才更有铭记的价...
2015-11-12
vue+element-ui+slot-scope实现可编辑表格
1.咱开发拿到需求大多数是去网上找成型的组件,找不到再看原生的方法能否实现,大牛除外哈,大牛一般喜欢封装组件框架。 2.可编辑表格在后台管理系统还是比较常用的,因为比较流行框架element,iview都没有这个应用,所以考虑了两种方法,下...
2017-12-25
JavaScript实现PC手机端和嵌入式滑动拼图验证码三种效果
PC和手机端网站滑动拼图验证码效果源码,同时包涵了弹出式Demo,使用ajax形式提交二次验证码所需的验证结果值,嵌入式Demo,使用表单形式提交二次验证所需的验证结果值,移动端手动实现弹出式Demo三种效果 首先要确认前端使用页面,比如...
2017-03-17
React.js编程思想
JavaScript框架层出不穷,在很多程序员看来,React.js是创建大型、快速的Web应用的最好方式。这一款由Facebook出品的JS框架,无论是在Facebook还是在Instagram中,它的表现都非常出色。 使用React.j...
2015-11-12
YouTube正式默认使用HTML5视频播放器
YouTube视频网站现在默认使用HTML5播放器,这意味着更好的性能、 稳定性、 电池寿命和甚至是更好的安全性。现在用户通过Chrome、IE 11、Safari 8和Beta版本的Firefox进行浏览的时候都默认使用HTML5视频播放...
2015-11-12
Vue获取DOM元素样式和样式更改示例
在 vue 中用 document 获取 dom 节点进行节点样式更改的时候有可能会出现 ‘style’ is not definde的错误,这时候可以在 mounted 里用 $refs 来获取样式,并进行更改: &lt;template...
2017-03-13
从2014年的发展来展望JS的未来将会如何
&lt;font face=&quot;寰�杞�闆呴粦, Arial, sans-serif &quot;&gt;2014骞达紝杞�浠惰�屼笟鍙戝睍杩呴€燂紝鍚勭�嶈��瑷€灞傚嚭涓嶇┓锛屼互婊¤冻鐢ㄦ埛涓嶆柇鍙樺寲鐨勯渶姹傘€傝繖浜涜��...
2015-11-12
12个你未必知道的CSS小知识
虽然CSS并不是一种很复杂的技术,但就算你是一个使用CSS多年的高手,仍然会有很多CSS用法/属性/属性值你从来没使用过,甚至从来没听说过。 1.CSS的color属性并非只能用于文本显示 对于CSS的color属性,相信所有Web开发人员...
2015-11-12
回到顶部