Vue源码解析(1)-数据劫持与模板编译

823 阅读4分钟

Vue源码解析(1)

以下代码我已做了详细的注释以及思维导图图片版,对数据劫持以及模板编译两大块源码的核心部分进行手写,如需思维导图的可联系博主,不喜勿喷(前端小白)。

Vue源码解析.png

1.png

2.png

3.png

一.src入口代码

//index.js
import {initMixin} from './init'
import {lifecycleMixin} from './lifecycle'
import {renderMixin} from '../vdom'

function Vue(options){
  this._init(options)
}
initMixin(Vue)  // 只有执行了这个方法才能将_init方法挂载到Vue的原型上
lifecycleMixin(Vue)  // 你虽然在原型上挂载了方法 但是你得执行
renderMixin(Vue)  // 为了使_render方法生效

let vm = new Vue({
  el:"#app",
  data(){
    return {
      name:'zcl',
      age:23,
      teacher:[[1,2,3],{name:'zcl',age:22}],
      info:{a:123}
      
    }
  }
})
// 测试用例
console.log(vm.teacher[0].push(4))
console.log(vm.teacher[1].name = 'zzz')
console.log(vm.info.a=2)
console.log(vm)
// init.js
import {initState} from '../vue/state'
import {compileToRenderFunction} from '../compiler'
import {mountComponent} from './lifecycle'
function initMixin(Vue){
  Vue.prototype._init = function(options){ // 在Vue这个构造函数的原型上添加一个_init方法
    const vm = this   // this肯定是构造函数Vue的一个实例对象 例如vm
    vm.$options = options // 将传进来的options参数 挂载到vm.$options上
    initState(vm)  // 把整个实例都传递进去 这样想获取实例上的任何参数都比较方便

    if(vm.$options.el){
      vm.$mount(vm.$options.el)
    }
  }
  Vue.prototype.$mount = function(el){
    const vm = this,
          options = this.$options;
    el = document.querySelector(el)
    vm.$el = el
    if(!options.render){
      // 先找是否有render函数
      let template = options.template
      if(!template && el){
        // 是否有template 如果没有只能获取html
        template = el.outerHTML
      }
      const render = compileToRenderFunction(template)
      options.render = render
    }
    mountComponent(vm)  // 上树
  }
}
export {initMixin}
//lifecycle.js
import {patch} from '../vdom/patch'
function mountComponent(vm){
  vm._update(vm._render())
}
function lifecycleMixin(Vue){
  Vue.prototype._update = function(vnode){
    const vm = this;
    patch(vm.$el,vnode)  // 这里的vm.$el是原本html上的根节点
  }
}
export {lifecycleMixin,mountComponent}

二.数据劫持

//state.js
import proxyData from "./proxy"
import observe from './observe'
function initState(vm){
    var options = vm.$options
    if(options.data){
      initData(vm)
    }
}
function initData(vm){
  var data = vm.$options.data  // 获取options选项中的data
  data = vm._data = typeof data === 'function' ? data.call(vm) : data  || {} // 将data挂载到vm._data上
  // 这里因为Vue实例中的data会呈现两种形式 一种是函数和一种是对象 所以需要进行处理
  for(var key in data){
    proxyData(vm,'_data',key)  // 进行数据代理 使数据的获取方式变成简单的vm.属性的方式 
  }
  observe(vm._data)  //  进行数据劫持
}
export {initState}
//proxy.js
function proxyData(vm,target,key){
  Object.defineProperty(vm,key,{
    // 因为我们在使用defineProperty之后
    // 访问数据的时候 通过vm.age访问 是通过get方法返回数据的 
    get(){
      return vm[target][key]
    },
    set(newValue){
      vm[target][key] = newValue
    }
  })

}
export default proxyData
//observe.js
import Observer from './observer'
function observe(data){
  // 这里其实是判断是否是深层次对象 如果是就递归到底
  if(typeof data !== 'object' || data === null) return
  return new Observer(data) // 这里才是真正的数据劫持操作
}
export default observe
//observer.js
import defineReactiveData from './defineReactiveData'
import observeArr from './observeArr'
import { arrMethods } from './array'
function Observer(data){
  if(Array.isArray(data)){
    // 如果data是数组形式
    data.__proto__ = arrMethods  // 只有真正的[]形式才添加扩展的数组方法 让其在使用这些方法的时候 进行重新劫持
    observeArr(data)  // 虽然是数组形式但是里面还有可能还有数组或{} 例如[[],[],{},{}]
  }else{
    // 如果data是{}形式
    this.walk(data)
  }
}
Observer.prototype.walk = function(data){
  var keys = Object.keys(data)
  for(var i=0;i<keys.length;i++){
    var key = keys[i],
        value = data[key]
    defineReactiveData(data,key,value) // 对{}形式的数据进行劫持
  }
}
export default Observer
//defineReactiveData.js
import defineReactiveData from './defineReactiveData'
import observeArr from './observeArr'
import { arrMethods } from './array'
function Observer(data){
  if(Array.isArray(data)){
    // 如果data是数组形式
    data.__proto__ = arrMethods  // 只有真正的[]形式才添加扩展的数组方法 让其在使用这些方法的时候 进行重新劫持
    observeArr(data)  // 虽然是数组形式但是里面还有可能还有数组或{} 例如[[],[],{},{}]
  }else{
    // 如果data是{}形式
    this.walk(data)
  }
}
Observer.prototype.walk = function(data){
  var keys = Object.keys(data)
  for(var i=0;i<keys.length;i++){
    var key = keys[i],
        value = data[key]
    defineReactiveData(data,key,value) // 对{}形式的数据进行劫持
  }
}
export default Observer
//config.js
var ARR_METHODS = [
  // 以下方法会对数组的数据进行改变 我们要对新增的元素进行重新劫持
  'push','pop','shift','unshift','splice','reverse','sort'
]
export {ARR_METHODS}
// observeArr.js
import observe from "./observe"
function observeArr(data){
  for(var i=0;i<data.length;i++){
    observe(data[i])   // 我只负责遍历出来 然后让observe去判断是否需要继续去深层递归
  }
}
export default observeArr
//array.js
import {ARR_METHODS} from './config'
import observeArr from './observeArr'

