前言
响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图。在面试中是经常考查的知识点,也是面试加分项。
本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行:
- 分析主要成员,了解它们有助于理解流程
- 将流程拆分,理解其中的作用
- 结合以上的点,理解整体流程
文章稍长,但大部分是代码实现,还请耐心观看。为了方便理解原理,文中的代码会进行简化,如果可以请对照源码学习。
主要成员
响应式原理中,Observe、Watcher、Dep这三个类是构成完整原理的主要成员。
- Observe,响应式原理的入口,根据数据类型处理观测逻辑
- Watcher,用于执行更新渲染,组件会拥有一个渲染Watcher,我们常说的收集依赖,就是收集 Watcher
- Dep,依赖收集器,属性都会有一个Dep,方便发生变化时能够找到对应的依赖触发更新
下面来看看这些类的实现,包含哪些主要属性和方法。
Observe:我会对数据进行观测
温馨提示:代码里的序号对应代码块下面序号的讲解
// 源码位置:/src/core/observer/index.js class Observe { constructor(data) { this.dep = new Dep() // 1 def(data, '__ob__', this) if (Array.isArray(data)) { // 2 protoAugment(data, arrayMethods) // 3 this.observeArray(data) } else { // 4 this.walk(data) } } walk(data) { Object.keys(data).forEach(key => { defineReactive(data, key, data[key]) }) } observeArray(data) { data.forEach(item => { observe(item) }) } }
- 为观测的属性添加 __ob__ 属性,它的值等于 this,即当前 Observe 的实例
- 为数组添加重写的数组方法,比如:push、unshift、splice 等方法,重写目的是在调用这些方法时,进行更新渲染
- 观测数组内的数据,observe 内部会调用 new Observe,形成递归观测
- 观测对象数据,defineReactive 为数据定义 get 和 set ,即数据劫持
Dep:我会为数据收集依赖
// 源码位置:/src/core/observer/dep.js let id = 0 class Dep{ constructor() { this.id = ++id // dep 唯一标识 this.subs = [] // 存储 Watcher } // 1 depend() { Dep.target.addDep(this) } // 2 addSub(watcher) { this.subs.push(watcher) } // 3 notify() { this.subs.forEach(watcher => watcher.update()) } } // 4 Dep.target = null export function pushTarget(watcher) { Dep.target = watcher } export function popTarget(){ Dep.target = null } export default Dep
- 据收集依赖的主要方法,Dep.target 是一个 watcher 实例
- 添加 watcher 到数组中,也就是添加依赖
- 属性在变化时会调用 notify 方法,通知每一个依赖进行更新
- Dep.target 用来记录 watcher 实例,是全局唯一的,主要作用是为了在收集依赖的过程中找到相应的 watcher
pushTarget 和 popTarget 这两个方法显而易见是用来设置 Dep.target的。Dep.target 也是一个关键点,这个概念可能初次查看源码会有些难以理解,在后面的流程中,会详细讲解它的作用,需要注意这部分的内容。
Watcher:我会触发视图更新
// 源码位置:/src/core/observer/watcher.js let id = 0 export class Watcher { constructor(vm, exprOrFn, cb, options){ this.id = ++id // watcher 唯一标识 this.vm = vm this.cb = cb this.options = options // 1 this.getter = exprOrFn this.deps = [] this.depIds = new Set() this.get() } run() { this.get() } get() { pushTarget(this) this.getter() popTarget(this) } // 2 addDep(dep) { // 防止重复添加 dep if (!this.depIds.has(dep.id)) { this.depIds.add(dep.id) this.deps.push(dep) dep.addSub(this) } } // 3 update() { queueWatcher(this) } }
- this.getter 存储的是更新视图的函数
- watcher 存储 dep,同时 dep 也存储 watcher,进行双向记录
- 触发更新,queueWatcher 是为了进行异步更新,异步更新会调用 run 方法进行更新页面
响应式原理流程
对于以上这些成员具有的功能,我们都有大概的了解。下面结合它们,来看看这些功能是如何在响应式原理流程中工作的。
数据观测
数据在初始化时会通过 observe 方法来创建 Observe 类
// 源码位置:/src/core/observer/index.js export function observe(data) { // 1 if (!isObject(data)) { return } let ob; // 2 if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observe) { ob = data.__ob__ } else { // 3 ob = new Observe(data) } return ob }
在初始化时,observe 拿到的 data 就是我们在 data 函数内返回的对象。
- observe 函数只对 object 类型数据进行观测
- 观测过的数据都会被添加上 __ob__ 属性,通过判断该属性是否存在,防止重复观测
- 创建 Observe 类,开始处理观测逻辑
对象观测
进入 Observe 内部,由于初始化的数据是一个对象,所以会调用 walk 方法:
walk(data) { Object.keys(data).forEach(key => { defineReactive(data, key, data[key]) }) }
defineReactive 方法内部使用 Object.defineProperty 对数据进行劫持,是实现响应式原理最核心的地方。
function defineReactive(obj, key, value) { // 1 let childOb = observe(value) // 2 const dep = new Dep() Object.defineProperty(obj, key, { get() { if (Dep.target) { // 3 dep.depend() if (childOb) { childOb.dep.depend() } } return value }, set(newVal) { if (newVal === value) { return } value = newVal // 4 childOb = observe(newVal) // 5 dep.notify() return value } }) }
- 由于值可能是对象类型,这里需要调用 observe 进行递归观测
- 这里的 dep 就是上面讲到的每一个属性都会有一个 dep,它是作为一个闭包的存在,负责收集依赖和通知更新
- 在初始化时,Dep.target 是组件的渲染 watcher,这里 dep.depend 收集的依赖就是这个 watcher,childOb.dep.depend 主要是为数组收集依赖
- 设置的新值可能是对象类型,需要对新值进行观测
- 值发生改变,dep.notify 通知 watcher 更新,这是我们改变数据后能够实时更新页面的触发点
通过 Object.defineProperty 对属性定义后,属性的获取触发 get 回调,属性的设置触发 set 回调,实现响应式更新。
通过上面的逻辑,也能得出为什么 Vue3.0 要使用 Proxy 代替 Object.defineProperty 了。Object.defineProperty 只能对单个属性进行定义,如果属性是对象类型,还需要递归去观测,会很消耗性能。而 Proxy 是代理整个对象,只要属性发生变化就会触发回调。
数组观测
对于数组类型观测,会调用 observeArray 方法:
observeArray(data) { data.forEach(item => { observe(item) }) }
与对象不同,它执行 observe 对数组内的对象类型进行观测,并没有对数组的每一项进行 Object.defineProperty 的定义,也就是说数组内的项是没有 dep 的。
所以,我们通过数组索引对项进行修改时,是不会触发更新的。但可以通过 this.$set 来修改触发更新。那么问题来了,为什么 Vue 要这样设计?
结合实际场景,数组中通常会存放多项数据,比如列表数据。这样观测起来会消耗性能。还有一点原因,一般修改数组元素很少会直接通过索引将整个元素替换掉。例如:
export default { data() { return { list: [ {id: 1, name: 'Jack'}, {id: 2, name: 'Mike'} ] } }, cretaed() { // 如果想要修改 name 的值,一般是这样使用 this.list[0].name = 'JOJO' // 而不是以下这样 // this.list[0] = {id:1, name: 'JOJO'} // 当然你可以这样更新 // this.$set(this.list, '0', {id:1, name: 'JOJO'}) } }
数组方法重写
当数组元素新增或删除,视图会随之更新。这并不是理所当然的,而是 Vue 内部重写了数组的方法,调用这些方法时,数组会更新检测,触发视图更新。这些方法包括:
- push()
- pop()
- shift()
- unshift()
- splice()
- sort()
- reverse()
回到 Observe 的类中,当观测的数据类型为数组时,会调用 protoAugment 方法。
if (Array.isArray(data)) { protoAugment(data, arrayMethods) // 观察数组 this.observeArray(data) } else { // 观察对象 this.walk(data) }
这个方法里把数组原型替换为 arrayMethods ,当调用改变数组的方法时,优先使用重写后的方法。
function protoAugment(data, arrayMethods) { data.__proto__ = arrayMethods }
接下来看看 arrayMethods 是如何实现的:
// 源码位置:/src/core/observer/array.js // 1 let arrayProto = Array.prototype // 2 export let arrayMethods = Object.create(arrayProto) let methods = [ 'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice' ] methods.forEach(method => { arrayMethods[method] = function(...args) { // 3 let res = arrayProto[method].apply(this, args) let ob = this.__ob__ let inserted = '' switch(method){ case 'push': case 'unshift': inserted = args break; case 'splice': inserted = args.slice(2) break; } // 4 inserted && ob.observeArray(inserted) // 5 ob.dep.notify() return res } })
- 将数组的原型保存起来,因为重写的数组方法里,还是需要调用原生数组方法的
- arrayMethods 是一个对象,用于保存重写的方法,这里使用 Object.create(arrayProto) 创建对象是为了使用者在调用非重写方法时,能够继承使用原生的方法
- 调用原生方法,存储返回值,用于设置重写函数的返回值
- inserted 存储新增的值,若 inserted 存在,对新值进行观测
- ob.dep.notify 触发视图更新
依赖收集
依赖收集是视图更新的前提,也是响应式原理中至关重要的环节。
伪代码流程
为了方便理解,这里写一段伪代码,大概了解依赖收集的流程:
// data 数据 let data = { name: 'joe' } // 渲染watcher let watcher = { run() { dep.tagret = watcher document.write(data.name) } } // dep let dep = [] // 存储依赖 dep.tagret = null // 记录 watcher // 数据劫持 Object.defineProperty(data, 'name', { get(){ // 收集依赖 dep.push(dep.tagret) }, set(newVal){ data.name = newVal dep.forEach(watcher => { watcher.run() }) } })
初始化:
- 首先会对 name 属性定义 get 和 set
- 然后初始化会执行一次 watcher.run 渲染页面
- 这时候获取 data.name,触发 get 函数收集依赖。
更新:
修改 data.name,触发 set 函数,调用 run 更新视图。
真正流程
下面来看看真正的依赖收集流程是如何进行的。
function defineReactive(obj, key, value) { let childOb = observe(value) const dep = new Dep() Object.defineProperty(obj, key, { get() { if (Dep.target) { dep.depend() // 收集依赖 if (childOb) { childOb.dep.depend() } } return value }, set(newVal) { if (newVal === value) { return } value = newVal childOb = observe(newVal) dep.notify() return value } }) }
首先初始化数据,调用 defineReactive 函数对数据进行劫持。
export class Watcher { constructor(vm, exprOrFn, cb, options){ this.getter = exprOrFn this.get() } get() { pushTarget(this) this.getter() popTarget(this) } }
初始化将 watcher 挂载到 Dep.target,this.getter 开始渲染页面。渲染页面需要对数据取值,触发 get 回调,dep.depend 收集依赖。
class Dep{ constructor() { this.id = id++ this.subs = [] } depend() { Dep.target.addDep(this) } }
Dep.target 为 watcher,调用 addDep 方法,并传入 dep 实例。
export class Watcher { constructor(vm, exprOrFn, cb, options){ this.deps = [] this.depIds = new Set() } addDep(dep) { if (!this.depIds.has(dep.id)) { this.depIds.add(dep.id) this.deps.push(dep) dep.addSub(this) } } }
addDep 中添加完 dep 后,调用 dep.addSub 并传入当前 watcher 实例。
class Dep{ constructor() { this.id = id++ this.subs = [] } addSub(watcher) { this.subs.push(watcher) } }
将传入的 watcher 收集起来,至此依赖收集流程完毕。
补充一点,通常页面上会绑定很多属性变量,渲染会对属性取值,此时每个属性收集的依赖都是同一个 watcher,即组件的渲染 watcher。
数组的依赖收集
methods.forEach(method => { arrayMethods[method] = function(...args) { let res = arrayProto[method].apply(this, args) let ob = this.__ob__ let inserted = '' switch(method){ case 'push': case 'unshift': inserted = args break; case 'splice': inserted = args.slice(2) break; } // 对新增的值观测 inserted && ob.observeArray(inserted) // 更新视图 ob.dep.notify() return res } })
还记得重写的方法里,会调用 ob.dep.notify 更新视图,__ob__ 是我们在 Observe 为观测数据定义的标识,值为 Observe 实例。那么 ob.dep 的依赖是在哪里收集的?
function defineReactive(obj, key, value) { // 1 let childOb = observe(value) const dep = new Dep() Object.defineProperty(obj, key, { get() { if (Dep.target) { dep.depend() // 2 if (childOb) { childOb.dep.depend() } } return value }, set(newVal) { if (newVal === value) { return } value = newVal childOb = observe(newVal) dep.notify() return value } }) }
- observe 函数返回值为 Observe 实例
- childOb.dep.depend 执行,为 Observe 实例的 dep 添加依赖
所以在数组更新时,ob.dep 内已经收集到依赖了。
整体流程
下面捋一遍初始化流程和更新流程,如果你是初次看源码,不知道从哪里看起,也可以参照以下的顺序。由于源码实现比较多,下面展示的源码会稍微删减一些代码
初始化流程
入口文件:
// 源码位置:/src/core/instance/index.js import { initMixin } from './init' import { stateMixin } from './state' import { renderMixin } from './render' import { eventsMixin } from './events' import { lifecycleMixin } from './lifecycle' import { warn } from '../util/index' function Vue (options) { this._init(options) } initMixin(Vue) stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) export default Vue
_init:
// 源码位置:/src/core/instance/init.js export function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options"htmlcode">// 源码位置:/src/core/instance/state.js export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options 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) } } function initData (vm: Component) { let data = vm.$options.data // data 为函数时,执行 data 函数,取出返回值 data = vm._data = typeof data === 'function' "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(key)) { proxy(vm, `_data`, key) } } // observe data // 这里就开始走观测数据的逻辑了 observe(data, true /* asRootData */) }observe 内部流程在上面已经讲过,这里再简单过一遍:
- new Observe 观测数据
- defineReactive 对数据进行劫持
initState 逻辑执行完毕,回到开头,接下来执行 vm.$mount(vm.$options.el) 渲染页面:
$mount:
// 源码位置:/src/platforms/web/runtime/index.js Vue.prototype.$mount = function ( el"htmlcode">// 源码位置:/src/core/instance/lifecycle.js export function mountComponent ( vm: Component, el: "htmlcode">// 源码位置:/src/core/observer/watcher.js let uid = 0 export default class Watcher { constructor(vm, exprOrFn, cb, options){ this.id = ++id this.vm = vm this.cb = cb this.options = options // exprOrFn 就是上面传入的 updateComponent this.getter = exprOrFn this.deps = [] this.depIds = new Set() this.get() } get() { // 1. pushTarget 将当前 watcher 记录到 Dep.target,Dep.target 是全局唯一的 pushTarget(this) let value const vm = this.vm try { // 2. 调用 this.getter 相当于会执行 vm._render 函数,对实例上的属性取值, //由此触发 Object.defineProperty 的 get 方法,在 get 方法内进行依赖收集(dep.depend),这里依赖收集就需要用到 Dep.target 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) } // 3. popTarget 将 Dep.target 置空 popTarget() this.cleanupDeps() } return value } }至此初始化流程完毕,初始化流程的主要工作是数据劫持、渲染页面和收集依赖。
更新流程
数据发生变化,触发 set ,执行 dep.notify
// 源码位置:/src/core/observer/dep.js let uid = 0 /** * A dep is an observable that can have multiple * directives subscribing to it. */ export default class Dep { static target: "htmlcode">// 源码位置:/src/core/observer/watcher.js /** * 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) } }queueWatcher:
// 源码位置:/src/core/observer/scheduler.js // 用于存储 watcher const queue: Array<Watcher> = [] // 用于 watcher 去重 let has: { [key: number]: "htmlcode">// 源码位置:/src/core/util/next-tick.js const callbacks = [] let pending = false function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 // 遍历回调函数执行 for (let i = 0; i < copies.length; i++) { copies[i]() } } let timerFunc if (typeof Promise !== 'undefined' && isNative(Promise)) { const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) } } export function nextTick (cb"_blank" href="https://github.com/ChanWahFung/vue-source-demo" rel="external nofollow" >github,有需要的可以看看。以上就是详细分析vue响应式原理的详细内容,更多关于Vue响应式原理的资料请关注其它相关文章!