面试五 vue源码解析

it2023-01-07  63

文章目录

综述vue源码分析说明准备知识数据代理模板解析大括号事件指令一般指令 数据绑定dep和watcher的关系MVVM结构图双向数据绑定 vuex状态自管理应用多组件共享状态的问题vuex-counter应用vuex核心APIstateactionsmutationsgettersmodules向外暴露store对象映射storestore对象总结

综述

vue源码分析

说明

主要分析vue作为MVVM框架的基本实现原理

数据代理

模板代理

数据绑定

不直接看vue.js源码

剖析github上某基友模仿vue实现的mvvm库:https://github.com/DMQ/mvvm

准备知识

documentFragment:文档对象(批量更新多个节点)

document:对应显示的页面,且包含n个element,一旦更新document里面定义一个element,页面也会更新

documentFragment:不与界面相关联,包含n个element的【容器对象】,如果更新frament里面的某一个element,页面不会更新

【常见node和nodeType】:

常量值说明Node.ELEMENT_NODE1一个元素节点,如: 或

Node.TEXT_NODE3文本节点Node.DOCUMENT_FRAGMENT_NODE11一个DocumentFragment节点

因此documentFragment节点也属于节点

【案例】:将ul下的li内容更改为’hi

<ul id='test'> <li>hello</li> <li>hello</li> <li>hello</li> </ul>

【代码实现】:

const test = document.getElementById('test') // 创建fragment容器对象 const fragment = document.createDocumentFragment() // 获取test中的所有节点(也包含文本节点),并插入到fragment对象中 let child while(child = test.firstChild){ fragment.appendChild(child) // 取出test中子节点并保存到fragment中 } // 更改这些节点的内容 Array.prototype.slice.call(fragment.childNodes).forEach( node => { if(node.nodeType == 1){ // 元素节点 node.textContent = 'hi' } } ) // 将更改好的fragment插入到document中 test.appendChild(fragment)

数据代理

数据代理:通过一个对象代理对另一个对象中属性的操作(读/写)

vue数据代理:通过vm对象代理对data对象中所有属性的操作

好处:更方便的操作data里面的数据

基本实现流程:

遍历data中的每一个属性,并通过Object.defineProperty()给vm添加与data对象的属性对应的属性描述符

所有添加属性都包含getter/setter

getter/setter内部去操作data中对应的属性数据

模板解析

大括号

【预备知识】:

var reg = /\{\{.*}\}\/匹配左右两边的大括号,以及中间是任意多个字符

var reg = /\{\{(.*)}\}\/里面的小括号=》子匹配。既匹配了左右两边的大括号,又将里面的变量值进行匹配并保存在RegExp.$1里面。如果有多个子匹配,则分别保存到:RegExp.$2、RegExp.$3、RegExp.$4等

【大括号解析步骤】:

根据$options配置对象里面的el值,来从页面查找对应的标签 <div id = 'test'></div>

将该元素中所有【子元素】都【拷贝】到fragment容器中。此时test容器中的标签为空。注意fragment也是节点,它的所有子节点就是之前test元素中的所有子节点

遍历fragment中的所有子节点,对遍历的每一个节点:

a. 判断它是【元素节点】还是【文本节点】

元素节点:编译元素节点内的所有指令(一般指令和事件指令)文本节点且它的textContent有大括号:根据大括号里面的变量从vm的配置对象中的data获取数据,然后在赋值给文本节点

b. 判断遍历的子节点内是否还有子元素节点,如果有就再次递归调用进行编译

编译好的fragment重新插入到页面中

事件指令

根据指令名获取事件类型根据指令的值即v-on:click="show"从vm的methods方法中得到对应的事件处理函数给当前元素节点绑定事件名和回调函数的dom事件监听指令解析完后,移除此指令属性

一般指令

得到指令名和指令值 (一条指令:v-text='msg')

从vm的配置对象的data中根据指令值获取对应的数据

