vue(2.x)基本原理---笔记

一 实现数据代理

通过Object.defineProperty()把data中的数据代理到Vue的实例上。这样就可以通过vm.xxx来获取data中的数据。

注意:数据代理并不是把data中的数据copy一份到vm上,而是在getter()中对数据进行了劫持,获取vm.xxx其实还是获取data中的数据。

class Vue {
     
    constructor(options){
     
        this.$options=options;
        this.$data=options.data;
        this.initData();
    }
    initData(){
     
        let data=this.$data;
        const keys=Object.keys(data);
        for(let i=0;i<keys.length;i++){
     
            Object.defineProperty(this,keys[i],{
     
                configurable:true,
                enumerable:true,
                get:function proxyGetter(){
     
                    return data[keys[i]]; //对数据进行了劫持
                },
                set:function proxySetter(val){
     
                    data[keys[i]]=val;
                }
            })
        }
    }
}

二 实现数据的响应式

1.利用Object.defineProperty()把data中的数据变成响应式。
2.后面可能经常会用到这个方法,所以把它封装成一个函数。
3.考虑到一个对象里边可能会再嵌套一个对象,所以要递归遍历,把每一个属性都变成响应式。

class Vue {
     
    constructor(options){
     
        this.$options=options;
        this.$data=options.data;
        this.initData();
    }
    initData(){
     
        let data=this.$data;
        const keys=Object.keys(data);
        for(let i=0;i<keys.length;i++){
     
            Object.defineProperty(this,keys[i],{
     
                configurable:true,
                enumerable:true,
                get:function proxyGetter(){
     
                    return data[keys[i]];
                },
                set:function proxySetter(val){
     
                    data[keys[i]]=val;
                }
            })
        }
        observe(data); //observe()作用:对data进行观测
    }
}
function defineReactive(obj,key,value){
     
    observe(value);   //如果被观测的值是对象,则递归
    Object.defineProperty(obj,key,{
     
        configurable:true,
        enumerable:true,
        get:function reactiveGetter(){
     
            console.log(`获取data中${
       key}的值`);
            return value;      //不能在getter()中再次获取值
        },
        set:function reactiveSetter(val){
     
            if(val!==value){
     
                console.log(`设置data中${
       key}的值`);
                value=val;    //不能在setter()中再次设置值
            }
        }
    })
}
function observe(data){
     
    var type=Object.prototype.toString.call(data);
    if(type!=='[object Object]'&&type!=='[object Array]'){
       //不是复杂类型,返回为空
        return;
    }
    new Observer(data);  //Observer类作用:把data中的数据变成响应式
}
class Observer {
     
    constructor(data){
     
        this.walk(data);
    }
    walk(data){
     
        let keys=Object.keys(data);
        for(let i=0;i<keys.length;i++){
     
            defineReactive(data,keys[i],data[keys[i]]);
        }
    }
}

三 实现Watcher

先用watch和$watch来测试对属性的监听。

1.当属性发生变化的时候,对应的回调函数肯定不能硬编码在对应的setter()中
解决方法:我们每个属性需要有一个自己的"筐",不管是使用watch初始化还是$watch来监听某个属性时,我们需要把这些回调函数添加到属性对应的"筐"中,等到属性setter触发时,从"筐"中把回调拿出来通知(notify)他们执行。

2.有可能同一个回调依赖多个属性,例如 模板或者computed
解决方法:我们可以对属性进行求值,来触发相应的getter(),在getter中让属性的"筐"去收集当前的回调。

3."筐"去哪里找当前的回调
解决方法:我们可以把当前的回调在触发getter()之前放在一个公共的地方,触发后从公共的地方移除掉。(就像一个人上舞台和下舞台的过程)

把"筐"抽象成一个Dep类的实例,把回调函数抽象成一个Watcher类的实例。

class Vue {
     