var originArrMethods = Array.prototype,
    arrMethods = Object.create(originArrMethods)

ARR_METHODS.map(function(m){
  arrMethods[m] = function(){ // 扩展数组原型上的方法
    var args = originArrMethods.slice.call(arguments), // 拷贝传递进来的参数也就是数据
        rt = originArrMethods[m].apply(this,args)  // 调用原型本身的方法
    
    var newArr
    switch(m){
      case 'push':
      case 'unshift':
        newArr = args;
        break;
      case 'splice':
        newArr = args.slice(2)
        break;
      default:
        break;
    }
    newArr && observeArr(newArr)
    return rt  // 数组调用方法是时候返回的东西 返不返回都可以
  }
})
export {arrMethods}

三.template->Ast->render

// index.js
import {parseHtmlToAst} from './astParser'
import {generate} from './generate'
function compileToRenderFunction(template){
  const ast = parseHtmlToAst(template)  // template -> ast
  // ast -> render
  const code = generate(ast)  
  const render = new Function(`with(this)return{${code}}`)
  return render
}
export {compileToRenderFunction}
//astParser.js
// 匹配属性
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
// 匹配开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`)
// 匹配开始标签的结束标签
const startTagClose = /^\s*(\/?)>/
// 匹配真正的结束标签
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)

function parseHtmlToAst(html){
  let root,
      currentParent,
      text,
      stack = [];
  while(html){
    let textEnd = html.indexOf('<')  // 匹配<标签判断下面一段是标签还是文本
    if(text === 0){ // 如果是标签
      const startTagMatch = parseStartTag() // 是否能匹配到开始标签
      if(startTagMatch){
        //如果匹配到了开始标签
        start(startTagMatch.tagName,startTagMatch.attrs) // 处理这段已匹配的标签
        continue;
      }
      const endTagMatch = html.match(endTag) // 是否能匹配到真正的结束标签
      if(endTagMatch){
        advance(endTagMatch[0].length)
        end()  // 保存父子级关系
        continue;
      }
    }
    // 处理文本节点
    if(textEnd>0){
      text = html.substring(0,textEnd)
    }
    if(text){
      advance(text.length)
      chars(text)
    }
  }

  function parseStartTag(){
    const start = html.match(startTagOpen) // 匹配开始标签
    let end,
        attr;
    if(start){
      const match = {
        tagName:start[1],
        attrs:[]
      }
      advance(start[0].length)  // 删除这段已匹配的标签
      if(!(end=html.match(startTagClose)) && (attr=html.match(attribute))){
        // 如果没有匹配到开始标签的结束标签但是匹配到了属性标签 处理属性
        match.attrs.push({
          name:attr[1],
          value:attr[3] || attr[4] || attr[5]
        })
        advance(attr[0].length)
      }
      if(end){
        // 如果匹配到了开始标签的结束标签
        advance(end[0].length)
        return match
      }
    }
  }
  function advance(length){
    html = html.substring(length)
  }
  function start(tagName,attrs){
    const element = createASTElement(tagName,attrs) // 组装AST树
    if(!root){
      root = element  // 第一个节点作为根节点
    }
    currentParent = element // 保存当前节点为父亲节点
    stack.push(element)  // 保存当前节点
  }
  function end(){
    const element = stack.pop()
    currentParent = stack[stack.length-1]
    if(currentParent){
      element.parent = currentParent
      currentParent.children.push(element)
    }
  }
  function chars(text){
    text = text.trim()
    if(text.length>0){
      currentParent.children.push({
        type:3,
        text
      })
    }
  }
  function createASTElement(tagName,attrs){
    return{
      tag:tagName,
      attrs,
      children:[],
      parent,
      type:1
    }
  }
  return root;
}
export {parseHtmlToAst}
//generate.js
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;

