vue是双向数据绑定的框架,数据驱动是他的灵魂,他的实现原理众所周知是Object.defineProperty方法实现的get、set重写,可是这样说太牵强外门了。本文将宏观介绍他的实现javascript
举个很是简单的栗子html
# html <div id="#app"> {{msg}} </div> # script <script> new Vue({ el: '#app', data: { msg: 'hello' }, mounted() { setTimeout(() => { this.msg = 'hi' }, 1000); } }) </script>
上面代码, new Vue进行建立vue对象, el属性是挂载的dom选择器,这里选择id为app的dom,data对象保存这全部数据响应的属性,当其中的某一属性值改变,就触发view渲染,从而实现了“数据->视图”的动态响应;vue
示例中msg初始值为hello,所以页面渲染时为hello,一秒以后,msg变为了hi,触发了view渲染,咱们看到hello变为了li。那么接下来就从这简单的栗子来说解vue的数据驱动把。java
咱们说vue是怎么实现双向数据绑定的?是Object.defineProperty实现了,那么咱们就直接聚焦Object.defineProperty
node
如下是代码react
function defineReactive ( obj, key, val, customSetter, shallow ) { // 建立派发器 var dep = new Dep(); var property = Object.getOwnPropertyDescriptor(obj, key); if (property && property.configurable === false) { return } // cater for pre-defined getter/setters var getter = property && property.get; var setter = property && property.set; if ((!getter || setter) && arguments.length === 2) { val = obj[key]; } var childOb = !shallow && observe(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { var 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) { 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 = !shallow && observe(newVal); dep.notify(); } }); }
vue在给每个data的属性执行defineReactive函数,来达到数据绑定的目的。从代码中能够看到几点:面试
dep.depend()
操做;dep.notify()
操做;这一部分很容易了解,在data的属性get时,触发了派发器的依赖收集(dep.depend),在data的属性set时,触发了派发器的事件通知(dep.notify);express
结合已知知识,Vue的数据绑定是上面这个函数带来的反作用,所以能够得出结论:bash
上一节已经肯定,当更改属性值时,是Dep.target.update更新了view,所以带着这个目的,此小节作一个简单的源码解析app
function Vue (options) { this._init(options); } Vue.prototype._init = function (options) { var vm = this; callHook(vm, 'beforeCreate'); initState(vm); callHook(vm, 'created'); if (vm.$options.el) { vm.$mount(vm.$options.el); } }; function initState (vm) { vm._watchers = []; var opts = vm.$options; if (opts.data) { initData(vm); } else { observe(vm._data = {}, true /* asRootData */); } } function initData (vm) { var data = vm.$options.data; observe(data, true /* asRootData */); } function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob = new Observer(value);; return ob }
从头开始,一步一步进入,发现最终咱们new Vue传进来的data进入了new Observer中;
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); } }; Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]); } };
Observer构造函数中,最终执行了defineReactive为每个属性进行定义,而且是递归调用,以树型遍历咱们传入的data对象的全部节点属性,每个节点都会被包装为一个观察者,当数据get时,进行依赖收集,当数据set时,事件分发。
看到这里,感受好像少了点什么,好像data到这里就结束了,可是并无看懂为何数据改变动新视图的,那么继续往下看
回看一切从头开始的_init方法,在这个方法中,最后调用了vm.$mount(vm.$options.el)
,这是把vm挂载到真实dom,并渲染view的地方,所以接着看下去。
Vue.prototype.$mount = function ( el, hydrating ) { return mountComponent(this, el, hydrating) }; // 渲染dom的真实函数 function mountComponent ( vm, el, hydrating ) { vm.$el = el; callHook(vm, 'beforeMount'); var updateComponent; updateComponent = function () { vm._update(vm._render(), hydrating); }; // new 一个Watcher,开启了数据驱动之旅 new Watcher(vm, updateComponent, noop, { before: function before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate'); } } }, true /* isRenderWatcher */); hydrating = false; if (vm.$vnode == null) { vm._isMounted = true; callHook(vm, 'mounted'); } return vm }
上面部分看到的是,vue将vue对象挂载到真实dom的经历,最终执行了new Watcher,而且回调为vm._update(vm._render(), hydrating)
。顾名思义,这里是执行了vue的更新view的操做(本文暂且不讲更新view,在其余文章已经讲过。本文专一数据驱动部分)。
问:为何说new Watcher开启了数据驱动之旅呢?Watcher又是什么功能?
若是说Object.defineProperty是vue数据驱动的灵魂,那么Watcher则是他的骨骼。
// 超级简单的Watcher var Watcher = function Watcher ( vm, expOrFn, cb, options, isRenderWatcher ) { this.cb = cb; this.deps = []; this.newDeps = []; // 计算属性走if if (this.computed) { this.value = undefined; this.dep = new Dep(); } else { this.value = this.get(); } }; Watcher.prototype.get = function get () { pushTarget(this); var value; var 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 { popTarget(); this.cleanupDeps(); } return value };
简化后Watcher在new时,最终会调用本身的get方法,get方法中第一个语句pushTarget(this)
是开启数据驱动的第一把钥匙,看下文
function pushTarget (_target) { if (Dep.target) { targetStack.push(Dep.target); } Dep.target = _target; }
pushTarget将传入的Watcher对象赋值给了Dep.target,还记得在讲Object.defineProperty时提到了,Dep.target.update是更新view的触发点,在这里终于找到了!
下面看Dep.targe.update
Watcher.prototype.update = function update () { var this$1 = this; /* istanbul ignore else */ if (this.computed) { if (this.dep.subs.length === 0) { this.dirty = true; } else { this.getAndInvoke(function () { this$1.dep.notify(); }); } } else if (this.sync) { this.run(); } else { // update执行了这里 queueWatcher(this); } };
咱们看到update方法最后执行了queueWatcher,继续看下去发现,这实际上是一个更新队列,vue对同一个微任务的全部update进行了收集更新,最终执行了watcher.run,run方法又执行了getAndInvoke
方法,getAndInvoke又执行了this.get
方法。
到来一大圈,终于找到:在改变属性值时,触发了Dep.target所对应的Watcher的 this.get
方法,this.get方法其实就是传入进来的回调函数。回想前面介绍的,vue在挂载到真实dom时,new Watcher传入的回调是updateComponent。串联起来获得告终论:
this.get
,这时候view会更新。到这里,有没有明白为何全部属性的派发器都会收集updateComponent的Watcher,从而在本身set时通知更新?若是没明白,那就看下一节分析
this.get
初始化一次值,对标updateComponent函数,这个时候会触发vue渲染过程vue数据驱动是有前提条件的,不是怎么用均可以的,前提条件就是必须在data中声明的属性才会参与数据驱动,数据->视图。看下面栗子
有以下html:
<div id="app"> <div>{{prev}}{{next}}</div> </div>
以下js:
new Vue({ el: "#app", data: { prev: 'hello', }, created() { }, mounted() { this.next = 'world'; } })
页面渲染的结果是什呢?
答:hello;
为何this.next明明赋值,没有渲染到view中去?由于他并无参与数据驱动的观察者,还记得前面讲到vue会把传入的data对象深度遍历包装为观察者来吧,这里next属性并无被成为观察者,所以并不会引起view更新。
为何看到的未必真实的,上面的栗子咱们发现,view中看到的只有hello,可是数据真的是有hello么?未必,看下面栗子。
new Vue({ el: "#app", data: { prev: 'hello', }, created() { }, mounted() { this.next = 'world'; setTimeout(() => { this.prev = 'hi'; }, 1000); } })
这个代码比上面栗子就多了3行代码,再页面渲染1秒后,改变prev的值为hi,那么页面会展示什么效果呢?
答:hi world
从这里能够看到,虽然next赋值并无引发view更新,可是data确实成功变动了,当prev改变时,触发了update,从而将view更新,此时next有值,所以就显示在了view中。这就是不少初学者会遇到为何明明赋值没有显示,可是点了一下其余的东西,却显示了的问题。
仍是根据第一个栗子引伸一个案例以下:
new Vue({ el: "#app", data: { prev: 'hello', }, created() { this.next = 'world'; }, mounted() { setTimeout(() => { this.next = 'memory' }, 1000) } })
咱们在created生命周期中赋值next,在mounted生命周期延迟一秒改变next的值,那结果会这样?
答:永远显示helloworld
若是已经掌握了vue实例化过程的同窗可能已经猜到了为何
当created生命周期执行时,此时尚未作vnode转化为真实dom的操做,此时data属性已经代理到this下,所以修改this.next就修改了data对象的值,data就变为了{prev: 'hello', next: 'world'}
,所以在render时就将next也渲染到了页面上
另外此时已经完成了数据驱动的灵魂步骤(将data遍历包装为观察者),所以在延迟1s后改变next值,仍然跟栗子2同样不会引发view更新的。
所以,写vue出现以上改变data时view未更新,首先要检查本身的代码,而不是怀疑vue框架的问题。。
有些面试官会问在异步获取数据并改变data值时,放在created仍是mounted?
我感受没什么可答的,2个都没问题,固然对于代码优化,放在created更早的发出请求,所以放在created里更合适。