在Vue中,我们赋值的时候发现都是响应式的,所以我们在设计属性值的时候,也应该是响应式的。

一、概念

我们在给一个对象赋值的时候可以通过简单的 . 形式进行赋值,同时等价于使用 defineProperty

let o = {}
// 给o提供属性
o.name = '张三'
// 等价于
Object.defineProperty(o, 'age', {
    configurable: true,
    writable: true,
    enumerable: true,
    value: 19
})

而响应式,就是在赋值和取值的时候需要做的一些事情。因此,可以使用 defineProperty 自带的set,get方法来设置和获取属性值。

 

二、将对象转换成响应式

首先定义一个对象,然后通过 defineProperty

var obj = {
    name: 'jim',
    age: 19,
    gender: '男'
}
// 简化后的版本
function defineReactive(target, key, value, enumerable) {
    Object.defineProperty(target, key, {
        configurable: true,
        enumerable: !!enumerable,
        get() {
            console.log(`读取 o 的 ${ key } 属性`);
            return value
        },
        set(newVal) {
            console.log(`读取 o 的 ${ key } 属性:${newVal}`);
            value = newVal
        }
    })
}

// 将对象转换为响应式的
let keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i], obj[keys[i]], true)
}

 

elementui 开发响应式 vue响应式源码_elementui 开发响应式

 

 

obj ,可以看到输出响应式的内容,然后点击age属性的值对应的调用了 defineProperty

 

上面代码可以看出,对象结构是比较单一的类型,如果加入数组情况,发现就很难实现了,下面将代码进行优化:

let data = {
    name: '张三',
    age: 19,
    course: [{
            name: '语文'
        },
        {
            name: '数学'
        },
        {
            name: '英语'
        }
    ]
}
// 简化后的版本
function defineReactive(target, key, value, enumerable) {
    if (typeof value === 'object' && value != null && !Array.isArray(value)) {
        // 非数组的引用类型
        reactify(value)
    }
    Object.defineProperty(target, key, {
        configurable: true,
        enumerable: !!enumerable,
        get() {
            console.log(`读取 ${ key } 属性`);
            return value
        },
        set(newVal) {
            console.log(`设置 ${ key } 属性:${newVal}`);
            value = newVal
        }
    })
}

function reactify(o) {
    let keys = Object.keys(o)
    for (let i = 0; i < keys.length; i++) {
        let key = keys[i] // 属性名
        let value = o[key] // 属性值

        // 判断这个属性是不是引用类型,判断是不是数组
        // 如果引用类型需要递归, 不管是不是引用类型,都需要使用 defineReactive 将其变成响应式的
        // 如果是数组,就需要循环数组,将数据里面的元素进行响应式化
        if (Array.isArray(value)) {
            // 数组
            for (let j = 0; j < value.length; j++) {
                reactify(value[j]) // 递归
            }
        } else {
            // 对象或值类型
            defineReactive(o, key, value, true)
        }
    }
}

reactify(data)

在 defineReactive 方法中添加对数组对判断,同时封装 reactify

 

三、数组方法重写

上面我们实现了对象中数组的响应式,但是发现,如果我是给对象数组进行操作,操作后的数据不是响应式的了,这个这么办呢?都知道,数组的方法都是在原型链中,通过拦截原型链来实现数组的重写。

下面就先简单的实现方法重写:

let ARRAY_METHOD = [
    "push",
    "pop",
    "shift",
    "unshift",
    "reverse",
    "sort",
    "splice",
]

// 思路:原型式继承:修改原型链的结构
let arr = []
// 继承关系: arr -> Array.prototype -> Object.prototype ...
// 继承关系: arr -> 改写的方法 -> Array.prototype -> Object.prototype ...
let array_methods = Object.create(Array.prototype)
ARRAY_METHOD.forEach(method => {
    array_methods[method] = function () {
        // 调用原来的方法
        console.log('调用的是拦截' + method + '方法');
        let res = Array.prototype[method].apply(this, arguments)
        // Array.prototype[method].call(this, ...arguments)    // 类比,...用在真数组
        return res
    }
})

arr.__proto__ = array_methods

测试截图如下:

elementui 开发响应式 vue响应式源码_数据_02

push方法的时候都会进行打印,表明已经拦截。

 

下面将数组响应式化:

elementui 开发响应式 vue响应式源码_响应式_03

elementui 开发响应式 vue响应式源码_elementui 开发响应式_04

