深入Vue框架

368 阅读15分钟

第一部分 Vue2响应式原理

1. Object.defineProperty

Object.defineProperty(obj, prop, descriptor)

- obj:必需,目标对象
- prop:必需,需定义或修改的属性名
- descriptor:必需,目标属性所拥有的特性

descriptor描述符包含以下内容:

  • value:被定义的属性值,默认为undefined

  • writable:属性值是否可以被重写,true可以重写,false不能重写。默认值为false

  • enumerable:属性值是否可以被枚举(使用for...in或Object.keys()),true可以被枚举,false不能被枚举。默认为false

  • configurable:是否可以删除属性或是否可以再次修改属性的特性(writable, configurable, enumerable),true可以被删除或可以重新设置特性,false不能被可以被删除或不可以重新设置特性。默认为false

存取器getter/setter

如果使用getter或setter方法时不能允许再使用writable和value这两个属性

  • getter:当访问该属性时,该方法会被执行。函数的返回值会作为该属性的值返回

  • setter:当属性值修改时,该方法会被执行。该方法将接受唯一参数,即该属性新的参数值

var obj = {};
var initValue = 'hello';
Object.defineProperty(obj,"newKey",{
    get:function(){
        //当获取值的时候触发的函数
        return initValue;
    },
    set:function (value){
        //当设置值的时候触发的函数,设置的新值通过参数value拿到
        initValue = value;
    }
});
//获取值
console.log(obj.newKey); //hello
//设置值
obj.newKey = 'change value';
console.log( obj.newKey ); //change value

不要在getter中再次获取该属性值,也不要在setter中再次设置改属性,否则会栈溢出!

2. 数据代理

构建Vue实例对象代码如下所示:

let vm = new Vue({
    data:{
        message:'hello vue.js'
    }
})

由于是通过new Vue创建的Vue实例对象,所以如果想模拟Vue底层的数据代理原理,在创建Vue时应该构建一个Vue类,并且类会接收一个options配置对象。

暂时先不考虑new Vue(函数)的情况,只讨论new Vue({...})

class Vue {
    constructor(options){
        this.$options = options
        this._data = options.data
        // 初始化数据代理
        this.initData()
    }
    initData(){
        let data = this._data
        let keys = Object.keys(data)
        // 通过Object.defineProperty对options中的每一个属性实现代理
        for (let i = 0;i < keys.length; i++) {
            Object.defineProperty(this,keys[i],{
                enumerable:true,
                configurable:true,
                // 函数名称可有可无,名称函数是为了后续操作方便
                get:function proxyGetter(){
                    return data[keys[i]]
                },
                set:function proxySetter(value){
                    data[keys[i]] = value
                }
            })
        }
    }
}

image.png

3. 数据劫持

当data中的属性值是基本数据类型时,单层for循环即可实现数据代理和劫持。

class Vue {
    constructor(options) {
        this.$options = options
        this._data = options.data
        this.initData()
    }
    initData(){
        let data = this._data
        let keys = Object.keys(data)
        // 数据代理
        for(let i = 0; i <keys.length; i++){
            Object.defineProperty(this,keys[i],{
                enumerable:true,
                configurable:true,
                get:function proxyGetter() {
                    return data[keys[i]]
                },
                set:function proxySetter(value) {
                    data[keys[i]] = value
                }
            })
        }
        // 数据劫持
        for(let i=0;i<keys.length;i++){
            let value = data[keys[i]];
            Object.defineProperty(this,keys[i],{
                enumerable:true,
                configurable:true,
                get: function reactiveGetter(){
                    console.log(`data中的${keys[i]}被读取`);
                    return value;
                },
                set: function reactiveSetter(val){
                    if(val === value) return;
                    console.log(`data中的${keys[i]}被修改为${val}`);
                    value = val;
                }
            })
        }
    }
}

image.png image.png

4. 数据递归劫持

let vm = new Vue({
    data:{
        message:'hello',
        person:{
            name:'小明',
            age:25,
            sex:"Man"
        }
    }
});

image.png

🤕:修改person的age属性时无法触发proxySetter监听函数

当data中的属性值为引用类型时,单层for循环则无法实现数据的深度劫持,需要对以上方法进行改写。

// 数据劫持
observe(data);

