Vue底层-待更新整理

34 阅读7分钟

Vue解决了什么问题

  • vue用虚拟DOM解决了JS直接操作DOM的弊端。
  • vue很好的处理了业务分层。

框架对比

比较内容备注
jQuery库
2006年诞生
解决了浏览器兼容问题
本质是封装了DOM操作,通过JS直接操作DOM
没有解决业务层混乱的问题,而且大片操作DOM时,浏览器会出现白屏和闪烁的情况,需长时等待才渲染完成
Vue.js虚拟DOM Vue只操作数据,不直接操作DOM 且数据和视图是分开的
很好的处理了业务分层
核心是数据驱动视图(响应式原理)+组件化
优点是:渐进式,组件化,轻量级,虚拟dom,响应式,单页面路由,数据与视图分开;
缺点是单页面不利于seo,首屏加载时间长;
React.js数据变化需要手动调用 api
setState方法封装了视图的更新
小程序数据变化需调用 api
setData方法封装了视图的更新

数据-视图变化模式

模式内容备注
命令式改变变量值、innerHTMLJS原生
MVVM模式数据驱动视图数据变化,视图会自动变化

vue 分层式架构

标题内容备注
最底层ES5构造函数Vue-
原型上定义的方法_init、$watch、_render-
全局APIset、nextTick、use在构造函数自身定义的
跨平台和服务端渲染及编译器--

vue 源码结构及功能

标题功能备注
flow定义和检测类型vue3替换为ts
src/compilertemplate模板编译为render函数vue-loader编译
(完整版自带)
src/observevue检测数据数据变化改变视图
src/vdomrender函数转为vnode
从而patch为真实dom以及diff算法实现

Vue 执行流程

image.png

上图为vue在初始渲染过程中的主干流程:先对选项对象初始化,通过Object.defineProperty建立一套响应式系统,然后将模板解析成render函数,再使用render函数生成虚拟节点vnode,在渲染前,对vnode进行diff操作,最后进行必要的渲染。具体每一步都做了什么:

初始化

// Vue构造函数
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 执行初始化逻辑
  this._init(options)
}

从Vue构造函数中得知,当执行new Vue()时,只执行了一个_init方法。_init会根据传入的选项对vue进行初始化。 props、data、生命周期,事件机制的初始化都是在此过程中完成的。以data的初始化为例,vue会通过 Object.defineProperty 的方式将data的属性定义到vue实例上。这也就解释了为什么我们可以在vue中通过对 this.msg 进行赋值,可以修改data中属性的值了。

响应式处理

为什么数据驱动视图,即数据变化,视图也随之变化呢?

image.png

如何对数据进行响应式观测,核心就是通过Object.defineProperty对数据进行劫持,在getter中收集依赖,setter中派发依赖,完整的响应式原理,如修改数据后视图是如何更新视图的还需要结合Dep和Watcher来看。

响应式原理用到的函数、方法

主题内容作用备注
Object.defineProperty()方法JS引擎的功能。在对象上定义新属性/隐藏属性检测对象属性变化
实现对象的响应式
数据响应式原理的核心
defineReactive函数封装Object.defineProperty()方法
利用闭包特性来实现数据劫持
实现对象的响应式适用于简单结构的对象
Observer递归侦测对象全部属性检测对象属性变化
逐层遍历属性,逐层defineReactiveObserve()
适用于复杂嵌套结构的对象:
把嵌套结构对象转换为每个层级的属性都是响应式
Object.setPrototypeOf方法新数组的原型指向数组备份arrayMethods实现数组的响应式触发新的数组方法被调用
Dep封装了依赖收集的代码管理依赖每个Observer实例的成员中都有一个Dep的实例
Watcher数据发生变化时通过Watcher中转中转并通知组件

Object.defineProperty()

Object.defineProperty是JS引擎的功能,用于检测对象属性变化,以完成数据劫持和数据代理。该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

