欢迎来到数字知识商城-小可爱商城
   |   学习记录   |    会员特权   |    登录   |    免费注册
数字知识商城-小可爱商城
热门搜索:   
vue 快速入门 系列
时间:2021-04-02 23:17:57 作者: 133983788 阅读:882

初步认识 vue

vue 是什么

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架

所谓渐进式,就是你可以一步一步、有阶段的使用 vue,不必一开始就使用所有的东西。

命令式框架 vs 声明式框架

jQuery 是命令式操作 DOM 的前端框架。比如点击一个新增按钮,需要出现一个录入信息的弹框,在 jQuery 中,我们需要在新增按钮被点击的时候,发出一个命令,让录入信息的弹框显示。随着交互越来越复杂,代码中会有相当一部分的代码是在操作 DOM,不好维护的问题也就出现了。

现在主流的框架 vue、angular 和 react 都是声明式操作 DOM 的框架。所谓声明式,就是我们只需要描述状态与 DOM 之间的映射关系,就可以将状态渲染成视图。状态到视图的转换,框架会帮我们做,不需要我们手动去操作 DOM。我们只需要关注状态的维护,而不用再关心 DOM 的操作。

hello-world

直接用 script 的方式引入 vue,开始我们的第一个例子。

// 新建 demo/1.html: <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <!-- 模板将会替换挂载的元素。最后显示的是 section,而不是 div -->
    <div id='app'></div>
    <script type="text/x-template" id="tpl">
        <section>
            {{ message }}
            <p v-if="seen">现在你看到我了</p>
        </section>
    </script>

    <script>
    var app = new Vue({
        el: '#app',
        // 模板。描述状态与 DOM 之间的关系。
        template: '#tpl',
        // 状态
        data: {
            message: 'Hello Vue!',
            seen: true
        }
    })
    </script>
</body>
</html>

Tip:笔者使用 anywhere 来启动一个服务器。通过 npm install anywhere --global 即可安装。在任意目录下执行 anywhere 就能启动一个服务,也可以指定端口启动 anywhere -p 8090。

进入 demo 目录,启动服务:

$ anywhere
Running at http://169.254.53.24:8000/ Also running at https://169.254.53.24:8001/ 通过浏览器请求 1.html 页面:http://169.254.53.24:8000/demo/1.html 页面显示:
Hello Vue!
现在你看到我了

如果在浏览器控制台下执行 app.seen = false,你会发现页面中的 现在你看到我了 不见了。

在这个例子中,明面上我们做的只有:在模板中描述状态与 DOM 之间的关系。背地里,vue 帮我们把状态渲染成视图,如果我们更改了状态(数据),视图会自动更新,无需我们操作 DOM。

vue 的开发模式

vue 框架的开发模式是多样化的。可以把 vue 当成 js 库来使用;也可以使用 .vue 单文件形式配合 webpack 使用,必要时还可以使用 vuex 来管理状态,vue-router 来管理路由。

侦测数据的变化 - [基本实现]

在 初步认识 vue 这篇文章的 hello-world 示例中,我们通过修改数据(app.seen = false),页面中的一行文本(现在你看到我了)就不见了。

这里涉及到 Vue 一个重要特性:响应式系统。数据模型只是普通的 JavaScript 对象,当我们修改时,视图会被更新。而变化侦测是响应式系统的核心。

Object的变化侦测

下面我们就来模拟侦测数据变化的逻辑。

强调一下我们要做的事情:数据变化,通知到外界(外界再做一些自己的逻辑处理,比如重新渲染视图)。

开始编码之前,我们首先得回答以下几个问题:

  1. 如何侦测对象的变化?
    • 使用 Object.defineProperty()。读数据的时候会触发 getter,修改数据会触发 setter。
    • 只有能侦测对象的变化,才能在数据发生变化的时候发出通知
  2. 当数据发生变化的时候,我们通知谁?
    • 通知用到数据的地方。而数据可以用在模板中,也可以用在 vm.$watch() 中,地方不同,行为也不相同,比如这里要渲染模板,那里要进行其他逻辑。所以干脆抽象出一个类。当数据变化的时候通知它,再由它去通知其他地方。
    • 这个类起名叫 Watcher。就是一个中介。
  3. 依赖谁?
    • 通知谁,就依赖谁,依赖 Watcher。
  4. 何时通知?
    • 修改数据的时候。也就是 setter 中通知
  5. 何时收集依赖?
    • 因为要通知用数据的地方。用数据就得读数据,我们就可以在读数据的时候收集,也就是在 getter 中收集
  6. 收集到哪里?
    • 可以在每个属性里面定义一个数组,与该属性有关的依赖都放里面

编码如下(可直接运行):

