Vue2.x原理
vue2 原理大概分如下几部分:
组件化:
-
-
- 很久以前已经有组件化了,如: asp、jsp、php、nodejs也有类似组件
-
数据驱动视图(MVVM,setState)
-
-
- 传统组件只时静态渲染,更新还要依赖于操作DOM。
- Vue 是数据驱动视图,通过MVVM实现的数据驱动视图
- React 是数据驱动视图,通过setState实现的数据驱动视图
-
Vue 的MVVM
-
-
- Model(模型/数据): vue 中 data里面定义的数据
- View(视图): vue 中的 DOM元素,最总展示在页面上的视图
- ViewModel(模型视图):负责将模型转化成视图,并监视图的变化来更新数据模型。vue事件绑定和methods中的事件回调方法
-
二、响应式原理
组件 data 的数据一旦变化,立刻触发视图更新,这也是实现数据驱动的第一步。那么数据是怎么触发视图更新的呢?
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 操作
//触发更新视图 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) //监听数组
-
-
- 深度监听需要递归到底,一次性计算量比较大(如果对象深度表较深的话)
- 无法监听新增属性和删除属性(新增删除需要使用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原理流程如:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南