Vue源码分析-响应式原理
除了使用递归,还可以使用队列(深度优先转化为广度优先)
一、将属性转化为响应式的
1.首先需要使用Object.defineProperty()函数对每个属性进行get和set的转化2.由于get和set之间需要一个中间变量来保存真实的值,所以将其封装在defineReactive()函数中,真正的数据存储在其局部变量value中3.如果需要进行响应式的对象有多个需要循环,并且得判断属性值是否为数组1)是数组的话需要循环数组中每个元素,并对每个元素再进行判断是否为数组2)不是数组的话就调用封装defineReactive()函数对元素进行响应式化处理,defineProperty()函数对 对象 的某个属性进行转化,set和get,如果是引用属性的话需要递归对层级属性进行响应式处理,否则的话进行后面的操作
function defineReactive(target, key, value, enumerable) {
if (typeof value === 'object' && value != null && !Array.isArray(value)) {
return reactify(value) /** 判断属性值是否为引用类型,引用类型的话需要递归*/
}
Object.defineProperty(target, key, {
enumerable: !!enumerable,
get() {
return value
},
set(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[keys[i]];
/** 如果值是数组的话,需要循环元素进行响应式化*/
if (Array.isArray(value)) {
for (let j = 0; j < value.length; j++) {
reactify(value[j])
}
} else {
defineReactive(o, key, value, true);/** 内部会判断是否为引用类型 是的话需要递归操作*/
}
}
}
二、对数组的方法进行拦截
1. 缺陷:如果使用数组的方法添加、删除....等操作是不会有响应式的操作的2.解决:对于对象可以使用递归进行响应式化,而对于数组也需要进行一些处理比如:push pop shift unshift reverse sort splice3.要做的事情:1)在改变数组数据的时候,需要发出通知,在Vue2.x中数组发送变化设置length没法通知(没有实现这个行为,在Vue 3.x中使用Proxy语法ES6的语法解决了这个问题)2)加入的元素应该变成响应式的4.技巧:如果一个函数已经定义了,但是需要扩展功能,一般的处理方法:1)使用一个临时的函数名存储函数2)重新定义原来的函数3)定义扩展的功能4)调用临时的那个函数
let ARRAY_METHOD = [
'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'
]
let array_methds = Object.create(Array.prototype);/** 继承自Array原型*/
ARRAY_METHOD.forEach(method => {
array_methds[method] = function () {/** 重写array_methds上之前定义的方法*/
/** 将方法中传进来的每个数据进行响应式化处理*/
for (let i = 0; i < arguments.length; i++) {
reactify(arguments[i])
}
/** 调用原来的方法,改变函数的调用对象,并传入方法传入的参数*/
let length = Array.prototype[method].apply(this, arguments);
return length;
}
})
三、*响应式* 结合之前 *数据驱动* 的完整代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<!--模板-->
<div id="root">
<p>{{name.firstName}}</p>
<p>{{message}}</p>
</div>
<script>
/**
* 在Vue中使用了二次提交的设计结构
* 1.在页面中的DOM和虚拟DOM是一一对应的关系
* 2.先由AST和数据生成新的VNode(render函数)
* 3.将旧的VNode和新的VNode进行比较(diff),更新(update函数)
*/
/** Vue构造函数*/
function JGVue(options) {
this._data = options.data;
let elm = document.querySelector('#root');
this._template = elm;
this._parent = elm.parentNode;
reactify(this._data, this)/** 将Vue实例传入,后面需要使用到*/
this.mount();//挂载函数
}
/** 虚拟DOM构造函数*/
class VNode {
constructor(tag, value, data, type) {
this.tag = tag && tag.toLowerCase();
this.value = value;
this.data = data;
this.children = [];
this.type = type
}
appendChild(vnode) {
this.children.push(vnode)
}
}
/** 由HTML DOM生成虚拟DOM,将这个函数当做complier函数使用,模拟抽象语法树
* 真实Vue中将真正的node作为一个字符串解析,得到一个抽象语法树
*/
function getVNode(node) {
let nodeType = node.nodeType;
let _vnode = null;
if (nodeType == 1) {
let nodeName = node.nodeName;
/**attrs返回所有属性组成的维数组
* 需要将attrs变为一个对象
*/
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, undefined, _attrObj, 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, node.nodeValue, undefined, nodeType);
}
return _vnode;
}
/**
* 将虚拟DOM转为真实DOM
*/
function parseNode(vnode) {
let _node = null
if (vnode.type == 1) {
_node = document.createElement(vnode.tag);
if (vnode.data) {
for (let item in vnode.data) {
_node.setAttribute(item, vnode.data[item])
}
}
if (vnode.children) {
for (let i = 0; i < vnode.children.length; i++) {
_node.appendChild(parseNode(vnode.children[i]));
}
}
} else if (vnode.type == 3) {
_node = document.createTextNode(vnode.value);
}
return _node;
}
function getValueByPath(obj, path) {
/** 根据路径访问对象任意层级的属性*/
let paths = path.split('.');
let res = obj;
for (let i = 0; i < paths.length; i++) {
res = res[paths[i]];
}
return res;
}
let parren = /\{\{(.+?)\}\}/g
function combine(vnode, data) {
/** 将带有{{}}的vnode与数据结合得到填充数据的vnode,模拟AST到vnode的行为*/
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(parren, function (str, path) {
return getValueByPath(data, path.trim());/** 会触发get*/
});
_vnode = new VNode(_tag, _value, _data, _type);
} else if (_type == 1) {
_vnode = new VNode(_tag, _value, _data, _type);
_children.forEach(item => {
_vnode.appendChild(combine(item, data))
})
}
return _vnode;
}
/** 1.根据Vue构造函数中的内容生成虚拟DOM*/
JGVue.prototype.mount = function () {
this.render = this.createRenderFn()
this.mountComponent();
}
/** 2.将虚拟DOM渲染到页面*/
JGVue.prototype.mountComponent = function () {
/* let mount = () => {
* this.update(this.render())//渲染到页面上
*}
* mount.call(this)//本质上应该交给watcher来调用
*/
this.update(this.render())//渲染到页面上
}
JGVue.prototype.createRenderFn = function () {
/** 生成render函数,目的是缓存AST(使用虚拟DOM模拟)*/
let ast = getVNode(this._template)
console.log(ast)
return function render() {
/**
* Vue中:将AST+data=>VNode
* 这里使用带{{}}的模板和数据来模拟=》含有数据的VNode
*/
return combine(ast, this._data)
}
}
JGVue.prototype.update = function (vnode) {
/** 将虚拟DOM渲染到页面中,diff算法(减少比较、减少操作)就在这里
* 简化 直接生成HTML DOM replaceChild到页面中
*/
let realDOM = parseNode(vnode)
this._parent.replaceChild(realDOM, document.querySelector('#root'))
/**
* 每次会将页面中的DOM全部替换,会使每次ID都是新的,需要重新获取
* Vue使用了diff算法,只会进行局部替换
*/
}
/** 响应式原理部分*/
let ARRAY_METHOD = [
'push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'
]
let array_methds = Object.create(Array.prototype);/** 继承自Array原型*/
ARRAY_METHOD.forEach(method => {
array_methds[method] = function () {/** 重写array_methds上之前定义的方法*/
/** 将方法中传进来的每个数据进行响应式化*/
for (let i = 0; i < arguments.length; i++) {
reactify(arguments[i])
}
/** 调用原来的方法,改变函数的调用对象,并传入方法传入的参数*/
let length = Array.prototype[method].apply(this, arguments);
return length;
}
})
function defineReactive(target, key, value, enumerable) {
/** 之前通过call调用,因此this就是vue实例*/
let that = this;
if (typeof value === 'object' && value != null && !Array.isArray(value)) {
return reactify(value, that)
}
Object.defineProperty(target, key, {
enumerable: !!enumerable,
get() {
console.log('get:' + value)
return value;
},
set(newVal) {
value = newVal;
console.log(that)
that.mountComponent();/** 刷新模板*/
console.log('set:' + newVal)
}
})
}
function reactify(o, vm) {
let keys = Object.keys(o);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
let value = o[keys[i]];
if (Array.isArray(value)) {
value.__proto__ = array_methds;/** 数组响应式 */
for (let j = 0; j < value.length; j++) {
reactify(value[j], vm)
}
} else {
defineReactive.call(vm, o, keys[i], o[keys[i]], true);
}
}
}
let app = new JGVue({
el: '#root',
data: {
name: {
firstName: 'luyiyi'
},
message: 'joaihia'
}
})
</script>
</body>
</html> 未解决的问题:对象重新赋值为另一个对象后,就不是响应式的了
