vue2

224 阅读8分钟

一、vue的响应式原理

首先要了解vue中的三个核心类

Observer、Dep、Watcher

1、Observer:给对象添加getter和setter,用于依赖收集和派发更新;

2、Dep:用于收集当前响应式对象的依赖关系,每个响应式对象都有一个dep实例。dep.subs = watcher[];dep.subs是watcher实例的数组,当数据有变更时,会通过dep.notify()通知各个watcher。

3、Watcher:观察者对象,实例分为render watcher(渲染)、computed watcher(计算属性)、user watcher(侦听器)三种。

依赖收集:

initState时,对computed属性初始化时,触发computed watcher依赖收集;

initState时,对侦听属性初始化时,触发user watcher依赖收集;

render() 的过程中,触发render watcher依赖收集;

re-render时,vm.render() 再次执行,会移除所有subs中的watcher的订阅,重新赋值。

派发更新:

1、组件中对响应式的数据进行了修改,触发setter的逻辑;

2、调用dep.notify();

3、遍历所有subs(Watcher实例),调用每一个watcher的update方法;

总结:

当创建vue实例时,vue会遍历data选项中的属性,利用Object.defineProperty为其添加getter和setter对数据的读取进行劫持(getter用来依赖收集,setter用来派发更新),每个组件实例都有对应的watcher实例,并且在内部追踪依赖,在属性被访问和修改时通知变化。

二、双向绑定原理

首先,解释下双向绑定:

所谓的双向绑定建立在MVVM的模型基础上的:

  • 数据层 Model:应用的数据以及业务逻辑
  • 视图层 View:应用的展示效果,各类的UI组件等
  • 业务逻辑层 ViewModel: 负责将数据和视图关联起来

1. 数据变化后更新视图

2. 视图变化后更新数据

主要包含两个主要的组成部分

  1. 监听器 Observer:对所有的数据属性进行监听
  2. 解析器 Compiler:对每个元素节点的指令进行扫描和解析,根据指令替换数据,绑定对应的更新函数

具体实现原理

  1. new Vue() 实例的过程中,执行初始化。对data通过Object.defineProperty进行响应化处理,这个过程发生在Observer中,每个key都会有一个dep实例来存储watcher实例数组。
  2. 对模板进行编译时,v- 开头的关键词作为指令解析,找到动态绑定的数据,从data中获取数据并初始化视图,这个过程发生在 Compiler 里。如果遇到了 v-model,就监听input事件,更新data对应的数值。
  3. 在解析指令的过程中,会定义一个更新函数和Watcher,之后对应的数据变化时 Watcher 会调用更新函数。new Watcher 的过程中会去读取data的key,触发getter的依赖收集,将对应的watcher添加到dep里。
  4. 将来data中数据一旦发生变化,会首先找到对应的dep,通知所有的watcher执行更新函数。

来简单实现一个响应式函数?对一个对象内的所有key添加响应式的特性?

const render = (key,val) => {
    console.log(`SET key=${key} value=${val}`);
}
// 对对象的每个key做处理
const defineReactive = (obj,key,val) => {
    reactive(val); // 递归
    Object.defineProperty(obj,key,{
        get(){
            return val;
        },
        set(newVal){
            if(val === newVal){
                // 模拟 diff data
                return;
            }
            val = newVal;
            render(key,val);
        }
    })}
// 对对象做处理
const reactive = (obj) => {
    // 可以作为一个递归的终止条件
    if(typeof obj === 'object'){
        for(const key in obj){
            defineReactive(obj,key,obj[key]) 
       }
    }}
const data = {
    a: 1,
    b: 2,
    c: {
        c1: {
            af: 999
        },
        c2: 4
    }}
reactive(data);
data.a = 5  // SET
 key=a val=5data.b = 7  // SET
 key=b val = 7data.c.c2 = 4
 data.c.c1.af = 121 // SET
 key=af val = 121

那Vue中对于数组类型是怎么处理的?能简单模拟下对数组算法的监听吗?

