Skip to content

谈谈你对 MVVM 的理解 ​

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

MVC 模式 代表:Backbone + underscore + jquery

img

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

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

img

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

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

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

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

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

TIP

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

优点:

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

缺点:

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

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

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

实现一个 SPA

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

img

  1. hash 模式:核心通过监听url中的hash来进行路由跳转
js
// 定义 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
// 定义 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 api,api 提供了丰富的 router 相关属性先了解一个几个相关的 api
  • history.pushState 浏览器历史纪录添加记录
  • history.replaceState修改浏览器历史纪录中当前纪录
  • history.popState 当 history 发生变化时触发
js
// 定义 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
// 定义 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

Vue2.x 响应式数据原理 ​

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

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

Object.defineProperty 基本使用

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

源码分析

img

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

Vue3.x 响应式数据原理 ​

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

proxy 基本用法 ​

js
// 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默认只会代理第一层对象,只有取值再次是对象的时候再次代理,不是一上来就代理,提高性能。不像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-的优势说说你对 proxy 的理解,Proxy 相比于 defineProperty 的优势 ​

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

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

Proxy 的优势如下:

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

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

Object.defineProperty 的优势如下:

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

defineProperty 的属性值有哪些

js
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) {},
})
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) {},
})

相关代码如下

js
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, // 当修改属性时调用此方法
}
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

Vue 中如何检测数组变化 ​

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

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

Vue 提供了以下操作方法

js
// 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)
// 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 进行更新

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

原理

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

img

源码分析

js
// 拿到数组原型拷贝一份
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) // 进行深度监控
// 拿到数组原型拷贝一份
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 ,可直接监听对象数组的变化

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

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

依赖收集简版

js
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是多对多的关系
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是多对多的关系

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

简单 ​

TIP

分析

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

  • 初始化
  • 建立更新机制

把这两件事说清楚即可!

回答范例

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

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

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)
}
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 中定义

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.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 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等

vue2.x 详细 ​

1. 分析

首先找到vue的构造函数

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

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)
}
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方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法

js
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
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

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)
  }
}
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之前,数据初始化并未完成,像data、props这些属性无法访问到
  • 到了created的时候,数据已经初始化完成,能够访问data、props这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素
  • 挂载方法是调用vm.$mount方法

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

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

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)
  }
}
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在同一文件上

js
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 */)
}
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 */)
}

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

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

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

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

源码位置:

js
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)
}
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或者直接使用template、el表示元素选择器
  • 最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数

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

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

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

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

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)
}
// 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渲染组件

js
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
}
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初始化时声明的render,update方法

render的作用主要是生成vnode

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

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
}
// 定义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

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.
}
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
    new Vue

    的时候调用会调用

    _init
    _init

    方法

    • 定义 $set、$get 、$delete、$watch 等方法
    • 定义 $on、$off、$emit、$off等事件
    • 定义 _update、$forceUpdate、$destroy生命周期
  • 调用$mount进行页面的挂载

  • 挂载的时候主要是通过mountComponent方法

  • 定义updateComponent更新函数

  • 执行render生成虚拟DOM

  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中

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

全局概览 ​

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

img

初始化及挂载 ​

img

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

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

编译 ​

compile编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。

img

1. parse

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

2. optimize

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

3. generate

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

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

响应式 ​

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

img

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

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

    image-20241101174855534

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

Virtual DOM ​

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

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

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

渲染后可以得到

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

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

更新视图 ​

img

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

再看全局 ​

image-20241101174931806

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

如何理解 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中编译器何时执行?

img

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

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

react 使用babel将JSX语法解析

html
<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>
<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>

源码分析

js
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
}
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 字符串

Vue 生命周期相关 ​

Vue 的生命周期方法有哪些 ​

  1. Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是Vue的生命周期
  2. Vue 生命周期总共分为 8 个阶段创建前/后,载入前/后,更新前/后,销毁前/后
beforeCreate` => `created` => `beforeMount` => `Mounted` => `beforeUpdate` => `updated` => `beforeDestroy` => `destroyed`。`keep-alive`下:`activated` `deactivated
beforeCreate` => `created` => `beforeMount` => `Mounted` => `beforeUpdate` => `updated` => `beforeDestroy` => `destroyed`。`keep-alive`下:`activated` `deactivated
生命周期 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调试钩子,响应式依赖被触发时调用
-serverPrefetchssr only,组件实例在服务器上被渲染前调用
  1. 要掌握每个生命周期内部可以做什么事
  • beforeCreate 初始化vue实例,进行数据观测。执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
  • created 组件初始化完毕,可以访问各种数据,获取接口数据等
  • beforeMount 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上
  • mounted 实例已经挂载完成,可以进行一些DOM操作
  • beforeUpdate 更新前,可用于获取更新前各种状态。此时view层还未更新,可用于获取更新前各种状态。可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
  • updated 完成view层的更新,更新后,所有状态已是最新。可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。 该钩子在服务器端渲染期间不被调用。
  • destroyed 可以执行一些优化操作,清空定时器,解除绑定事件
  • vue3 beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消
  • vue3 unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

img

html
<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>
<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 是围绕 beforeCreate 和 created 生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup 函数中编写

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

setup和created谁先执行?

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

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

js
import { ref } from 'vue'
export default {
  // setup函数是组合api的入口函数,注意在组合api中定义的变量或者方法,要在template响应式需要return{}出去
  setup() {
    let count = ref(1)
    function myFn() {
      count.value += 1
    }
    return { count, myFn }
  },
}
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个阶段:创建前/后、载入前/后、更新前/后、销毁前/销毁后。

  • 第一次页面加载会触发哪几个钩子? 会触发下面这几个beforeCreate、created、beforeMount、mounted 。

  • 你的接口请求一般放在哪个生命周期中? 接口请求一般放在mounted中,但需要注意的是服务端渲染时不支持mounted,需要放到created中

  • DOM 渲染在哪个周期中就已经完成?

    在 mounted 中

    • 注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted
    js
      mounted: function () {
        this.$nextTick(function () {
            // Code that will run only after the
            // entire view has been rendered
        })
      }
      mounted: function () {
        this.$nextTick(function () {
            // Code that will run only after the
            // entire view has been rendered
        })
      }

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

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

js
// Parent.vue
<Child @mounted="doSomething"/>

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

// Child.vue
mounted() {
  this.$emit("mounted");
}

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

js
//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>

doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},

//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
},

// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
//  Parent.vue
<Child @hook:mounted="doSomething" ></Child>

doSomething() {
   console.log('父组件监听到 mounted 钩子函数 ...');
},

//  Child.vue
mounted(){
   console.log('子组件触发 mounted 钩子函数 ...');
},

// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...

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

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

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

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

html
<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>
<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>

相关代码如下

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

原理流程图

img

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

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

加载渲染过程

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

子组件更新过程

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

父组件更新过程

父 beforeUpdate->父 updated

销毁过程

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

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

img

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