深度解析 Vue 响应式原理

时间:2022-09-26 16:54:09

深度解析 Vue 响应式原理

该文章内容节选自团队的开源项目 InterviewMap。项目目前内容包含了 JS、网络、浏览器相关、性能优化、安全、框架、Git、数据结构、算法等内容,无论是基础还是进阶,亦或是源码解读,你都能在本图谱中得到满意的答案,希望这个面试图谱能够帮助到大家更好的准备面试。

Vue 初始化

在 Vue 的初始化中,会先对 props 和 data 进行初始化

Vue.prototype._init = function(options?: Object) {
// ...
// 初始化 props 和 data
initState(vm)
initProvide(vm)
callHook(vm, 'created') if (vm.$options.el) {
// 挂载组件
vm.$mount(vm.$options.el)
}
}
复制代码

接下来看下如何初始化 props 和 data

export function initState (vm: Component) {
// 初始化 props
if (opts.props) initProps(vm, opts.props)
if (opts.data) {
// 初始化 data
initData(vm)
}
}
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// 缓存 key
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// 非根组件的 props 不需要观测
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
// 验证 prop
const value = validateProp(key, propsOptions, propsData, vm)
// 通过 defineProperty 函数实现双向绑定
defineReactive(props, key, value)
// 可以让 vm._props.x 通过 vm.x 访问
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
} 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
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 (props && hasOwn(props, key)) {
} else if (!isReserved(key)) {
// 可以让 vm._data.x 通过 vm.x 访问
proxy(vm, `_data`, key)
}
}
// 监听 data
observe(data, true /* asRootData */)
}
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果 value 不是对象或者使 VNode 类型就返回
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
}
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()
this.vmCount = 0
// 通过 defineProperty 为对象添加 __ob__ 属性,并且配置为不可枚举
// 这样做的意义是对象遍历时不会遍历到 __ob__ 属性
def(value, '__ob__', this)
// 判断类型,不同的类型不同处理
if (Array.isArray(value)) {
// 判断数组是否有原型
// 在该处重写数组的一些方法,因为 Object.defineProperty 函数
// 对于数组的数据变化支持的不好,这部分内容会在下面讲到
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
// 遍历对象,通过 defineProperty 函数实现双向绑定
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 遍历数组,对每一个元素进行观测
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
复制代码

Object.defineProperty

无论是对象还是数组,需要实现双向绑定的话最终都会执行这个函数,该函数可以监听到 set 和 get 的事件。

export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创建依赖实例,通过闭包的方式让
// set get 函数使用
const dep = new Dep()
// 获得属性对象
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
} // 获取自定义的 getter 和 setter
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
// 如果 val 是对象的话递归监听
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 拦截 getter,当取值时会触发该函数
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 进行依赖收集
// 初始化时会在初始化渲染 Watcher 时访问到需要双向绑定的对象
// 从而触发 get 函数
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
// 拦截 setter,当赋值时会触发该函数
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// 判断值是否发生变化
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 如果新值是对象的话递归监听
childOb = !shallow && observe(newVal)
// 派发更新
dep.notify()
}
})
}
复制代码

在 Object.defineProperty 中自定义 get 和 set 函数,并在 get 中进行依赖收集,在 set 中派发更新。接下来我们先看如何进行依赖收集。

依赖收集

依赖收集是通过 Dep 来实现的,但是也与 Watcher 息息相关

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) {、
// 调用 Watcher 的 addDep 函数
Dep.target.addDep(this)
}
}
// 派发更新
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
// 同一时间只有一个观察者使用,赋值观察者
Dep.target = null
复制代码

对于 Watcher 来说,分为两种 Watcher,分别为渲染 Watcher 和用户写的 Watcher。渲染 Watcher 是在初始化中实例化的。

export function mountComponent(
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {} else {
// 组件渲染,该回调会在初始化和数据变化时调用
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
// 实例化渲染 Watcher
new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
},
true /* isRenderWatcher */
)
return vm
}
复制代码

接下来看一下 Watcher 的部分实现

