手写一个数据响应式函数

  • 目的:当数据改变时,要能监听到数组或对象上值的改变,并且进行自己的操作。
  • demo:index.js文件的简单使用为示例:
// 引入observe函数 与 Watcher 工具类(类似于vue.$Watch())
import observe from './my-reactive/observe'
import Watcher from './my-reactive/Watcher'
let obj = {
    a: {
        m: {
            n: 999
        }
    },
    b: '666',
    c:[1,2,3,4,5]
}
// 监听与调用
observe(obj)
new Watcher(obj, 'a.m.n', (val)=>{
    console.log("😔", val);
})
// 改变对象和数组
obj.c.splice(2,1,88,99)
obj.a.m.n = 10000

创建oberve侦察函数

import Observer from "./Observer"

// 创建oberve侦察函数
export default function observe(val) {
    if (typeof val !== 'object') return
    let ob
    if (typeof val.__ob__ !== 'undefined') {
        ob = val.__ob__
    } else {
        ob = new Observer(val)
    }
    return ob
}

Observer类

循环递归对象的属性 将一个正常的obj转换成为每一个层级的属性都是响应式的(都可以被侦测的)obj


import defineReactive from "./defineReactive"
import { arrayMethods } from './array'
import observe from "./observe";
import Dep from "./Dep";

function def(obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
        value,
        enumerable,
        writable: true,
        configurable: true,
    })
}

export default class Observer {
    constructor(value) {
        // 每一个Observer的实例上 都会有一个dep
        this.dep = new Dep()
        // this是Oberver类的实例,不是类本身
        def(value, '__ob__', this, false)
        // 检查是数组还是对象
        if (Array.isArray(value)) {
            Object.setPrototypeOf(value, arrayMethods)
            // 让这个数组进行observe
            this.observeArray(value)
        } else {
            this.walk(value)
        }
    }
    walk(value) {
        for (const k in value) {
            defineReactive(value, k)
        }
    }
    observeArray(arr){
        for (let i = 0; i < arr.length; i++) {
            // 逐项去observe
            observe(arr[i])
        }
    }
}

defineReactive

创建一个可以使用外部变量的闭包函数,包含了Object.defineProperties的核心功能

import observe from "./observe";
import Dep from "./Dep";
export default function defineReactive(data, key, val) {
    // 每个层级的__ob__属性上都有一个dep: Dep {}
    const dep = new Dep()
    if (arguments.length == 2) {
        val = data[key]
    }
    // 子元素要进行observe,至此形成了循环递归
    let childOb = observe(val)
    Object.defineProperty(data, key, {
        // 可被枚举
        enumerable: false,
        // 一些可配置的属性
        configurable: true,
        // getter 收集依赖
        get() {
            if (Dep.target) {
                dep.depend()
                if (childOb && childOb.length) {
                    childOb.dep.depend()
                }
            }
            return val
        },
        // setter 数据劫持
        set(newVal) {
            if (val === newVal) return
            val = newVal
            // 当设置了新值,这个值也要被observe
            childOb = observe(newVal)
            // 观察者模式 
            dep.notify()
        }
    })
}

Dep类

使用发布订阅的广播模式,用来管理依赖
dep.depend()
dep.notify()

let uid = 0
export default class Dep{
    constructor(){
        // 这个数组里放的是watcher的实例
        this.id = uid++
        this.subs = []
    }
    // 添加订阅
    addSub(sub){
        this.subs.push(sub)
    }
    // 添加依赖
    depend(){
        if (Dep.target) {
            this.addSub(Dep.target)
        }
    }
    // 通知更新
    notify(){
        console.log('dep类 notify方法');
        // 浅拷贝一份subs
        const subs = this.subs.slice()
        // 遍历
        for (let i = 0; i < subs.length; i++) {
            subs[i].update()
        }

    }
} 

watcher类

是一个中介,数据发生变化的时候通过watcher中转,通知组件更新

import Dep from "./Dep"
let uid = 0
// 闭包 函数柯里化 高阶函数
function parsePath(str) {
    let strArr = str.split('.')
    return (obj) => {
        for (let i = 0; i < strArr.length; i++) {
            if (!obj) return
            obj = obj[strArr[i]]
        }
        return obj
    }
}
export default class Watcher {
    constructor(target, expression, callback) {
        console.log('Watcher类的依赖收集器');
        this.id = uid++
        this.target = target
        this.getter = parsePath(expression)
        this.callback = callback
        this.value = this.get()
    }
    update() {
        this.run()
    }
    get() {
        // 进入依赖收集
        Dep.target = this
        const obj = this.target
        let value
        try {
            value = this.getter(obj)
        } catch (error) {
            console.log('Watcher 报错:', error);
        } finally {
            Dep.target = null
        }
        return value
    }
    run() {
        this.getAndInVoke(this.callback)
    }
    getAndInVoke(cb) {
        const value = this.get()
        if (value !== this.value || typeof value == 'object') {
            const oldValue = this.value
            this.value = value
            cb.call(this.target, value, oldValue)
        }
    }
} 

重点:ArrayMethod

  • 改写7个方法,原来的方法在Array.prototype上
  • vue当中数组的响应侦测是如何实现的?以arrayPrototype为原型,创建了arrayMethords的对象,使用了Object.setPrototypeOf()改写了七个数组的方法
function def(obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
        value,
        enumerable,
        writable: true,
        configurable: true,
    })
}
// 得到Array的原型对象
const arrayPrototype = Array.prototype

// 以arrayPrototype为原型,创建了arrayMethords的对象, 并暴露
export const arrayMethods = Object.create(arrayPrototype)

// 要改写的7个方法
const methodNeedChange = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'revert'
]

// 遍历 并把改写的方法添加到自己的原型上
methodNeedChange.forEach(item => {
    // 备份原来的方法
    const OriginalMethod = arrayPrototype[item]
    // 定义新的方法
    def(arrayMethods, item, function()  { // 这里不能用箭头函数,当前this指向的是传入的这个数组本身 [2, 3, 4, 5]
        // 恢复原来的功能
        const result =  OriginalMethod.apply(this, arguments)
        // arguments是类数组对象 需要转变为数组 才能使用数组的slice等方法
        const args = [...arguments]
        let insertArr = []
        switch (item) {
            case 'push':
            case 'unshift':
                insertArr = args
                break;
            case 'splice':
                insertArr = args.slice(2)
        }
        // 让新项也变为可响应的
        const ob = this['__ob__']
        if (insertArr.length>0) {
            ob.observeArray(insertArr)
        }
        // 让数组也有notify方法
        ob.dep.notify()
        return result

    }, false)
})
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,039评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,223评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,916评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,009评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,030评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,011评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,934评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,754评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,202评论 1 309
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,433评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,590评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,321评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,917评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,568评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,738评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,583评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,482评论 2 352

推荐阅读更多精彩内容