学习每一门语言,一般都是从其数据结构开始,JavaScript也是一样,而JavaScript的数据结构中对象(Object)是最基础也是使用最频繁的概念和语法,坊间有言,JavaScript中,一切皆对象,基本可以描述对象在JavaScript中的地位,而且JavaScript中对象的强大也使其地位名副其实,本篇介绍JavaScript对象属性描述器接口及其在数据视图绑定方向的实践,然后对Vue.js的响应式原理进行剖析。

前言

JavaScript的对象,是一组键值对的集合,可以拥有任意数量的唯一键,键可以是字符串(String)类型或标记(Symbol,ES6新增的基本数据类型)类型,每个键对应一个值,值可以是任意类型的任意值。对于对象内的属性,JavaScript提供了一个属性描述器接口PropertyDescriptor,大部分开发者并不需要直接使用它,但是很多框架和类库内部实现使用了它,如avalon.js,Vue.js,本篇介绍属性描述器及相关应用。

定义对象属性

在介绍对象属性描述之前,先介绍一下如何定义对象属性。最常用的方式就是使用如下方式:

var a = {name: 'jh'
};// or
var b = {};
b.name = 'jh';// or
var c = {};
var key = 'name';
c[key] = 'jh';

本文使用字面量方式创建对象,但是JavaScript还提供其他方式,如,new Object()Object.create(),了解更多请查看对象初始化。

Object.defineProperty()

上面通常使用的方式不能实现对属性描述器的操作,我们需要使用defineProperty()方法,该方法为一个对象定义新属性或修改一个已定义属性,接受三个参数Object.defineProperty(obj, prop, descriptor),返回值为操作后的对象:

  • obj, 待操作对象
  • 属性名
  • 操作属性的属性描述对象

例如:

var x = {}
Object.defineProperty(x, 'count', {})
console.log(x)

由于传入一个空的属性描述对象,所以输出对象属性值为undefined,当使用defineProperty()方法操作属性时,描述对象默认值为:

  • valueundefined
  • setundefined
  • getundefined
  • writablefalse
  • enumerablefalse
  • configurablefalse

不使用该方法定义属性,则属性默认描述为:

  • valueundefined
  • setundefined
  • getundefined
  • writabletrue
  • enumerabletrue
  • configurabletrue

默认值均可被明确参数值设置覆盖。

当然还支持批量定义对象属性及描述对象,使用Object.defineProperties()方法,如:

var x = {}Object.defineProperties(x, {count: {value: 0},name: {value: 'jh'}
})console.log(x)

读取属性描述对象

JavaScript支持我们读取某对象属性的描述对象,使用Object.getOwnPropertyDescriptor(obj, prop)方法:

var x = {name: 'jh'
}Object.defineProperty(x, 'count', {})Object.getOwnPropertyDescriptor(x, 'count')Object.getOwnPropertyDescriptor(x, 'name')

该实例也印证了上面介绍的以不同方式定义属性时,其默认属性描述对象是不同的。

属性描述对象

PropertyDescriptor API提供了六大实例属性以描述对象属性,包括:configurableenumerablegetsetvaluewritable

value

指定对象属性值:

var x = {}Object.defineProperty(x, 'count', {value: 0
})console.log(x)

writable

指定对象属性是否可变:

var x = {}Object.defineProperty(x, 'count', {value: 0
})console.log(x)x.count = 1 // 静默失败,不会报错console.log(x)

使用defineProperty()方法时,默认有writable: false, 需要显示设置writable: true

存取器函数(getter/setter)

对象属性可以设置存取器函数,使用get声明存取器getter函数,set声明存取器setter函数;若存在存取器函数,则在访问或设置该属性时,将调用对应的存取器函数:

get

读取该属性值时调用该函数并将该函数返回值赋值给属性值:

var x = {}Object.defineProperty(x, 'count', {get: function () {console.log('读取count属性 +1')return 0;}
})console.log(x)x.count = 1console.log(x.count)