// 判断data的数据类型
function observe(data) {
    let type = Object.prototype.toString.call(data);
    if (type !== '[object Object]' && type !== '[object Array]') {
        return;
    }
    // 通过vue内置的Observer类实现引用类型的劫持
    new Observer(data);
}

// 抽离数据劫持代码
function defineReactive(obj, key, value) {
    // 递归判断数据类型
    observe(obj[key]);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            console.log(`${key}被读取`);
            return value;
        },
        set: function reactiveSetter(val) {
            if (val === value) return;
            console.log(`${key}被修改为${val}`);
            value = val;
        }
    })
}
// 实现内置的Observer类
class Observer {
    constructor(data) {
        this.walk(data);
    }
    // 抽离遍历data属性值的方法
    walk(data) {
        let keys = Object.keys(data);
        for (let i = 0; i < keys.length; i++) {
            defineReactive(data,keys[i],data[keys[i]]);
        }
    }
}

image.png image.png

5. Watcher监听

Vue项目中Watcher监听的调用如下:

// 第一种方式
vm.$watcher('message',()=>{
    ....
})

// 第二种方式
watch:{
    message(){
        ....
    }
}

Watcher监听的逻辑:

  • 每个属性可以有多个监听回调,所以需要使用数组存放每个属性的监听回调函数
  • 一个回调可能包含多个属性,所以getter需要具有收集当前回调的能力
  • 回调触发前需要收集,触发后需要移除来节省内存空间,所以可以存放在一个公共区域
  • 属性的回调函数异步执行
  • 属性的多个相同的回调函数会合并执行

实现Watcher数据监听

第一步:实现存放监听回调的筐“发布-订阅者模式

class Dep{
    constructor(){
        this.subs = []
    }
    // 收集回调
    depend(){
        if(Dep.target){
            this.subs.push(Dep.target)
        }
    }
    // 触发回调
    notify(){
        this.subs.forEach(watcher=>{
            watcher.run()
        })
    }
}

第二步:发布/订阅监听的回调函数

get: function reactiveGetter() {
    // 订阅
    dep.depend();
},

set: function reactiveSetter(val) {
    // 发布
    dep.notify();
}

第三步:构建watcher类(每个回调函数都是一个watcher实例对象)

let watchId = 0; // 回调函数唯一标识
let watchQueue = []; // 当前执行的回调函数队列

class Watcher{
    // vm实例、exp属性、cb回调函数
    constructor(vm,exp,cb){
        this.vm = vm
        this.exp = exp
        this.cb = cb
        // id自增,保持唯一性
        this.id = ++watchId
        this.get()
    }
    // 对属性求值
    get(){
        Dep.target = this
        this.vm[this.exp]
        Dep.target = null
    }
    // 更新队列中的回调函数,并异步执行当前回调
    run(){
        if(watchQueue.indexOf(this.id) !== -1){
            return;
        }
        watchQueue.push(this.id);
        let index = watchQueue.length-1;
        // 通过promise实现异步执行
        Promise.resolve().then(()=>{
            this.cb.call(this.vm);
            watchQueue.splice(index,1)
        })
    }
}

6. $set设置响应式属性

vue.$set方法可以随时添加响应式的属性,其实现逻辑如下所示:

  1. 在创建observer实例时,再创建一个新的dep筐,并挂在observer实例上
  2. 把observer实例挂载到对象的__ob__属性上
  3. 触发getter时,把watcher收集一份到之前的“筐”和创建的新dep筐
  4. 用户调用$set时,手动地触发__ob__.dep.notify()
  5. 在notify()执行前调用defineReactive把新的属性也定义成响应式

image.png

function set(target, key, val) {
    var ob = target.__ob__; // 1. 新框
    if (isArray(target) && isValidArrayIndex(key)) {
        target.length = Math.max(target.length, key);
        target.splice(key, 1, val);
        // 2. 收集watcher
        if (ob && !ob.shallow && ob.mock) {
            observe(val, false, true);
        }
        return val;
    }
    // 4. 将属性定义为响应式
    defineReactive(ob.value, key, val, undefined, ob.shallow, ob.mock);
    // 5. 触发通知函数
    {
        ob.dep.notify({
            type: "add" /* TriggerOpTypes.ADD */,
            target: target,
            key: key,
            newValue: val,
            oldValue: undefined
        });
    }
    return val;
}