let data = {
    name: '张三',
    age: 19,
    course: [{
            name: '语文'
        },
        {
            name: '数学'
        },
        {
            name: '英语'
        }
    ]
}
let ARRAY_METHOD = [
    "push",
    "pop",
    "shift",
    "unshift",
    "reverse",
    "sort",
    "splice",
]
let array_methods = Object.create(Array.prototype)
ARRAY_METHOD.forEach(method => {
    array_methods[method] = function () {
        // 调用原来的方法
        console.log('调用的是拦截' + method + '方法');
        // 将数据进行响应化
        for (let i = 0; i < arguments.length; i++) {
            reactify(arguments[i])
        }

        let res = Array.prototype[method].apply(this, arguments)
        // Array.prototype[method].call(this, ...arguments)    // 类比,...用在真数组
        return res
    }
})


// 简化后的版本
function defineReactive(target, key, value, enumerable) {
    if (typeof value === 'object' && value != null && !Array.isArray(value)) {
        // 非数组的引用类型
        reactify(value)
    }
    Object.defineProperty(target, key, {
        configurable: true,
        enumerable: !!enumerable,
        get() {
            console.log(`读取 ${ key } 属性`);
            return value
        },
        set(newVal) {
            console.log(`设置 ${ key } 属性:${newVal}`);
            value = newVal
        }
    })
}

function reactify(o) {
    let keys = Object.keys(o)
    for (let i = 0; i < keys.length; i++) {
        let key = keys[i] // 属性名
        let value = o[key] // 属性值
        if (Array.isArray(value)) {
            // 数组
            value.__proto__ = array_methods // 数组响应式
            for (let j = 0; j < value.length; j++) {
                reactify(value[j]) // 递归
            }
        } else {
            // 对象或值类型
            defineReactive(o, key, value, true)
        }
    }
}

reactify(data)

折叠代码

 

四、JGVue-render封装实现

elementui 开发响应式 vue响应式源码_响应式_03

elementui 开发响应式 vue响应式源码_elementui 开发响应式_04

<div id="root" data-id="1">
    <div>
        <div>{{ name }}</div>
        <div>{{ age }}</div>
        <div>{{ gender }}</div>
        <ul>
            <li>1</li>
            <li>2</li>
            <li>3</li>
        </ul>
    </div>
</div>

Html

elementui 开发响应式 vue响应式源码_响应式_03

elementui 开发响应式 vue响应式源码_elementui 开发响应式_04

// 虚拟DOM构造函数
class VNode {
    constructor(tag, data, value, type) {
        this.tag = tag && tag.toLowerCase()
        this.data = data
        this.value = value
        this.type = type
        this.children = []
    }

    appendChild(vnode) {
        this.children.push(vnode)
    }
}
// 由 HTML DOM -> VNode : 将这个函数当做 compiler 函数
function getVNode(node) {
    let nodeType = node.nodeType
    let _vnode = null
    if (nodeType === 1) {
        // 元素
        let nodeName = node.nodeName
        let attrs = node.attributes
        let _attrObj = {}
        for (let i = 0; i < attrs.length; i++) {
            _attrObj[attrs[i].nodeName] = attrs[i].nodeValue
        }
        _vnode = new VNode(nodeName, _attrObj, undefined, nodeType)

        // 考虑 node 的子元素
        let childNodes = node.childNodes
        for (let i = 0; i < childNodes.length; i++) {
            _vnode.appendChild(getVNode(childNodes[i])) // 递归
        }

    } else if (nodeType === 3) {
        // 文本
        _vnode = new VNode(undefined, undefined, node.nodeValue, nodeType)
    }
    return _vnode
}

// 将 vnode 转换为真正的 DOM
function parseVNode(vnode) {
    // 创建真实DOM
    let type = vnode.type
    if (type === 3) {
        return document.createTextNode(vnode.value) // 创建文本节点
    } else if (type === 1) {
        let _node = document.createElement(vnode.tag)

        // 属性
        let data = vnode.data
        Object.keys(data).forEach(key => {
            let attrName = key
            let attrValue = data[key]
            _node.setAttribute(attrName, attrValue)
        })

        // 子元素
        let children = vnode.children
        children.forEach(subvnode => {
            _node.appendChild(parseVNode(subvnode)); // 递归转换子元素 ( 虚拟DOM )

        })

        return _node
    }
}

function JGVue(options) {
    this._data = options.data
    let elm = document.querySelector(options.el)
    this._template = elm
    this._parent = elm.parentNode
    reactify(this._data, this /* 将Vue实例传入 折中处理 */ )
    this.mount() // 挂载
}

// 根据路径访问对象成员
function getValueByPath(obj, path) {
    let paths = path.split('.')

    let res = obj;
    let prop;
    while (prop = paths.shift()) {
        res = res[prop]
    }
    return res
}