const render = (action, ...args) => {
    console.log(`Action = ${action}, args = ${args.join(',')}`);
};
const arrPrototype = Array.prototype; // 保存数组的原型
const newArrProtoType = Object.create(arrPrototype); // 创建一个新的数组原型
['push','pop','shift','unshift','sort','splice','reverse'].forEach(methodName => {
    newArrProtoType[methodName] = function(){
        // 执行原有数组的方法
        arrPrototype[methodName].call(this,...arguments) // 改变this指向
        // 触发渲染
        render(methodName,...arguments);
    }});
const reactive = (obj) => {
    if(Array.isArray(obj)){
        // 把新定义的原型对象指向obj.__proto__
        obj.__proto__ = newArrProtoType;
    }}
const data = [1,2,3,4];
reactive(data);
data.push(5); // Action = push, args = 5
data.splice(0,2); // Action = splice , args = 0,2

三、计算属性的实现原理

上⾯提到的watcher实例, 就有⼀个叫做computed watcher的东⻄, 这个就是计算属性的

watcher。

computed watcher 持有⼀个 dep 实例, 通过 this.dirty 属性标记计算属性是否需要重新求值。

当computed的依赖值改变时, 就会通知订阅的watcher进⾏更新,对于computed的watcher会

将dirty设置为true并且进⾏计算属性⽅法的调⽤. 

1、computed所谓的缓存是指什么?

计算属性是基于它们的响应式依赖进⾏缓存的。只在相关响应式依赖发⽣改变时它们才会重新

求值.

2、那computed缓存存在的意义是什么? 

⽐如computed内的操作⾮常耗时, 可能是遍历⼀个⼤数组. 计算⼀次可能要耗时1s, 那么当后

续再通过计算属性获取的时候, 如果依赖的值没有变化, 就⽆需重新计算⼀遍了.

3、以下情况, computed可以监听到数据的变化吗?

      computed: {
        storageMsg(){
          return sessionStorage.getItem('xxx')
        },
        timer(){
          return Date.now()
        }
      }

答案是不能。因为computed计算的是响应式的数据,这个并没有给它添加defineReactive属性,当直接去修改这些数据时,computed是不会重新计算的。

四、Vue.nextTick的原理 

Vue是异步执⾏dom更新的,⼀旦观察到数据变化,Vue就会开启⼀个异步队列,然后把在同

⼀个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个

watcher被触发多次,只会被推送到队列⼀次。

这种缓冲⾏为可以有效的去掉重复数据造成的不必要的计算和DOm操作。⽽在下⼀个事件循

环时,Vue会清空队列,并进⾏必要的DOM更新。

⽽vue内部这个异步队列是怎么开启的? 这⾥有⼀个优先级, Promise.then >

MutationObserver > setImmediate > setTimeout 

所以可以理解为, nextTick会优先尝试使⽤微任务, 如果浏览器不⽀持, 就⽤宏任务.

当你设置 vm.someData = ‘new value’,DOM 并不会⻢上更新,⽽是在异步队列被清除,也

就是下⼀个事件循环开始时执⾏更新时才会进⾏必要的DOM更新.

所以nextTick的回调是在下⼀轮事件循环⾥执⾏的.

⼀般在什么时候⽤到nextTick呢?

1、在数据变化后要执⾏的某个操作,⽽这个操作需要使⽤随数据改变⽽改变的DOM结构的时候,这个操作都应该放进Vue.nextTick()的回调函数中 

  <template>
    <div v-if="loaded" ref="test"></div>
  </template>
 
  async showDiv(){
    this.loaded = true;
    await Vue.nextTick();
    this.$ref.test.xxxxxxxx
  }

尝试手写一个简单的Vue,实现响应式更新,详情见 

github:  github.com/Luna988/kvu…

简述下思路:

1、目录,核心文件

index.html

vue.js   Vue主文件

compile.js编译模版,解析指令

dep.js 收集依赖关系,存储观察者,发布订阅

observer.js 数据劫持

watcher 观察者对象

2、index.html内容

