学无先后,达者为师

网站首页 Vue 正文

vue双向数据绑定原理学习

作者:Smile_zxx 更新时间: 2022-01-05 Vue

vue源码
1、双向数据绑定

  • 学习数组的reduce函数
var arr = [2,4,5,67,78,5,1]
// arr.reduce(函数,初始值)
// arr.reduce((上次的初始值,当前循环item项,当前的元素的索引,当前元素所属的数组对象)=>{ return XX },初始值)
const total = arr.reduce((val,item)=>{
    return val + item
}, 0)
console.log(total)  // 162
  • reduce链式获取对象属性值操作
const obj ={
    name: 'zs',
    info: {
        address: {
            location: '北京大东'
        }
    }
}
const attrstr = 'info.address.location'
// const location = attrstr.split('.') // [ 'info', 'address', 'location' ]
const location = attrstr.split('.').reduce((newObj, k) => newObj[k], obj)
console.log(location, 'location') // 北京大东 location

2 、vue发布订阅原理和实现

  • 简单实现
// 发布订阅模式简单实现


// 个简单的比喻就是发布订阅模式就像我们以前像杂志社订杂志,
// 你会告诉杂志社你需要什么杂志,并且杂志除了之后邮寄到哪里,
// 而杂志社就相当于一个发布者,当最新一期的杂志出来之后他就会先查
// 看某某杂志有哪些人订阅了,然后通知邮递员根据地址分发出去,这个根
// 据不同人订阅时填的地址分发出去的过程实际上就可以理解为订阅者的回调。


// Dep类 有一个收集者,收集订阅者的行为,当某个时间成熟了,就把收到的订阅者行为都发布出来
// (首先有个数组,专门来存放一个向数组中追加订阅信息的方法,其次,还要提供一个向数组追加订阅信息的方法,然后,还要提供一个循环,循坏出发数组中的每个订阅信息

// Watcher类 负责订阅一些事件

// 收集依赖、收集订阅者
class Dep{
    constructor() {
        // 这个subs数组,用来存放所有订阅者的信息
        this.subs = []
    }

    // 向subs数组中,添加订阅者的信息
    addSub(watcher){
        this.subs.push(watcher)
    }

    // 发布通知的方法
    notify() {
        this.subs.forEach((watcher) => watcher.update())
    }
}

// 订阅者的类
class Watcher {
    constructor(cb) {
        this.cb = cb
    }
    
    // 触发回调的方法
    update() {
        this.cb()
    }
}

// 实例化一个watcher类
const w1 = new Watcher(()=>{
    console.log('我是一个订阅者') 
})

// 实例化一个watcher类
const w2 = new Watcher(()=>{
    console.log('我是二个订阅者')
})

const dep = new Dep()  // 实例化一个Dep类
dep.addSub(w1)  // 在Dep类中添加一个watcher的实例
dep.addSub(w2)  // 在Dep类中添加一个watcher的实例

dep.notify()  // 发布
  • vue的发布订阅模式运作
    • 只要我们为vue中data数据重新赋值了,这个赋值的动作,会被vue监听到
    • 然后vue要把数据的变化,通知到每个订阅者
    • 接下来,订阅者(DOM元素)要根据最新的数据,更新自己的内容
    • 谁是订阅者
    • 为什么要订阅
    • 通俗易懂的说 就是当vue中的数据被重新赋值,变化后的一瞬间,就要调用notify通知每个订阅者(DOM元素)更新dom结构,当订阅者(DOM元素)刚被创建的时候就要添加到存放订阅者信息来

3、手动实现vue类 为vue添加getter setter

class Vue{
    constructor(options){
        this.$data = options.data

        // 调用数据劫持的方法
         Observe(this.$data)


        //  属性代理,为当前vue的实例添加属性,不用通过.$data获取到,使用户更加的方便,直接通过this.xxx可以获取到,不用通过this.$data获取到
        console.log(Object.keys(this.$data))
        Object.keys(this.$data).forEach(key =>{
            Object.defineProperty(this, key, {
                enumerable: true, // 当前属性,允许被循环
                configurable: true, // 当前属性, 运行被配置 delete
                get(){
                  return this.$data[key]
                },
                set(newValue){
                  this.$data[key] = newValue
                }
              })
        })
    }

}

