likes
comments
collection
share

自己搭建一个Vue响应式框架

作者站长头像
站长
· 阅读数 22

Vue响应式原理

一、研究Vue对象本身

创建一个文件夹(项目),初始化这个文件夹

npm init -y

执行了项目初始化,我们才能npm下载我们第三方的包。

必须保证我们项目中有package.json这个文件,我们才能在这个项目中下载第三方包。

下载vue

npm install vue@2.6.10

需要了解到一些特定:

$el:获取到当前这个vue对象根节点

$data:获取到vue中的data数据

$parent:当前这个组件的父组件

$children:当前这个组件的所有子组件

_vnode:抽象出来的虚拟dom对象

打印出来的内容

Vue
$attrs: (...)
$children: []
$createElement: ƒ (a, b, c, d)
$el: div#app
$listeners: (...)
$options: {components: {…}, directives: {…}, filters: {…}, el: "#app", _base: ƒ, …}
$parent: undefined
$refs: {}
$root: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
$scopedSlots: {}
$slots: {}
$vnode: undefined
password: (...)
username: (...)
_renderProxy: Proxy {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
_self: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
_staticTrees: null
_uid: 0
_vnode: VNode {tag: "div", data: {…}, children: Array(1), text: undefined, elm: div#app, …}
_watcher: Watcher {vm: Vue, deep: false, user: false, lazy: false, sync: false, …}
_watchers: [Watcher]
$data: (...)
$isServer: (...)
$props: (...)

二、创建vuejs文件实现数据劫持

ES5里面JS提供了一个Object.defineProperty方法

可以对你指定的对象进行一个数据劫持。

当你操作指定对象的时候,我能检测到修改的内容,

当你在页面使用我的对象属性的时候。我能检测到被使用了

ld数据劫持

<script>
        const user = {
            username:"xiaowang",
            password:123
        }
        let username = user.username
        // 好好研究一下Object这个对象里有哪些方法
        Object.defineProperty(user,"username",{
            // 监控是否使用
            get(){
                console.log("使用了user.username");
                return username
            },
            // 监控是否修改
            set(val){
                console.log("设置user.username的值");
                console.log(val);
            }
        })
        console.log(user.username)
        user.username = "xiaofeifei"
        console.log(user)

    </script>

Object.defineProperty接受三个参数

  1. 监控的对象
  2. 监控属性
  3. 执行get和set

Vue底层默认就是采用数据劫持的方式来上实现数据变化,驱动dom的变化

三、对象属性Reactive化

我们上面的代码可以实现数据劫持,但是仅仅只能针对一个属性。

<script>
        const user = {
            username: "xiaowang",
            password: 123
        }

        //  封装一个函数,这个函数负责数据劫持
        function defineReactive(data, key, value) {
            Object.defineProperty(data, key, {
                // 监控是否使用
                get() {
                    console.log(`${data}${key}被使用`);
                    return value
                },
                // 监控是否修val改
                set(val) {
                    console.log(`${data}${key}被修改`);
                    value = val
                }
            })
        }
        // 以后只要有一个属性要被接触,调用一下这个函数
        // defineReactive(user,"username",user.username)
        // console.log(user.username);
        // user.username = "xiaofeifei"
        // 根据user的key来进行循环。
        Object.keys(user).forEach(key=>{
            defineReactive(user,key,user[key])
        })

        console.log(user.password);
        console.log(user.username);


    </script>

我们需要用到一个Object.,keys来获取所有对象的key。进行遍历。

四、Vue的数据劫持

定义Observer类完成data数据劫持

在自己的vuejs文件中进行数据劫持的类定义

满足一个单一职责:一个类做一件事,一个函数实现一个业务

// Vue提供了一个Observer类来进行数据劫持
// 这个类主要就是针对我们页面Vue对象中data进行数据劫持
class Observer {
    constructor(data) {
        this.data = data
        this.walk()
    }
    // 这个方法就是针对你传递进来的data进行数据劫持
    defineReactive(data,key,value) {
        Object.defineProperty(data, key, {
            get() {
                console.log(`${data}对象的${key}属性被调用`);
                return value
            },
            set(val) {
                console.log(`${data}对象的${key}属性被赋值`);
                value = val
            }
        })
    }

    walk(){
        Object.keys(this.data).forEach(key => {
            this.defineReactive(this.data,key,this.data[key])
        });
    }
}

const user = {
    username:"xiaowang"
}
new Observer(user)
console.log(user.username)

五、Vue对象创建

我们在页面中要使用vue,需要引入vue对象,创建这个对象

const app = new Vue({
    el:"#app"
    data(){
    return{
        username:"xiaowang"
	}
}
})

接下来要定义好Vue类

class Vue{
    // 创建Vue对象的时候,传递进来的对象
    constructor(options){
        this.$options = options
        this.$data = options.data()
        this.$el = options.el
        //  针对$data这个对象里所有属性进行数据劫持
        new Observer(this.$data)
        // 针对Vue对象上面的属性进行劫持
        this.proxy()
        
    }
    // 需要将传递进来$data数据,绑定到Vue对象身上
    // this.username
    // this.$data.username
    // Vue对象身上默认会有属性,Vue里$data也会有属性
    proxy(){
        Object.keys(this.$data).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    return this.$data[key]
                },
                set(val){
                    this.$data[key] = val
                }
            })
        })
    }
}

