vue知识汇总

Vue 知识汇总

1. 创建实例

通过Vue函数创建一个Vue实例.

const vm = new Vue({})

2. 数据

当创建了一个实例后,通过将数据传入实例的data属性中,会将数据进行劫持,实现响应式,也就是说当这些数据发生变化时,对应的视图也会发生改变。

const data = {
  a: 1
}
const vm = new Vue({
  data: data
})

console.log(vm.a === data.a);  // true

vm.a = 2;
console.log(data.a)            // 2

data.a = 3;
console.log(vm.a)              // 3

数据响应式源码解析

编写一个mini观察器

可能大部分人都已知道了Vue2.0是采用Object.defineProperty()这个API进行实现。
Object.defineProperty的使用

var data={}
Object.defineProperty(data,'name',{
  get:function(){
    return value
  },
  set:function(newValue){
    value=newValue
  }
})
//如果不为属性设置初始值,就会报错。所以在使用时需要为data中的属性设置‘’这个空的初始化值。
data.name='leo' 

在页面上进行响应:

<div id="app">
  <p>你好,<span id='name'>span>p>
div>
<script>
var obj = {};
// 数据劫持
Object.defineProperty(obj, "name", {
  get() {
    console.log('获取name')
    return document.querySelector('#name').innerHTML;
  },
  set(nick) {
    console.log('设置name')
    document.querySelector('#name').innerHTML = nick;
  }
});
console.log(obj.name);   
obj.name = "leo";
console.log(obj.name)
script>

理解了Object.defineProperty()的使用后,现在我们从0开始通过Object.defineProperty()编写一个mini观察器来理解Vue响应式的原理思路。

  1. getter和setter
    实现一个转换函数(或者说是拦截函数),对对象中的属性进行setget.
function observe(obj) {
  // 遍历data中的所有键
  Object.keys(obj).forEach(key => {
    // 获取键对应的值
    let value = obj[key];
    Object.defineProperty(obj, key, {
      get() {
        console.log(`${key}': `, value);
        return value;
      },
      set(newVal) {
        console.log(`set ${newVal} to ${key}`);
        value = newVal;
      }
    })
  })
}
  1. 依赖收集
    就是追踪当data改变时哪些代码应该执行。
    定义一个Dep类,至少应该包含depend(收集)和 notify(唤醒)这两个方法。
class Dep {
  constructor() {
    // 定义一个数组用于保存已经收集到更新方法
    this.subs = new Set();
  }
  depend(dep) { 
    if(dep) {
      this.subs.add(dep);
    }
  }
  notify() {
    // 执行所有收集到的更新方法
    this.subs.forEach(sub => sub());
  }
}

那么依赖是在什么时候收集的呢?
其实某段代码在调用data中的值的时候对这个代码块进行收集。
比如说我们在observe上下文中用到了data中的某个值,那么这时候收集到的代码块应该是整片代码:
修改dep.js中的更新方法:

// dep.js
class Dep {
  constructor() {
    // 定义一个数组用于保存已经收集到更新方法
    this.subs = new Set();
  }
  depend(dep) { 
    this.subs.add(dep);
  }
  notify() {
    // 执行所有收集到的更新方法
    this.subs.forEach(function(sub) {
      // 调用收集到的代码块中的update方法
      sub.update()
    });
  }
}
// observe.js
// 定义上下文
let context = this;

function observe(obj) {
  // 遍历data中的所有键
  Object.keys(obj).forEach(key => {
    // 获取键对应的值
    let value = obj[key];
    let dep = new Dep();
    Object.defineProperty(obj, key, {
      get() {
        // 收集,是哪块代码在进行收集
        dep.depend(context);
        console.log(`${key}': `, value);
        return value;
      },
      set(newVal) {
        if(value !== newVal) {
          console.log(`set ${newVal} to ${key}`);
          value = newVal;
          // 更新
          dep.notify();
        }
      }
    })
  })
}

// 定义数据
const obj = {
  count: 0
}
observe(obj);

// 在observe文件中调用了data中的值
console.log(obj.count);

// 更新方法
function update() {
  console.log("updated")
  console.log(obj.count);
}