    constructor(options){
     
        this.$options=options;
        this.$data=options.data;
        this.initData();
        this.initWatch();
    }
    initData(){
     
        let data=this.$data;
        const keys=Object.keys(data);
        for(let i=0;i<keys.length;i++){
     
            Object.defineProperty(this,keys[i],{
     
                configurable:true,
                enumerable:true,
                get:function proxyGetter(){
     
                    return data[keys[i]];
                },
                set:function proxySetter(val){
     
                    data[keys[i]]=val;
                }
            })
        }
        observe(data); //observe()作用:对data进行观测
    }
    initWatch(){
     
        const watch=this.$options.watch;
        if(watch){
     
            const watches=Object.keys(watch);
            for(let i=0;i<watches.length;i++){
     
                new Watcher(this,watches[i],watch[watches[i]]);
            }
        }
    }
    $watch(exp,cb){
     
        new Watcher(this,exp,cb);
    }
}
function defineReactive(obj,key,value){
     
    observe(value);   //如果被观测的值是对象,则递归
    const dep=new Dep();  //每个属性都有自己对应的筐
    Object.defineProperty(obj,key,{
     
        configurable:true,
        enumerable:true,
        get:function reactiveGetter(){
     
            // console.log(`获取data中${key}的值`);
            if(Dep.target){
     
                dep.depend();
            }
            return value;      //不能在getter()中再次获取值
        },
        set:function reactiveSetter(val){
     
            if(val!==value){
     
                dep.notify();
                // console.log(`设置data中${key}的值`);
                value=val;    //不能在setter()中再次设置值
            }
        }
    })
}
function observe(data){
     
    var type=Object.prototype.toString.call(data);
    if(type!=='[object Object]'&&type!=='[object Array]'){
       //不是复杂类型,返回为空
        return;
    }
    new Observer(data);  //Observer类作用:把data中的数据变成响应式
}
class Observer {
     
    constructor(data){
     
        this.walk(data);
    }
    walk(data){
     
        let keys=Object.keys(data);
        for(let i=0;i<keys.length;i++){
     
            defineReactive(data,keys[i],data[keys[i]]);
        }
    }
}
class Dep {
     
    constructor(){
     
        this.sub=[];
    }
    depend(){
      //收集
        if(Dep.target){
     
            this.sub.push(Dep.target);
        }
    }
    notify(){
      //通知
        this.sub.forEach(watcher=>{
     
            watcher.run();
        })
    }
}
class Watcher {
     
    constructor(vm,exp,cb){
     
        this.vm=vm;
        this.exp=exp;
        this.cb=cb;
        this.get();
    }
    get(){
     
        Dep.target=this;
        this.vm[this.exp];
        Dep.target=null;
    }
    run(){
     
        this.cb.call(this.vm);
    }
}

注意:在实际的vue中,watcher实例的求值和调用回调函数是异步调用的,并且在上一个事件循环当中无论改变几次属性,对应的回调函数只会异步调用一次,所以继续对Watcher类及run方法进行改造

let watcherQueue=[];  //所有的watcher队列
let watcherId=0;
class Watcher {
     
    constructor(vm,exp,cb){
     
        this.vm=vm;
        this.exp=exp;
        this.cb=cb;
        this.get();
        this.id=++watcherId;//对每个watcher进行标记,没个watcher都是独一无二的
    }
    get(){
     
        Dep.target=this;
        this.vm[this.exp];
        Dep.target=null;
    }
    run(){
     
        if(watcherQueue.indexOf(this.id)!==-1){
     
            return
        }
        watcherQueue.push(this.id);
        let index=watcherQueue.length-1;  //保存新添加watcher的索引
        Promise.resolve().then(()=>{
       //函数的调用变成异步
            this.cb.call(this.vm);
            watcherQueue.splice(index,1);
        })
    }
}

至此,实现了一个基于发布订阅的Dep和Watcher,但还存在以下问题:
1.对象新增的属性无法触发回调
2.数组没有做处理,如果用Object.defineProperty对数组进行劫持,会有以下问题
a.改变数组的顺序,删除数组某一项,数组的下标就会全乱了,之前的初始化数据也全乱了
b.数组原生方法进行增删改查无法触发回调

四 实现$set()方法

在实际vue中,每个对象上都有一个__ob__属性,他就是Observer实例,并且该属性上还有一个Dep实例。
1.在创建Observer实例时,也创建一个筐,挂载Observer实例上,再把Observer实例挂载到对象的__ob__属性上。
2.触发getter()时,不光把watcher收集一份到之前的筐里,也收集一份到这个筐里。
3.在用户调用$set时,手动的触发__ob__.dep.notify()
4.在调用notify()之前,把新的属性也定义成响应式。

class Vue {
     
