vue是双向数据绑定的框架,数据驱动是他的灵魂,他的实现原理众所周知是Object.defineProperty方法实现的get、set重写,但是这样说太牵强外门了。本文将宏观介绍他的实现

使用vue

举个非常简单的栗子

  1. # html
  2. <div id="#app">
  3. {{msg}}
  4. </div>
  5. # script
  6. <script>
  7. new Vue({
  8. el: '#app',
  9. data: {
  10. msg: 'hello'
  11. },
  12. mounted() {
  13. setTimeout(() => {
  14. this.msg = 'hi'
  15. }, 1000);
  16. }
  17. })
  18. </script>

上面代码, new Vue进行创建vue对象, el属性是挂载的dom选择器,这里选择id为app的dom,data对象保存这所有数据响应的属性,当其中的某一属性值改变,就触发view渲染,从而实现了“数据->视图”的动态响应;

示例中msg初始值为hello,因此页面渲染时为hello,一秒之后,msg变为了hi,触发了view渲染,我们看到hello变为了li。那么接下来就从这简单的栗子来讲解vue的数据驱动把。

分析Object.defineProperty

我们说vue是怎么实现双向数据绑定的?是Object.defineProperty实现了,那么我们就直接聚焦Object.defineProperty

以下是代码

  1. function defineReactive (
  2. obj,
  3. key,
  4. val,
  5. customSetter,
  6. shallow
  7. ) {
  8. // 创建派发器
  9. var dep = new Dep();
  10. var property = Object.getOwnPropertyDescriptor(obj, key);
  11. if (property && property.configurable === false) {
  12. return
  13. }
  14. // cater for pre-defined getter/setters
  15. var getter = property && property.get;
  16. var setter = property && property.set;
  17. if ((!getter || setter) && arguments.length === 2) {
  18. val = obj[key];
  19. }
  20. var childOb = !shallow && observe(val);
  21. Object.defineProperty(obj, key, {
  22. enumerable: true,
  23. configurable: true,
  24. get: function reactiveGetter () {
  25. var value = getter ? getter.call(obj) : val;
  26. // 收集依赖对象
  27. if (Dep.target) {
  28. dep.depend();
  29. if (childOb) {
  30. childOb.dep.depend();
  31. if (Array.isArray(value)) {
  32. dependArray(value);
  33. }
  34. }
  35. }
  36. return value
  37. },
  38. set: function reactiveSetter (newVal) {
  39. var value = getter ? getter.call(obj) : val;
  40. /* eslint-disable no-self-compare */
  41. if (newVal === value || (newVal !== newVal && value !== value)) {
  42. return
  43. }
  44. /* eslint-enable no-self-compare */
  45. if ("development" !== 'production' && customSetter) {
  46. customSetter();
  47. }
  48. if (setter) {
  49. setter.call(obj, newVal);
  50. } else {
  51. val = newVal;
  52. }
  53. childOb = !shallow && observe(newVal);
  54. dep.notify();
  55. }
  56. });
  57. }

vue在给每一个data的属性执行defineReactive函数,来达到数据绑定的目的。从代码中可以看到几点:

  1. 每一个数据绑定,都会new一个Dep(暂且叫他派发器),派发器的功能是什么?依赖收集以事件分发;
  2. 在属性get中,除了获取当前属性的值,还做了dep.depend()操作;
  3. dep.depend的目的是什么?看Dep部分代码,很简单,其实就是依赖收集,将Dep.target需要收集的依赖进行添加到自己的派发器里
  4. 在属性set时,就是给属性改变值时,除了改变值意外,还执行了dep.notify()操作;
  5. dep.notify的目的又是什么?看代码,依旧很简单,将自己派发器的所有依赖触发update函数;

这一部分很容易了解,在data的属性get时,触发了派发器的依赖收集(dep.depend),在data的属性set时,触发了派发器的事件通知(dep.notify);

