原文链接: https://interview.poetries.top/docs/excellent-docs/7-Vue.html

1 谈谈你对MVVM的理解

为什么要有这些模式,目的:职责划分、分层(将Model层、View层进行分类)借鉴后端思想,对于前端而已,就是如何将数据同步到页面上

MVC模式 代表:Backbone + underscore + jquery

  • 传统的 MVC 指的是,用户操作会请求服务端路由,路由会调用对应的控制器来处理,控制器会获取数据。将结果返回给前端,页面重新渲染
  • MVVM:传统的前端会将数据手动渲染到页面上, MVVM 模式不需要用户收到操作 dom 元素,将数据绑定到 viewModel 层上,会自动将数据渲染到页面中,视图变化会通知 viewModel层 更新数据。ViewModel 就是我们 MVVM 模式中的桥梁

MVVM模式 映射关系的简化,隐藏了controller

MVVMModel-View- ViewModel缩写,也就是把MVC中的Controller演变成ViewModelModel层代表数据模型,View代表UI组件,ViewModelViewModel层的桥梁,数据会绑定到viewModel层并自动将数据渲染到页面中,视图变化的时候会通知viewModel层更新数据。

  • Model: 代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。我们可以把Model称为数据层,因为它仅仅关注数据本身,不关心任何行为
  • View: 用户操作界面。当ViewModelModel进行更新的时候,会通过数据绑定更新到View
  • ViewModel: 业务逻辑层,View需要什么数据,ViewModel要提供这个数据;View有某些操作,ViewModel就要响应这些操作,所以可以说它是Model for View.

总结MVVM模式简化了界面与业务的依赖,解决了数据频繁更新。MVVM 在使用当中,利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。

我们以下通过一个 Vue 实例来说明 MVVM 的具体实现

    <!-- View 层 -->
    
    <div id="app">
      <p>{{message}}</p>
      <button v-on:click="showMessage()">Click me</button>
    </div>
    var app = new Vue({
      el: '#app',
      data: {  // 用于描述视图状态  
        message: 'Hello Vue!', // Model 层
      },
      // ViewModel 层:通过事件修改model层数据
      methods: {  // 用于描述视图行为  
        showMessage(){
          let vm = this;
          alert(vm.message);
        }
      },
      created(){
        let vm = this;
        // Ajax 获取 Model 层的数据
        ajax({
          url: '/your/server/data/api',
          success(res){
            vm.message = res;
          }
        });
      }
    })

2 谈谈你对SPA单页面的理解

SPA( single-page application )仅在 Web 页面初始化时加载相应的 HTMLJavaScriptCSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载

优点:

  • 用户体验好、快,内容的改变不需要重新加载整个页面,避免了不必要的跳转和重复渲染;
  • 基于上面一点,SPA 相对对服务器压力小;
  • 前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理

缺点:

  • 初次加载耗时多:为实现单页 Web 应用功能及显示效果,需要在加载页面的时候将 JavaScriptCSS 统一加载,部分页面按需加载;
  • 前进后退路由管理:由于单页应用在一个页面中显示所有的内容,所以不能使用浏览器的前进后退功能,所有的页面切换需要自己建立堆栈管理;
  • SEO 难度较大:由于所有的内容都在一个页面中动态替换显示,所以在 SEO 上其有着天然的弱势

单页应用与多页应用的区别

| 单页面应用(SPA) | 多页面应用(MPA)
---|---|---
组成 | 一个主页面和多个页面片段 | 多个主页面
刷新方式 | 局部刷新 | 整页刷新
url模式 | 哈希模式 | 历史模式
SEO搜索引擎优化 | 难实现,可使用SSR方式改善 | 容易实现
数据传递 | 容易 | 通过urlcookielocalStorage等传递
页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差
维护成本 | 相对容易 | 相对复杂

实现一个SPA

  • 监听地址栏中hash变化驱动界面变化
  • pushsate记录浏览器的历史,驱动界面发送变化

  1. hash 模式 :核心通过监听url中的hash来进行路由跳转
    // 定义 Router  
    class Router {  
        constructor () {  
            this.routes = {}; // 存放路由path及callback  
            this.currentUrl = '';  
              
            // 监听路由change调用相对应的路由回调  
            window.addEventListener('load', this.refresh, false);  
            window.addEventListener('hashchange', this.refresh, false);  
        }  
          
        route(path, callback){  
            this.routes[path] = callback;  
        }  
          
        push(path) {  
            this.routes[path] && this.routes[path]()  
        }  
    }  
      
    // 使用 router  
    window.miniRouter = new Router();  
    miniRouter.route('/', () => console.log('page1'))  
    miniRouter.route('/page2', () => console.log('page2'))  
      
    miniRouter.push('/') // page1  
    miniRouter.push('/page2') // page2  
  1. history模式history 模式核心借用 HTML5 history apiapi 提供了丰富的 router 相关属性先了解一个几个相关的api
  • history.pushState 浏览器历史纪录添加记录
  • history.replaceState修改浏览器历史纪录中当前纪录
  • history.popStatehistory 发生变化时触发
    // 定义 Router  
    class Router {  
        constructor () {  
            this.routes = {};  
            this.listerPopState()  
        }  
          
        init(path) {  
            history.replaceState({path: path}, null, path);  
            this.routes[path] && this.routes[path]();  
        }  
          
        route(path, callback){  
            this.routes[path] = callback;  
        }  
          
        push(path) {  
            history.pushState({path: path}, null, path);  
            this.routes[path] && this.routes[path]();  
        }  
          
        listerPopState () {  
            window.addEventListener('popstate' , e => {  
                const path = e.state && e.state.path;  
                this.routers[path] && this.routers[path]()  
            })  
        }  
    }  
      
    // 使用 Router  
      
    window.miniRouter = new Router();  
    miniRouter.route('/', ()=> console.log('page1'))  
    miniRouter.route('/page2', ()=> console.log('page2'))  
      
    // 跳转  
    miniRouter.push('/page2')  // page2  

题外话:如何给SPA做SEO

  1. SSR服务端渲染

将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js

  1. 静态化

目前主流的静态化主要有两种:

  • 一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中
  • 另外一种是通过WEB服务器的 URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果
  1. 使用Phantomjs针对爬虫处理

原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。下面是大致流程图

3 Vue2.x 响应式数据原理

整体思路是数据劫持+观察者模式

对象内部通过 defineReactive 方法,使用 Object.defineProperty 来劫持各个属性的 settergetter(只会劫持已经存在的属性),数组则是通过重写数组7个方法来实现。当页面使用对应属性时,每个属性都拥有自己的 dep 属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)

Object.defineProperty基本使用

    function observer(value) { // proxy reflect
        if (typeof value === 'object' && typeof value !== null)
        for (let key in value) {
            defineReactive(value, key, value[key]);
        }
    }
    
    function defineReactive(obj, key, value) {
        observer(value);
        Object.defineProperty(obj, key, {
            get() { // 收集对应的key 在哪个方法(组件)中被使用
                return value;
            },
            set(newValue) {
                if (newValue !== value) {
                    observer(newValue);
                    value = newValue; // 让key对应的方法(组件重新渲染)重新执行
                }
            }
        })
    }
    let obj1 = { school: { name: 'poetry', age: 20 } };
    observer(obj1);
    console.log(obj1)

源码分析

    class Observer {
      // 观测值
      constructor(value) {
        this.walk(value);
      }
      walk(data) {
        // 对象上的所有属性依次进行观测
        let keys = Object.keys(data);
        for (let i = 0; i < keys.length; i++) {
          let key = keys[i];
          let value = data[key];
          defineReactive(data, key, value);
        }
      }
    }
    // Object.defineProperty数据劫持核心 兼容性在ie9以及以上
    function defineReactive(data, key, value) {
      observe(value); // 递归关键
      // --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
      //   思考?如果Vue数据嵌套层级过深 >>性能会受影响
      Object.defineProperty(data, key, {
        get() {
          console.log("获取值");
    
          //需要做依赖收集过程 这里代码没写出来
          return value;
        },
        set(newValue) {
          if (newValue === value) return;
          console.log("设置值");
          //需要做派发更新过程 这里代码没写出来
          value = newValue;
        },
      });
    }
    export function observe(value) {
      // 如果传过来的是对象或者数组 进行属性劫持
      if (
        Object.prototype.toString.call(value) === "[object Object]" ||
        Array.isArray(value)
      ) {
        return new Observer(value);
      }
    }

说一说你对vue响应式理解回答范例

  • 所谓数据响应式就是能够使数据变化可以被检测并对这种变化做出响应的机制
  • MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动 应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理,这样一旦数据发生变化就可以立即做出更新处理
  • vue为例说明,通过数据响应式加上虚拟DOMpatch算法,开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度
  • vue2中的数据响应式会根据数据类型来做不同处理,如果是对象则采用Object.defineProperty()的方式定义数据拦截,当数据被访问或发生变化时,我们感知并作出响应;如果是数组则通过覆盖数组对象原型的7个变更方法,使这些方法可以额外的做更新通知,从而作出响应。这种机制很好的解决了数据响应化的问题,但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的MapSet这些数据结构不支持等问题
  • 为了解决这些问题,vue3重新编写了这一部分的实现:利用ES6Proxy代理要响应化的数据,它有很多好处,编程体验是一致的,不需要使用特殊api,初始化性能和内存消耗都得到了大幅改善;另外由于响应化的实现代码抽取为独立的reactivity包,使得我们可以更灵活的使用它,第三方的扩展开发起来更加灵活了

4 Vue3.x 响应式数据原理

Vue3.x改用Proxy替代Object.defineProperty。因为Proxy可以直接监听对象和数组的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。

proxy基本用法

    // proxy默认只会代理第一层对象,只有取值再次是对象的时候再次代理,不是一上来就代理,提高性能。不像vue2.x递归遍历每个对象属性
    let handler = {
        set(target, key, value) {
            return Reflect.set(target, key, value);
        },
        get(target, key) {
            if (typeof target[key] == 'object' && target[key] !== null) {
                return new Proxy(target[key], handler); // 懒代理,只有取值再次是对象的时候再次代理,提高性能
            }
            return Reflect.get(target, key);
        }
    }
    let obj = { school: { name: 'poetry', age: 20 } };
    let proxy = new Proxy(obj, handler);
    
    // 返回对象的代理
    proxy.school

说说你对 proxy 的理解,Proxy 相比于 defineProperty 的优势

Object.defineProperty() 的问题主要有三个:

  • 不能监听数组的变化 :无法监控到数组下标的变化,导致通过数组下标添加元素,不能实时响应
  • 必须遍历对象的每个属性 :只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历。Proxy 可以劫持整个对象,并返回一个新的对象
  • 必须深层遍历嵌套的对象

Proxy的优势如下:

  • 针对对象:针对整个对象,而不是对象的某个属性 ,所以也就不需要对 keys 进行遍历
  • 支持数组:Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的
  • Proxy的第二个参数可以有 13 种拦截方:不限于applyownKeysdeletePropertyhas等等是Object.defineProperty不具备的
  • Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
  • Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利

proxy详细使用点击查看 (opens new window)

Object.defineProperty的优势如下:

兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平

defineProperty的属性值有哪些

    Object.defineProperty(obj, prop, descriptor)
    
    // obj 要定义属性的对象
    // prop 要定义或修改的属性的名称
    // descriptor 要定义或修改的属性描述符
    
    Object.defineProperty(obj,"name",{
      value:"poetry", // 初始值
      writable:true, // 该属性是否可写入
      enumerable:true, // 该属性是否可被遍历得到(for...in, Object.keys等)
      configurable:true, // 定该属性是否可被删除,且除writable外的其他描述符是否可被修改
      get: function() {},
      set: function(newVal) {}
    })

相关代码如下

    import { mutableHandlers } from "./baseHandlers"; // 代理相关逻辑
    import { isObject } from "./util"; // 工具方法
    
    export function reactive(target) {
      // 根据不同参数创建不同响应式对象
      return createReactiveObject(target, mutableHandlers);
    }
    function createReactiveObject(target, baseHandler) {
      if (!isObject(target)) {
        return target;
      }
      const observed = new Proxy(target, baseHandler);
      return observed;
    }
    
    const get = createGetter();
    const set = createSetter();
    
    function createGetter() {
      return function get(target, key, receiver) {
        // 对获取的值进行放射
        const res = Reflect.get(target, key, receiver);
        console.log("属性获取", key);
        if (isObject(res)) {
          // 如果获取的值是对象类型,则返回当前对象的代理对象
          return reactive(res);
        }
        return res;
      };
    }
    function createSetter() {
      return function set(target, key, value, receiver) {
        const oldValue = target[key];
        const hadKey = hasOwn(target, key);
        const result = Reflect.set(target, key, value, receiver);
        if (!hadKey) {
          console.log("属性新增", key, value);
        } else if (hasChanged(value, oldValue)) {
          console.log("属性值被修改", key, value);
        }
        return result;
      };
    }
    export const mutableHandlers = {
      get, // 当获取属性时调用此方法
      set, // 当修改属性时调用此方法
    };

Proxy只会代理对象的第一层,那么Vue3又是怎样处理这个问题的呢?

判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。

监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?

我们可以判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger

5 Vue中如何检测数组变化

前言

Vue 不能检测到以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

Vue 提供了以下操作方法

    // Vue.set
    Vue.set(vm.items, indexOfItem, newValue)
    // vm.$set,Vue.set的一个别名
    vm.$set(vm.items, indexOfItem, newValue)
    // Array.prototype.splice
    vm.items.splice(indexOfItem, 1, newValue)

分析

数组考虑性能原因没有用 defineProperty 对数组的每一项进行拦截,而是选择对 7 种数组(push,shift,pop,splice,unshift,sort,reverse)方法进行重写(AOP 切片思想)

所以在 Vue 中修改数组的索引和长度是无法监控到的。需要通过以上 7 种变异方法修改数组才会触发数组对应的 watcher 进行更新

  • 用函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新
  • 数组里每一项可能是对象,那么我就是会对数组的每一项进行观测(且只有数组里的对象才能进行观测,观测过的也不会进行观测)

原理

Vuedata 中的数组,进行了原型链重写。指向了自己定义的数组原型方法,这样当调用数组api 时,可以通知依赖更新,如果数组中包含着引用类型。会对数组中的引用类型再次进行监控。

手写简版分析

    let oldArray = Object.create(Array.prototype);
    ['shift', 'unshift', 'push', 'pop', 'reverse','sort'].forEach(method => {
        oldArray[method] = function() { // 这里可以触发页面更新逻辑
            console.log('method', method)
            Array.prototype[method].call(this,...arguments);
        }
    });
    let arr = [1,2,3];
    arr.__proto__ = oldArray;
    arr.unshift(4);

源码分析

    // 拿到数组原型拷贝一份
    const arrayProto = Array.prototype 
    // 然后将arrayMethods继承自数组原型
    // 这里是面向切片编程思想(AOP)--不破坏封装的前提下,动态的扩展功能
    export const arrayMethods = Object.create(arrayProto) 
    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) 
            // 这句话是关键
            // this代表的就是数据本身 比如数据是{a:[1,2,3]} 那么我们使用a.push(4)  this就是a  ob就是a.__ob__ 这个属性就是上段代码增加的 代表的是该数据已经被响应式观察过了指向Observer实例
            const ob = this.__ob__ 
    
            // 这里的标志就是代表数组有新增操作
            let inserted
            switch (method) { 
                case 'push': 
                case 'unshift': 
                    inserted = args 
                    break 
                case 'splice': 
                    inserted = args.slice(2) 
                    break 
            }
            // 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测
            if (inserted) ob.observeArray(inserted) 
    
            ob.dep.notify() // 当调用数组方法后,手动通知视图更新 
    
            return result 
        }) 
    })
    
    this.observeArray(value) // 进行深度监控

vue3:改用 proxy ,可直接监听对象数组的变化

6 Vue中如何进行依赖收集?

  • 每个属性都有自己的dep属性,存放他所依赖的watcher,当属性变化之后会通知自己对应的watcher去更新
  • 默认会在初始化时调用render函数,此时会触发属性依赖收集 dep.depend
  • 当属性发生修改时会触发watcher更新dep.notify()