// 全局变量,用于存储依赖 let globalData = undefined; // 将数据转为响应式 function defineReactive (obj,key,val) { // 依赖列表 let dependList = [] Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { // 收集依赖(Watcher) globalData && dependList.push(globalData) return val
      }, set: function reactiveSetter (newVal) { if(val === newVal){ return } // 通知依赖项(Watcher) dependList.forEach(w => {
            w.update(newVal, val)
        })
        val = newVal
      }
    });
} // 依赖 class Watcher{ constructor(data, key, callback){ this.data = data; this.key = key; this.callback = callback; this.val = this.get();
    } // 这段代码可以将自己添加到依赖列表中 get(){ // 将依赖保存在 globalData globalData = this; // 读数据的时候收集依赖 let value = this.data[this.key]
        globalData = undefined return value;
    } // 数据改变时收到通知,然后再通知到外界 update(newVal, oldVal){ this.callback(newVal, oldVal)
    }
} /* 以下是测试代码 */ let data = {}; // 将 name 属性转为响应式 defineReactive(data, 'age', '88') // 当数据 age 改变时,会通知到 Watcher,再由 Watcher 通知到外界 new Watcher(data, 'age', (newVal, oldVal) => { console.log(`外界:newVal = ${newVal} ; oldVal = ${oldVal}`)
})

data.age -= 1 // 控制台输出: 外界:newVal = 87 ; oldVal = 88 

在控制台下继续执行 data.age -= 1,则会输出 外界:newVal = 86 ; oldVal = 87。

附上一张 Data、defineReactive、dependList、Watcher和外界的关系图。

首先通过 defineReactive() 方法将 data 转为响应式(defineReactive(data, 'age', '88'))。

外界通过 Watcher 读取数据(let value = this.data[this.key]),数据的 getter 则会被触发,于是通过 globalData 收集Watcher。

当数据被修改(data.age -= 1), 会触发 setter,会通知依赖(dependList),依赖则会通知 Watcher(w.update(newVal, val)),最后 Watcher 再通知给外界。

关于 Object 的问题

思考一下:上面的例子,继续执行 delete data.age 会通知到外界吗?

不会。因为不会触发 setter。请接着看:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
    <div id='app'>
        <section>
            {{ p1.name }}
            {{ p1.age }}
        </section>
    </div>
<script>
const app = new Vue({
    el: '#app',
    data: {
        p1: {
            name: 'ph',
            age: 18
        }
    }
})
</script>
</body>
</html>

运行后,页面会显示 ph 18。我们知道更改数据,视图会重新渲染,于是在控制台执行 delete app.p1.name,发现页面没有变化。这与上面示例中执行 delete data.age 一样,都不会触发setter,也就不会通知到外界。

为了解决这个问题,Vue提供了两个 API(稍后将介绍它们):vm.$set 和 vm.$delete。

如果你继续执行 app.$delete(app.p1, 'age'),你会发现页面没有任何信息了(name 属性已经用 delete 删除了,只是当时没有重新渲染而已)。

:如果这里执行 app.p1.sex = 'man',用到数据 p1 的地方也不会被通知到,这个问题可以通过 vm.$set 解决。

Array 的变化侦测

背景

假如数据是 let data = {a:1, b:[11, 22]},通过 Object.defineProperty 将其转为响应式之后,我们修改数据 data.a = 2,会通知到外界,这个好理解;同理 data.b = [11, 22, 33] 也会通知到外界,但如果换一种方式修改数据 b,就像这样 data.b.push(33),是不会通知到外界的,因为没走 setter。请看示例:

function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function () { console.log(`get val = ${val}`) return val
      }, set: function reactiveSetter (newVal) { if(val === newVal){ return } console.log(`set val = ${newVal}; oldVal = ${val}`)
        val = newVal
      }
    });
} // 以下是测试代码 {1} let data = {}
defineReactive(data, 'a', [11,22])
data.a.push(33) // get val = 11,22               (没有触发 setter)    {2}  data.a // get val = 11,22,33  data.a = 1 // set val = 1; oldVal = 11,22,33(触发 setter) 

通过 push() 方法改变数组的值,确实没有触发 setter(行{2}),也就不能通知外界。这里好像说明了一个问题:通过 Object.definePropery() 方法,只能将对象转为响应式,不能将数组转为响应式。

其实 Object.definePropery() 可以将数组转为响应式。请看示例:

// 继续上面的例子,将测试代码(行{1})改为: let data = []
defineReactive(data, '0', 11)
data[0] = 22 // set val = 22; oldVal = 11 data.push(33) // 不会触发                     {10} 

虽然 Object.definePropery() 可以将数组转为响应式,但通过 data.push(33)(行{10})这种方式修改数组,仍然不会通知到外界。

所以在 Vue 中,将数据转为响应式,用了两套方式:对象使用 Object.defineProperty();数组则使用另一套。

实现

es6 中可以用 Proxy 侦测数组的变化。请看示例:

let data = [11,22] let p = new Proxy(data, { set: function(target, prop, value, receiver) {
        target[prop] = value; console.log('property set: ' + prop + ' = ' + value); return true;
    }
    }) console.log(p)
p.push(33) /*
输出:
[ 11, 22 ]
property set: 2 = 33
property set: length = 3
*/ 

es6 以前就稍微麻烦点,可以使用拦截器。原理是:当我们执行 [].push() 时会调用数组原型(Array.prototype)中的方法。我们在 [].push() 和 Array.prototype 之间增加一个拦截器,以后调用 [].push() 时先执行拦截器中的 push() 方法,拦截器中的 push() 在调用 Array.prototype 中的 push() 方法。请看示例:

// 数组原型 let arrayPrototype = Array.prototype // 创建拦截器 let interceptor = Object.create(arrayPrototype) // 将拦截器与原始数组的方法关联起来 ;('push,pop,unshift,shift,splice,sort,reverse').split(',')
.forEach(method => { let origin = arrayPrototype[method]; Object.defineProperty(interceptor, method, { value: function(...args){ console.log(`拦截器: args = ${args}`) return origin.apply(this, args);
        }, enumerable: false, writable: true, configurable: true })
}); // 测试 let arr1 = ['a'] let arr2 = [10]
arr1.push('b') // 侦测数组 arr2 的变化 Object.setPrototypeOf(arr2, interceptor) // {20} arr2.push(11) // 拦截器: args = 11 arr2.unshift(22) // 拦截器: args = 22 

这个例子将能改变数组自身内容的 7 个方法都加入到了拦截器。如果需要侦测哪个数组的变化,就将该数组的原型指向拦截器(行{20})。当我们通过 push 等 7 个方法修改该数组时,则会在拦截器中触发,从而可以通知外界。

到这里,我们只完成了侦测数组变化的任务。

数据变化,通知到外界。上文编码的实现只是针对 Object 数据,而这里需要针对 Array 数据。

我们也来思考一下同样的问题:

  1. 如何侦测数组的变化?
    • 拦截器
  2. 当数据发生变化的时候,我们通知谁?
    • Watcher
  3. 依赖谁?
    • Watcher
  4. 何时通知?
    • 修改数据的时候。拦截器中通知。
  5. 何时收集依赖?
    • 因为要通知用数据的地方。用数据就得读数据。在读数据的时候收集。这和对象收集依赖是一样的。
    • {a: [11,22]} 比如我们要使用 a 数组,肯定得访问对象的属性 a。
  6. 收集到哪里?
    • 对象是在每个属性中收集依赖,但这里得考虑数组在拦截器中能触发依赖,位置可能得调整

就到这里,不在继续展开了。接下来的文章中,我会将 vue 中与数据侦测相关的源码摘出来,配合本文,简单分析一下。

关于 Array 的问题

// 需要自己引入 vue.js。后续也尽可能只罗列核心代码 <div id='app'> <section> {{ p1[0] }}
            {{ p1[1] }} </section> </div> <script> const app = new Vue({ el: '#app', data: { p1: ['ph', '18']
    }
}) </script> 

运行后在页面显示 ph 18,控制台执行 app.p1[0] = 'lj' 页面没反应,因为数组只有调用指定的 7 个方法才能通过拦截器通知外界。如果执行 app.$set(app.p1, 0, 'pm') 页面内容会变成 pm 18。

侦测数据的变化 - [vue 源码分析]

本文将 vue 中与数据侦测相关的源码摘了出来,配合上文(侦测数据的变化 - [基本实现]) 一起来分析一下 vue 是如何实现数据侦测的。

Tip: 以下代码出自 vue.esm.js,版本为 v2.5.20。无关代码有一些删减。中文注释都是笔者添加。

