Vue2.x原理

vue2 原理大概分如下几部分:

1.组件化和MVVM

2.响应式原理

3.vdom 和 diff算法

4.模板编译

5.组件渲染过程

6.前端路由

一、组件化 和 MVVM

  组件化:

      • 很久以前已经有组件化了,如: asp、jsp、php、nodejs也有类似组件      

  数据驱动视图(MVVM,setState)

      • 传统组件只时静态渲染,更新还要依赖于操作DOM。
      • Vue 是数据驱动视图,通过MVVM实现的数据驱动视图
      • React 是数据驱动视图,通过setState实现的数据驱动视图  

  Vue 的MVVM 

      • Model(模型/数据): vue 中 data里面定义的数据
      • View(视图): vue 中的 DOM元素,最总展示在页面上的视图
      • ViewModel(模型视图):负责将模型转化成视图,并监视图的变化来更新数据模型。vue事件绑定和methods中的事件回调方法                     

 

二、响应式原理

  组件 data 的数据一旦变化,立刻触发视图更新,这也是实现数据驱动的第一步。那么数据是怎么触发视图更新的呢?

  1. 核心API -- Object.defineProperty
    Object.defineProperty基本用法:
复制代码
let obj = {};
let name = 'zhangsan';
Object.defineProperty(obj,'name',{
      enumberable: true, // 是否可枚举
     writeable: true, // 是否可写
     configurable: true , // 是否可修改
     get(){
            console.log('执行了 get 操作')
            return name
     },
    set(newValue){
             console.log(' 执行了 set 操作')
             name = newValue
     }
})

obj.name = 'lisi'  //执行了 set操作
console.log(obj.name) // 执行了 get 操作
复制代码
  2. 如何实现响应式,如何监听对象(深度监听,递归调用),监听数组(重写数组方法push、pop、shift、unshift、splice、sort、reverse)
  1.数据data 的响应式简单实现
    将data中的属性用Object.defineProperty 重新定义,监听起来。该API 不支持数组。具体实现如下:
复制代码
 //触发更新视图
  function updateView(){
    console.log('更新视图')
  }
  
  //监听对象属性
  function observer(obj){
    if(typeof obj !== 'object'){
      // 不是对象或者数组,直接返回,不用重新定义属性
      return obj
    }
     //重新定义各个属性(for in 也可以遍历)
     for(let key in obj){
        defineReactive(obj,key,obj[key])
     }
  }
 
  //重新定义属性,监听起来
  function defineReactive(target,key,value){
    //核心 API
    Object.defineProperty(target,key,{ //不支持数组
      get(){
        return value
      },
      set(newValue){
        if(newValue !== value){
          //设置新值
          //注意:value 一直在闭包中
          value = newValue
          //触发更新是图
          updateView()
        }
      }
    })
  }

  //准备数据
  const data = {
    name:'zhangsan',
    age: 20
  }

  //监听数据
  observer(data)

  //测试
  data.name = 'lisi'
  data.age = 21
复制代码

  上面的就实现了数据的响应式,即数据驱动视图,只要数据变化就会触发视图更新。

  如果data的属性值是对象类型,上面的实现就无法监听数据改变后通知视图更新:例如:

复制代码
//准备数据
  const data = {
    name:'zhangsan',
    age: 20,
    info:{
      address: '北京' // 需要深度监听,数据有多深就要递归几次,并且是一次性监听完
    }
  }


//测试
data.info.address='上海' //此时不会监听到数据变化
复制代码

  2.对于data 中的属性值 是 对象类型的,需要深度监听,一次性递归到底,具体实现如下:

  只需要在上一个的基础上判断

复制代码

 //监听对象属性
  function observer(obj){
    if(typeof obj !== 'object'){
      // 不是对象或者数组,直接返回,不用重新定义属性
      return obj
    }
     //重新定义各个属性(for in 也可以遍历)
     for(let key in obj){
        defineReactive(obj,key,obj[key])
     }
  }