依赖收集简版

    let obj = { name: 'poetry', age: 20 };
    
    class Dep {
        constructor() {
          this.subs = [] // subs [watcher]
        }
        depend() {
          this.subs.push(Dep.target)
        }
        notify() {
          this.subs.forEach(watcher => watcher.update())
        }
    }
    Dep.target = null;
    observer(obj); // 响应式属性劫持
    
    // 依赖收集  所有属性都会增加一个dep属性,
    // 当渲染的时候取值了 ,这个dep属性 就会将渲染的watcher收集起来
    // 数据更新 会让watcher重新执行
    
    // 观察者模式
    
    // 渲染组件时 会创建watcher
    class Watcher {
        constructor(render) {
          this.get();
        }
        get() {
          Dep.target = this;
          render(); // 执行render
          Dep.target = null;
        }
        update() {
          this.get();
        }
    }
    const render = () => {
        console.log(obj.name); // obj.name => get方法
    }
    
    // 组件是watcher、计算属性是watcher
    new Watcher(render);
    
    function observer(value) { // proxy reflect
        if (typeof value === 'object' && typeof value !== null)
        for (let key in value) {
            defineReactive(value, key, value[key]);
        }
    }
    function defineReactive(obj, key, value) {
        // 创建一个dep
        let dep = new Dep();
    
        // 递归观察子属性
        observer(value);
    
        Object.defineProperty(obj, key, {
            get() { // 收集对应的key 在哪个方法(组件)中被使用
                if (Dep.target) { // watcher
                    dep.depend(); // 这里会建立 dep 和watcher的关系
                }
                return value;
            },
            set(newValue) {
                if (newValue !== value) {
                    observer(newValue);
                    value = newValue; // 让key对应的方法(组件重新渲染)重新执行
                    dep.notify()
                }
            }
        })
    }
    
    // 模拟数据获取,触发getter
    obj.name = 'poetries'
    
    // 一个属性一个dep,一个属性可以对应多个watcher(一个属性可以在任何组件中使用、在多个组件中使用)
    // 一个dep 对应多个watcher 
    // 一个watcher 对应多个dep (一个视图对应多个属性)
    // dep 和 watcher是多对多的关系

7 Vue实例挂载的过程中发生了什么

简单

TIP

分析

挂载过程完成了最重要的两件事:

  • 初始化
  • 建立更新机制

把这两件事说清楚即可!

回答范例

  1. 挂载过程指的是app.mount()过程,这个过程中整体上做了两件事:初始化建立更新机制
  2. 初始化会创建组件实例、初始化组件状态,创建各种响应式数据
  3. 建立更新机制这一步会立即执行一次组件更新函数,这会首次执行组件渲染函数并执行patch将前面获得vnode转换为dom;同时首次执行渲染函数会创建它内部响应式数据之间和组件更新函数之间的依赖关系,这使得以后数据变化时会执行对应的更新函数

来看一下源码,在src/core/instance/index.js

    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 关键字初始化,然后会调用 this._init 方法, 该方法在 src/core/instance/init.js 中定义

    Vue.prototype._init = function (options?: Object) {
      const vm: Component = this
      // a uid
      vm._uid = uid++
    
      let startTag, endTag
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        startTag = `vue-perf-start:${vm._uid}`
        endTag = `vue-perf-end:${vm._uid}`
        mark(startTag)
      }
    
      // a flag to avoid this being observed
      vm._isVue = true
      // merge options
      if (options && options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options)
      } else {
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        )
      }
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        initProxy(vm)
      } else {
        vm._renderProxy = vm
      }
      // expose real self
      vm._self = vm
      initLifecycle(vm)
      initEvents(vm)
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm) // resolve injections before data/props
      initState(vm)
      initProvide(vm) // resolve provide after data/props
      callHook(vm, 'created')
    
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        vm._name = formatComponentName(vm, false)
        mark(endTag)
        measure(`vue ${vm._name} init`, startTag, endTag)
      }
    
      if (vm.$options.el) {
        vm.$mount(vm.$options.el)
      }
    }

Vue 初始化主要就干了几件事情,合并配置初始化生命周期初始化事件中心初始化渲染初始化 datapropscomputedwatcher

vue2.x详细

1. 分析

首先找到vue的构造函数

源码位置:src\core\instance\index.js

    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)
    }

options是用户传递过来的配置项,如data、methods等常用的方法

vue构建函数调用_init方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法

    initMixin(Vue);     // 定义 _init
    stateMixin(Vue);    // 定义 $set $get $delete $watch 等
    eventsMixin(Vue);   // 定义事件  $on  $once $off $emit
    lifecycleMixin(Vue);// 定义 _update  $forceUpdate  $destroy
    renderMixin(Vue);   // 定义 _render 返回虚拟dom

首先可以看initMixin方法,发现该方法在Vue原型上定义了_init方法

源码位置:src\core\instance\init.js

    Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // a uid
        vm._uid = uid++
        let startTag, endTag
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          startTag = `vue-perf-start:${vm._uid}`
          endTag = `vue-perf-end:${vm._uid}`
          mark(startTag)
        }
    
        // a flag to avoid this being observed
        vm._isVue = true
        // merge options
        // 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法
        if (options && options._isComponent) {
          // optimize internal component instantiation
          // since dynamic options merging is pretty slow, and none of the
          // internal component options needs special treatment.
          initInternalComponent(vm, options)
        } else { // 合并vue属性
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
          // 初始化proxy拦截器
          initProxy(vm)
        } else {
          vm._renderProxy = vm
        }
        // expose real self
        vm._self = vm
        // 初始化组件生命周期标志位
        initLifecycle(vm)
        // 初始化组件事件侦听
        initEvents(vm)
        // 初始化渲染方法
        initRender(vm)
        callHook(vm, 'beforeCreate')
        // 初始化依赖注入内容,在初始化data、props之前
        initInjections(vm) // resolve injections before data/props
        // 初始化props/data/method/watch/methods
        initState(vm)
        initProvide(vm) // resolve provide after data/props
        callHook(vm, 'created')
    
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          vm._name = formatComponentName(vm, false)
          mark(endTag)
          measure(`vue ${vm._name} init`, startTag, endTag)
        }
        // 挂载元素
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }

仔细阅读上面的代码,我们得到以下结论:

  • 在调用beforeCreate之前,数据初始化并未完成,像dataprops这些属性无法访问到
  • 到了created的时候,数据已经初始化完成,能够访问dataprops这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素
  • 挂载方法是调用vm.$mount方法

initState方法是完成props/data/method/watch/methods的初始化

源码位置:src\core\instance\state.js

    export function initState (vm: Component) {
      // 初始化组件的watcher列表
      vm._watchers = []
      const opts = vm.$options
      // 初始化props
      if (opts.props) initProps(vm, opts.props)
      // 初始化methods方法
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        // 初始化data  
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch && opts.watch !== nativeWatch) {
        initWatch(vm, opts.watch)
      }
    }

我们和这里主要看初始化data的方法为initData,它与initState在同一文件上

    function initData (vm: Component) {
      let data = vm.$options.data
      // 获取到组件上的data
      data = vm._data = typeof data === 'function'
        ? getData(data, vm)
        : data || {}
      if (!isPlainObject(data)) {
        data = {}
        process.env.NODE_ENV !== 'production' && warn(
          'data functions should return an object:\n' +
          'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
          vm
        )
      }
      // proxy data on instance
      const keys = Object.keys(data)
      const props = vm.$options.props
      const methods = vm.$options.methods
      let i = keys.length
      while (i--) {
        const key = keys[i]
        if (process.env.NODE_ENV !== 'production') {
          // 属性名不能与方法名重复
          if (methods && hasOwn(methods, key)) {
            warn(
              `Method "${key}" has already been defined as a data property.`,
              vm
            )
          }
        }
        // 属性名不能与state名称重复
        if (props && hasOwn(props, key)) {
          process.env.NODE_ENV !== 'production' && warn(
            `The data property "${key}" is already declared as a prop. ` +
            `Use prop default value instead.`,
            vm
          )
        } else if (!isReserved(key)) { // 验证key值的合法性
          // 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据
          proxy(vm, `_data`, key)
        }
      }
      // observe data
      // 响应式监听data是数据的变化
      observe(data, true /* asRootData */)
    }

仔细阅读上面的代码,我们可以得到以下结论:

  • 初始化顺序:propsmethodsdata
  • data定义的时候可选择函数形式或者对象形式(组件只能为函数形式)

关于数据响应式在这就不展开详细说明

上文提到挂载方法是调用vm.$mount方法