// 定义一个数据劫持(获取)的方法
function Observe(obj) {
//   递归条件终止条件
if(!obj || typeof obj !=='object' ) return

  Object.keys(obj).forEach(key =>{  
    let value = obj[key]

    // 把value子节点进行递归
    Observe(value) // 深层的也需要重新改写

    Object.defineProperty(obj, key, {
    enumerable: true, // 当前属性,允许被循环
    configurable: true, // 当前属性, 运行被配置 delete
    get(){
        //我们在这里拦截到了数据
        console.log("get方法被调用");
        return value
    },
    set(newValue){
        //改变数据的值,拦截下来额
        console.log("set方法被调用", newValue);
        value = newValue
        // 重新赋值后若为对象也需要添加getter setter属性,需要再重新调用一次Observe
        Observe(value)
     }
    });
 })
}


const vm = new Vue({
    el: '#app',
    data:{
      name: 'zs',
      age: 20,
      info: {
        a: 'a1',
        c: 'c1'
       }
    }
})
// console.log(vm.$data.name)
vm.$data.info = {d:'d1', e:'e1'}
console.log(vm.$data.info)

4、 对HTML结构进行模板编译的方法

  • js性能优化,创建文档碎片,提高DOM操作性能
    ( createDocumentFragment作用是创建一个文档碎片,把要插入的新节点先附加在它上面,然后再一次性添加到document中)
class Vue{
    constructor(options){
        this.$data = options.data

        // 调用数据劫持的方法
         Observe(this.$data)


        //  属性代理,为当前vue的实例添加属性,不用通过.$data获取到,使用户更加的方便,直接通过this.xxx可以获取到,不用通过this.$data获取到
        Object.keys(this.$data).forEach(key =>{
        ......
        })

        // 调用模板编译的函数
        Complile(options.el, this)
    }

}

// 定义一个数据劫持(获取)的方法
function Observe(obj) {
  ....
}

// 对HTML结构进行模板编译的方法
function Complile(el, vm) {

    // 获取el对应的DOM元素
    vm.$el = document.querySelector(el)

    // 创建文档碎片,提高DOM操作性能
    const fragment = document.createDocumentFragment
    while(chileNode = vm.$el.firstChild){
        fragment.appendChild(chileNode)
    }

    // 进行模板编译
    replace(fragment)


    vm.$el.appendChild(fragment)

    // 负责对DOM模板进行编译的方法
    function replace(node) {
        // 定义匹配插值表达式
        const regMustache =/\{\{\s*(\S+)\s*\}\}\}/

        // 证明当前的node节点是一个文本子节点,需要进行正则的替换
        if(node.nodeType === 3){
            // 注意: 文本子节点,也是一个DOM对象,如果获取文本子节点的字符串内容,需要调用textContent属性获取
            const text = node.textContent
            // 进行字符串的正则匹配与获取
            const exectResult = regMustache.exec(text)

            if(exectResult) {
                const value = exectResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
                node.textContent = text.replace(exectResult, value)
            }

            // 终止递归的条件
            return
        }

        // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
        node.chileNodes.forEach(child => replace(child))
    }
}


5、 发布订阅实现

class Vue{
    constructor(options){
        this.$data = options.data

        // 调用数据劫持的方法
         Observe(this.$data)


        //  属性代理,为当前vue的实例添加属性,不用通过.$data获取到,使用户更加的方便,直接通过this.xxx可以获取到,不用通过this.$data获取到
        console.log(Object.keys(this.$data))
        Object.keys(this.$data).forEach(key =>{
            Object.defineProperty(this, key, {
                enumerable: true, // 当前属性,允许被循环
                configurable: true, // 当前属性, 运行被配置 delete
                get(){
                  return this.$data[key]
                },
                set(newValue){
                  this.$data[key] = newValue
                }
              })
        })

        // 调用模板编译的函数
        Complile(options.el, this)
    }

}