首先要有一个根元素

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue</title>
</head>
<body>
  <div id="yun">
    <h1>template</h1>
    <p>{{ msg }}</p>
    <h2>{{count}}</h2>
    <br />
    <h1>v-text</h1>
    <div v-text="msg"></div>
    <br>
    <h1>v-model</h1>
    <div v-html="testHtml"></div>
    <br>
    <h1>v-model</h1>
    <input type="text" v-model="msg">
    <input type="text" v-model="count">
    <button v-on:click="handleChange">按钮</button>
  </div>
  <script src="./yunVue.js"></script>
  <script>
    const app = new yunVue({
      el: '#yun',
      data: {
        msg: '面向工资编程!',
        count: 100,
        testHtml: '<ul><li>渲染!!</li></ul>'
      },
      methods: {
        handleChange(){
          console.log(this.msg + 'msg33333')
        }
      }
    })
  </script>
</body>
</html>

3、kvue.js文件

主要结构

class yunVue{}
class Dep{}
class Observer{}
class Watcher{}
class Compile{}

3-1、先初始化Vue class

Vue的类就在vue.js文件里实现,包含构造函数,接收配置等等。

先实现一个constructor,接收传入的数据并存储下来。内部变量这里使用$命名,便于区分.

class yunVue{
  constructor(options = {}){
    // 存储options, data, methods
    this.$options = options;
    this.$data = options.data;
    this.$methods = options.methods;
    this.initRootElement(options);    // 利⽤Object.defineProperty将data⾥的属性注⼊到vue实例中
    this._proxyData(this.$data);
    new Observer(this.$data);
    new Compiler(this);
  }
  // 获取根元素,并存储到Vue实例,
  initRootElement(options){
    if(typeof options.el === 'string'){      // 传⼊的是元素id或者class
      this.$el = document.querySelector(options.el);
    }else if(options.el instanceof HTMLElement){
      this.$el = options.el;
    }
    if(!this.$el){
      throw new Error('传入的el不合法,请传入css selector或者Element!')
    }
  }
  _proxyData(data){    // 遍历所有data
    Object.keys(data).forEach(key => {      // 将data属性注入到vue中
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get(){
          return data[key]
        },
        set(newValue){
          if(data[key] === newValue){
            return
          }
          data[key] = newValue
        }
      })
    })
  }}

3-2、核心类:Dep

class Dep{
  constructor(){
    // 存储所有的观察者
    this.subs = []
  }
  // 添加观察者
  addSub(watcher){
    if(watcher && watcher.update){
      this.subs.push(watcher)
    }
  }
  // 发送通知
  notify(){
    this.subs.forEach(watcher => {
      watcher.update()
    })
  }}

addSub,如果watcher没有update方法,那就没必要添加到sub里了;

notify,提供给外界调用的,数据变更时,外界调用notify去通知各个watcher,即执行watcher.update()

dep在哪里实例化?因为触发setter时需要通知所有的watcher更新,那么就要在Observer的defineReactive里面去实例化;

3-3、核心类:Observer

数据被获取的时候去收集依赖

class Observer{
  constructor(data){
    this.traverse(data)
  }
  // 递归遍历data里的所有属性
  traverse(data){
    if (!data || typeof data !== 'object') {
      return
    }
    Object.keys(data).forEach(key => {
      this.defineReactive(data, key, data[key])
    })
  }
  // 给传入的数据设置getter/setter
  defineReactive(obj,key,val){
    const that = this
    this.traverse(val) // 递归设置
    const dep = new Dep() // 负责依赖收集,并发送依赖
    Object.defineProperty(obj,key,{
      configurable: true,
      enumerable: true,
      get(){
        Dep.target && dep.addSub(Dep.target) // 收集依赖
        return val;
      },
      set(newValue){
        if(newValue === val){
          return
        }
        val = newValue
        that.traverse(newValue) // newValue可能是个对象
        dep.notify() // 通知watcher数据更新了
      }
    })
  }}

3-4、核心类:Watcher