var obj = {};
// defineProperty 定义 obj 对象的某个属性的值,主要是定义隐藏属性
// defineProperty方法定义属性 与 obj.a = 3 的区别在于
Object.defineProperty(obj, 'a', {
  value: 3writable:false  // 属性是否可写,false时不可改变
  // getter
  get: function(){  // ES6 可省略写为 get(){}
    console.log('访问obj的a属性')
  },
  // setter
  set(){
    console.log('改变obj的a属性')
  }
});
Object.defineProperty(obj, 'b', {
  value: 5,
  enumerable:false // 属性是否被枚举,false时不可遍历
});

console.log(obj);    // {a:3, b:5}
console.log(obj.a);  // 访问obj的a属性,也称为数据劫持
obj.a ++;            // 改变obj的a属性

defineReactive 函数

基于Object.defineProperty()并不好用,其gettersetter函数需要一个变量周转,才能正常工作,所以用defineReactive函数封装Object.defineProperty(),利用闭包特性来实现数据劫持:

// defineReactive.js
var obj = {};

function defineReactive(data, key, val){
    Object.defineProperty(data, key, {
        writable:true,    // 可读写
        enumerable:true,   // 可枚举
        get(){
          console.log('访问 obj 的' + key + '属性');
          return val;
        },
        set(newValue){
          console.log('改变 obj 的' + key + '属性', newValue)
          if(val === newValue){return;}
          val = newValue;
        }
    });
}

defineReactive(obj, 'a', 10); // 访问 obj 的 a 属性


obj.b = 66;
console.log(obj.b);   // 改变 obj 的 b 属性 66

Observer

对于对象内的嵌套结构,需要用循环递归的方法侦测。具体是通过创建observer类,实现对任何嵌套结构的对象obj2,转换为每个层级的属性都是响应式的,可以被侦测的obj

具体的实现思路是:

image.png

  1. observe(obj2)
  2. obj2身上有没有__ob__属性
  3. 若没有2中属性,则 new Observer(),将产生的实例添加__ob__
  4. 遍历下一层属性,逐个defineReactive
  5. 当设置某个属性值的时候,会触发set,里面有newValue,也被observe()一下
// utils.js  遍历工具函数
export const def = function(obj, key, value, enumerable){
    Object.defineProperty(obj, key, {
        value,
        enumerable,
        writable:true,
        configurable:true
    });
}
// Observer.js
// 创建类以后,就考虑如何实例化
import {def} from './ utils.js';
import defineReactive from 'defineReactive.js';
export default class Observer {
    constructor(value){
        // 给实例添加了 __ob__ 属性,值是这次new的实例
        def(value, '__ob__', this, false);
        console.log('我是Observer构造器', value);
        this.walk(value);
    }
    // 遍历
    walk(value){
        for(let k in value){
            defineReactive(value, k);
        }
    }
};
// defineReactive.js
var obj = {
    a: {
        m: {
            n: 5 
        }
    },
    b: 4
};
function defineReactive(data, key, val){
    if(arguments.length == 2){
       val = obj[key];
    }
    Object.defineProperty(data, key, {
        writable:true,    // 可读写
        enumerable:true,   // 可枚举
        get(){
          console.log('访问 obj 的' + key + '属性');
          return val;
        },
        set(newValue){
          console.log('改变 obj 的' + key + '属性', newValue)
          if(val === newValue){return;}
          val = newValue;
        }
    });
}
// index.js
import defineReactive from './defineReactive.js';
import Observer from './Observer.js';
var obj = {
    a: {
        m: {
            n: 5 
        }
    },
    b: 4
};
// 创建 observe 函数,只为对象服务
function observe(value){
    if(typeof value != 'object') return;
    var ob;
    if(typeof value.__ob__ !== 'undefined'){
        ob = value.__ob__; 
    } else {
        ob = new Observer(value);
    }
    return ob;
}
observe(obj);

Object.setPrototypeOf方法

用于实现数组的响应式:

  • vue 改写了数组的7个方法:push/pop/shift/unshift/splice/sort/reserve

所有数组的方法都在数组的原型Array.prototype对象上

vue 中数组的响应式实现方式:以Array.prototype为原型,创建了一个arrayMethods为对象的备份,再用ES6语法中的Object.setPrototypeOf方法强制的将新数组的原型指向了arrayMethods,从而触发新的数组方法被调用。