根据指令名来确定需要操作元素节点的什么属性

v-text==> textContent属性

v-html==>innerHtml属性

v-class==> class属性

将得到的表达式的值设置到对应的属性上

移除元素的指令属性

数据绑定

一旦更新了data中的某个属性数据,所有界面上直接使用或间接使用了此属性的节点都会更新

换句话说:更新数据=》对应界面会改变

【数据劫持】:

是vue中用来实现数据绑定的一种技术(使用Observer劫持监听所有属性)基本思想:通过defineProperty()来监视,data中的所有属性(任意层次)数据的变化,一旦改变就去更新对应的界面

当执行vm.msg = 'a'时,vm.set()最先察觉到,会更改data里面的值,然后data里面的set()被调用去更新对应的界面

因此vm里面的set是实现数据代理的;data里面的set是实现数据绑定的

【数据劫持的准备】:

​ 对配置对象中的data的每一个属性(每一层)都进行defineProperty()操作。具体设置如下:

遍历每一个data属性

给当前属性创建Dep实例(data中的属性和dep是一一对应的关系)设置属性的getter, 每次获取属性值时,对watcher进行判断进行关联设置属性的setter,每次更改属性值时,通知当前属性的dep所关联到的所有watcher进行更新 // 每一个属性都对应一个新的Dep实例 var dep = new Dep() // 当前属性值如果是对象,则再次遍历对其也进行数据劫持 var childOjb = observer(val) Object.defineProperty(data, key, { enumerable: true, // 可枚举 configurable: false, // 不能再define // 获取该属性的值时系统自动调用 get: function() { // 如果该watcher用到了该属性,且没有和当前属性的dep进行关联,则将watcher添加到dep.subs里面 if (Dep.target) { dep.depend(); } return val; }, // 设置该属性的值时系统自动调用 set: function(newVal) { if (newVal === val) { return; } val = newVal; // 新的值是object的话,进行监听 childObj = observe(newVal); // 通知订阅者。通知dep.subs里面所有的watcher进行更新数据 dep.notify(); } });

【注意:】

上面属性的getter调用的情况:

在首次模板编译时:当遇到{{msg}}或一般指令v-text='msg'时,都会用到data里面的属性的值,因此这个时候会调用该属性的getter,此时每一个{{msg}}或一般指令都会各自对应自己的watcher。即此时Dep.target不为空(等于大括号或一般指令的watcher),这个时候在获取属性的值调用getter时,就会将watcher关联到当前的属性的dep.subs数组里面。最后要记得Dep.target置为null当更该数据时:比如:this.msg='哈哈'时,系统会自动调用该属性的setter,在setter里面会跟新属性的值,同时还会通知该属性dep所关联的所有watcher进行更新。而watcher在进行更新时,会获取vm中data里该属性值进行页面的更新。(此时获取vm中data里面的数据时也会自动调用该属性的getter,而当前watcher早已经在进行模板编译时就已经关联到该属性的dep上了。此时的Dep.target=null,因此不会再次进行关联,这次的getter操作只是单纯的获取属性的值而已)

dep和watcher的关系

实例创建的时机个数结构Dep初始化时给data的每一个属性进行数据劫持时与data里面的属性一一对应如下:Watcher初始化时解析大括号表达式或一般指令时创建模板中大括号表达式+一般指令的数量如下:

【Dep的结构】:

ids: 标识 subs: [] // n个相关的Watcher容器

【Watcher的结构】:

this.cb = cb; // 用于更新界面的回调 this.vm = vm; // vm对象 this.exp = exp; // 对应的表达式 this.depIds = {}; // 相关的n个dep的容器对象 this.value = this.get(); // 当前表达式对应的value

【dep的初始化】:

【watcher的初始化】:

【Dep的创建时机】:初始化时给data的每一个属性进行数据劫持时

【Watcher的创建时机】:初始化时解析大括号表达式或一般指令时创建