// 更改值,此时就会触发更新
obj.count = 2;

为了更方便我们传入上下文,我们创建一个watcher来监听一下。

// watcher.js
class Watcher {
  constructor(key) {
    Dep.target = this;
    this.key = key;
  }
  update() {
    console.log(`属性${this.key}更新了`);
  }
}

更改一下observe的依赖收集信息:

// observe.js
function observe(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key];
    let dep = new Dep();
    Object.defineProperty(obj, key, {
      get() {
        // 更改
        Dep.target && dep.depend(Dep.target);
        console.log(`${key}': `, value);
        return value;
      },
      set(newVal) {
        if(value !== newVal) {
          console.log(`set ${newVal} to ${key}`);
          value = newVal;
          dep.notify();
        }
      }
    })
  })
}

const obj = {
  count: 0
}
observe(obj);

// 传入需要监听的代码块
new Watcher(this, "count");

// 触发收集
console.log(obj.count);

// 触发更新
obj.count = 2;

理解了大概的思路后我们就把上面这些代码完善一下,实现自己的vue响应式代码。

class Leo {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    // 进行响应化
    this.observe(this.$data);
  }

  observe(value) {
    // 对象类型
    if(!value || typeof value !== 'object') {
      return;
    }
    Object.keys(value).forEach(key => {
      // 响应化
      this.defineReactive(value, key, value[key]);
      
      // 执行代理
      this.proxyData(key);
    })
  }
  defineReactive(obj, key, val) {
    // 递归
    this.observe(val);

    const dep = new Dep();

    Object.defineProperty(obj, key, {
      get() {
        Dep.target && dep.addDep(Dep.target);
        return val;
      },
      set(newVal) {
        if(newVal === val) {
          return;
        }
        val = newVal;
        dep.notify();
      } 
    })
  }
  
  proxyData(key) {
    // 在实例上定义属性的话是需要this.$data.xxx的方法,我们采用defineProperty方式进行代理,使得能够通过this.xxx 进行处理
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key];
      },
      set(newVal) {
        this.$data[key] = newVal;
      }
    })
  }
}

class Dep {
  constructor() {
    this.deps = [];
  }
  addDep(dep) {
    this.deps.push(dep);
  }
  notify() {
    this.deps.forEach(dep => dep.update())
  }
}

class Watcher {
  constructor(key) {
    // console.log(context);
    Dep.target = this;
    this.key = key;
  }
  update() {
    console.log(`属性${this.key}更新了`);
  }
}

由于目前还没有实现编译部分代码,所以对于watcher的监听只能局限与实例里边,watcher创建代码如下:

class Leo {
  constructor(options) {
    // ...
    // 代码测试
    new Watcher(this, 'test');
    this.test
  }
  // ...
}

// 测试
const leo = new Leo({
  data: {
    test: "test"
  }
})
leo.test = 'change'

经过上面的代码,你应该对Vue的响应式有一定的理解了,现在我们将开始讲解Vue的响应式源码。

数据响应式源码

Vue一大特点是数据响应式,数据的变化会作用于UI而不用进行DOM操作。原理上来讲,是利用了JS语言特性Object.defineProperty(),通过定义对象属性setter方法拦截对象属性变更,从而将数值的变化转换为UI的变化。

具体实现是在Vue初始化时,会调用initState,它会初始化dataprops等,这里着重关注data初始化.

初始化数据源码目录:src/core/instance/state.js

核心代码便是initData:

function initData (vm: Component) { // 初始化数据
  let data = vm.$options.data // 获取data
  data = vm._data = typeof data === 'function' // data(){return {}} 这种情况跟 data:{} 这种
    ? getData(data, vm)
    : data || {}
  
  // ..
  
  // proxy data on instance // 将data代理到实例上
  proxy(vm, `_data`, key) // 代理,通过this.dataName就可以直接访问定义的数据,而不用通过this.$data.dataName
  // observe data
  
  // ..

  observe(data, true /* asRootData */) // 执行数据的响应化
}

core/observer/index.js
observe方法返回一个Observer实例