结合已知知识,Vue的数据绑定是上面这个函数带来的副作用,因此可以得出结论:

  1. 当我们改变某个属性值时,派发器Dep通知了view层去更新
  2. Dep.target是派发器Dep收集的依赖,并在属性值改变时触发了update函数,view层的更新与Dep.target有必然的联系。换句话说:数据->视图的数据驱动就等于Dep.target.update()

简单的源码解析

上一节已经确定,当更改属性值时,是Dep.target.update更新了view,因此带着这个目的,此小节做一个简单的源码解析

一切从头开始

  1. function Vue (options) {
  2. this._init(options);
  3. }
  4. Vue.prototype._init = function (options) {
  5. var vm = this;
  6. callHook(vm, 'beforeCreate');
  7. initState(vm);
  8. callHook(vm, 'created');
  9. if (vm.$options.el) {
  10. vm.$mount(vm.$options.el);
  11. }
  12. };
  13. function initState (vm) {
  14. vm._watchers = [];
  15. var opts = vm.$options;
  16. if (opts.data) {
  17. initData(vm);
  18. } else {
  19. observe(vm._data = {}, true /* asRootData */);
  20. }
  21. }
  22. function initData (vm) {
  23. var data = vm.$options.data;
  24. observe(data, true /* asRootData */);
  25. }
  26. function observe (value, asRootData) {
  27. if (!isObject(value) || value instanceof VNode) {
  28. return
  29. }
  30. var ob = new Observer(value);;
  31. return ob
  32. }

从头开始,一步一步进入,发现最终我们new Vue传进来的data进入了new Observer中;

数据驱动部分-观察者

  1. var Observer = function Observer (value) {
  2. this.value = value;
  3. this.dep = new Dep();
  4. this.vmCount = 0;
  5. def(value, '__ob__', this);
  6. if (Array.isArray(value)) {
  7. var augment = hasProto
  8. ? protoAugment
  9. : copyAugment;
  10. augment(value, arrayMethods, arrayKeys);
  11. this.observeArray(value);
  12. } else {
  13. this.walk(value);
  14. }
  15. };
  16. Observer.prototype.walk = function walk (obj) {
  17. var keys = Object.keys(obj);
  18. for (var i = 0; i < keys.length; i++) {
  19. defineReactive(obj, keys[i]);
  20. }
  21. };

Observer构造函数中,最终执行了defineReactive为每一个属性进行定义,并且是递归调用,以树型遍历我们传入的data对象的所有节点属性,每一个节点都会被包装为一个观察者,当数据get时,进行依赖收集,当数据set时,事件分发。

看到这里,感觉好像少了点什么,好像data到这里就结束了,但是并没有看懂为什么数据改变更新视图的,那么继续往下看

vue挂载到dom

回看一切从头开始的_init方法,在这个方法中,最后调用了 vm.$mount(vm.$options.el),这是把vm挂载到真实dom,并渲染view的地方,因此接着看下去。

  1. Vue.prototype.$mount = function (
  2. el,
  3. hydrating
  4. ) {
  5. return mountComponent(this, el, hydrating)
  6. };
  7. // 渲染dom的真实函数
  8. function mountComponent (
  9. vm,
  10. el,
  11. hydrating
  12. ) {
  13. vm.$el = el;
  14. callHook(vm, 'beforeMount');
  15. var updateComponent;
  16. updateComponent = function () {
  17. vm._update(vm._render(), hydrating);
  18. };
  19. // new 一个Watcher,开启了数据驱动之旅
  20. new Watcher(vm, updateComponent, noop, {
  21. before: function before () {
  22. if (vm._isMounted) {
  23. callHook(vm, 'beforeUpdate');
  24. }
  25. }
  26. }, true /* isRenderWatcher */);
  27. hydrating = false;
  28. if (vm.$vnode == null) {
  29. vm._isMounted = true;
  30. callHook(vm, 'mounted');
  31. }
  32. return vm
  33. }