/**
   * Define a property.
   * 定义属性的方法
   */ function def (obj, key, val, enumerable) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true });
  } /**
   * Parse simple path.
   * 解析简单路径。比如 vm.$watch('a.b.c', function(){})
   */ var bailRE = /[^\w.$]/; function parsePath (path) { if (bailRE.test(path)) { return } var segments = path.split('.'); return function (obj) { // 例如 a.b.c for (var i = 0; i < segments.length; i++) { if (!obj) { return } // 最后读取到 c obj = obj[segments[i]];
      } return obj
    }
  } /**
   * A dep is an observable that can have multiple
   * directives subscribing to it.
   * 依赖。对我们的依赖列表 dependList 进行了封装,这里提取出来了一个类,用于存储依赖(Watcher)。
   */ var Dep = function Dep () { this.id = uid++; // subs 也就是我们的依赖列表 dependList this.subs = [];
  };

  Dep.prototype.addSub = function addSub (sub) { this.subs.push(sub);
  };

  Dep.prototype.removeSub = function removeSub (sub) {
    remove(this.subs, sub);
  }; // 收集依赖 Dep.prototype.depend = function depend () { // Dep.target 也就是我们的全局变量(globalData),指向 Watcher。 if (Dep.target) { // 收集依赖 Watcher Dep.target.addDep(this);
    }
  }; // 通知依赖 Dep.prototype.notify = function notify () { // stabilize the subscriber list first var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }; // the current target watcher being evaluated. // this is globally unique because there could be only one // watcher being evaluated at any time. // 类似我们的全局变量(globalData ),用于存储 Watcher Dep.target = null; var targetStack = []; function pushTarget (target) {
    targetStack.push(target);
    Dep.target = target;
  } function popTarget () {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
  } /*
   * not type checking this file because flow doesn't play well with
   * dynamically accessing methods on Array prototype
   */ // 接下来是侦测数组的变化 // 也就是通过拦截器来实现数组的侦测 var arrayProto = Array.prototype; // arrayMethods就是拦截器 var arrayMethods = Object.create(arrayProto); // 能改变数组的7个方法 var methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]; /**
   * Intercept mutating methods and emit events
   * 给拦截器(arrayMethods)定义以上7个方法
   */ methodsToPatch.forEach(function (method) { // cache original method // 数组的原始方法 var original = arrayProto[method];
    def(arrayMethods, method, function mutator () { var args = [], len = arguments.length; while ( len-- ) args[ len ] = arguments[ len ]; // 调用拦截器中的方法,拦截器接着会去调用数组中对应的方法 var result = original.apply(this, args); // 数据变成响应式后,数据上就会挂载 __ob__(Observer 的实例) 属性,里面有数据的依赖 var ob = this.__ob__; // 只有 push、unshift、splice 这三个方法能增加数据,而增加的数据也需要转为响应式 var inserted; switch (method) { case 'push': case 'unshift':
          inserted = args; break case 'splice':
          inserted = args.slice(2); break } // 数组增加的数据也需要转为响应式 if (inserted) { ob.observeArray(inserted); } // notify change // 通知依赖 ob.dep.notify(); return result
    });
  }); /**
   * Observer class that is attached to each observed
   * object. Once attached, the observer converts the target
   * object's property keys into getter/setters that
   * collect dependencies and dispatch updates.
   * 1. 将数据转为响应式的主入口。
   * 2. 在我们的实现中是通过 defineReactive() 将数据转为响应式,没有递归侦测所有的 key。比如 
   * data = {a: 1, b: {c:1}},我们只侦测了数据的第一层(data.a、data.b),孩子节点如果是对象,
   * 也需要侦测 data.b.c。
   * 3. 递归侦测调用顺序:Observer -> walk -> defineReactive$$1 -> observe -> Observer
   * 4. 将对象和数组分别处理。
   */ var Observer = function Observer (value) { this.value = value; // 定义依赖,用于存储于数据有关的依赖 // 比如数据 let data = {a: [11,22]},某处使用了 data.a。当执行 data.a.push(33) 时, // data.a 就应该通知其依赖 this.dep = new Dep(); this.vmCount = 0; // 将 this 挂载到数据的 __ob__ 属性上。Array 的拦截器就可以通过数据取得 Observer 的 dep,从而通知依赖 def(value, '__ob__', this); if (Array.isArray(value)) { // 如果有原型,就通过更改原型的方式将拦截器挂载到数组上,否则就将拦截器中的方法依次拷贝到数组上 if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      } // 数组中的每一项也需要转为响应式 this.observeArray(value);
    } else { // 依次遍历对象中每个 key,将其转为响应式 this.walk(value);
    }
  }; /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */ Observer.prototype.walk = function walk (obj) { var keys = Object.keys(obj); for (var i = 0; i < keys.length; i++) { // 通过 Object.defineProperty() 侦测对象 defineReactive$$1(obj, keys[i]);
    }
  }; /**
   * Observe a list of Array items.
   */ Observer.prototype.observeArray = function observeArray (items) { for (var i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }; /**
   * Augment a target Object or Array by intercepting
   * the prototype chain using __proto__
   * 通过更改原型来挂载拦截器,实现数组的侦测。
   */ function protoAugment (target, src) { // 作用与 setPrototype 相同 target.__proto__ = src;
  } // 将拦截器中的方法拷贝到数组中,实现数组的侦测。 function copyAugment (target, src, keys) { for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i];
      def(target, key, src[key]);
    }
  } /**
   * Attempt to create an observer instance for a value,
   * returns the new observer if successfully observed,
   * or the existing observer if the value already has one.
   * 观察数据。如果数据不是对象,直接返回;如果已经是响应式,则返回 Observer 的实例;否则将值转为响应式
   */ function observe (value, asRootData) { if (!isObject(value) || value instanceof VNode) { return } var ob; if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
    } else if (
      shouldObserve &&
      !isServerRendering() &&
      (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && // 不能是 vue 实例 !value._isVue
    ) {
      ob = new Observer(value);
    } if (asRootData && ob) {
      ob.vmCount++;
    } return ob
  } /**
   * Define a reactive property on an Object.
   * 侦测数据变化。功能与我们的 defineReactive() 方法类似。
   */ function defineReactive$$1 ( obj,
    key,
    val,
    customSetter,
    shallow ) { // 每个 key 都有一个 Dep 用于存储依赖 // dep 就是我们的依赖列表 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(); // 如果值(childOb)是对象,childOb也需要收集依赖 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 (customSetter) {
          customSetter();
        } // #7981: for accessor properties without setter if (getter && !setter) { return } if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        } // 新的值也需要转为响应式 childOb = !shallow && observe(newVal); // 通知依赖 dep.notify();
      }
    });
  } /**
   * A watcher parses an expression, collects dependencies,
   * and fires callback when the expression value changes.
   * This is used for both the $watch() api and directives.
   * Watcher 相对比较复杂,稍微分析一下
   */ var Watcher = function Watcher ( vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher ) { // vue 实例 this.vm = vm; if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this); // options if (options) { this.deep = !!options.deep; this.user = !!options.user; this.lazy = !!options.lazy; this.sync = !!options.sync; this.before = options.before;
    } else { this.deep = this.user = this.lazy = this.sync = false;
    } this.cb = cb; this.id = ++uid$1; // uid for batching this.active = true; this.dirty = this.lazy; // for lazy watchers this.deps = []; this.newDeps = []; this.depIds = new _Set(); this.newDepIds = new _Set(); this.expression = expOrFn.toString(); // parse expression for getter // expOrFn 可以是函数,也可以是表达式,例如 a.b.c,统一为 this.getter if (typeof expOrFn === 'function') { this.getter = expOrFn;
    } else { this.getter = parsePath(expOrFn); if (!this.getter) { this.getter = noop;
        warn( "Failed watching path: \"" + expOrFn + "\" " + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.',
          vm
        );
      }
    } // 通过 get() 方法读取数据 this.value = this.lazy
      ? undefined : this.get();
  }; /**
   * Evaluate the getter, and re-collect dependencies.
   * 
   */ Watcher.prototype.get = function get () { // 会将自己赋值给 Dep.target pushTarget(this); var value; var vm = this.vm; try { // 调用 Watcher 构造函数中分装的 getter() 方法 // 触发数据的 getter,从而收集依赖(Watcher) value = this.getter.call(vm, vm);
    } catch (e) { if (this.user) {
        handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
      } else { throw e
      }
    } finally { // "touch" every property so they are all tracked as // dependencies for deep watching if (this.deep) {
        traverse(value);
      }
      popTarget(); this.cleanupDeps();
    } return value
  }; /**
   * Add a dependency to this directive.
   */ Watcher.prototype.addDep = function addDep (dep) { var id = dep.id; if (!this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  }; /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */ Watcher.prototype.update = function update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true;
    } else if (this.sync) { this.run();
    } else {
      queueWatcher(this);
    }
  };