export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
// ...
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
} get () {
// 该函数用于缓存 Watcher
// 因为在组件含有嵌套组件的情况下,需要恢复父组件的 Watcher
pushTarget(this)
let value
const vm = this.vm
try {
// 调用回调函数,也就是 updateComponent 函数
// 在这个函数中会对需要双向绑定的对象求值,从而触发依赖收集
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)
}
// 恢复 Watcher
popTarget()
// 清理依赖,判断是否还需要某些依赖,不需要的清除
// 这是为了性能优化
this.cleanupDeps()
}
return value
}
// 在依赖收集中调用
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 函数
// 将当前 Watcher push 进数组
dep.addSub(this)
}
}
}
}
export function pushTarget (_target: ?Watcher) {
// 设置全局的 target
if (Dep.target) targetStack.push(Dep.target)
Dep.target = _target
}
export function popTarget () {
Dep.target = targetStack.pop()
}
复制代码

以上就是依赖收集的全过程。核心流程是先对配置中的 props 和 data 中的每一个值调用 Obeject.defineProperty() 来拦截 set 和 get 函数,再在渲染 Watcher 中访问到模板中需要双向绑定的对象的值触发依赖收集。

派发更新

改变对象的数据时,会触发派发更新,调用 Dep 的 notify 函数

notify () {
// 执行 Watcher 的 update
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
update () {
if (this.computed) {
// ...
} else if (this.sync) {
// ...
} else {
// 一般会进入这个条件
queueWatcher(this)
}
}
export function queueWatcher(watcher: Watcher) {
// 获得 id
const id = watcher.id
// 判断 Watcher 是否 push 过
// 因为存在改变了多个数据,多个数据的 Watch 是同一个
if (has[id] == null) {
has[id] = true
if (!flushing) {
// 最初会进入这个条件
queue.push(watcher)
} else {
// 在执行 flushSchedulerQueue 函数时,如果有新的派发更新会进入这里
// 插入新的 watcher
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// 最初会进入这个条件
if (!waiting) {
waiting = true
// 将所有 Watcher 统一放入 nextTick 调用
// 因为每次派发更新都会引发渲染
nextTick(flushSchedulerQueue)
}
}
}
function flushSchedulerQueue() {
flushing = true
let watcher, id // 根据 id 排序 watch,确保如下条件
// 1. 组件更新从父到子
// 2. 用户写的 Watch 先于渲染 Watch
// 3. 如果在父组件 watch run 的时候有组件销毁了,这个 Watch 可以被跳过
queue.sort((a, b) => a.id - b.id) // 不缓存队列长度,因为在遍历的过程中可能队列的长度发生变化
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// 执行 beforeUpdate 钩子函数
watcher.before()
}
id = watcher.id
has[id] = null
// 在这里执行用户写的 Watch 的回调函数并且渲染组件
watcher.run()
// 判断无限循环
// 比如在 watch 中又重新给对象赋值了,就会出现这个情况
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' +
(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),
watcher.vm
)
break
}
}
}
// ...
}
复制代码

以上就是派发更新的全过程。核心流程就是给对象赋值,触发 set 中的派发更新函数。将所有 Watcher 都放入 nextTick 中进行更新,nextTick 回调中执行用户 Watch 的回调函数并且渲染组件。

Object.defineProperty 的缺陷

以上已经分析完了 Vue 的响应式原理,接下来说一点 Object.defineProperty 中的缺陷。

如果通过下标方式修改数组数据或者给对象新增属性并不会触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作,更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。

对于第一个问题,Vue 提供了一个 API 解决

export function set (target: Array<any> | Object, key: any, val: any): any {
// 判断是否为数组且下标是否有效
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 调用 splice 函数触发派发更新
// 该函数已被重写
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
// 判断 key 是否已经存在
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
}
复制代码

对于数组而言,Vue 内部重写了以下函数实现派发更新

// 获得数组原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 重写以下函数
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 缓存原生函数
const original = arrayProto[method]
// 重写函数
def(arrayMethods, method, function mutator (...args) {
// 先调用原生函数获得结果
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
// 调用以下几个函数时,监听新数据
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 手动派发更新
ob.dep.notify()
return result
})
})
复制代码

求职

最近本人在寻找工作机会,如果有杭州的不错岗位的话,欢迎联系我 zx597813039@gmail.com。