源码位置:

    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      // 获取或查询元素
      el = el && query(el)
    
      /* istanbul ignore if */
      // vue 不允许直接挂载到body或页面文档上
      if (el === document.body || el === document.documentElement) {
        process.env.NODE_ENV !== 'production' && warn(
          `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
        )
        return this
      }
    
      const options = this.$options
      // resolve template/el and convert to render function
      if (!options.render) {
        let template = options.template
        // 存在template模板,解析vue模板文件
        if (template) {
          if (typeof template === 'string') {
            if (template.charAt(0) === '#') {
              template = idToTemplate(template)
              /* istanbul ignore if */
              if (process.env.NODE_ENV !== 'production' && !template) {
                warn(
                  `Template element not found or is empty: ${options.template}`,
                  this
                )
              }
            }
          } else if (template.nodeType) {
            template = template.innerHTML
          } else {
            if (process.env.NODE_ENV !== 'production') {
              warn('invalid template option:' + template, this)
            }
            return this
          }
        } else if (el) {
          // 通过选择器获取元素内容
          template = getOuterHTML(el)
        }
        if (template) {
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            mark('compile')
          }
          /**
           *  1.将temmplate解析ast tree
           *  2.将ast tree转换成render语法字符串
           *  3.生成render方法
           */
          const { render, staticRenderFns } = compileToFunctions(template, {
            outputSourceRange: process.env.NODE_ENV !== 'production',
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
          }, this)
          options.render = render
          options.staticRenderFns = staticRenderFns
    
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            mark('compile end')
            measure(`vue ${this._name} compile`, 'compile', 'compile end')
          }
        }
      }
      return mount.call(this, el, hydrating)
    }

阅读上面代码,我们能得到以下结论:

  • 不要将根元素放到body或者html
  • 可以在对象中定义template/render或者直接使用templateel表示元素选择器
  • 最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数

template的解析步骤大致分为以下几步:

  • html文档片段解析成ast描述符
  • ast描述符解析成字符串
  • 生成render函数

生成render函数,挂载到vm上后,会再次调用mount方法

源码位置:src\platforms\web\runtime\index.js

    // public mount method
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && inBrowser ? query(el) : undefined
      // 渲染组件
      return mountComponent(this, el, hydrating)
    }

调用mountComponent渲染组件

    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      vm.$el = el
      // 如果没有获取解析的render函数,则会抛出警告
      // render是解析模板文件生成的
      if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode
        if (process.env.NODE_ENV !== 'production') {
          /* istanbul ignore if */
          if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
            vm.$options.el || el) {
            warn(
              'You are using the runtime-only build of Vue where the template ' +
              'compiler is not available. Either pre-compile the templates into ' +
              'render functions, or use the compiler-included build.',
              vm
            )
          } else {
            // 没有获取到vue的模板文件
            warn(
              'Failed to mount component: template or render function not defined.',
              vm
            )
          }
        }
      }
      // 执行beforeMount钩子
      callHook(vm, 'beforeMount')
    
      let updateComponent
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        updateComponent = () => {
          const name = vm._name
          const id = vm._uid
          const startTag = `vue-perf-start:${id}`
          const endTag = `vue-perf-end:${id}`
    
          mark(startTag)
          const vnode = vm._render()
          mark(endTag)
          measure(`vue ${name} render`, startTag, endTag)
    
          mark(startTag)
          vm._update(vnode, hydrating)
          mark(endTag)
          measure(`vue ${name} patch`, startTag, endTag)
        }
      } else {
        // 定义更新函数
        updateComponent = () => {
          // 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
          vm._update(vm._render(), hydrating)
        }
      }
      // we set this to vm._watcher inside the watcher's constructor
      // since the watcher's initial patch may call $forceUpdate (e.g. inside child
      // component's mounted hook), which relies on vm._watcher being already defined
      // 监听当前组件状态,当有数据变化时,更新组件
      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            // 数据更新引发的组件更新
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
      hydrating = false
    
      // manually mounted instance, call mounted on self
      // mounted is called for render-created child components in its inserted hook
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
      }
      return vm
    }

阅读上面代码,我们得到以下结论:

  • 会触发boforeCreate钩子
  • 定义updateComponent渲染页面视图的方法
  • 监听组件数据,一旦发生变化,触发beforeUpdate生命钩子

updateComponent方法主要执行在vue初始化时声明的renderupdate方法

render的作用主要是生成vnode

源码位置:src\core\instance\render.js

    // 定义vue 原型上的render方法
    Vue.prototype._render = function (): VNode {
        const vm: Component = this
        // render函数来自于组件的option
        const { render, _parentVnode } = vm.$options
    
        if (_parentVnode) {
            vm.$scopedSlots = normalizeScopedSlots(
                _parentVnode.data.scopedSlots,
                vm.$slots,
                vm.$scopedSlots
            )
        }
    
        // set parent vnode. this allows render functions to have access
        // to the data on the placeholder node.
        vm.$vnode = _parentVnode
        // render self
        let vnode
        try {
            // There's no need to maintain a stack because all render fns are called
            // separately from one another. Nested component's render fns are called
            // when parent component is patched.
            currentRenderingInstance = vm
            // 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode
            vnode = render.call(vm._renderProxy, vm.$createElement)
        } catch (e) {
            handleError(e, vm, `render`)
            // return error render result,
            // or previous vnode to prevent render error causing blank component
            /* istanbul ignore else */
            if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
                try {
                    vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
                } catch (e) {
                    handleError(e, vm, `renderError`)
                    vnode = vm._vnode
                }
            } else {
                vnode = vm._vnode
            }
        } finally {
            currentRenderingInstance = null
        }
        // if the returned array contains only a single node, allow it
        if (Array.isArray(vnode) && vnode.length === 1) {
            vnode = vnode[0]
        }
        // return empty vnode in case the render function errored out
        if (!(vnode instanceof VNode)) {
            if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
                warn(
                    'Multiple root nodes returned from render function. Render function ' +
                    'should return a single root node.',
                    vm
                )
            }
            vnode = createEmptyVNode()
        }
        // set parent
        vnode.parent = _parentVnode
        return vnode
    }

_update主要功能是调用patch,将vnode转换为真实DOM,并且更新到页面中

源码位置:src\core\instance\lifecycle.js

    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
        const vm: Component = this
        const prevEl = vm.$el
        const prevVnode = vm._vnode
        // 设置当前激活的作用域
        const restoreActiveInstance = setActiveInstance(vm)
        vm._vnode = vnode
        // Vue.prototype.__patch__ is injected in entry points
        // based on the rendering backend used.
        if (!prevVnode) {
          // initial render
          // 执行具体的挂载逻辑
          vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
        } else {
          // updates
          vm.$el = vm.__patch__(prevVnode, vnode)
        }
        restoreActiveInstance()
        // update __vue__ reference
        if (prevEl) {
          prevEl.__vue__ = null
        }
        if (vm.$el) {
          vm.$el.__vue__ = vm
        }
        // if parent is an HOC, update its $el as well
        if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
          vm.$parent.$el = vm.$el
        }
        // updated hook is called by the scheduler to ensure that children are
        // updated in a parent's updated hook.
      }

2. 结论

  • new Vue的时候调用会调用_init方法
    • 定义 $set$get$delete$watch 等方法
    • 定义 $on$off$emit$off等事件
    • 定义 _update$forceUpdate$destroy生命周期
  • 调用$mount进行页面的挂载
  • 挂载的时候主要是通过mountComponent方法
  • 定义updateComponent更新函数
  • 执行render生成虚拟DOM
  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中

8 理解Vue运行机制全局概览

全局概览

首先我们来看一下笔者画的内部流程图。

大家第一次看到这个图一定是一头雾水的,没有关系,我们来逐个讲一下这些模块的作用以及调用关系。相信讲完之后大家对Vue.js内部运行机制会有一个大概的认识。

初始化及挂载

new Vue() 之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 settergetter 函数,用来实现「响应式 」以及「依赖收集 」,后面会详细讲到,这里只要有一个印象即可。

初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「编译 」步骤。

编译

compile编译可以分成 parseoptimizegenerate 三个阶段,最终需要得到 render function

1. parse

parse 会用正则等方式解析 template 模板中的指令、classstyle等数据,形成AST

2. optimize

optimize 的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。

3. generate

generate 是将 AST 转化成 render function 字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。

  • 在经历过 parseoptimizegenerate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。

响应式

接下来也就是 Vue.js 响应式核心部分。

这里的 gettersetter 已经在之前介绍过了,在 init 的时候通过 Object.defineProperty 进行了绑定,它使得当被设置的对象被读取的时候会执行 getter 函数,而在当被赋值的时候会执行 setter 函数。

  • render function 被渲染的时候,因为会读取所需对象的值,所以会触发 getter 函数进行「依赖收集 」,「依赖收集 」的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Depsubs 中。形成如下所示的这样一个关系。

在修改对象的值的时候,会触发对应的 settersetter 通知之前「依赖收集 」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch 的过程以及使用队列来异步更新的策略,这个我们后面再讲。

Virtual DOM

我们知道,render function 会被转化成 VNode 节点。Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

比如说下面这样一个例子:

    {
        tag: 'div',                 /*说明这是一个div标签*/
        children: [                 /*存放该标签的子节点*/
            {
                tag: 'a',           /*说明这是一个a标签*/
                text: 'click me'    /*标签的内容*/
            }
        ]
    }

渲染后可以得到

    <div>
        <a>click me</a>
    </div>

这只是一个简单的例子,实际上的节点有更多的属性来标志节点,比如 isStatic (代表是否为静态节点)、 isComment (代表是否为注释节点)等。

更新视图

  • 前面我们说到,在修改一个对象值的时候,会通过 setter -> Watcher -> update 的流程来修改对应的视图,那么最终是如何更新视图的呢?
  • 当数据变化后,执行 render function 就可以得到一个新的 VNode 节点,我们如果想要得到新的视图,最简单粗暴的方法就是直接解析这个新的 VNode 节点,然后用 innerHTML 直接全部渲染到真实 DOM 中。但是其实我们只对其中的一小块内容进行了修改,这样做似乎有些「浪费 」。
  • 那么我们为什么不能只修改那些「改变了的地方」呢?这个时候就要介绍我们的「patch 」了。我们会将新的 VNode 与旧的 VNode 一起传入 patch 进行比较,经过 diff 算法得出它们的「差异 」。最后我们只需要将这些「差异 」的对应 DOM 进行修改即可。

再看全局

回过头再来看看这张图,是不是大脑中已经有一个大概的脉络了呢?

9 如何理解Vue中模板编译原理

Vue 的编译过程就是将 template 转化为 render 函数的过程

  • 解析生成AST树template模板转化成AST语法树,使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理
  • 标记优化 对静态语法做静态标记 markup(静态节点如div下有p标签内容不会变化) diff来做优化 静态节点跳过diff操作
    • Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用
    • 等待后续节点更新,如果是静态的,不会在比较children
  • 代码生成 编译的最后一步是将优化后的AST树转换为可执行的代码

回答范例

思路

  • 引入vue编译器概念
  • 说明编译器的必要性
  • 阐述编译器工作流程

回答范例

  1. Vue中有个独特的编译器模块,称为compiler,它的主要作用是将用户编写的template编译为js中可执行的render函数。
  2. 之所以需要这个编译过程是为了便于前端能高效的编写视图模板。相比而言,我们还是更愿意用HTML来编写视图,直观且高效。手写render函数不仅效率底下,而且失去了编译期的优化能力。
  3. Vue中编译器会先对template进行解析,这一步称为parse,结束之后会得到一个JS对象,我们称为抽象语法树AST ,然后是对AST进行深加工的转换过程,这一步成为transform,最后将前面得到的AST生成为JS代码,也就是render函数

可能的追问

  1. Vue中编译器何时执行?

new Vue()之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 propsmethodsdatacomputedwatch等。其中最重要的是通过 Object.defineProperty 设置 settergetter 函数,用来实现「响应式」以及「依赖收集」

  • 初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「编译」步骤
  • compile编译可以分成 parseoptimizegenerate 三个阶段,最终需要得到 render function
  1. React有没有编译器?

react 使用babelJSX语法解析

    <div id="app"></div>
    <script>
        let vm = new Vue({
            el: '#app',
            template: `<div>
                // <span>hello world</span> 是静态节点
                <span>hello world</span>    
                // <p>{{name}}</p> 是动态节点
                <p>{{name}}</p>
            </div>`,
            data() {
              return { name: 'test' }
            }
        });
    </script>

源码分析

    export function compileToFunctions(template) {
      // 我们需要把html字符串变成render函数
      // 1.把html代码转成ast语法树  ast用来描述代码本身形成树结构 不仅可以描述html 也能描述css以及js语法
      // 很多库都运用到了ast 比如 webpack babel eslint等等
      let ast = parse(template);
      // 2.优化静态节点:对ast树进行标记,标记静态节点
        if (options.optimize !== false) {
          optimize(ast, options);
        }
    
      // 3.通过ast 重新生成代码
      // 我们最后生成的代码需要和render函数一样
      // 类似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))
      // _c代表创建元素 _v代表创建文本 _s代表文Json.stringify--把对象解析成文本
      let code = generate(ast);
      //   使用with语法改变作用域为this  之后调用render函数可以使用call改变this 方便code里面的变量取值
      let renderFn = new Function(`with(this){return ${code}}`);
      return renderFn;
    }

Vue complier 实现

  • 模板解析这种事,本质是将数据转化为一段 html ,最开始出现在后端,经过各种处理吐给前端。随着各种 mv* 的兴起,模板解析交由前端处理。
  • 总的来说,Vue complier 是将 template 转化成一个 render 字符串。

可以简单理解成以下步骤:

  • parse 过程,将 template 利用正则转化成AST 抽象语法树。
  • optimize 过程,标记静态节点,后 diff 过程跳过静态节点,提升性能。
  • generate 过程,生成 render 字符串

10 Vue生命周期相关

Vue的生命周期方法有哪些

  1. Vue 实例有一个完整的生命周期,也就是从开始创建初始化数据编译模版挂载Dom -> 渲染更新 -> 渲染卸载等一系列过程,我们称这是Vue的生命周期
  2. Vue 生命周期总共分为8个阶段创建前/后载入前/后更新前/后销毁前/后

beforeCreate => created => beforeMount => Mounted => beforeUpdate => updated => beforeDestroy => destroyedkeep-alive下:activateddeactivated

生命周期vue2生命周期vue3描述
beforeCreatebeforeCreate在实例初始化之后,数据观测(data observer) 之前被调用。
createdcreated实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。这里没有$el
beforeMountbeforeMount在挂载开始之前被调用:相关的 render 函数首次被调用
mountedmountedel 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子
beforeUpdatebeforeUpdate组件数据更新之前调用,发生在虚拟 DOM 打补丁之前
updatedupdated由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子
beforeDestroybeforeUnmount实例销毁之前调用。在这一步,实例仍然完全可用
destroyedunmounted实例销毁后调用。调用后, Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。 该钩子在服务器端渲染期间不被调用。

其他几个生命周期

生命周期vue2生命周期vue3描述
activatedactivatedkeep-alive专属,组件被激活时调用
deactivateddeactivatedkeep-alive专属,组件被销毁时调用
errorCapturederrorCaptured捕获一个来自子孙组件的错误时被调用
  • | renderTracked | 调试钩子,响应式依赖被收集时调用

  • | renderTriggered | 调试钩子,响应式依赖被触发时调用

  • | serverPrefetch | ssr only,组件实例在服务器上被渲染前调用

    1. 要掌握每个生命周期内部可以做什么事
    • beforeCreate 初始化vue实例,进行数据观测。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
    • created 组件初始化完毕,可以访问各种数据,获取接口数据等
    • beforeMount 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上
    • mounted 实例已经挂载完成,可以进行一些DOM操作
    • beforeUpdate 更新前,可用于获取更新前各种状态。此时view层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
    • updated 完成view层的更新,更新后,所有状态已是最新。可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
    • destroyed 可以执行一些优化操作,清空定时器,解除绑定事件
    • vue3 beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消
    • vue3 unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

    <div id="app">{{name}}</div>
    <script>
        const vm = new Vue({
            data(){
                return {name:'poetries'}
            },
            el: '#app',
            beforeCreate(){
                // 数据观测(data observer) 和 event/watcher 事件配置之前被调用。
                console.log('beforeCreate');
            },
            created(){
                // 属性和方法的运算, watch/event 事件回调。这里没有$el
                console.log('created')
            },
            beforeMount(){
                // 相关的 render 函数首次被调用。
                console.log('beforeMount')
            },
            mounted(){
                // 被新创建的 vm.$el 替换
                console.log('mounted')
            },
            beforeUpdate(){
                //  数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
                console.log('beforeUpdate')
            },
            updated(){
                //  由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
                console.log('updated')
            },
            beforeDestroy(){
                // 实例销毁之前调用 实例仍然完全可用
                console.log('beforeDestroy')
            },
            destroyed(){ 
                // 所有东西都会解绑定,所有的事件监听器会被移除
                console.log('destroyed')
            }
        });
        setTimeout(() => {
            vm.name = 'poetry';
            setTimeout(() => {
                vm.$destroy()  
            }, 1000);
        }, 1000);
    </script>
  1. 组合式API生命周期钩子

你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。

下表包含如何在 setup() 内部调用生命周期钩子:

选项式 APIHook inside setup
beforeCreate不需要*
created不需要*
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeUnmountonBeforeUnmount
unmountedonUnmounted
errorCapturedonErrorCaptured
renderTrackedonRenderTracked
renderTriggeredonRenderTriggered

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写

    export default {
      setup() {
        // mounted
        onMounted(() => {
          console.log('Component is mounted!')
        })
      }
    }

setupcreated谁先执行?

  • beforeCreate:组件被创建出来,组件的methodsdata还没初始化好
  • setup:在beforeCreatecreated之前执行
  • created:组件被创建出来,组件的methodsdata已经初始化好了

由于在执行setup的时候,created还没有创建好,所以在setup函数内我们是无法使用datamethods的。所以vue为了让我们避免错误的使用,直接将setup函数内的this执行指向undefined

    import { ref } from "vue"
    export default {
      // setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去
      setup(){
        let count = ref(1)
        function myFn(){
          count.value +=1
        }
        return {count,myFn}
      },
      
    }
  1. 其他问题
  • 什么是vue生命周期? Vue 实例从创建到销毁的过程,就是生命周期。从开始创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、销毁等一系列过程,称之为 Vue 的生命周期。
  • vue生命周期的作用是什么? 它的生命周期中有多个事件钩子,让我们在控制整个Vue实例的过程时更容易形成好的逻辑。
  • vue生命周期总共有几个阶段? 它可以总共分为8个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。
  • 第一次页面加载会触发哪几个钩子? 会触发下面这几个beforeCreatecreatedbeforeMountmounted
  • 你的接口请求一般放在哪个生命周期中? 接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created
  • DOM 渲染在哪个周期中就已经完成?mounted中,
    • 注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted
          mounted: function () {
        this.$nextTick(function () {
            // Code that will run only after the
            // entire view has been rendered
        })
      }

父组件可以监听到子组件的生命周期吗

比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:

    // Parent.vue
    <Child @mounted="doSomething"/>
        
    // Child.vue
    mounted() {
      this.$emit("mounted");
    }

以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:

    //  Parent.vue
    <Child @hook:mounted="doSomething" ></Child>
    
    doSomething() {
       console.log('父组件监听到 mounted 钩子函数 ...');
    },
        
    //  Child.vue
    mounted(){
       console.log('子组件触发 mounted 钩子函数 ...');
    },    
        
    // 以上输出顺序为:
    // 子组件触发 mounted 钩子函数 ...
    // 父组件监听到 mounted 钩子函数 ...     

当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:createdupdated 等都可以监听

Vue生命周期钩子是如何实现的

  • vue的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法
  • 内部会对钩子函数进行处理,将钩子函数维护成数组的形式

Vue 的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)

    <script>
        // Vue.options 中会存放所有全局属性
    
        // 会用自身的 + Vue.options 中的属性进行合并
        // Vue.mixin({
        //     beforeCreate() {
        //         console.log('before 0')
        //     },
        // })
        debugger;
        const vm = new Vue({
            el: '#app',
            beforeCreate: [
                function() {
                    console.log('before 1')
                },
                function() {
                    console.log('before 2')
                }
            ]
        });
        console.log(vm);
    </script>

相关代码如下

    export function callHook(vm, hook) {
      // 依次执行生命周期对应的方法
      const handlers = vm.$options[hook];
      if (handlers) {
        for (let i = 0; i < handlers.length; i++) {
          handlers[i].call(vm); //生命周期里面的this指向当前实例
        }
      }
    }
    
    // 调用的时候
    Vue.prototype._init = function (options) {
      const vm = this;
      vm.$options = mergeOptions(vm.constructor.options, options);
      callHook(vm, "beforeCreate"); //初始化数据之前
      // 初始化状态
      initState(vm);
      callHook(vm, "created"); //初始化数据之后
      if (vm.$options.el) {
        vm.$mount(vm.$options.el);
      }
    };
    
    // 销毁实例实现
    Vue.prototype.$destory = function() {
    	 // 触发钩子
        callHook(vm, 'beforeDestory')
        // 自身及子节点
        remove() 
        // 删除依赖
        watcher.teardown() 
        // 删除监听
        vm.$off() 
        // 触发钩子
        callHook(vm, 'destoryed')
    }

原理流程图

Vue 的父子组件生命周期钩子函数执行顺序

  • 渲染顺序 :先父后子,完成顺序:先子后父
  • 更新顺序 :父更新导致子更新,子更新完成后父
  • 销毁顺序 :先父后子,完成顺序:先子后父

加载渲染过程

beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted子组件先挂载,然后到父组件

子组件更新过程

beforeUpdate->子 beforeUpdate->子 updated->父 updated

父组件更新过程

beforeUpdate->父 updated

销毁过程

beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed

之所以会这样是因为Vue创建过程是一个递归过程,先创建父组件,有子组件就会创建子组件,因此创建时先有父组件再有子组件;子组件首次创建时会添加mounted钩子到队列,等到patch结束再执行它们,可见子组件的mounted钩子是先进入到队列中的,因此等到patch结束执行这些钩子时也先执行。

    function patch (oldVnode, vnode, hydrating, removeOnly) { 
        if (isUndef(vnode)) { 
          if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return 
        }
        let isInitialPatch = false 
        const insertedVnodeQueue = [] // 定义收集所有组件的insert hook方法的数组 // somthing ... 
        createElm( 
            vnode, 
            insertedVnodeQueue, oldElm._leaveCb ? null : parentElm, 
            nodeOps.nextSibling(oldElm) 
        )// somthing... 
        // 最终会依次调用收集的insert hook 
        invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
        return vnode.elm
    }
    
    function createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { 
        // createChildren 会递归创建儿子组件 
        createChildren(vnode, children, insertedVnodeQueue) // something... 
    } 
    // 将组件的vnode插入到数组中 
    function invokeCreateHooks (vnode, insertedVnodeQueue) { 
        for (let i = 0; i < cbs.create.length; ++i) { 
            cbs.create[i](emptyNode, vnode) 
        }
        i = vnode.data.hook // Reuse variable 
        if (isDef(i)) { 
            if (isDef(i.create)) i.create(emptyNode, vnode) 
            if (isDef(i.insert)) insertedVnodeQueue.push(vnode) 
        } 
    } 
    // insert方法中会依次调用mounted方法 
    insert (vnode: MountedComponentVNode) { 
        const { context, componentInstance } = vnode 
        if (!componentInstance._isMounted) { 
            componentInstance._isMounted = true 
            callHook(componentInstance, 'mounted') 
        } 
    }
    function invokeInsertHook (vnode, queue, initial) { 
        // delay insert hooks for component root nodes, invoke them after the // element is really inserted 
        if (isTrue(initial) && isDef(vnode.parent)) { 
            vnode.parent.data.pendingInsert = queue 
        } else { 
            for (let i = 0; i < queue.length; ++i) { 
                queue[i].data.hook.insert(queue[i]); // 调用insert方法 
            } 
        } 
    }
    
    Vue.prototype.$destroy = function () { 
        callHook(vm, 'beforeDestroy') 
        // invoke destroy hooks on current rendered tree 
        vm.__patch__(vm._vnode, null) // 先销毁儿子 
        // fire destroyed hook 
        callHook(vm, 'destroyed') 
    }

11 Vue.mixin的使用场景和原理

  • 在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立,可以通过 Vuemixin 功能抽离公共的业务逻辑,原理类似“对象的继承”,当组件初始化时会调用 mergeOptions 方法进行合并,采用策略模式针对不同的属性进行合并。当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”;如果混入的数据和本身组件的数据冲突,会以组件的数据为准
  • mixin有很多缺陷如:命名冲突、依赖问题、数据来源问题

基本使用

    <script>
        // Vue.options
        Vue.mixin({ // 如果他是对象 每个组件都用mixin里的对象进行合并
            data(){
                return {a: 1,b: 2}
            }
        });
        // Vue.extend
        Vue.component('my',{ // 组件必须是函数 Vue.extend  => render(xxx)
            data(){
                return {x:1}
            }
        }) 
        // 没有 new 没有实例  _init()
        // const vm = this
        new Vue({
            el:'#app',
            data(){ // 根可以不是函数 
                return {c:3}
            }
        })
    </script>

相关源码

    export default function initMixin(Vue){
      Vue.mixin = function (mixin) {
        //   合并对象
          this.options=mergeOptions(this.options,mixin)
      };
    }
    };
    
    // src/util/index.js
    // 定义生命周期
    export const LIFECYCLE_HOOKS = [
      "beforeCreate",
      "created",
      "beforeMount",
      "mounted",
      "beforeUpdate",
      "updated",
      "beforeDestroy",
      "destroyed",
    ];
    
    // 合并策略
    const strats = {};
    // mixin核心方法
    export function mergeOptions(parent, child) {
      const options = {};
      // 遍历父亲
      for (let k in parent) {
        mergeFiled(k);
      }
      // 父亲没有 儿子有
      for (let k in child) {
        if (!parent.hasOwnProperty(k)) {
          mergeFiled(k);
        }
      }
    
      //真正合并字段方法
      function mergeFiled(k) {
        // strats合并策略
        if (strats[k]) {
          options[k] = strats[k](parent[k], child[k]);
        } else {
          // 默认策略
          options[k] = child[k] ? child[k] : parent[k];
        }
      }
      return options;
    }

12 Vue组件data为什么必须是个函数?

  • 根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况
  • 组件实例对象data必须为函数 .vue文件在使用的时候实际上会转换成一个class,一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件在不同的实例之间data不冲突,data必须是一个函数,

简版理解

    // 1.组件的渲染流程 调用Vue.component -> Vue.extend -> 子类 -> new 子类
    // Vue.extend 根据用户定义产生一个新的类
    function Vue() {}
    function Sub() { // 会将data存起来
        this.data = this.constructor.options.data();
    }
    Vue.extend = function(options) {
        Sub.options = options; // 静态属性
        return Sub;
    }
    let Child = Vue.extend({
        data:()=>( { name: 'test' })
    });
    
    // 两个组件就是两个实例, 希望数据互不感染
    let child1 = new Child();
    let child2 = new Child();
    
    console.log(child1.data.name);
    child1.data.name = 'poetry';
    console.log(child2.data.name);
    
    // 根不需要 任何的合并操作   根才有vm属性 所以他可以是函数和对象  但是组件mixin他们都没有vm 所以我就可以判断 当前data是不是个函数

相关源码

    // 源码位置 src/core/global-api/extend.js
    export function initExtend (Vue: GlobalAPI) {
      Vue.extend = function (extendOptions: Object): Function {
        extendOptions = extendOptions || {}
        const Super = this
        const SuperId = Super.cid
        const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
        if (cachedCtors[SuperId]) {
          return cachedCtors[SuperId]
        }
    
        const name = extendOptions.name || Super.options.name
        if (process.env.NODE_ENV !== 'production' && name) {
          validateComponentName(name)
        }
    
        const Sub = function VueComponent (options) {
          this._init(options)
        }
        // 子类继承大Vue父类的原型
        Sub.prototype = Object.create(Super.prototype)
        Sub.prototype.constructor = Sub
        Sub.cid = cid++
        Sub.options = mergeOptions(
          Super.options,
          extendOptions
        )
        Sub['super'] = Super
    
        // For props and computed properties, we define the proxy getters on
        // the Vue instances at extension time, on the extended prototype. This
        // avoids Object.defineProperty calls for each instance created.
        if (Sub.options.props) {
          initProps(Sub)
        }
        if (Sub.options.computed) {
          initComputed(Sub)
        }
    
        // allow further extension/mixin/plugin usage
        Sub.extend = Super.extend
        Sub.mixin = Super.mixin
        Sub.use = Super.use
    
        // create asset registers, so extended classes
        // can have their private assets too.
        ASSET_TYPES.forEach(function (type) {
          Sub[type] = Super[type]
        })
        // enable recursive self-lookup
        if (name) { 
          Sub.options.components[name] = Sub // 记录自己 在组件中递归自己  -> jsx
        }
    
        // keep a reference to the super options at extension time.
        // later at instantiation we can check if Super's options have
        // been updated.
        Sub.superOptions = Super.options
        Sub.extendOptions = extendOptions
        Sub.sealedOptions = extend({}, Sub.options)
    
        // cache constructor
        cachedCtors[SuperId] = Sub
        return Sub
      }
    }

13 nextTick在哪里使用?原理是?

  • nextTick 中的回调是在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM
  • 在修改数据之后立即使用这个方法,获取更新后的 DOM
  • 主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法

nextTick 方法主要是使用了宏任务和微任务,定义了一个异步方法.多次调用 nextTick 会将方法存入队列中,通过这个异步方法清空当前队列。所以这个 nextTick 方法就是异步方法

根据执行环境分别尝试采用

  • 先采用Promise
  • Promise不支持,再采用MutationObserver
  • MutationObserver不支持,再采用setImmediate
  • 如果以上都不行则采用setTimeout
  • 最后执行flushCallbacks,把callbacks里面的数据依次执行

回答范例

  1. nextTick 中的回调是在下次 DOM 更新循环结束之后执行延迟回调,用于获得更新后的 DOM
  2. Vue有个异步更新策略,意思是如果数据变化,Vue不会立刻更新DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。这一策略导致我们对数据的修改不会立刻体现在DOM上,此时如果想要获取更新后的DOM状态,就需要使用nextTick
  3. 开发时,有两个场景我们会用到nextTick
  • created中想要获取DOM
  • 响应式数据变化后获取DOM更新后的状态,比如希望获取列表更新后的高度
  1. nextTick签名如下:function nextTick(callback?: () => void): Promise<void>

所以我们只需要在传入的回调函数中访问最新DOM状态即可,或者我们可以await nextTick()方法返回的Promise之后做这件事

  1. Vue内部,nextTick之所以能够让我们看到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取到最新的DOM值

基本使用

    const vm = new Vue({
        el: '#app',
        data() {
            return { a: 1 }
        }
    }); 
    
    // vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue]
    //     console.log(vm.$el.innerHTML)
    // })
    
    // 是将内容维护到一个数组里,最终按照顺序顺序。 第一次会开启一个异步任务
    
    vm.a = 'test'; // 修改了数据后并不会马上更新视图
    vm.$nextTick(() => {// [nextTick回调函数fn,内部更新flushSchedulerQueue]
        console.log(vm.$el.innerHTML)
    })
    
    // nextTick中的方法会被放到 更新页面watcher的后面去

相关代码如下

    // src/core/utils/nextTick
    let callbacks = [];
    let pending = false;
    function flushCallbacks() {
      pending = false; //把标志还原为false
      // 依次执行回调
      for (let i = 0; i < callbacks.length; i++) {
        callbacks[i]();
      }
    }
    let timerFunc; //定义异步方法  采用优雅降级
    if (typeof Promise !== "undefined") {
      // 如果支持promise
      const p = Promise.resolve();
      timerFunc = () => {
        p.then(flushCallbacks);
      };
    } else if (typeof MutationObserver !== "undefined") {
      // MutationObserver 主要是监听dom变化 也是一个异步方法
      let counter = 1;
      const observer = new MutationObserver(flushCallbacks);
      const textNode = document.createTextNode(String(counter));
      observer.observe(textNode, {
        characterData: true,
      });
      timerFunc = () => {
        counter = (counter + 1) % 2;
        textNode.data = String(counter);
      };
    } else if (typeof setImmediate !== "undefined") {
      // 如果前面都不支持 判断setImmediate
      timerFunc = () => {
        setImmediate(flushCallbacks);
      };
    } else {
      // 最后降级采用setTimeout
      timerFunc = () => {
        setTimeout(flushCallbacks, 0);
      };
    }
    
    export function nextTick(cb) {
      // 除了渲染watcher  还有用户自己手动调用的nextTick 一起被收集到数组
      callbacks.push(cb);
      if (!pending) {
        // 如果多次调用nextTick  只会执行一次异步 等异步队列清空之后再把标志变为false
        pending = true;
        timerFunc();
      }
    }

数据更新的时候内部会调用nextTick

    // src/core/observer/scheduler.js
    
    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id
      if (has[id] == null) {
        has[id] = true
        if (!flushing) {
          queue.push(watcher)
        } else {
          // if already flushing, splice the watcher based on its id
          // if already past its id, it will be run next immediately.
          let i = queue.length - 1
          while (i > index && queue[i].id > watcher.id) {
            i--
          }
          queue.splice(i + 1, 0, watcher)
        }
        // queue the flush
        if (!waiting) {
          waiting = true
    
          if (process.env.NODE_ENV !== 'production' && !config.async) {
            flushSchedulerQueue()
            return
          }
          // 把更新方法放到数组中维护[nextTick回调函数,更新函数flushSchedulerQueue]
          /**
           * vm.a = 'test'; // 修改了数据后并不会马上更新视图
            vm.$nextTick(() => {// [fn,更新]
                console.log(vm.$el.innerHTML)
            })
           */
          nextTick(flushSchedulerQueue)
        }
      }
    }

14 computed和watch相关

computed和watch区别

  1. 当页面中有某些数据依赖其他数据进行变动的时候,可以使用计算属性computed

Computed本质是一个具备缓存的watcher,依赖的属性发生变化就会更新视图。 适用于计算比较消耗性能的计算场景。当表达式过于复杂时,在模板中放入过多逻辑会让模板难以维护,可以将复杂的逻辑放入计算属性中处理

    <template>{{fullName}}</template>
    export default {
        data(){
            return {
                firstName: 'zhang',
                lastName: 'san',
            }
        },
        computed:{
            fullName: function(){
                return this.firstName + ' ' + this.lastName
            }
        }
    }
  1. watch用于观察和监听页面上的vue实例,如果要在数据变化的同时进行异步操作或者是比较大的开销,那么watch为最佳选择

Watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听,如果没有写到组件中,不要忘记使用unWatch手动注销

    <template>{{fullName}}</template>
    export default {
        data(){
            return {
                firstName: 'zhang',
                lastName: 'san',
                fullName: 'zhang san'
            }
        },
        watch:{
            firstName(val) {
                this.fullName = val + ' ' + this.lastName
            },
            lastName(val) {
                this.fullName = this.firstName + ' ' + val
            }
        }
    }

computed:

  • computed是计算属性,也就是计算值,它更多用于计算值的场景
  • computed具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算
  • computed适用于计算比较消耗性能的计算场景

watch:

  • 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察props $emit或者本组件的值,当数据变化时来执行回调进行后续操作
  • 无缓存性,页面重新渲染时值不变化也会执行

小结:

  • computedwatch都是基于watcher来实现的
  • computed属性是具备缓存的,依赖的值不发生变化,对其取值时计算属性方法不会重新执行
  • watch是监控值的变化,当值发生变化时调用其对应的回调函数
  • 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed
  • 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化

回答范例

思路分析

  • 先看computed, watch两者定义,列举使用上的差异
  • 列举使用场景上的差异,如何选择
  • 使用细节、注意事项
  • vue3变化

computed特点:具有响应式的返回值

    const count = ref(1)
    const plusOne = computed(() => count.value + 1)

watch特点:侦测变化,执行回调

    const state = reactive({ count: 0 })
    watch(
      () => state.count,
      (count, prevCount) => {
        /* ... */
      }
    )

回答范例

  1. 计算属性可以从组件数据派生出新数据,最常见的使用方式是设置一个函数,返回计算之后的结果,computedmethods的差异是它具备缓存性,如果依赖项不变时不会重新计算。侦听器可以侦测某个响应式数据的变化并执行副作用,常见用法是传递一个函数,执行副作用,watch没有返回值,但可以执行异步操作等复杂逻辑
  2. 计算属性常用场景是简化行内模板中的复杂表达式,模板中出现太多逻辑会是模板变得臃肿不易维护。侦听器常用场景是状态变化之后做一些额外的DOM操作或者异步操作。选择采用何用方案时首先看是否需要派生出新值,基本能用计算属性实现的方式首选计算属性.
  3. 使用过程中有一些细节,比如计算属性也是可以传递对象,成为既可读又可写的计算属性。watch可以传递对象,设置deepimmediate等选项
  4. vue3watch选项发生了一些变化,例如不再能侦测一个点操作符之外的字符串形式的表达式; reactivity API中新出现了watchwatchEffect可以完全替代目前的watch选项,且功能更加强大

基本使用

    // src/core/observer:45;
    
    // 渲染watcher  /  computed watcher  /  watch
    const vm = new Vue({
        el: '#app',
        data: {
            firstname:'张',
            lastname:'三'
        },
        computed:{ // watcher  =>   firstname lastname
            // computed 只有取值时才执行
    
            // Object.defineProperty .get
            fullName(){ // firstName lastName 会收集fullName计算属性
                return this.firstname + this.lastname
            }
        },
        watch:{
            firstname(newVal,oldVal){
                console.log(newVal)
            }
        }
    });
    
    setTimeout(() => {
        debugger;
        vm.firstname = '赵'
    }, 1000);

相关源码

    // 初始化state
    function initState (vm: Component) {
      vm._watchers = []
      const opts = vm.$options
      if (opts.props) initProps(vm, opts.props)
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        initData(vm)
      } else {
        observe(vm._data = {}, true /* asRootData */)
      }
    
      // 初始化计算属性
      if (opts.computed) initComputed(vm, opts.computed) 
    
      // 初始化watch
      if (opts.watch && opts.watch !== nativeWatch) { 
        initWatch(vm, opts.watch)
      }
    }
    
    // 计算属性取值函数
    function createComputedGetter (key) {
      return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          if (watcher.dirty) { // 如果值依赖的值发生变化,就会进行重新求值
            watcher.evaluate(); // this.firstname lastname
          }
          if (Dep.target) { // 让计算属性所依赖的属性 收集渲染watcher
            watcher.depend()
          }
          return watcher.value
        }
      }
    }
    
    // watch的实现
    Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
      ): Function {
        const vm: Component = this
        debugger;
        if (isPlainObject(cb)) {
          return createWatcher(vm, expOrFn, cb, options)
        }
        options = options || {}
        options.user = true
        const watcher = new Watcher(vm, expOrFn, cb, options) // 创建watcher,数据更新调用cb
        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()
        }
    }

vue3中 watch、watchEffect区别

  • watch是惰性执行,也就是只有监听的值发生变化的时候才会执行,但是watchEffect不同,每次代码加载watchEffect都会执行(忽略watch第三个参数的配置,如果修改配置项也可以实现立即执行)
  • watch需要传递监听的对象,watchEffect不需要
  • watch只能监听响应式数据:ref定义的属性和reactive定义的对象,如果直接监听reactive定义对象中的属性是不允许的(会报警告),除非使用函数转换一下。其实就是官网上说的监听一个getter
  • watchEffect如果监听reactive定义的对象是不起作用的,只能监听对象中的属性

看一下watchEffect的代码

    <template>
    <div>
      请输入firstName:
      <input type="text" v-model="firstName">
    </div>
    <div>
      请输入lastName:
      <input type="text" v-model="lastName">
    </div>
    <div>
      请输入obj.text:
      <input type="text" v-model="obj.text">
    </div>
     <div>
     【obj.text】 {{obj.text}}
     </div>
    </template>
    
    <script>
    import {ref, reactive, watch, watchEffect} from 'vue'
    export default {
      name: "HelloWorld",
      props: {
        msg: String,
      },
      setup(props,content){
        let firstName = ref('')
        let lastName = ref('')
        let obj= reactive({
          text:'hello'
        })
        watchEffect(()=>{
          console.log('触发了watchEffect');
          console.log(`组合后的名称为:${firstName.value} ${lastName.value}`)
        })
        return{
          obj,
          firstName,
          lastName
        }
      }
    };
    </script>

改造一下代码

    watchEffect(()=>{
      console.log('触发了watchEffect');
      // 这里我们不使用firstName.value/lastName.value ,相当于是监控整个ref,对应第四点上面的结论
      console.log(`组合后的名称为:${firstName} ${lastName}`)
    })

    watchEffect(()=>{
      console.log('触发了watchEffect');
      console.log(obj);
    })

稍微改造一下

    let obj = reactive({
      text:'hello'
    })
    watchEffect(()=>{
      console.log('触发了watchEffect');
      console.log(obj.text);
    })

再看一下watch的代码,验证一下

    let obj= reactive({
      text:'hello'
    })
    // watch是惰性执行, 默认初始化之后不会执行,只有值有变化才会触发,可通过配置参数实现默认执行
    watch(obj, (newValue, oldValue) => {
      // 回调函数
      console.log('触发监控更新了new',  newValue);
      console.log('触发监控更新了old',  oldValue);
    },{
      // 配置immediate参数,立即执行,以及深层次监听
      immediate: true,
      deep: true
    })

  • 监控整个reactive对象,从上面的图可以看到 deep 实际默认是开启的,就算我们设置为false也还是无效。而且旧值获取不到。
  • 要获取旧值则需要监控对象的属性,也就是监听一个getter,看下图

总结

  • 如果定义了reactive的数据,想去使用watch监听数据改变,则无法正确获取旧值,并且deep属性配置无效,自动强制开启了深层次监听。
  • 如果使用 ref 初始化一个对象或者数组类型的数据,会被自动转成reactive的实现方式,生成proxy代理对象。也会变得无法正确取旧值。
  • 用任何方式生成的数据,如果接收的变量是一个proxy代理对象,就都会导致watch这个对象时,watch回调里无法正确获取旧值。
  • 所以当大家使用watch监听对象时,如果在不需要使用旧值的情况,可以正常监听对象没关系;但是如果当监听改变函数里面需要用到旧值时,只能监听 对象.xxx`属性 的方式才行

watch和watchEffect异同总结

体验

watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数

    const count = ref(0)

    watchEffect(() => console.log(count.value))
    // -> logs 0

    count.value++
    // -> logs 1

watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数

    const state = reactive({ count: 0 })
    watch(
      () => state.count,
      (count, prevCount) => {
        /* ... */
      }
    )

回答范例

  1. watchEffect立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数
  2. watchEffect(effect)是一种特殊watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么watchEffect就是我们需要的。watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch
  3. watchEffect在使用时,传入的函数会立刻执行一次。watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项
  4. 从实现上来说,watchEffect(fn)相当于watch(fn,fn,{immediate:true})

watchEffect定义如下

    export function watchEffect(
      effect: WatchEffect,
      options?: WatchOptionsBase
    ): WatchStopHandle {
      return doWatch(effect, null, options)
    }

watch定义如下

    export function watch<T = any, Immediate extends Readonly<boolean> = false>(
      source: T | WatchSource<T>,
      cb: any,
      options?: WatchOptions<Immediate>
    ): WatchStopHandle {
      return doWatch(source as any, cb, options)
    }

很明显watchEffect就是一种特殊的watch实现。

Watch中的deep:true是如何实现的

当用户指定了 watch 中的deep属性为 true 时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前 watcher存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新

源码相关

    get () { 
        pushTarget(this) // 先将当前依赖放到 Dep.target上 
        let value 
        const 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 { 
            if (this.deep) { // 如果需要深度监控 
            traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法 
        }
        popTarget() 
    }

Vue computed 实现

  • 建立与其他属性(如:dataStore)的联系;
  • 属性改变后,通知计算属性重新计算

实现时,主要如下

  • 初始化 data, 使用 Object.defineProperty 把这些属性全部转为 getter/setter
  • 初始化 computed, 遍历 computed 里的每个属性,每个 computed 属性都是一个 watch 实例。每个属性提供的函数作为属性的 getter,使用 Object.defineProperty 转化。
  • Object.defineProperty getter 依赖收集。用于依赖发生变化时,触发属性重新计算。
  • 若出现当前 computed 计算属性嵌套其他 computed 计算属性时,先进行其他的依赖收集

watch 原理

watch 本质上是为每个监听属性 setter 创建了一个 watcher,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deepimmediate,对应原理如下

  • deep:深度监听对象,为对象的每一个属性创建一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象 setter,因此引入 deep 能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。
  • immediate:在初始化时直接调用回调函数,可以通过在 created 阶段手动调用回调函数实现相同的效果

15 Vue.set的实现原理

  • 给对象和数组本身都增加了dep属性
  • 当给对象新增不存在的属性则触发对象依赖的watcher去更新
  • 当修改数组索引时,我们调用数组本身的splice去更新数组(数组的响应式原理就是重新了splice等方法,调用splice就会触发视图更新)

基本使用

以下方法调用会改变原始数组:push(), pop(), shift(), unshift(), splice(), sort(), reverse(),Vue.set( target, key, value )

  • 调用方法:Vue.set(target, key, value )
    • target:要更改的数据源(可以是对象或者数组)
    • key:要更改的具体数据
    • value :重新赋的值
    <div id="app">{{user.name}} {{user.age}}</div>
    <div id="app"></div>
    <script>
        // 1. 依赖收集的特点:给每个属性都增加一个dep属性,dep属性会进行收集,收集的是watcher
        // 2. vue会给每个对象也增加一个dep属性
        const vm = new Vue({
            el: '#app',
            data: { // vm._data  
                user: {name:'poetry'}
            }
        });
        // 对象的话:调用defineReactive在user对象上定义一个age属性,增加到响应式数据中,触发对象本身的watcher,ob.dep.notify()更新 
        // 如果是数组 通过调用 splice方法,触发视图更新
        vm.$set(vm.user, 'age', 20); // 不能给根属性添加,因为给根添加属性 性能消耗太大,需要做很多处理
    
        // 修改肯定是同步的 -> 更新都是一步的  queuewatcher
    </script>

相关源码

    // src/core/observer/index.js 44
    export class Observer { // new Observer(value)
      value: any;
      dep: Dep;
      vmCount: number; // number of vms that have this object as root $data
    
      constructor (value: any) {
        this.value = value
        this.dep = new Dep() // 给所有对象类型增加dep属性
      }
    }
    // src/core/observer/index.js 201
    export function set (target: Array<any> | Object, key: any, val: any): any {
      // 1.是开发环境 target 没定义或者是基础类型则报错
      if (process.env.NODE_ENV !== 'production' &&
        (isUndef(target) || isPrimitive(target))
      ) {
        warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
      }
      // 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
      if (Array.isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key)
        // 利用数组的splice变异方法触发响应式  
        target.splice(key, 1, val)
        return val
      }
      // 3.如果是对象本身的属性,则直接添加即可
      if (key in target && !(key in Object.prototype)) {
        target[key] = val // 直接修改属性值  
        return val
      }
      // 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)
      const ob = (target: any).__ob__
      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
      }
      // 5.如果不是响应式的也不需要将其定义成响应式属性
      if (!ob) {
        target[key] = val
        return val
      }
      // 6.将属性定义成响应式的
      defineReactive(ob.value, key, val)
      // 通知视图更新
      ob.dep.notify()
      return val
    }

我们阅读以上源码可知,vm.$set 的实现原理是:

  • 如果目标是数组 ,直接使用数组的 splice 方法触发响应式;
  • 如果目标是对象 ,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 gettersetter 的功能所调用的方法)