function observe (value: any, asRootData: ?boolean): Observer | void {  // 返回一个Observer实例,一个data对应这一个__ob__(有value,dep这两者个数相同)

  // ..

  ob = new Observer(value) // 新建一个Observer实例,通过将这个实例添加在value的__ob__属性中

  // ..

  return ob
}

core/observer/index.js
Observer对象根据数据类型执行对应的响应化操作

class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value // data本身
    // 最开始是data调用了一次
    this.dep = new Dep() // dependence 依赖收集,data中的每一个键都对应着一个dep

    // ..

    def(value, '__ob__', this) // 定义一个property 设置_ob__,它的值就是Observer实例
    if (Array.isArray(value)) { // data中可能具有object或者array类型的数据,需要进行不同的处理方式
      
      // ..

      // 循环遍历所有的value进行observe操作
      this.observeArray(value)
    } else {
      this.walk(value) // 如果不是array类型,就直接进行操作
    }
  }

  // 处理对象类型数据
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) { 
      defineReactive(obj, keys[i]) // 添加响应式处理
    }
  }

  // 遍历数组添加监听(data里边的数组数据)
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive定义对象属性的getter/settergetter负责添加依赖,setter负责通知更新.

// 定义响应式
function defineReactive {
  // 为每一个属性也添加依赖收集
  const dep = new Dep()

  // 获取对象描述符(用于判断是否可以配置)
  const property = Object.getOwnPropertyDescriptor(obj, key) // 获取键的属性描述符
  if (property && property.configurable === false) { // 如果是不可配置的属性,直接返回
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get // 获取键的getter
  const setter = property && property.set // 获取键的setter

  if ((!getter || setter) && arguments.length === 2) { // 如果键没有设置getter或者setter而且传入的参数只有两个(会设置其他非用户传进来的参数($attrs,$listeners等))
    val = obj[key] // 就直接保存这个键对应的值,进行下面的操作
  }

  // 判断是否观察子对象
  let childOb = !shallow && observe(val) // 判断是否具有子对象(返回undefined或者observer) 例如 data:{name:{lastName:'lau',firstName:'leo'}} // 给子对象也添加observer
  
  // 拦截获取
  Object.defineProperty(obj, key, { // 设置getter和setter
    enumerable: true,
    configurable: true,
    // 获取
    get: function reactiveGetter () { // 设置响应化获取
      const value = getter ? getter.call(obj) : val // 如果有配置getter,就绑定到data中,否则就直接输出结果
      if (Dep.target) { // watch实例
        dep.depend() // 给watcher添加dep
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
  
    // 拦截
    set: function reactiveSetter (newVal) {
      // 获取之前的值 且不为 NaN
      const value = getter ? getter.call(obj) : val

      // .. 判断新值

      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果存在子对象,且非浅监听,直接观察
      childOb = !shallow && observe(newVal)

      // 触发更新
      dep.notify()
    }
  })
}

core/observer/dep.js
Dep负责管理一组Watcher,包括watcher实例的增删及通知更新。

class Dep { // 依赖,管理watcher
  static target: ?Watcher;  // target就是watcher实例
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) { // 添加订阅者,有多个访问了data中的某个属性
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) { // 删除订阅者
    remove(this.subs, sub)
  }

  depend () { // watcher中添加dep实例
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {

    // stabilize the subscriber list
    // ..

    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // 通知更新
    }
  }
}

Watcher

Watcher解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API和指令中。
每个组件也会有对应的Watcher,数值变化会触发其update函数导致重新渲染。

class Watcher {
  constructor () {}
  // 重新收集依赖
  get () {
    pushTarget(this) // 将Dep.target设置成当前的watcher实例,将当前的watcher添加进watcher队列中
  }
  // 给当前组件的watcher添加依赖
  addDep (dep: Dep) {
    // ..
    
    // 添加watcher实例
    dep.addSub(this)
  }
  // 清除数据
  cleanupDeps () {}

  // 更新 懒更新和同步
  update () { // 通知更新
    
    // ..
    // 同步执行更新渲染
    this.run()

    // ..
    // 异步就添加到watcher队列之后统一更新
  }

  // 执行更新
  run() {}

以上就是一些数据响应式相关的源码,在使用Vue时,数组是特别注意的。

数组响应化
数组数据变化的侦测跟对象不同,我们操作数组通常使用pushpopsplice等方法,此时没有办法得知数据变化。所以vue中采取的策略是拦截这些方法并通知dep

可以用之前的observe方法来验证一下数组的变化其实是不会触发更新。

让我们来看看vue中是如何处理数组的。

src/core/observer/array.js
为数组原型中的7个可以改变内容的方法定义拦截器。

const methodsToPatch = [ // 定义拦截器
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse' 
];

// 重写以上方法并添加相应的处理
methodsToPatch.forEach(function (method) {
  // 获取原始方法
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let 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
  })
})

src/core/observer/index.js
Observer类中覆盖数组原型

if (Array.isArray(value)) { // data中可能具有object或者array类型的数据,需要进行不同的处理方式
  protoAugment(value, arrayMethods)
  // 循环遍历所有的value进行observe操作
  this.observeArray(value)
}

// 改变目标的_proto__指向,使其指向添加了拦截器的Array原型对象上
function protoAugment (target, src) {
  target.__proto__ = src
}

// 观察数组
function observeArray(items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

原型链扩展

上方设计到原型链的一些知识,简单介绍一下原型链,如果已经熟悉原型链,可以跳过。

  1. 原型

原型对象:在声明了一个函数之后,浏览器会自动按照一定的规则创建一个对象,这个对象就叫做原型对象。这个原型对象其实是储存在了内存当中。

在js中,每个构造函数内部都有一个prototype属性,该属性的值是个对象(原型对象),该对象包含了该构造函数所有实例共享的属性和方法。当我们通过构造函数创建对象的时候,在这个对象中有一个指针(__proto__),这个指针指向构造函数的prototype的值,我们将这个指向prototype的指针称为原型。或者用另一种简单却难理解的说法是:js中的对象都有一个特殊的[[Prototype]]内置属性,其实这就是原型。

Object来举一个例子:

Object.prototype指向图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VphNkR0d-1645337381069)(./images/prototype指向图.png)]