上面部分看到的是,vue将vue对象挂载到真实dom的经历,最终执行了new Watcher,并且回调为vm._update(vm._render(), hydrating)。顾名思义,这里是执行了vue的更新view的操作(本文暂且不讲更新view,在其他文章已经讲过。本文专注数据驱动部分)。

问:为什么说new Watcher开启了数据驱动之旅呢?Watcher又是什么功能?

简述Watcher

如果说Object.defineProperty是vue数据驱动的灵魂,那么Watcher则是他的骨骼。

  1. // 超级简单的Watcher
  2. var Watcher = function Watcher (
  3. vm,
  4. expOrFn,
  5. cb,
  6. options,
  7. isRenderWatcher
  8. ) {
  9. this.cb = cb;
  10. this.deps = [];
  11. this.newDeps = [];
  12. // 计算属性走if
  13. if (this.computed) {
  14. this.value = undefined;
  15. this.dep = new Dep();
  16. } else {
  17. this.value = this.get();
  18. }
  19. };
  20. Watcher.prototype.get = function get () {
  21. pushTarget(this);
  22. var value;
  23. var vm = this.vm;
  24. try {
  25. value = this.getter.call(vm, vm);
  26. } catch (e) {
  27. if (this.user) {
  28. handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
  29. } else {
  30. throw e
  31. }
  32. } finally {
  33. popTarget();
  34. this.cleanupDeps();
  35. }
  36. return value
  37. };

简化后Watcher在new时,最终会调用自己的get方法,get方法中第一个语句pushTarget(this)是开启数据驱动的第一把钥匙,看下文

  1. function pushTarget (_target) {
  2. if (Dep.target) { targetStack.push(Dep.target); }
  3. Dep.target = _target;
  4. }

pushTarget将传入的Watcher对象赋值给了Dep.target,还记得在讲Object.defineProperty时提到了,Dep.target.update是更新view的触发点,在这里终于找到了!

下面看Dep.targe.update

  1. Watcher.prototype.update = function update () {
  2. var this$1 = this;
  3. /* istanbul ignore else */
  4. if (this.computed) {
  5. if (this.dep.subs.length === 0) {
  6. this.dirty = true;
  7. } else {
  8. this.getAndInvoke(function () {
  9. this$1.dep.notify();
  10. });
  11. }
  12. } else if (this.sync) {
  13. this.run();
  14. } else {
  15. // update执行了这里
  16. queueWatcher(this);
  17. }
  18. };

我们看到update方法最后执行了queueWatcher,继续看下去发现,这其实是一个更新队列,vue对同一个微任务的所有update进行了收集更新,最终执行了watcher.run,run方法又执行了getAndInvoke方法,getAndInvoke又执行了this.get方法。

到来一大圈,终于找到:在改变属性值时,触发了Dep.target所对应的Watcher的 this.get方法,this.get方法其实就是传入进来的回调函数。回想前面介绍的,vue在挂载到真实dom时,new Watcher传入的回调是updateComponent。串联起来得到了结论:

  1. 我们在get属性时,Dep派发器收集到了Watcher当作依赖
  2. 当我们set属性时,Dep派发器事件分发,使所有收集到的依赖执行this.get,这时候view会更新。

到这里,有没有明白为什么所有属性的派发器都会收集updateComponent的Watcher,从而在自己set时通知更新?如果没明白,那就看下一节分析

从宏观角度看问题

  1. 当我们new Vue时,首先会将传入的data将被vue包装为观察者,所有get和set行为都会捕捉到并执行响应的操作
  2. 接下来vue会将vue对象挂载到真实dom(其实指的虚拟的渲染),这个时候new 一个Watcher, 在new Watcher时,会执行一次this.get初始化一次值,对标updateComponent函数,这个时候会触发vue渲染过程
  3. 在vue渲染过程中,所有的数据都需要进行get行为才能得到值并给真实dom赋值,因此这时触发了所有data属性的get,并且此时Dep.target是updateComponent的Watcher,因此所有的data属性派发器都收集到了此Watcher,在set时,派发器notify进行事件分发,收集到的依赖Watcher都得到了通知进行update,所有又会执行updateCompoent进行更新view