    $set(obj,key,value){
     
        let ob=obj.__ob__;
        defineReactive(ovj,key,value);
        ob.dep.notify();
    }
}
function defineReactive(obj,key,value){
     
    let childOb=observe(value);   //如果被观测的值是对象,则递归
    const dep=new Dep();  //每个属性都有自己对应的筐
    Object.defineProperty(obj,key,{
     
        configurable:true,
        enumerable:true,
        get:function reactiveGetter(){
     
            // console.log(`获取data中${key}的值`);
            if(Dep.target){
     
                dep.depend();
                if(childOb){
      //如果是一个复杂类型,再收集一份
                    childOb.dep.depend();
                }
            }
            return value;      //不能在getter()中再次获取值
        },
    })
}
function observe(data){
     
    var type=Object.prototype.toString.call(data);
    if(type!=='[object Object]'&&type!=='[object Array]'){
       //不是复杂类型,返回为空
        return;
    }
    if(data.__ob__){
     
        return data.__ob__;
    }
    return new Observer(data);  //Observer类作用:把data中的数据变成响应式
}
class Observer {
     
    constructor(data){
     
        this.walk(data);
        this.dep=new Dep();
        Object.defineProperty(data,'__ob__',{
     
            value:this,
            writable:true,
            enumerable:false,
            configurable:false
        })
    }
}

五 实现对数组的处理

1.数组的回调也是通过__ob__.dep来收集的,在数组调用push,pop等方法时,手动触发__ob__.dep.notify
2.在数组的原型链上插入一个自定义对象,拦截原来的push等方法,在自定义对象的同名方法中先执行原本的方法,再人为的调用__ob__.dep.notify()去执行之前收集到的回调。
3.数组里边的元素如果是对象的话,需要变成响应式
4.数组中push的元素也要变成响应式

class Observer {
     
    constructor(data) {
     
        this.dep = new Dep();
        if (Array.isArray(data)) {
      //如果是数组,用改变数组原型链的这种方式
            data.__proto__ = arrayMethods;//自定义的对象
            this.observeArray(data);//数组中若有对象,把对象中的元素变成响应式
        } else {
     
            this.walk(data);
        }
        Object.defineProperty(data, '__ob__', {
     
            value: this,
            writable: true,
            enumerable: false,
            configurable: false
        })
    }
    observeArray(arr) {
     
        for (let i = 0; i < arr.length; i++) {
     
            observe(arr[i]);
        }
    }
    walk(data) {
     
        let keys = Object.keys(data);
        for (let i = 0; i < keys.length; i++) {
     
            defineReactive(data, keys[i], data[keys[i]]);
        }
    }
}

//处理数组
const methods = ['push', 'pop', 'shift'];
//自定义的对象
const arrayMethods = Object.create(Array.prototype);//创建一个对象,这个对象.proto的值为Array.prototype
const arrayProto = Array.prototype;
methods.forEach(method => {
     
    arrayMethods[method] = function (...arg) {
     
        if (method === 'push') {
     
            this.__ob__.observeArray(args);//数组中添加的元素也变成响应式
        }
        arrayMethods[method] = function (...arg) {
     
            const result = arrayProto[method].apply(this, args);//apply接收一个数组作为参数
            this.__ob__.dep.notify();
            return result;
        }
    }
})

六 实现computed

1.是一个函数运行的结果
2.函数里用到的所有属性都会引起计算属性的变化。本质还是一个watcher.
watcher第二个参数exp也可以传一个函数,然后运行这个函数并获取返回值,运行的过程中,函数中的this.xxx都会触发getter(),这样就可以让多个筐收集到这个watcher
3.计算属性不存在data中,要单独进行初始化
4.计算属性只能取,不能存。也就是说,设置setter无效
5.计算属性本身不再需要筐去收集,对一个计算属性进行监听,回调触发的本质是计算属性依赖的其他属性发生了变化
注意:
1.计算属性是惰性的:计算属性依赖的其他属性发生变化时,计算属性不会立即重新计算,而是求值的时候,才会重新计算
2.计算属性是缓存的:如果计算属性依赖的值没有发生变化,即使重新对计算属性求值,也不会重新计算计算属性

还存在问题:

let vm = new Vue({
  data:{
    person:{
      name:'zs'
    }
  },
  watch:{
    x() { //2号watcher
      console.log('x监听');
    }
  },
  computed:{
    x() { //1号watcher
      console.log('x计算');
      return JSON.stringify(this.person)
    }
  }
})