function generate(el){
  let children = getChildren(el)
  return `_c('${el.tag}',${el.attrs.length>0?`${formatProps(el.attrs)}`:'undefined'}${children?`,${children}`:''})`
}
function getChildren(el){
  // 获取孩子并将处理后的结果进行拼接
  const children = el.children
  if(children){
    return children.map(child=>generateChild(child)).join(',')
  }
}
function generateChild(node){
  // 处理孩子
  if(node.type===1){
    // 如果是元素节点 则判断它是否还有孩子
    return generate(node)
  }else if(node.type === 3){
    // 文本节点
    let text = node.text
    if(!defaultTagRE.test(text)){
      // 纯文本
      return `_v(${JSON.stringify(text)})`
    }
    // 处理带有差值表达式的问呗
    let match,
        index,
        lastIndex = defaultTagRE.lastIndex = 0
    let textArr = []
    while(match = defaultTagRE.exec(text)){
      index = match.index // 所以为插值表达式第一个{的位置
      if(index>lastIndex){
        // 纯文本部分
        textArr.push(JSON.stringify(text.slice(lastIndex,index)))
      }
      // 插值表达式部分
      textArr.push(`_s(${match[1].trim()})`)
      lastIndex = index + match[0].length
    }
    if(lastIndex>text.length){
      // 仍有纯文本
      textArr.push(JSON.stringify(text.slice(lastIndex)))
    }
    return `_v(${textArr.join('+')})`
  }
}
function formatProps(attrs){
  // 处理属性
  let attrStr = ""
  for(var i=0;i<attrs.length;i++){
    let attr = attrs[i]
    if(attr.name === "style"){
      let styleAttrs = {}
      attr.value.split(';').map((styleAttr)=>{
        let [key,value] = styleAttr.split(':')
        styleAttrs[key] = value
      })
      attr.value  = styleAttrs
    }
    attrStr += `${attr.name}:${JSON.stringify(attr.value)},`
  }
  return `{${attrStr.slice(0,-1)}}`  // 去除多余的逗号
}
export {generate}

四.render->vnode->上树

//index.js
import {createElement,createTextVnode} from './vnode'
function renderMixin(Vue){
  Vue.prototype._c = function(){
    // 创建元素虚拟节点
    return createElement(...arguments)
  }
  Vue.prototype._v = function(text){
    // 创建文本虚拟节点
    return createTextVnode(text)
  }
  Vue.prototype._s = function(value){
    // 处理_s(name)
    if(value === null) return 
    return typeof value === 'object' ? JSON.stringify(value) : value
  }
  Vue.prototype._render = function(){
          render = this.$options.render
          vnode = render() // 生成虚拟节点
    return vnode 
  }
}
export {renderMixin}
//vnode.js
function vnode(tag,props,children,text){
  // 组装虚拟节点
  return {
    tag,props,children,text
  }
}
function createElement(tag,attrs={},...children){
  // 返回元素节点  元素节点是没有文本的
  return vnode(tag,attrs,children)
}
function createTextVnode(text){
  return vnode(undefined,undefined,undefined,text)
}
export {createElement,createTextVnode}
//patch.js
function patch(oldNode,vNode){
  let el = createElement(vNode),
      parentElement = oldNode.parentNode; // body
  parentElement.insertBefore(el,oldNode.nextSibling)
  parentElement.removeChild(oldNode)
}

function createElement(vnode){
  const {tag,props,children,text} = vnode;
  if(typeof tag === 'string'){
    // 处理标签
    vnode.el = document.createElement(tag)
    updateProps(vnode)
    children.map((child)=>{
      // 这已经是在真实DOM里面添加节点
      vnode.el.appendChild(createElement(child))
    })
  }else{
    vnode.el = document.createTextNode(text)
  }
  return vnode.el
}

function updateProps(vnode){
  const el = vnode.el,
        newProps = vnode.props || {}
  for(let key in newProps){
    if(key === 'style'){ 
      // 如果key为style 则代表是深层次对象 需要在进行遍历
      for(let sKey in newProps.style){
        el.style[sKey] = newProps.style[sKey]
      }
    }else if(key === 'class'){
      el.className = newProps[key]
    }else{
      el.setAttributes(key,newProps[key])
    }
  }
}
export {patch}