通过案例进行分析

vue数据驱动的前提

vue数据驱动是有前提条件的,不是怎么用都可以的,前提条件就是必须在data中声明的属性才会参与数据驱动,数据->视图。看下面栗子

有如下html:

  1. <div id="app">
  2. <div>{{prev}}{{next}}</div>
  3. </div>

如下js:

  1. new Vue({
  2. el: "#app",
  3. data: {
  4. prev: 'hello',
  5. },
  6. created() {
  7. },
  8. mounted() {
  9. this.next = 'world';
  10. }
  11. })

页面渲染的结果是什呢?

答:hello;

为什么this.next明明赋值,没有渲染到view中去?因为他并没有参与数据驱动的观察者,还记得前面讲到vue会把传入的data对象深度遍历包装为观察者来吧,这里next属性并没有被成为观察者,因此并不会引发view更新。

看到的未必真实的

为什么看到的未必真实的,上面的栗子我们发现,view中看到的只有hello,但是数据真的是有hello么?未必,看下面栗子。

  1. new Vue({
  2. el: "#app",
  3. data: {
  4. prev: 'hello',
  5. },
  6. created() {
  7. },
  8. mounted() {
  9. this.next = 'world';
  10. setTimeout(() => {
  11. this.prev = 'hi';
  12. }, 1000);
  13. }
  14. })

这个代码比上面栗子就多了3行代码,再页面渲染1秒后,改变prev的值为hi,那么页面会展现什么效果呢?

答:hi world

从这里可以看到,虽然next赋值并没有引起view更新,但是data确实成功变更了,当prev改变时,触发了update,从而将view更新,此时next有值,因此就显示在了view中。这就是很多初学者会遇到为什么明明赋值没有显示,但是点了一下其他的东西,却显示了的问题。

看到的未必真实2

还是根据第一个栗子引申一个案例如下:

  1. new Vue({
  2. el: "#app",
  3. data: {
  4. prev: 'hello',
  5. },
  6. created() {
  7. this.next = 'world';
  8. },
  9. mounted() {
  10. setTimeout(() => {
  11. this.next = 'memory'
  12. }, 1000)
  13. }
  14. })

我们在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框架的问题。。

注意事项

  • 本文参考vue版本v2.5.17-beta.0
  • 本文专注数据驱动主线data,computed等支线并没有介绍,因此贴的代码都做了大量删减
  • data属性收集到的依赖Dep.target并不止updateComponent的Watcher,还可能多个,比如computed属性

附加讨论

有些面试官会问在异步获取数据并改变data值时,放在created还是mounted?

我感觉没什么可答的,2个都没问题,当然对于代码优化,放在created更早的发出请求,因此放在created里更合适。

