vue源码之手动实现vue - 4. 响应式原理
在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) }

在控制台输入 obj ,可以看到输出响应式的内容,然后点击age属性的值对应的调用了 defineProperty 方法的get请求,打印出对应的内容。
上面代码可以看出,对象结构是比较单一的类型,如果加入数组情况,发现就很难实现了,下面将代码进行优化:
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
测试截图如下:

每次调用push方法的时候都会进行打印,表明已经拦截。
下面将数组响应式化:
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封装实现
<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>
// 虚拟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' }, ] } })
在 JGVue 定义中调用 reactify 方法,在 defineReactive 方法的 set 内调用 mountComponent 实现重新渲染,这里需要传入 this 实例。

浙公网安备 33010602011771号