【dep和watcher建立关系的时机】:

【要知道的】:

每个属性在进行数据劫持实现数据绑定时就已经创建了对应的dep

当编译解析大括号表达式或一般指令时

【dep和watcher的关系】?

两者是多对多的关系

name => 一个dep => n个watcher

模板中有多个表达式使用到此属性。(wacher个数 = {{}} + 一般指令)

一个表达式=>一个watcher=>n个dep

一个表达式是多层表达式时,比如:this.user.sex = 'male'(使用到多个属性,每一个属性分别对应一个dep)

【dep和watcher什么时候建立关系的】:初始化解析模板中的表达式时,创建watcher的内部就会建立两者之间的关系

dep先被创建(Observer数据劫持中)

watcher后被创建(解析模板的时候)

【dep和watcher在哪里建立关系的】:data中每个属性对应的get()方法中

【分析】:如果执行vm.name = 'xx'会发生什么??

this.name = '李四'

data中的name属性值发生改变

name属性对应的set()方法被调用

在set()方法中调用dep.notify

因为Dep实例和属性值一一对应,因此每一个属性在数据劫持Observer()都会创建一个dep实例,且每一个dep都存放着和他相关联的所有watcher

遍历相关的所有watcher

每一个Dep实例都存有相关的watcher(数组存放)分别执行每一个watcher的run(),根据新的value和旧的value调用watcher的更新回调

调用watcher的更新回调

创建Watcher实例时会传入更新的回调作为参数。最终去调用updater中对应的指令

Watcher实例的个数 = 编译 {{}}的个数 + 编译【一般指令】的个数

【一般指令】: v-text、 v-html、 v-class等

调用对应的updater

【案例】:根据配置对象中data以及页面中的代码来推断dep和watcher之间的关系

MVVM结构图

双向数据绑定

【要知道:】单向数据绑定的流程?

更改配置对象中data里面的某一个属性比如:name = 'jack'。则该属性的set被调用 =》 set里面的dep.notify被调用 =》相关的watcher被调用 =》每一个watcher对应的页面数据更新

双向数据绑定时建立在单向数据绑定的基础上。首先会执行this.bind(node, vm, exp, 'model');

双向数据绑定的实现流程:

a: 在解析v-model指令时,给当前元素添加input监听

b: 当input的value发生变化时,将最新的值赋值给当前表达式所对应的data属性。data属性发生变化=》该属性的set()被调用 =》 dep.notify();通知该属性dep所关联的watcher =》 watcher更新 =》 页面更新

// v-model 双向数据绑定 model: function(node, vm, exp) { // 即双向绑定是建立在单项绑定的基础上 this.bind(node, vm, exp, 'model'); var me = this, // 获取该属性的值 val = this._getVMVal(vm, exp); // 给该节点绑定input监听,一旦输入内容就会执行传入的回调 node.addEventListener('input', function(e) { var newValue = e.target.value; if (val === newValue) { return; } // 数据有更新,调用compile的_setVMVal() => 调用该属性的set()方法 // =》 调用该属性dep所关联的watcher => watcher更新 =》 对应的页面更新 me._setVMVal(vm, exp, newValue); val = newValue; }); },

vuex

作用:对vue应用中多个组件的共享状态进行集中式的管理(读/写)

redux和react没有直接关联,它是一个独立的库

vuex是vue的一个插件

状态自管理应用

多组件共享状态的问题

【案例问题】:

​ 多个视图依赖同一状态,这个时候来自不同视图的行为如果需要变更同一状态,该怎么处理呢??

【以前处理】:

​ 将数据以及操作数据的行为都定义在父组件,然后在将数据和这些行为传递给需要的子组件(此时就有可能需要多级传递)

现在vuex就是来解决这个问题的!!

vuex-counter应用

【目录结构】:

store.js

