写点什么

深入解析 vue 响应式原理

发布于: 2021 年 01 月 20 日

摘要:本文主要通过结合 vue 官方文档及源码,对 vue 响应式原理进行深入分析。


1.定义

作为 vue 最独特的特性,响应式可以说是 vue 的灵魂了,表面上看就是数据发生变化后,对应的界面会重新渲染,那么响应式系统的底层细节到底是怎么一回事呢?


Tips:vue 的响应式系统在 vue2.0 和 vue3.0 版本中的底层实现有所不同,简单了来说就是处理属性的 getter/setter 部分从 Object.defineProperty 替换成了 Proxy(不过 vue3 也保留了 Object.defineProperty 方式用于支持 IE 浏览器)


1.1.vue2.0 实现原理

当一个普通的 javascript 对象传入 vue 实例作为 data 选项时,vue 将遍历 data 的所有属性,并使用 Object.defineProperty 重写这些属性的 getter/setter 方法,这些属性的 getter/setter 对于用户不可见,但是 vue 可以利用他们来追踪依赖,在属性值被访问和修改时通知变更。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中访问过的属性设置为依赖。之后当属性的 setter 触发时,会通知 watcher 对关联的组件进行重新渲染。



1.2.vue3.0 实现原理

当一个普通的 javascript 对象传入 vue 实例作为 data 选项时,vue 会将其转化为 Proxy。首次渲染后,组件将跟踪在渲染过程中被访问的属性,组件就成了这些属性的订阅者。当 proxy 拦截到 set 操作时,该属性将通知所有订阅了它的组件进行重新渲染。


2.源码解析

通过上面的定义可能对于响应式的原理还不够清楚,接下来通过对 vue 源码的分析进行深入理解。


2.1.vue2.0 源码实现

在 vue2.0 中,vue 的响应式系统是基于数据拦截+发布订阅的模式,包含了四个模块:


  1. Observer:通过 Object.defineProperty 拦截 data 属性的 setter/getter 方法,从而使每个属性都拥有一个 Dep,当触发 getter 时收集依赖(使用该属性的 watcher),当触发 setter 时通知更新;

  2. Dep:依赖收集器,用于维护依赖 data 属性的所有 Watcher;

  3. Watcher:将视图依赖的属性绑定到 Dep 中,当数据修改时触发 setter,调用 Dep 的 notify 方法,通知所有依赖该属性的 Watcher 进行 update 更新视图,使属性值与视图绑定起来;

  4. Compile:模板指令解析器,对模板每个元素节点的指令进行扫描解析,根据指令模板替换属性数据,同时注入 Watcher 更新数据的回调方法。



  • Observer


import Dep from './dep'import VNode from '../vdom/vnode'import { arrayMethods } from './array'import {  def,  warn,  hasOwn,  hasProto,  isObject,  isPlainObject,  isPrimitive,  isUndef,  isValidArrayIndex,  isServerRendering} from '../util/index'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
/** * In some cases we may want to disable observation inside a component's * update computation. */export let shouldObserve: boolean = true
export function toggleObserving (value: boolean) { shouldObserve = value}
/** * Observer class that is attached to each observed * object. Once attached, the observer converts the target * object's property keys into getter/setters that * collect dependencies and dispatch updates. */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) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { this.walk(value) } }
/** * Walk through all properties 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. */ observeArray (items: Array) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } }}
// helpers
/** * Augment a target Object or Array by intercepting * the prototype chain using __proto__ */function protoAugment (target, src: Object) { /* eslint-disable no-proto */ target.__proto__ = src /* eslint-enable no-proto */}
/** * Augment a target Object or Array by defining * hidden properties. *//* istanbul ignore next */function copyAugment (target: Object, src: Object, keys: Array) { for (let i = 0, l = keys.length; i < l; i++) { const key = keys[i] def(target, key, src[key]) }}
/** * Attempt to create an observer instance for a value, * returns the new observer if successfully observed, * or the existing observer if the value already has one. */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) } if (asRootData && ob) { ob.vmCount++ } return ob}
/** * Define a reactive property on an Object. */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 const getter = property && property.get const setter = property && property.set if ((!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) : 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() } })}
/** * Set a property on an object. Adds the new property and * triggers change notification if the property doesn't * already exist. */export function set (target: Array | Object, key: any, val: any): any { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`) } if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) target.splice(key, 1, val) return val } if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ) return val } if (!ob) { target[key] = val return val } defineReactive(ob.value, key, val) ob.dep.notify() return val}
/** * Delete a property and trigger change if necessary. */export function del (target: Array | Object, key: any) { if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot delete reactive property on undefined, null, or primitive value: ${(target: any)}`) } if (Array.isArray(target) && isValidArrayIndex(key)) { target.splice(key, 1) return } const ob = (target: any).__ob__ if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ) return } if (!hasOwn(target, key)) { return } delete target[key] if (!ob) { return } ob.dep.notify()}
/** * Collect dependencies on array elements when the array is touched, since * we cannot intercept array element access like property getters. */function dependArray (value: Array) { for (let e, i = 0, l = value.length; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.dep.depend() if (Array.isArray(e)) { dependArray(e) } }}
复制代码