set

当设置函数值时调用该函数,该函数接收设置的属性值作参数:

var x = {}Object.defineProperty(x, 'count', {set: function (val) {this.count = val}
})console.log(x)x.count = 1console.log(x.count)

执行上面的代码,会发现报错,执行栈溢出:

上述代码在设置count属性时,会调用set方法,而在该方法内为count属性赋值会再次触发set方法,所以这样是行不通的,JavaScript使用另一种方式,通常存取器函数得同时声明,代码如下:

var x = {}Object.defineProperty(x, 'count', {get: function () {return this._count},set: function (val) {console.log('设置count属性 +1')this._count = val}
})console.log(x)x.count = 1// 设置count属性 +1
console.log(x.count)

事实上,在使用defineProperty()方法设置属性时,通常需要在对象内部维护一个新内部变量(以下划线_开头,表示不希望被外部访问),作为存取器函数的中介。

注:当设置了存取器描述时,不能设置valuewritable描述。

我们发现,设置属性存取器函数后,我们可以实现对该属性的实时监控,这在实践中很有用武之地,后文会印证这一点。

enumerable

指定对象内某属性是否可枚举,即使用for in操作是否可遍历:

var x = {name: 'jh'
}Object.defineProperty(x, 'count', {value: 0
})for (var key in x) {console.log(`${key} is ${x[key]}`) // => name is jh
}

上面无法遍历count属性,因为使用defineProperty()方法时,默认有enumerable: false,需要显示声明该描述:

var x = {name: 'jh'
}Object.defineProperty(x, 'count', {value: 0,enumerable: true
})console.log(x)for (var key in x) {console.log(`${key} is ${x[key]}`)
}x.propertyIsEnumerable('count')

configurable

该值指定对象属性描述是否可变:

var x = {}Object.defineProperty(x, 'count', {value: 0,writable: false
})Object.defineProperty(x, 'count', {value: 0,writable: true
})

执行上述代码会报错,因为使用defineProperty()方法时默认是configurable: false,输出如图:

修改如下,即可:

var x = {}Object.defineProperty(x, 'count', {value: 0,writable: false,configurable: true
})x.count = 1console.log(x.count)Object.defineProperty(x, 'count', {writable: true
})x.count = 1console.log(x.count)

属性描述与视图模型绑定

介绍完属性描述对象,我们来看看其在现代JavaScript框架和类库上的应用。目前有很多框架和类库实现数据和DOM视图的单向甚至双向绑定,如React,Angular.js,Avalon.js,Vue.js等,使用它们很容易做到对数据变更进行响应式更新DOM视图,甚至视图和模型可以实现双向绑定,同步更新。当然这些框架、类库内部实现原理主要分为三大阵营。本文以Vue.js为例,Vue.js是当下比较流行的一个响应式的视图层类库,其内部实现响应式原理就是本文介绍的属性描述在技术中的具体应用。

可以点击此处,查看一个原生JavaScript实现的简易数据视图单向绑定实例,在该实例中,点击按钮可以实现计数自增,在输入框输入内容会同步更新到展示DOM,甚至在控制台改变data对象属性值,DOM会响应更新,如图:

点击查看完整实例代码。

数据视图单向绑定

现有如下代码:

var data = {}
var contentEl = document.querySelector('.content')Object.defineProperty(data, 'text', {writable: true,configurable: true,enumerable: true,get: function () {return contentEl.innerHTML},set: function (val) {contentEl.innerHTML = val}
})

很容易看出,当我们设置data对象的text属性时,会将该值设置为视图DOM元素的内容,而访问该属性值时,返回的是视图DOM元素的内容,这就简单的实现了数据到视图的单向绑定,即数据变更,视图也会更新。

以上仅是针对一个元素的数据视图绑定,但稍微有经验的开发者便可以根据以上思路,进行封装,很容易的实现一个简易的数据到视图单向绑定的工具类。