person.ob.dep.subs中也就是person对应的筐中,应该收集到两个watcher,一个是1号watcher,一个是2号watcher
解决思路:
1.维护一个栈,有新的watcher上台时入栈,下台时出栈,台上永远是栈顶的watcher
2.watcher被dep收集时,也收集dep,相互收集。计算属性getter完成之后,检查舞台上还有没有watcher,有的话就把自己的watcher收集的dep拿出来通知,收集舞台上的watcher

class Vue {
     
    constructor(options) {
     
        this.$options = options;
        this.$data = options.data;
        this.initData();
        this.initComputed();
        this.initWatch();
    }
    initData() {
     
        let data = this.$data;
        const keys = Object.keys(data);
        for (let i = 0; i < keys.length; i++) {
     
            Object.defineProperty(this, keys[i], {
     
                configurable: true,
                enumerable: true,
                get: function proxyGetter() {
     
                    return data[keys[i]];
                },
                set: function proxySetter(val) {
     
                    data[keys[i]] = val;
                }
            })
        }
        observe(data); //observe()作用:对data进行观测
    }
    initWatch() {
     
        const watch = this.$options.watch;
        if (watch) {
     
            const watches = Object.keys(watch);
            for (let i = 0; i < watches.length; i++) {
     
                new Watcher(this, watches[i], watch[watches[i]]);
            }
        }
    }
    $watch(exp, cb) {
     
        new Watcher(this, exp, cb);
    }
    $set(obj, key, value) {
     
        let ob = obj.__ob__;
        defineReactive(ovj, key, value);
        ob.dep.notify();
    }
    initComputed(){
     
        const computed=this.$options.computed;
        if(computed){
     
            const keys=Object.keys(computed);
            for(let i=0;i<keys.length;i++){
     
                const watcher=new Watcher(this,computed[keys[i]],function(){
     },{
     lazy:true})
                Object.defineProperty(this,keys[i],{
     
                    enumerable:true,
                    configurable:true,
                    get:function computedGetter(){
     
                        if(watcher.dirty){
       //依赖的属性发生变化时,计算属性才会执行
                            watcher.get();
                            watcher.dirty=false;
                        }
                        if(Dep.target) {
     //如果舞台上还有watcher
                            watcher.deps.forEach(dep=>{
     
                                dep.depend()
                            })
                        }
                        return watcher.value;
                    },
                    set:function computedSetter(){
     
                        console.warn('请不要对computed进行赋值');
                    }
                })
            }
        }
    }
}
function defineReactive(obj, key, value) {
     
    let childOb = observe(value);   //如果被观测的值是对象,则递归
    const dep = new Dep();  //每个属性都有自己对应的筐
    Object.defineProperty(obj, key, {
     
        configurable: true,
        enumerable: true,
        get: function reactiveGetter() {
     
            // console.log(`获取data中${key}的值`);
            if (Dep.target) {
     
                dep.depend();
                if (childOb) {
      //如果是一个复杂类型,再收集一份
                    childOb.dep.depend();
                }
            }
            return value;      //不能在getter()中再次获取值
        },
        set: function reactiveSetter(val) {
     
            if (val !== value) {
     
                dep.notify();
                // console.log(`设置data中${key}的值`);
                value = val;    //不能在setter()中再次设置值
            }
        }
    })
}
function observe(data) {
     
    var type = Object.prototype.toString.call(data);
    if (type !== '[object Object]' && type !== '[object Array]') {
       //不是复杂类型,返回为空
        return;
    }
    if (data.__ob__) {
     
        return data.__ob__;
    }
    return new Observer(data);  //Observer类作用:把data中的数据变成响应式
}
class Observer {
     
    constructor(data) {
     
        this.dep = new Dep();
        if (Array.isArray(data)) {
      //如果是数组,用改变数组原型链的这种方式
            data.__proto__ = arrayMethods;//自定义的对象
            this.observeArray(data);//数组中若有对象,把对象中的元素变成响应式
        } else {
     
            this.walk(data);
        }
        Object.defineProperty(data, '__ob__', {
     
            value: this,
            writable: true,
            enumerable: false,
            configurable: false
        })
    }
    observeArray(arr) {
     
        for (let i = 0; i < arr.length; i++) {
     
            observe(arr[i]);
        }
    }
    walk(data) {
     
        let keys = Object.keys(data);
        for (let i = 0; i < keys.length; i++) {
     
            defineReactive(data, keys[i], data[keys[i]]);
        }
    }
}
class Dep {
     
