Vue v2.6.11 响应式实现源码解析
注:建议学习源码从易到难(学习react也可建议先学习react16),核心是感受框架设计的理念,学习vue2后,再去对照学习vue3,从中对比效果更好。
github源码地址:
https://github.com/vuejs/vue/tree/v2.6.11
源码学习路线(红色是重点了解模块)
-
响应式实现篇
学习Vue中如何实现数据的响应式系统,从而达到数据驱动视图
-
虚拟 DOM 篇
学习什么是虚拟 DOM,以及Vue中的DOM-Diff原理
-
模板编译篇
学习Vue内部是怎么把template模板编译成虚拟DOM,从而渲染出真实DOM
-
实例方法篇
学习Vue中所有实例方法(即所有以$开头的方法)的实现原理
-
全局 API 篇
学习Vue中所有全局API的实现原理
-
生命周期篇
学习Vue中组件的生命周期实现原理
-
指令篇
学习Vue中所有指令的实现原理
-
过滤器篇
学习Vue中所有过滤器的实现原理
-
内置组件篇
学习Vue中内置组件的实现原理
项目目录:complier 和 core 是核心
├─dist # 项目构建后的文件├─scripts # 与项目构建相关的脚本和配置文件├─flow # flow的类型声明文件├─src # 项目源代码│ ├─complier # 与模板编译相关的代码│ ├─core # 通用的、与运行平台无关的运行时代码│ │ ├─observe # 实现变化侦测的代码│ │ ├─vdom # 实现virtual dom的代码│ │ ├─instance # Vue.js实例的构造函数和原型方法│ │ ├─global-api # 全局api的代码│ │ └─components # 内置组件的代码│ ├─server # 与服务端渲染相关的代码│ ├─platforms # 特定运行平台的代码,如weex│ ├─sfc # 单文件组件的解析代码│ └─shared # 项目公用的工具代码└─test # 项目测试代码
Vue的响应式实现
众所周知,Vue最大的特点之一就是数据驱动视图,那么什么是数据驱动视图呢?在这里,我们可以把数据理解为状态,而视图就是用户可直观看到页面。页面不可能是一成不变的,它应该是动态变化的,而它的变化也不应该是无迹可寻的,它或者是由用户操作引起的,亦或者是由后端数据变化引起的,不管它是因为什么引起的,我们统称为它的状态变了,它由前一个状态变到了后一个状态,页面也就应该随之而变化,所以我们就可以得到如下一个公式:
UI = render(state)
上述公式中:状态state是输入,页面UI输出,状态输入一旦变化了,页面输出也随之而变化。我们把这种特性称之为数据驱动视图。
Object的响应式实现
使对象变得可观测:this.walk(value);
let car = {};let val = 3000;Object.defineProperty(car, "price", {configurabel: true,enumerable: true,get() {console.log("price属性被读取了");return val;},set(newVal) {console.log("price属性被修改了");val = newVal}})
首先定义了一个observer类,将一个普通的object对象转换为可观测的object并且会给value新增一个__ob__的属性,相当于给value打了一个标记,表示已经被转化成响应式数据,避免重复操作。
判断数据的类型是否数组,只有object的数据才调用walk方法,把每一个属性转换成getter/setter的形式去监听数据变化。
if (Array.isArray(value)) {if (hasProto) {protoAugment(value, arrayMethods)} else {copyAugment(value, arrayMethods, arrayKeys)}this.observeArray(value)} else {this.walk(value)}}
在defineReactive中
/*** Define a reactive property on an Object.*/export function defineReactive (obj: Object,key: string,val: any,customSetter?: ?Function,shallow?: boolean) {// 实例化一个依赖管理器,生成一个依赖管理数组depconst dep = new Dep()const property = Object.getOwnPropertyDescriptor(obj, key)if (property && property.configurable === false) {return}// cater for pre-defined getter/settersconst getter = property && property.getconst setter = property && property.setif ((!getter || setter) && arguments.length === 2) {val = obj[key]}let childOb = !shallow && observe(val)Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () {const value = getter ? getter.call(obj) : valif (Dep.target) {// 在getter中收集依赖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 setterif (getter && !setter) returnif (setter) {setter.call(obj, newVal)} else {val = newVal}childOb = !shallow && observe(newVal)// 在setter中通知依赖更新dep.notify()}})}
依赖收集
我们可以监听到一个数据的变化,然后去更新视图,但是我们不可能数据一变化,就把全部视图都更新一遍,因此我们需要知道,谁依赖了这个数据。我们需要给每个数据建立一个依赖数组(一个数据可能被多处使用),谁依赖了这个数据(谁用到了这个数据),我们就把谁放入到依赖数组中,当这个数据发生变化,我们就去他对应的依赖数组中,通知每个依赖,告诉他们:你们依赖的数据变化了快去更新。
所谓的谁用到了这个数据,就是谁获取了这个数据。
在getter中收集依赖,在setter中去通知依赖更新。
我们应该给每一个数据都建立一个依赖管理器,把这个数据所有的依赖都管理起来,这里就我们的依赖管理器Dep。
/* @flow */import type Watcher from './watcher'import { remove } from '../util/index'import config from '../config'let uid = 0/*** A dep is an observable that can have multiple* directives subscribing to it.*/export default 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 () {// stabilize the subscriber list firstconst subs = this.subs.slice()if (process.env.NODE_ENV !== 'production' && !config.async) {// subs aren't sorted in scheduler if not running async// we need to sort them now to make sure they fire in correct// ordersubs.sort((a, b) => a.id - b.id)}for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}}}// The current target watcher being evaluated.// This is globally unique because only one watcher// can be evaluated at a time.Dep.target = nullconst targetStack = []export function pushTarget (target: ?Watcher) {targetStack.push(target)Dep.target = target}export function popTarget () {targetStack.pop()Dep.target = targetStack[targetStack.length - 1]}
依赖到底是谁
在Vue中实现了一个叫Watcher的类,Watcher类的实例就是我们上边说的“谁”。谁用到了数据,谁就是依赖,我们就给谁创建一个Watcher实例。在之后数据变化时,我们不会直接去通知依赖更新,而是通知这个依赖对应的Watch实例,由Watcher实例去通知真正的视图。
export default class 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<Dep>;depIds: SimpleSet;newDepIds: SimpleSet;before: ?Function;getter: Function;value: any;constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: ?Object,isRenderWatcher?: boolean) {this.vm = vmif (isRenderWatcher) {vm._watcher = this}vm._watchers.push(this)// optionsif (options) {this.deep = !!options.deepthis.user = !!options.userthis.lazy = !!options.lazythis.sync = !!options.syncthis.before = options.before} else {this.deep = this.user = this.lazy = this.sync = false}this.cb = cbthis.id = ++uid // uid for batchingthis.active = truethis.dirty = this.lazy // for lazy watchersthis.deps = []this.newDeps = []this.depIds = new Set()this.newDepIds = new Set()this.expression = process.env.NODE_ENV !== 'production'? expOrFn.toString(): ''// parse expression for getterif (typeof expOrFn === 'function') {this.getter = expOrFn} else {this.getter = parsePath(expOrFn)if (!this.getter) {this.getter = noopprocess.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` +'Watcher only accepts simple dot-delimited paths. ' +'For full control, use a function instead.',vm)}}this.value = this.lazy? undefined: this.get()}/*** Evaluate the getter, and re-collect dependencies.*/get () {pushTarget(this)let valueconst vm = this.vmtry {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 watchingif (this.deep) {traverse(value)}popTarget()this.cleanupDeps()}return value}/*** Add a dependency to this directive.*/addDep (dep: Dep) {const id = dep.idif (!this.newDepIds.has(id)) {this.newDepIds.add(id)this.newDeps.push(dep)if (!this.depIds.has(id)) {dep.addSub(this)}}}/*** Clean up for dependency collection.*/cleanupDeps () {let i = this.deps.lengthwhile (i--) {const dep = this.deps[i]if (!this.newDepIds.has(dep.id)) {dep.removeSub(this)}}let tmp = this.depIdsthis.depIds = this.newDepIdsthis.newDepIds = tmpthis.newDepIds.clear()tmp = this.depsthis.deps = this.newDepsthis.newDeps = tmpthis.newDeps.length = 0}/*** Subscriber interface.* Will be called when a dependency changes.*/update () {/* istanbul ignore else */if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {queueWatcher(this)}}/*** Scheduler job interface.* Will be called by the scheduler.*/run () {if (this.active) {const value = this.get()if (value !== this.value ||// Deep watchers and watchers on Object/Arrays should fire even// when the value is the same, because the value may// have mutated.isObject(value) ||this.deep) {// set new valueconst oldValue = this.valuethis.value = valueif (this.user) {try {this.cb.call(this.vm, value, oldValue)} catch (e) {handleError(e, this.vm, `callback for watcher "${this.expression}"`)}} else {this.cb.call(this.vm, value, oldValue)}}}}/*** Evaluate the value of the watcher.* This only gets called for lazy watchers.*/evaluate () {this.value = this.get()this.dirty = false}/*** Depend on all deps collected by this watcher.*/depend () {let i = this.deps.lengthwhile (i--) {this.deps[i].depend()}}/*** Remove self from all dependencies' subscriber list.*/teardown () {if (this.active) {// remove self from vm's watcher list// this is a somewhat expensive operation so we skip it// if the vm is being destroyed.if (!this.vm._isBeingDestroyed) {remove(this.vm._watchers, this)}let i = this.deps.lengthwhile (i--) {this.deps[i].removeSub(this)}this.active = false}}}

通过全局API:Vue.use和Vue.delete触发对象的新增和删除
Object总结流程
-
Object通过observer转换成getter/setter的形式去追踪数据变化。
-
当外界通过Watcher读取数据时,会触发getter从而将Watcher添加到依赖中。
-
当数据发生了变化会触发setter,从而向Dep中的依赖发送通知。
-
当Watcher接收到通知后,会向外界发送通知,可能会触发视图的更新,也有可能触发某个回调函数。
Array的响应式实现
我们依然还是要在获取数据的时候收集依赖。在数据变化时通知依赖更新。
在哪里收集依赖
data() {return {arr: [1,2,3],user: {},isLogin: true}}
Array型数据还是在getter中收集
let arr = [1,2,3]arr.push(4)Array.prototype.newPush = function(val) {console.log("arr被修改了")this.push(val)}arr.newPush(4)

创建拦截器
/** not type checking this file because flow doesn't play well with* dynamically accessing methods on Array prototype*/import { def } from '../util/index'const arrayProto = Array.prototype// 创建了一个对象作为拦截器export const arrayMethods = Object.create(arrayProto)const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']/*** Intercept mutating methods and emit events*/methodsToPatch.forEach(function (method) {// cache original methodconst original = arrayProto[method]def(arrayMethods, method, function mutator (...args) {const result = original.apply(this, args)const ob = this.__ob__let insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':inserted = args.slice(2)break}if (inserted) ob.observeArray(inserted)// notify changeob.dep.notify()return result})})
使用拦截器
constructor (value: any) {this.value = valuethis.dep = new Dep()this.vmCount = 0def(value, '__ob__', this)if (Array.isArray(value)) {if (hasProto) {// value.__proto__= arrayMethodsprotoAugment(value, arrayMethods)} else {copyAugment(value, arrayMethods, arrayKeys)}this.observeArray(value)} else {this.walk(value)}}
如何收集依赖
observeArray (items: Array<any>) {for (let i = 0, l = items.length; i < l; i++) {observe(items[i])}}
如何通知依赖
methodsToPatch.forEach(function (method) {// cache original methodconst original = arrayProto[method]def(arrayMethods, method, function mutator (...args) {const result = original.apply(this, args)const ob = this.__ob__let insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':inserted = args.slice(2)break}if (inserted) ob.observeArray(inserted)// notify changeob.dep.notify()return result})})
深度监听
在Vue中,不管是Object还是Array实现的响应式数据,都是深度监听,也就是不但要监听自身数据的变化,还要监听数据中所有的子数据的变化。
let arr = [{name: 'test',age,info: {address: 'xxx'}}]
数据元素的新增
我们向数组中新增了一个元素,我们需要把新增的元素转换为响应式数据。
Vue2版本的响应式不足
日常开发中,我们可能这样写
let arr = [1,2,3]arr[0] = 5; // 通过数组下标去修改数组中的数据arr.length = 0; // 通过修改数组长度去清空数组
但这两种方法都无法监听到,也不会触发响应式更新。因此才有后续Vue3的Proxy响应式处理方案。
夜雨聆风