7. Vue2响应式存在的问题

  • 对象:通过Object.defineProperty属性拦截实现响应式
    • 缺点1:对象每一个属性都需要重写,添加getter和setter
    • 缺点2:对象新增属性无法监测,需要借助$set方法
    • 缺点3:对象删除属性无法监测,需要借助$delete方法
    • 缺点4:多层对象需要通过递归实现响应式
  • 数组:重写数组原生API(push,shift,unshift,pop,splice,sort,reverse),实现数组元素的响应式
    • 缺点1:无法通过Object.defineProperty拦截,需要单独处理
    • 缺点2:通过索引改变数组,或者改变数组长度无法触发视图更新
    • 缺点3:ES6新增的Map、Set数据结构不支持

8. Vue响应式原理

image.png

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项时,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter,这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 追踪依赖,在属性被访问和修改时通知变化,每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

Vue响应系统三个核心的要素:observewatcherdep

  1. observe:遍历data中的属性,使用 Object.definePropertyget/set方法对其进行数据劫持
  2. dep:每个属性都有一个消息订阅器dep,存放所有订阅了该属性的观察者对象
  3. watcher:观察者对象,通过dep实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应

第二部分 虚拟DOM和diff算法

1. 虚拟DOM

Virtual DOM 就是用js对象来描述真实DOM,是对真实DOM的抽象

直接操作真实DOM性能很低,而且js层的操作效率相对较高,所以可以将DOM操作转化成对象操作,最终通过diff算法比对差异更新真实DOM。

虚拟DOM不依赖真实平台环境从而也可以实现跨平台应用。

image.png

1.1 createElement函数

createElement方法接收三个参数:

  • type表示标签类型
  • props表示标签属性
  • children表示标签子元素

createElement方法返回值为一个Element实例对象,用于创建虚拟节点。

function createElement(type,props,children){
    return new Element(type,props,children)
}
class Element{
    constructor(type,props,children){
        this.type = type
        this.props = props
        this.children = children
    }
    ... ...
}

虚拟DOM数据格式实例 image.png

  • sel:元素选择器
  • elm:对应的真实DOM节点,undefined表示该虚拟DOM还没有被添加到DOM树上
  • key:元素唯一标识符
  • data:元素的标签属性
  • text:文本内容
  • children:子元素

1.2 render函数

render函数将虚拟DOM转化为真实DOM

function render(vDom){
    const { type, props,children } = vDom;
    const el = document.createElement(type);
    // 处理props属性
    for(let key in props){
        switch (key){
            case 'value':
                if(el.tagName === 'INPUT' || el.tagName === 'TEXTAREA'){
                    el.value = props[key];
                }else{
                    el.setAttribute(key,props[key]);
                }
            break;
            case 'style':
                el.style.cssText = props[key];
            break;
            default:
                el.setAttribute(key,props[key]);
        }
    }
    // 处理子元素
    children.map((c)=>{
        c = c instanceof Element ? render(c) : document.createTextNode(c);
        el.appendChild(c);
    })

    return el;
}

1.3 renderDOM函数

renderDOM函数用于渲染真实DOM到页面上

function renderDOM(el,rootEl){
    // el虚拟DOM元素,rootEl根节点
    rootEl.appendChild(el);
}
renderDOM(vDom,document.getElementById('app'))

2. Diff算法

2.1 传统diff算法

假设新旧虚拟DOM看作两棵节点树,节点个数为n

  1. 左侧树的节点需要与右侧树的节点一一对比,需要O(n²)复杂度
  2. 删除未找到的节点,寻找合适节点放到被删除位置,需要O(n)复杂度
  3. 添加新节点,需要O(n)复杂度

所以,传统diff算法比较新旧虚拟DOM的复杂度是O(n³)

2.2 Vue中的diff优化

  1. 只比较同一层级节点,对于节点间跨层级的移动操作忽略不计
  2. 标签名不同直接删除,不再进行深度比较
  3. 标签名和key都相同,则认为是相同的节点,不再进行深度比较

Vue中的diff算法比较新旧虚拟DOM的复杂度是O(n)

2.3 diff算法逻辑

当数据发生变化的时候,会触发setter,通知所有的订阅者Watcher,订阅者会调用patch方法更新虚拟DOM及真实DOM。