理解vue数据驱动的更多相关文章

  1. 模拟源码深入理解Vue数据驱动原理(1)

    Vue有一核心就是数据驱动(Data Driven),允许我们采用简洁的模板语法来声明式的将数据渲染进DOM,且数据与DOM是绑定在一起的,这样当我们改变Vue实例的数据时,对应的DOM元素也就会改变 ...

  2. 模拟源码深入理解Vue数据驱动原理(2)

    我们说到如果监听的属性是个对象呢?那么这个对象中的其他属性岂不就是监听不了了吗?如下: 倘若user中的name.age属性变化,如何知道它们变化了呢?今儿,就来解决这一问题. 通过走读Vue源码,发 ...

  3. 深入理解Vue组件3大核心概念

    摘要: 搞懂Vue组件! 作者:浪里行舟 原文:详解vue组件三大核心概念 Fundebug经授权转载,版权归原作者所有. 前言 本文主要介绍属性.事件和插槽这三个vue基础概念.使用方法及其容易被忽 ...

  4. 深入理解vue的watch

    深入理解vue的watch vue中的wactch可以监听到data的变化,执行定义的回调,在某些场景是很有用的,本文将深入源码揭开watch额面纱 前言 watch的使用 watch的多种使用方式 ...

  5. 理解vue中的scope的使用

    理解vue中的scope的使用 我们都知道vue slot插槽可以传递任何属性或html元素,但是在调用组件的页面中我们可以使用 template scope="props"来获取 ...

  6. 理解Vue中的Render渲染函数

    理解Vue中的Render渲染函数 VUE一般使用template来创建HTML,然后在有的时候,我们需要使用javascript来创建html,这时候我们需要使用render函数.比如如下我想要实现 ...

  7. 深入理解vue

    一 理解vue的核心理念 使用vue会让人感到身心愉悦,它同时具备angular和react的优点,轻量级,api简单,文档齐全,简单强大,麻雀虽小五脏俱全. 倘若用一句话来概括vue,那么我首先想到 ...

  8. 深入理解 Vue 组件

    深入理解 Vue 组件 组件使用中的细节点 使用 is 属性,解决组件使用中的bug问题 <!DOCTYPE html> <html lang="en"> ...

  9. vue系列---理解Vue中的computed,watch,methods的区别及源码实现(六)

    _ 阅读目录 一. 理解Vue中的computed用法 二:computed 和 methods的区别? 三:Vue中的watch的用法 四:computed的基本原理及源码实现 回到顶部 一. 理解 ...

随机推荐

  1. WebGL three.js学习笔记 加载外部模型以及Tween.js动画

    WebGL three.js学习笔记 加载外部模型以及Tween.js动画 本文的程序实现了加载外部stl格式的模型,以及学习了如何把加载的模型变为一个粒子系统,并使用Tween.js对该粒子系统进行 ...

  2. spring boot初步

    spring boot介绍 Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程. 该框架使用了特定的方式来进行配置,从而 ...

  3. 配置 IO 时要记得换 Page

    配置 IO 时要记得换 Page 在配置某些芯片时,配置 IO 时要记得换页,不然不生效. 注意查看 IO 的相关规格书说明,而且每个厂商是不一样的.

  4. Centos7 Apache配置虚拟主机的三种方式

    https://blog.csdn.net/tladagio/article/details/80760261 一.虚机主机的三种方式 1.基于IP 2.基于IP+端口 3.基于域名 官网文档:htt ...

  5. linux 端口映射设置

    iptables -t nat -A PREROUTING -p tcp --dport 新端口 -j REDIRECT --to-ports 旧端口

  6. selenium webdriver学习(十)------------如何把一个元素拖放到另一个元素里面(转)

    selenium webdriver学习(十)------------如何把一个元素拖放到另一个元素里面 博客分类: Selenium-webdriver 元素拖放drag and drop  Q群里 ...

  7. @bzoj - 4382@ [POI2015] Podział naszyjnika

    目录 @description@ @solution@ @accepted code@ @details@ @description@ 长度为 n 的一串项链,每颗珠子是 k 种颜色之一. 第 i 颗 ...

  8. Sublime插件:增强篇

    Sublime Text 如何安装插件详见:https://packagecontrol.io/installation WordCount:可以实时显示当前文件的字数. 安装后,后下角多出字数 En ...

  9. LA 4973 Ardenia (3D Geometry + Simulation)

    ACM-ICPC Live Archive 三维几何,题意是要求求出两条空间线段的距离.题目难度在于要求用有理数的形式输出,这就要求写一个有理数类了. 开始的时候写出来的有理数类就各种疯狂乱套,TLE ...

  10. Python--day70--ORM多对多的三种方式