image.png

Dep

需要用到数据的地方,称为依赖。在getter中收集依赖,在setter中触发依赖。对于 Vue2 为中等粒度依赖,用到数据的组件是依赖。把依赖收集的代码封装成一个Dep类,专门用来管理依赖,每个Observer实例成员中都有一个Dep的实例。Watcher是一个中介,数据发生变化时,通过Watcher中转,通知组件。

// Dep.js
export default class Dep{
    constructor(){
        console.log('我是Dep类的构造器')
    }
}

Watcher

// Watcher.js
export default class Watcher{
    constructor(){
        console.log('我是Watcher类的构造器')
    }
}
import {def} from './ utils.js';
import defineReactive from 'defineReactive.js';
imoport Dep from './Dep.js';
export default class Observer {
    constructor(value){
        // 每一个Observer的实例,都有一个dep
        this.dep = new Dep();
        // 给实例添加了 __ob__ 属性,值是这次new的实例
        def(value, '__ob__', this, false);
        console.log('我是Observer构造器', value);
        this.walk(value);
    }
    // 遍历
    walk(value){
        for(let k in value){
            defineReactive(value, k);
        }
    }
};

模板解析

步骤作用备注
1. Parse接收 template 原始模板
按照模板的节点 和数据 生成对应的 ast
ast 就是以数据的形式描述一个东西的所有特征
2. Optimize遍历递归每一个ast节点
标记静态的节点
排除掉静态节点,优化性能
3. Generate把前两步生成完善的 ast 组装成 render 字符串render 字符串形态
后面会转变成函数
4. new Fun(render)转为render函数后保存在实例上vm.$options.render

当数据发生变化时,会触页面的重新渲染。vue是如何进行渲染的?

首先,vue会把将我们编写的HTML模板解析成一个AST描述对象,该对象是通过childrenparent链接而成的树形结构,完整地描述了HTML标签的所有信息。

<div id="app">
    <p>{{msg}}</p>
</div>

最终会解析成如下形式的AST对象:

{
   attrs: [{name: "id", value: ""app"", dynamic: undefined, start: 5, end: 13}],
   attrsList: [{name: "id", value: "app", start: 5, end: 13}],
   attrsMap: {id: "app"},
   children: [{
        attrsList: [],
        attrsMap: {},
        children: [],
        end: 33,
        parent: {type: 1, tag: "div", ...},
        plain: true,
        pre: undefined,
        rawAttrsMap:{},
        start: 19
        tag: "p",
        type: 1
   }],
   end: 263,
   parent: undefined,
   plain: false,
   rawAttrsMap:{id: {name: "id", value: "app", start: 5, end: 13}},
   start: 0
   tag: "div",
   type: 1
}

然后 vue 根据AST对象生成 render 函数,该函数的函数体大致如下:

with(this){
    return _c('div', {attrs:{"id":"app"}}, [_c('p', [_v(_s(msg))])])
}

也就是说,我们的模板最终在vue内部都是会以一个render函数的形式存在。

vue官网上对此也有提及,一般推荐大家使用template,el等方式来指定模板,此外还可以通过使用render来自定义个性化的编译函数,不过vue内部最终都会解析成render函数。

先虚后实

我们得到render函数之后,vue并未直接渲染成DOM树,而是先通过render函数得到一个vnode。而render的作用,也是为了生成跟模板节点一一对应的vnode

实际上这一步是非常有必要的,我们都知道频繁大量地操作DOM节点是极耗性能的。vue在渲染之前通过对vnode的比较,可以大大规避非必要的DOM操作。下面是一个vnode大致结构:

{
    tag: "div",
    children: [{tag: "p", ...}],
    data: {attrs: {id: "app"}}
    elm: DOM节点(div#app),
    parent: undefined,
    context: Vue实例,
    ...
}

最后,vue根据diff之后的结果,执行真正的dom节点的插入更新删除等操作,同时触发vue实例的生命周期钩子函数。之后,vue要做的就是观察数据的变化,进而决定是否重新渲染页面了。

Diff 作用

Diff 是精细化比对最小量更新。Diff 的出现,就会为了减少更新量,找到最小差异部分DOM,只更新差异部分DOM就好了,这样消耗就会小一些,数据变化一下,没必要把其他没有涉及的没有变化的DOM 也替换了。

Diff 做法

同层级比较新旧Vnode节点,而不是比较DOM,并不需要递归。Vue 只会对新旧节点中 父节点是相同节点 的 那一层子节点 进行比较。只有两个新旧节点是相同节点的时候,才会去比较他们各自的子节点,

最后,vue根据diff之后的结果,执行真正的dom节点的插入更新删除等操作,同时触发vue实例的生命周期钩子函数。之后,vue要做的就是观察数据的变化,进而决定是否重新渲染页面了。

Diff 比较逻辑

Diff 比较的内核是 节点复用,所以 Diff 比较就是为了在 新旧节点中 找到 相同的节点。这个的比较逻辑是建立在上一步说过的同层比较基础之上的。所以说,节点复用,找到相同节点并不是无限制递归查找

最后,vue根据diff之后的结果,执行真正的dom节点的插入更新删除等操作,同时触发vue实例的生命周期钩子函数。之后,vue要做的就是观察数据的变化,进而决定是否重新渲染页面了。

博文参考:孟思行《图解 Vue 响应式原理》、 Sunshine_Lin《Vue 源码学习》

感谢尚硅谷的vue源码解析课程~

响应式参考资料:

数据响应原理

image.png

通过Object.defineProperty建立一套响应式系统。只要在 Vue 实例中声明过的数据,那么这个数据就是响应式的。

Object.defineProperty方法 - Vue响应式系统的核心

该方法是JS引擎的功能,用于检测对象属性变化。具体讲:使用该方法可以为对象中的每一个属性,设置get和set方法。当属性被访问时,会触发 getter 函数;当属性被赋值时,会触发 setter 函数:

var obj={    
    name:"梅老板"
}
Object.defineProperty(obj,"name",{
    get(){        
        console.log("get 被触发")
    },
    set(val){        
        console.log("set 被触发")
    }
})

// 当访问 obj.name 时,会打印 ' get 被触发 '
// 当为 obj.name 赋值时,obj.name = 5,会打印 ' set 被触发 '

数据驱动具体是怎么实现的呢?数据改变驱动视图自动更新的大致过程为:

当执行new Vue()时,只执行了一个_init方法。_init会根据传入的选项对vue进行初始化。 props、data、生命周期,事件机制的初始化都是在此过程中完成的。

以data的初始化为例,vue会通过 Object.defineProperty 的方式将data的属性定义到vue实例上。创建一个observer对象,该对象与data绑定,通过 Object.defineProperty 将data中的所有的属性转换成getter/setter。当data中的属性在vue实例中被访问(会触发getter),observer 对象就会把该属性收集为watcher实例的依赖,之后当data中的属性在vue实例中被改变(会触发setter), observer 会通知依赖该属性的 watcher 实例重新渲染页面。

依赖收集

data 中的声明的每个属性,都拥有一个数组,保存着 谁依赖(使用)了 它

new Vue({    
    data(){        
        return {            
            name:"神仙朱"        
        }    
    }
})

另一个页面A引用了name

<div>{{name}}</div>

此时,name 使用 Dep 把页面A 的Watcher(每个Vue实例都会拥有一个专属的watcher,用于实例更新)存储在他的小本本(每个声明的属性都会有一个专属的依赖收集器 subs )里,并标记这个页面A 依赖我。有了这个记录,它就可以在发生改变的时候,通知依赖他的页面A ,从而让其完成更新。 此外,依赖 name 的地方,不只是页面,还会有 computedwatch ...等。

依赖更新

通知所有的依赖进行更新。

数据代理

代理 proxy 是一种设计模式,vm 就 myData的代理,对myData对象的属性读写,全权由另一个对象vm负责

vm = new Vue({data: myData})

  1. 会让 vm 成为 myData 的代理 proxy
  2. 会让 myData 的所有属性进行监控
  3. 为什么要监控,为了防止 myData 的属性变了,vm不知道;
  4. vm 知道属性变了就可以调用 render(data),UI=render(data)