    constructor() {
     
        this.sub = [];
    }
    addSub(watcher){
     
        this.sub.push(watcher);
    }
    depend() {
      //收集
        if (Dep.target) {
     
            // this.sub.push(Dep.target);
            Dep.target.addDep(this);//watcher收集dep
        }
    }
    notify() {
      //通知
        this.sub.forEach(watcher => {
     
            watcher.update();
        })
    }
}
let targetStack=[];//用栈来维护舞台上的watcher
let watcherQueue = []; //所有的watcher队列
let watcherId = 0;
class Watcher {
     
    constructor(vm, exp, cb,options={
     }) {
     
        this.dirty=this.lazy=!!options.lazy;
        this.vm = vm;
        this.exp = exp;
        this.cb = cb;
        this.deps=[];
        this.lazy || this.get();
        this.id = ++watcherId;//对每个watcher进行标记,没个watcher都是独一无二的
    }
    addDep(dep){
     
        if(this.deps.indexOf(dep)!==-1){
     
            return
        }
        this.deps.push(dep);
        dep.addSub(this);
    }
    get() {
     
        Dep.target = this;
        targetStack.push(this);
        if(typeof this.exp === 'function'){
     
            this.value=this.exp.call(this.vm);
        }else {
     
            this.value=this.vm[this.exp];
        }
        targetStack.pop();
        Dep.target=targetStack.length? targetStack[targetStack.length-1]:null;
        // Dep.target = null;
    }
    update(){
     
        if(this.lazy){
     
            this.dirty=true;
        }else {
     
            this.run();
        }
    }
    run() {
     
        if (watcherQueue.indexOf(this.id) !== -1) {
     
            return
        }
        watcherQueue.push(this.id);
        let index = watcherQueue.length - 1;  //保存新添加watcher的索引
        Promise.resolve().then(() => {
       //函数的调用变成异步
            this.get();//重新求值
            this.cb.call(this.vm);
            watcherQueue.splice(index, 1);
        })
    }
}
const methods = ['push', 'pop', 'shift'];
//自定义的对象
const arrayMethods = Object.create(Array.prototype);//创建一个对象,这个对象.proto的值为Array.prototype
const arrayProto = Array.prototype;
methods.forEach(method => {
     
    arrayMethods[method] = function (...arg) {
     
        if (method === 'push') {
     
            this.__ob__.observeArray(args);//数组中添加的元素也变成响应式
        }
        arrayMethods[method] = function (...arg) {
     
            const result = arrayProto[method].apply(this, args);//apply接收一个数组作为参数
            this.__ob__.dep.notify();
            return result;
        }
    }
})

七 实现对模板的编译

对html进行响应,最简单的方法就是创建一个watcher:

new Watcher(this,()=>{
     
	document.querySelector('#app').innerHTML=`

${ this.name}

`
},()=>{ })

这个watcher被称为render watcher
还存在一些问题:
1.用户是可以使用模板语法的,需要把模板进行一些处理,最终转化成一个执行dom更新的函数
2.直接替换所有的dom开销很大,最好按需更新dom

Vdom:js对象,描述当前dom长什么样。
作用:
a.如果视图通过vdom来描述,当数据发生改变时,将新vdom和旧vdom进行对比,找到哪里发生了变化,再去对应的dom上改变响应的元素。不用暴力刷新整个dom。操作js对象是很快的,但是操作dom元素很慢。
b.上述步骤只有最后一步更新需要依赖dom api,意味着只要能跑js的地方就可以用vdom去描述当前视图,更新时,只要调用相应平台的api就行了,实现了跨平台。
渲染函数:运行渲染函数,就会生成vdom。

Vue实例如果传入了dom或者template,首先就是要把模板字符串转化为渲染函数,这个过程叫 编译。
关于编译原理:
1.将模板字符串转化为AST(parser:解析器)
2.对AST进行静态节点标记,主要用来做虚拟dom的渲染优化(优化器)
3.使用AST生成render()函数 (codegenerate)
AST树:
{
children: [{…}],
parent: {},
tag: “div”,
type: 1, //1-元素节点 2-带变量的文本节点 3-纯文本节点,
expression:’_s(name)’, //type如果是2,则返回_s(变量)
text:’{ {name}}’ //文本节点编译前的字符串
}

