• 35648

    文章

  • 23

    评论

  • 20

    友链

  • 最近新加了很多技术文章,大家多来逛逛吧~~~~
  • 喜欢这个网站的朋友可以加一下QQ群,我们一起交流技术。

Vue 面试知识点总结【持续更新中~】

欢迎来到阿八个人博客网站。本 阿八个人博客 网站提供最新的站长新闻,各种互联网资讯。 喜欢本站的朋友可以收藏本站,或者加QQ:我们大家一起来交流技术! URL链接:https://www.abboke.com/jsh/2019/0702/4798.html 1190000019633325
本文内容来自网络,整理出来分享于大家~~

一、剖析 Vue.js 内部运行机制

参考至小册剖析 Vue.js 内部运行机制

先来一张总体图,然后我们对每一部分详细分析。

clipboard.png

1、初始化与挂载

clipboard.png

new Vue之后回调用一个_init方法去初始化,会初始化datapropsmethods声明周期watchcomputed事件等。其中最重要的一点就是通过Object.defineProperty来设置gettersetter,从而实现数据的【双向绑定响应式】【依赖收集】

初始化完之后会调用一个$mount来实现挂载。如果是运行时编译,则不存在render function ,存在template的情况需要重新编译。(我理解的意思:最开始我们需要去解析编译template中的内容,实现依赖收集和数据绑定,最后会生成一个render function.但是如果是运行时候比如响应数据的更改等,则不会在生成render function,而是通过diff算法直接操作虚拟DOM,实现正式结点的更新)。

2、响应式系统的实现原理

Vue是一款MVVM的框架,数据模型仅仅是普通的js对象,但是在操作这些对象的时候确可以及时的响应视图的变化。依赖的就是Vue的【响应式系统】。

面试题 —— 你了解Vue的MVVM吗?

MVVM包含三层:模型层Model,视图层View,控制层ViewModel.

clipboard.png

联系:
  • 视图层变化可以被viewModel监听到,从而更改Model中的数据。是通过DOM事件监听实现。
  • Model层发生变化,可以被viewModel响应到view层,从而更新视图。是通过数据绑定
总之:DOM事件监听和数据绑定是MVVM的关键。DOM Listeners监听页面所有View层DOM元素的变化,当发生变化,Model层的数据随之变化;Data Bindings监听Model层的数据,当数据发生变化,View层的DOM元素随之变化。

(1)Object.defineProperty

首先我们来介绍一下 Object.defineProperty,Vue.js就是基于它实现「响应式系统」的。

Object.defineProperty(obj, prop, descriptor);

descriptor的一些属性,简单介绍几个属性:

  • enumerable,属性是否可枚举,默认 false。
  • configurable,属性是否可以被修改或者删除,默认 false。
  • get,获取属性的方法。
  • set,设置属性的方法。
  • writable,当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。
var o = {}; // 创建一个新对象

// 【1】在对象中添加一个属性与数据描述符的示例
Object.defineProperty(o, "a", {
  value : 37,
  writable : true,
  enumerable : true,
  configurable : true
});

// 对象o拥有了属性a,值为37

// 【2】在对象中添加一个属性与存取描述符的示例
var bValue;
Object.defineProperty(o, "b", {
  get : function(){
    return bValue;
  },
  set : function(newValue){
    bValue = newValue;
  },
  enumerable : true,
  configurable : true
});

o.b = 38;
// 对象o拥有了属性b,值为38

// o.b的值现在总是与bValue相同,除非重新定义o.b

(2)实现数据的观察(observer)

这是响应式系统最为重要的一步。利用的便是我们上面提到的Object.defineProperty

实现一个简单的对数据的getter和setter监听:

// 遍历数据对象的每个属性,这里我们只做了一层,实际上会使用递归去处理深层次的数据
// 这里为了我们的方便理解,就假设是单层对象
function observer (value) {
    if (!value || (typeof value !== 'object')) {
        return;
    }
    
    Object.keys(value).forEach((key) => {
        defineReactive(value, key, value[key]);
    });
}

// 函数模拟视图更新
function cb (val) {
    console.log("视图更新啦~", val);
}

// 数据对象成员的响应式监听
function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true, // 可枚举
        configurable: true, // 可配置
        get: function reactiveGetter () {
            return val;     // 当使用到我们的这个属性的时候会触发get方法,这里用来依赖收集,我们之后实现
        },
        set: function reactiveSetter (newVal) {
            // 监听数据的修改,模拟视图更新,其实这里的过程相当的复杂,diff是一个必经过程
            if (newVal === val) return;
            val = newVal;
            cb(newVal);
        }
    });
}


class Vue {
    constructor(options) {
        this._data = options.data; // 获取数据对象
        observer(this._data); // 实现对数据中每个元素的观察,即为每个属性去设置get和set。
    }  
}