//重新定义属性,监听起来
  function defineReactive(target,key,value){
    //深度监听(如果value是一个对象的时候需要递归)
    observer(value) //再循环一次

    //核心 API
    Object.defineProperty(target,key,{ //不支持数组
      get(){
        return value
      },
      set(newValue){
        if(newValue !== value){
          //深度监听(设置新值的时候也要有深度监听)
          observer(newValue)

          //设置新值
          //注意:value 一直在闭包中
          value = newValue

          //触发更新是图
          updateView()
        }
      }
    })

 //准备数据
  const data = {
    name:'zhangsan',
    age: 20,
    info:{
      address: '北京' // 需要深度监听,数据有多深就要递归几次,并且是一次性监听完
    }
  }
  //监听数据
  observer(data)

  //测试
  data.name = 'lisi'
  data.age = 21
  data.info.address='上海' //深度监听
复制代码

  3、对于data中属性值是 数组类型的,除了监听数组中对象的属性(如果数组中有对象),还需要重写数组的原型方法。具体实现如下:

复制代码
//重新定义数组原型
const oldArrayProperty = Array.prototype;
// 创建新对象,原型指向oldArrayPrpperty,再扩展新的方法,不会影响原型
const arrProto = Object.create(oldArrayProperty);
//需要重写的数组的方法
['push','pop','shift','unshift','splice','sort','reverse'].forEach(method =>{
    arrProto[method] = function(){
      updateView() //先触发视图更新
      oldArrayProperty[method].call(this,...arguments) //再调用数组原型中的方法
    }
});
//监听对象属性
function observer(obj){
    if(typeof obj !== 'object'){
       // 不是对象或者数组,直接返回,不用重新定义属性
       return obj
    }
    if(Array.isArray(obj)){//是数组,重写数组原型上的方法(需要先出触发视图更新,然后再执行数组原型上的方法)
       obj.__proto__ = arrProto //将数组的原型上的方法重写
       observerArray(obj)
    }else{
       //重新定义各个属性(for in 也可以遍历)
       for(let key in obj){
          defineReactive(obj,key,obj[key])
       }
    }
}
//将数组中的对象属性监听起来(如果数组中有对象的话)
  function observeArray(items){
    for(let i = 0; i < items.length;i++){
      observer(items[i]) //观察数组中的每一项都进行地柜
    }
  }

//准备数据
  const data = {
    nums:[10,20,30] 
  }
data.nums.push(4) //监听数组
复制代码

 4. 综上,整个数据驱动视图响应式原理,代码demo:

复制代码
//触发更新视图
function updateView(){
    console.log('更新视图')
}
//重新定义数组原型
const oldArrayProperty = Array.prototype;
// 创建新对象,原型指向oldArrayPrpperty,再扩展新的方法,不会影响原型
const arrProto = Object.create(oldArrayProperty);
//需要重写的数组的方法
['push','pop','shift','unshift','splice','sort','reverse'].forEach(method =>{
    arrProto[method] = function(){
       updateView() //先触发视图更新
       oldArrayProperty[method].call(this,...arguments) //再调用数组原型中的方法
    }
});

 //监听对象属性
 function observer(obj){
    if(typeof obj !== 'object'){
      // 不是对象或者数组,直接返回,不用重新定义属性
      return obj
    }
    
    if(Array.isArray(obj)){//是数组,重写数组原型上的方法(需要先出触发视图更新,然后再执行数组原型上的方法)
      obj.__proto__ = arrProto //将数组的原型上的方法重写
      observerArray(obj)
    }else{
       //重新定义各个属性(for in 也可以遍历)
      for(let key in obj){
        defineReactive(obj,key,obj[key])
      }
    }
  }
 
  //重新定义属性,监听起来
  function defineReactive(target,key,value){
    //深度监听(如果value是一个对象的时候需要递归)
    observer(value) //再循环一次

    //核心 API
    Object.defineProperty(target,key,{ //不支持数组
      get(){
        return value
      },
      set(newValue){
        if(newValue !== value){
          //深度监听(设置新值的时候也要有深度监听)
          observer(newValue)

          //设置新值
          //注意:value 一直在闭包中
          value = newValue

          //触发更新是图
          updateView()
        }
      }
    })
  }

  //将数组中的对象属性监听起来(如果数组中有对象的话)
  function observeArray(items){
    for(let i = 0; i < items.length;i++){
      observer(items[i]) //观察数组中的每一项都进行地柜
    }
  }

  //准备数据
  const data = {
    name:'zhangsan',
    age: 20,
    info:{
      address: '北京' // 需要深度监听,数据有多深就要递归几次,并且是一次性监听完
    },
    nums:[10,20,30] 
  }
  //监听数据
  observer(data)

  //测试
  data.name = 'lisi'
  data.age = 21
  data.sex = 0 //新增属性监听不到,需要使用 Vue.set(data,'sex',0) 或者 this.$set(data,'sex',0)
  delete data.name   //删除属性监听不到, 需要使用 Vue.delete(data,'name') 或者 this.$delete(data,'name')

  data.info.address='上海' //深度监听
  data.nums.push(4) //监听数组
复制代码
      3.Object.defineProperty的一些缺点(Vue3.0 启用Proxy来实现响应式原理) 
      • 深度监听需要递归到底,一次性计算量比较大(如果对象深度表较深的话)
      • 无法监听新增属性和删除属性(新增删除需要使用Vue.set、Vue.delete这两个API来实现)
      • 无法监听数组原生方法,需要特殊处理    

四、模板编译   

     1.vue中模板为什么需要编译: 

      • 模板不是原生html, 有指令、插值、JS表达式、判断、循环,这些在原生html语法中不存在,但页面也正常显示了,因此,模板一定是转换了某种JS代码(即编译模板)
      • 这就归功于vue的模板编译了,vue会把<template></template>标签中写的类似于原生html的内容进行编译。
      • 把原生HTML找出来,非原生HTML找出来经过一系列处理生成渲染函数(render函数)
      • 执行render函数会将模板内容生成VNode
      • VNode再执行 patch和 diff
      • 最后根据VNode创建真实的DOM节点插入到视图中,最终完成视图的渲染更新 

    2.编译模板的核心流成 

      • 模板解析:将模板字符串解析成抽象语法AST
      • 优化阶段:遍历AST,找出其中的静态节点并打上标记,为后续更新渲染可以直接跳过静态节点做优化
      • 生成render函数: 将AST生成render函数  

    在开发环境下我们可以使用 webpack vue-loader,会在开发环境下编译模板

五、组件渲染过程 

    1、初次渲染过程

      • 解析模板为render函数(或者在开发环境已经通过vue-loader完成了解析过程) 
      • 执行render函数,生成vnode
      • 执行render函数的同时,模板中用到的响应式数据, 会触发getter,收集依赖watcher
      • 执行patch(elem,vnode)过程,页面初次渲染完成      

    2、更新过程

      •  修改data,会触发setter,通知watcher重新渲染 (watcher此前在getter中已被监听)
      • 重新执行render函数,生成newVnode 
      • 执行patch(vnode,newVnode)过程

    3、异步渲染  

      • 回顾$nextTick
      • 汇总 data 修改,一次性更新视图
      • 减少DOM操纵次数,提高性能        

    渲染/更新流程图: 

 

      

我们创建Vue实例,需要两行代码:

import Vue from ‘vue'
new Vue(options)

这两步分别经理了一个比较复杂的构造过程:

  1.创建vue构造函数,以及Vue一系列的原型方法

  2.创建一个 Vue实例,初始化数据、事件、模板等

一、创建Vue类

  我们导入Vue的时候,会生成一个Vuew的构造函数,然后通过initMixin、stateMixin、eventMxin、lifecyleMixin、renderMixin 为Vue.prototype(原型) 上添加一些属性和方法。代码如下:

复制代码
//  省略import语句
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)
}
initMixin(Vue)//主要添加了 _init方法,Vue构造函数中调用的this._init(options)就是这个方法
stateMixin(Vue)//主要添加了 $data,$props,$watch,$delete几个属性和方法
eventsMixin(Vue)//主要添加了$on, $off, $once,$emit三个方法
lifecycleMixin(Vue)//主要添加了 _update,$forceUpdate,$destroy 三个方法
renderMixin(Vue)//主要添加了 $nextTick 和 _render
export default Vue
复制代码

