前言
响应式原理作为 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?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// mergeOptions 对 mixin 选项和传入的 options 选项进行合并
// 这里的 $options 可以理解为 new Vue 时传入的对象
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
// 初始化数据
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
if (vm.$options.el) {
// 初始化渲染页面 挂载组件
vm.$mount(vm.$options.el)
}
}
}
上面主要关注两个函数,initState
初始化数据,vm.$mount(vm.$options.el)
初始化渲染页面。
先进入 initState
:
// 源码位置:/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'
? getData(data, vm)
: data || {}
// 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)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${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?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
mountComponent
:
// 源码位置:/src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
callHook(vm, 'beforeMount')
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`
mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)
mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
// 数据改变时 会调用此方法
updateComponent = () => {
// vm._render() 返回 vnode,这里面会就对 data 数据进行取值
// vm._update 将 vnode 转为真实dom,渲染到页面上
vm._update(vm._render(), hydrating)
}
}
// 执行 Watcher,这个就是上面所说的渲染wacther
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
Watcher
:
// 源码位置:/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: ?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 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++) {
// 执行 watcher 的 update 方法
subs[i].update()
}
}
}
wathcer.update
:
// 源码位置:/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]: ?true } = {}
/**
* Flush both queues and run the watchers.
*/
function flushSchedulerQueue () {
let watcher, id
// 对 watcher 排序
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
// run方法更新视图
watcher.run()
}
}
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
// watcher 加入数组
queue.push(watcher)
// 异步更新
nextTick(flushSchedulerQueue)
}
}
nextTick
:
// 源码位置:/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?: Function, ctx?: Object) {
let _resolve
// 将回调函数加入数组
callbacks.push(() => {
if (cb) {
cb.call(ctx)
}
})
if (!pending) {
pending = true
// 遍历回调函数执行
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
这一步是为了使用微任务将回调函数异步执行,也就是上面的p.then
。最终,会调用 watcher.run
更新页面。
至此更新流程完毕。
写在最后
如果没有接触过源码的同学,我相信看完可能还是会有点懵的,这很正常。建议对照源码再自己多看几遍就能知道流程了。对于有基础的同学就当做是复习了。
想要变强,学会看源码是必经之路。在这过程中,不仅能学习框架的设计思想,还能培养自己的逻辑思维。万事开头难,迟早都要迈出这一步,不如就从今天开始。
简化后的代码我已放在 github,有需要的可以看看。
手摸手带你理解Vue响应式原理的更多相关文章
-
手摸手带你理解Vue的Computed原理
前言 computed 在 Vue 中是很常用的属性配置,它能够随着依赖属性的变化而变化,为我们带来很大便利.那么本文就来带大家全面理解 computed 的内部原理以及工作流程. 在这之前,希望你能 ...
-
手摸手带你理解Vue的Watch原理
前言 watch 是由用户定义的数据监听,当监听的属性发生改变就会触发回调,这项配置在业务中是很常用.在面试时,也是必问知识点,一般会用作和 computed 进行比较. 那么本文就来带大家从源码理解 ...
-
详解Vue响应式原理
摘要: 搞懂Vue响应式原理! 作者:浪里行舟 原文:深入浅出Vue响应式原理 Fundebug经授权转载,版权归原作者所有. 前言 Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是 ...
-
Vue源码--解读vue响应式原理
原文链接:https://geniuspeng.github.io/2018/01/05/vue-reactivity/ Vue的官方说明里有深入响应式原理这一节.在此官方也提到过: 当你把一个普通的 ...
-
深入Vue响应式原理
深入Vue.js响应式原理 一.创建一个Vue应用 new Vue({ data() { return { name: 'yjh', }; }, router, store, render: h =& ...
-
深入解析vue响应式原理
摘要:本文主要通过结合vue官方文档及源码,对vue响应式原理进行深入分析. 1.定义 作为vue最独特的特性,响应式可以说是vue的灵魂了,表面上看就是数据发生变化后,对应的界面会重新渲染,那么响应 ...
-
深度解析 Vue 响应式原理
深度解析 Vue 响应式原理 该文章内容节选自团队的开源项目 InterviewMap.项目目前内容包含了 JS.网络.浏览器相关.性能优化.安全.框架.Git.数据结构.算法等内容,无论是基础还是进 ...
-
vue响应式原理,去掉优化,只看核心
Vue响应式原理 作为写业务的码农,几乎不必知道原理.但是当你去找工作的时候,可是需要造原子弹的,什么都得知道一些才行.所以找工作之前可以先复习下,只要是关于vue的,必定会问响应式原理. 核心: / ...
-
vue响应式原理解析
# Vue响应式原理解析 首先定义了四个核心的js文件 - 1. observer.js 观察者函数,用来设置data的get和set函数,并且把watcher存放在dep中 - 2. watcher ...
随机推荐
-
Python黑帽编程 4.1 Sniffer(嗅探器)之数据捕获(上)
Python黑帽编程 4.1 Sniffer(嗅探器)之数据捕获(上) 网络嗅探,是监听流经本机网卡数据包的一种技术,嗅探器就是利用这种技术进行数据捕获和分析的软件. 编写嗅探器,捕获数据是前置功能, ...
-
ComboboxColumn取值——Winform中DataGridView中某一列使用下拉框
ComboboxColumn的用法网上很多,绑定数据源都很简单,这里我遇到的是.不绑定数据源,即所有comobox的绑定都是固定的几个数据: 可以看到没有绑定任何数据源, ,在后台cs中取到下拉框的值 ...
-
ASP.NET MVC案例——————拦截器
摘要 本文将对“MVC公告发布系统”的发布公告功能添加日志功能和异常处理功能,借此来讨论ASP.NET MVC中拦截器的使用方法. 一个小难题 我们继续完善“MVC公告发布系统”, ...
-
使用Obsolete特性来标记方法过时或弃用
我们在维护一些老的系统的时候,经常会遇到某个方法不再使用的情况,我们又不能直接将其删除,因为系统中可能还有很多地方有引用它,所以比较安全保险的做法是,使用Obsolete特性来标记它过时或弃用.如下代 ...
-
五大Android布局方式浅析
Android布局是应用界面开发的重要一环,在Android中,共有五种布局方式,分别是:FrameLayout(框架布局),LinearLayout (线性布局),AbsoluteLayout(绝对 ...
-
hdoj 1233 还是畅通工程(最小生成树)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1233 思路分析:该问题为最小生成树问题,使用kruskal算法或者prim算法即可解决: 代码如下: ...
-
基于visual Studio2013解决C语言竞赛题之1010计算
题目 解决代码及点评 /************************************************************************/ ...
-
Centos 6.5升级到Git2.1.2的步骤
Centos 6.5升级到Git2.1.2的步骤 Centos 6.5升级到Git2.1.2其实是非常的简单,因这款版本控制程序非常的好用,所以小编自己也是使用它了,下面一起来看看Centos 6.5 ...
-
Swift Review总结:从 Swift Style 开始
每个语言都有自己的推荐风格.显然OC与Swift有着不同的风格.当我们开始写Swift,首先要注意的就是按照Swift的风格写,而不是沿用OC的风格. 省略句末的分号 swift推崇简洁的语法.如果一 ...
-
blast及其格式输出简介
1)blast产生背景 双序列比对可以采用是基于动态规划算法的Needleman-Wunsch(NW)和Smith-Waterman algorithm(SW)算法,虽然精度高,但计算消耗大.当与数据 ...