16 Vue diff算法相关问题

Vue为什么需要虚拟DOM?优缺点有哪些

由于在浏览器中操作 DOM 是很昂贵的。频繁的操作 DOM,会产生一定的性能问题。这就是虚拟 Dom 的产生原因。Vue2Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象

优点:

  • 保证性能下限 : 框架的虚拟 DOM 需要适配任何上层 API 可能产生的操作,它的一些 DOM 操作的实现必须是普适的,所以它的性能并不是最优的;但是比起粗暴的 DOM 操作性能要好很多,因此框架的虚拟 DOM 至少可以保证在你不需要手动优化的情况下,依然可以提供还不错的性能,即保证性能的下限;
  • 无需手动操作 DOM : 我们不再需要手动去操作 DOM,只需要写好 View-Model 的代码逻辑,框架会根据虚拟 DOM 和 数据双向绑定,帮我们以可预期的方式更新视图,极大提高我们的开发效率;
  • 跨平台 : 虚拟 DOM 本质上是 JavaScript 对象,而 DOM 与平台强相关,相比之下虚拟 DOM 可以进行更方便地跨平台操作,例如服务器渲染、weex 开发等等。

缺点:

  • 无法进行极致优化:虽然虚拟 DOM + 合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。
  • 首次渲染大量DOM时,由于多了一层虚拟 DOM 的计算,会比 innerHTML 插入慢。