Object.prototype对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N0RaZv0t-1645337381072)(./images/Object.prototype对象图.png)]

使用Object构造函数来创建一个对象:

var obj = new Object();

obj就是构造函数Object创造出的对象,它不存在prototype属性,但是obj却有一个__proto__属性,这个属性指向了构造函数Object的原型对象(也就是说,obj.__proto__ === Object.prototype)。每个原型对象都有constructor属性,指向了它的构造函数(也就是说,Object.prototype.constructor === Object
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pq1wnSu2-1645337381073)(./images/__proto__指向图.png)]

总结:

  • 所有引用类型都有一个__proto__(隐式原型)属性,属性值是一个普通的对象
  • 所有的函数,都有一个 prototype(显式原型)属性,属性值也是一个普通的对象
  • 所有的引用类型(数组、对象、函数),__proto__(隐式原型)属性值指向它的构造函数的prototype属性值。
  1. 原型链
    当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。

3. 模板语法

Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML,所以能被遵循规范的浏览器和 HTML 解析器解析。

文本

数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值:

<span>Message: {{ msg }}span>

Mustache 标签将会被替代为对应数据对象上 msg 的值。无论何时,绑定的数据对象上 msg 发生了改变,插值处的内容都会更新。

通过使用 v-once 指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定:

<span v-once>这个将不会改变: {{ msg }}span>

原始HTML

双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用 v-html 指令:

<p>Using mustaches: {{ rawHtml }}p>
<p>Using v-html directive: <span v-html="rawHtml">span>p>

attribute

Mustache 语法不能作用在 HTML attribute 上,遇到这种情况应该使用 v-bind 指令

<div v-bind:id="dynamicId">div>

对于布尔 attribute (它们只要存在就意味着值为 true),v-bind 工作起来略有不同,在这个例子中:

<button v-bind:disabled="isButtonDisabled">Buttonbutton>

如果 isButtonDisabled 的值是 nullundefinedfalse,则 disabled属性甚至不会被包含在渲染出来的