深入浅出Vue变化侦测__Vue.js
发布于 3 年前 作者 banyungong 1107 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

1.前言

我们都知道vue是个很优秀的框架,官网上也说明了是一个渐进式框架。那么什么是渐进式框架呢?
所谓渐进式,就是把框架分层。如图所示:

最核心的部分就是视图层渲染,然后往外就是组件机制,在这个基础上再加入路由机制,状态管理,构建工具。 所谓分层,就是既可以只用最核心的视图渲染功能快速开发一些需求,也可以使用全家桶开发大型应用。Vue足够灵活,根据自己的需求,选择不同的层级

视图层渲染作为最核心部分,其特性之一就是响应式系统,视图会随着状态的变化而变化。这也是我最喜欢Vue的地方,视图里任何一个地方,都可以用一种状态(变量)来表示。

从状态生成DOM,在输出到用户界面显示一整套过程叫做渲染。vue在运行时不断地重新渲染。而响应式系统赋予了框架重新渲染的能力,其重要组成部分就是变化侦测。学会了变化侦测,更有利于接下来对api的原理学习,接下来我们便开始从0到1实现一个变化侦测逻辑。

2.目录

3.1 什么是变化侦测
3.2 如何追踪变化
3.3 什么是依赖,如何收集依赖
3.4 依赖收集在哪里
3.5 依赖是谁,什么是watcher?
3.6递归侦测所有key
3.7 object的问题

3.1什么是变化侦测

上面我们说过,渲染就是Vue会自动通过状态生成DOM,并输出到页面上。Vue的渲染过程是声明式,我们可以通过模板来描述状态与DOM之间的映射关系。
在网页运行时,通过各种用户交互,Vue内部的数据状态会不断改变,此时页面也会不断渲染。但是,我们又怎么知道哪些状态发生了怎么样的改变?这就是变化侦测,只要是状态一改变,我们的vue就能知道,通过跟新的状态去渲染视图。

3.2如何追踪变化

关于追踪,在JavaScript中,我们如何知道一个对象改变了呢?

  • Object.defineProperty
  • ES6的proxy
    在Vue3之前,我们还是使用Object.defineProperty

我们知道,Object.defineProperty用来侦测变化会有很多缺陷,并且在Vue3之后都用Proxy重写这部分代码了,那么我们还有必要学习这部分吗?其实我觉得很有必要,我们毕竟是学习原理和思想的,通过对原理的探索,我们更能领会牛人解决问题的思想,在以后的编程路上,还是很有必要的。
知道了如何追踪对象的变化,那么我们就可以写出以下代码:

function defineReactive (data, key, val) {
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            return val
        },
        set: function (newval) {
            if (val === newval) {
                return
            }
            val = newval
        }
    })
}

我们定义了一个函数来封装了一下Object.defineProperty。其作用就是定义一个响应式数据,封装后我们只需传递data,key,val就行了。那么如何追踪变化?每当我们从data中的key读取数据时,get函数触发了,在设置data的key数据时,set就被触发了。

3.3什么是依赖,如何收集呢?

上面只是对Object.defineProperty进行封装了一下,但实际上并没什么作用,真正有用的是收集依赖。现在我们就有两个问题了:

  • 什么是依赖?
  • 如何收集依赖?

我们先回头思考一下,什么是响应式。就是数据改变了,视图自动更新。所以我们要去观察数据,当数据的属性发生变化时,我们就可以通知曾经使用了该数据的地方,这些地方,就被称作为依赖,举个例子:

<template>
  <div>
    <p>{{name}}</p>      //一个依赖
    <h2 v-text='name'></h2>         //另一个依赖
  </div>
</template>

模板中,有两个地方是用了数据name,所以就有两个依赖。当数据改变时,我们就要向这两个依赖发送通知。
通过变化侦测中,在读取数据的时候,会触发getter,所以就通过在getter函数中去收集依赖,数据发生变化时,就需要在setter中触发依赖,所以我们可以把defineReactive函数改造一下:

function defineReactive (data, key, val) {
    let dep=[]                     //新增,依赖收集器
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
        dep.push(window.target)    //新增,收集依赖,假设window.target就是一个依赖
            return val
        },
        set: function (newval) {
            if (val === newval) {
                return
            }
            val = newval
            dep.notify()      //新增,通知依赖数据改变,关于dep后面会讲,这里只是抽象的表示需要做的事情
        }
    })
}