生成AST:

/**
 * 
以<为标识符,代表一个开始标签或者是结束标签,如果是开始标签,代表树的层级加了一层,如果是结束标签代表层级回退了一层。同时每一层要记录它的父级元素是谁。
所以可以使用一个栈去维护当前元素到了哪一层。有开始标签则入栈,结束标签则出栈。另外,标签之间是文本节点,文本节点不对栈进行操作
实现对HTML进行parse
 */
function parser(html) {
     
    let stack = [];
    let root;
    let currentParent;
    while (html) {
     
        let ltIndex = html.indexOf('<')
        if (ltIndex > 0) {
      //前面有文本
            //type 1-元素节点  2-带变量的文本节点  3-纯文本节点
            let text = html.slice(0, ltIndex)
            // const element = {
     
            //     type: 3,
            //     text,
            //     parent:currentParent
            //   }
            const element = parseText(text)
            element.parent = currentParent
            currentParent.children.push(element)
            html = html.slice(ltIndex)
        } else if (html[ltIndex + 1] !== '/') {
      //前面没有文本,且是开始标签
            let gtIndex = html.indexOf('>')
            const element = {
     
                type: 1,
                tag: html.slice(ltIndex + 1, gtIndex), //不考虑dom的任何属性
                parent: currentParent,
                children: [],
            }

            if (!root) {
     
                root = element
            } else {
     
                currentParent.children.push(element)
            }
            stack.push(element)
            currentParent = element
            html = html.slice(gtIndex + 1)
        } else {
      //结束标签
            let gtIndex = html.indexOf('>')
            stack.pop()
            currentParent = stack[stack.length - 1]
            html = html.slice(gtIndex + 1)
        }
    }
    return root
}
/**
实现对文本节点的parse
以{
     {和}}为标识符,对把插值变量名转换成_s(name)的形式。
 */