虚拟 DOM 实现原理?

虚拟 DOM 的实现原理主要包括以下 3 部分:

  • JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
  • diff 算法 — 比较两棵虚拟 DOM 树的差异;
  • pach 算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树。

说说你对虚拟 DOM 的理解?回答范例

思路

  • vdom是什么
  • 引入vdom的好处
  • vdom如何生成,又如何成为dom
  • 在后续的diff中的作用

回答范例

  1. 虚拟dom顾名思义就是虚拟的dom对象,它本身就是一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构
  2. 通过引入vdom我们可以获得如下好处:
  • 将真实元素节点抽象成VNode,有效减少直接操作 dom 次数,从而提高程序性能
    • 直接操作 dom 是有限制的,比如:diffclone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了
    • 操作 dom 是比较昂贵的操作,频繁的dom操作容易引起页面的重绘和回流,但是通过抽象 VNode 进行中间处理,可以有效减少直接操作dom的次数,从而减少页面重绘和回流
  • 方便实现跨平台
    • 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android)变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
    • Vue3 中允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台进行渲染
  1. vdom如何生成?在vue中我们常常会为组件编写模板 - template, 这个模板会被编译器 - compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom

  1. 挂载过程结束后,vue程序进入更新流程。如果某些响应式数据发生变化,将会引起组件重新render,此时就会生成新的vdom,和上一次的渲染结果diff就能得到变化的地方,从而转换为最小量的dom操作,高效更新视图

为什么要用vdom?案例解析

现在有一个场景,实现以下需求:

    [    
      { name: "张三", age: "20", address: "北京"},    
      { name: "李四", age: "21", address: "武汉"},    
      { name: "王五", age: "22", address: "杭州"},
    ]

将该数据展示成一个表格,并且随便修改一个信息,表格也跟着修改。 用jQuery实现如下:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    
    <body>
      <div id="container"></div>
      <button id="btn-change">改变</button>
    
      <script src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
      <script>
        const data = [{
            name: "张三",
            age: "20",
            address: "北京"
          },
          {
            name: "李四",
            age: "21",
            address: "武汉"
          },
          {
            name: "王五",
            age: "22",
            address: "杭州"
          },
        ];
        //渲染函数
        function render(data) {
          const $container = $('#container');
          $container.html('');
          const $table = $('<table>');
          // 重绘一次
          $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'));
          data.forEach(item => {
            //每次进入都重绘
            $table.append($(`<tr><td>${item.name}</td><td>${item.age}</td><td>${item.address}</td></tr>`))
          })
          $container.append($table);
        }
        
        $('#btn-change').click(function () {
          data[1].age = 30;
          data[2].address = '深圳';
          render(data);
        });
      </script>
    </body>
    </html>
  • 这样点击按钮,会有相应的视图变化,但是你审查以下元素,每次改动之后,table标签都得重新创建,也就是说table下面的每一个栏目,不管是数据是否和原来一样,都得重新渲染,这并不是理想中的情况,当其中的一栏数据和原来一样,我们希望这一栏不要重新渲染,因为DOM重绘相当消耗浏览器性能。
  • 因此我们采用JS对象模拟的方法,将DOM的比对操作放在JS层,减少浏览器不必要的重绘,提高效率。
  • 当然有人说虚拟DOM并不比真实的DOM快,其实也是有道理的。当上述table中的每一条数据都改变时,显然真实的DOM操作更快,因为虚拟DOM还存在jsdiff算法的比对过程。所以,上述性能优势仅仅适用于大量数据的渲染并且改变的数据只是一小部分的情况。

如下DOM结构:

    <ul id="list">
        <li class="item">Item1</li>
        <li class="item">Item2</li>
    </ul>

映射成虚拟DOM就是这样:

    {
      tag: "ul",
      attrs: {
        id:&emsp;"list"
      },
      children: [
        {
          tag: "li",
          attrs: { className: "item" },
          children: ["Item1"]
        }, {
          tag: "li",
          attrs: { className: "item" },
          children: ["Item2"]
        }
      ]
    } 

使用snabbdom实现vdom

这是一个简易的实现vdom功能的库,相比vuereact,对于vdom这块更加简易,适合我们学习vdomvdom里面有两个核心的api,一个是h函数,一个是patch函数,前者用来生成vdom对象,后者的功能在于做虚拟dom的比对和将vdom挂载到真实DOM

简单介绍一下这两个函数的用法:

    h('标签名', {属性}, [子元素])
    h('标签名', {属性}, [文本])
    patch(container, vnode) // container为容器DOM元素
    patch(vnode, newVnode)

现在我们就来用snabbdom重写一下刚才的例子:

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <title>Document</title>
    </head>
    <body>
      <div id="container"></div>
      <button id="btn-change">改变</button>
      <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
      <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
      <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
      <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
      <script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.min.js"></script>
      <script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
      <script>
        let snabbdom = window.snabbdom;
    
        // 定义patch
        let patch = snabbdom.init([
          snabbdom_class,
          snabbdom_props,
          snabbdom_style,
          snabbdom_eventlisteners
        ]);
    
        //定义h
        let h = snabbdom.h;
    
        const data = [{
            name: "张三",
            age: "20",
            address: "北京"
          },
          {
            name: "李四",
            age: "21",
            address: "武汉"
          },
          {
            name: "王五",
            age: "22",
            address: "杭州"
          },
        ];
        data.unshift({name: "姓名", age: "年龄", address: "地址"});
    
        let container = document.getElementById('container');
        let vnode;
        const render = (data) => {
          let newVnode = h('table', {}, data.map(item => { 
            let tds = [];
            for(let i in item) {
              if(item.hasOwnProperty(i)) {
                tds.push(h('td', {}, item[i] + ''));
              }
            }
            return h('tr', {}, tds);
          }));
    
          if(vnode) {
              patch(vnode, newVnode);
          } else {
              patch(container, newVnode);
          }
          vnode = newVnode;
        }
    
        render(data);
    
        let btnChnage = document.getElementById('btn-change');
        btnChnage.addEventListener('click', function() {
          data[1].age = 30;
          data[2].address = "深圳";
          //re-render
          render(data);
        })
      </script>
    </body>
    </html>

你会发现,只有改变的栏目才闪烁,也就是进行重绘 ,数据没有改变的栏目还是保持原样,这样就大大节省了浏览器重新渲染的开销

vue中使用h函数生成虚拟DOM返回

    const vm = new Vue({
      el: '#app',
      data: {
        user: {name:'poetry'}
      },
      render(h){
        // h()
        // h(App)
        // h('div',[])
        let vnode = h('div',{},'hello world');
        return vnode
      }
    });
