面试五 vue源码解析

文章目录

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

综述

面试五 vue源码解析_第1张图片

vue源码分析

说明

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

    数据代理

    模板代理

    数据绑定

  2. 不直接看vue.js源码

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

准备知识

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

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

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

【常见node和nodeType】:

常量 说明
Node.ELEMENT_NODE 1 一个元素节点,如:

Node.TEXT_NODE 3 文本节点
Node.DOCUMENT_FRAGMENT_NODE 11 一个DocumentFragment节点

因此documentFragment节点也属于节点

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

<ul id='test'>
  <li>helloli>
  <li>helloli>
  <li>helloli>
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里面的数据

基本实现流程:

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

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

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

面试五 vue源码解析_第2张图片

模板解析

大括号

【预备知识】:

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

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

【大括号解析步骤】:

  1. 根据$options配置对象里面的el值,来从页面查找对应的标签

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

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

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

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

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

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

面试五 vue源码解析_第3张图片

事件指令

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

面试五 vue源码解析_第4张图片

一般指令

面试五 vue源码解析_第5张图片

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

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

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

    v-text==> textContent属性

    v-html==>innerHtml属性

    v-class==> class属性

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

  5. 移除元素的指令属性

数据绑定

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

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

【数据劫持】:

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

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

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

【数据劫持的准备】:

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

遍历每一个data属性

  1. 给当前属性创建Dep实例(data中的属性和dep是一一对应的关系)
  2. 设置属性的getter, 每次获取属性值时,对watcher进行判断进行关联
  3. 设置属性的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调用的情况:

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

面试五 vue源码解析_第6张图片

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的初始化】:

面试五 vue源码解析_第7张图片

【watcher的初始化】:

面试五 vue源码解析_第8张图片

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

面试五 vue源码解析_第9张图片

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

面试五 vue源码解析_第10张图片

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

【要知道的】:

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

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

面试五 vue源码解析_第11张图片

  1. 【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'会发生什么??

面试五 vue源码解析_第12张图片

  1. this.name = '李四'

  2. data中的name属性值发生改变

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

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

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

  5. 遍历相关的所有watcher

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

  6. 调用watcher的更新回调

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

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

    【一般指令】: v-textv-htmlv-class

  7. 调用对应的updater

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

面试五 vue源码解析_第13张图片

MVVM结构图

面试五 vue源码解析_第14张图片

双向数据绑定

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

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

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

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

    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的一个插件

状态自管理应用

面试五 vue源码解析_第15张图片

多组件共享状态的问题

【案例问题】:

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

【以前处理】:

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

现在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: '',
  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

总结

一般使用顺序:

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

  2. actions中定义:

    const actions : {
           
            handleIncrement({
           commit}){
           
              commit('increment')
            }
          }
    
  3. mutations中定义increment

    const mutations: {
           
            increment (state) {
           
                //更改状态
                ...
            }
        }
    

你可能感兴趣的:(面试)