侦测数据的变化 - [vue api 原理]

前面(侦测数据的变化 - [基本实现])我们已经介绍了新增属性无法被侦测到,以及通过 delete 删除数据也不会通知外界,因此 vue 提供了 vm.$set() 和 vm.$delete() 来解决这个问题。

vm.$watch() 方法赋予我们监听实例上数据变化的能力。

下面依次对这三个方法的使用以及原理进行介绍。

Tip: 以下代码出自 vue.esm.js,版本为 v2.5.20。无关代码有一些删减。中文注释都是笔者添加。

vm.$set

这是全局 Vue.set 的别名。向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。

语法:

  • vm.$set( target, propertyName/index, value )

参数:

  • {Object | Array} target
  • {string | number} propertyName/index
  • {any} value

以下是相关源码:

Vue.prototype.$set = set; /**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */ function set (target, key, val) { if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(("Cannot set reactive property on undefined, null, or primitive value: " + 
    ((target))));
  } // 如果 target 是数组,并且 key 是一个有效的数组索引 if (Array.isArray(target) && isValidArrayIndex(key)) { // 如果传递的索引比数组长度的值大,则将其设置为 length target.length = Math.max(target.length, key); // 触发拦截器的行为,会自动将新增的 val 转为响应式 target.splice(key, 1, val); return val
  } // 如果 key 已经存在,说明这个 key 已经被侦测了,直接修改即可 if (key in target && !(key in Object.prototype)) {
    target[key] = val; return val
  } // 取得数据的 Observer 实例 var ob = (target).__ob__; // 处理文档中说的 ”注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象“ if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn( 'Avoid adding reactive properties to a Vue instance or its root $data ' + 'at runtime - declare it upfront in the data option.' ); return val
  } // 如果数据没有 __ob__,说明不是响应式的,也就不需要做任何特殊处理 if (!ob) {
    target[key] = val; return val
  } // 通过 defineReactive$$1() 方法在响应式数据上新增一个属性,该方法会将新增属性 // 转成 getter/setter defineReactive$$1(ob.value, key, val);
  ob.dep.notify(); return val
} /**
 * Check if val is a valid array index.
 * 检查 val 是否是一个有效的数组索引
 */ function isValidArrayIndex (val) { var n = parseFloat(String(val)); return n >= 0 && Math.floor(n) === n && isFinite(val)
}

vm.$delete

这是全局 Vue.delete 的别名。删除对象的 property。如果对象是响应式的,确保删除能触发更新视图。你应该很少会使用它。

语法:

  • Vue.delete( target, propertyName/index )

参数:

  • {Object | Array} target
  • {string | number} propertyName/index

实现思路与 vm.$set 类似。请看:

Vue.prototype.$delete = del; /**
 * Delete a property and trigger change if necessary.
 * 删除属性,并在必要时触发更改。
 */ function del (target, key) { if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(("Cannot delete reactive property on undefined, null, or primitive value: " + 
    ((target))));
  } // 如果 target 是数组,并且 key 是一个有效的数组索引 if (Array.isArray(target) && isValidArrayIndex(key)) { // 触发拦截器的行为 target.splice(key, 1); return } // 取得数据的 Observer 实例 var ob = (target).__ob__; // 处理文档中说的 ”注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象“ if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn( 'Avoid deleting properties on a Vue instance or its root $data ' + '- just set it to null.' ); return } // key 不是 target 自身属性,直接返回 if (!hasOwn(target, key)) { return } delete target[key]; // 不是响应式数据,终止程序 if (!ob) { return } // 通知依赖 ob.dep.notify();
}

vm.$watch