```**相关源码**  
```js
    // src/core/vdom/create-element.js 
    
    export function createElement ( // 创建元素
      context: Component,
      tag: any,
      data: any,
      children: any,
      normalizationType: any,
      alwaysNormalize: boolean
    ): VNode | Array<VNode> {
      if (Array.isArray(data) || isPrimitive(data)) {
        normalizationType = children
        children = data
        data = undefined
      }
      if (isTrue(alwaysNormalize)) {
        normalizationType = ALWAYS_NORMALIZE
      }
      // 创建元素
      return _createElement(context, tag, data, children, normalizationType)
    }
    
    export function _createElement (
      context: Component,
      tag?: string | Class<Component> | Function | Object,
      data?: VNodeData,
      children?: any,
      normalizationType?: number
    ): VNode | Array<VNode> {
      if (isDef(data) && isDef((data: any).__ob__)) {
        process.env.NODE_ENV !== 'production' && warn(
          `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
          'Always create fresh vnode data objects in each render!',
          context
        )
        return createEmptyVNode()
      }
      // object syntax in v-bind
      if (isDef(data) && isDef(data.is)) {
        tag = data.is
      }
      if (!tag) { // 如果 h() 返回空节点
        // in case of component :is set to falsy value
        return createEmptyVNode()
      }
      // warn against non-primitive key
      if (process.env.NODE_ENV !== 'production' &&
        isDef(data) && isDef(data.key) && !isPrimitive(data.key)
      ) {
        if (!__WEEX__ || !('@binding' in data.key)) {
          warn(
            'Avoid using non-primitive value as key, ' +
            'use string/number value instead.',
            context
          )
        }
      }
      // support single function children as default scoped slot
      if (Array.isArray(children) &&
        typeof children[0] === 'function'
      ) {
        data = data || {}
        data.scopedSlots = { default: children[0] }
        children.length = 0
      }
      if (normalizationType === ALWAYS_NORMALIZE) { //  处理儿子节点个数
        children = normalizeChildren(children)
      } else if (normalizationType === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children)
      } 
      let vnode, ns
      if (typeof tag === 'string') { // 标签是字符串
        let Ctor
        ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
        if (config.isReservedTag(tag)) {
          // platform built-in elements
          if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
            warn(
              `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
              context
            )
          }
          vnode = new VNode(
            config.parsePlatformTagName(tag), data, children,
            undefined, undefined, context
          )
        } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
          // component  组件的虚拟节点
          vnode = createComponent(Ctor, data, context, children, tag)
        } else {
          // unknown or unlisted namespaced elements
          // check at runtime because it may get assigned a namespace when its
          // parent normalizes children
          vnode = new VNode(
            tag, data, children,
            undefined, undefined, context
          )
        }
      } else {
        // direct component options / constructor  组件的虚拟节点
        vnode = createComponent(tag, data, context, children)
      }
      if (Array.isArray(vnode)) {
        return vnode
      } else if (isDef(vnode)) {
        if (isDef(ns)) applyNS(vnode, ns)
        if (isDef(data)) registerDeepBindings(data)
        return vnode
      } else {
        return createEmptyVNode()
      }
    }
    
    function applyNS (vnode, ns, force) {
      vnode.ns = ns
      if (vnode.tag === 'foreignObject') {
        // use default namespace inside foreignObject
        ns = undefined
        force = true
      }
      if (isDef(vnode.children)) {
        for (let i = 0, l = vnode.children.length; i < l; i++) {
          const child = vnode.children[i]
          if (isDef(child.tag) && (
            isUndef(child.ns) || (isTrue(force) && child.tag !== 'svg'))) {
            applyNS(child, ns, force)
          }
        }
      }
    }
    
    // ref #5318
    // necessary to ensure parent re-render when deep bindings like :style and
    // :class are used on slot nodes
    function registerDeepBindings (data) {
      if (isObject(data.style)) {
        traverse(data.style)
      }
      if (isObject(data.class)) {
        traverse(data.class)
      }
    }
    // 虚拟节点的实现 src/core/vdom/vnode.js
    
    export default class VNode {
      tag: string | void;
      data: VNodeData | void;
      children: ?Array<VNode>;
      text: string | void;
      elm: Node | void;
      ns: string | void;
      context: Component | void; // rendered in this component's scope
      key: string | number | void;
      componentOptions: VNodeComponentOptions | void;
      componentInstance: Component | void; // component instance
      parent: VNode | void; // component placeholder node
    
      // strictly internal
      raw: boolean; // contains raw HTML? (server only)
      isStatic: boolean; // hoisted static node
      isRootInsert: boolean; // necessary for enter transition check
      isComment: boolean; // empty comment placeholder?
      isCloned: boolean; // is a cloned node?
      isOnce: boolean; // is a v-once node?
      asyncFactory: Function | void; // async component factory function
      asyncMeta: Object | void;
      isAsyncPlaceholder: boolean;
      ssrContext: Object | void;
      fnContext: Component | void; // real context vm for functional nodes
      fnOptions: ?ComponentOptions; // for SSR caching
      devtoolsMeta: ?Object; // used to store functional render context for devtools
      fnScopeId: ?string; // functional scope id support
    
      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
      }
    
      // DEPRECATED: alias for componentInstance for backwards compat.
      /* istanbul ignore next */
      get child (): Component | void {
        return this.componentInstance
      }
    }
    
    export const createEmptyVNode = (text: string = '') => {
      const node = new VNode()
      node.text = text
      node.isComment = true
      return node
    }
    
    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
    }

Vue中diff算法原理

DOM操作是非常昂贵的,因此我们需要尽量地减少DOM操作。这就需要找出本次DOM必须更新的节点来更新,其他的不更新,这个找出的过程,就需要应用diff算法

vuediff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针(头尾都加指针)的方式进行比较。

简单来说,Diff算法有以下过程

  • 同级比较,再比较子节点(根据keytag标签名判断)

  • 先判断一方有子节点和一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)

  • 比较都有子节点的情况(核心diff)

  • 递归比较子节点

  • 正常Diff两个树的时间复杂度是O(n^3),但实际情况下我们很少会进行跨层级的移动DOM,所以VueDiff进行了优化,从O(n^3) -> O(n),只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。

  • Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比ReactDiff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅

  • 在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升

vue3中采用最长递增子序列来实现diff优化

回答范例

思路

  • diff算法是干什么的
  • 它的必要性
  • 它何时执行
  • 具体执行方式
  • 拔高:说一下vue3中的优化

回答范例

  1. Vue中的diff算法称为patching算法,它由Snabbdom修改而来,虚拟DOM要想转化为真实DOM就需要通过patch方法转换
  2. 最初Vue1.x视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOMpatching算法支持,但是这样粒度过细导致Vue1.x无法承载较大应用;Vue 2.x中为了降低Watcher粒度,每个组件只有一个Watcher与之对应,此时就需要引入patching算法才能精确找到发生变化的地方并高效更新
  3. vuediff执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render函数获得最新的虚拟DOM,然后执行patch函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM操作
  4. patch过程是一个递归过程,遵循深度优先、同层比较的策略;

vue3patch为例

  • 首先判断两个节点是否为相同同类节点,不同则删除重新创建
  • 如果双方都是文本则更新文本内容
  • 如果双方都是元素节点则递归更新子元素,同时更新元素属性
  • 更新子节点时又分了几种情况
    • 新的子节点是文本,老的子节点是数组则清空,并设置文本;
    • 新的子节点是文本,老的子节点是文本则直接更新文本;
    • 新的子节点是数组,老的子节点是文本则清空文本,并创建新子节点数组中的子元素;
    • 新的子节点是数组,老的子节点也是数组,那么比较两组子节点,更新细节blabla
  • vue3中引入的更新策略:静态节点标记等

vdom中diff算法的简易实现

以下代码只是帮助大家理解diff算法的原理和流程

  1. vdom转化为真实dom
    const createElement = (vnode) => {
      let tag = vnode.tag;
      let attrs = vnode.attrs || {};
      let children = vnode.children || [];
      if(!tag) {
        return null;
      }
      //创建元素
      let elem = document.createElement(tag);
      //属性
      let attrName;
      for (attrName in attrs) {
        if(attrs.hasOwnProperty(attrName)) {
          elem.setAttribute(attrName, attrs[attrName]);
        }
      }
      //子元素
      children.forEach(childVnode => {
        //给elem添加子元素
        elem.appendChild(createElement(childVnode));
      })
    
      //返回真实的dom元素
      return elem;
    }
  1. 用简易diff算法做更新操作
    function updateChildren(vnode, newVnode) {
      let children = vnode.children || [];
      let newChildren = newVnode.children || [];
    
      children.forEach((childVnode, index) => {
        let newChildVNode = newChildren[index];
        if(childVnode.tag === newChildVNode.tag) {
          //深层次对比, 递归过程
          updateChildren(childVnode, newChildVNode);
        } else {
          //替换
          replaceNode(childVnode, newChildVNode);
        }
      })
    }
```**Vue diff相关源码**  
```js
    // src/core/vdom/patch.js 700
    
    function assertNodeMatch (node, vnode, inVPre) {
        if (isDef(vnode.tag)) {
          return vnode.tag.indexOf('vue-component') === 0 || (
            !isUnknownElement(vnode, inVPre) &&
            vnode.tag.toLowerCase() === (node.tagName && node.tagName.toLowerCase())
          )
        } else {
          return node.nodeType === (vnode.isComment ? 8 : 3)
        }
      }
    
      return function patch (oldVnode, vnode, hydrating, removeOnly) {
        if (isUndef(vnode)) { // 此为组件卸载逻辑
          if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
          return
        }
    
        let isInitialPatch = false
        const insertedVnodeQueue = []
    
        if (isUndef(oldVnode)) { // 此为组件挂载
          // empty mount (likely as component), create new root element
          isInitialPatch = true
          createElm(vnode, insertedVnodeQueue)
        } else {
          const isRealElement = isDef(oldVnode.nodeType)
          if (!isRealElement && sameVnode(oldVnode, vnode)) {
            // patch existing root node  patchVnode
            patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
          } else {
            if (isRealElement) { // 真实元素挂载
              // mounting to a real element
              // check if this is server-rendered content and if we can perform
              // a successful hydration.
              if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
                oldVnode.removeAttribute(SSR_ATTR)
                hydrating = true
              }
              if (isTrue(hydrating)) {
                if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
                  invokeInsertHook(vnode, insertedVnodeQueue, true)
                  return oldVnode
                } else if (process.env.NODE_ENV !== 'production') {
                  warn(
                    'The client-side rendered virtual DOM tree is not matching ' +
                    'server-rendered content. This is likely caused by incorrect ' +
                    'HTML markup, for example nesting block-level elements inside ' +
                    '<p>, or missing <tbody>. Bailing hydration and performing ' +
                    'full client-side render.'
                  )
                }
              }
              // either not server-rendered, or hydration failed.
              // create an empty node and replace it
              oldVnode = emptyNodeAt(oldVnode) // 根据真实元素 产生虚拟节点
            }
    
            // replacing existing element
            const oldElm = oldVnode.elm
            const parentElm = nodeOps.parentNode(oldElm) // 找到父亲
    
            // create new node  创建新节点
            createElm(
              vnode,
              insertedVnodeQueue,
              // extremely rare edge case: do not insert if old element is in a
              // leaving transition. Only happens when combining transition +
              // keep-alive + HOCs. (#4590)
              oldElm._leaveCb ? null : parentElm,
              nodeOps.nextSibling(oldElm)
            )
    
            // update parent placeholder node element, recursively
            if (isDef(vnode.parent)) { // 依次更新父占位符
              let ancestor = vnode.parent
              const patchable = isPatchable(vnode)
              while (ancestor) {
                for (let i = 0; i < cbs.destroy.length; ++i) {
                  cbs.destroy[i](ancestor)
                }
                ancestor.elm = vnode.elm
                if (patchable) {
                  for (let i = 0; i < cbs.create.length; ++i) {
                    cbs.create[i](emptyNode, ancestor)
                  }
                  // #6513
                  // invoke insert hooks that may have been merged by create hooks.
                  // e.g. for directives that uses the "inserted" hook.
                  const insert = ancestor.data.hook.insert
                  if (insert.merged) {
                    // start at index 1 to avoid re-invoking component mounted hook
                    for (let i = 1; i < insert.fns.length; i++) {
                      insert.fns[i]()
                    }
                  }
                } else {
                  registerRef(ancestor)
                }
                ancestor = ancestor.parent
              }
            }
    
            // destroy old node
            if (isDef(parentElm)) { // 销毁老节点
              removeVnodes([oldVnode], 0, 0)
            } else if (isDef(oldVnode.tag)) {
              invokeDestroyHook(oldVnode)
            }
          }
        }
        // 调用插入的钩子 -》 callInsert
        invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
        return vnode.elm
      }
    // 比较两个虚拟节点
    function patchVnode (
        oldVnode,
        vnode,
        insertedVnodeQueue,
        ownerArray,
        index,
        removeOnly
      ) {
        if (oldVnode === vnode) {
          return
        }
    
        if (isDef(vnode.elm) && isDef(ownerArray)) {
          // clone reused vnode
          vnode = ownerArray[index] = cloneVNode(vnode)
        }
    
        const elm = vnode.elm = oldVnode.elm // 复用老节点
    
        if (isTrue(oldVnode.isAsyncPlaceholder)) { // 如果是异步占位符跳过
          if (isDef(vnode.asyncFactory.resolved)) {
            hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
          } else {
            vnode.isAsyncPlaceholder = true
          }
          return
        }
    
        // reuse element for static trees.
        // note we only do this if the vnode is cloned -
        // if the new node is not cloned it means the render functions have been
        // reset by the hot-reload-api and we need to do a proper re-render.
        if (isTrue(vnode.isStatic) &&
          isTrue(oldVnode.isStatic) &&
          vnode.key === oldVnode.key && // 都是静态节点,key相同
          (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) // 是克隆节点 或者 带有once,直接复用
        ) {
          vnode.componentInstance = oldVnode.componentInstance
          return
        } 
    
        let i // 组件更新逻辑
        const data = vnode.data
        if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
          i(oldVnode, vnode)
        }
    
        const oldCh = oldVnode.children
        const ch = vnode.children
        if (isDef(data) && isPatchable(vnode)) { // 调用更新方法
          for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
          if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
        }
        if (isUndef(vnode.text)) { // 如果不是文本节点
          if (isDef(oldCh) && isDef(ch)) { // 两方都有儿子, 而且不是同一个儿子
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
          } else if (isDef(ch)) { // 如果只有新的有儿子
            if (process.env.NODE_ENV !== 'production') {
              checkDuplicateKeys(ch)
            }
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // 删除添加新节点
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
          } else if (isDef(oldCh)) { // 如果老得有儿子
            removeVnodes(oldCh, 0, oldCh.length - 1) // 删除节点
          } else if (isDef(oldVnode.text)) { // 如果老的是文本
            nodeOps.setTextContent(elm, '') // 清空文本中内容
          }
        } else if (oldVnode.text !== vnode.text) {
          nodeOps.setTextContent(elm, vnode.text) // 文本不相同直接设置新值
        }
        if (isDef(data)) { // 调用postpatch钩子
          if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
        }
      }
    
      function invokeInsertHook (vnode, queue, initial) {
        // delay insert hooks for component root nodes, invoke them after the
        // element is really inserted
        if (isTrue(initial) && isDef(vnode.parent)) {
          vnode.parent.data.pendingInsert = queue
        } else {
          for (let i = 0; i < queue.length; ++i) {
            queue[i].data.hook.insert(queue[i])
          }
        }
      }
    
      let hydrationBailed = false
      // list of modules that can skip create hook during hydration because they
      // are already rendered on the client or has no need for initialization
      // Note: style is excluded because it relies on initial clone for future
      // deep updates (#7063).
      const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key')
    
      // Note: this is a browser-only function so we can assume elms are DOM nodes.
      function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {
        let i
        const { tag, data, children } = vnode
        inVPre = inVPre || (data && data.pre)
        vnode.elm = elm
    
        if (isTrue(vnode.isComment) && isDef(vnode.asyncFactory)) {
          vnode.isAsyncPlaceholder = true
          return true
        }
        // assert node match
        if (process.env.NODE_ENV !== 'production') {
          if (!assertNodeMatch(elm, vnode, inVPre)) {
            return false
          }
        }
        if (isDef(data)) {
          if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */)
          if (isDef(i = vnode.componentInstance)) {
            // child component. it should have hydrated its own tree.
            initComponent(vnode, insertedVnodeQueue)
            return true
          }
        }
        if (isDef(tag)) {
          if (isDef(children)) {
            // empty element, allow client to pick up and populate children
            if (!elm.hasChildNodes()) {
              createChildren(vnode, children, insertedVnodeQueue)
            } else {
              // v-html and domProps: innerHTML
              if (isDef(i = data) && isDef(i = i.domProps) && isDef(i = i.innerHTML)) {
                if (i !== elm.innerHTML) {
                  /* istanbul ignore if */
                  if (process.env.NODE_ENV !== 'production' &&
                    typeof console !== 'undefined' &&
                    !hydrationBailed
                  ) {
                    hydrationBailed = true
                    console.warn('Parent: ', elm)
                    console.warn('server innerHTML: ', i)
                    console.warn('client innerHTML: ', elm.innerHTML)
                  }
                  return false
                }
              } else {
                // iterate and compare children lists
                let childrenMatch = true
                let childNode = elm.firstChild
                for (let i = 0; i < children.length; i++) {
                  if (!childNode || !hydrate(childNode, children[i], insertedVnodeQueue, inVPre)) {
                    childrenMatch = false
                    break
                  }
                  childNode = childNode.nextSibling
                }
                // if childNode is not null, it means the actual childNodes list is
                // longer than the virtual children list.
                if (!childrenMatch || childNode) {
                  /* istanbul ignore if */
                  if (process.env.NODE_ENV !== 'production' &&
                    typeof console !== 'undefined' &&
                    !hydrationBailed
                  ) {
                    hydrationBailed = true
                    console.warn('Parent: ', elm)
                    console.warn('Mismatching childNodes vs. VNodes: ', elm.childNodes, children)
                  }
                  return false
                }
              }
            }
          }
          if (isDef(data)) {
            let fullInvoke = false
            for (const key in data) {
              if (!isRenderedModule(key)) {
                fullInvoke = true
                invokeCreateHooks(vnode, insertedVnodeQueue)
                break
              }
            }
            if (!fullInvoke && data['class']) {
              // ensure collecting deps for deep class bindings for future updates
              traverse(data['class'])
            }
          }
        } else if (elm.data !== vnode.text) {
          elm.data = vnode.text
        }
        return true
      }
    function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
        let oldStartIdx = 0
        let newStartIdx = 0
        let oldEndIdx = oldCh.length - 1
        let oldStartVnode = oldCh[0]
        let oldEndVnode = oldCh[oldEndIdx]
        let newEndIdx = newCh.length - 1
        let newStartVnode = newCh[0]
        let newEndVnode = newCh[newEndIdx]
        let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    
        // removeOnly is a special flag used only by <transition-group>
        // to ensure removed elements stay in correct relative positions
        // during leaving transitions
        const canMove = !removeOnly
    
        if (process.env.NODE_ENV !== 'production') {
          checkDuplicateKeys(newCh)
        }
        // 新老节点有一方循环完毕则patch 完毕
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          if (isUndef(oldStartVnode)) {
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
          } else if (isUndef(oldEndVnode)) {
            oldEndVnode = oldCh[--oldEndIdx]
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
          } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
          } else {
            // 乱序比对
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
            if (isUndef(idxInOld)) { // New element
              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
            } else {
              vnodeToMove = oldCh[idxInOld]
              if (sameVnode(vnodeToMove, newStartVnode)) {
                patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
                oldCh[idxInOld] = undefined
                canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
              } else {
                // same key but different element. treat as new element
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
              }
            }
            newStartVnode = newCh[++newStartIdx]
          }
        }
        if (oldStartIdx > oldEndIdx) {
          refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
          addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
        } else if (newStartIdx > newEndIdx) {
          removeVnodes(oldCh, oldStartIdx, oldEndIdx)
        }
      }

