Vue实例方法、响应式原理、自定义简易vue实现(数据$watch|set|delete、事件$on|once|off|emit、响应式原理详解、vue限制)

10 篇文章 0 订阅

目录

Vue实例方法(数据)

this.$watch(expOrFn,callback,[option])

this.$set(target,propertyName/idnex,value)

this.$delete(target,propertyName/index)

Vue实例方法(事件)

this.$on(myEvent,function(){})

this.$once(myEvent,function(){})

this.$off(event,fun)

this.$emit(eventName,arg1,...,argn)

响应式原理

响应式原理详解

Object.defineProperty函数介绍

限制

解决

不改变引用

改变引用

自定义简易Vue实现

 Watcher 

Dep

Observer

compiler

vue


Vue实例方法(数据)

this.$watch(expOrFn,callback,[option])

用于监听数据的变化,发生监听的数据发生变化后调用回调函数。

expOrFn可以是字符串(对应data中的数据名)或函数(监听return 值),callback为expOrFn的值或返回值发生变化后调用的函数(默认接收2个参数,分别是变化的新值,和变化之前的旧值)。option是可选参数为一个对象,可以设置deep或immediate属性的值为true((默认都为false)),deep是为了发现对象内部值的变化immediate是立即以表达式的当前值触发回调(此时callback中无参数即监听的值还未发生变化)。最后该方法调用完后会返回一个取消观察函数。

注意:在带有 immediate 选项时,你不能在第一次回调时取消侦听给定的 property。

例子:

// 监听路径为this.a.b.c
vm.$watch('a.b.c', function (newVal, oldVal) {
  // 做点什么
})

// 函数
vm.$watch(
  function () {
    // 表达式 `this.a + this.b` 每次得出一个不同的结果时
    // 处理函数都会被调用。
    // 这就像监听一个未被定义的计算属性
    return this.a + this.b
  },
  function (newVal, oldVal) {
    // 做点什么
  }
)
var unwatch = vm.$watch('a', cb)
// 之后取消观察
unwatch()
//在带有 immediate 选项时,如果你希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性
var unwatch = vm.$watch(
  'value',
  function () {
    doSomething()
    if (unwatch) {
      unwatch()
    }
  },
  { immediate: true }
)

this.$set(target,propertyName/idnex,value)

用于解决vue2的限制(见响应式原理中的限制)。实现通过实例方法定义属性。第一个参数target为对象或数组,第二个参数根据第一个参数为属性或序号,第三个参数为值,相当于target[propertyName/index]=value,不同在于触发dom更新。

this.$delete(target,propertyName/index)

用于解决vue2的限制(见响应式原理中的限制)。实现响应式删除属性(触发dom更新即触发监听),直接通过delete删除属性时不会触发data的监听,也就不会使dom上内容响应式变化。第一个参数t为对象或数组,第二个参数根据第一个参数为属性或序号,相当于delete target[propertyName/index,不同在于触发dom更新]。

Vue实例方法(事件)

this.$on(myEvent,function(){})

自定义事件myEvent。事件可以由 vm.$emit 触发,回调函数会接收所有传入事件触发函数的额外参数。

this.$once(myEvent,function(){})

同上,只能被触发一次

this.$off(event,fun)

移除自定义事件监听器。如果没有提供参数,则移除所有的事件监听器;如果只提供了事件,则移除该事件所有的监听器;如果同时提供了事件与回调,则只移除这个回调的监听器。

this.$emit(eventName,arg1,...,argn)

触发当前实例上的自定义事件,附加参数都会传给监听器回调。

响应式原理

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用Object.defineProperty把这些 property 全部转为getter/setter。Object.defineProperty是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。 

响应式原理详解

简述vue响应式原理_aXin_li的博客-CSDN博客

Object.defineProperty函数介绍

Object.defineProperty() - JavaScript | MDN

限制

由于 JavaScript (Object.defineProperty)的限制,Vue不能检测数组(直接去赋值)和对象(添加属性或删除已有属性)的变化。例如vue的data中有obj:{a:'a'},arr:[0,1,2],当我们通过obj.b='b'(添加属性)、delete obj.a(删除已有属性)或arr[0]=9,arr[3]=1(数组直接赋值)这种形式去赋值时,vue不会监听到变化,因此dom节点完全不会有改变。