观察 Vue 实例上的一个表达式或者一个函数计算结果的变化。回调函数得到的参数为新值和旧值。表达式只接受简单的键路径。对于更复杂的表达式,用一个函数取代。

语法:

  • vm.$watch( expOrFn, callback, [options] )

参数:

  • {string | Function} expOrFn
  • {Function | Object} callback
  • {Object} [options]
    • {boolean} deep
    • {boolean} immediate

返回值:

  • {Function} unwatch

例如:

// 键路径 vm.$watch('a.b.c', function (newVal, oldVal) { // 做点什么 }) // 函数 vm.$watch( function () { return this.a + this.b
  }, function (newVal, oldVal) { // 做点什么 }
)

相关源码请看:

Vue.prototype.$watch = function ( expOrFn,
    cb,
    options ) { var vm = this; if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {};
    options.user = true; // 通过 Watcher() 来实现 vm.$watch 的基本功能 var watcher = new Watcher(vm, expOrFn, cb, options); // 在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调 if (options.immediate) { try {
        cb.call(vm, watcher.value);
      } catch (error) {
        handleError(error, vm, ("callback for immediate watcher \"" + 
        (watcher.expression) + "\""));
      }
    } // 返回一个函数,作用是取消观察 return function unwatchFn () {
      watcher.teardown();
    }
  }; /**
 * Remove self from all dependencies' subscriber list.
 * 取消观察。也就是从所有依赖(Dep)中把自己删除
 */ Watcher.prototype.teardown = function teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) {
      remove(this.vm._watchers, this);
    } // this.deps 中记录这收集了自己(Wtacher)的依赖 var i = this.deps.length; while (i--) { // 依赖中删除自己 this.deps[i].removeSub(this);
    } this.active = false;
  }
}; /**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */ var Watcher = function Watcher ( vm,
  expOrFn,
  cb,
  options,
  isRenderWatcher ) { this.vm = vm; if (isRenderWatcher) {
    vm._watcher = this;
  }
  vm._watchers.push(this); // options if (options) { // deep 监听对象内部值的变化 this.deep = !!options.deep; this.user = !!options.user; this.lazy = !!options.lazy; this.sync = !!options.sync; this.before = options.before;
  } else { this.deep = this.user = this.lazy = this.sync = false;
  } this.cb = cb; this.id = ++uid$1; // uid for batching this.active = true; this.dirty = this.lazy; // for lazy watchers // 存储依赖(Dep)。Watcher 可以通过 deps 得知自己被哪些 Dep 收集了。 // 可用于取消观察 this.deps = []; this.newDeps = []; this.depIds = new _Set(); this.newDepIds = new _Set(); this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString()
    : ''; // parse expression for getter // expOrFn可以是简单的键路径或函数。本质上都是读取数据的时候收集依赖, // 所以函数可以同时监听多个数据的变化 // 函数: vm.$watch(() => {return this.a + this.b},...) if (typeof expOrFn === 'function') { this.getter = expOrFn; // 键路径: vm.$watch('a.b.c',...) } else { // 返回一个读取键路径(a.b.c)的函数 this.getter = parsePath(expOrFn); if (!this.getter) { this.getter = noop;
      process.env.NODE_ENV !== 'production' && warn( "Failed watching path: \"" + expOrFn + "\" " + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.',
        vm
      );
    }
  } this.value = this.lazy
    ? undefined : this.get();
}; /**
 * Evaluate the getter, and re-collect dependencies.
 */ 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 { // "touch" every property so they are all tracked as // dependencies for deep watching // 对象内部的值发生变化,也需要通知依赖。 if (this.deep) { // 把当前值的子值都触发一遍收集依赖的逻辑即可 traverse(value);
    }
    popTarget(); this.cleanupDeps();
  } return value
}; /**
 * Recursively traverse an object to evoke all converted
 * getters, so that every nested property inside the object
 * is collected as a "deep" dependency.
 */ function traverse (val) {
  _traverse(val, seenObjects);
  seenObjects.clear();
} function _traverse (val, seen) { var i, keys; var isA = Array.isArray(val); // 不是数组和对象、已经被冻结,或者虚拟节点,直接返回 if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) { return } if (val.__ob__) { var depId = val.__ob__.dep.id; // 拿到 val 的 dep.id,防止重复收集依赖 if (seen.has(depId)) { return }
    seen.add(depId);
  } // 如果是数组,循环数组,将数组中的每一项递归调用 _traverse if (isA) {
    i = val.length; while (i--) { _traverse(val[i], seen); }
  } else {
    keys = Object.keys(val);
    i = keys.length; // 重点来了:读取数据(val[keys[i]])触发收集依赖的逻辑 while (i--) { _traverse(val[keys[i]], seen); }
  }
}

虚拟 DOM

什么是虚拟 dom

dom 是文档对象模型,以节点树的形式来表现文档。

虚拟 dom 不是真正意义上的 dom。而是一个 javascript 对象。

正常的 dom 节点在 html 中是这样表示:

<div class='testId'> <p>你好</p> <p>欢迎光临</p> </div>

而在虚拟 dom 中大概是这样:

{ tag: 'div', attributes:{ class: ['testId']
    }, children:[ // p 元素 // p 元素 ]
}

我们可以将虚拟 dom 拆分成两部分进行理解:虚拟 + dom。

  • 虚拟: 表示虚拟 dom 不是真正意义上的 dom,而是一个 javascript 对象;
  • dom: 表示虚拟 dom 能以类似节点树的形式表示文档。

虚拟 dom 的作用

前文(初步认识 vue)提到,现在主流的框架都是声明式操作 dom 的框架。我们只需要描述状态与 dom 之间的映射关系即可,状态到视图(真实的 dom)的转换,框架会帮我们做。

最粗暴的做法是将状态渲染成视图,每次更新状态,都重新更新整个视图。

这种做法的性能可想而知。比较好的想法是:状态改变,只更新与状态相关的 dom 节点。虚拟 dom 只是实现这个想法的其中一种方法而已。

具体做法:

  • 状态 -> 真实 dom(最初)
  • 状态 -> 虚拟 dom -> 真实 dom(使用虚拟 dom)

状态改变,重新生成一份虚拟 dom,将上一份和这一份虚拟 dom 进行对比,找出需要更新的部分,更新真实 dom。

vue 中的虚拟 dom

真实的 dom 是由 节点(Node)组成,虚拟 dom 则是由虚拟节点(vNode)组成。

虚拟 dom 在 vue 中主要做两件事:

  • 提供与真实节点(Node)对应的虚拟节点(vNode)
  • 将新的虚拟节点与旧的虚拟节点进行对比,找出需要差异,然后更新视图

“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼 —— vue 官网

vNode

什么是 vNode

上文提到,vNode(虚拟节点)对应的是真实节点(Node)。

vNode 可以理解成节点描述对象。描述了如何创建真实的 dom 节点。

vue.js 中有一个 vNode 类。可以使用它创建不同类型的 vNode 实例,不同类型的 vNode 对应着不同类型的 dom 元素。代码如下:

export default class VNode { constructor ( tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function ) { this.tag = tag this.data = data this.children = children this.text = text this.elm = elm this.ns = undefined this.context = context this.fnContext = undefined this.fnOptions = undefined this.fnScopeId = undefined this.key = data && data.key this.componentOptions = componentOptions this.componentInstance = undefined this.parent = undefined this.raw = false this.isStatic = false this.isRootInsert = true this.isComment = false this.isCloned = false this.isOnce = false this.asyncFactory = asyncFactory this.asyncMeta = undefined this.isAsyncPlaceholder = false }

  get child (): Component | void { return this.componentInstance
  }
}

从代码不难看出 vNode 类创建的实例,本质上就是一个普通的 javascript 对象。

vNode 的类型

前面我们已经介绍通过 vNode 类可以创建不同类型的 vNode。而不同类型的 vNode 是由有效属性区分。例如 isComment = true 表示注释节点;isCloned = true 表示克隆节点等等。

vNode 类型有:注释节点、文本节点、克隆节点、元素节点、组件节点。

以下是注释节点、文本节点和克隆节点的代码:

/*
注释节点
有效属性:{isComment: true, text: '注释节点'}
*/ export const createEmptyVNode = (text: string = '') => { const node = new VNode()
  node.text = text // 注释 node.isComment = true return node
} /*
文本节点
有效属性:{text: '文本节点'}
*/ export function createTextVNode (val: string | number) { return new VNode(undefined, undefined, undefined, String(val))
} // optimized shallow clone // used for static nodes and slot nodes because they may be reused across // 用于静态节点和插槽节点 // multiple renders, cloning them avoids errors when DOM manipulations rely // on their elm reference. // 克隆节点 export function cloneVNode (vnode: VNode): VNode { const cloned = new VNode(
    vnode.tag,
    vnode.data, // #7975 // clone children array to avoid mutating original in case of cloning // a child. vnode.children && vnode.children.slice(),
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta // 标记是克隆节点 cloned.isCloned = true return cloned
}

克隆节点其实就是将现有节点的所有属性赋值到新节点中,最后用 cloned.isCloned = true 标记自身是克隆节点。

元素节点通常有以下 4 个属性:

  • tag:节点名称。例如 div、p
  • data:节点上的数据。例如 class、style
  • children:子节点
  • context:在组件内呈现

组件节点与元素节点类似,包含两个独有的属性:

  • componentOptions:组件节点的选项参数,例如propsData、listeners、children、tag
  • componentInstance:组件的实例

patch

前面已经介绍了虚拟 dom 在 vue 中做的第一件事:提供与真实节点(Node)对应的虚拟节点(vNode);接下来介绍第二件事:将新的虚拟节点与旧的虚拟节点进行对比,找出需要差异,然后更新视图。

第二件事在 vue 中的实现叫做 patch,即打补丁、修补的意思。通过对比新旧 vNode,找出差异,然后在现有 dom 的基础上进行修补,从而实现视图更新。

对比 vNode 找差异是手段,更新视图才是目的。

而更新视图无非就是新增节点、删除节点和更新节点。接下来我们逐一分析什么时候新增节点、在哪里新增;什么时候删除节点,删除哪个;什么时候更新节点,更新哪个;

:当 vNode 与 oldVNode 不相同的时候,以 vNode 为准。

新增节点

一种情况是:vNode 存在而 oldVNode 不存在时,需要新增节点。最典型的是初次渲染,因为 odlVNode 是不存在的。

