浅析Vue响应系统原理与搭建Vue2.x迷你版

Vue2.x响应式原理怎么实现的?

Vue 最独特的特性之一,是其非侵入性的响应式系统。那么什么是响应式原理?

让客户满意是我们工作的目标,不断超越客户的期望值来自于我们对这个行业的热爱。我们立志把好的技术通过有效、简单的方式提供给客户,将通过不懈努力成为客户在信息化领域值得信任、有价值的长期合作伙伴,公司提供的服务项目有:域名申请、网络空间、营销软件、网站建设、城关网站维护、网站推广。

数据模型仅仅是普通的JavaScript对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率。简言之,在改变数据的时候,视图会跟着更新。

了解概念之后,那么它是怎么实现的呢?

其实是利用Object.defineProperty()中的getter 和setter方法和设计模式中的观察者模式。

那么,我们先来看下Object.defineProperty()。MDN中它是这样解释它的:Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

 
 
 
 
  1. let data = {
  2.  msg:'hello'
  3. };
  4. let vm = {};
  5. Object.defineProperty(vm, 'msg', {
  6.         enumerable: true, // 可枚举(可遍历)
  7.         configurable: true, // 可配置(可以使用delete 删除,可以通过defineProperty重新定义)
  8.         // 当获取值的时候执行
  9.         get() {
  10.             console.log('get', data.msg);
  11.             return data.msg
  12.         },
  13.         // 当设置值的时候执行
  14.         set(newVal) {
  15.             if (newVal === data.msg) {
  16.                 return
  17.             }
  18.             data.msg = newVal;
  19.             console.log('set', data.msg);
  20.         }
  21. })
  22. // 测试
  23. console.log(vm.msg);
  24. /* 
  25. > "get" "hello"
  26. > "hello"
  27. */
  28. vm.msg = 'world'; // > "set" "world"

简单介绍Object.defineProperty()之后,接着就是了解观察者模式,看到它,你可能会想起发布-订阅模式。其实它们的本质是相同的,但是也存在一定的区别。

我们不妨先来看下发布-订阅模式。

发布-订阅者模式里面包含了三个模块,发布者,订阅者和统一调度中心。这里统一调度中心相当于报刊办事大厅。发布者相当与某个杂志负责人,他来中心这注册一个的杂志,而订阅者相当于用户,我在中心订阅了这分杂志。每当发布者发布了一期杂志,办事大厅就会通知订阅者来拿新杂志。发布-订阅者模式由统一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

下面,我们将通过一个实现Vue自定义事件的例子来更进一步了解发布-订阅模式。

 
 
 
 
  1. function EventEmitter(){
  2.     // 初始化统一调度中心
  3.     this.subs = Object.create(null); // {'click':[fn1,fn2]}
  4. }
  5. // 注册事件
  6. EventEmitter.prototype.$on = function (eventType,handler){
  7.         console.log(this);
  8.         this.subs[eventType]= this.subs[eventType]||[];
  9.         this.subs[eventType].push(handler);
  10. }
  11. // 触发事件
  12. EventEmitter.prototype.$emit = function (eventType,data){
  13.         if(this.subs[eventType]){
  14.                 this.subs[eventType].forEach(handler => {
  15.                     handler(data);
  16.                 });
  17.         }
  18. }
  19. // 测试
  20. const em = new EventEmitter();
  21. //订阅者
  22. em.$on('click1',(data)=>{
  23.     console.log(data);
  24. })
  25. // 发布者
  26. em.$emit('click1','maomin') //maomin

这种自定义事件广泛应用于Vue同级组件传值。

接下来,我们来介绍观察者模式。

观察者模式是由目标调度,比如当事件触发时,目标就会调用观察者的方法,所以观察者模式的订阅者(观察者)与发布者(目标)之间存在依赖。

 
 
 
 
  1. // 发布者(目标)
  2. function Dep(){
  3.     this.subs = [];
  4. }
  5. Dep.prototype.addSub = function (sub){
  6.     if(sub&&sub.update){
  7.             this.subs.push(sub);
  8.     }
  9. }
  10. Dep.prototype.notify = function (data){
  11.         this.subs.forEach(sub=>{
  12.             sub.update(data);
  13.         })
  14. }
  15. // 订阅者(观察者)
  16. function Watcher(){}
  17.     Watcher.prototype.update=function(data){
  18.     console.log(data);
  19. }
  20. // 测试
  21. let dep = new Dep();
  22. let watcher = new Watcher();
  23. // 收集依赖
  24. dep.addSub(watcher);
  25. // 发送通知
  26. dep.notify('1');
  27. dep.notify('2');

