深入解析vue响应式原理
摘要:本文主要通过结合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的响应式系统是基于数据拦截+发布订阅的模式,包含了四个模块:
- Observer:通过Object.defineProperty拦截data属性的setter/getter方法,从而使每个属性都拥有一个Dep,当触发getter时收集依赖(使用该属性的watcher),当触发setter时通知更新;
- Dep:依赖收集器,用于维护依赖data属性的所有Watcher;
- Watcher:将视图依赖的属性绑定到Dep中,当数据修改时触发setter,调用Dep的notify方法,通知所有依赖该属性的Watcher进行update更新视图,使属性值与视图绑定起来;
- 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 = null
- const 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)
- }
- }
- }
blockquote { border-left: 3px solid rgba(128, 128, 128, 1) }
p { line-height: 1.8 }
h3 { margin-left: 0 !important; padding-left: 0 !important }
pre { color: rgba(255, 250, 250, 1); font-family: "Courier New"; background-color: rgba(45, 45, 45, 1); border: 1px solid rgba(128, 128, 128, 1) }
深入解析vue响应式原理的更多相关文章
- 深度解析 Vue 响应式原理
深度解析 Vue 响应式原理 该文章内容节选自团队的开源项目 InterviewMap.项目目前内容包含了 JS.网络.浏览器相关.性能优化.安全.框架.Git.数据结构.算法等内容,无论是基础还是进 ...
- vue响应式原理解析
# Vue响应式原理解析 首先定义了四个核心的js文件 - 1. observer.js 观察者函数,用来设置data的get和set函数,并且把watcher存放在dep中 - 2. watcher ...
- Vue源码--解读vue响应式原理
原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/ Vue的官方说明里有深入响应式原理这一节.在此官方也提到过: 当你把一个普通的 ...
- 详解Vue响应式原理
摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...
- vue响应式原理,去掉优化,只看核心
Vue响应式原理 作为写业务的码农,几乎不必知道原理.但是当你去找工作的时候,可是需要造原子弹的,什么都得知道一些才行.所以找工作之前可以先复习下,只要是关于vue的,必定会问响应式原理. 核心: / ...
- 深入Vue响应式原理
深入Vue.js响应式原理 一.创建一个Vue应用 new Vue({ data() { return { name: 'yjh', }; }, router, store, render: h =& ...
- 浅析Vue响应式原理(三)
Vue响应式原理之defineReactive defineReactive 不论如何,最终响应式数据都要通过defineReactive来实现,实际要借助ES5新增的Object.definePro ...
- 浅谈vue响应式原理及发布订阅模式和观察者模式
一.Vue响应式原理 首先要了解几个概念: 数据响应式:数据模型仅仅是普通的Javascript对象,而我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率. 双向绑定:数据改变,视图 ...
- 手摸手带你理解Vue响应式原理
前言 响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图.在面试中是经常考查的知识点,也是面试加分项. 本文将会循序渐进的解析响应式原理的工作流程,主要以下面结构进行: 分析主要成员,了解它 ...
随机推荐
- selenium IDE使用-1
selenium 硒 Mercury汞,外国人喜欢取这化学的名字 一.selenium概述 1.selenium是开源免费的,针对web应用程序功能自动化测试的工作. 2.做功能自动化的原因:回归测试 ...
- js下 Day07、DOM案例
一.折叠菜单 效果图: 功能思路分析: 功能一:数据渲染 \1. 模拟数据,一级数据为数组套对象的形式,二级数据为数组: \2. 先渲染一级数据,然后再嵌套渲染二级数据(map().join('')) ...
- Ecshop V2.7代码执行漏洞分析
0x01 此漏洞形成是由于未对Referer的值进行过滤,首先导致SQL注入,其次导致任意代码执行. 0x02 payload: 554fcae493e564ee0dc75bdf2ebf94caads ...
- PHPCMS V9.6.0 SQL注入漏洞分析
0x01 此SQL注入漏洞与metinfo v6.2.0版本以下SQL盲注漏洞个人认为较为相似.且较为有趣,故在此分析并附上exp. 0x02 首先复现漏洞,环境为: PHP:5.4.45 + Apa ...
- Windows安装VsCode 和Nodejs Vue
一.安装VSCode 1.在官网下载并安装VSCode https://code.visualstudio.com/Download 注意:解压到非系统盘(节约系统盘空间,也方便后面使用) 文件夹最好 ...
- js 点击input焦点不弹出键盘 PDA扫描枪
直接贴代码 1.利用input readonly属性 当input有readonly属性的时候,即使获取焦点,也不会吊起小键盘 扫码枪输入的间隔大概在15-60毫秒,然后手动输入的100-200毫秒之 ...
- C#访问Access数据库提示未安装ISAM
解决办法 1.在前面加上Jet OLEDB:,如: Jet OLEDB:Database Password='zt' <add name="ConStrOleDb" conn ...
- 关于MVC中 服务器无法在发送 HTTP 标头之后修改 cookie此类问题的解决
处理方法 使用过滤器控制权限时,若无权则跳转到无权页面,但是每次跳转都会出现 ERROR - System.Web.HttpException (0x80004005): 服务器无法在已发送 HTTP ...
- 3.mysql小表驱动大表的4种表连接算法
小表驱动大表 1.概念 驱动表的概念是指多表关联查询时,第一个被处理的表,使用此表的记录去关联其他表.驱动表的确定很关键,会直接影响多表连接的关联顺序,也决定了后续关联时的查询性能. 2.原则 驱动表 ...
- JPA 复杂查询 - Querydsl
添加依赖 <!--query dsl --> <dependency> <groupId>com.querydsl</groupId> <art ...