class Watcher{
  // vm: vue实例
  // key:data中的属性名
  // cb:负责更新视图的回调函数
  constructor(vm,key,cb){
    this.vm = vm;
    this.key = key;
    this.cb = cb;
    // 把watcher对象记录到Dep类的静态属性target
    Dep.target = this
    // 触发get方法,在get方法中会调用addSub
    this.oldValue = vm[key]
    Dep.target = null;
  }
  // 当数据发生变化的时候更新视图
  update(){
    let newValue = this.vm[this.key]
    if(this.oldValue === newValue) {
      return
    }
    this.cb(newValue)
  }}

3-5、核心类Compiler 模版编译

class Compiler{
  constructor(vm){
    this.el = vm.$el
    this.vm = vm
    this.methods = vm.$methods
    this.compile(vm.$el)
  }
  // 编译模版  
compile(el){
    let childNodes = el.childNodes
    Array.from(childNodes).forEach(node => {
      if(this.isTextNode(node)){ // 处理文本节点
          this.compileText(node)
      }else if(this.isElementNode(node)){ // 处理元素节点
        this.compileElement(node)
      }
      // 如果还是有子节点,递归调用
      if(node.childNodes && node.childNodes.length > 0){
        this.compile(node)
      }
    })
  }
  // 编译元素节点,处理指令  
compileElement(node){
    if(node.attributes.length){
      Array.from(node.attributes).forEach(attr => {
        let attrName = attr.name
        if(this.isDirective(attrName)){ // 判断是否是指令
          attrName = attrName.indexOf(':') > -1 ? attrName.substr(5) : attrName.substr(2) // 获取v-后面的值
          let key = attr.value // 获取data名称
          this.update(node,key,attrName)
        }
      })
    }
  }
  // 更新  
update(node,key,attrName){
    const updateFn = this[attrName + 'Updater']
    updateFn && updateFn.call(this,node,this.vm[key],key,attrName)
  }
  // 解析 v-text  
textUpdater(node,value,key){
    node.textContent = value
    new Watcher(this.vm, key, (newValue) => {
      // 创建watcher对象,当数据改变更新视图
        node.textContent = newValue
    })
  }
  // 解析 v-model  
modelUpdater(node,value,key){
    node.value = value
    new Watcher(this.vm, key, (newValue) => {
      // 创建watcher对象,当数据改变更新视图
        node.value = newValue
    })
    // 双向绑定    
node.addEventListener('input', () => {
      this.vm[key] = node.value
    })
  }
  // 解析v-html  
htmlUpdater(node,value,key){
    node.innerHTML = value
    new Watcher(this.vm,key,newValue => {
      node.textContent = newValue
    })
  }
  // 解析v-on:click  
clickUpdater(node,value,key,attrName){
    node.addEventListener(attrName,this.methods[key])
  }
  // 编译文本节点,处理插值表达式,{{  }}  
compileText(node){
    // 获取 {{  }} 中的值
    let reg = /\{\{(.+?)\}\}/
    let value = node.textContent
    if(reg.test(value)){
      let key = RegExp.$1.trim() // 返回匹配到的第一个字符串,去掉空格
      node.textContent = value.replace(reg,this.vm[key])
      new Watcher(this.vm, key, (newValue) => {
        // 创建watcher对象,当数据改变更新视图
        node.textContent = newValue
      })
    }
  }
  // 判断是否是文本节点  
isTextNode(node){
    return node.nodeType === 3
  }
  // 判断是否是元素节点  
isElementNode(node){
    return node.nodeType === 1
  }
  // 判断元素属性是否是指令
  isDirective(attrName){
    return attrName.startsWith('v-')
  }}

想一下,这些方法之间如何相互调用的,vue初始化过程都应该做些什么?

初始化时,this._proxyData(this.data)data里的属性注入到vue实例中;实例化observer,即newObserver(this.data)将data里的属性注入到vue实例中;实例化observer,即new Observer(this.data);实例化编译对象,解析模版,如差值表达式,指令解析等。new Compiler(this);