// 测试案例
let o = new Vue({
    data: {
        test: "I am test."
    }
});
o._data.test = "hello,test.";

上面我们实现的是一个简单的响应式原理案例,我们只是实现了对数据对象的观察。当我们的数据使用和被修改的时候会调用我们的自定义get和set方法。下面我们去了解一下,数据【依赖收集】。

(3)依赖收集

为什么要进行依赖收集呢?

new Vue({
    template: 
        `<div>
            <span>{{text1}}</span> 
            <span>{{text2}}</span> 
        <div>`,
    data: {
        text1: 'text1',
        text2: 'text2',
        text3: 'text3'
    }
});

上面例子中,text1,text2使用了一次,text3未使用。

如果我们对某一个数据进行了修改,那么我们应该知道的哪些地方使用了该数据,为了我们视图的更新做好准备。

「依赖收集」会让 text1 这个数据知道“哦~有两个地方依赖我的数据,我变化的时候需要通知它们~”。

clipboard.png

订阅者Dep

class Dep {
    constructor () {
        /* 用来存放Watcher对象的数组 */
        this.subs = [];
    }

    /* 在subs中添加一个Watcher对象 */
    addSub (sub) {
        this.subs.push(sub);
    }

    /* 通知所有Watcher对象更新视图 */
    notify () {
        this.subs.forEach((sub) => {
            sub.update();
        })
    }
}

订阅者对象含有两个方法,addSub用来收集watcher对象,notify用来通知watcher对象去更新视图。

观察者Watcher

class Watcher {
    constructor () {
        /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
        Dep.target = this;
    }

    /* 更新视图的方法 */
    update () {
        console.log("视图更新啦~");
    }
}

Dep.target = null;

观察者对象在实例化的时候就需要绑定它所属的Dep。同时还有一个update方法去更新视图。

依赖收集原理

function defineReactive (obj, key, val) {
    /* 一个Dep类对象 */
    const dep = new Dep();
    
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
            /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
            dep.addSub(Dep.target);
            return val;         
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            /* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
            dep.notify();
        }
    });
}

class Vue {
    constructor(options) {
        this._data = options.data;
        observer(this._data);
        /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象*/
        // 实例化一个观察者
        new Watcher();
        /* 在这里模拟render的过程,为了触发test属性的get函数 */
        console.log('render~', this._data.test);
        // 触发get之后,会将上面刚实例化的watcher对象,添加到Dep对象中。
        
        // 注:这里只实例化了一个watcher,其实watcher对象没有我们上诉的那么简单,它记录的是当前引用的相关信息。为方便下次数据的更新时候,去更新视图
    }
}

当触发一个属性的get方法后,会执行我们的依赖收集。首先实例化一个watcher对象,这个watcher对象有这个属性的更新视图的方法。然后通过Dep的addSub方法将该watcher对象添加到Dep订阅者中。

【依赖收集】的关键条件:(1)触发get方法 (2)新建一个watcher对象
总结: 到了这里我们已经吧响应式系统学了,主要是get进行依赖收集,set中用过watcher观察者去更新视图。

面试题 —— 你了解Vue的响应式系统原理吗?

Vue采用的是数据劫持的方式,当你设置data属性的值时候,vue就会遍历data属性,对每一个属性通过Object.defineProperty来设置gettersetter。当触发render function 渲染的时候,就会触发属性的getter方法,同时触发getter方法中的依赖收集,所谓的依赖收集就是将观察者Watcher对象存放到当前闭包中的订阅者Depsubs 中。形成如下所示的这样一个关系。

clipboard.png

在修改对象的值的时候,会触发对应的settersetter通知之前「依赖收集」得到的Dep中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个patch 的过程以及使用队列来异步更新的策略。实质就是在数据变动时发布消息给订阅者,触发需要修改的watcher中的notify方法相应的监听回调.

clipboard.png

3、模版编译

compile编译可以分成 Html解析parse优化optimize转换generate 三个阶段,最终需要得到render function

clipboard.png

parse解析

<div :class="c" class="demo" v-if="isShow">
    <span v-for="item in sz">{{item}}</span>
</div>

对HTML进行字符串解析,从而得到class、style、指令等数据,形成AST。AST是一种抽象语法树。上面的例子解析完后是:

{
    /* 标签属性的map,记录了标签上属性 */
    'attrsMap': {
        ':class': 'c',
        'class': 'demo',
        'v-if': 'isShow'
    },
    /* 解析得到的:class */
    'classBinding': 'c',
    /* 标签属性v-if */
    'if': 'isShow',
    /* v-if的条件 */
    'ifConditions': [
        {
            'exp': 'isShow'
        }
    ],
    /* 标签属性class */
    'staticClass': 'demo',
    /* 标签的tag */
    'tag': 'div',
    /* 子标签数组 */
    'children': [
        {
            'attrsMap': {
                'v-for': "item in sz"
            },
            /* for循环的参数 */
            'alias': "item",
            /* for循环的对象 */
            'for': 'sz',
            /* for循环是否已经被处理的标记位 */
            'forProcessed': true,
            'tag': 'span',
            'children': [
                {
                    /* 表达式,_s是一个转字符串的函数 */
                    'expression': '_s(item)',
                    'text': '{{item}}'
                }
            ]
        }
    ]
}