深度解析 Vue 响应式原理的更多相关文章

  1. 深入解析vue响应式原理

    摘要:本文主要通过结合vue官方文档及源码,对vue响应式原理进行深入分析. 1.定义 作为vue最独特的特性,响应式可以说是vue的灵魂了,表面上看就是数据发生变化后,对应的界面会重新渲染,那么响应 ...

  2. vue响应式原理解析

    # Vue响应式原理解析 首先定义了四个核心的js文件 - 1. observer.js 观察者函数,用来设置data的get和set函数,并且把watcher存放在dep中 - 2. watcher ...

  3. Vue源码--解读vue响应式原理

    原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/ Vue的官方说明里有深入响应式原理这一节.在此官方也提到过: 当你把一个普通的 ...

  4. 详解Vue响应式原理

    摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...

  5. vue响应式原理,去掉优化,只看核心

    Vue响应式原理 作为写业务的码农,几乎不必知道原理.但是当你去找工作的时候,可是需要造原子弹的,什么都得知道一些才行.所以找工作之前可以先复习下,只要是关于vue的,必定会问响应式原理. 核心: / ...

  6. 深入Vue响应式原理

    深入Vue.js响应式原理 一.创建一个Vue应用 new Vue({ data() { return { name: 'yjh', }; }, router, store, render: h =& ...

  7. 浅析Vue响应式原理(三)

    Vue响应式原理之defineReactive defineReactive 不论如何,最终响应式数据都要通过defineReactive来实现,实际要借助ES5新增的Object.definePro ...

  8. 浅谈vue响应式原理及发布订阅模式和观察者模式

    一.Vue响应式原理 首先要了解几个概念: 数据响应式:数据模型仅仅是普通的Javascript对象,而我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率. 双向绑定:数据改变,视图 ...

  9. Vue响应式原理及总结

    Vue 的响应式原理是核心是通过 ES5 的保护对象的 Object.defindeProperty 中的访问器属性中的 get 和 set 方法,data 中声明的属性都被添加了访问器属性,当读取 ...

随机推荐

  1. 微信小程序开发初探

    一.关于微信小程序 1.1 小程序诞生的背景 张小龙说道: (1)一切以用户价值为依归→用户是微信的核心,所以微信中没有很多与客户无关的功能,比如QQ中的乱七八糟一系列东西. (2)让创造发挥价值→所 ...

  2. Asp&period;Net MVC3 简单入门详解过滤器Filter&lpar;转&rpar;

    前言 在开发大项目的时候总会有相关的AOP面向切面编程的组件,而MVC(特指:Asp.Net MVC,以下皆同)项目中不想让MVC开发人员去关心和写类似身份验证,日志,异常,行为截取等这部分重复的代码 ...

  3. &lpar;WPF&rpar; MVVM&colon; ComboBox Binding&comma; XML 序列化

    基本思路还是在View的Xmal里面绑定ViewModel的属性,虽然在View的后台代码中也可以实现binding,但是还是在Xmal里面相对的代码量要少一些. 此例子要实现的效果就是将一个List ...

  4. Java Calendar 计算时间差

    public static void main(String[] args) { Calendar c=Calendar.getInstance(); int y=2016;//年 int M=1;/ ...

  5. 大型分布式C&plus;&plus;框架《四:netio之buffer管理器 下》

    每周一篇又来了.这次主要介绍netio的buffer管理器. 首先buffer管理是每一个网络层不可回避的问题.怎么高效的使用buffer是很关键的问题.这里主要介绍下我们的netio是怎么处理.说实 ...

  6. IIS7 URL Rewrite 用法实例

    原文:IIS7 URL Rewrite 用法实例 很友好的URL地址,使访问的人很容易记住.要求你的用户记住" http://www.contoso.com/article.aspx?id= ...

  7. CentOS 7 rsync

    CentOS 7 rsync 1)软件简介 Rsync 是一个远程数据同步工具,可通过 LAN/WAN 快速同步多台主机间的文件.Rsync 本来是用以取代rcp 的一个工具,它当前由 Rsync.s ...

  8. vue移动端金融UI组件库滴滴MandMobile面向金融场景设计附功能思维导图

    vue移动端金融UI组件库滴滴MandMobile面向金融场景设计附功能思维导图 Mand Mobile是面向金融场景设计的移动端组件库,基于Vue.js实现.目前已实际应用于滴滴四大金融业务板块的1 ...

  9. day20 Python 高阶函数&comma;函数,嵌套,闭包 装饰器

    高阶函数定义 1.函数接收的参数是一个函数名 2.函数的返回值是一个函数名 3.满足上述条件任意一个都可以称之为高阶函数 一.函数的接收参数是一个函数名 import time def foo(): ...

  10. UNIX环境编程学习笔记(11)——文件I&sol;O之文件时间以及 utime 函数

    lienhua342014-09-16 1 文件的时间 每个文件都有三个时间字段,如表 1 所示. 表 1: 文件的三个时间字段 说明 字段 st_atime 文件数据的最后访问时间 st_mtime ...