以下代码仅包括关键环节,具体细节在此处省略

image.png

function patch(oldNode,vNode){
    // 如果oldNode不是虚拟节点,则通过emptyNodeAt方法根据oldNode创建虚拟节点
    if(!isVnode(oldNode)){
        oldNode = emptyNodeAt(oldNode);
    }
    // 是否为同一个节点
    if(sameVnode(oldNode,vNode)){
        // 继续进行节点间的diff对比
        patchVnode(oldNode,vNode,insertedVnodeQueue);
    }else{
        // 删除旧节点,创建新节点
        elm = oldNode.elm;
        parent = api.parentNode(elm);
        createElm(vNode,insertedVnodeQueue);
        if(parent !== null){
            removeVnodes(parent,[oldNode],0,0);
        }
    }
}

sameVnode()方法判断是否为同一个节点

// sel为元素选择器,key为虚拟节点唯一标识符
function sameVnode(vnode1,vnode2){
    return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}

patchVnode()方法进行两个节点的diff对比

image.png

function patchVnode(oldVnode,newVnode){
    if(oldVnode === newVnode) return;
    if(newVnode.text !== undefined && (newVnode.children === undefined || newVnode.children.length === 0)){
        // 新虚拟节点有text属性
        if(newVnode.text !== oldVnode.text){
            oldVnode.elm.innerText = newVnode.text;
        }
    }else{
        // 新虚拟节点没有text属性
        if(oldVnode.children !== undefined && oldVnode.children.length > 0){
            // 需要递归处理oldVnode和newVnode的子元素
            updateChildren(oldVnode.children,newVnode.children)
        }else{
            oldVnode.elm.innerHTML = '';
            for(let i=0;i<newVnode.children.length;i++){
                let dom = createElement(newVnode.children[i]);
                oldVnode.elm.appendChild(dom);
            }
        }
    }
}

updateChildren()方法进行子节点的对比

对于同一节点的children元素,需要添加唯一key值进行区分。

节点的对比过程包括四个指针:旧前、旧后、新前、新后

image.png

节点的对比流程如下所示:

  • 首尾对比:新前&旧前,新后&旧后
  • 交叉对比:新后&旧前,新前&旧后

image.png

如果命中一种查找条件后,不会继续向下执行其他查找条件 如果四个条件均为命中,则需要在oldVnode中遍历查找当前节点

  • 1️⃣/2️⃣命中:删除多余旧节点,创建新节点
  • 3️⃣新后与旧前命中:将新后指针指向的节点移动到老节点的旧后指针的后面
  • 4️⃣新前与旧后命中:将新前指针指向的节点移动到老节点的旧前指针的前面
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    var oldStartIdx = 0; // 旧前
    var newStartIdx = 0; // 新前
    var oldEndIdx = oldCh.length - 1; // 旧后
    var oldStartVnode = oldCh[0];
    var oldEndVnode = oldCh[oldEndIdx];
    var newEndIdx = newCh.length - 1; // 新后
    var newStartVnode = newCh[0];
    var newEndVnode = newCh[newEndIdx];
    var oldKeyToIdx, idxInOld, vnodeToMove, refElm;

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (isUndef(oldStartVnode)) {
            oldStartVnode = oldCh[++oldStartIdx];
        }else if (isUndef(oldEndVnode)) {
            oldEndVnode = oldCh[--oldEndIdx];
        }else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
            oldStartVnode = oldCh[++oldStartIdx];
            newStartVnode = newCh[++newStartIdx];
        }else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
            oldEndVnode = oldCh[--oldEndIdx];
            newEndVnode = newCh[--newEndIdx];
        }else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
            canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
            oldStartVnode = oldCh[++oldStartIdx];
            newEndVnode = newCh[--newEndIdx];
        }else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
            canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
            oldEndVnode = oldCh[--oldEndIdx];
            newStartVnode = newCh[++newStartIdx];
        }else {
            ... ...
        }
    }
}

2.4 Vue2与Vue3的差异

两者在剩余节点的处理方式上略有不同:

Vue2:Key To Index哈希表

  • 首先进行新老节点头尾对比,头与头、尾与尾对比,寻找未移动的节点

  • 新老节点头尾对比完后,进行交叉对比,头与尾、尾与头对比,寻找移动后可复用的节点

  • 然后在剩余新老节点中对比寻找可复用节点,创建一个旧节点的key To Index哈希表记录key,然后继续遍历新节点索引,通过key查找能复用的旧的节点

  • 节点遍历完成后,通过新老索引,进行移除多余旧节点或者增加新节点的操作

Vue3:剩余节点与旧节点索引哈希表

  • 创建一个映射关系数组,存放新节点数组中的剩余节点旧节点数组索引的映射关系

  • 通过数组直接确定可复用节点,然后通过映射关系表计算最长递增子序列(子序列内所有节点位置均正确,不用更新)

  • 最后将新节点数组中的剩余节点移动到正确的位置

2.5 patch更新

通过diff算法对比新旧虚拟DOM树后,会得到一个patches补丁集合,其数据格式如下所示:

patches={
    '0':[ // 元素下标
        {
            type:'ATTR', // 补丁类型
            attr:'list-wrap' // 新虚拟DOM较旧虚拟DOM的变化
        }
    ],
    '2':[
        {
            type:'ATTR',
            attr:'list-wrap'
        }
    ],
    ... ...
}

将获取的patches补丁包作用在真实DOM上,完成真实DOM得更新操作。

第三部分 Vue3响应式原理

1. Proxy

Proxy对象用于创建一个普通对象的代理,也可以理解成在对象前面设了一层拦截,包括基本操作的拦截和一些自定义操作(比如一些赋值、属性查找、函数调用等)。

var proxy = new Proxy(target, handler);
  • target:目标对象,需要代理的对象
  • handler:代理行为,包括各种操作的拦截函数

2. Reflect

Reflect是es6为操作对象而提供的新API,设计它的目的有:

  • 把Object对象上一些明显属于语言内部的方法放到Reflect对象身上,比如Object.defineProperty

  • 修改某些object方法返回的结果

  • 让Object操作都变成函数行为

  • Reflect对象上的方法和Proxy对象上的方法一一对应,这样就可以让Proxy对象方便地调用对应的Reflect方法

Reflect.get(target, propertyKey, receiver)等价于target[propertyKey]

Reflect.get方法查找并返回target对象的propertyKey属性,如果没有该属性,则返回undefined

Reflect.set(target, propertyKey, value, receiver)等价于target[propertyKey] = value

Reflect.set方法设置target对象的propertyKey属性等于value

3. Proxy和Reflect的使用

const obj = {
  name: 'win'
}

const handler = {
  get: function(target, key){
    return Reflect.get(...arguments)  
  },
  set: function(target, key, value){
    return Reflect.set(...arguments)
  }
}

const data = new Proxy(obj, handler);

image.png

4. 使用Proxy和Reflect完成响应式

reactive是一个返回Proxy对象的函数

function reactive(obj){
  const handler = {
    get(target, key, receiver){
      console.log('get--', key)
      const value = Reflect.get(...arguments);
      if(typeof value === 'object'){
        // 递归实现多层对象属性的响应式
        return reactive(value)
      }else{
        return value
      }
    },
    set(target, key, val, receiver){
      console.log('set--', key, '=', value)
      return Reflect.set(...arguments)
    }
  };
  
  return new Proxy(obj, handler);
}

const data = reactive({name: 'win'});

image.png

延伸

Vue中keep-alive的LRU算法实现

源码的 created 钩子里绑定了两个属性 catch 和keys

  • catch数组缓存所有组件的 vNode 实例
  • keys数组按顺序存储当前展示组件的标识符

当 catch 内原有组件被使用时会将该组件 key 从 keys 数组中删除,然后 push 到 keys 数组最后,如果新增后的缓存超过 max ,则将 keys 的首项删除掉。

created () {  
    this.cache = Object.create(null)  
    this.keys = []  
}  
  
cacheVNode() {  
  const { cache, keys, vnodeToCache, keyToCache } = this  
  if (vnodeToCache) {  
    const { tag, componentInstance, componentOptions } = vnodeToCache  
    cache[keyToCache] = {  
      name: getComponentName(componentOptions),  
      tag,  
      componentInstance,  
    }  
    keys.push(keyToCache)  // 添加到最后
   
    if (this.max && keys.length > parseInt(this.max)) {  
      pruneCacheEntry(cache, keys[0], keys, this._vnode) //删除首项  
    }  
    this.vnodeToCache = null  
  }  
}

