学习每一门语言,一般都是从其数据结构开始,JavaScript 也是一样,而 JavaScript 的数据结构中对象(Object)是最基础也是使用最频繁的概念和语法,坊间有言,JavaScript 中,一切皆对象,基本可以描述对象在 JavaScript 中的地位,而且 JavaScript 中对象的强大也使其地位名副其实,本篇介绍 JavaScript 对象属性描述器接口及其在数据视图绑定方向的实践,然后对 Vue.js 的响应式原理进行剖析。
可以先看一个应用实例, 点击此处
索引
JavaScript 的对象,是一组键值对的集合,可以拥有任意数量的唯一键,键可以是字符串(String)类型或标记(Symbol,ES6 新增的基本数据类型)类型,每个键对应一个值,值可以是任意类型的任意值。对于对象内的属性,JavaScript 提供了一个属性描述器接口
,大部分开发者并不需要直接使用它,但是很多框架和类库内部实现使用了它,如 avalon.js,Vue.js,本篇介绍属性描述器及相关应用。
- PropertyDescriptor
在介绍对象属性描述之前,先介绍一下如何定义对象属性。最常用的方式就是使用如下方式:
- var a = {
- name: 'jh'
- };
- // or
- var b = {};
- b.name = 'jh';
- // or
- var c = {};
- var key = 'name';
- c[key] = 'jh';
本文使用字面量方式创建对象,但是 JavaScript 还提供其他方式,如,new Object(),Object.create(),了解更多请查看 对象初始化 。
上面通常使用的方式不能实现对属性描述器的操作,我们需要使用
方法,该方法为一个对象定义新属性或修改一个已定义属性,接受三个参数
- defineProperty()
,返回值为操作后的对象:
- Object.defineProperty(obj, prop, descriptor)
- var x = {};
- Object.defineProperty(x, 'count', {});
- console.log(x); // Object {count: undefined}
由于传入一个空的属性描述对象,所以输出对象属性值为 undefined,当使用
方法操作属性时,描述对象默认值为:
- defineProperty()
不使用该方法定义属性,则属性默认描述为:
默认值均可被明确参数值设置覆盖。
当然还支持批量定义对象属性及描述对象,使用 "Object.defineProperties()` 方法,如:
- var x = {};
- Object.defineProperties(x, {
- count: {
- value: 0
- },
- name: {
- value: 'jh'
- }
- });
- console.log(x); // Object {count: 0, name: 'jh'}
JavaScript 支持我们读取某对象属性的描述对象,使用
方法:
- Object.getOwnPropertyDescriptor(obj, prop)
- var x = {
- name: 'jh'
- };
- Object.defineProperty(x, 'count', {});
- Object.getOwnPropertyDescriptor(x, 'count');
- Object.getOwnPropertyDescriptor(x, 'name');
- // Object {value: undefined, writable: false, enumerable: false, configurable: false}
- // Object {value: "jh", writable: true, enumerable: true, configurable: true}
该实例也印证了上面介绍的以不同方式定义属性时,其默认属性描述对象是不同的。
API 提供了六大实例属性以描述对象属性,包括:configurable, enumerable, get, set, value, writable.
- PropertyDescriptor
指定对象属性值:
- var x = {};
- Object.defineProperty(x, 'count', {
- value: 0
- });
- console.log(x); // Object {count: 0}
指定对象属性是否可变:
- var x = {};
- Object.defineProperty(x, 'count', {
- value: 0
- });
- console.log(x); // Object {count: 0}
- x.count = 1; // 静默失败,不会报错
- console.log(x); // Object {count: 0}
使用
方法时,默认有
- defineProperty()
, 需要显示设置
- writable: false
。
- writable: true
对象属性可以设置存取器函数,使用
声明存取器 getter 函数,
- get
声明存取器 setter 函数;若存在存取器函数,则在访问或设置该属性时,将调用对应的存取器函数:
- set
读取该属性值时调用该函数并将该函数返回值赋值给属性值;
- var x = {};
- Object.defineProperty(x, 'count', {
- get: function() {
- console.log('读取count属性 +1');
- return 0;
- }
- });
- console.log(x); // Object {count: 0}
- x.count = 1;
- // '读取count属性 +1'
- console.log(x.count); // 0
当设置函数值时调用该函数,该函数接收设置的属性值作参数:
- var x = {};
- Object.defineProperty(x, 'count', {
- set: function(val) {
- this.count = val;
- }
- });
- console.log(x);
- x.count = 1;
执行上诉代码,会发现报错,执行栈溢出:
上述代码在设置
属性时,会调用
- count
方法,而在该方法内为
- set
属性赋值会再次触发
- count
方法,所以这样是行不通的,JavaScript 使用另一种方式,通常存取器函数得同时声明,代码如下:
- set
- var x = {};
- Object.defineProperty(x, 'count', {
- get: function() {
- return this._count;
- },
- set: function(val) {
- console.log('设置count属性 +1');
- this._count = val;
- }
- });
- console.log(x); // Object {count: undefined}
- x.count = 1;
- // '设置count属性 +1'
- console.log(x.count);
- 1
事实上,在使用
方法设置属性时,通常需要在对象内部维护一个新内部变量(以下划线
- defineProperty()
开头,表示不希望被外部访问),作为存取器函数的中介。
- _
注:当设置了存取器描述时,不能设置
和
- value
描述。
- writable
我们发现,设置属性存取器函数后,我们可以实现对该属性的实时监控,这在实践中很有用武之地,后文会印证这一点。
指定对象内某属性是否可枚举,即使用
操作是否可遍历:
- 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
- });
- for (var key in x) {
- console.log(key + ' is ' + x[key]);
- }
- // name is jh
- // count is 0
- x.propertyIsEnumerable('count'); // true
该值指定对象属性描述是否可变:
- 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 = 1;
- console.log(x.count); // 0
- Object.defineProperty(x, 'count', {
- writable: true
- });
- x.count = 1;
- console.log(x.count); // 1
介绍完属性描述对象,我们来看看其在现代 JavaScript 框架和类库上的应用。目前有很多框架和类库实现数据和 DOM 视图的单向甚至双向绑定,如 React,angular.js,avalon.js,,Vue.js 等,使用它们很容易做到对数据变更进行响应式更新 DOM 视图,甚至视图和模型可以实现双向绑定,同步更新。当然这些框架、类库内部实现原理主要分为三大阵营。本文以 Vue.js 为例,Vue.js 是当下比较流行的一个响应式的视图层类库,其内部实现响应式原理就是本文介绍的属性描述在技术中的具体应用。
可以点击此处,查看一个原生 JavaScript 实现的简易数据视图单向绑定实例,在该实例中,点击按钮可以实现计数自增,在输入框输入内容会同步更新到展示 DOM,甚至在控制台改变
对象属性值,DOM 会响应更新,如图:
- data
点击查看完整实例代码 。
现有如下代码:
- 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 对象的
属性时,会将该值设置为视图 DOM 元素的内容,而访问该属性值时,返回的是视图 DOM 元素的内容,这就简单的实现了数据到视图的单向绑定,即数据变更,视图也会更新。
- text
以上仅是针对一个元素的数据视图绑定,但稍微有经验的开发者便可以根据以上思路,进行封装,很容易的实现一个简易的数据到视图单向绑定的工具类。
接下来对以上实例进行简单抽象封装, 点击查看完整实例代码 。
首先声明数据结构:
- 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 = val;
- return value;
- },
- set: function(newVal) {
- if (newVal !== val) {
- // 值发生变更才执行
- val = newVal;
- Observer.emit(key, newVal); // 触发更新DOM
- }
- }
- });
- Observer.subscribe(key); // 为该属性注册回调
- }
- }
以发布订阅模式管理属性变更事件及回调:
- // 使用发布/订阅模式,集中管理监控和触发回调事件
- var Observer = {
- watchers: {},
- subscribe: function(key) {
- var el = document.querySelector('[' + attr + '="'+ key + '"]');
- // demo
- var 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].length;
- if (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 代码参考如下:
- <h2 class="title" data-on="title">
- </h2>
- <div class="content" data-on="content">
- </div>
- <div class="count" data-on="count">
- </div>
- <div>
- 请输入内容:
- <input type="text" class="content-input" placeholder="请输入内容">
- </div>
- <button class="add" onclick="">
- 加1
- </button>
上一节实现了一个简单的数据视图单向绑定实例,现在对 Vue.js 的响应式单向绑定进行简要分析,主要需要理解其如何追踪数据变更。
Vue.js 支持我们通过
参数传递一个 JavaScript 对象做为组件数据,然后 Vue.js 将遍历此对象属性,使用
- data
方法设置描述对象,通过存取器函数可以追踪该属性的变更,本质原理和上一节实例差不多,但是不同的是,Vue.js 创建了一层
- Object.defineProperty
层,在组件渲染的过程中把属性记录为依赖,之后当依赖项的
- Watcher
被调用时,会通知
- setter
重新计算,从而使它关联的组件得以更新, 如下图:
- Watcher
组件挂载时,实例化
实例,并把该实例传递给依赖管理类,组件渲染时,使用对象观察接口遍历传入的 data 对象,为每个属性创建一个依赖管理实例并设置属性描述对象,在存取器函数 get 函数中,依赖管理实例添加(记录)该属性为一个依赖,然后当该依赖变更时,触发 set 函数,在该函数内通知依赖管理实例,依赖管理实例分发该变更给其内存储的所有
- watcher
实例,
- watcher
实例重新计算,更新组件。
- watcher
因此可以总结说 Vue.js 的响应式原理是 依赖追踪 ,通过一个观察对象,为每个属性,设置存取器函数并注册一个依赖管理实例
,
- dep
内为每个组件实例维护一个
- dep
实例,在属性变更时,通过 setter 通知
- watcher
实例,
- dep
实例分发该变更给每一个
- dep
实例,
- watcher
实例各自计算更新组件实例,即
- watcher
追踪
- watcher
添加的依赖,
- dep
方法提供这种追踪的技术支持,
- Object.defineProperty()
实例维护这种追踪关系。
- dep
接下来对 Vue.js 源码进行简单分析,从对 JavaScript 对象和属性的处理开始:
首先,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()
方法对每一个对象均产生一个
- 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()
存在,则递归记录依赖;
- watcher
方法发布更新事件;
- dep.notify()
Vue.js 需要管理对象的依赖,在属性更新时通知
更新组件,进而更新视图,Vue.js 管理依赖接口采用发布订阅模式实现,源码如下:
- watcher
- var uid$1 = 0;
- var Dep = function Dep() {
- this.id = uid$1++; // 依赖管理实例id
- this.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);
- }
- }
- };
实例可能同时追踪多个属性(即订阅多个依赖管理实例),所以需要维护一个数组,存储多个订阅的依赖管理实例,同时记录每一个实例的 id,便于判断是否已订阅,而后调用依赖管理实例的
- watcher
方法:
- 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
每一个响应式属性都是由一个
实例追踪其变更,而针对不同属性(data, computed, watch),Vue.js 进行了一些差异处理,如下是接口主要逻辑:
- Watcher
- var Watcher = function Watcher (vm,expOrFn,cb,options) {
- this.cb = cb;
- ...
- // parse expression for getter
- if (typeof expOrFn === 'function') {
- this.getter = expOrFn;
- } else {
- this.getter = parsePath(expOrFn);
- }
- this.value = this.lazy
- ? undefined
- : this.get();
- };
在初始化
实例时,会解析
- Watcher
参数(表达式或者函数)成拓展 getter
- expOrFn
,然后调用
- this.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
方法返回 undefined,而对于
- get
计算属性而言,会传入对应指定函数给
- computed
,其返回值就是此
- this.getter
方法返回值。
- get
Vue.jsdata 属性是一个对象,需要调用对象观察接口
:
- 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 -- 拓展getter
- var getter = typeof userDef === 'function' ?
- userDef : userDef.get;
- watchers[key] = new Watcher(vm,
- getter, noop, computedWatcherOptions);
- }
- }
而对于 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
,也就是将
- Dep.target
和
- Watcher
建立连接,如此在组件渲染时才能对属性依赖进行追踪:
- Dep
- 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 响应式的设计原理,对其源码熟悉度也有较大提升,之后在工作和学习过程中,会进行更多的总结分享。
原创文章,转载请注明:转载自熊建刚的博客
本文链接地址: 从 JavaScript 属性描述器剖析 Vue.js 响应式视图
来源: http://www.tuicool.com/articles/3eY7nam