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>
未解决的问题:对象重新赋值为另一个对象后,就不是响应式的了