// 定义一个数据劫持(获取)的方法
function Observe(obj) { 

const dep = new Dep()

//   递归条件终止条件
if(!obj || typeof obj !=='object' ) return

  Object.keys(obj).forEach(key =>{  
    let value = obj[key]

    // 把value子节点进行递归
    Observe(value) // 深层的也需要重新改写

    Object.defineProperty(obj, key, {
    enumerable: true, // 当前属性,允许被循环
    configurable: true, // 当前属性, 运行被配置 delete
    get(){
        //我们在这里拦截到了数据
        console.log("get方法被调用");

        // 只要执行了下面这一行,那么刚才new的Watcher实例,就被放到了dep.subs这个数组中了
        Dep.target && dep.addSub(Dep.target)


        return value
    },
    set(newValue){
        //改变数据的值,拦截下来额
        console.log("set方法被调用", newValue);
        value = newValue
        // 重新赋值后若为对象也需要添加getter setter属性,需要再重新调用一次Observe
        Observe(value)

        // 通知每一个订阅者更新自己的文本
        dep.notify()
     }
    });
 })
}

// 对HTML结构进行模板编译的方法
function Complile(el, vm) {

    // 获取el对应的DOM元素
    vm.$el = document.querySelector(el)

    // 创建文档碎片,提高DOM操作性能
    const fragment = document.createDocumentFragment
    while(chileNode = vm.$el.firstChild){
        fragment.appendChild(chileNode)
    }

    // 进行模板编译
    replace(fragment)


    vm.$el.appendChild(fragment)

    // 负责对DOM模板进行编译的方法
    function replace(node) {
        // 定义匹配插值表达式
        const regMustache =/\{\{\s*(\S+)\s*\}\}\}/

        // 证明当前的node节点是一个文本子节点,需要进行正则的替换
        if(node.nodeType === 3){
            // 注意: 文本子节点,也是一个DOM对象,如果获取文本子节点的字符串内容,需要调用textContent属性获取
            const text = node.textContent
            // 进行字符串的正则匹配与获取
            const exectResult = regMustache.exec(text)

            if(exectResult) {
                const value = exectResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
                node.textContent = text.replace(regMustache, value)

                //  在此时,创建Watcher类实例
                new Watcher(vm , exectResult[1], (newValue) =>{
                    node.textContent = text.replace(regMustache, newValue)
                })

            }

            // 终止递归的条件
            return
        }

        // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
        node.chileNodes.forEach(child => replace(child))
    }
}


// 实现vue发布订阅

// 收集依赖、收集订阅者
class Dep{
    constructor() {
        // 这个subs数组,用来存放所有订阅者的信息
        this.subs = []
    }

    // 向subs数组中,添加订阅者的信息
    addSub(watcher){
        this.subs.push(watcher)
    }

    // 发布通知的方法
    notify() {
        this.subs.forEach((watcher) => watcher.update())
    }
}

// 订阅者的类
class Watcher {
    // cb 回调函数中,记录当前Watcher 如何更新自己的文本内容
    // 但是,只知道如果更新自己还是不行,必须拿到最新的数据
    // 因此,还需要在new Watcher期间,把vm也传递过来,因为vm中保存者最新的数据
    // 除此之外,还需要知道,在vm身上众多的数据中,哪个数据才是自己当前所需要的数据
    // 因此,必须在new Wather期间指定watcher对应数据的名字
    // cb : 回调函数
    // vm: 数据
    // key : 当前更新的key
    constructor(vm,key,cb) {
        this.vm = vm
        this.key = key
        this.cb = cb

        //  下面三行代码,负责把创建的Watcher实例存到Dep实例的subs数组中
        Dep.target = this // Dep添加一个属性target指向当前watcher的实例
        key.split('.').reduce((newObj, k) => newObj[k], vm) // 此处vm 中有取值数据,目的不是为了取值,而是为了触发getter
        Dep.target = null
    }
    
    // 触发回调的方法,能让发布者能够通知进行更新
    update() {
        const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
        this.cb(value)
    }
}


在这里插入图片描述

原文链接:https://blog.csdn.net/Smile_666666/article/details/122085842

栏目分类
最近更新