optimize优化

optimize 主要作用就跟它的名字一样,用作「优化」
这个涉及到后面要讲 patch 的过程,因为patch的过程实际上是将 VNode 节点进行一层一层的比对,然后将「差异」更新到视图上。

那么一些静态节点是不会根据数据变化而产生变化的,我们就需要为静态的节点做上一些「标记」,在 patch 的时候我们就可以直接跳过这些被标记的节点的比对,从而达到「优化」的目的。

generate 转为 render function

generate 会将 AST 转化成 render funtion 字符串

render function 看起来就像下面:

with(this){
    return (isShow) ? 
    _c(
        'div',
        {
            staticClass: "demo",
            class: c
        },
        _l(
            (sz),
            function(item){
                return _c('span',[_v(_s(item))])
            }
        )
    )
    : _e()
}

clipboard.png

经历过这些过程以后,我们已经把 template 顺利转成了 render function 了,之后 render function 就会转换为Virtual DOM。

4、虚拟DOM(VirtualDOM)

虚拟DOM实质就是一个实打实的javascript对象。它是对真是DOM的一层映射。用对象属性来描述某个结点,以及它的子结点。由于虚拟DOM是javascript对象为基础,所以不依赖任何环境,所以具有跨平台的特性。也正式因为基于这一点,Vue具有跨平台的能力~~

我们来看一个简单的虚拟DOM实例:

class VNode {
    constructor (tag, data, children, text, elm) {
        /*当前节点的标签名*/
        this.tag = tag;
        /*当前节点的一些数据信息,比如props、attrs等数据*/
        this.data = data;
        /*当前节点的子节点,是一个数组*/
        this.children = children;
        /*当前节点的文本*/
        this.text = text;
        /*当前虚拟节点对应的真实dom节点*/
        this.elm = elm;
    }
}

我们有一段template代码:

<template>
  <span class="demo" v-show="isShow">
    This is a span.
  </span>
</template>

用js对象表示就是:

function render () {
    return new VNode(
        'span',
        {
            /* 指令集合数组 */
            directives: [
                {
                    /* v-show指令 */
                    rawName: 'v-show',
                    expression: 'isShow',
                    name: 'show',
                    value: true
                }
            ],
            /* 静态class */
            staticClass: 'demo'
        },
        [ new VNode(undefined, undefined, undefined, 'This is a span.') ]
    );
}

转换成 VNode 以后的情况。

{
    tag: 'span',
    data: {
        /* 指令集合数组 */
        directives: [
            {
                /* v-show指令 */
                rawName: 'v-show',
                expression: 'isShow',
                name: 'show',
                value: true
            }
        ],
        /* 静态class */
        staticClass: 'demo'
    },
    text: undefined,
    children: [
        /* 子节点是一个文本VNode节点 */
        {
            tag: undefined,
            data: undefined,
            text: 'This is a span.',
            children: undefined
        }
    ]
}

该种形式就可以让我们在不同的平台实现很好的兼容了。

如何产生上诉对象呢,我们需要通过一些自定义函数来实现,举一个简答例子:我们创建一个空结点。

function createEmptyVNode () {
    const node = new VNode();
    node.text = '';
    return node;
}

所以虚拟DOM可以通过调用一系列自定义的内部函数来实现,最终创建的就是 一个 VNode 实例对象。

再来看我们的render function:
with(this){
    return (isShow) ? 
    _c(
        'div',
        {
            staticClass: "demo",
            class: c
        },
        _l(
            (sz),
            function(item){
                return _c('span',[_v(_s(item))])
            }
        )
    )
    : _e()
}

上面这个 render function看到这里可能会纳闷了,这些_c,_l 到底是什么?其实他们是 Vue.js 对一些函数的简写,比如说 _c对应的是createElement 这个函数。

到了这里你是不是懂了我们之前所说的一句话了:我们之前说render function是用来生成虚拟DOM对象的。其实render function就是一个复杂的函数调用。最后会通过层层调用来实现一个真正的js对象(虚拟对象)。

5、数据状态更新时的差异 diff 及 patch 机制

当我们触发数据的更新时,会调用Dep中的watcher对象的update方法来更新视图。最终是将新产生的 VNode 节点与老 VNode 进行一个 patch 的过程,比对得出「差异」,最终将这些「差异」更新到视图上。