注意:通过数组的对象上改变原生数组的方法(被vue重写了)去改变的是可以监听改变对象上的已有属性(非删除)也是可以监听的。

还有一类是直接通过vue实例定义的属性,这种属性也是无法监听的,这种属性的解决方法是只能通过vm.$set( target, propertyName/index, value )去定义。例如下面代码中的数据b:

var vm = new Vue({
  data:{
    a:1
  }
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
//解决
Vue.set(vm, 'b', 2)
//此时b是响应的

解决

对于限制中的Vue不能检测数组(直接去赋值)和对象(添加属性或删除已有属性)的变化的解决。

不改变引用

1.改变其它数据,触发vue的监听(改变其它数据时,vue会自动比较所有数据的变化,当发现对象和数组的变化时,也会触发dom更新)。 

2.对于数组使用原型上改变原生数组的方法(splice、pop、push等)。

3.使用vue提供的重新渲染方法vm.$forceUpdate()(该方法仅影响实例本身和插入插槽内容的子组件,而不是所有子组件)

4.使用vm.$set( target, propertyName/index, value )方法。

改变引用

1.对于对象使用Object.assign方法。

2.对于数组使用原型上的其它方法(slice等)赋值。

自定义简易Vue实现

实现v-model、v-text指令以v-on绑定事件和响应式触发界面更新。使用和Vue基本相同,new Vue(el:'#app',data:{}),然后创建对应html。

注意:没有加入调度器Scheduler和各种生命周期方法。

 Watcher 

页面渲染时,每个被替换的表达式对应一个watcher对象,用于当值被更新时调用update方法中的cb方法更新页面视图。

class Watcher {
    constructor(vm, key, cb) {
        this.vm = vm
        // data中的属性名称
        this.key = key
        // 回调函数负责更新视图
        this.cb = cb
        // 把watcher对象记录到Dep类的静态属性target
        Dep.target = this
        // 触发get方法,在get方法中会调用addSub被dep所记录
        this.oldValue = vm[key]
        Dep.target = null
    }
    update() {
        let newValue = this.vm[this.key]
        if (this.oldValue === newValue) {
            return
        }
        this.cb(newValue)
    }
}

Dep

用于记录依赖,通常一个属性对应一个Dep对应多个Watcher对象。当属性值更新时,调用该属性对应Dep中的所有Watcher对象的update方法更新页面视图上所有用到该属性的地方。

// 发布者-目标
class Dep {
    constructor() {
        // 记录所有的订阅者
        this.subs = []
    }
    // 添加观察者
    addSub(sub) {
        if (sub && sub.update) {
            this.subs.push(sub)
        }
    }
    // 发送通知更新
    notify() {
        this.subs.forEach(sub => {
            sub.update()
        })
    }
}

Observer

设置vue中的data属性变成响应式,并且对每个设置的属性添加一个Dep对象记录依赖。

class Observer {
    constructor(data) {
        this.walk(data)
    }
    walk(data) {
        if (!data || typeof data !== 'object') {
            return
        }
        Object.keys(data).forEach(key => {
            this.defineReactive(data, key, data[key])
        })
    }
    defineReactive(obj, key, val) {
        let that = this
        // 收集依赖,发送通知
        let dep = new Dep()
        // 如果val是的对象,将val中的属性转换为响应式。
        this.walk(val)
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: true,
            get() {
                // 收集依赖 
                Dep.target && dep.addSub(Dep.target)
                return val
            },
            set(newValue) {
                if (newValue === data[key]) {
                    return
                }
                val = newValue
                // 这里的this是obj
                that.walk(newValue)
                // 发送更新通知
                dep.notify()
            }
        })
    }
}

compiler

替换差值表达式,以及识别v-text、v-model、v-on指令,完成对应操作并对html进行渲染。

注意:渲染的时候会创建watcher来用于之后值更新触发视图更新。