核心思想,接受创建Vue的时候传递进来的对象。

针对$data进行数据劫持

针对 Vue身上的属性进行数据劫持

六、模板编译

将你们Vue对象中定义好的数据,渲染到页面模板上

默认Vue采用的mastache语法{{}}

// 模板编译代码
// 专门用于模板编译
class Complier{
    // 获取到Vue根节点,data
    // $el:"#app"
    constructor(el,data){
        // #app document.querySelector("#app")
        this.$el = document.querySelector(el)
        this.$data = data
        this.complier()
    }
    complier(){
        // this.$el.children.forEach(item=>{
        //     console.log(item)
        // })
        // 遍历所有的子标签,寻找子标签中间有{{}}
        [...this.$el.children].forEach(item=>{
            if(/\{\{([a-zA-Z0-9]+)\}\}/.test(item.innerHTML)){
                // RegExp.$1 代表获取到 正则表达式第一个 ()里面文本
                const key = RegExp.$1.trim()
                console.log(key);
                item.innerHTML = this.$data[key]
            }
        })
    }
}

完整代码

// Vue提供了一个Observer类来进行数据劫持
// 这个类主要就是针对我们页面Vue对象中data进行数据劫持
class Observer {
    constructor(data) {
        this.data = data
        this.walk()
    }
    // 这个方法就是针对你传递进来的data进行数据劫持
    defineReactive(data,key,value) {
        Object.defineProperty(data, key, {
            get() {
                console.log(`${data}对象的${key}属性被调用`);
                return value
            },
            set(val) {
                console.log(`${data}对象的${key}属性被赋值`);
                value = val
            }
        })
    }

    walk(){
        Object.keys(this.data).forEach(key => {
            this.defineReactive(this.data,key,this.data[key])
        });
    }
}

class Vue{
    // 创建Vue对象的时候,传递进来的对象
    constructor(options){
        this.$options = options
        this.$data = options.data()
        this.$el = options.el
        //  针对$data这个对象里所有属性进行数据劫持
        new Observer(this.$data)
        // 针对Vue对象上面的属性进行劫持
        this.proxy()
        // 实现模板编译,显示数据
        new Complier(this.$el,this.$data)

    }
    // 需要将传递进来$data数据,绑定到Vue对象身上
    // this.username
    // this.$data.username
    // Vue对象身上默认会有属性,Vue里$data也会有属性
    proxy(){
        Object.keys(this.$data).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    return this.$data[key]
                },
                set(val){
                    this.$data[key] = val
                }
            })
        })
    }
}

// 模板编译代码
// 专门用于模板编译
class Complier{
    // 获取到Vue根节点,data
    // $el:"#app"
    constructor(el,data){
        this.$el = document.querySelector(el)
        this.$data = data
        this.complier()
    }
    complier(){
        // this.$el.children.forEach(item=>{
        //     console.log(item)
        // })
        // 遍历所有的子标签,寻找子标签中间有{{}}
        [...this.$el.children].forEach(item=>{
            if(/\{\{([a-zA-Z0-9]+)\}\}/.test(item.innerHTML)){
                // RegExp.$1 代表获取到 正则表达式第一个 ()里面文本
                const key = RegExp.$1.trim()
                console.log(key);
                item.innerHTML = this.$data[key]
            }
        })
    }
}

