vue的data属性数据劫持思路
使用入口
<body>
<!-- 'hello' + arr + 'world' -->
<div id="app" a=1 style="color:red;background:blue">
hello {
{
arr}} world
</div>
<script src="dist/vue.js"></script>
<script>
// viewModel 数据模型
// 典型的MVVM view vm model
let vm = new Vue({
data() {
//this = vm
return {
arr: {
name:'zf'}
}
}
});
vm.$mount('#app')
// 数组没有监控 索引的变化 , 但是索引对应的内容是对象类型,需要被监控 Object.freeze
// 用户很少通过索引操作数组 arr[82] = 1000 内部就想到不对索引进行拦截,因为消耗严重,内部数组不采用defineProperty
// push shift pop unshift reverse sort splice 7个方法都是变异方法,就是会更改原数组
</script>
解释:
也就是 new Vue()
function Vue(options) {
// options 为用户传入的选项
this._init(options); // 初始化操作, 组件
}
执行 _init , _init在initMixin函数定义了方法。 然后为了拿到 options,,
放在vm.$options上 执行initState(vm)
export function initMixin(Vue) {
// 表示在vue的基础上做一次混合操作
Vue.prototype._init = function(options) {
// el,data
const vm = this; // var that = this;
vm.$options = options; // 后面会对options进行扩展操作
// 对数据进行初始化 watch computed props data ...
initState(vm); // vm.$options.data 数据劫持
if(vm.$options.el){
// 将数据挂载到这个模板上
vm.$mount(vm.$options.el);
}
}
Vue.prototype.$mount = function (el) {
const vm = this;
const options = vm.$options
el = document.querySelector(el);
vm.$el = el;
// 把模板转化成 对应的渲染函数 =》 虚拟dom概念 vnode =》 diff算法 更新虚拟dom =》 产生真实节点,更新
if(!options.render){
// 没有render用template,目前没render
let template = options.template;
if(!template && el){
// 用户也没有传递template 就取el的内容作为模板
template = el.outerHTML;
let render = compileToFunction(template);
options.render = render;
}
}
// options.render 就是渲染函数
// 调用render方法 渲染成真实dom 替换掉页面的内容
mountComponent(vm,el); // 组件的挂载流程
}
}
import {
observe } from "./observer/index"; // node_resolve_plugin
import {
isFunction } from "./utils";
export function initState(vm) {
// 状态的初始化
const opts = vm.$options;
if (opts.data) {
initData(vm);
}
// if(opts.computed){
// initComputed();
// }
// if(opts.watch){
// initWatch();
// }
}
function proxy(vm,source,key){
Object.defineProperty(vm,key,{
get(){
return vm[source][key];
},
set(newValue){
vm[source][key] = newValue
}
})
}
function initData(vm) {
//
let data = vm.$options.data; // vm.$el vue 内部会对属性检测如果是以$开头 不会进行代理
// vue2中会将data中的所有数据 进行数据劫持 Object.defineProperty
// 这个时候 vm 和 data没有任何关系, 通过_data 进行关联
data = vm._data = isFunction(data) ? data.call(vm) : data;
// 用户去vm.xxx => vm._data.xxx
for(let key in data){
// vm.name = 'xxx' vm._data.name = 'xxx'
proxy(vm,'_data',key);
}
observe(data);
}
initData(vm): data做参数兼容后 放在vm._data 上 然后做代理处理,为了方便取值 比如取 name属性 不用vm._data.name , 直接可以vm.name取值,这一步的代理是为了简化书写方法。
接着执行observe(data);
操作如下 实际调用 new Observer(data)
export function observe(data) {
// 如果是对象才观测
if (!isObject(data)) {
return;
}
if(data.__ob__){
return;
}
// 默认最外层的data必须是一个对象
return new Observer(data)
}
兼容data是数组或者对象格式
如果是数组 就改写数组的__proto__, 即 data.proto = arrayMethods; 为的是7个变异方法的重写,方便有新增的内容要进行继续劫持
同时 this.observeArray(data) 是为了递归数组各项 然后递归观察 执行observe ,最好都是每项数据最终都是对象格式,执行this.walk()
new Observer(data)如果data是对象的属性 那么直接执行this.walk,
this.walk就是数据劫持了, 核心就是每一属性执行 defineReactive(data, key, data[key]);
class Observer {
constructor(data) {
// 对对象中的所有属性 进行劫持
Object.defineProperty(data,'__ob__',{
value:this,
enumerable:false // 不可枚举的
})
// data.__ob__ = this; // 所有被劫持过的属性都有__ob__
if(Array.isArray(data)){
// 数组劫持的逻辑
// 对数组原来的方法进行改写, 切片编程 高阶函数
data.__proto__ = arrayMethods;
// 如果数组中的数据是对象类型,需要监控对象的变化
this.observeArray(data);
}else{
this.walk(data); //对象劫持的逻辑
}
}
observeArray(data){
// 对我们数组的数组 和 数组中的对象再次劫持 递归了
// [{a:1},{b:2}]
data.forEach(item=>observe(item))
}
walk(data) {
// 对象
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
})
}
}
// vue2 会对对象进行遍历 将每个属性 用defineProperty 重新定义 性能差
function defineReactive(data,key,value){
// value有可能是对象
observe(value); // 本身用户默认值是对象套对象 需要递归处理 (性能差)
Object.defineProperty(data,key,{
get(){
return value
},
set(newV){
// todo... 更新视图
observe(newV); // 如果用户赋值一个新对象 ,需要将这个对象进行劫持
value = newV;
}
})
}