function parseText(text) {
     
    let originText = text
    let tokens = []
    let type = 3
    while (text) {
     
        let start = text.indexOf('{
     {')
        let end = text.indexOf('}}')
        if (start !== -1 && end !== -1) {
     
            type = 2
            if (start > 0) {
     
                tokens.push(JSON.stringify(text.slice(0, start)))
            }
            let exp = text.slice(start + 2, end)
            tokens.push(`_s(${
       exp})`)
            text = text.slice(end + 2)
        } else {
     
            tokens.push(JSON.stringify(text))
            text = ''
        }
    }
    let element = {
     
        type,
        text: originText,
    }
    type === 2 ? element.expression = tokens.join('+') : ''

    return element
}

生成render()函数

/**
 *  生成AST后需要把AST再转换成渲染函数
1. 递归AST,遇到元素节点则生成如下格式的字符串_c(标签名, 属性对象, 后代数组)
2. 遇到文本节点,如果是纯文本节点,则生成如下格式的字符串_v(文本字符串)
3. 如果是带变量的文本节点,则生成如下格式的字符串_v(_s(变量名))
4. 为了让变量能正常取到,生成最后将字符串包一层with(this)
5. 最后把字符串作为函数体生成一个函数,挂载到vm.$options上
 * 
 */
function generate(ast) {
     
    const code = genElement(ast)
    return {
     
        render: `with(this){return ${
       code}}`,
    }
}
function genElement(el) {
     
    const children = genChildren(el)
    let code = `_c('${
       el.tag}',{},${
       children})`
    return code
}
function genChildren(el) {
     
    if (el.children.length) {
     
        return '[' + el.children.map(child => genNode(child)).join(',') + ']'
    }
}
function genNode(node) {
     
    if (node.type === 1) {
     
        return genElement(node)
    } else {
     
        return genText(node)
    }
}
function genText(text) {
     
    return `_v(${
       text.type === 2 ? text.expression : JSON.stringify(text.text)})`
}

八 实现Vdom

由渲染函数生成vdom。
定义一个简单的类VNode,描述一个DOM节点的相关信息,实现上一节渲染函数中未实现的_c,_v,_s函数。

_c(tag, attrs, children) {
     
        return new VNode(tag, attrs, children)
    }
_v(text) {
     
        return new VNode(null, null, null, text)
    }
_s(val) {
     
        if (val === null || val === undefined) {
     
            return ''
        } else if (typeof val === 'object') {
     
            return JSON.stringify(val)
        } else {
     
            return String(val)
        }
    }
class VNode {
     
    constructor(tag, attrs, children, text) {
     
        this.tag = tag
        this.attrs = attrs
        this.children = children
        this.text = text
    }
}

运行vm.$options.render.call(vm)即可得到当前vdom。描述了渲染后的HTML应该长什么样
首先实现一个createElm函数将Vdom转化为真正的dom,稍后更新dom会用到此函数

//首先实现一个createElm函数将Vdom转化为真正的dom,稍后更新dom会用到此函数
function createElm(vnode) {
     
    if (!vnode.tag) {
     
        const el = document.createTextNode(vnode.text)
        vnode.elm = el
        return el
    }
    const el = document.createElement(vnode.tag)
    vnode.elm = el
    vnode.children.map(createElm).forEach(childDom => {
     
        el.appendChild(childDom)
    })
    return el
}

vdom中最核心的就是patch

  //改造后
  constructor(options){
     
   if (options.el) {
     
            let html = document.querySelector(options.el).outerHTML;
            let ast = parser(html);
            let code = generate(ast).render;
            this.$options.render = new Function(code);//生成了渲染函数
            this.$mount(options.el);  //把vue实例和dom元素联系起来
        }
  }
   $mount(el) {
     
        this.$el = document.querySelector(el);//把dom元素挂载到vue实例上
        this._watcher = new Watcher(this, () => {
       //生成一个 render watcher
            this._update(this.$options.render.call(this));
        }, () => {
      })
    }
    _update(vnode) {
     
        if (this._vnode) {
     //this._vnode:旧的vdom
            patch(this._vnode, vnode);//旧的vnode 新的vnode
        } else {
     
            patch(this.$el, vnode);
        }
        this._vnode = vnode; //把旧的vdom存起来
    }
//将原先粗暴式的代码进行改造
//1.实现一个$mount函数,将原先的初始化render watcher的逻辑搬到$mount里 
//2.实现一个_update函数,该函数接受一个新的vdom,然后对比新旧vdom并更新真实dom,render watcher中不再暴力更新dom,而是调用_update函数


//接下来,实现vdom机制中最核心的patch.vue中vdom进行patch的逻辑是基于snabbdom
/**
 * 1.patch函数接受两个参数:旧的vdom和新的vdom
 * 2.当第一次挂载时旧的vdom是一个真实dom,单独处理
 * 3.后续更新时,分为如下三种情况
 *   a.新节点不存在,则删除对应的dom
 *   b.新旧节点标签不一样或文本不一样,则调用createElm生成新dom,并替换旧dom
 *   c.旧节点不存在,新节点存在,则调用createElm生成新dom,并原dom后添加新dom
 *   d.递归以上逻辑
 */

function patch(oldNode, newNode,) {
     
    const isRealElement = oldNode.nodeType  //只有真实的dom元素才会有nodeType
    //如果是对真实dom进行patch
    if (isRealElement) {
     
        let parent = oldNode.parentNode
           parent.replaceChild(createElm(newNode), oldNode) //数据和dom元素进行拼接
        return
    }
    //当前vdom对应的真实dom
    let el = oldNode.elm  //对el进行删改
    //当前vdom对应的真实父级dom
    let parent = el.parentNode  //在父级的基础上
    if (newNode) {
     
        newNode.elm = el
    }
    //开始进行对比
    if (!newNode) {
      //新节点不存在,删除
        parent.removeChild(el)
    } else if (changed(newNode, oldNode)) {
      //发生了变化,替换
        parent.replaceChild(createElm(newNode), el)
    } else if (newNode.children) {
     

        const newLength = newNode.children.length
        const oldLength = oldNode.children.length
        for (let i = 0; i < newLength || i < oldLength; i++) {
     
            if (i >= oldLength) {
      //旧节点不存在,有多余的新节点,增加
                el.appendChild(createElm(newNode.children[i]))
            } else {
      //递归
                patch(oldNode.children[i], newNode.children[i])
            }
        }
    }
}

//判断是否是相同节点
function changed(newNode, oldNode) {
     
    return (newNode.tag !== oldNode.tag || newNode.text !== oldNode.text)
}

以上就是vue的基本原理!

你可能感兴趣的:(vue基础原理)