七、发布订阅(观察者模式)

Vue底层引入了观察者模式(发布订阅)

因为我们在实际开发过程中,页面上会有很不多节点使用了data数据。

订阅者:食客就是订阅者,订阅了干拌抄手。

发布者:发布了干拌抄手的消息就会接受到通知

Vue发布订阅模式流程

自己搭建一个Vue响应式框架

草图:

自己搭建一个Vue响应式框架

完整代码

// Vue提供了一个Observer类来进行数据劫持
// 这个类主要就是针对我们页面Vue对象中data进行数据劫持
class Observer {
    constructor(data) {
        this.data = data
        this.walk()
    }
    // 这个方法就是针对你传递进来的data进行数据劫持
    defineReactive(data,key,value) {
        const dep = new Dep()
        Object.defineProperty(data, key, {
            get() {
                // 依赖收集
                // 将wathcer存放到dep对象
                // 页面console.log执行get 页面{{username}}
                if(Dep.target){
                    dep.subs.push(Dep.target)
                }
                console.log(`${data}对象的${key}属性被调用`);
                return value
            },
            set(val) {
                // 检测到页面修改的指定的属性
                // 调用dep通知所有的watcher进行页面更新
                console.log(`${data}对象的${key}属性被赋值`);
                value = val
                // 让dep来通知所有的Wathcer进行页面更新
                dep.notify()
            }
        })
    }

    walk(){
        Object.keys(this.data).forEach(key => {
            this.defineReactive(this.data,key,this.data[key])
        });
    }
}

class Vue{
    // 创建Vue对象的时候,传递进来的对象
    constructor(options){
        this.$options = options
        this.$data = options.data()
        this.$el = options.el
        //  针对$data这个对象里所有属性进行数据劫持
        new Observer(this.$data)
        // 针对Vue对象上面的属性进行劫持
        this.proxy()
        // 实现模板编译,显示数据
        new Complier(this.$el,this.$data)

    }
    // 需要将传递进来$data数据,绑定到Vue对象身上
    // this.username
    // this.$data.username
    // Vue对象身上默认会有属性,Vue里$data也会有属性
    proxy(){
        Object.keys(this.$data).forEach(key=>{
            Object.defineProperty(this,key,{
                get(){
                    return this.$data[key]
                },
                set(val){
                    this.$data[key] = val
                }
            })
        })
    }
}

// 模板编译代码
// 专门用于模板编译
class Complier{
    // 获取到Vue根节点,data
    // $el:"#app"
    constructor(el,data){
        this.$el = document.querySelector(el)
        this.$data = data
        this.complier()
    }
    complier(){
        // this.$el.children.forEach(item=>{
        //     console.log(item)
        // })
        // 遍历所有的子标签,寻找子标签中间有{{}}
        [...this.$el.children].forEach(item=>{
            if(/\{\{([a-zA-Z0-9]+)\}\}/.test(item.innerHTML)){
                // RegExp.$1 代表获取到 正则表达式第一个 ()里面文本
                const key = RegExp.$1.trim()
                console.log(key);
                // 这个代码是直接渲染到页面上。底层不是直接操作
                // render方法就是页面上渲染方法
                const render = ()=>item.innerHTML = this.$data[key]
                render()
                // 给页面的元素创建Watcher对象
                new Watcher(render)

            }
        })
    }
}
// 创建订阅者(Watcher)
class Watcher{
    // 接受render方法,完成页面渲染
    constructor(callback){
        // Dep类新增了一个静态属性,this代表当前watcher
        Dep.target = this
        this.callback = callback
        this.update()
        Dep.target = null
    }
    update(){
        this.callback()
    }
}

// 创建一个发布者
class Dep{
    constructor(){
        // 存放所有我需要管理Watcher
        this.subs = []
    }
    notify(){
        // 通知所有watcher进行页面修改
        this.subs.forEach(watcher=>{
            watcher.update()
        })
    }
}


八、抽象语法树AST

AST称为抽象语法树(Abstract Syntanx Tree)

简称:语法树

将你们源代码抽象为JavaScript对象,用对象的形式来表示我们的源代码

Vue的源代码

<div id="app">
        <p :class="{active:true}">{{username}}</p>
        <span v-bind:index="active">{{password}}</span>
        <span v-on:click="check">{{password}}</span>
    </div>