Vue原型上添加完属性和方法后,会调用initGlobalAPI(Vue)为Vue添加一些全局的属性和方法,总结一下全局的方法如下:

  • Vue.use  注册插件
  • Vue.component 注册组件
  • Vue.directive 注册指令
  • Vue.nextTick 下一个tick执行函数
  • Vue.set/delete 数据的修改
  • Vue.mixin 混入

二、创建Vue实例 

    通过 new Vue(options) 来创建一个实例,构造函数中只调用了 this_init(options) 进行初始化,看一下这个函数的具体实现(部分源码)

复制代码
Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // merge options
      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
    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')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
复制代码

从上面代码中可以了解到Vue实例的初始化过程做了如下工作:

  • 首先是进行了options合并最总生成了$options,其次依次调用如下函数
  • initLifecycle(vm) :初始化一些生命周期的初始工作,主要是设置了父子组件的引用关系即:$parent 和 $children
  • initEvents(vm) :注册了父组件的事件。父组件的监听器
  • initRender(vm): 初始化了render准备工作,比如处理父子继承关系,这里并没有真正开始render
  • calHook(vm,'beforeCreate') 创建beforeCreate生命周期
  • initInjections(vm): //resolve injecyions before data/props
  • initState(vm):data,props,computed,methods,watch 的初始化 以及数据响应的实现都是在这各函数里实现的
  • initProvide(vm): 初始化 provide
  • callHook(vm,'created') : 创建created生命周期,此时data、props、methods 、watch、computed 都初始化完成(即此时可以获取到这些值/方法)
  • 最后如果options中指定了el,则进行$mount挂载阶段。而一般情况下我们是不设置el,而是通过直接调用$mount('#app)来触发的

以上就是Vue实例的初始化过程,其中 initState 函数是最重要的,因为涉及到数据的响应式的实现。下面详细介绍一下数据响应式的原理

三、vue中的data 响应式原理

   initState 主要是对 props,methods,data,computed,watch进行了初始化。我们可以选择最简单的但也能完整揭示数据响应式原理的 data 作为切入点

  1.首先是通过getData 把options中传入的data取出来.这期间做了一些依赖处理那 getData又做了哪些事呢? 

    1.首先调用pushTarget() 这里没有传参数,目的是把Dep.target 给清空,这样就不会在获取 data初始值的过程意外把依赖记录下来

    2.然后返回data函数的返回值

    3.最后调用了 popTarget()

  2. this._data = data 将data赋值给_data

  3.然后对遍历data中的key,调用了proxy(vm, `_data`, key)。将每个可以在vm上都做一次代理。这样方便以后取值

    数据代理是通过Object.defineProperty来实现的具体代码如下:

复制代码
function proxy (target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  };
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val;
  };
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
复制代码

   其中target 是组件自身 vm,sourceKey 是_data,也就是我们的data。通过上面的代码会把vm上的数据的读写代理到_data上。这也就是为什么我们通过data.msg定义的数据,却可以通过this.msg访问到

  4. 数据代理处理完之后,开始实现数据响应式,调用observe()方法,observe函数中主要就是创建了一个Observer实例 即 :new Observer()

    Observer构造函数主要功能:

    1.首先new Dep()来记录对value的依赖;

    2.如果value是数组, 对数组的每一项进行递归调用observe;

    3.如果value不是数组而是一个对象,则会遍历value将每一个key做响应处理,即调用defineReactive(obj,key[i])