Vue的diff算法详细分析

1. 是什么

diff 算法是一种通过同层的树节点进行比较的高效算法

其有两个特点:

  • 比较只会在同层级进行, 不会跨层级比较
  • 在diff比较的过程中,循环从两边向中间比较

diff 算法在很多场景下都有应用,在 vue 中,作用于虚拟 dom 渲染成真实 dom 的新旧 VNode 节点比较

2. 比较方式

diff整体策略为:深度优先,同层比较

  1. 比较只会在同层级进行, 不会跨层级比较

  1. 比较的过程中,循环从两边向中间收拢

下面举个vue通过diff算法更新的例子:

新旧VNode节点如下图所示:

第一次循环后,发现旧节点D与新节点D相同,直接复用旧节点D作为diff后的第一个真实节点,同时旧节点endIndex移动到C,新节点的 startIndex 移动到了 C

第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,diff 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 endIndex 移动到了 B,新节点的 startIndex 移动到了 E

第三次循环中,发现E没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 startIndex 移动到了 A。旧节点的 startIndexendIndex 都保持不动

第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 diff 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 startIndex 移动到了 B,新节点的startIndex 移动到了 B

第五次循环中,情形同第四次循环一样,因此 diff 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 startIndex移动到了 C,新节点的 startIndex 移动到了 F

新节点的 startIndex 已经大于 endIndex 了,需要创建 newStartIdxnewEndIdx 之间的所有节点,也就是节点F,直接创建 F 节点对应的真实节点放到 B 节点后面

3. 原理分析

当数据发生改变时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实的DOM打补丁,更新相应的视图

源码位置:src/core/vdom/patch.js

    function patch(oldVnode, vnode, hydrating, removeOnly) {
        if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数
            if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
            return
        }
    
        let isInitialPatch = false
        const insertedVnodeQueue = []
    
        if (isUndef(oldVnode)) {
            isInitialPatch = true
            createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素
        } else {
            const isRealElement = isDef(oldVnode.nodeType)
            if (!isRealElement && sameVnode(oldVnode, vnode)) {
                // 判断旧节点和新节点自身一样,一致执行patchVnode
                patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
            } else {
                // 否则直接销毁及旧节点,根据新节点生成dom元素
                if (isRealElement) {
    
                    if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
                        oldVnode.removeAttribute(SSR_ATTR)
                        hydrating = true
                    }
                    if (isTrue(hydrating)) {
                        if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
                            invokeInsertHook(vnode, insertedVnodeQueue, true)
                            return oldVnode
                        }
                    }
                    oldVnode = emptyNodeAt(oldVnode)
                }
                return vnode.elm
            }
        }
    }

patch函数前两个参数位为oldVnodeVnode ,分别代表新的节点和之前的旧节点,主要做了四个判断:

  • 没有新节点,直接触发旧节点的destory钩子
  • 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 createElm
  • 旧节点和新节点自身一样,通过 sameVnode 判断节点是否一样,一样时,直接调用 patchVnode去处理这两个节点
  • 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点

下面主要讲的是patchVnode部分

    function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
        // 如果新旧节点一致,什么都不做
        if (oldVnode === vnode) {
          return
        }
    
        // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
        const elm = vnode.elm = oldVnode.elm
    
        // 异步占位符
        if (isTrue(oldVnode.isAsyncPlaceholder)) {
          if (isDef(vnode.asyncFactory.resolved)) {
            hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
          } else {
            vnode.isAsyncPlaceholder = true
          }
          return
        }
        // 如果新旧都是静态节点,并且具有相同的key
        // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上
        // 也不用再有其他操作
        if (isTrue(vnode.isStatic) &&
          isTrue(oldVnode.isStatic) &&
          vnode.key === oldVnode.key &&
          (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
        ) {
          vnode.componentInstance = oldVnode.componentInstance
          return
        }
    
        let i
        const data = vnode.data
        if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
          i(oldVnode, vnode)
        }
    
        const oldCh = oldVnode.children
        const ch = vnode.children
        if (isDef(data) && isPatchable(vnode)) {
          for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
          if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
        }
        // 如果vnode不是文本节点或者注释节点
        if (isUndef(vnode.text)) {
          // 并且都有子节点
          if (isDef(oldCh) && isDef(ch)) {
            // 并且子节点不完全一致,则调用updateChildren
            if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    
            // 如果只有新的vnode有子节点
          } else if (isDef(ch)) {
            if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
            // elm已经引用了老的dom节点,在老的dom节点上添加子节点
            addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    
            // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh
          } else if (isDef(oldCh)) {
            removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    
            // 如果老节点是文本节点
          } else if (isDef(oldVnode.text)) {
            nodeOps.setTextContent(elm, '')
          }
    
          // 如果新vnode和老vnode是文本节点或注释节点
          // 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以
        } else if (oldVnode.text !== vnode.text) {
          nodeOps.setTextContent(elm, vnode.text)
        }
        if (isDef(data)) {
          if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
        }
      }

patchVnode主要做了几个判断:

  • 新节点是否是文本节点,如果是,则直接更新dom的文本内容为新节点的文本内容
  • 新节点和旧节点如果都有子节点,则处理比较更新子节点
  • 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节点
  • 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM 删除

子节点不完全一致,则调用updateChildren

    function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
        let oldStartIdx = 0 // 旧头索引
        let newStartIdx = 0 // 新头索引
        let oldEndIdx = oldCh.length - 1 // 旧尾索引
        let newEndIdx = newCh.length - 1 // 新尾索引
        let oldStartVnode = oldCh[0] // oldVnode的第一个child
        let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child
        let newStartVnode = newCh[0] // newVnode的第一个child
        let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child
        let oldKeyToIdx, idxInOld, vnodeToMove, refElm
    
        // removeOnly is a special flag used only by <transition-group>
        // to ensure removed elements stay in correct relative positions
        // during leaving transitions
        const canMove = !removeOnly
    
        // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束
        while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
          // 如果oldVnode的第一个child不存在
          if (isUndef(oldStartVnode)) {
            // oldStart索引右移
            oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    
          // 如果oldVnode的最后一个child不存在
          } else if (isUndef(oldEndVnode)) {
            // oldEnd索引左移
            oldEndVnode = oldCh[--oldEndIdx]
    
          // oldStartVnode和newStartVnode是同一个节点
          } else if (sameVnode(oldStartVnode, newStartVnode)) {
            // patch oldStartVnode和newStartVnode, 索引左移,继续循环
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
    
          // oldEndVnode和newEndVnode是同一个节点
          } else if (sameVnode(oldEndVnode, newEndVnode)) {
            // patch oldEndVnode和newEndVnode,索引右移,继续循环
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
    
          // oldStartVnode和newEndVnode是同一个节点
          } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
            // patch oldStartVnode和newEndVnode
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
            // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
            // oldStart索引右移,newEnd索引左移
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
    
          // 如果oldEndVnode和newStartVnode是同一个节点
          } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
            // patch oldEndVnode和newStartVnode
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
            // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
            // oldEnd索引左移,newStart索引右移
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
    
          // 如果都不匹配
          } else {
            if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
    
            // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode
            idxInOld = isDef(newStartVnode.key)
              ? oldKeyToIdx[newStartVnode.key]
              : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
    
            // 如果未找到,说明newStartVnode是一个新的节点
            if (isUndef(idxInOld)) { // New element
              // 创建一个新Vnode
              createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
    
            // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove
            } else {
              vnodeToMove = oldCh[idxInOld]
              /* istanbul ignore if */
              if (process.env.NODE_ENV !== 'production' && !vnodeToMove) {
                warn(
                  'It seems there are duplicate keys that is causing an update error. ' +
                  'Make sure each v-for item has a unique key.'
                )
              }
    
              // 比较两个具有相同的key的新节点是否是同一个节点
              //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。
              if (sameVnode(vnodeToMove, newStartVnode)) {
                // patch vnodeToMove和newStartVnode
                patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
                // 清除
                oldCh[idxInOld] = undefined
                // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm
                // 移动到oldStartVnode.elm之前
                canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
    
              // 如果key相同,但是节点不相同,则创建一个新的节点
              } else {
                // same key but different element. treat as new element
                createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm)
              }
            }
    
            // 右移
            newStartVnode = newCh[++newStartIdx]
          }
        }

while循环主要处理了以下五种情景:

  • 当新老 VNode 节点的 start 相同时,直接 patchVnode ,同时新老 VNode 节点的开始索引都加 1
  • 当新老 VNode 节点的 end相同时,同样直接 patchVnode ,同时新老 VNode 节点的结束索引都减 1
  • 当老 VNode 节点的 start 和新 VNode 节点的 end 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldEndVnode 的后面,同时老 VNode 节点开始索引加 1,新 VNode 节点的结束索引减 1
  • 当老 VNode 节点的 end 和新 VNode 节点的 start 相同时,这时候在 patchVnode 后,还需要将当前真实 dom 节点移动到 oldStartVnode 的前面,同时老 VNode 节点结束索引减 1,新 VNode 节点的开始索引加 1
  • 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况:
    • 从旧的 VNodekey 值,对应 index 序列为 value 值的哈希表中找到与 newStartVnode 一致 key 的旧的 VNode 节点,再进行patchVnode,同时将这个真实 dom移动到 oldStartVnode 对应的真实 dom 的前面
    • 调用 createElm 创建一个新的 dom 节点放到当前 newStartIdx 的位置

小结

  • 当数据发生改变时,订阅者watcher就会调用patch给真实的DOM打补丁
  • 通过isSameVnode进行判断,相同则调用patchVnode方法
  • patchVnode做了以下操作:
    • 找到对应的真实dom,称为el
    • 如果都有都有文本节点且不相等,将el文本节点设置为Vnode的文本节点
    • 如果oldVnode有子节点而VNode没有,则删除el子节点
    • 如果oldVnode没有子节点而VNode有,则将VNode的子节点真实化后添加到el
    • 如果两者都有子节点,则执行updateChildren函数比较子节点
  • updateChildren主要做了以下操作:
    • 设置新旧VNode的头尾指针
    • 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻找 key一致的VNode 节点再分情况操作

Vue2和Vue3和React三者的diff算法有什么区别

如果要严格diff两颗树,时间复杂度O(n^3)不可用,react把复杂度优化到了O(n)

Tree diff的优化

  • 只比较同一层级,不跨级比较
  • tag不同则删掉重建(不在去比较内部细节)
  • 子节点通过key区分

React diff仅右移动

Vue2 深度递归的方式 + 双指针的方式

Vue2diff算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较

  • 比较过程:
    • 先比较是否是相同节点
    • 相同节点比较属性,并复用老节点
    • 比较儿子节点,考虑老节点和新节点儿子的情况
    • 优化比较:头头尾尾头尾尾头
    • 比对查找进行复用

Vue3中采用最长递增子序列实现diff算法

Vue React为何循环时必须使用key

  • vdom diff 算法会根据key判断元素是否要删除
  • 如果匹配了key,则只移动元素-性能较好
  • 未匹配key,则删除重建,性能较差

既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异

  • 响应式数据变化,Vue确实可以在数据变化时,响应式系统可以立刻得知。但是如果给每个属性都添加watcher用于更新的话,会产生大量的watcher从而降低性能
  • 而且粒度过细也得导致更新不准确的问题,所以vue采用了组件级的watcher配合diff来检测差异

请说明Vue中key的作用和原理,谈谈你对它的理解

  • key是为Vue中的VNode标记的唯一id,在patch过程中通过key可以判断两个虚拟节点是否是相同节点,通过这个key,我们的diff操作可以更准确、更快速
  • diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的key与旧节点进行比对,然后检出差异
  • 尽量不要采用索引作为key
  • 如果不加key,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug
  • 更准确 :因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
  • 更快速key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n)Map的时间复杂度仅仅为O(1),比遍历方式更快。

源码如下:

    function createKeyToOldIdx (children, beginIdx, endIdx) {
      let i, key
      const map = {}
      for (i = beginIdx; i <= endIdx; ++i) {
        key = children[i].key
        if (isDef(key)) map[key] = i
      }
      return map
    }

回答范例

分析

这是一道特别常见的问题,主要考查大家对虚拟DOMpatch细节的掌握程度,能够反映面试者理解层次