// vuex的引入(同时也要将vue引入,因为vuex依赖于vue) import Vue from 'vue' import Vuex from 'vuex' // 声明使用 Vue.use(Vuex) // 暴露核心对象模块store const state = { } const mutations = { } const actions = { } const getters = { } export default new Vuex.Store({ state, // 状态对象,唯一的 mutations, // 包含多个更新state函数的对象 actions, // 包含多个对应事件回调函数的对象 getters // 包含多个getter计算属性的函数 })

此时store并没有和项目发生关系,需要在main.js中进行映射处理

【main.js】

import Vue from 'vue' import App from './App.vue' import Store from './store' const vm = new Vue({ el: '#app', components: {App}, template: '<App/>', Store }) Vue.use(vm)

vuex核心API

state

vuex管理的状态对象

它应该是唯一的

// state.js export default{ xxx: initValue, todosArr: [] }

actions

包含多个事件回调函数的对象

通过执行commit()来触发mutation的调用,从而间接更新state

谁来触发action??

组件中:$store.dispatch('handleIncrement',data1) // 第一个参数时 对应action名称,第二个参数:执行action所需的数据

可以包含异步代码(比如:定时器,ajax)

// actions.js export default{ zzz({commit,state},data1){ commit('yyy',{data1}) }, addTodo({commit},todoItem){ // 这里的ADD_TODO是mutation的常量标识 // 传入的数据从actions传递给mutations都会包装成一个对象的形式传过去,(和数据本身是什么类型无关) commit(ADD_TODO,{todoItem}) } /** handleIncrement ({commit}) { // 执行commit来触发mutation的调用,间接更新state commit('increment') } handleIncrementIfOdd ({commit,state}) { if(state.num % 2 !== 0){ commit('increment') } **/ }

mutations

包含多个直接更新state的方法(即回调函数)的对象

谁来触发mutations里面的方法?

action中的commit(‘mutation名称’)

只能包含同步代码,不可以有异步代码

// mutations.js import {ADD_TODO} from './mutation-type.js' export defalut{ yyy(state,{data1}){ 更新state的某个属性 }, // 因为要直接更新数据,因此第一个参数为state // 因为从actions里面传到mutations里面的数据是以对象包含然在传过来,因此接收时,还需以对象的形式,且名字要和actions里面数据对象的名字一直 ADD_TODO(state,{todoItem}){ state.todosArr.unshift(todoItem) } /** increment (state) { // 更新state中某个属性 state.num++ } */ } // mutation-type.js export const ADD_TODO = 'add_todo' // 对应添加操作,这里的add_todo大小写都可以,只是一个更新标识。

只要保证actions.js里面的commit(ADD_TODO,{todoItem})的第一个参数和mutations里面的有对应的函数就可以。

mutation-type.js文件:修改里面的标识,同时actions.js和mutations.js里面的函数名也会改变

getters

包含多个计算属性(get)的对象

谁来读取??

组件中:$store.getters.xxx

const getters = { mmm (state) { return ... } }

modules

包含多个module,每一个module都是一个store的配置对象module与一个组件(包含共享数据)对应

向外暴露store对象

// 引入vue、vuex、actions、mutations、state、getters Vue.use(Vuex) export default new Vuex.Store({ actions, mutations, state, getters })

映射store

main.js

import store from './store' new Vue({ ... store ... })

store对象

所有用vuex管理的组件中都多了一个属性$store,他就是一个store对象(是vuex的管理者)

【属性】:

$store{ // 注册的state对象 state: {} // 注册的getters对象 getters:{} }

【方法】 :$store.dispatch(actionName,data)分发调用action

【要知道的】:一旦在main.js中做了映射配置,所有的组件对象都多了一个属性$store

总结

一般使用顺序:

组件中$store.dispatch('handleIncrement')

actions中定义:

const actions : { handleIncrement({commit}){ commit('increment') } }

mutations中定义increment

const mutations: { increment (state) { //更改状态 ... } }
最新回复(0)