3.4依赖收集在哪里

我们可以封装一个类Dep,专门帮助我们管理依赖。在这个类中,我们可以收集依赖,删除依赖,通知依赖等等,其代码如下:

export default class dep {
    constructor () {
        this.subs = []
    }
    addSub (sub) {
        this.subs.push(sub)
    }
    removeSub (sub) {
        remove(this.sub, sub)
    }
    depend () {
        if (window.target) {
            this.addSub(window.target)
        }
    }
    notify () {
        const subs = this.subs.slice()
        for (let i=0,l = subs.length;i<1;i++) {
            subs[i].update()
        }
    }
}
function remove (arr, item) {
    if (arr.length) {
        const index = arr.indexOf(item)
        if (index > -1) {
            return arr.splice(index, 1)
        }
    }
}
  • subs:用数组作为一个容器,收集依赖
  • addSub:添加依赖
  • removeSub:移除依赖
  • depend:假设window.target是一个依赖,判断是否有这个依赖,进而执行添加依赖操作
  • notify:通知每个依赖,数据变化了,要执行每个依赖的更新视图方法。

之后我们还需要改造一下defineReactive:

function defineReactive (data, key, val) {
    let dep = new dep()                         //创建依赖收集器
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function () {
            dep.depend()                       //判断是否有依赖
            return val
        },
        set: function (newval) {
            if (val === newval) {
                return
            }
            val = newval
            dep.notify()                        //通知每个依赖
        }
    })
}

3.5依赖是谁,什么是watcher?

在上面演示里,我们将winodw.target代表依赖,作用就是数据改变了,依赖就接受到了通知,然后再去通知其他地方。所以我们需要封装一个类,就叫watcher把。watcher实例就是一个一个的依赖。代码如下:

export default class watcher{
  constructor(vm,exp,cb){
    this.vm=vm
    this.getter=parsePath(exp)
    this.cb=cb
    this.value=this.get()
  }
  get(){
    window.target=this
    let value=this.getter.call(this.vm,this.vm)
    window.target=undefined
    return value
  }
  update(){
    const oldValue=this.value
    this.value=this.get()
    this.cb.call(this.vm,this.value,oldValue)
  }
}

watcher接受三个参数:

  • vm:vue实例
  • exp:{{}}这里面的表达式,还有v-text和v-html中的表达式
  • cb:真正的更新DOM的函数(知道作用就行,后面模板解析会详细讲解) 其它参数:
  • getter:通过parsePath函数解析表达式,获取表达式的值
  • value:get方法返回值
  • get:通过getter获取表达式的值
  • update:更新视图

现在关于对象变化侦测基本原理都已近说完了,可能你现在还是感觉很懵,接下来我将整个过程从头来顺一下:

初始化过程:

响应式过程:

3.6递归侦测所有key

上面,整个变化侦测功能都已近实现,但是,只能侦测数据中某一个属性,我们希望能够把数据中所有属性都要侦测到,于是我们就要封装一个observer类.通过递归的形式,把data数据中所有属性都变成响应式。代码如下:

class Observer {
    constructor(value) {
        this.value = value
        if(!Array.isArray(value) {
            this.walk(value)
        }
    }
    walk (obj) {
        const keys = Object.keys(obj)
        for(let i = 0; i < keys.length; i++) {
            definedReactive(obj, keys[i], obj[keys[i]])
        }
    }
}
function definedReactive(data, key, value) {
    if(typeof val === 'object') {
        new Observer(value)
    }
    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumberable: true,
        configurable: true,
        get: function () {
            dep.depend()
            return value
        },
        set: function (newVal) {
            if(value === newVal) {    
                return 
            }
            value = newVal   
            dep.notify()
        }
    })
}

简单理解:

3.6Object的问题

由于Object类型数据是通过setter/getter来追踪的,所以在有些语法中,即使数据改变,vue也追踪不到。
什么情况无法侦测:

  • 新增属性
  • 删除属性

解决办法通过vue提供的两个API——vm.$set和vm.$delete。后续会慢慢讲解的。

总结

只有懂得变化侦测原理,我们才能更深入的去学习vue的api,也能大大的帮助我们阅读源码。

每天去较真一个api,每天多一些进步。

    版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    作者: liu6
    原文链接:<a href='https://juejin.im/post/6844904095124291597'>https://juejin.im/post/6844904095124291597</a>
  </p>
回到顶部