let rkuohao = /\{\{(.+?)\}\}/g
// 将带有坑的Vnode与数据data结合,得到填充数据的VNode
function combine(vnode, data) {
    let _type = vnode.type
    let _data = vnode.data
    let _value = vnode.value
    let _tag = vnode.tag
    let _children = vnode.children

    let _vnode = null
    if (_type === 3) {
        // 文本
        // 对文本处理
        _value = _value.replace(rkuohao, (_, g) => {
            return getValueByPath(data, g.trim())
        })
        _vnode = new VNode(_tag, _data, _value, _type)
    } else if (_type === 1) {
        // 元素
        _vnode = new VNode(_tag, _data, _value, _type)
        _children.forEach(_subvnode => _vnode.appendChild(combine(_subvnode, data)));
    }

    return _vnode
}

JGVue.prototype.mount = function () {
    // 需要提供一个 reder 方法:生成虚拟DOM
    this.render = this.createRederFn()
    this.mountComponent()
}

JGVue.prototype.mountComponent = function () {

    // 执行 mountComponent 函数
    let mount = () => {
        this.update(this.render())
    }

    mount.call(this) // 本质上应该交给 watcher 来调用
}

// 在真正的Vue中,使用了二次提交的设计结构 
// 1. 在页面中的DOM和虚拟DOM是一一对应关系
// 2. 先有 AST 和数据生成 VNode(新,reder)
// 3. 将旧VNode和新VNode比较( diff ) ,更新 ( update )

// 这里是生成reder函数, 目的是缓存抽象语法树( 我们使用虚拟DOM来模拟)
JGVue.prototype.createRederFn = function () {
    let ast = getVNode(this._template)
    // Vue : 将AST + data => VNode
    // 带有坑的 VNode + data => 含有数据的VNode

    return function render() {
        let _tmp = combine(ast, this._data)
        return _tmp
    }
}

// 将虚拟DOM渲染到页面中:diff算法就在这里
JGVue.prototype.update = function (vnode) {

    // this._parent.replaceChild()
    let realDOM = parseVNode(vnode)
    // this._parent.replaceChild(realDOM, this._template)
    this._parent.replaceChild(realDOM, document.querySelector("#root"))
}

// 响应式化
let data = {
    name: '张三',
    age: 19,
    course: [{
            name: '语文'
        },
        {
            name: '数学'
        },
        {
            name: '英语'
        }
    ]
}
let ARRAY_METHOD = [
    "push",
    "pop",
    "shift",
    "unshift",
    "reverse",
    "sort",
    "splice",
]
let array_methods = Object.create(Array.prototype)
ARRAY_METHOD.forEach(method => {
    array_methods[method] = function () {
        // 调用原来的方法
        console.log('调用的是拦截' + method + '方法');
        // 将数据进行响应化
        for (let i = 0; i < arguments.length; i++) {
            reactify(arguments[i])
        }

        let res = Array.prototype[method].apply(this, arguments)
        // Array.prototype[method].call(this, ...arguments)    // 类比,...用在真数组
        return res
    }
})


// 简化后的版本
function defineReactive(target, key, value, enumerable) {
    // 折中处理后,this。就是vue实例
    let that = this
    if (typeof value === 'object' && value != null && !Array.isArray(value)) {
        // 非数组的引用类型
        reactify(value)
    }
    Object.defineProperty(target, key, {
        configurable: true,
        enumerable: !!enumerable,
        get() {
            console.log(`读取 ${ key } 属性`);
            return value
        },
        set(newVal) {
            console.log(`设置 ${ key } 属性:${newVal}`);

            value = newVal

            // 模版刷新(这里是假的,只做展示)
            that.mountComponent()
        }
    })
}

function reactify(o, vm) {
    let keys = Object.keys(o)
    for (let i = 0; i < keys.length; i++) {
        let key = keys[i] // 属性名
        let value = o[key] // 属性值
        if (Array.isArray(value)) {
            // 数组
            value.__proto__ = array_methods // 数组响应式
            for (let j = 0; j < value.length; j++) {
                reactify(value[j], vm) // 递归
            }
        } else {
            // 对象或值类型
            defineReactive.call(vm, o, key, value, true)
        }
    }
}

let app = new JGVue({
    el: '#root',
    data: {
        name: '张三',
        age: '19',
        gender: "男",
        datas: [{
                info: '好难1'
            },
            {
                info: '好难2'
            },
            {
                info: '好难3'
            },
        ]
    }
})

Script

在 JGVue 定义中调用 reactify 方法,在 defineReactive 方法的 set 内调用 mountComponent 实现重新渲染,这里需要传入 this 实例。