首先vue创建实例过程一般如下:
js
import Vue from 'vue';//这个Vue是需要实现的构造函数
let vm = new Vue({
el:'#app',
data(){
return {
msg:'hello',
profile:{name:'zs',age:18},
arr:[{a:1},2,3]
}
},
computed:{
},
watch:{
}
});
Vue构造函数
js
// index.js
import {initState} from './observe'
function Vue(options){ // vue中原始用户传入的数据
this._init(options); // 初始化vue 并且将用户选项传入
}
Vue.prototype._init = function (options) {
// vue中初始化 this.$options 表示的是vue中参数
let vm = this;
vm.$options = options;
// 数据初始化,包含有data computed watch
initState(vm);
...
}
export default Vue
initState数据初始化函数,需要根据不同的数据进行初始化(就是做响应式和一些其他初始化)
js
// /observe/index.js
export function initState(vm){
//做不同的初始化工作
let opts = vm.$options;
if(opts.data){
initData(vm); // 初始化数据
}
if(opts.computed){
initComputed(); // 初始化计算属性
}
if(opts.watch){
initWatch(); // 初始化watch
}
}
function initData(vm){
...
}
function initComputed(){
...
}
function initWatch(){
...
}
首先开始对initData进行初始化
js
// /observe/index.js
import Observer from './observer';
function initData(vm){ // 将用户插入的数据 通过object.defineProperty重新定义
let data = vm.$options.data; // 用户传入的data
//根据data是函数形式还是对象形式进行处理,data数据挂载到vm实例的_datas属性
data = vm._data = typeof data === 'function'?data.call(vm) :data || {}
for(let key in data){
proxy(vm,'_data',key); //会将对vm上的取值操作和赋值操作代理给vm._data 属性
}
observe(data); // 观察数据,做响应式
}
export function observe(data){
if(typeof data !== 'object' || data == null) {
return // 不是对象或者是null 我就不用执行后续逻辑
}
return new Observer(data)//Observer里面实现数据劫持
}
function proxy(vm,source,key){ // 代理数据 vm.msg = vm._data.msg
Object.defineProperty(vm,key,{
get(){
return vm[source][key]
},
set(newValue){
vm[source][key] = newValue
}
})
}
这里proxy主要是将通过vm的操作代理data中的数据,data中的所有数据都会出现在vm上面,例如data中有msg属性,可以直接通过vm.msg获取,或者修改msg的值,vm.msg='world'。
Observer类的实现
observer类作用:实现数据响应式,只要将需要做响应式的数据传入即可
js
// /observe/observer.js
class Observer {
constructor(data){ // data 就是最初定义的data
this.walk(data);
}
walk(data){
Object.keys(data).forEach(key=>{
defineReactive(data,key,data[key]);
})
}
}
export default Observer
defineReactive是做数据劫持的一个方法,利用了 Object.defineProperty,对每个属性做劫持
对象数据劫持
js
// /observe/observer.js
import { observe } from "./index";
class Observer {
constructor(data){ // data 就是最初定义的data
this.walk(data);
}
walk(data){
Object.keys(data).forEach(key=>{
defineReactive(data,key,data[key]);
})
}
}
export function defineReactive(data, key, value) {
// 定义响应式的数据变化
// 如果value 依旧是一个对象的话 需要深度观察value:{school:{name:'zs',age:18}}
observe(value);
//这里的 observe(value); 是上面proxy函数的上一个observe函数,做观察数据用
Object.defineProperty(data, key, {
get() {
console.log("获取数据");
return value;
},
set(newValue) {
if (newValue === value) return;
observe(newValue); // 如果你设置的值是一个对象的话 应该在进行监控这个新增的对象
console.log("设置数据");
value = newValue;
},
});
}
export default Observer
解释上面defineReactive函数中的observe(value)调用:假如当前value是一个对象,比如{school:{name:'zs',age:18}},调用defineReactive的时候就会先进入observe,里面就会判断if(typeof data !== 'object' || data == null),很明显,这个是一个对象,所以会执行下面的return new Observer(data),然后调用walk函数,接着就是遍历school里面的每个key值,并做数据劫持defineReactive。
然后等到observe执行完成之后,就会开始执行observe下面的Object.defineProperty,这个时候是对school这个对象本身的劫持,如果发生vm.school={},赋值为一个新对象,那么就会劫持到school本身的变化了。
对于set函数中设置了新值,也需要进行observe,进行响应式处理。
数组劫持
数组劫持主要拦截对数组本身有改变的方法,例如push shift unshift pop reverse sort splice。
js
// /observe/array.js
import { observe } from ".";
//为了避免对数组的原型产生影响,需要拷贝它的原型出来,并指定为你定义的数组的原型
let oldArrayProtoMethods = Array.prototype;
// 拷贝的一个新的对象 可以查找到 老的方法
export let arrayMethods = Object.create(oldArrayProtoMethods);
let methods = [
'push',
'shift',
'pop',
'unshift',
'reverse',
'sort',
'splice'
];
export function observerArray(inserted){ // 对数组中每一项进行观测,因为数组中有些项可能是对象,例如arr:[{a:1},2,3]
for(let i = 0 ; i < inserted.length;i++){
observe(inserted[i]); //这里的observe也是上面观察数据的方法
}
}
methods.forEach(method=>{ // arr.push(1,2,3) args=[1,2,3]
arrayMethods[method] = function (...args) { //args参数是调用数组方法加入的数值,例如push(1,2,3)
let r = oldArrayProtoMethods[method].apply(this,args);
let inserted;//定义一个变量存放新增内容,这个内容如果是对象需要做响应式
//只有下面这三个方法可能存在新增内容,其他没有
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
//例如arr.splice(0,1,4)
inserted = args.slice(2); // 获取splice 新增的内容
default:
break;
}
//如果有新增,需要做观察
if(inserted) observerArray(inserted);
console.log('调用了数组更新的方法')
return r;
}
})
上面对数组方法做了拦截处理,但是还没用起来,所以需要在Observer类中做判断处理
js
// /observe/observer.js
class Observer {
constructor(data){ // data 就是最初定义的data
if (Array.isArray(data)) {
// 只能拦截数组的方法 ,数组里的每一项 还需要去观测一下
data.__proto__ = arrayMethods; //修改数组的原型,让他调用方法时找到的都是经过自己重写的方法
observerArray(data); // 观测数据中的每一项
} else {
this.walk(data);
}
}
walk(data){
Object.keys(data).forEach(key=>{
defineReactive(data,key,data[key]);
})
}
}
此后对于数组使用push,pop等改变数组的方法都是可以检测得到的,而通过数组下标修改时检测不到的。
模板编译compiler
html
<div id="app">
{{msg}}
<div>
<div>
名字 {{profile.name}}
</div>
<div>
年龄 {{profile.age}}
</div>
</div>
</div>
做完数据响应式之后,需要解析这个模板语法,例如{{msg}},将他替换成data中的数据
js
// index.js
Vue.prototype._init = function (options) {
// vue中初始化 this.$options 表示的是vue中参数
let vm = this;
vm.$options = options;
// 数据初始化,包含有data computed watch
initState(vm);
//需要判断el用户是否传入
if(vm.$options.el){
vm.$mount();//挂载组件
}
}
$mount方法实现,主要实现挂载元素和更新视图
js
// index.js
import {compiler} from './util'
Vue.prototype.$mount = function () {
let vm = this;
let el = vm.$options.el; // 获取元素 #app
el = vm.$el= query(el); // 获取当前挂载的节点 vm.$el就是要挂载的一个元素
let updateComponent = ()=>{ // 更新组件 、渲染的逻辑
vm._update(); // 更新组件
}
new Watcher(vm,updateComponent); // 渲染watcher,默认会调用updateComponent这个方法
}
function query(el){
if(typeof el === 'string'){
return document.querySelector(el);
}
return el
}
// 用用户传入的数据 去更新视图
Vue.prototype._update = function () {
let vm = this;
let el = vm.$el;
// 创建一个文档碎片,将el模板里面的数据循环获取节点
let node = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild){ // 每次拿到第一个元素就将这个元素放入到文档碎片中
node.appendChild(firstChild);//appendChid会移动这个元素,所以每次循环第一次都会变化
}
// 对文本进行替换
compiler(node,vm);
el.appendChild(node);
}
执行$mount的时候,最后会创建一个watcher,这个watcher主要是在数据进行更新后,会通知watcher执行更新操作。一开始组件创建的时候便是更新组件,而计算属性computed和监视属性watch便是他们的定义的回调函数。
compiler函数实现
js
//utils.js
//?:表示不捕获分组,+?表示惰性
const defaultRE = /\{\{((?:.|\n)+?)\}\}/g
export const util = {
getValue(vm,expr){ // [msg]
let keys = expr.split('.');
//这里主要是返回匹配到的变量的值,例如匹配到msg,需要返回vm.msg的值,
//但是这里可能存在 vm.school.name,这种访问形式,所以单纯的vm[expr]是不行的
return keys.reduce((memo,current)=>{
memo = memo[current];
return memo
},vm);
},
compilerText(node,vm){ // 编译文本 替换{{school.name}}
node.textContent = node.textContent.replace(defaultRE,function (...args) {
return util.getValue(vm,args[1].trim());//这里注意要去除空格,因为模板里面可能会是这样 {{ msg }}
});
}
}
export function compiler(node,vm){ // node 上面创建的文档碎片
let childNodes = node.childNodes;
[...childNodes].forEach(child=>{
if(child.nodeType == 1){ //1 元素 3表示文本
compiler(child,vm); // 递归,编译当前元素的孩子节点
}else if(child.nodeType == 3){
util.compilerText(child,vm);
}
});
}
watcher类
watcher作用:数据更新后会通知watcher执行相应的回调进行更新
js
// observe/watcher.js
let id = 0;
class Watcher{ // 每次产生一个watcher 都要有一个唯一的标识
/**
* @param {*} vm 当前组件的实例 new Vue
* @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
* @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
* @param {*} opts // 一些其他参数
*/
constructor(vm,exprOrFn,cb=()=>{},opts={}){
this.vm = vm;
this.exprOrFn = exprOrFn;
if(typeof exprOrFn === 'function'){
this.getter = exprOrFn; // getter就是new Watcher传入的第二个函数
}
this.cb = cb;
this.opts = opts;
this.id = id++;
this.get(); // 默认创建一个watcher 会调用自身的get方法
}
get(){
this.getter(); // 让这个当前传入的函数执行
}
}
export default Watcher
到这里已经实现数据的劫持,并在组件一开始时进行数据渲染了,但是在数据发生变化之后,视图还不会进行更新,这里就需要进行依赖收集。
收集依赖
所谓依赖收集,就是给data中每个属性在被模板访问时,比如{{msg}},此时msg就需要创建一个dep实例,这个实例得依赖数组存储那些访问了他的watcher。例如在一开始组件创建时,就会创建一个组件watcher,然后将这个watcher收集到每个属性得dep得依赖数组中。
dep类
js
// observe/dep.js
let id = 0;
class Dep{
constructor(){
this.id = id++;
this.subs = [];
}
addSub(watcher){ // 订阅
this.subs.push(watcher);
}
//dep就是通过notify通知watcher得
notify(){
this.subs.forEach(watcher=>watcher.update());
}
depend(){
if(Dep.target){ // 为了防止直接调用depend方法 先判断一下
// Dep.target是一个渲染watcher
Dep.target.addDep(this); // 在watcher中互相记忆
}
}
}
// 用来保存当前的watcher
let stack = [];
export function pushTarget(watcher){
Dep.target = watcher;
stack.push(watcher);
}
export function popTarget(){
stack.pop();
Dep.target = stack[stack.length-1];
}
export default Dep;
这里得Dep.target记录着使用了当前属性的watcher。
watcher会做如下修改
js
let id = 0;
import {pushTarget,popTarget} from './dep'
class Watcher{ // 每次产生一个watcher 都要有一个唯一的标识
/**
* @param {*} vm 当前组件的实例 new Vue
* @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
* @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
* @param {*} opts // 一些其他参数
*/
constructor(vm,exprOrFn,cb=()=>{},opts={}){
this.vm = vm;
this.exprOrFn = exprOrFn;
if(typeof exprOrFn === 'function'){
this.getter = exprOrFn; // getter就是new Watcher传入的第二个函数
}
this.cb = cb;
this.deps = [];
this.depsId = new Set()
this.opts = opts;
this.id = id++;
this.get(); // 默认创建一个watcher 会调用自身的get方法
}
get(){
pushTarget(this); // 渲染watcher Dep.target = watcher msg变化了 需要让这个watcher重新执行
// 默认创建watcher 会执行此方法
this.getter(); // 让这个当前传入的函数执行
//清除watcher
popTarget();
}
addDep(dep){ // 让watcher记录dep
let id = dep.id; // msg 的dep,每一个属性创建时都会创建一个Dep,都有唯一id
if(!this.depsId.has(id)){
this.depsId.add(id)
this.deps.push(dep);
dep.addSub(this);
}
}
update(){
this.get();
}
}
export default Watcher
defineReactive也要做出改变,就是在get时候,收集依赖watcher,而set时数据更新,需要通知wathcer调用更新方法。
js
import Dep from './dep'
export function defineReactive(data,key,value){ // 定义响应式的数据变化
observe(value); // 递归观察
// 每一个属性都new一个Dep,相同的属性用的是同一个,比如msg,就算模板用了多次也是用的同一个Dep
let dep = new Dep(); // dep里可以收集依赖 收集的是watcher 每一个属性都增加一个dep实例
Object.defineProperty(data,key,{
get(){ //只要对这个属性进行了取值操作 ,就会将当前的watcher 存入进去
if(Dep.target){ // 这次有值用的是渲染watcher
//依赖收集,必须在Dep.target有值时
dep.depend();
}
return value;
},
set(newValue){
if(newValue === value) return;
observe(newValue); // 如果你设置的值是一个对象的话 应该在进行监控这个新增的对象
console.log('设置数据')
value = newValue;
dep.notify();
}
})
}
到这里还需要修改编译函数compiler。
此时数据设置新值后的更新过程:当数据被更新之后,会调用dep.notify函数,notify里面通知各个watcher(虽然目前只有一个,就是组件的渲染watcher), watcher调用update,这个方法会调用this.getter(),这个getter是开始new Watcher时候传给watcher的回调,也就是updateComponent回调,updateComponent会调用vm._update,从而更新这个视图。
但是里面再遇到compiler编译模板时,会遇到,再匹配到一个文本节点时,本来会根据{{msg}}这种方式将{{msg}}替换成data中的值,但是此时拿到的节点是已经渲染好的值,例如msg的值是hello,此时拿到的文本就是hello,所以按照原本的替换方式会造成替换失败。需要如下修改,记录第一次的文本节点形式({{msg}})。
原本的
js
compilerText(node, vm) {
// 编译文本 替换{{school.name}}
node.textContent = node.textContent.replace(defaultRE, function (...args) {
return util.getValue(vm, args[1]);
});
},
修改后
js
compilerText(node,vm){ // 编译文本 替换{{school.name}}
if(!node.expr){ //记录{{msg}}形式,方便下次更新后做替换
node.expr = node.textContent;
}
node.textContent = node.expr.replace(defaultRE,function (...args) {
return util.getValue(vm,args[1]);
});
}
测试
html
<div id="app">
{{msg}}
<div>
<div>名字 {{profile.name}}</div>
<div>年龄 {{profile.age}}</div>
</div>
{{msg}}
</div>
渲染如下
然后更新一下msg的值
js
setTimeout(() => {
vm.msg = "world";
}, 2000);
便可再两秒后观察到模板更新
但是此时如果设置多次,会发现他会更新多次,就如下面
js
setTimeout(() => {
vm.msg = "world";
vm.msg = "yes";
}, 2000);
此时就需要用到异步批量更新
异步批量更新
异步批量更新是等到数据的赋值完成再统一执行更新视图功能
需要对watcher做出修改,因为更新就是从watcher的update方法开始的,每设置一个新值,就会调用这个update方法。
js
let id = 0;
import {pushTarget,popTarget} from './dep'
import Vue from '..';
class Watcher{ // 每次产生一个watcher 都要有一个唯一的标识
/**
* @param {*} vm 当前组件的实例 new Vue
* @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
* @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
* @param {*} opts // 一些其他参数
*/
constructor(vm,exprOrFn,cb=()=>{},opts={}){
this.vm = vm;
this.exprOrFn = exprOrFn;
if(typeof exprOrFn === 'function'){
this.getter = exprOrFn;
}
this.cb = cb;
this.deps = [];
this.depsId = new Set()
this.opts = opts;
this.id = id++;
this.get(); // 默认创建一个watcher 会调用自身的get方法
}
get(){
pushTarget(this);
this.getter();
popTarget(); // Dep.target = undefined
}
addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.depsId.add(id)
this.deps.push(dep);
dep.addSub(this);
}
}
update(){ // 如果立即调用get 会导致页面刷新 异步来更新
queueWatcher(this);
}
//添加一个run方法,由他来调用更新
run(){
this.get();
}
}
let has = {};
let queue = [];
function flushQueue(){
queue.forEach(watcher=>watcher.run());
has = {}; // 恢复正常 下一轮更新时继续使用
queue = [];
}
function queueWatcher(watcher){ // 对相同属性的重复的watcher进行过滤操作
//比如msg,多次设置msg会重复调用这个函数,但这里拿到的watcher是相同的一个
let id = watcher.id;
if(has[id] == null){
has[id] = true;
queue.push(watcher);
// 延迟清空队列
nextTick(flushQueue);// 异步方法会等待所有同步方法执行完毕后调用此方法
}
}
nextTick方法实现
js
let callbacks = [];//记录传入的会回调函数,等待一起更新
function flushCallbacks(){
callbacks.forEach(cb=>cb());
}
function nextTick(cb){ // cb就是flushQueue
callbacks.push(cb);
// 要异步刷新这个callbacks ,获取一个异步的方法
// 微任务 宏任务
// 异步是分执行顺序的 会先执行(promise mutationObserver) setImmediate setTimeout
//这里通过多重判断,主要考虑兼容性和性能问题
let timerFunc = ()=>{
flushCallbacks();
}
if(Promise){ // then方法是异步的
return Promise.resolve().then(timerFunc)
}
if(MutationObserver){
//MutationObserver可以对dom树做监听,这里是当节点的文本发生变化时,会调用传入MutationObserver中的回调,也就是timeFunc
let observe = new MutationObserver(timerFunc);
let textNode = document.createTextNode(1);
observe.observe(textNode,{characterData:true});
textNode.textContent = 2;
return
}
if(setImmediate){
return setImmediate(timerFunc)
}
setTimeout(timerFunc, 0);
}
此时对数据进行更新就不会触发多次了
watch实现
实现最开始initState中的initWatch函数。
js
export function initState(vm) {
//做不同的初始化工作
let opts = vm.$options;
if (opts.data) {
initData(vm); // 初始化数据
}
if (opts.computed) {
initComputed();
}
if (opts.watch) {
initWatch(vm); // 初始化watch,传入vm实例
}
}
js
function initWatch(vm) {
let watch = vm.$options.watch; // 获取用户传入的watch属性
//遍历每个监视属性,为每个监视属性开启一个watcher
for (let key in watch) {
// msg(newValue,oldValue){...}
let userDef = watch[key];
//这里的handler是用户定义的监视属性是对象形式里面的handler处理函数
let handler = userDef;
if (userDef.handler) {
handler = userDef.handler;
}
//immediate用于是否立即调用
createWatcher(vm, key, handler, { immediate: userDef.immediate });
}
}
function createWatcher(vm, key, handler, opts) {
// 内部最终也会使用$watch方法
return vm.$watch(key, handler, opts);
}
createWatcher函数,就是给监视属性创建一个watcher,到时候监视的属性发生了更新,watcher里面调用update
$watch函数实现
js
Vue.prototype.$watch = function (expr,handler,opts) {
let vm = this;
new Watcher(vm,expr,handler,{user:true,...opts}); // user标记时用户定义的watcher,例如watch和computed的回调
}
watcher类也需要做出相应的改变处理
js
class Watcher{
/**
* @param {*} vm 当前组件的实例 new Vue
* @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
* @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
* @param {*} opts // 一些其他参数
*/
//监视属性new watcher时传入参数例子
// vm , msg ,(newValue,oldValue)=>{} ,{user:true}
constructor(vm,exprOrFn,cb=()=>{},opts={}){
this.vm = vm;
this.exprOrFn = exprOrFn;
if(typeof exprOrFn === 'function'){
this.getter = exprOrFn;
}else {
this.getter = function () { // 如果调用此方法 会将vm上对应的exprOrFn属性取出来
return util.getValue(vm,exprOrFn)
}
}
if(opts.user){ // 标识是用户自己写的watcher
this.user = true;
}
this.cb = cb;
this.deps = [];
this.depsId = new Set()
this.opts = opts;
this.id = id++;
this.immediate = opts.immediate
// 监视属性创建watcher的时候,就会发生调用一次,并且获取一个值(老值)
this.value = this.get(); //记录老值
if(this.immediate){ // 如果有immediate 就直接运行用户定义的函数
this.cb(this.value);
}
}
get(){
pushTarget(this);
let value = this.getter(); // 让这个当前传入的函数执行
popTarget();
return value;
}
addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.depsId.add(id)
this.deps.push(dep);
dep.addSub(this);
}
}
update(){
queueWatcher(this);
}
run(){
let value = this.get(); // 这里的调用是发生在监视属性更新时调用的,此时获取的值是新值
if(this.value !== value){
this.cb(value,this.value);
}
}
}
至此,watch的功能已经实现。watch函数的流程:当初始化每一个watcher属性时,都会创建一个对应的watcher,watcher实例里面调用get()方法获取旧值,如果此时有传入immediate,就会发生调用一次cb,cb是用户定义的watch回调函数,cb的第二个参数没传,所以就是undefined。
然后当属性发生更新时,会调用run方法,此时会再次调用get方法获取最新的值,然后判断新旧值是否一致,进行调用cb回调函数,第二个参数this.value就是之前获取的旧值。
computed实现
js
let vm = new Vue({
el:document.getElementById('app')
data(){
return {
firstName:'s',
lastName:'z'
}
},
computed:{
fullName(){
return this.firstName + this.lastName
}
}
});
js
export function initState(vm) {
//做不同的初始化工作
let opts = vm.$options;
if (opts.data) {
initData(vm); // 初始化数据
}
if (opts.computed) {
initComputed(vm, opts.computed); // 初始化计算属性
}
if (opts.watch) {
initWatch(vm); // 初始化watch
}
}
initComputed函数实现
js
function initComputed(vm, computed) {
// 将计算属性的配置 放到vm上
let watchers = (vm._watchersComputed = Object.create(null)); // 创建存储计算属性的watcher的对象
for (let key in computed) {
// computed:{fullName:()=>this.firstName+this.lastName}
let userDef = computed[key];
//为每个计算属性创建一个watcher
watchers[key] = new Watcher(vm, userDef, () => {}, { lazy: true }); //lazy标识这个watcher是计算属性watcher, 计算属性watcher 默认刚开始这个方法不会执行,需要在读取到这个属性才会执行
Object.defineProperty(vm, key, {
get: createComputedGetter(vm, key),
}); // 将这个属性 定义到vm上,例如vm.fullName
}
}
当在解析模板的时候,会读取到计算属性,这个时候就会调用这里的createComputedGetter函数,这个函数经过一些列操作会返回最新的值
createComputedGetter函数
js
function createComputedGetter(vm, key) {
//之前在这个_watchersComputed上面存过计算属性的watcher了,可以直接取出来
let watcher = vm._watchersComputed[key];
// 用户取值是会执行此方法
return function () {
if (watcher) {
// 如果dirty 是false的话 不需要重新执行计算属性中的方法,直接取到缓存值
if (watcher.dirty) {
//dirty为true,标识需要重新计算最新值, 调用watcher的evaluate方法计算最新值,这个方法需要定义
watcher.evaluate();
}
//这里在后面会解释
if (Dep.target) {
// watcher 就是计算属性watcher dep = [firstName.dep,lastName.Dep]
watcher.depend();
}
return watcher.value;
}
};
}
if (Dep.target)这里的判断后面会解释到
watcher的evaluate方法和depend方法的实现
js
class Watcher{
/**
* @param {*} vm 当前组件的实例 new Vue
* @param {*} exprOrFn 用户可能传入的是一个表达式 也有可能传入的是一个函数
* @param {*} cb 用户传入的回调函数 vm.$watch('msg',cb)
* @param {*} opts // 一些其他参数
*/
// vm ()=>this.firstName+this.lastName ()=>{} lazy:true
constructor(vm,exprOrFn,cb=()=>{},opts={}){
this.vm = vm;
this.exprOrFn = exprOrFn;
if(typeof exprOrFn === 'function'){
this.getter = exprOrFn;
}else {
this.getter = function () {
return util.getValue(vm,exprOrFn)
}
}
if(opts.user){ // 标识是用户自己写的watcher
this.user = true;
}
this.lazy = opts.lazy; // 如果这个值为true 说明他是计算属性
this.dirty = this.lazy;
this.cb = cb;
this.deps = [];
this.depsId = new Set()
this.opts = opts;
this.id = id++;
this.immediate = opts.immediate
// 如果当前我们是计算属性的话 不会默认调用get方法,等到模板解析到计算属性才会调用
this.value = this.lazy? undefined : this.get(); // 默认创建一个watcher 会调用自身的get方法;
if(this.immediate){
this.cb(this.value);
}
}
get(){
pushTarget(this);
// dep = [watcher] dep =[watcher]
// fullName(){return this.firstName + this.lastName}
// 这个函数调用时就会将当前计算属性watcher 存起来
let value = this.getter.call(this.vm);
popTarget();
return value;
}
evaluate(){
this.value = this.get();
this.dirty = false; // 值求过了,标识为false,下次直接拿缓存值
}
addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.depsId.add(id)
this.deps.push(dep); // 就让watcher 记住了当前的dep
dep.addSub(this);
}
}
depend(){
let i = this.deps.length;
while(i--){
this.deps[i].depend();
}
}
update(){
if(this.lazy){ // 如果是计算属性
this.dirty = true; // 计算属性依赖的值变化了 ,标识为true,后面取值需要重新计算结果
}else {
queueWatcher(this);
}
}
run(){
let value = this.get();
if(this.value !== value){
this.cb(value,this.value);
}
}
}
computed的实现过程:首先在开始初始化计算属性时,也就是调用initComputed函数时,会为每个计算属性创建一个watcher,而在new Watcher时,什么都不会调用,因为传递的lazy是true,里面通过this.value = this.lazy? undefined : this.get(),所以不会执行get函数。
计算属性只有在读取到的时候才会调用get方法,计算他的值。
接着将每个计算属性定义到vm实例上面,通过vm.fullname1获取,并使用defineProperty做了get拦截。initState初始化完成之后,就开始执行挂载方法$mount,这个方法也会new Watcher,这个watcher称他为渲染watcher,创建watcher时,内部会执行get方法,这个时候会通过pushTarget(this)将这个渲染watcher推入数组,然后执行getter方法(开始模板解析)。
在进行模板解析时,例如遇到{{fullname}},此时就会调用这个createComputedGetter方法,然后通过fullname这个key拿到对应的watcher实例,这个watcher称他为计算属性watcher,接着调用evaluate开始计算它的值。
evaluate内部直接调用get方法,此时又会pushTarget(this),这里的this指向的是计算属性的watcher,将当前这个计算属性的watcher推入数组中,这个时候数组内部有两个watcher,分别是【渲染watcher,计算属性watcher】, 然后调用getter方法,计算fullname的结果,这个getter就是当初创建watcher传入内部的计算属性的函数,例如fullName(){return this.firstName + this.lastName}
调用这个getter函数时,内部会访问this.firstName和this.lastName,就会触发这两个变量的get拦截。这两个拦截就会将Dep.target(指向数组中的最后一个,计算属性watcher)的deps加入这个两个属性的dep实例,同样的,这个两个属性的dep实例的subs数组(用于收集watcher依赖)也会收入这个计算属性watcher。因为前面使用了相互收集依赖
形式如下:
js
firstName的依赖数组=[fullname计算属性watcher]
lastName的依赖数组=[fullname计算属性watcher]
计算属性watcher的依赖数组=[firstName的dep,lastName的dep]
求完值之后,赋值给value,调用popTarget(),将【渲染watcher,计算属性watcher】中的计算属性watcher推出数组,然后value返回,并记录在这个计算属性watcher的this.value上面,并标记disty为false,下次可以直接走缓存。至此evaluate执行完成了。
接下来就进入判断Dep.target,这个才是在修改完firstname或lastname后,触发更新视图的关键。此时Dep.target是渲染watcher,调用计算属性watcher的depend方法,这个方法内部拿到deps数组,也就是------计算属性watcher的依赖数组=[firstName的dep,lastName的dep],循环里面的每个属性的dep,并调用depend方法,将Dep.target指向的watcher推入firstname和lastname的依赖数组里面,firstname和lastname的依赖数组就会多了一个渲染watcher
js
firstName的依赖数组=[fullname计算属性watcher,渲染watcher]
lastName的依赖数组=[fullname计算属性watcher,渲染watcher]
然后在修改了计算属性fullname的任何一个依赖(例如firstname)时,就会被依赖属性的set方法拦截到,set里面调用notify方法,notify调用firstname的依赖数组中每个watcher的update进行更新,就是上面的[fullname计算属性watcher,渲染watcher],fullname计算属性watcher会计算最新值,渲染watcher负责更新视图,此时就实现了computed的功能。