思路分析:

  • 给出结论,key的作用是用于优化patch性能
  • key的必要性
  • 实际使用方式
  • 总结:可从源码层面描述一下vue如何判断两个节点是否相同

回答范例:

  1. key的作用主要是为了更高效的更新虚拟DOM
  2. vuepatch过程中判断两个节点是否是相同节点是key是一个必要条件,渲染一组列表时,key往往是唯一标识,所以如果不定义key的话,vue只能认为比较的两个节点是同一个,哪怕它们实际上不是,这导致了频繁更新元素,使得整个patch过程比较低效,影响性能
  3. 实际使用中在渲染一组列表时key必须设置,而且必须是唯一标识,应该避免使用数组索引作为key,这可能导致一些隐蔽的bugvue中在使用相同标签元素过渡切换时,也会使用key属性,其目的也是为了让vue可以区分它们,否则vue只会替换其内部属性而不会触发过渡效果
  4. 从源码中可以知道,vue判断两个节点是否相同时主要判断两者的key标签类型(如div)等,因此如果不设置key,它的值就是undefined,则可能永远认为这是两个相同节点,只能去做更新操作,这造成了大量的dom更新操作,明显是不可取的

如果不使用 keyVue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vuevnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速

diff程可以概括为:oldChnewCh各有两个头尾的变量StartIdxEndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldChnewCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是旧尾新头旧头新尾

相关代码如下

    // 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用
    function isSameVnode(oldVnode, newVnode) {
      return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
    }
    
    // 根据key来创建老的儿子的index映射表  类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置
    function makeIndexByKey(children) {
      let map = {};
      children.forEach((item, index) => {
        map[item.key] = index;
      });
      return map;
    }
    // 生成的映射表
    let map = makeIndexByKey(oldCh);

17 Vue组件相关

Vue组件为什么只能有一个根元素

vue3中没有问题

    Vue.createApp({
      components: {
        comp: {
          template: `
            <div>root1</div>
            <div>root2</div>
          `
        }
      }
    }).mount('#app')
  1. vue2中组件确实只能有一个根,但vue3中组件已经可以多根节点了。
  2. 之所以需要这样是因为vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
  3. vue3中之所以可以写多个根节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。将来patch的时候,如果发现是一个Fragment节点,则直接遍历children创建或更新

谈一谈对Vue组件化的理解

  • 组件化开发能大幅提高开发效率、测试性、复用性等
  • 常用的组件化技术:属性、自定义事件、插槽
  • 降低更新频率,只重新渲染变化的组件
  • 组件的特点:高内聚、低耦合、单向数据流

Vue组件渲染和更新过程

渲染组件时,会通过 Vue.extend 方法构建子组件的构造函数,并进行实例化。最终手动调用$mount() 进行挂载。更新组件时会进行 patchVnode 流程,核心就是diff算法

异步组件是什么?使用场景有哪些?

分析

因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。

体验

大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们

    import { defineAsyncComponent } from 'vue'
    // defineAsyncComponent定义异步组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容
    const AsyncComp = defineAsyncComponent(() => {
      // 加载函数返回Promise
      return new Promise((resolve, reject) => {
        // ...可以从服务器加载组件
        resolve(/* loaded component */)
      })
    })
    // 借助打包工具实现ES模块动态导入
    const AsyncComp = defineAsyncComponent(() =>
      import('./components/MyComponent.vue')
    )

回答范例

  1. 在大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
  2. 我们不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。
  3. 使用异步组件最简单的方式是直接给defineAsyncComponent指定一个loader函数,结合ES模块动态导入函数import可以快速实现。我们甚至可以指定loadingComponenterrorComponent选项从而给用户一个很好的加载反馈。另外Vue3中还可以结合Suspense组件使用异步组件。
  4. 异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是vue框架,处理路由组件加载的是vue-router。但是可以在懒加载的路由组件中使用异步组件

为什么要使用异步组件

  1. 节省打包出的结果,异步组件分开打包,采用jsonp的方式进行加载,有效解决文件过大的问题。
  2. 核心就是包组件定义变成一个函数,依赖import() 语法,可以实现文件的分割加载。
    components:{ 
      AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([]) 
    }

原理

    export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { 
        // async component 
        let asyncFactory 
        if (isUndef(Ctor.cid)) { 
            asyncFactory = Ctor 
            Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend 
            // 第二次渲染时Ctor不为undefined 
            if (Ctor === undefined) { 
                return createAsyncPlaceholder( // 渲染占位符 空虚拟节点 
                    asyncFactory, 
                    data, 
                    context, 
                    children, 
                    tag 
                ) 
            } 
        } 
    }
    function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void { 
        if (isDef(factory.resolved)) { 
            // 3.在次渲染时可以拿到获取的最新组件 
            return factory.resolved 
        }
        const resolve = once((res: Object | Class<Component>) => { 
            factory.resolved = ensureCtor(res, baseCtor) 
            if (!sync) { 
                forceRender(true) //2. 强制更新视图重新渲染 
            } else { 
                owners.length = 0 
            } 
        })
        const reject = once(reason => { 
            if (isDef(factory.errorComp)) { 
                factory.error = true forceRender(true) 
            } 
        })
        const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后 
        sync = false 
        return factory.resolved 
    }

函数式组件优势和原理

函数组件的特点

  1. 函数式组件需要在声明组件是指定 functional:true
  2. 不需要实例化,所以没有this,this通过render函数的第二个参数context来代替
  3. 没有生命周期钩子函数,不能使用计算属性,watch
  4. 不能通过$emit 对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件
  5. 因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement
  6. 函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都解析到$attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)

优点

  1. 由于函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件
  2. 函数式组件结构比较简单,代码结构更清晰

使用场景:

  • 一个简单的展示组件,作为容器组件使用 比如 router-view 就是一个函数式组件
  • “高阶组件”——用于接收一个组件作为参数,返回一个被包装过的组件

例子

    Vue.component('functional',{ // 构造函数产生虚拟节点的
        functional:true, // 函数式组件 // data={attrs:{}}
        render(h){
            return h('div','test')
        }
    })
    const vm = new Vue({
        el: '#app'
    })

源码相关

    // functional component
    if (isTrue(Ctor.options.functional)) { // 带有functional的属性的就是函数式组件
      return createFunctionalComponent(Ctor, propsData, data, context, children)
    }
    
    // extract listeners, since these needs to be treated as
    // child component listeners instead of DOM listeners
    const listeners = data.on // 处理事件
    // replace with listeners with .native modifier
    // so it gets processed during parent component patch.
    data.on = data.nativeOn // 处理原生事件
    
    // install component management hooks onto the placeholder node
    installComponentHooks(data) // 安装组件相关钩子 (函数式组件没有调用此方法,从而性能高于普通组件)

Vue组件之间通信方式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信父子组件通信隔代组件通信兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信

组件传参的各种方式

组件通信常用方式有以下几种

  • props / $emit 适用 父子组件通信
    • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
  • ref$parent / $children(vue3废弃) 适用 父子组件通信
    • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
    • $parent / $children:访问访问父组件的属性或方法 / 访问子组件的属性或方法
  • EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信
    • 这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件
  • $attrs / $listeners(vue3废弃) 适用于 隔代组件通信
    • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( classstyle 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( classstyle 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用,多余的属性不会被解析到标签上
    • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件
  • provide / inject 适用于 隔代组件通信
    • 祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态 ,跨级组件间建立了一种主动提供与依赖注入的关系
  • $root 适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root只对根组件有用
  • Vuex 适用于 父子、隔代、兄弟组件通信
    • Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )
    • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
    • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

根据组件之间关系讨论组件通信最为清晰有效

  • 父子组件:props/$emit/$parent/ref
  • 兄弟组件:$parent/eventbus/vuex
  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root

下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯

1. 父子组件通信

使用props,父组件可以使用props向子组件传递数据。

父组件vue模板father.vue:

    <template>
      <child :msg="message"></child>
    </template>
    
    <script>
    import child from './child.vue';
    export default {
      components: {
        child
      },
      data () {
        return {
          message: 'father message';
        }
      }
    }
    </script>

子组件vue模板child.vue:

    <template>
        <div>{{msg}}</div>
    </template>
    
    <script>
    export default {
      props: {
        msg: {
          type: String,
          required: true
        }
      }
    }
    </script>

回调函数(callBack)

父传子:将父组件里定义的method作为props传入子组件

    // 父组件Parent.vue:
    <Child :changeMsgFn="changeMessage">
    methods: {
    	changeMessage(){
    		this.message = 'test'
    	}
    }
    // 子组件Child.vue:
    <button @click="changeMsgFn">
    props:['changeMsgFn']

子组件向父组件通信

父组件向子组件传递事件方法,子组件通过$emit触发事件,回调给父组件

父组件vue模板father.vue:

    <template>
        <child @msgFunc="func"></child>
    </template>
    
    <script>
    import child from './child.vue';
    export default {
        components: {
            child
        },
        methods: {
            func (msg) {
                console.log(msg);
            }
        }
    }
    </script>

子组件vue模板child.vue:

    <template>
        <button @click="handleClick">点我</button>
    </template>
    
    <script>
    export default {
        props: {
            msg: {
                type: String,
                required: true
            }
        },
        methods () {
            handleClick () {
              //........
              this.$emit('msgFunc');
            }
        }
    }
    </script>

2. provide / inject 跨级访问祖先组件的数据

父组件通过使用provide(){return{}}提供需要传递的数据

    export default {
      data() {
        return {
          title: '我是父组件',
          name: 'poetry'
        }
      },
      methods: {
        say() {
          alert(1)
        }
      },
      // provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法
      provide() {
        return {
          message: '我是祖先组件提供的数据',
          name: this.name, // 传递属性
          say: this.say
        }
      }
    }

子组件通过使用inject:[“参数1”,”参数2”,…]接收父组件传递的参数

    <template>
      <p>曾孙组件</p>
      <p>{{message}}</p>
    </template>
    <script>
    export default {
      // inject 注入/接收祖先组件传递的所需要的数据即可 
      //接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {}
      inject: [ "message","say"],
      mounted() {
        this.say();
      },
    };
    </script>

3. $parent + $children 获取父组件实例和子组件实例的集合

  • this.$parent 可以直接访问该组件的父实例或组件
  • 父组件也可以通过 this.$children 访问它所有的子组件;需要注意 $children 并不保证顺序,也不是响应式的
    <!-- parent.vue -->
    <template>
    <div>
      <child1></child1>   
      <child2></child2> 
      <button @click="clickChild">$children方式获取子组件值</button>
    </div>
    </template>
    <script>
    import child1 from './child1'
    import child2 from './child2'
    export default {
      data(){
        return {
          total: 108
        }
      },
      components: {
        child1,
        child2  
      },
      methods: {
        funa(e){
          console.log("index",e)
        },
        clickChild(){
          console.log(this.$children[0].msg);
          console.log(this.$children[1].msg);
        }
      }
    }
    </script>
    <!-- child1.vue -->
    <template>
      <div>
        <button @click="parentClick">点击访问父组件</button>
      </div>
    </template>
    <script>
    export default {
      data(){
        return {
          msg:"child1"
        }
      },
      methods: {
        // 访问父组件数据
        parentClick(){
          this.$parent.funa("xx")
          console.log(this.$parent.total);
        }
      }
    }
    </script>
    <!-- child2.vue -->
    <template>
      <div>
        child2
      </div>
    </template>
    <script>
    export default {
      data(){
        return {
         msg: 'child2'
        }
      }
    }
    </script>

4. $attrs + $listeners多级组件通信

$attrs 包含了从父组件传过来的所有props属性

    // 父组件Parent.vue:
    <Child :name="name" :age="age"/>
        
    // 子组件Child.vue:
    <GrandChild v-bind="$attrs" />
    
    // 孙子组件GrandChild
    <p>姓名:{{$attrs.name}}</p>
    <p>年龄:{{$attrs.age}}</p>

$listeners包含了父组件监听的所有事件

    // 父组件Parent.vue:
    <Child :name="name" :age="age" @changeNameFn="changeName"/>
        
    // 子组件Child.vue:
    <button @click="$listeners.changeNameFn"></button>

5. ref 父子组件通信

    // 父组件Parent.vue:
    <Child ref="childComp"/>
    <button @click="changeName"></button>
    changeName(){
    	console.log(this.$refs.childComp.age);
    	this.$refs.childComp.changeAge()
    }
    
    // 子组件Child.vue:
    data(){
    	return{
    		age:20
    	}
    },
    methods(){
    	changeAge(){
    		this.age=15
      }
    }

6. 非父子, 兄弟组件之间通信

vue2中废弃了broadcast广播和分发事件的方法。父子组件中可以用props$emit()。如何实现非父子组件间的通信,可以通过实例一个vue实例Bus作为媒介,要相互通信的兄弟组件之中,都引入Bus,然后通过分别调用Bus事件触发和监听来实现通信和参数传递。Bus.js可以是这样:

    // Bus.js
    
    // 创建一个中央时间总线类  
    class Bus {  
      constructor() {  
        this.callbacks = {};   // 存放事件的名字  
      }  
      $on(name, fn) {  
        this.callbacks[name] = this.callbacks[name] || [];  
        this.callbacks[name].push(fn);  
      }  
      $emit(name, args) {  
        if (this.callbacks[name]) {  
          this.callbacks[name].forEach((cb) => cb(args));  
        }  
      }  
    }  
      
    // main.js  
    Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上  
    // 另一种方式  
    Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能  
    <template>
    	<button @click="toBus">子组件传给兄弟组件</button>
    </template>
    
    <script>
    export default{
    	methods: {
        toBus () {
          this.$bus.$emit('foo', '来自兄弟组件')
        }
      }
    }
    </script>

另一个组件也在钩子函数中监听on事件

    export default {
      data() {
        return {
          message: ''
        }
      },
      mounted() {
        this.$bus.$on('foo', (msg) => {
          this.message = msg
        })
      }
    }

7. $root 访问根组件中的属性或方法

  • 作用:访问根组件中的属性或方法
  • 注意:是根组件,不是父组件。$root只对根组件有用
    var vm = new Vue({
      el: "#app",
      data() {
        return {
          rootInfo:"我是根元素的属性"
        }
      },
      methods: {
        alerts() {
          alert(111)
        }
      },
      components: {
        com1: {
          data() {
            return {
              info: "组件1"
            }
          },
          template: "<p>{{ info }} <com2></com2></p>",
          components: {
            com2: {
              template: "<p>我是组件1的子组件</p>",
              created() {
                this.$root.alerts()// 根组件方法
                console.log(this.$root.rootInfo)// 我是根元素的属性
              }
            }
          }
        }
      }
    });

8. vuex

  • 适用场景: 复杂关系的组件数据传递
  • Vuex作用相当于一个用来存储共享变量的容器

  • state用来存放共享变量的地方
  • getter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值
  • mutations用来存放修改state的方法。
  • actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。常用来做一些异步操作

小结

  • 父子关系的组件数据传递选择 props$emit进行传递,也可选择ref
  • 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递
  • 祖先与后代组件数据传递可选择attrslisteners或者 ProvideInject
  • 复杂关系的组件数据传递可以通过vuex存放共享的变量

组件中写name属性的好处

可以标识组件的具体名称方便调试和查找对应属性

    // 源码位置 src/core/global-api/extend.js
    
    // enable recursive self-lookup
    if (name) { 
        Sub.options.components[name] = Sub // 记录自己 在组件中递归自己  -> jsx
    }

Vue.extend 作用和原理

官方解释:Vue.extend 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

其实就是一个子类构造器 是 Vue 组件的核心 api 实现思路就是使用原型继承的方法返回了 Vue 的子类 并且利用 mergeOptions 把传入组件的 options 和父类的 options 进行了合并

  • extend是构造一个组件的语法器。然后这个组件你可以作用到Vue.component这个全局注册方法里还可以在任意vue模板里使用组件。 也可以作用到vue实例或者某个组件中的components属性中并在内部使用apple组件。
  • Vue.component你可以创建 ,也可以取组件。

相关代码如下

    export default function initExtend(Vue) {
      let cid = 0; //组件的唯一标识
      // 创建子类继承Vue父类 便于属性扩展
      Vue.extend = function (extendOptions) {
        // 创建子类的构造函数 并且调用初始化方法