抽象封装

接下来对以上实例进行简单抽象封装,点击查看完整实例代码。

首先声明数据结构:

window.data = {title: '数据视图单向绑定',content: '使用属性描述器实现数据视图绑定',count: 0
}var attr = 'data-on' // => 约定好的语法,声明DOM绑定对象属性

然后封装函数批量处理对象,遍历对象属性,设置描述对象同时为属性注册变更时的回调:

// 为对象中每一个属性设置描述对象,尤其是存取器函数
function defineDescriptors (obj) {for (var key in obj) {// 遍历属性defineDescriptor(obj, key, obj[key])}// 为特定属性设置描述对象function defineDescriptor (obj, key, val) {Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function () {var value = valreturn value},set: function (newVal) {if (newVal !== val) {// 值发生变更才执行val = newValObserver.emit(key, newVal) // 触发更新DOM}}})Observer.subscribe(key) // 为该属性注册回调}
}

管理事件

以发布订阅模式管理属性变更事件及回调:

// 使用发布/订阅模式,集中管理监控和触发回调事件
var Observer = {watchers: {},subscribe: function (key) {var el = document.querySelector('[' + attr + '="' + key + '"]')// Demovar cb = function react (val) {el.innerHTML = val}if (this.watchers[key]) {this.watchers[key].push(cb)} else {this.watchers[key] = [].concat(cb)}},emit: function (key, val) {var len = this.watchers[key] && this.watchers[key].lengthif (len && len > 0) {for (var i = 0; i < len; i++) {this.watchers[key][i](val)}}}
}

初始化实例

最后初始化实例:

// 初始化DEMO
function init() {defineDescriptors(data); // 处理数据对象var eles = document.querySelectorAll('[' + attr +']')// 初始遍历 DOM 展示数据// 其实可以将该操作放到属性描述对象的get方法内// 则在初始化时只需要对属性遍历访问即可for (var i = 0, len = eles.length; i < len; i++) {eles[i].innerHTML = data[eles[i].getAttribute(attr)]}// 辅助测试实例document.querySelector('.add').addEventListener('click', function (e) {data.count += 1})
}init()

HTML代码参考如下:

<div id="app"><h2 class="title" data-on="title"></h2><div class="content" data-on="content"></div><div class="count" data-on="count"></div><div class="control"><label for="content-input">请输入内容:</label><input type="text" class="content-input" placeholder="请输入内容" id="content-input" /></div><button class="add" onclick="">加1</button>
</div>

Vue.js的响应式原理

上一节实现了一个简单的数据视图单向绑定实例,现在对Vue.js的响应式单向绑定进行简要分析,主要需要理解其如何追踪数据变更。

依赖追踪

Vue.js支持我们通过data参数传递一个JavaScript对象做为组件数据,然后Vue.js将遍历此对象属性,使用Object.defineProperty方法设置描述对象,通过存取器函数可以追踪该属性的变更,本质原理和上一节实例差不多,但是不同的是,Vue.js创建了一层Watcher层,在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知Watcher重新计算,从而使它关联的组件得以更新,如下图:

组件挂载时,实例化watcher实例,并把该实例传递给依赖管理类,组件渲染时,使用对象观察接口遍历传入的data对象,为每个属性创建一个依赖管理实例并设置属性描述对象,在存取器函数get函数中,依赖管理实例添加(记录)该属性为一个依赖,然后当该依赖变更时,触发set函数,在该函数内通知依赖管理实例,依赖管理实例分发该变更给其内存储的所有watcher实例,watcher实例重新计算,更新组件。

因此可以总结说Vue.js的响应式原理是依赖追踪,通过一个观察对象,为每个属性,设置存取器函数并注册一个依赖管理实例depdep内为每个组件实例维护一个watcher实例,在属性变更时,通过setter通知dep实例,dep实例分发该变更给每一个watcher实例,watcher实例各自计算更新组件实例,即watcher追踪dep添加的依赖,Object.defineProperty()方法提供这种追踪的技术支持,dep实例维护这种追踪关系。

源码简单分析

接下来对Vue.js源码进行简单分析,从对JavaScript对象和属性的处理开始:

观察对象(Observer)

首先,Vue.js也提供了一个抽象接口观察对象,为对象属性设置存储器函数,收集属性依赖然后分发依赖更新:

var Observer = function Observer (value) {this.value = value;this.dep = new Dep();       // 管理对象依赖this.vmCount = 0;def(value, '__ob__', this); // 缓存处理的对象,标记该对象已处理if (Array.isArray(value)) {var augment = hasProto? protoAugment: copyAugment;augment(value, arrayMethods, arrayKeys);this.observeArray(value);} else {this.walk(value);}
};

上面代码关注两个节点,this.observeArray(value)this.walk(value)

若为对象,则调用walk()方法,遍历该对象属性,将属性转换为响应式:

Observer.prototype.walk = function walk (obj) {var keys = Object.keys(obj);for (var i = 0; i < keys.length; i++) {defineReactive$$1(obj, keys[i], obj[keys[i]]);}
};

可以看到,最终设置属性描述对象是通过调用defineReactive$$1()方法。

value为对象数组,则需要额外处理,调用observeArray()方法对每一个对象均产生一个Observer实例,遍历监听该对象属性:

Observer.prototype.observeArray = function observeArray (items) {for (var i = 0, l = items.length; i < l; i++) {observe(items[i]);}
};

核心是为每个数组项调用observe函数:

function observe(value, asRootData) {if (!isObject(value)) {return // 只需要处理对象}var ob;if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {ob = value.__ob__; // 处理过的则直接读取缓存} else if (observerState.shouldConvert &&!isServerRendering() &&(Array.isArray(value) || isPlainObject(value)) &&Object.isExtensible(value) &&!value._isVue) {ob = new Observer(value); // 处理该对象}if (asRootData && ob) {ob.vmCount++;}return ob
}

调用ob = new Observer(value);后就回到第一种情况的结果:调用defineReactive$$1()方法生成响应式属性。

生成响应式属性

源码如下:

function defineReactive$$1 (obj,key,val,customSetter) {var dep = new Dep(); // 管理属性依赖var property = Object.getOwnPropertyDescriptor(obj, key);if (property && property.configurable === false) {return}// 之前已经设置了的get/set需要合并调用var getter = property && property.get; var setter = property && property.set;var childOb = observe(val); // 属性值也可能是对象,需要递归观察处理Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () {var value = getter ? getter.call(obj) : val;if (Dep.target) { // 管理依赖对象存在指向的watcher实例dep.depend(); // 添加依赖(记录)if (childOb) { // 属性值为对象childOb.dep.depend(); // 属性值对象也需要添加依赖}if (Array.isArray(value)) {dependArray(value); // 处理数组}}return value},set: function reactiveSetter (newVal) {var 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 ("development" !== 'production' && customSetter) {customSetter();}if (setter) {setter.call(obj, newVal); // 更新属性值} else {val = newVal; // 更新属性值}childOb = observe(newVal); // 每次值变更时需要重新观察,因为可能值为对象dep.notify(); // 发布更新事件}});
}

该方法使用Object.defineProperty()方法设置属性描述对象,逻辑集中在属性存取器函数内:

  • get: 返回属性值,如果watcher存在,则递归记录依赖;
  • set: 属性值发生变更时,更新属性值,并调用dep.notify()方法发布更新事件;

管理依赖

Vue.js需要管理对象的依赖,在属性更新时通知watcher更新组件,进而更新视图,Vue.js管理依赖接口采用发布订阅模式实现,源码如下:

var uid$1 = 0;
var Dep = function Dep () {this.id = uid$1++; // 依赖管理实例idthis.subs = []; // 订阅该依赖管理实例的watcher实例数组
};
Dep.prototype.depend = function depend () { // 添加依赖if (Dep.target) {Dep.target.addDep(this); // 调用watcher实例方法订阅此依赖管理实例}
};
Dep.target = null; // watcher实例
var targetStack = []; // 维护watcher实例栈function pushTarget (_target) {if (Dep.target) { targetStack.push(Dep.target); }Dep.target = _target; // 初始化Dep指向的watcher实例
}function popTarget () {Dep.target = targetStack.pop();
}

订阅

如之前,生成响应式属性为属性设置存取器函数时,get函数内调用dep.depend()方法添加依赖,该方法内调用Dep.target.addDep(this),即调用指向的watcher实例的addDep方法,订阅此依赖管理实例:

Watcher.prototype.addDep = function addDep (dep) {var id = dep.id;if (!this.newDepIds.has(id)) { // 是否已订阅this.newDepIds.add(id); // watcher实例维护的依赖管理实例id集合this.newDeps.push(dep); // watcher实例维护的依赖管理实例数组if (!this.depIds.has(id)) { // watcher实例维护的依赖管理实例id集合// 调用传递过来的依赖管理实例方法,添加此watcher实例为订阅者dep.addSub(this); }}
};

watcher实例可能同时追踪多个属性(即订阅多个依赖管理实例),所以需要维护一个数组,存储多个订阅的依赖管理实例,同时记录每一个实例的id,便于判断是否已订阅,而后调用依赖管理实例的addSub方法:

Dep.prototype.addSub = function addSub (sub) {this.subs.push(sub); // 实现watcher到依赖管理实例的订阅关系
};

该方法只是简单的在订阅数组内添加一个订阅该依赖管理实例的watcher实例。

发布

属性变更时,在属性的存取器set函数内调用了dep.notify()方法,发布此属性变更:

Dep.prototype.notify = function notify () {// 复制订阅者数组var subs = this.subs.slice();for (var i = 0, l = subs.length; i < l; i++) {subs[i].update(); // 分发变更}
};

触发更新

前面提到,Vue.js中由watcher层追踪依赖变更,发生变更时,通知组件更新:

Watcher.prototype.update = function update () {/* istanbul ignore else */if (this.lazy) {this.dirty = true;} else if (this.sync) { // 同步this.run();} else { // 异步queueWatcher(this); // 最后也是调用run()方法}
};

调用run方法,通知组件更新:

Watcher.prototype.run = function run () {if (this.active) {var value = this.get();         // 获取新属性值if (value !== this.value ||     // 若值isObject(value) || this.deep) {var 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);}}}
};

调用this.get()方法,实际上,后面会看到在该方法内处理了属性值的更新与组件的更新,这里判断当属性变更时调用初始化时传给实例的cb回调函数,并且回调函数接受属性新旧值两个参数,此回调通常是对于watch声明的监听属性才会存在,否则默认为空函数。

追踪依赖接口实例化

每一个响应式属性都是由一个Watcher实例追踪其变更,而针对不同属性(datacomputedwatch),Vue.js进行了一些差异处理,如下是接口主要逻辑:

var Watcher = function Watcher (vm,expOrFn,cb,options) {this.cb = cb;...// parse expression for getterif (typeof expOrFn === 'function') {this.getter = expOrFn;} else {this.getter = parsePath(expOrFn);}this.value = this.lazy? undefined: this.get();
};

在初始化Watcher实例时,会解析expOrFn参数(表达式或者函数)成拓展getterthis.getter,然后调用this.get()方法,返回值作为this.value值:

Watcher.prototype.get = function get () {pushTarget(this); // 入栈watcher实例var value;var vm = this.vm;if (this.user) {try {value = this.getter.call(vm, vm); // 通过this.getter获取新值} catch (e) {handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));}} else {value = this.getter.call(vm, vm); // 通过this.getter获取新值}if (this.deep) { // 深度递归遍历对象追踪依赖traverse(value);}popTarget(); // 出栈watcher实例this.cleanupDeps(); // 清空缓存依赖return value // 返回新值
};

这里需要注意的是对于data属性,而非computed属性或watch属性,而言,其watcher实例的this.getter通常就是updateComponent函数,即渲染更新组件,get方法返回undefined,而对于computed计算属性而言,会传入对应指定函数给this.getter,其返回值就是此get方法返回值。

data普通属性

Vue.js的data属性是一个对象,需要调用对象观察接口new Observer(value)

function observe (value, asRootData) {if (!isObject(value)) {return}var ob;ob = new Observer(value); // 对象观察实例return ob;
}// 初始处理data属性
function initData (vm) {// 调用observe函数observe(data, true /* asRootData */);
}

计算属性

Vue.js对计算属性处理是有差异的,它是一个变量,可以直接调用Watcher接口,把其属性指定的计算规则传递为,属性的拓展getter,即:

// 初始处理computed计算属性
function initComputed (vm, computed) {for (var key in computed) {var userDef = computed[key]; // 对应的计算规则// 传递给watcher实例的this.getter -- 拓展gettervar getter = typeof userDef === 'function' ? userDef : userDef.get; watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions);}
}

watch属性

而对于watch属性又有不同,该属性是变量或表达式,而且与计算属性不同的是,它需要指定一个变更事件发生后的回调函数:

function initWatch (vm, watch) {for (var key in watch) {var handler = watch[key];createWatcher(vm, key, handler[i]); // 传递回调}
}function createWatcher (vm, key, handler) {vm.$watch(key, handler, options); // 回调
}Vue.prototype.$watch = function (expOrFn, cb, options) {// 实例化watcher,并传递回调var watcher = new Watcher(vm, expOrFn, cb, options);
}

初始化Watcher与依赖管理接口的连接

无论哪种属性最后都是由watcher接口实现追踪依赖,而且组件在挂载时,即会初始化一次Watcher实例,绑定到Dep.target,也就是将WatcherDep建立连接,如此在组件渲染时才能对属性依赖进行追踪:

function mountComponent (vm, el, hydrating) {...updateComponent = function () {vm._update(vm._render(), hydrating);...};...vm._watcher = new Watcher(vm, updateComponent, noop);...
}

如上,传递updateComponent方法给watcher实例,该方法内触发组件实例的vm._render()渲染方法,触发组件更新,此mountComponent()方法会在$mount()挂载组件公开方法中调用:

// public mount method
Vue$3.prototype.$mount = function (el, hydrating) {el = el && inBrowser ? query(el) : undefined;return mountComponent(this, el, hydrating)
};

总结

到此为止,对于JavaScript属性描述器接口的介绍及其应用,还有其在Vue.js中的响应式实践原理基本阐述完了,这次总结从原理到应用,再到实践剖析,花费比较多精力,但是收获是成正比的,不仅对JavaScript基础有更深的理解,还更熟悉了Vue.js响应式的设计原理,对其源码熟悉度也有较大提升,之后在工作和学习过程中,会进行更多的总结分享。

参考

  • Object.defineProperty
  • Vue.js Reactivity in Depth
  • Object Initializer

如需转载,烦请注明出处:https://www.w3cplus.com/vue/reactive.html

转自https://www.w3cplus.com/vue/reactive.html

从JavaScript属性描述器剖析Vue.js响应式视图相关推荐

  1. 从 JavaScript 属性描述器剖析 Vue.js 响应式视图

    学习每一门语言,一般都是从其数据结构开始,JavaScript也是一样,而JavaScript的数据结构中对象(Object)是最基础也是使用最频繁的概念和语法,坊间有言,JavaScript中,一切 ...

  2. vue.js响应式原理解析与实现

    从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页面更新渲染.之后,再接触了vue.js,当时也一度很好奇vue.js是如何监测数据更新 ...

  3. Vue.js响应式原理

    Vue.js响应式原理 框架 浏览数:659 2017-9-20 关于Vue.js Vue.js是一款MVVM框架,上手快速简单易用,通过响应式在修改数据的时候更新视图.Vue.js的响应式原理依赖于 ...

  4. Vue.js 响应式原理

    文章目录 Vue 数据响应式原理 `Object.defineProperty()` 数据响应式原理 `Proxy` 相关设计模式 观察者模式 发布-订阅模式 Vue 响应式原理模拟 Vue 类 Ob ...

  5. java反射--PropertyDescriptor类:(属性描述器)、Introspector类

    JAVA中反射机制(JavaBean的内省与BeanUtils库) 内省(Introspector) 是Java 语言对JavaBean类属性.事件的一种缺省处理方法. JavaBean是一种特殊的类 ...

  6. 解决2020版IDEA的JAVAScript中找不到vue.js问题

    解决2020版IDEA的JAVAScript中找不到vue.js问题 1.安装插件 打开IDEA的界面如下 步骤:(1)file-->setting-->plugins ​ (2)在搜索框 ...

  7. 基于vue的响应式ui框架_基于Vue.js的响应式和可配置UI框架

    基于vue的响应式ui框架 Framevuerk (Framevuerk) Fast, Responsive, Multi Language, Both Direction Support and C ...

  8. 利用JS响应式修改vue实现页面的input赋值

    背景 公司有一个基于Vue实现的登录,页面上是一个常规的登录界面,用户名输入框.密码输入框和登录按钮各一个. 需求:拉起登录页面时,自动将用户帐号和密码填入,然后自动点击登录. 尝试 我们把登录页面简 ...

  9. 彻底理解Vue数据响应式原理

    彻底理解Vue数据响应式原理 当创建了一个实例后,通过将数据传入实例的data属性中,会将数据进行劫持,实现响应式,也就是说当这些数据发生变化时,对应的视图也会发生改变. const data = { ...

最新文章

  1. 【组队学习】【29期】4. 吃瓜教程——西瓜书+南瓜书
  2. 学计算机方面该怎样保养眼睛,电脑工作者如何保护眼睛?吃什么对眼睛好
  3. 计算机网络总结:第三章 运输层
  4. C++ 对引用的理解4
  5. 近些年deep learning在推荐系统的应用
  6. u盘修复计算机系统,用u盘修复win7系统
  7. python pip卸载
  8. uva 10246(最短路变形)
  9. 淡泊明志, 宁静致远--如何在自学之路上爬得更高更远?
  10. 分布式事务中的时间戳详解
  11. css设置格子背景,跟本子一样
  12. 利用python处理pdf文本_Python用于NLP :处理文本和PDF文件
  13. Centos 下使用speedtest-cli 进行测速
  14. 图片不能置于底层怎么办_word图片为什么不能置于底层
  15. uniapp 小程序 easycom 配置 找不到组件 失效问题
  16. PCL点云库必备知识点4——pointcloud2消息格式的转换
  17. Python统计某一只股票每天的开盘,收盘,最高,最低价格!
  18. python学习:爬取房源信息
  19. bugku练习Web 1(web2--成绩单)
  20. 多电脑共享键鼠,传输数据和剪贴板的“黑科技”,在座肯定幻想过

热门文章

  1. centos 修改host
  2. 【数字IC验证快速入门】1、浅谈数字IC验证,了解专栏内容,明确学习目标
  3. 在某乎上回答了一个问题
  4. 使用手机号查询物流信息
  5. 程序设计与C语言引论笔记——字符数组与字符串
  6. 其他应收款的贷方是什么意思?借方又表示什么意思?
  7. 瀑布流(waterfall flow)的原理以及实现
  8. python 特效字_Python的文字特效,炫酷了!
  9. 阿里巴巴开源项目 -- Druid
  10. Sansen精粹阅读笔记(2) CMFB 共模反馈