总结:Observer 通过重写 data 上各个属性的 setter/getter 方法,对每个属性都维护一个 Dep,用于收集依赖该属性的所有 Watcher,当该属性触发 setter 时,派发更新的通知。


  • Dep


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;
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() 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 // order subs.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]}
复制代码


总结:Dep 一方面用数组收集与属性相关的 Watcher,另一方面遍历数组通知每个 Watcher 进行 update。


  • Watcher


import {  warn,  remove,  isObject,  parsePath,  _Set as Set,  handleError,  noop} from '../util/index'
import { traverse } from './traverse'import { queueWatcher } from './scheduler'import Dep, { pushTarget, popTarget } from './dep'
import type { SimpleSet } from '../util/index'
let uid = 0
/** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */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; newDeps: Array; depIds: SimpleSet; newDepIds: SimpleSet; before: ?Function; getter: Function; value: any;
constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } 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) if (!this.getter) { this.getter = noop process.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 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 }
/** * Add a dependency to this directive. */ 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) } } }
/** * Clean up for dependency collection. */ cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.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 value const oldValue = this.value this.value = value if (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.length while (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.length while (i--) { this.deps[i].removeSub(this) } this.active = false } }}
复制代码


总结:Watcher 是一个依赖于数据的订阅者,当数据发生变化时,Dep 调用 notify 方法,触发这些 Watcher 的 update 方法。


  • Compile


class Complie {  constructor(el, vm) {    this.el = this.isElementNode(el) ? el : document.querySelector(el)    this.vm = vm
if (this.el) { // 1.获取文档碎片对象,放入内存中,会减少页面的回流和重绘 let fragment = this.nodeFragment(this.el)
// 2.编译模板 this.complie(fragment)
// 3.追加子元素到根元素上 this.el.appendChild(fragment) } }
isElementNode(node) { return node.nodeType === 1 } nodeFragment(el) { el.firstChild // 创建一个内存碎片对象 const fragment = document.createDocumentFragment() let firstChild while ((firstChild = el.firstChild)) { fragment.appendChild(firstChild) } return fragment }
// 遍历获取并区分元素节点还是文本节点,然后进行相应的处理 complie(fragment) { // 1.获取到每个子节点 const childNodes = fragment.childNodes childNodes.forEach(child => { if (this.isElementNode(child)) { // 是元素节点 // 编译元素节点 this.complieElement(child) } else { // 文本节点 // 编译文本节点 this.complieText(child) }
if (child.childNodes && child.childNodes.length) { this.complie(child) } }) } // 编译元素 complieElement(node) { const attributes = node.attributes Array.from(attributes).forEach(attr => { const { name, value } = attr if (this.isDirective(name)) { // 表明是一个指令 const [, dirctive] = name.split('-') const [dirName, eventName] = dirctive.split(':') // text html model on
/* node(整个节点) value(msg) this.vm(相当于整个 MVue实例对象) eventName(v-on:click='btnClick') 中的事件名btnClick */ // 更新数据 数据驱动视图 compileUtil[dirName](node, value, this.vm, eventName)
// 删除有指令的标签上的属性 node.removeAttribute('v-' + dirctive) } else if (this.isEventName(name)) { // @click='handleClick' let [, eventName] = name.split('@') compileUtil['on'](node, value, this.vm, eventName) } }) } isEventName(eventName) { return eventName.startsWith('@') }
// 检测字符串是否以 v- 开头 isDirective(attrName) { return attrName.startsWith('v-') }
// 编译文本 complieText(node) { const content = node.textContent if (/\{\{(.+?)\}\}/.test(content)) { compileUtil['text'](node, content, this.vm) } }}
复制代码


作者:肖路

链接:https://www.cnblogs.com/xh666/p/14303738.html


用户头像

还未添加个人签名 2020.06.14 加入

领取文中资料加微信:gyhycx7980 备注:InfoQ 即可

评论

发布
暂无评论
深入解析vue响应式原理