下图是区分两种模式。

实现Vue2.x迷你版本

为什么要实现一个Vue迷你版本,目的就是加深对Vue响应式原理以及其中一些API的理解。首先我们先来分析Vue2.x 响应式原理的整体结构。

如下图所示:

我们接下来,将根据这幅图片描述的流程来实现一款迷你版Vue。Vue2.x采用了Virtual DOM,但是因为这里只需要实现一个迷你版,所以我们这里做了简化,我们这里就是直接操作DOM。

下面,我们来看下我是如何搭建一款Vue mini的。

第一步

页面结构如下,我们可以先引入Vue2.x完整版本,看下实现效果。

 
 
 
 
  1.     
  2.     
  3.     
  4.     Vue2.x Reactive
  5.     
  6.         

    文本节点

  7.         
    {{msg}}
  8.         
    {{count}}
  9.         
    {{obj.name}}
  10.         
    {{arr[0]}}
  11.         
    {{obj.inner.age}}
  12.         
    {{obj.inner.name}}
  13.         

    v-text

  14.         
  •         

    v-model

  •         
  •         
  •         

    v-html

  •         
  •         

    v-show

  •         {{isShow}}
  •         

    v-on

  •         handler
  •         onClick
  •         

    v-if

  •         
  •             {{isIf}}

  •         
  •     
  •     
  •     
  •  经过测试,Vue2.x完整版搭载的页面显示如下。我们将使用Vue迷你版本同样实现以下页面效果。

    第二步

    我们将根据整体结构图和页面结构来搭建这个Vue迷你版本,我们姑且将这个版本叫做vuemini.js。

    通过整体结构图我们发现,一共有Vue、Observer、Compiler、Dep、Watcher这几个构造函数。我们首先创建这几个构造函数,这里不使用class类来定义是因为Vue源码大部分也使用构造函数,另外,相对也好拓展。

    Vue

     
     
     
     
    1. // 实例。
    2. function Vue(options) {
    3.     this.$options = options || {};
    4.     this._data = typeof options.data === 'function' ? options.data() : options.data || {};
    5.     this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el;
    6.     // 负责把data中的属性注入到Vue实例,转换成getter/setter
    7.     this._proxyData(this._data);
    8.     this.initMethods(this, options.methods || {})
    9.     // 负责调用observer监听data中所有属性的变化
    10.     new Observer(this._data);
    11.     // 负责调用compiler解析指令/插值表达式
    12.     new Compiler(this);
    13. }
    14. // 将data中的属性挂载到this上
    15. Vue.prototype._proxyData = function (data) {
    16.     Object.keys(data).forEach(key => {
    17.         Object.defineProperty(this, key, {
    18.             configurable: true,
    19.             enumerable: true,
    20.             get() {
    21.                 return data[key]
    22.             },
    23.             set(newVal) {
    24.                 if (newVal === data[key]) {
    25.                     return
    26.                 }
    27.                 data[key] = newVal;
    28.             }
    29.         })
    30.     })
    31. }
    32. function noop(a, b, c) { }
    33. function polyfillBind(fn, ctx) {
    34.     function boundFn(a) {
    35.         var l = arguments.length;
    36.         return l
    37.             ? l > 1
    38.                 ? fn.apply(ctx, arguments)
    39.                 : fn.call(ctx, a)
    40.             : fn.call(ctx)
    41.     }
    42.     boundFn._length = fn.length;
    43.     return boundFn
    44. }
    45. function nativeBind(fn, ctx) {
    46.     return fn.bind(ctx)
    47. }
    48. const bind = Function.prototype.bind
    49.     ? nativeBind
    50.     : polyfillBind;
    51. // 初始化methods属性
    52. Vue.prototype.initMethods = function (vm, methods) {
    53.     for (var key in methods) {
    54.         {
    55.             if (typeof methods[key] !== 'function') {
    56.                 warn(
    57.                     "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
    58.                     "Did you reference the function correctly?",
    59.                     vm
    60.                 );
    61.             }
    62.         }
    63.         vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    64.     }
    65. }

    Observer

     
     
     
     
    1. // 数据劫持。
    2. // 负责把data(_data)选项中的属性转换成响应式数据。
    3. function Observer(data) {
    4.     this.walk(data);
    5. }
    6. Observer.prototype.walk = function (data) {
    7.     if (!data || typeof data !== 'object') {
    8.         return
    9.     }
    10.     Object.keys(data).forEach(key => {
    11.         this.defineReactive(data, key, data[key]);
    12.     })
    13. }
    14. Observer.prototype.defineReactive = function (obj, key, val) {
    15.     let that = this;
    16.     // 负责收集依赖
    17.     let dep = new Dep();
    18.     // 如果val是对象,把val内部的属性转换成响应式数据
    19.     this.walk(val);
    20.     Object.defineProperty(obj, key, {
    21.         enumerable: true,
    22.         configurable: true,
    23.         get() {
    24.             // 收集依赖
    25.             Dep.target && dep.addSub(Dep.target)
    26.             return val
    27.         },
    28.         set(newVal) {
    29.             if (newVal === val) {
    30.                 return
    31.             }
    32.             val = newVal;
    33.             // data内属性重新赋值后,使其转化为响应式数据。
    34.             that.walk(newVal);
    35.             // 发送通知
    36.             dep.notify();
    37.         }
    38.     })
    39. }

    Compiler

     
     
     
     
    1. // 编译模板,解析指令/插值表达式
    2. // 负责页面的首次渲染
    3. // 当数据变化时重新渲染视图
    4. function Compiler(vm) {
    5.     this.el = vm.$el;
    6.     this.vm = vm;
    7.     // 立即编译模板
    8.     this.compile(this.el);
    9. }
    10. // 编译模板,处理文本节点和元素节点
    11. Compiler.prototype.compile = function (el) {
    12.     let childNodes = el.childNodes;
    13.     Array.from(childNodes).forEach(node => {
    14.         // 处理文本节点
    15.         if (this.isTextNode(node)) {
    16.             this.compileText(node);
    17.         }
    18.         // 处理元素节点 
    19.         else if (this.isElementNode(node)) {
    20.             this.compileElement(node);
    21.         }
    22.         // 判断node节点,是否有子节点,如果有子节点,要递归调用compile方法
    23.         if (node.childNodes && node.childNodes.length) {
    24.             this.compile(node);
    25.         }
    26.     })
    27. }
    28. // 编译文本节点,处理插值表达式
    29. Compiler.prototype.compileText = function (node) {
    30.     // console.dir(node);
    31.     let reg = /\{\{(.+?)\}\}/;
    32.     let value = node.textContent;
    33.     if (reg.test(value)) {
    34.         let key = RegExp.$1.trim();
    35.         if (this.vm.hasOwnProperty(key)) {
    36.             node.textContent = value.replace(reg, typeof this.vm[key] === 'object' ? JSON.stringify(this.vm[key]) : this.vm[key]);
    37.             // 创建watcher对象,当数据改变更新视图
    38.             new Watcher(this.vm, key, (newVal) => {
    39.                 node.textContent = newVal;
    40.             })
    41.         } else {
    42.             const str = `this.vm.${key}`;
    43.             node.textContent = value.replace(reg, eval(str));
    44.             // 创建watcher对象,当数据改变更新视图
    45.             new Watcher(this.vm, key, () => {
    46.                 const strw = `this.vm.${key}`;
    47.                 node.textContent = value.replace(reg, eval(strw));
    48.             })
    49.         }
    50.     }
    51. }
    52. // 判断节点是否是文本节点
    53. Compiler.prototype.isTextNode = function (node) {
    54.     return node.nodeType === 3;
    55. }
    56. // 判断节点是否是元素节点
    57. Compiler.prototype.isElementNode = function (node) {
    58.     return node.nodeType === 1;
    59. }
    60. // 编译元素节点,处理指令
    61. Compiler.prototype.compileElement = function (node) {
    62.     // console.log(node.attributes);
    63.     // 遍历所有的属性节点
    64.     Array.from(node.attributes).forEach(attr => {
    65.         let attrName = attr.name;
    66.         // console.log(attrName);
    67.         // 判断是否是指令
    68.         if (this.isDirective(attrName)) {
    69.             // 判断:如v-on:click
    70.             let eventName;
    71.             if (attrName.indexOf(':') !== -1) {
    72.                 const strArr = attrName.substr(2).split(':');
    73.                 attrName = strArr[0];
    74.                 eventName = strArr[1];
    75.             } else if (attrName.indexOf('@') !== -1) {
    76.                 eventName = attrName.substr(1);
    77.                 attrName = 'on';
    78.             } else {
    79.                 attrName = attrName.substr(2);
    80.             }
    81.             let key = attr.value;
    82.             this.update(node, key, attrName, eventName);
    83.         }
    84.     })
    85. }
    86. // 判断元素属性是否是指令
    87. Compiler.prototype.isDirective = function (attrName) {
    88.     return attrName.startsWith('v-') || attrName.startsWith('@');
    89. }
    90. // 指令辅助函数
    91. Compiler.prototype.update = function (node, key, attrName, eventName) {
    92.     let updateFn = this[attrName + 'Updater'];
    93.     updateFn && updateFn.call(this, node, this.vm[key], key, eventName);
    94. }
    95. // 处理v-text指令
    96. Compiler.prototype.textUpdater = function (node, value, key) {
    97.     node.textContent = value;
    98.     new Watcher(this.vm, key, (newVal) => {
    99.         node.textContent = newVal;
    100.     })
    101. }
    102. // 处理v-html指令
    103. Compiler.prototype.htmlUpdater = function (node, value, key) {
    104.     node.insertAdjacentHTML('beforeend', value);
    105.     new Watcher(this.vm, key, (newVal) => {
    106.         node.insertAdjacentHTML('beforeend', newVal);
    107.     })
    108. }
    109. // 处理v-show指令
    110. Compiler.prototype.showUpdater = function (node, value, key) {
    111.     !value ? node.style.display = 'none' : node.style.display = 'block'
    112.     new Watcher(this.vm, key, (newVal) => {
    113.         !newVal ? node.style.display = 'none' : node.style.display = 'block';
    114.     })
    115. }
    116. // 处理v-if指令
    117. Compiler.prototype.ifUpdater = function (node, value, key) {
    118.     const nodew = node;
    119.     const nodep = node.parentNode;
    120.     if (!value) {
    121.         node.parentNode.removeChild(node)
    122.     }
    123.     new Watcher(this.vm, key, (newVal) => {
    124.         console.log(newVal);
    125.         !newVal ? nodep.removeChild(node) : nodep.appendChild(nodew);
    126.     })
    127. }
    128. // 处理v-on指令
    129. Compiler.prototype.onUpdater = function (node, value, key, eventName) {
    130.     if (eventName) {
    131.         const handler = this.vm.$options.methods[key].bind(this.vm);
    132.         node.addEventListener(eventName, handler);
    133.     }
    134. }
    135. // 处理v-model指令
    136. Compiler.prototype.modelUpdater = function (node, value, key) {
    137.     node.value = value;
    138.     new Watcher(this.vm, key, (newVal) => {
    139.         node.value = newVal;
    140.     })
    141.     // 双向绑定,视图变化更新数据
    142.     node.addEventListener('input', () => {
    143.         this.vm[key] = node.value;
    144.     })
    145. }

    Dep

     
     
     
     
    1. // 发布者。
    2. // 收集依赖,添加所有的观察者(watcher)。通知所有的观察者。
    3. function Dep() {
    4.     // 存储所有的观察者watcher
    5.     this.subs = [];
    6. }
    7. // 添加观察者
    8. Dep.prototype.addSub = function (sub) {
    9.     if (sub && sub.update) {
    10.         this.subs.push(sub);
    11.     }
    12. }
    13. // 发送通知
    14. Dep.prototype.notify = function () {
    15.     this.subs.forEach(sub => {
    16.         sub.update();
    17.     })
    18. }

    Watcher

     
     
     
     
    1. function Watcher(vm, key, cb) {
    2.     this.vm = vm;
    3.     this.key = key;
    4.     this.cb = cb;
    5.     // 把当前watcher对象记录到Dep类的静态属性target
    6.     Dep.target = this;
    7.     if (vm.hasOwnProperty(key)) {
    8.         this.oldVal = vm[key];
    9.     } else {
    10.         const str = `vm.${key}`;
    11.         this.oldVal = eval(str);
    12.     }
    13.     Dep.target = null;
    14. }
    15. // 当数据发生变化的时候更新视图
    16. Watcher.prototype.update = function () {
    17.     let newVal;
    18.     if (this.vm.hasOwnProperty(this.key)) {
    19.         newVal = this.vm[this.key];
    20.     } else {
    21.         const str = `this.vm.${this.key}`;
    22.         newVal = eval(str);
    23.     }
    24.     this.cb(newVal);
    25. }

    以上这几个构造函数就实现了我们所说的迷你版本,将它们整合成一个文件vuemini.js。在上面所提示的页面引入,查看效果。

    另外,我在data中绑定了一个html属性,值为一个'

    {{msg}}
    ',与之前完整版相比,图中的v-html下方的maomin文本也被渲染出来。

    尤大开发的Vue2.x迷你版本

    下面,我们将看下尤大开发的迷你版本,这个版本引入了Virtual DOM,但是主要是针对响应式式原理的,可以根据尤大的迷你版本与上面的版本作个比较,可以看下有哪些相似之处。

     
     
     
     
    1.     
    2.     
    3.     
    4.     vue2mini
    5.     
    6.