class Compiler {
    constructor(vm) {
        this.el = vm.$el
        this.vm = vm
        this.compile(this.el)
    }
    // 编译模板,处理文本节点和元素节点
    compile(el) {
        let childNodes = el.childNodes
        Array.from(childNodes).forEach(node => {
            if (this.isTextNode(node)) {
                // 处理文本节点
                this.compileText(node)
            } else if (this.isElementNode(node)) {
                // 处理元素节点
                this.compileElement(node)
            }
            // 判断node节点,是否有子节点,如果有子节点,递归调用compile
            if (node.childNodes && node.childNodes.length) {
                this.compile(node)
            }
        })
    }
    // 编译元素节点,处理指令
    compileElement(node) {
        // 遍历所有的属性节点
        Array.from(node.attributes).forEach(attr => {
            let attrName = attr.name
            if (this.isDirective(attrName)) {
                attrName = attrName.substr(2)
                let key = attr.value
                if (this.isEventDirective(attrName)) {
                    // 处理v-on事件绑定
                    this.eventHandler(node, attrName, key)
                } else {
                    // 处理v-model和v-text
                    this.update(node, key, attrName)
                }
            }
        })
    }
    // 判断是否是处理事件的指令
    isEventDirective(attrName) {
        return attrName.includes('on')
    }
    eventHandler(node, attrName, fnName) {
        let eventType = attrName.substr(attrName.indexOf(':') + 1)
        let fn = this.vm.$options.methods && this.vm.$options.methods[fnName]
        fn && node.addEventListener(eventType, fn.bind(this.vm))
    }
    update(node, key, attrName) {
        let updateFn = this[attrName + ' Updater']
        updateFn && updateFn.call(this, node, this.vm[key], key)
    }
    // 处理v-text指令
    textUpdater(node, value, key) {
        node.textContent = value
        new Watcher(this.vm, key, (newValue) => {
            node.textContent = newValue
        })
    }
    // 处理v-model指令
    modelUpdater(node, value, key) {
        node.value = value
        new Watcher(this.vm, key, (newValue) => {
            node.value = newValue
        })
        // 双向绑定
        node.addEventListener('input', () => {
            this.vm[key] = node.value
        })
    }
    // 编译文本节点,处理差值表达式
    compileText(node) {
        let reg = /\{\{(.+?)\}\}/
        let value = node.textContent
        if (reg.test(value)) {
            let key = RegExp.$1.trim()
            node.textContent = value.replace(reg, this.vm[key])
            // 创建watcher对象,当数据改变更新视图
            new Watcher(this.vm, key, (newValue) => {
                node.textContent = newValue
            })
        }
    }
    // 判断元素属性是否是指令
    isDirective(attrName) {
        return attrName.startsWith('v-')
    }
    // 判断节点是否是文本节点
    isTextNode(node) {
        return node.nodeType === 3
    }
    // 判断节点是否是元素节点
    isElementNode(node) {
        return node.nodeType === 1
    }
}

vue

  • 把data的属性注入vue实例中(可以vm.属性名访问和设置vm.data下的属性)。
  • 调用observer对象,监听数据变化。
  • 调用compiler对象,解析指令和差值表达式进行页面渲染。
class Vue {
    constructor(options) {
        // 1.通过属性保存选项的数据
        this.$options = options || {}
        this.$data = options.data || {}
        this.$el = typeof options.el === 'string' ?
            document.querySelector(options.el) : options.el
        // 2.把data中的成员转换成getter和setter,注入到Vue实例中
        this._proxyData(this.$data)
        // 3.调用observer对象,监听数据的变化
        new Observer(this.$data)
        // 4.调用compiler对象,解析指令和差值表达式
        new Compiler(this)
    }
    _proxyData(data) {
        // 遍历data中的所有属性
        Object.keys(data).forEach(key => {
            // 把data的属性注入vue实例中
            Object.defineProperty(vm, key, {
                enumerable: true,
                configurable: true,
                get() {
                    return data[key]
                },
                set(newValue) {
                    if (newValue === data[key]) {
                        return
                    }
                    data[key] = newValue
                }
            })
        })
    }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值