LRU算法

LRU 算法的基本思想当缓存空间已满时,优先淘汰最近最少使用的缓存数据,以腾出更多的缓存空间。

可以通过Map数据结构模拟实现一个LRU算法。

class LRUCatchByMap {  
  length = 0;  
  catchs = new Map();  
  
  constructor(length) {  
    if (length < 1throw Error("缓存长度参数不合法");  
    this.length = length;  
  }  
  // 不存在怎返回null,存在的化更新到最后
  get(key) {  
    const { catchs } = this;  
    if (!catchs.has(key)) return null;
    const value = catchs.get(key);  
    catchs.delete(key);  
    catchs.set(key, value);  
    return value;  
  }  
  // 是否已经存在key,不存在则末尾追加key-value,存在的化则更新之前的value
  set(key, value) {  
    const { catchs, length } = this;  
    if (catchs.has(key)) {  
      catchs.delete(key);  
    }  
    catchs.set(key, value);  
    // 更新栈后,需要校验当前栈是否溢出  
    if (catchs.size > length) {  
      const delKey = catchs.keys().next().value;
      catchs.delete(delKey)  
    }  
  }  
}

Vue与React的对比

Vue3较Vue2增加了compositionAPI,其使用和原理更接近于函数式编程思想。在这里只讨论Vue3与React Hooks的区别。

相同点

  1. 组件化开发与复用
  2. 函数式编程
  3. 组件间单向数据流,数据驱动视图
  4. 虚拟DOM+diff算法
  5. 生态周边相对成熟,都支持SSR

不同点

  1. Vue更偏向传统前端开发,使用template模版拥抱html;React使用JSX语法拥抱JS
  2. Vue有丰富的API,React更接近于原生开发

表现

  1. 监听数据变化实现原理不同
  • Vue3:通过proxy实现数据劫持,数据变化后会触发diff算法
  • React:通过shouldComponentUpdate生命周期决定是否需要渲染更新,如果重新渲染会触发diff算法

Vue与React设计理念不同:Vue强调可变数据,React强调数据的不可变

  1. 响应式原理不同
  • Vue2:响应式的特点就是依赖收集,数据可变,自动派发更新

    • 初始化:通过Object.defineProperty递归劫持data中所有的属性并添加gettersetter
    • 触发getter时收集依赖,触发setter时自动派发更新,使引用组件重新渲染
  • Vue3:使用原生Proxy重构响应式

    • 优点1:proxy不存在响应式存在的缺陷
    • 优点2:proxy支持更多的数据结构,不用递归劫持对象的多层属性,而是直接代理第一层对象本身
    • 优化1:用effect副作用代替watcher
    • 优化2:用一个依赖管理中心trackMap代替Dep来统一管理依赖
  • React:响应式的特点是基于组件状态,单向数据流,数据不可变的原理

    • 数据不可变:需要手动触发setState来更新数据
    • 重新渲染整棵DOM树:当数据改变时会以当前组件为根目录,重新渲染整个组件树,可以通过pureComponentshouldComponentUpdateuseMemouseCallback等方法来控制组件的重新渲染,提高性能
  1. Diff算法不同
  • Vue2:四个指针(旧前、旧后、新前、新后)

    • 同层节点:比较新老vnode,新的不存在老的存在就删除,新的存在老的不存在就创建
    • 子节点:采用双指针头尾两端对比的方式
    • 全量diff,移动节点时通过splice进行数组操作
  • Vue3:采用Map数据结构以及动静结合的方式

    • 编译阶段:提前标记静态节点

    • Diff阶段:直接跳过有静态标记的节点

    • 子节点对比:使用一个source数组来记录节点位置及最长递增子序列优化对比流程

  • React:同层对比,获取diff差异队列

    • 递归虚拟DOM树,只进行同层比较
      • 新老节点不同(创建节点):删除老节点重新创建新节点
      • 新老节点相同(复用节点):将节点在新集合中的位置和老集合中的lastIndex进行比较是否需要移动
      • 剩余老节点(删除节点):直接删除
    • 标识差异点保存到Diff队列
    • 获取patch树,批量更新真实DOM