复制代码
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  /**
   * Walk through each property and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]) //数组中的每一项都进行递归observe
    }
  }
}
复制代码

  4. defineReactive实现响应式的步骤:

    第一步:先创建dep,用来收集对当前obj.key的依赖

    第二步:通过Object.defineProperty(obj,key,{})把对象的每一个属性通过getter/setter 进行拦截读写操作

    第三步:当对象的属性被读取时,会被getter拦截到,首先会把需要的值取出来,然后会判断Dep.target存在就会dep.depend()添加依赖。其中Dep.target其实就是watcher

    第四步:当对象属性被修改时,会被setter拦截到,setter首先会把设置新的值,然后通过 dep.notify 来通知响应的tagert(watcher)去渲染,其中water的创建是在beforeMount周期之后mounted周期之前

复制代码
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }
  // cater for pre-defined getter/setters
  const getter = property && property.get
  if (!getter && arguments.length === 2) {
    val = obj[key]
  }
  const setter = property && property.set
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
复制代码

  5.initState执行完之后会进入挂在阶段,对模板进行编译生成渲染函数

  6.然后会创建一个watcher,并将Dep.target设置为自己

  7.执行渲染函数,在执行过程中会读取到某属性,在读取的时候会触发该属性的get操作,会收集本属性的watcher

  8.如果Dep.target存在就添加到队列中,如果Dep.target不存在说明并不是watcher要获取的属性

  9.将watcher成功收集到了对应属性的dep中,这样,在修改属性触发set的时候,就能通过dep.notify触发watcher.update将watcher加到异步队列中,通过nextTick使其在下一个事件循环中进行执行更新视图

 

vue2.x原理流程如:

 

posted @   yangkangkang  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示