patch过程其实就是利用diff算法进行一个差异比对的过程~

推荐两个diff算法执行过程的图解:

Vue 进阶
vue源码解析-图解diff详细过程

总结

无oldStartVnode则移动(参照round6)
对比头部,成功则更新并移动(参照round4)
对比尾部,成功则更新并移动(参照round1)
头尾对比,成功则更新并移动(参照round5)
尾头对比,成功则更新并移动(参照round2)
在oldKeyToIdx中根据newStartVnode的可以进行查找,成功则更新并移动(参照round3)
(更新并移动:patchVnode更新对应vnode的elm,并移动指针)
我们在整个过程中使用了diff算法去逐一判断,通过patch去判断两个节点是否更新,然后作出相应的DOM操作。总之:diff算法告诉我们如何去处理同层下的新旧VNode。
Diff过程中,Vue会尽可能的复用DOM,能不移动就不移动。

6、批量异步更新策略

我们知道在我们修改data 之后其实就是一个“setter -> Dep -> Watcher -> patch -> 视图”的过程。

假设我们有如下这么一种情况。

<template>
  <div>
    <div>{{number}}</div>
    <div @click="handleClick">click</div>
  </div>
</template>
export default {
    data () {
        return {
            number: 0
        };
    },
    methods: {
        handleClick () {
            for(let i = 0; i < 1000; i++) {
                this.number++;
            }
        }
    }
}

当我们按下 click 按钮的时候,number 会被循环增加1000次。
那么按照之前的理解,每次 number 被 +1 的时候,都会触发 number 的 setter 方法,从而根据上面的流程一直跑下来最后修改真实 DOM。那么在这个过程中,DOM 会被更新 1000 次!这样子太消耗性能了,太可怕了~。

Vue做了相应的处理:

Vue.js在默认情况下,每次触发某个数据的 setter 方法后,对应的 Watcher 对象其实会被 push 进一个队列 queue 中,这些watcher对象都设置了标识,如果是对同一个数据的更新,watcher的标识是相同的,在下一个 tick 的时候将这个队列 queue 全部拿出来 run( Watcher 对象的一个方法,用来触发 patch 操作) 一遍。run的时候会进行筛选,然后根据标识判断重复的watcher对象只执行最后的。
let watch1 = new Watcher();
let watch2 = new Watcher();

watch1.update();
watch1.update();
watch2.update();

watch1只调用最后那次。

二、Vue核心高频知识点

上面我们对Vue的底层进行了一定的了解,虽然不是源码解析,但是我们用一种简介明了的方式理解了底层的大致运行流程,下面我们针对一些面试题目,来温习一下我们Vue的知识点吧~~

1、vue.js的两个核心是什么?

(1)数据双向绑定

vue的数据响应式原理,技术上是采用Object.defineProperty和存储属性get、set来是实现的基于依赖收集的数据观测机制。核心是viewModel,保证数据和视图的一致性。

(2)组件

Vue中万物皆组件的理念使得它与虚拟DOM的契合度达到了非常好的地步。

.vue组件的形式如下:

1、模板(template):模板声明了数据和最终展现给用户的DOM之间的映射关系。
2、初始数据(data):一个组件的初始数据状态。对于可复用的组件来说,这通常是私有的状态。
3、接受的外部参数(props):组件之间通过参数来进行数据的传递和共享。
4、方法(methods):对数据的改动操作一般都在组件的方法内进行。
5、生命周期钩子函数(lifecycle hooks):一个组件会触发多个生命周期钩子函数,最新2.0版本对于生命周期函数名称改动很大。
6、私有资源(assets):Vue.js当中将用户自定义的指令、过滤器、组件等统称为资源。一个组件可以声明自己的私有资源。私有资源只有该组件和它的子组件可以调用。
等等。

2、对于 Vue 是一套 构建用户界面 的 渐进式框架 的理解

渐进式指的是:没有多做职责以外的事。Vue只提供了组件系统和数据响应式系统两大核心。基于vue-cli的生态,则还需要vue-router、vuex等的第三方库的支持。我们学习使用Vue,可以是需要什么功能,我们就学什么功能。

Vue与React、Angular的不同是,但它是渐进的:
  • 你可以在原有大系统的上面,把一两个组件改用Vue实现
  • 也可以整个用它全家桶开发
  • 还可以用它的视图,搭配你自己设计的整个下层用。
  • 也可以函数式,都可以,它只是个轻量视图而已,只做了最核心的东西。

3、你了解Vue常用的指令吗?

  • v-if:根据表达式的值的真假条件渲染元素。在切换时元素及它的数据绑定或组件被销毁并重建。(性能低)
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no 








									                    

相关文章

暂住......别动,不想说点什么吗?
  • 全部评论(0
    还没有评论,快来抢沙发吧!