另一种情况是 vNode 与 oldVNode 完全不是同一个节点。这时就需要使用 vNode 生成真实的 dom 节点并插入到 oldVNode 指向的真实 dom 节点旁边。oldVNode 则是一个被废弃的节点。例如下面这种情况:

<div> <p v-if="type === 'A'"> 我是节点A </p> <span v-else-if="type === 'B'"> 我是与A完全不同的节点B </span> </div>

当 type 由 A 变为 B,节点就会从 p 变成 span,由于 vNode 与 oldVNode 完全不是同一个节点,所以需要新增节点。

删除节点

当节点只在 oldVNode 中存在时,直接将其删除即可。

更新节点

前面介绍了新增节点和删除节点的场景,发现它们有一个共同点:vNode 与 oldVNode 完全不相同。

但更常见的场景是 vNode 与 oldVNode 是同一个节点。然后我们需要对它们(vNode 与 oldVNode)进行一个更细致的对比,再对 oldVNode 对应的真实节点进行更新。

对于文本节点,逻辑自然简单。首先对比新旧 vNode,发现是同一个节点,然后将 oldVNode 对应的 dom 节点的文本改成 vNode 中的文本即可。但对于复杂的 vNode,比如界面中的一颗树组件,这个过程就会变得复杂。

新增节点 - 源码分析

思考一下:前面说到 vNode 的类型有:注释节点、文本节点、克隆节点、元素节点、组件节点。请问这几种类型都会被创建并插入到 dom 中吗?

答:只有注释节点、文本节点、元素节点。因为 html 只认识这几种。

由于只有上面三种节点类型,根据类型做响应的创建,然后插入对应的位置即可。

以元素节点为例,如果 vNode 有 tag 属性,则说明是元素节点。则调用 createElement 方法创建对应的节点,接下来就通过 appendChild 方法插入到指定父节点中。如果父元素已经在视图中,那么把元素插入到它下面将会自动渲染出来;如果 vNode 的 isComment 属性是 true,则表示注释节点;都不是则是文本节点;

通常元素里面会有子节点,所以这里涉及一个递归的过程,也就是将 vNode 中的 children 依次遍历,创建节点,然后插入到父节点(父节点也就是刚刚创建出的 dom 节点)中,一层一层的递归进行。

请看源码:

// 创建元素 function createElm ( vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index ) { if (isDef(vnode.elm) && isDef(ownerArray)) { // This vnode was used in a previous render! // now it's used as a new node, overwriting its elm would cause // potential patch errors down the road when it's used as an insertion // reference node. Instead, we clone the node on-demand before creating // associated DOM element for it. vnode = ownerArray[index] = cloneVNode(vnode);
  }

  vnode.isRootInsert = !nested; // for transition enter check if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } var data = vnode.data; var children = vnode.children; var tag = vnode.tag; // 有 tag 属性,表示是元素节点 if (isDef(tag)) {
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag) // 创建元素。nodeOps 涉及到跨平台 : nodeOps.createElement(tag, vnode);
    setScope(vnode); /* istanbul ignore if */ { // 递归创建子节点,并将子节点插入到父节点上 createChildren(vnode, children, insertedVnodeQueue); if (isDef(data)) {
        invokeCreateHooks(vnode, insertedVnodeQueue);
      } // 将 vnode 对应的元素插入到父元素中 insert(parentElm, vnode.elm, refElm);
    } // isComment 属性表示注释节点 } else if (isTrue(vnode.isComment)) {
    vnode.elm = nodeOps.createComment(vnode.text); // 插入父节点 insert(parentElm, vnode.elm, refElm); // 否则就是子节点 } else {
    vnode.elm = nodeOps.createTextNode(vnode.text); // 插入父节点 insert(parentElm, vnode.elm, refElm);
  }
} // 递归创建子节点,并将子节点插入到父节点上。vnode 表示父节点 function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { if (process.env.NODE_ENV !== 'production') {
      checkDuplicateKeys(children);
    } // 依次创建子节点,并将子节点插入到父节点中 for (var i = 0; i < children.length; ++i) {
      createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
    }
  } else if (isPrimitive(vnode.text)) {
    nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
  }
}

删除节点 - 源码分析

删除节点非常简单。直接看源码:

// 删除一组指定节点 function removeVnodes (parentElm, vnodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { var ch = vnodes[startIdx]; if (isDef(ch)) { if (isDef(ch.tag)) {
        removeAndInvokeRemoveHook(ch);
        invokeDestroyHook(ch);
      } else { // Text node // 删除个节点 removeNode(ch.elm);
      }
    }
  }
} // 删除单个节点 function removeNode (el) { var parent = nodeOps.parentNode(el); // element may have already been removed due to v-html / v-text if (isDef(parent)) { // nodeOps里封装了跨平台的方法 nodeOps.removeChild(parent, el);
  }
}

更新节点 - 源码分析

有些复杂,而且涉及子节点更新,本文就不展开。

正品保障
如实描述
专业配送
金牌服务
万千信赖
友情链接: 免费试用虚拟主机  小可爱网络  
Copyright © 2017-2023小可爱网络科技 版权所有 苏ICP备18063367号