这个源代码是无法直接在浏览器里面进行加载。

自己搭建一个Vue响应式框架

Vue为了解决这个问题,将Vue模板转化为HTML代码,页面能直接识别的代码

使用抽象语法树来进行中间转换

自己搭建一个Vue响应式框架

Vue模板代码转化为抽象语法树

<div id="app">
        <p :class="{active:true}" index="1">{{username}}</p>
        <span v-bind:index="active">{{password}}</span>
        <span v-on:click="check">{{password}}</span>
    </div>

我们会将模板代码编译为字符串,innerHTML

<div id="app">
        <p :class="{active:true}">{{username}}</p>
        <span v-bind:index="active">{{password}}</span>
        <span v-on:click="check">{{password}}</span>
    </div>

将字符串解析为JavaScript对象

[
    {
        tag:"div",
        attrs:[
            {id:"app"}
        ],
        children:[
            {
                tag:"p",
                attrs:[
                    {
                      name:"class",
                      value:"active"
                    },
                    {
                      name:"index",
                      value:"1"
                    }
                ],
                children:[
                    
                ]
                type:0
            },
            {
                tag:"span",
                attrs:[
                    {
                        name:"index",
                        value:"1"
                    }
                ],
                type:1
            }
        ]
    }
]

抽象语法树:本质就是一个JavaScript对象,针对原来的属性代码进行了抽象后结果

自己搭建一个Vue响应式框架

Vue模板如何变成AST,这个过程:链表、递归等等很多算法

总结:

面试题1:请你说一下你了解Vue响应式原理?

Vue2的响应式原理,底层默认采用的数据劫持+发布订阅模式来实现的。

  1. 数据劫持使用Object.defineProperty进行data里面所有数据的接触。包括对Vue对象身上的属性接触和$data对象的属性进行劫持。
  2. 在Object.defineProperty里面会有get和set、get主要用于收集依赖(收集watcher和dep关系),set方法主要执行Dep里卖弄notify方法进行通知wacther进行页面更新
  3. Dep类属于发布者、Wacther属于订阅者,一旦Dep调用notify我们就会执行Watcher更新,更新执行render渲染

面试题2:Vue语法如何最终被浏览器识别?这个过程是什么?

Vue模板代码不好直接编译为HTML。里面包含特殊语法太多了

Vue底层默认会将Vue、template模板代码抽象为语法树AST,目的就是将Vue模板抽象为JavaScript方便我们后续解析,遍历里面每一个节点。

AST抽象语法树,使用编译函数、转化为虚拟DOM。虚拟dom里面包含变化的内容。

通过diff算法里卖弄patch函数实现页面的更新

页面渲染就是普通HTML代码

面试题3:Vue的效率相对于JS来说,谁高谁低?

根据情况来决定,Vue为了让我们开发方便,数据驱动。封装了很多底层代码。DOM操作。数据更新。封装的越多效率越低。

但是JS里对于JS的大量操作、频繁更新,这个无法进行很好优化,在这种情况下,用Vue,在这些方便提升性能

JS代码本身就很简单,没有复杂的操作。原生JS代码效率肯定比Vue更高

.env.development文件

VUE_APP_TITLE = 'woniu'
VUE_APP_BASE_URL = "/api"
VUE_APP_PORT = 8889

代码中

methods:{
    async fetchData(){
      console.log(process.env);
      axios.defaults.baseURL = process.env.VUE_APP_BASE_URL
      const res = await axios({
        url:"/usersAD/getSearch",
        method:"POST",
        data:{}
      })
      console.log(res);
    }
  }

使用环境配置代理服务器

const replacePath = "^"+process.env.VUE_APP_BASE_URL
module.exports = {
    devServer:{
        port:Number(process.env.VUE_APP_PORT),
        //配置代理服务器,webpack内置的
        proxy:{
            //当检测到路径包含了/api 进入代理服务器
            //http://127.0.0.1:4001/api/usersAD/getSearch
             [process.env.VUE_APP_BASE_URL]:{
                target:"http://127.0.0.1:4001",
                changeOrigin:true,
                //表达的意思就是将路径中/api,替换成空字符串
                pathRewrite:{
                    [replacePath]:''
                }
             },
        }
    }
}